@bongos/core 1.24.69 → 1.24.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,7 +11,7 @@
11
11
 
12
12
  const fs = require('node:fs');
13
13
  const path = require('node:path');
14
- const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
14
+ const { resolveCoreRoot, resolveInstanceRoot, resolveInstanceRootExplicit } = require('../../src/instance-config'); // ADR 0108 §1: configurable-root
15
15
 
16
16
 
17
17
  // ADR 0108 §Consequences #4 (configurable-root W6 / task 1886) — the guardrail walks TWO trees.
@@ -97,6 +97,23 @@ function isInstalledCore(root = CORE_ROOT) {
97
97
  } catch { return true; } // a package with no repo around it at all
98
98
  }
99
99
 
100
+ // resolveWorkRoot — the tree a repo-wide tool (docs-entropy, ship-check) works on (task 1005100).
101
+ // A core CHECKOUT works on itself, whatever the cwd, exactly as path.resolve(__dirname,'..','..')
102
+ // always did. An INSTALLED core's package dir is not the project — `git ls-files` there finds
103
+ // nothing and the generators read the package's own files — so it works on the project instead:
104
+ // an explicit pointer (<PREFIX>_INSTANCE_ROOT / bongos.config.json), else the git top of the cwd
105
+ // (`npx bongos exec` runs from the project), else the cwd.
106
+ function gitTopOf(cwd) {
107
+ try {
108
+ return require('node:child_process').execFileSync('git', ['rev-parse', '--show-toplevel'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
109
+ } catch { return null; }
110
+ }
111
+ function resolveWorkRoot({ installed, cwd = process.cwd(), explicit = resolveInstanceRootExplicit, gitTop = gitTopOf } = {}) {
112
+ if (installed === undefined) installed = isInstalledCore();
113
+ if (!installed) return CORE_ROOT;
114
+ return explicit() || gitTop(cwd) || cwd;
115
+ }
116
+
100
117
  // runChecks — run every fitness check, or none (task 1004737). Every check guards the core's
101
118
  // own source, and an INSTALLED core's source is not the project's to change: the core repo's
102
119
  // CI ran this gate before publishing it (the same stance ship-merge-gate.js takes for a
@@ -115,6 +132,7 @@ function runChecks(checks, { installed = isInstalledCore() } = {}) {
115
132
 
116
133
  module.exports = {
117
134
  isInstalledCore,
135
+ resolveWorkRoot,
118
136
  runChecks,
119
137
  keyTokenRe,
120
138
  CORE_ROOT,
@@ -8,6 +8,8 @@
8
8
  //
9
9
  // Usage:
10
10
  // bongos paste-token <token> # what the hall hands you (task 1003961)
11
+ // bongos paste-token # no argument: asks for the token without echo,
12
+ // # or reads it from stdin when piped (task 1005033)
11
13
  // node scripts/bongos/paste-token.js <token> # equivalent, inside a core checkout
12
14
  //
13
15
  // The portal returns the exact invocation as the "paste_command" field, so a
@@ -83,10 +85,71 @@ const ARGS = parseArgs(process.argv.slice(2));
83
85
  const RESOLVED = resolveBaseForPaste({ flag: ARGS.apiBase });
84
86
  const API_BASE = RESOLVED.base;
85
87
 
88
+ // READ THE TOKEN WITHOUT PUTTING IT ON THE COMMAND LINE (task 1005033). A token passed
89
+ // as an argument lands in shell history. With no argument we ask for it: on a terminal
90
+ // the read is raw and nothing is echoed (a 64-char secret has no business on screen
91
+ // either); piped (`... | bongos paste-token`) we take the first word of stdin. The
92
+ // argument form still works — the hall's one-liner uses it, and older instances hand
93
+ // out nothing else. Exported so the no-echo read is tested, not assumed.
94
+ function readTokenFromInput({ input = process.stdin, output = process.stderr } = {}) {
95
+ if (!input.isTTY) {
96
+ // Piped: settle on the FIRST line, not at end of stream — a pipe that never closes
97
+ // must not hang the paste, and nothing after the token is ours to buffer.
98
+ return new Promise((resolve, reject) => {
99
+ let data = '';
100
+ let settled = false;
101
+ const settle = () => {
102
+ if (settled) return;
103
+ settled = true;
104
+ input.removeListener('data', onData);
105
+ input.pause();
106
+ resolve(data.trim().split(/\s+/)[0] || '');
107
+ };
108
+ function onData(c) {
109
+ data += c;
110
+ if (data.replace(/^\s+/, '').includes('\n')) settle();
111
+ }
112
+ input.setEncoding('utf8');
113
+ input.on('data', onData);
114
+ input.on('end', settle);
115
+ input.on('error', (err) => { if (!settled) { settled = true; reject(err); } });
116
+ });
117
+ }
118
+ output.write('Paste your CLI token (Settings → CLI Access) and press Enter: ');
119
+ return new Promise((resolve, reject) => {
120
+ let buf = '';
121
+ const raw = typeof input.setRawMode === 'function';
122
+ if (raw) input.setRawMode(true);
123
+ input.setEncoding('utf8');
124
+ input.resume();
125
+ function finish(err) {
126
+ input.removeListener('data', onData);
127
+ if (raw) input.setRawMode(false);
128
+ input.pause();
129
+ output.write('\n');
130
+ if (err) reject(err); else resolve(buf.trim());
131
+ }
132
+ function onData(chunk) {
133
+ for (const ch of String(chunk)) {
134
+ if (ch === '\r' || ch === '\n') { finish(); return; }
135
+ // Ctrl+C always cancels; Ctrl+D (end of input) cancels too unless a token is typed.
136
+ if (ch === '\u0003' || (ch === '\u0004' && !buf.trim())) { finish(new Error('cancelled — nothing was saved')); return; }
137
+ if (ch === '\u0004') { finish(); return; }
138
+ if (ch === '\u007f' || ch === '\b') { buf = buf.slice(0, -1); continue; }
139
+ buf += ch;
140
+ }
141
+ }
142
+ input.on('data', onData);
143
+ });
144
+ }
145
+
86
146
  function usage(extra) {
87
147
  if (extra) console.error(extra);
88
148
  console.error('');
89
- console.error('Usage: npx bongos paste-token <token> [--api-base <url>]');
149
+ console.error('Usage: npx bongos paste-token [<token>] [--api-base <url>]');
150
+ console.error(' Leave out <token> and it asks for it, so the token stays out of shell history.');
151
+ console.error(' Run it from inside your project folder (cd there first).');
152
+ console.error(' In Windows PowerShell type npx.cmd instead of npx.');
90
153
  console.error(' (inside a core checkout: node scripts/bongos/paste-token.js <token>)');
91
154
  console.error('');
92
155
  // Fall back to the inferred base for this hint when --api-base was the thing that
@@ -103,9 +166,17 @@ async function main() {
103
166
  usage(`--api-base must be an http(s) URL, got: ${ARGS.apiBase}`);
104
167
  process.exit(2);
105
168
  }
106
- const token = ARGS.token;
169
+ let token = ARGS.token;
170
+ if (!token) {
171
+ try {
172
+ token = await readTokenFromInput();
173
+ } catch (err) {
174
+ console.error(`paste-token: ${err.message}`);
175
+ process.exit(2);
176
+ }
177
+ }
107
178
  if (!token) {
108
- usage('No token argument provided.');
179
+ usage('No token given.');
109
180
  process.exit(2);
110
181
  }
111
182
  // Tokens are hex-encoded crypto.randomBytes(32) → 64 lowercase hex chars.
@@ -209,4 +280,4 @@ if (require.main === module) {
209
280
  });
210
281
  }
211
282
 
212
- module.exports = { parseArgs, resolveBaseForPaste };
283
+ module.exports = { parseArgs, resolveBaseForPaste, readTokenFromInput };
@@ -21,7 +21,12 @@ const { spawnSync } = require('node:child_process');
21
21
  const fs = require('node:fs');
22
22
  const path = require('node:path');
23
23
 
24
- const REPO_ROOT = path.resolve(__dirname, '..', '..');
24
+ // Two roots (task 1005100). CORE_ROOT is where the step scripts live; WORK_ROOT is the tree they
25
+ // check. In a project that installs the core they differ: running the generators from the
26
+ // package dir checked the package's files, so gen-api-docs read STALE while the same --check
27
+ // run from the project said up to date. In a core checkout both are this checkout.
28
+ const CORE_ROOT = path.resolve(__dirname, '..', '..');
29
+ const WORK_ROOT = require('./fitness-lib.js').resolveWorkRoot();
25
30
 
26
31
  // Each step: a label, the argv to run (node scripts run with THIS node), the command that heals it,
27
32
  // and how to read a non-zero exit. `optional` steps are skipped when their script is absent. An `advisory`
@@ -45,8 +50,11 @@ const STEPS = [
45
50
  { key: 'tests', label: 'DB-free unit lane (npm test)', argv: ['scripts/bongos/run-unit-tests.js'], fix: 'read the FAIL lines; compare against origin/main before calling any of them environmental', onlyWith: '--tests', slow: true },
46
51
  ];
47
52
 
48
- function runStep(step, { cwd = REPO_ROOT } = {}) {
49
- const script = path.join(cwd, step.argv[0]);
53
+ // A caller that passes only `cwd` (tests) gets scripts resolved there too, as before.
54
+ function runStep(step, { cwd, coreRoot } = {}) {
55
+ coreRoot = coreRoot || cwd || CORE_ROOT;
56
+ cwd = cwd || WORK_ROOT;
57
+ const script = path.join(coreRoot, step.argv[0]);
50
58
  if (!fs.existsSync(script)) return { ...step, status: step.optional ? 'skipped' : 'missing', ms: 0, tail: `${step.argv[0]} not present` };
51
59
  const t0 = Date.now();
52
60
  const r = spawnSync(process.execPath, [script, ...step.argv.slice(1)], { cwd, encoding: 'utf8', timeout: step.slow ? 15 * 60 * 1000 : 5 * 60 * 1000, env: { ...process.env, GDS_QUIET: '1' } });
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+ // src/bongos/device-flow-blocker.js — tell the OWNER, loudly, that terminal sign-in is off
3
+ // (task 1005033).
4
+ //
5
+ // WHY. GitHub creates an App from a manifest with Device Flow OFF and has no API to switch it
6
+ // on — only a checkbox in the App's settings. Until the owner ticks it, every builder's
7
+ // `bongos setup` / `bongos login` is refused, and the only person told was that builder, in a
8
+ // terminal, who cannot fix it. The provisioning checklist shows the step as optional
9
+ // ("later"), which no owner reads as urgent. So the first time this server sees GitHub's
10
+ // device_flow_disabled refusal it files ONE blocker for the owner, through the ideas
11
+ // module's `ideas.fileBlocker` port (core never imports a module). The writer is idempotent
12
+ // on (source, source_ref), so a thousand refused sign-ins still make one blocker.
13
+ //
14
+ // Best effort by construction: no ideas module, a refused write or a DB hiccup changes
15
+ // nothing about the sign-in reply the builder gets. Never throws, never rejects.
16
+
17
+ const seams = require('../module-seams');
18
+
19
+ const SOURCE = 'auth';
20
+ const SOURCE_REF = 'github-device-flow-disabled';
21
+
22
+ function isDeviceFlowDisabledReason(reason) {
23
+ return /device_flow_disabled/i.test(String(reason || ''));
24
+ }
25
+
26
+ function blockerFor(origin) {
27
+ const settings = origin ? `${origin}/builders/settings` : 'Settings → CLI Access';
28
+ return {
29
+ title: 'Terminal sign-in is off: tick "Enable Device Flow" on the GitHub App',
30
+ bodyMd: [
31
+ 'A builder just tried to sign in from a terminal (`bongos setup` or `bongos login`) and GitHub refused:',
32
+ 'this project\'s GitHub App has **Device Flow switched off**. Apps made by the one-click setup start',
33
+ 'that way, and GitHub offers no way for the setup to switch it on.',
34
+ '',
35
+ '**Fix (the project owner, once, about a minute):** GitHub → Settings → Developer settings →',
36
+ 'GitHub Apps → this project\'s app → tick **Enable Device Flow** → Save. Web sign-in is unaffected.',
37
+ '',
38
+ `Until then builders can still sign in through the web: ${settings}, "Re-issue CLI token".`,
39
+ ].join('\n'),
40
+ source: SOURCE,
41
+ sourceRef: SOURCE_REF,
42
+ };
43
+ }
44
+
45
+ async function raiseDeviceFlowOffBlocker({ reason, origin, resolveOptional = seams.resolveOptional } = {}) {
46
+ try {
47
+ if (!isDeviceFlowDisabledReason(reason)) return null;
48
+ const file = resolveOptional('ideas.fileBlocker', null);
49
+ if (typeof file !== 'function') return null;
50
+ return (await file(blockerFor(origin))) || null;
51
+ } catch (_) {
52
+ return null;
53
+ }
54
+ }
55
+
56
+ module.exports = { raiseDeviceFlowOffBlocker, isDeviceFlowDisabledReason, SOURCE, SOURCE_REF };
@@ -42,6 +42,7 @@ const { accountExistenceReadRateLimit } = require('../middleware/rate-limit');
42
42
  const { branding } = require('../../branding');
43
43
  const { resolveEnv, envPrefix } = require('../../instance-config');
44
44
  const seams = require('../../module-seams');
45
+ const { raiseDeviceFlowOffBlocker } = require('../device-flow-blocker');
45
46
  // task 1003208: structured logging — was console.*. A core file logs through
46
47
  // the kernel logger directly; only MODULES go via api.logger (the doorway).
47
48
  const log = require('../logger').logger.child({ src: 'auth' });
@@ -623,6 +624,7 @@ module.exports = function buildAuthRouter() {
623
624
  // fresh-instance auth failure could only be read via curl on the box itself
624
625
  // (finding W2, task 1003052). A 4xx passes the proxy with the JSON intact.
625
626
  const reason = String(err && err.message ? err.message : 'internal error').slice(0, 300);
627
+ raiseDeviceFlowOffBlocker({ reason, origin: WEB_REDIRECT_ORIGIN }); // owner blocker, once (task 1005033)
626
628
  res.fail('device_start_failed', { status: 424, message: reason });
627
629
  }
628
630
  });
@@ -1353,6 +1355,9 @@ module.exports = function buildAuthRouter() {
1353
1355
  paste_command: issuer
1354
1356
  ? `npx bongos paste-token ${token} --api-base ${issuer}`
1355
1357
  : `npx bongos paste-token ${token}`,
1358
+ paste_command_windows: issuer
1359
+ ? `npx.cmd bongos paste-token ${token} --api-base ${issuer}`
1360
+ : `npx.cmd bongos paste-token ${token}`, // task 1005033: PowerShell blocks npx.ps1 by default
1356
1361
  });
1357
1362
  } catch (err) {
1358
1363
  log.error('[gds] POST /auth/cli-token/issue', err);
package/src/module-api.js CHANGED
@@ -81,7 +81,7 @@ const { sleep } = require('./sleep');
81
81
  // MAJOR (see allowBoxScope below): passes the request through untouched.
82
82
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
83
83
 
84
- const CORE_VERSION = '1.24.69'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
84
+ const CORE_VERSION = '1.24.70'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
85
85
 
86
86
  // A namespaced logger so a module's log lines are attributable + consistent.
87
87
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -76,7 +76,8 @@ test('the server emits exactly the verb the CLI registers', () => {
76
76
  assert.ok(cmds.length > 0, 'auth.js should emit at least one paste_command template literal');
77
77
  for (const cmd of cmds) {
78
78
  // `npx bongos <verb> …` since task 1004575 — a clone has no bare `bongos` on PATH.
79
- const verb = cmd.replace(/^npx /, '').split(' ')[1];
79
+ // npx.cmd is the Windows PowerShell form (task 1005033) — same verb after it.
80
+ const verb = cmd.replace(/^npx(\.cmd)? /, '').split(' ')[1];
80
81
  assert.ok(cli.VERBS[verb], `server emits "bongos ${verb}" but the CLI has no such verb`);
81
82
  }
82
83
  });
@@ -174,11 +174,13 @@ t('paste_command names a CLI VERB, never a filesystem path', () => {
174
174
  // applying without anyone editing it.
175
175
  const tail = issueBlock.slice(issueBlock.indexOf('paste_command:'));
176
176
  const cmds = [...tail.matchAll(/`([^`]*bongos paste-token[^`]*)`/g)].map((m) => m[1]);
177
- assert.equal(cmds.length, 2, `expected both ternary branches, got ${cmds.length}`);
177
+ // Four since task 1005033: paste_command's two branches, then paste_command_windows'
178
+ // two — the same line with npx.cmd, because npx.ps1 is blocked in a default PowerShell.
179
+ assert.equal(cmds.length, 4, `expected both ternary branches of both fields, got ${cmds.length}`);
178
180
  for (const cmd of cmds) {
179
181
  // Through npx (task 1004575): a builder's clone does not put bare `bongos` on
180
182
  // PATH, so a bare verb was "command not found" when pasted in the project folder.
181
- assert.ok(/^npx bongos paste-token /.test(cmd),
183
+ assert.ok(/^npx(\.cmd)? bongos paste-token /.test(cmd),
182
184
  `paste_command must start with the npx verb form, got: ${cmd}`);
183
185
  // The invariant is "names no FILE ON THE RECIPIENT'S DISK", which the original
184
186
  // spelling approximated as "contains no slash". That was too broad once the command
@@ -87,7 +87,9 @@ test('Settings CLI access says where to paste the npx command', () => {
87
87
  const js = read('modules/hall-ui/public/settings.js');
88
88
  assert.match(html, /into a terminal in your project folder/);
89
89
  assert.doesNotMatch(html, /bypasses Device Flow/);
90
- assert.match(js, /Paste this into a terminal in your project folder:/);
90
+ // Both spellings (task 1005033) name the project folder AND the cd that gets there.
91
+ assert.match(js, /Open a terminal, cd into your project folder first \(the command only runs there\), then paste this:/);
92
+ assert.match(js, /Open PowerShell, cd into your project folder first \(the command only runs there\), then paste this:/);
91
93
  });
92
94
 
93
95
  test('the npx paste command Settings hands out is pre-approved for a Claude session', () => {
@@ -338,8 +338,8 @@ test('the CLI token reaches the page as text, not as markup', () => {
338
338
  // The secret and the clipboard read the SAME resolved value, so the button can
339
339
  // never copy a different string than the one on screen — nor the literal
340
340
  // "undefined", which writeText(body.paste_command) produced on a body without one.
341
- assert.match(fn[0], /const pasteCommand = body\.paste_command \|\| ''/, 'one resolved value');
342
- assert.match(fn[0], /writeText\(pasteCommand\)/, 'the clipboard copies that same value');
341
+ assert.match(fn[0], /const pasteCommand = \(windows && body\.paste_command_windows\) \|\| body\.paste_command \|\| ''/, 'one resolved value');
342
+ assert.match(fn[0], /copy\(pasteCommand, /, 'the clipboard copies that same value');
343
343
  assert.match(fn[0], /result\.textContent = ''/, 're-issuing replaces the previous token rather than stacking a second');
344
344
  });
345
345
 
@@ -0,0 +1,165 @@
1
+ // tests/paste_token_windows.mjs — a first-time CLI sign-in on Windows took four failed
2
+ // attempts (task 1005033). Three of the four failure points are in code:
3
+ //
4
+ // 2. The hall's one-liner starts with `npx`. In Windows PowerShell `npx` resolves to
5
+ // npx.ps1, which the DEFAULT execution policy refuses to run ("running scripts is
6
+ // disabled on this system"). `npx.cmd` is the same program without the script
7
+ // host, and works in PowerShell and cmd alike. The hall now hands a Windows
8
+ // browser the npx.cmd form.
9
+ // 3. The one-liner only resolves `bongos` inside the project folder (npx finds the
10
+ // project's own bin); from anywhere else npm says "could not determine executable
11
+ // to run". Nothing said so — the hall now says it in the instruction itself.
12
+ // 4. The token sat on the command line (shell history). paste-token now also reads it
13
+ // from a hidden prompt, or from stdin when piped, when no token argument is given.
14
+ //
15
+ // 1. The App's Device Flow switch was off. GitHub has no API to tick it, so the fix is
16
+ // telling the OWNER: the first refusal files one idempotent blocker
17
+ // (src/bongos/device-flow-blocker.js). Task 1005118 later replaces the paste route
18
+ // with a browser approval.
19
+ //
20
+ // Run: node --test tests/paste_token_windows.mjs
21
+
22
+ import { test } from 'node:test';
23
+ import assert from 'node:assert/strict';
24
+ import fs from 'node:fs';
25
+ import path from 'node:path';
26
+ import { PassThrough } from 'node:stream';
27
+ import { createRequire } from 'node:module';
28
+ import { fileURLToPath } from 'node:url';
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const { readTokenFromInput } = require(path.join(ROOT, 'scripts', 'bongos', 'paste-token.js'));
33
+
34
+ const sink = () => { const s = new PassThrough(); s.text = ''; s.on('data', (c) => { s.text += c; }); return s; };
35
+
36
+ // ── 4. the token can arrive without touching the command line ────────────────
37
+
38
+ test('a piped token is read from stdin and trimmed', async () => {
39
+ const input = new PassThrough();
40
+ const p = readTokenFromInput({ input, output: sink() });
41
+ input.end(' abcdef0123456789abcdef0123456789 \r\n');
42
+ assert.equal(await p, 'abcdef0123456789abcdef0123456789');
43
+ });
44
+
45
+ test('an interactive prompt reads one line and never echoes the token', async () => {
46
+ const input = new PassThrough();
47
+ input.isTTY = true;
48
+ const modes = [];
49
+ input.setRawMode = (on) => { modes.push(on); input.isRaw = on; };
50
+ const output = sink();
51
+ const p = readTokenFromInput({ input, output });
52
+ input.write('abcdef01234567');
53
+ input.write('X\u007f'); // a typo, then backspace
54
+ input.write('89abcdef\r');
55
+ const token = await p;
56
+ assert.equal(token, 'abcdef0123456789abcdef');
57
+ assert.ok(!output.text.includes('abcdef'), `the token must not be echoed, got: ${JSON.stringify(output.text)}`);
58
+ assert.match(output.text, /token/i, 'it asks for the token');
59
+ assert.deepEqual(modes, [true, false], 'raw mode is switched on for the read and back off after');
60
+ });
61
+
62
+ test('Ctrl+C at the prompt cancels instead of saving a half-typed token', async () => {
63
+ const input = new PassThrough();
64
+ input.isTTY = true;
65
+ input.setRawMode = () => {};
66
+ const p = readTokenFromInput({ input, output: sink() });
67
+ input.write('abc\u0003');
68
+ await assert.rejects(p, /cancel/i);
69
+ });
70
+
71
+ test('paste-token falls back to the prompt when no token argument is given', () => {
72
+ const src = fs.readFileSync(path.join(ROOT, 'scripts', 'bongos', 'paste-token.js'), 'utf8');
73
+ const main = src.slice(src.indexOf('async function main()'));
74
+ assert.match(main, /readTokenFromInput\(/, 'main reads the token from input when argv has none');
75
+ });
76
+
77
+ // ── 2 + 3. the hall's one-liner works in a default Windows PowerShell ─────────
78
+
79
+ const routeSrc = fs.readFileSync(path.join(ROOT, 'src', 'bongos', 'routes', 'auth.js'), 'utf8');
80
+ const issueBlock = (() => {
81
+ const start = routeSrc.indexOf("'/auth/cli-token/issue'");
82
+ const next = routeSrc.indexOf('\n router.', start);
83
+ return routeSrc.slice(start, next === -1 ? routeSrc.length : next);
84
+ })();
85
+
86
+ test('the issue route also composes a Windows one-liner that avoids npx.ps1', () => {
87
+ const m = issueBlock.match(/paste_command_windows:\s*issuer\s*\?\s*`([^`]+)`\s*:\s*`([^`]+)`/);
88
+ assert.ok(m, 'paste_command_windows should be a readable template literal pair');
89
+ for (const cmd of [m[1], m[2]]) {
90
+ assert.match(cmd, /^npx\.cmd bongos paste-token \$\{token\}/, `npx.cmd, not npx — got: ${cmd}`);
91
+ }
92
+ assert.match(m[1], /--api-base \$\{issuer\}/, 'the Windows form carries the issuer too');
93
+ });
94
+
95
+ const settingsJs = fs.readFileSync(path.join(ROOT, 'modules', 'hall-ui', 'public', 'settings.js'), 'utf8');
96
+ const issueFn = (settingsJs.match(/async function issueCliToken\([\s\S]*?\n \}\n/) || [''])[0];
97
+
98
+ test('the hall shows the Windows one-liner to a Windows browser', () => {
99
+ assert.ok(issueFn, 'issueCliToken is a whole function');
100
+ assert.match(issueFn, /paste_command_windows/, 'it reads the Windows form');
101
+ assert.match(settingsJs, /function isWindowsBrowser\(/, 'it decides by the browser platform');
102
+ });
103
+
104
+ test('the hall says the command only runs from inside the project folder', () => {
105
+ assert.match(issueFn, /cd/, 'the instruction names cd');
106
+ assert.match(issueFn, /only (runs|works) (from )?(there|inside)/i, 'and says why');
107
+ });
108
+
109
+ // ── grade round 2 (task 1005033) ─────────────────────────────────────────────
110
+
111
+ test('a pipe that never closes still settles on the first line', async () => {
112
+ const input = new PassThrough();
113
+ const p = readTokenFromInput({ input, output: sink() });
114
+ input.write('abcdef0123456789abcdef\nsomething else that never ends');
115
+ assert.equal(await p, 'abcdef0123456789abcdef');
116
+ });
117
+
118
+ test('Ctrl+D on an empty prompt cancels instead of saving nothing', async () => {
119
+ const input = new PassThrough();
120
+ input.isTTY = true;
121
+ input.setRawMode = () => {};
122
+ const p = readTokenFromInput({ input, output: sink() });
123
+ input.write('\u0004');
124
+ await assert.rejects(p, /cancel/i);
125
+ });
126
+
127
+ test('the hall offers the token-free command with the token on its own button', () => {
128
+ assert.match(issueFn, /bongos paste-token\$\{body\.api_base/, 'a command with no token in it');
129
+ assert.match(issueFn, /Copy token/, 'the token is copied separately');
130
+ assert.match(issueFn, /Keep the token out of your shell history/, 'and it says why');
131
+ });
132
+
133
+ // ── 1. the owner hears about Device Flow being off ────────────────────────────
134
+
135
+ const { raiseDeviceFlowOffBlocker, SOURCE, SOURCE_REF } = require(path.join(ROOT, 'src', 'bongos', 'device-flow-blocker.js'));
136
+
137
+ test('a device_flow_disabled refusal files ONE owner blocker through ideas.fileBlocker', async () => {
138
+ const filed = [];
139
+ const resolveOptional = (port) => (port === 'ideas.fileBlocker' ? async (b) => { filed.push(b); return { id: 1 }; } : null);
140
+ const out = await raiseDeviceFlowOffBlocker({ reason: 'device_flow_disabled', origin: 'https://x.example', resolveOptional });
141
+ assert.deepEqual(out, { id: 1 });
142
+ assert.equal(filed.length, 1);
143
+ assert.equal(filed[0].source, SOURCE);
144
+ assert.equal(filed[0].sourceRef, SOURCE_REF, 'a fixed source_ref makes the writer idempotent');
145
+ assert.match(filed[0].title, /Enable Device Flow/);
146
+ assert.match(filed[0].bodyMd, /https:\/\/x\.example\/builders\/settings/);
147
+ });
148
+
149
+ test('any other start failure files nothing', async () => {
150
+ let called = false;
151
+ const resolveOptional = () => async () => { called = true; };
152
+ assert.equal(await raiseDeviceFlowOffBlocker({ reason: 'bad_client_id', resolveOptional }), null);
153
+ assert.equal(called, false);
154
+ });
155
+
156
+ test('no ideas module, or a failing write, never breaks the sign-in reply', async () => {
157
+ assert.equal(await raiseDeviceFlowOffBlocker({ reason: 'device_flow_disabled', resolveOptional: () => null }), null);
158
+ const boom = () => async () => { throw new Error('db down'); };
159
+ assert.equal(await raiseDeviceFlowOffBlocker({ reason: 'device_flow_disabled', resolveOptional: boom }), null);
160
+ });
161
+
162
+ test('the device/start route raises it on the refusal', () => {
163
+ const start = routeSrc.slice(routeSrc.indexOf("'/auth/device/start'"), routeSrc.indexOf("'/auth/device/start'") + 2500);
164
+ assert.match(start, /raiseDeviceFlowOffBlocker\(\{ reason/);
165
+ });
@@ -0,0 +1,72 @@
1
+ // tests/vendored_work_root.mjs
2
+ //
3
+ // task 1005100 - feedback instance vendors: in a project that installs the core from npm
4
+ // (toast, core 1.21.115), docs-entropy.js and ship-check.js took their root from
5
+ // path.resolve(__dirname, '..', '..') — the PACKAGE dir under node_modules/@bongos/core.
6
+ // docs-entropy then ran `git ls-files '*.md'` there, enumerated 0 files and died with a
7
+ // "scan defect"; ship-check ran every generator from the package dir, so gen-api-docs read
8
+ // STALE while `npx bongos exec scripts/bongos/gen-api-docs.js --check` from the project
9
+ // said up to date. resolveWorkRoot() is the root these tools work on: the core checkout when
10
+ // the core is a checkout, the project (pointer, else the git top of cwd, else cwd) when it
11
+ // is installed. ship-check spawns the script from the core and runs it in that root.
12
+ //
13
+ // Run: node tests/vendored_work_root.mjs
14
+
15
+ import assert from 'node:assert/strict';
16
+ import { test } from 'node:test';
17
+ import { execFileSync } from 'node:child_process';
18
+ import { createRequire } from 'node:module';
19
+ import { fileURLToPath } from 'node:url';
20
+ import fs from 'node:fs';
21
+ import os from 'node:os';
22
+ import path from 'node:path';
23
+
24
+ const require = createRequire(import.meta.url);
25
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
26
+ const { resolveWorkRoot, CORE_ROOT } = require('../scripts/bongos/fitness-lib.js');
27
+ const { runStep } = require('../scripts/bongos/ship-check.js');
28
+
29
+ const real = (p) => fs.realpathSync.native(p);
30
+ const tmp = (name) => fs.mkdtempSync(path.join(os.tmpdir(), `vwr-${name}-`));
31
+
32
+ test('a core checkout works on the core root, whatever the cwd', () => {
33
+ assert.equal(resolveWorkRoot({ installed: false, cwd: os.tmpdir() }), CORE_ROOT);
34
+ });
35
+
36
+ test('an installed core with a pointer works on the pointed-at project', () => {
37
+ assert.equal(resolveWorkRoot({ installed: true, cwd: os.tmpdir(), explicit: () => '/proj' }), '/proj');
38
+ });
39
+
40
+ test('an installed core with no pointer works on the git top of cwd, not the package dir', () => {
41
+ const repo = tmp('repo');
42
+ execFileSync('git', ['init', '-q'], { cwd: repo });
43
+ const sub = path.join(repo, 'docs');
44
+ fs.mkdirSync(sub);
45
+ const got = resolveWorkRoot({ installed: true, cwd: sub, explicit: () => null });
46
+ assert.equal(real(got), real(repo));
47
+ });
48
+
49
+ test('an installed core outside any repo falls back to cwd', () => {
50
+ const dir = tmp('norepo');
51
+ const got = resolveWorkRoot({ installed: true, cwd: dir, explicit: () => null, gitTop: () => null });
52
+ assert.equal(got, dir);
53
+ });
54
+
55
+ test('ship-check runs a step from the core but inside the work root', () => {
56
+ const core = tmp('core');
57
+ const inst = tmp('inst');
58
+ fs.mkdirSync(path.join(core, 'scripts', 'bongos'), { recursive: true });
59
+ fs.writeFileSync(path.join(core, 'scripts', 'bongos', 'where.js'), 'console.log("cwd=" + process.cwd());\n');
60
+ const r = runStep({ key: 'w', label: 'w', argv: ['scripts/bongos/where.js'], fix: 'f' }, { coreRoot: core, cwd: inst });
61
+ assert.equal(r.status, 'ok', r.tail);
62
+ assert.match(r.tail, /cwd=/);
63
+ assert.equal(real(r.tail.trim().replace(/^cwd=/, '')), real(inst));
64
+ });
65
+
66
+ test('docs-entropy and ship-check no longer hardwire their root to the package dir', () => {
67
+ for (const f of ['scripts/bongos/docs-entropy.js', 'scripts/bongos/ship-check.js']) {
68
+ const src = fs.readFileSync(path.join(ROOT, f), 'utf8');
69
+ assert.doesNotMatch(src, /const REPO_ROOT = path\.resolve\(__dirname, '\.\.', '\.\.'\)/, f);
70
+ assert.match(src, /resolveWorkRoot\(\)/, f);
71
+ }
72
+ });