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:
@@ -74,13 +74,36 @@ needs one extra step. `scripts/setup-npm-registry.mjs` (zero dependencies) does
|
||||
|
||||
```bash
|
||||
git clone git@github.com:redsentech/lean-harness.git && cd lean-harness && npm install
|
||||
node scripts/setup-npm-registry.mjs # finds a token (flag > env > `gh auth token` > prompt),
|
||||
# verifies it, writes ~/.npmrc, confirms npm can reach
|
||||
# the registry — never prints the token in full
|
||||
node scripts/setup-npm-registry.mjs # finds a token (flag > env > `gh auth token` > browser-
|
||||
# assisted classic-PAT creation + paste), verifies it,
|
||||
# writes ~/.npmrc, confirms npm can reach the registry —
|
||||
# never prints the token in full
|
||||
npm install -g @redsentech/lean-harness # or: npx @redsentech/lean-harness <command>
|
||||
lh doctor # verify the environment
|
||||
```
|
||||
|
||||
#### Generating a GitHub token
|
||||
|
||||
If no token is found via `--token`, `NPM_REGISTRY_TOKEN`/`GITHUB_TOKEN`/`GH_TOKEN`, or
|
||||
`gh auth token`, the script walks you through creating one — no manual scope-hunting required:
|
||||
|
||||
1. It opens `github.com/settings/tokens/new` in your default browser, pre-filled with the
|
||||
`read:packages` scope and a memorable description (falls back to printing the URL if it
|
||||
can't launch a browser — e.g. over SSH, in a container, or in CI).
|
||||
2. On that page, set:
|
||||
- **Note** — anything memorable, e.g. `npm-registry`
|
||||
- **Expiration** — your choice (90 days is a reasonable default)
|
||||
- **Scopes** — `read:packages` is pre-checked (required to install); also check
|
||||
`write:packages` if you need to publish
|
||||
3. Click **Generate token**, copy it (starts with `ghp_`) — GitHub only shows it once.
|
||||
4. Paste it back into the terminal prompt (input is hidden).
|
||||
|
||||
Only **classic** PATs work reliably with GitHub Packages — fine-grained tokens don't yet
|
||||
support package scopes, so the script always links to the classic token page.
|
||||
|
||||
Pass `--no-open` to skip the automatic browser launch and just print the URL (useful in
|
||||
headless/SSH sessions where there's no display to open a browser on).
|
||||
|
||||
Equivalent by hand, if you already have a token with `read:packages`:
|
||||
|
||||
```bash
|
||||
|
||||
+2
-1
@@ -220,7 +220,8 @@ Everything the harness does is self-documenting — nothing lives only in a chat
|
||||
| `lh` not found after onboarding | Re-run without `--no-npm-link`, or invoke via the absolute path the script prints |
|
||||
| Agents/skills installed via `copilot plugin install` but `lh init`/`lh doctor` fail with "command not found" | Expected — plugin install only adds the behaviour layer. Run `npm link` from a clone (path C) or `scripts/onboard.mjs` (path B) to get `lh` on `PATH` |
|
||||
| `copilot plugin install owner/repo` prints a deprecation warning | Expected for direct-source installs. Use `copilot plugin marketplace add` + `copilot plugin install name@marketplace` instead (path A) |
|
||||
| `npm install -g @redsentech/lean-harness` gives `404`/`403` | `.npmrc` isn't pointed at GitHub Packages, or the token lacks `read:packages`. Run `node scripts/setup-npm-registry.mjs` |
|
||||
| `npm install -g @redsentech/lean-harness` gives `404`/`403` | `.npmrc` isn't pointed at GitHub Packages, or the token lacks `read:packages`. Run `node scripts/setup-npm-registry.mjs` — see [Generating a GitHub token](../README.md#generating-a-github-token) if you don't have one yet |
|
||||
| `setup-npm-registry.mjs` doesn't open a browser (SSH/headless) | Expected — it falls back to printing the token creation URL. Pass `--no-open` to skip the attempt entirely |
|
||||
| Pipeline stuck at design gate | `interrogator` is waiting on your answers — this is intentional, answer the questions |
|
||||
|
||||
## Next steps
|
||||
|
||||
Generated
+2
-2
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "@redsen/lean-harness",
|
||||
"name": "@redsentech/lean-harness",
|
||||
"version": "0.1.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@redsen/lean-harness",
|
||||
"name": "@redsentech/lean-harness",
|
||||
"version": "0.1.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -95,6 +95,27 @@ describe('setup-npm-registry script', () => {
|
||||
assert.match(content, /\/\/example\.test\/:_authToken=fake-token/);
|
||||
});
|
||||
|
||||
test('--no-open prints the classic-PAT URL and accepts a pasted token via stdin', () => {
|
||||
const npmrc = tempNpmrcPath();
|
||||
const r = execFileSync(process.execPath, [
|
||||
SCRIPT, '--no-open', '--skip-verify', `--npmrc=${npmrc}`,
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
input: 'ghp_pastedtoken1234\n',
|
||||
env: {
|
||||
...process.env,
|
||||
NPM_REGISTRY_TOKEN: '',
|
||||
GITHUB_TOKEN: '',
|
||||
GH_TOKEN: '',
|
||||
PATH: '/nonexistent', // hide `gh` so gh-CLI discovery can't short-circuit the prompt
|
||||
},
|
||||
});
|
||||
assert.match(r, /github\.com\/settings\/tokens\/new\?scopes=read%3Apackages/);
|
||||
assert.match(r, /--no-open.*skipping automatic browser launch/);
|
||||
const content = readFileSync(npmrc, 'utf8');
|
||||
assert.match(content, /_authToken=ghp_pastedtoken1234/);
|
||||
});
|
||||
|
||||
test('fails cleanly with --yes and no token available anywhere', () => {
|
||||
const npmrc = tempNpmrcPath();
|
||||
const r = setup(['--yes', `--npmrc=${npmrc}`], {
|
||||
|
||||
Reference in New Issue
Block a user