feat(setup-npm-registry): browser-assisted classic PAT creation
When no token is found via --token/env/gh CLI, the script now opens github.com/settings/tokens/new pre-scoped to read:packages with clear instructions on Note/Expiration/Scopes, falling back to printing the URL when a browser can't be launched (SSH, containers, CI). Add --no-open to skip the launch attempt outright. Also document the flow in README (new 'Generating a GitHub token' section) and QUICKSTART troubleshooting, and add a hermetic test covering the --no-open + piped-token path.
This commit is contained in:
@@ -6,9 +6,9 @@
|
||||
* GitHub Packages is a private, org-scoped npm registry — plain `npm install`
|
||||
* does not know about it until the consuming scope is mapped to it, with a
|
||||
* token that has at least `read:packages`. This script gets that token
|
||||
* (flag > env > `gh auth token` > interactive masked prompt) and writes the
|
||||
* two required lines into an .npmrc, without ever printing the token in full
|
||||
* or storing it anywhere else.
|
||||
* (flag > env > `gh auth token` > browser-assisted classic-PAT creation +
|
||||
* paste) and writes the two required lines into an .npmrc, without ever
|
||||
* printing the token in full or storing it anywhere else.
|
||||
*
|
||||
* Usage:
|
||||
* node scripts/setup-npm-registry.mjs [options]
|
||||
@@ -20,6 +20,7 @@
|
||||
* --npmrc=<path> .npmrc to edit (default: ~/.npmrc)
|
||||
* --local edit ./.npmrc in the current directory instead
|
||||
* --yes never prompt; fail if no token can be found non-interactively
|
||||
* --no-open don't try to open the token creation page in a browser
|
||||
* --dry-run print the plan, write nothing
|
||||
* --skip-verify don't call the GitHub/npm APIs to validate the token
|
||||
* --unset remove this scope's entries instead of adding them
|
||||
@@ -63,6 +64,7 @@ function usage() {
|
||||
--npmrc=<path> .npmrc to edit (default: ~/.npmrc)
|
||||
--local edit ./.npmrc in the current directory instead
|
||||
--yes never prompt; fail if no token can be found non-interactively
|
||||
--no-open don't try to open the token creation page in a browser
|
||||
--dry-run print the plan, write nothing
|
||||
--skip-verify don't call GitHub/npm to validate the token
|
||||
--unset remove this scope's entries instead of adding them
|
||||
@@ -140,7 +142,33 @@ function promptHidden(question) {
|
||||
});
|
||||
}
|
||||
|
||||
async function discoverToken({ explicit, yes }) {
|
||||
// GitHub Packages' npm registry only accepts classic personal access tokens
|
||||
// (fine-grained PATs don't reliably carry package scopes yet). This builds
|
||||
// the pre-filled "New personal access token (classic)" page: the scopes
|
||||
// query param pre-ticks the checkboxes so the user doesn't have to hunt for
|
||||
// them in a long list of ~40 scopes.
|
||||
function classicTokenUrl({ scopes, description }) {
|
||||
const url = new URL('https://github.com/settings/tokens/new');
|
||||
url.searchParams.set('scopes', scopes.join(','));
|
||||
url.searchParams.set('description', description);
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
// Best-effort browser launch. Never throws — if there's no display (SSH,
|
||||
// container, CI) or no matching opener, the caller falls back to printing
|
||||
// the URL for the user to open by hand.
|
||||
function openBrowser(url) {
|
||||
const platform = os.platform();
|
||||
const [cmd, args] = platform === 'darwin'
|
||||
? ['open', [url]]
|
||||
: platform === 'win32'
|
||||
? ['cmd', ['/c', 'start', '""', url]]
|
||||
: ['xdg-open', [url]];
|
||||
const result = run(cmd, args, { stdio: 'ignore' });
|
||||
return result.code === 0;
|
||||
}
|
||||
|
||||
async function discoverToken({ explicit, yes, noOpen }) {
|
||||
if (explicit) return { token: explicit, source: '--token' };
|
||||
|
||||
const envToken = tokenFromEnv();
|
||||
@@ -151,11 +179,31 @@ async function discoverToken({ explicit, yes }) {
|
||||
|
||||
if (yes) return { token: null, source: null };
|
||||
|
||||
const tokenUrl = classicTokenUrl({ scopes: ['read:packages'], description: 'npm-registry (GitHub Packages)' });
|
||||
|
||||
out(dim('No token found via flag, environment, or `gh auth token`.'));
|
||||
out(dim('Create one with at least the "read:packages" scope:'));
|
||||
out(dim(' https://github.com/settings/tokens/new?scopes=read:packages&description=npm-registry'));
|
||||
const token = await promptHidden('Paste a GitHub token (input hidden): ');
|
||||
return { token: token || null, source: 'interactive prompt' };
|
||||
step('Create a GitHub personal access token (classic)');
|
||||
out(`GitHub Packages' npm registry only works with a ${bold('classic')} PAT — fine-grained`);
|
||||
out('tokens don\'t reliably support package scopes yet. On the page that opens (or the URL');
|
||||
out('below), set:');
|
||||
out(` ${bold('Note')} anything memorable, e.g. "npm-registry"`);
|
||||
out(` ${bold('Expiration')} your choice (90 days is a reasonable default)`);
|
||||
out(` ${bold('Scopes')} check ${bold('read:packages')} (required, to install)`);
|
||||
out(` also check ${bold('write:packages')} if you also need to publish`);
|
||||
out('Then click "Generate token" and copy it (starts with `ghp_`) — GitHub only shows it once.');
|
||||
out();
|
||||
const opened = !noOpen && process.stdin.isTTY && openBrowser(tokenUrl);
|
||||
if (opened) {
|
||||
ok('Opened the token creation page in your browser.');
|
||||
} else if (noOpen) {
|
||||
out(dim('(--no-open) skipping automatic browser launch — open this URL by hand:'));
|
||||
} else {
|
||||
warn('Could not open a browser automatically (no display, SSH session, or CI). Open this URL by hand:');
|
||||
}
|
||||
out(` ${tokenUrl}`);
|
||||
out();
|
||||
const token = await promptHidden('Paste the generated token here (input hidden): ');
|
||||
return { token: token || null, source: 'interactive prompt (classic PAT page)' };
|
||||
}
|
||||
|
||||
// ---- token verification -------------------------------------------------
|
||||
@@ -243,7 +291,7 @@ async function main() {
|
||||
}
|
||||
|
||||
step('1. token');
|
||||
const { token, source } = await discoverToken({ explicit: flags.token, yes });
|
||||
const { token, source } = await discoverToken({ explicit: flags.token, yes, noOpen: flags['no-open'] });
|
||||
if (!token) {
|
||||
fail('no token available and none provided (run without --yes to be prompted, or pass --token)');
|
||||
process.exit(1);
|
||||
|
||||
Reference in New Issue
Block a user