@bongos/core 1.21.147 → 1.21.148

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.
package/.bongos-core.json CHANGED
@@ -2,22 +2,22 @@
2
2
  "artifact": "bongos-core",
3
3
  "manifest_schema": 1,
4
4
  "generator": "scripts/gds/package-core.js",
5
- "core_version": "1.21.147",
6
- "core_contract": "1.21.147",
7
- "source_commit": "13137d54836ff06c25504af346ea4bd102a63f79",
5
+ "core_version": "1.21.148",
6
+ "core_contract": "1.21.148",
7
+ "source_commit": "96de3db7b01db9d1d1ea6de0020e2863549ace48",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-10-19T15:05:58.444Z",
9
+ "built_at": "2026-10-19T16:38:35.780Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
12
  "docs_redacted": 608,
13
13
  "agent_docs_stubbed": 28,
14
- "functional_verbatim": 3166,
14
+ "functional_verbatim": 3168,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 3803,
20
- "tree_sha256": "888e10ec8ed80028fa81d68f55de93405ff4ba3f7ea6adc2f61de4d94e3826ac",
19
+ "file_count": 3805,
20
+ "tree_sha256": "9aa795e140a79d60c77adddcf814eb815324f8778aa3e839f2eed0d221aba834",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/ask-for-help/SKILL.md",
@@ -312,7 +312,7 @@
312
312
  {
313
313
  "path": "bin/bongos.js",
314
314
  "mode": "0000755",
315
- "sha256": "656e91004fc2236dfd121dd3f46848ba5a51331c2eb17095398eb30a5255b7ef"
315
+ "sha256": "10da188550dd74f74a8598b8815076cf098908196f366aad1f6ca39e16c68c33"
316
316
  },
317
317
  {
318
318
  "path": "clients/bongos-client/README.md",
@@ -2927,7 +2927,7 @@
2927
2927
  {
2928
2928
  "path": "docs/module-api-changelog.md",
2929
2929
  "mode": "0000644",
2930
- "sha256": "76717a07eeea4ba8500f793a15a62e3cbe9a31516ecd5223cdc8e6ff3c752014"
2930
+ "sha256": "8f019f26dca4f3a5b624cbabd6d28976deef91faab61279479708ab9585b3fc9"
2931
2931
  },
2932
2932
  {
2933
2933
  "path": "docs/modules-contract.md",
@@ -10552,12 +10552,12 @@
10552
10552
  {
10553
10553
  "path": "package-lock.json",
10554
10554
  "mode": "0000644",
10555
- "sha256": "8e87b847de2767eb834d302fa4d49688a8c725da1daee28bfa195ec2cb5da693"
10555
+ "sha256": "c5456eee2c0d7c56f4a86b537ad4a056d7ee25178017ce8b2c4a6d584a700b91"
10556
10556
  },
10557
10557
  {
10558
10558
  "path": "package.json",
10559
10559
  "mode": "0000644",
10560
- "sha256": "2ddbcdc32b9be33b09ca6af8c0fe28b50a9e0f620f72c8e6adab1a1c054f721d"
10560
+ "sha256": "eeee21b5c79606fce2ec960818bbca2bda46e8487e821e086db727725dd34483"
10561
10561
  },
10562
10562
  {
10563
10563
  "path": "public-docs/index.html",
@@ -10577,7 +10577,7 @@
10577
10577
  {
10578
10578
  "path": "release-notes.json",
10579
10579
  "mode": "0000644",
10580
- "sha256": "d017350fb8de15dc36139bfac03dc0126d51ff14c632ab29c2e08a5e017102dc"
10580
+ "sha256": "e75ce28a56ae19d8e908bca8b07e539fcb4066b72fb7e6f282e8dff5803f22b4"
10581
10581
  },
10582
10582
  {
10583
10583
  "path": "scripts/cron/style_guide_audit.sh",
@@ -10954,10 +10954,15 @@
10954
10954
  "mode": "0000644",
10955
10955
  "sha256": "9dd80d2c37602c59cb75db0be02cfadc737fc64891a81d03f5be5f0ea5e35a27"
10956
10956
  },
10957
+ {
10958
+ "path": "scripts/gds/core-origin-follow.js",
10959
+ "mode": "0000644",
10960
+ "sha256": "5d12058796dc7373584fc8447ee83b8da0820a447e783b2462156f1ec158b8c5"
10961
+ },
10957
10962
  {
10958
10963
  "path": "scripts/gds/core-pin-sync.js",
10959
10964
  "mode": "0000644",
10960
- "sha256": "f8a3a0ee2c0f4dbc2583603cd6e7b547140a5f8dc286b7c5f0b6dadd6096a002"
10965
+ "sha256": "9e2e6dd476938df51bf027396df8a92e0ed2ca96a9adf59cb666ad2681cc721e"
10961
10966
  },
10962
10967
  {
10963
10968
  "path": "scripts/gds/coreB-carve-proof.sh",
@@ -12952,7 +12957,7 @@
12952
12957
  {
12953
12958
  "path": "src/module-api.js",
12954
12959
  "mode": "0000644",
12955
- "sha256": "93da702f9872966c95f6453a31c64b87d41bf6d462058749b190b69bb86d15b5"
12960
+ "sha256": "2b61d02ebd3b94b723ccec869d7d6432ce9c035c5135259cf6e709759622f0af"
12956
12961
  },
12957
12962
  {
12958
12963
  "path": "src/module-loader/catalog.js",
@@ -13974,10 +13979,15 @@
13974
13979
  "mode": "0000644",
13975
13980
  "sha256": "4612301c02a9a6f9927a49f76a2b49d2892e8266580cf31bcd47dd5b36ad71b0"
13976
13981
  },
13982
+ {
13983
+ "path": "tests/core_origin_follow.mjs",
13984
+ "mode": "0000644",
13985
+ "sha256": "1b136186f5d91fe9f63034559115ba505959a742712e4103a143a7ede0917869"
13986
+ },
13977
13987
  {
13978
13988
  "path": "tests/core_pin_sync.mjs",
13979
13989
  "mode": "0000644",
13980
- "sha256": "ac3714b1b089ba2450f1125c74cbe5491deb4ddec1eec77c4c83a8a635a84d93"
13990
+ "sha256": "34d78bb4998adaec12f5666d4ac58815ebceaccff1d560ca978c58ba64190575"
13981
13991
  },
13982
13992
  {
13983
13993
  "path": "tests/core_update_banner.mjs",
package/bin/bongos.js CHANGED
@@ -195,8 +195,11 @@ function main(argv) {
195
195
  // of its own, so after a core update lands it resolves the main checkout's older core
196
196
  // and the ship fails on a bug the pin already fixed. `ship` installs the pinned core
197
197
  // into the checkout and re-runs under it; scripts/gds/core-pin-sync.js.
198
- if (!COMPILED && verb === 'ship') {
199
- const synced = require('../scripts/gds/core-pin-sync.js').ensurePinnedCore({ coreRoot: REPO_ROOT, argv });
198
+ // First, both `ship` and `start` (an instance's session start) follow a core move the
199
+ // hub pushed to GitHub: one line saying so, and on a clean main a fast-forward plus
200
+ // install (task 1004732; scripts/gds/core-origin-follow.js).
201
+ if (!COMPILED && (verb === 'ship' || verb === 'start')) {
202
+ const synced = require('../scripts/gds/core-origin-follow.js').ensureCurrentCore({ coreRoot: REPO_ROOT, argv, verb });
200
203
  if (synced != null) return synced;
201
204
  }
202
205
 
@@ -3131,5 +3131,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
3131
3131
  landed since 1.21.145 with no explicit bump. run 39077391288. (task 1002620)
3132
3132
  1.21.147 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3133
3133
  landed since 1.21.146 with no explicit bump. run 39274828902. (task 1002620)
3134
+ 1.21.148 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
3135
+ landed since 1.21.147 with no explicit bump. run 39286989989. (task 1002620)
3134
3136
  ---------------------------------------------------------------------------
3135
3137
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.147",
3
+ "version": "1.21.148",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.21.147",
9
+ "version": "1.21.148",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.21.147",
3
+ "version": "1.21.148",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -9896,5 +9896,11 @@
9896
9896
  "id": "1004731",
9897
9897
  "text": "After a project's software is updated, its owner now sees a one-time note saying which version it moved to, when, and what changed, and the Software update page lists the recent updates, including any that failed and were undo"
9898
9898
  }
9899
+ ],
9900
+ "1.21.148": [
9901
+ {
9902
+ "id": "1004732",
9903
+ "text": "When the hub upgrades a project's core, your laptop now notices at the next session start or ship, says so in one line, and on a clean main catches up and reinstalls the new core by itself."
9904
+ }
9899
9905
  ]
9900
9906
  }
@@ -0,0 +1,185 @@
1
+ // scripts/gds/core-origin-follow.js — the laptop follows a core move the hub made (task 1004732)
2
+ //
3
+ // WHY. When the hub moves a hosted project's core (provision-core-upgrade.js), it commits
4
+ // the new pin — package.json + lockfile, usually a vendored `file:vendor/bongos-core-X.tgz`
5
+ // rewritten to an exact registry version — on the box and pushes it to the project's
6
+ // GitHub repo. The owner's laptop checkout never hears of it: the next session runs the
7
+ // old core, and the next ship merges against the old pin. Seen on toast (1.21.20 on the
8
+ // laptop while fixes sat in newer releases).
9
+ //
10
+ // THE FIX. Before `bongos start` (an instance's session start: its CLAUDE.md opens every
11
+ // session with it, and a package-installed instance carries no SessionStart hooks) and
12
+ // before `bongos ship`, the dispatcher calls ensureCurrentCore(). It fetches origin/main
13
+ // (bounded; allowed to fail) and compares the checkout's @bongos/core pin with
14
+ // origin/main's. When ORIGIN moved it, the builder gets one plain line saying so. On a
15
+ // clean `main` with no local commits, the checkout is fast-forwarded and the pinned core
16
+ // is installed through core-pin-sync.js (task 1004632) — whose gates (the pin's shape,
17
+ // and the pin having landed on main) still decide what may be installed — then the
18
+ // command re-runs under the new core. A dirty tree, a feature branch or local commits get
19
+ // the line and the command to run, never a forced change. So does a move that is not
20
+ // pin-only (notPinOnly): the fast-forward installs and runs what it brings in, so it is
21
+ // taken only for the hub's commit shape — the pin, the lockfile, a removed tarball — onto
22
+ // a published registry release; anything wider waits for the owner to look.
23
+ //
24
+ // Silent and fail-open: in the core repo itself (the running package is not
25
+ // @bongos/core) it returns before any git spawn; outside git, with no pin, when origin
26
+ // cannot be read, or when the pins already agree it says nothing. A pin the LOCAL side
27
+ // moved (origin's pin is still the one this branch forked from) is not a hub move.
28
+
29
+ 'use strict';
30
+
31
+ const fs = require('node:fs');
32
+ const path = require('node:path');
33
+ const { spawnSync } = require('node:child_process');
34
+ const pinSync = require('./core-pin-sync.js');
35
+
36
+ const FETCH_TIMEOUT_MS = 5000;
37
+
38
+ function readJson(file, fsImpl = fs) {
39
+ try { return JSON.parse(String(fsImpl.readFileSync(file, 'utf8')).replace(/^/, '')); } catch { return null; }
40
+ }
41
+
42
+ function gitOut(args, cwd, run, extra = {}) {
43
+ try {
44
+ const r = run('git', args, { cwd, encoding: 'utf8', ...extra });
45
+ return r && r.status === 0 ? String(r.stdout || '').trim() : null;
46
+ } catch { return null; }
47
+ }
48
+
49
+ function specOf(pkg) {
50
+ if (!pkg) return null;
51
+ return (pkg.dependencies || {})[pinSync.CORE_PKG] || (pkg.devDependencies || {})[pinSync.CORE_PKG] || null;
52
+ }
53
+
54
+ // The core pin package.json carries at `rev`, or null when that file cannot be read there.
55
+ function specAt(root, rev, run) {
56
+ const body = gitOut(['show', `${rev}:package.json`], root, run, { maxBuffer: 8 * 1024 * 1024 });
57
+ if (body == null) return null;
58
+ try { return specOf(JSON.parse(body.replace(/^/, ''))); } catch { return null; }
59
+ }
60
+
61
+ const shown = (spec) => pinSync.versionFromSpec(spec) || spec;
62
+
63
+ // Why the move cannot be applied to this checkout automatically, or null when it can.
64
+ function blockedReason({ branch, dirty, ahead }) {
65
+ if (!branch) return 'this checkout is not on a branch';
66
+ if (branch !== 'main') return `you are on branch ${branch}, which picks the move up when it merges main`;
67
+ if (dirty == null || dirty !== '') return 'this checkout has uncommitted changes';
68
+ if (!Number.isFinite(ahead) || ahead !== 0) return 'main has local commits GitHub does not have';
69
+ return null;
70
+ }
71
+
72
+ const REMEDY = 'On a clean main: `git pull --ff-only origin main`, then `npm ci`.';
73
+ const REVIEW_REMEDY = 'Review what GitHub has (`git log HEAD..origin/main`), then `git pull --ff-only origin main` and `npm ci`.';
74
+ const REGISTRY = 'https://registry.npmjs.org/';
75
+ const EXACT_SEMVER = /^\d+\.\d+\.\d+(-[A-Za-z0-9.]+)?$/;
76
+
77
+ function jsonAt(root, rev, file, run) {
78
+ const body = gitOut(['show', `${rev}:${file}`], root, run, { maxBuffer: 64 * 1024 * 1024 });
79
+ if (body == null) return null;
80
+ try { return JSON.parse(body.replace(/^/, '')); } catch { return null; }
81
+ }
82
+
83
+ // The fast-forward installs and then RUNS what it brings in, unattended — so it is taken
84
+ // only when what GitHub has is the hub's pin move and nothing else (the shape
85
+ // provision-core-upgrade.js pushes). Anything wider is reported, never applied. Returns
86
+ // null when the move is pin-only, else the reason it is not.
87
+ // - the only files that differ are package.json, package-lock.json and a removed
88
+ // vendored tarball;
89
+ // - package.json differs ONLY in the @bongos/core spec;
90
+ // - the new spec is an exact version, i.e. a published release (a tarball brought in
91
+ // from GitHub is code nobody released);
92
+ // - the lockfile pins that version from the public registry, and every lockfile
93
+ // source it adds is the public registry too.
94
+ function notPinOnly(root, run) {
95
+ const diff = gitOut(['diff', '--name-status', '--no-renames', 'HEAD', 'origin/main'], root, run, { maxBuffer: 16 * 1024 * 1024 });
96
+ if (diff == null) return 'the change on GitHub could not be read';
97
+ for (const line of diff.split('\n').filter(Boolean)) {
98
+ const [status, file] = line.split('\t');
99
+ const ok = file === 'package.json' || file === 'package-lock.json' ||
100
+ (status === 'D' && /^vendor\/bongos-core-[^/]+\.tgz$/.test(file));
101
+ if (!ok) return 'GitHub has other changes besides the core move';
102
+ }
103
+ const before = jsonAt(root, 'HEAD', 'package.json', run);
104
+ const after = jsonAt(root, 'origin/main', 'package.json', run);
105
+ if (!before || !after) return 'package.json could not be read';
106
+ const spec = specOf(after);
107
+ const strip = (pkg) => {
108
+ const c = JSON.parse(JSON.stringify(pkg));
109
+ for (const k of ['dependencies', 'devDependencies']) if (c[k]) delete c[k][pinSync.CORE_PKG];
110
+ return JSON.stringify(c);
111
+ };
112
+ if (strip(before) !== strip(after)) return 'package.json changed beyond the core pin';
113
+ if (!EXACT_SEMVER.test(String(spec || ''))) return 'the new pin is not a published release';
114
+ const lock = jsonAt(root, 'origin/main', 'package-lock.json', run);
115
+ const entry = lock && lock.packages && lock.packages[`node_modules/${pinSync.CORE_PKG}`];
116
+ if (!entry || entry.version !== spec || !String(entry.resolved || '').startsWith(`${REGISTRY}${pinSync.CORE_PKG}/-/`)) {
117
+ return 'the lockfile does not pin that release from the public registry';
118
+ }
119
+ const prior = new Set(Object.values(((jsonAt(root, 'HEAD', 'package-lock.json', run) || {}).packages) || {}).map((p) => p && p.resolved));
120
+ for (const p of Object.values(lock.packages)) {
121
+ const r = p && p.resolved;
122
+ if (r && !prior.has(r) && !String(r).startsWith(REGISTRY)) return 'the lockfile adds a source outside the public registry';
123
+ }
124
+ return null;
125
+ }
126
+
127
+ // Returns { action: 'none' | 'reported' | 'fast-forwarded', reason, from?, to? }.
128
+ function followOriginPin({ coreRoot, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, fetchTimeoutMs = FETCH_TIMEOUT_MS } = {}) {
129
+ const none = (reason) => ({ action: 'none', reason });
130
+ if (env && (env.BONGOS_SKIP_CORE_SYNC || env[pinSync.SYNCED_ENV])) return none('skipped');
131
+ const running = readJson(path.join(coreRoot, 'package.json'), fsImpl);
132
+ if (!running || running.name !== pinSync.CORE_PKG) return none('core-repo'); // no git spawn
133
+ const top = gitOut(['rev-parse', '--show-toplevel'], cwd, run);
134
+ if (!top) return none('no-git');
135
+ const root = path.resolve(top);
136
+ const local = specOf(readJson(path.join(root, 'package.json'), fsImpl));
137
+ if (!local) return none('no-pin');
138
+
139
+ // Bounded, quiet, never prompts; a failure is fine — the ref below decides.
140
+ gitOut(['fetch', '--quiet', 'origin', 'main'], root, run, {
141
+ timeout: fetchTimeoutMs, env: { ...process.env, ...env, GIT_TERMINAL_PROMPT: '0' },
142
+ });
143
+ const origin = specAt(root, 'origin/main', run);
144
+ if (!origin) return none('origin-unreadable');
145
+ if (origin === local) return none('in-step');
146
+ const forkPoint = gitOut(['merge-base', 'HEAD', 'origin/main'], root, run);
147
+ if (forkPoint && specAt(root, forkPoint, run) === origin) return none('local-change');
148
+
149
+ const say = log || ((s) => process.stderr.write(s + '\n'));
150
+ const move = { from: local, to: origin };
151
+ say(`bongos: this project was upgraded to core ${shown(origin)} on GitHub (this checkout has ${shown(local)}).`);
152
+ const why = blockedReason({
153
+ branch: gitOut(['symbolic-ref', '--short', '-q', 'HEAD'], root, run),
154
+ dirty: gitOut(['status', '--porcelain', '--untracked-files=no'], root, run),
155
+ ahead: Number(gitOut(['rev-list', '--count', 'origin/main..HEAD'], root, run)),
156
+ });
157
+ if (why) {
158
+ say(` Not updated here: ${why}. ${REMEDY}`);
159
+ return { action: 'reported', reason: why, ...move };
160
+ }
161
+ const wider = notPinOnly(root, run);
162
+ if (wider) {
163
+ say(` Not updated automatically: ${wider}. ${REVIEW_REMEDY}`);
164
+ return { action: 'reported', reason: wider, ...move };
165
+ }
166
+ if (gitOut(['merge', '--ff-only', '--quiet', 'origin/main'], root, run) == null) {
167
+ say(` The fast-forward did not go through. ${REMEDY}`);
168
+ return { action: 'reported', reason: 'ff-failed', ...move };
169
+ }
170
+ say(' Fast-forwarded this checkout to it; installing that core next.');
171
+ return { action: 'fast-forwarded', reason: 'origin-moved', ...move };
172
+ }
173
+
174
+ // The dispatcher's one call, for `ship` and `start`. Returns an exit code when it handled
175
+ // the command (installed and re-ran, or refused), or null to run the verb as it is.
176
+ // `ship` always checks the running core against the pin (task 1004632); `start` only
177
+ // after a fast-forward actually moved the pin, so a normal session start pays no more
178
+ // than the fetch.
179
+ function ensureCurrentCore({ coreRoot, argv, verb, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
180
+ const followed = followOriginPin({ coreRoot, cwd, env, run, fsImpl, log });
181
+ if (verb !== 'ship' && followed.action !== 'fast-forwarded') return null;
182
+ return pinSync.ensurePinnedCore({ coreRoot, argv, cwd, env, run, fsImpl, log, ...(npmCli !== undefined ? { npmCli } : {}) });
183
+ }
184
+
185
+ module.exports = { followOriginPin, ensureCurrentCore };
@@ -172,6 +172,28 @@ function unlinkLink(linkPath, { platform = process.platform, fsImpl = fs } = {})
172
172
  }
173
173
  }
174
174
 
175
+ // A vendored instance keeps an untracked `scripts -> node_modules/@bongos/core/scripts`
176
+ // link in its main checkout (grading needs it; task 1004728). After an install it must
177
+ // still resolve (task 1004732): a link left dangling is re-pointed at the installed core.
178
+ // Only ever a LINK — a real scripts/ directory (the core repo) is never touched.
179
+ // Returns 'absent' | 'not-a-link' | 'ok' | 'repointed' | 'unresolved'.
180
+ function healScriptsLink(root, { fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), platform = process.platform } = {}) {
181
+ const link = path.join(root, 'scripts');
182
+ let st = null;
183
+ try { st = fsImpl.lstatSync(link); } catch { return 'absent'; }
184
+ if (!st.isSymbolicLink()) return 'not-a-link';
185
+ if (fsImpl.existsSync(path.join(link, 'gds'))) return 'ok';
186
+ const target = path.join(root, 'node_modules', '@bongos', 'core', 'scripts');
187
+ const fix = `bongos: the \`scripts\` link in ${root} no longer resolves; point it at ${target}.`;
188
+ if (!fsImpl.existsSync(target)) { log(fix); return 'unresolved'; }
189
+ try {
190
+ unlinkLink(link, { platform, fsImpl });
191
+ fsImpl.symlinkSync(target, link, 'junction');
192
+ } catch { log(fix); return 'unresolved'; }
193
+ log('bongos: re-pointed the `scripts` link at the installed core.');
194
+ return 'repointed';
195
+ }
196
+
175
197
  // Install the pinned core into `root` and re-run the same bongos command under it.
176
198
  // Returns the exit code to use.
177
199
  function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), env = process.env, platform = process.platform, npmCli = npmCliPath(process.execPath, fsImpl) } = {}) {
@@ -192,10 +214,13 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
192
214
  // npm's JS entry under this node — no shell. Fallback: `npm`, which on win32 is
193
215
  // npm.cmd and needs a shell (Node refuses a .cmd without one); the args are fixed
194
216
  // strings, never branch data.
217
+ // npm's output goes to stderr (task 1004732): `bongos start --widget` hands stdout to
218
+ // the renderer verbatim, so an install must never print there.
195
219
  const cli = npmCli;
220
+ const stdio = ['ignore', 2, 2];
196
221
  const inst = cli
197
- ? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio: 'inherit', env })
198
- : run('npm', installArgs(hasLock), { cwd: root, stdio: 'inherit', env, ...(platform === 'win32' ? { shell: true } : {}) });
222
+ ? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio, env })
223
+ : run('npm', installArgs(hasLock), { cwd: root, stdio, env, ...(platform === 'win32' ? { shell: true } : {}) });
199
224
  if (!inst || inst.error || inst.status !== 0) {
200
225
  log(`bongos: installing core ${pinned} failed.`);
201
226
  remedy();
@@ -203,6 +228,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
203
228
  }
204
229
  const bin = path.join(nm, '@bongos', 'core', 'bin', 'bongos.js');
205
230
  if (!fsImpl.existsSync(bin)) { remedy(); return 1; }
231
+ healScriptsLink(root, { fsImpl, log, platform });
206
232
  const res = run(process.execPath, [bin, ...argv.slice(2)], { stdio: 'inherit', env: { ...env, [SYNCED_ENV]: '1' } });
207
233
  if (!res || res.error) return 1;
208
234
  return res.status == null ? 1 : res.status;
@@ -210,7 +236,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
210
236
 
211
237
  // The dispatcher's one call. Returns an exit code when it handled the command (synced
212
238
  // and re-ran, or refused), or null to run the verb on the current core.
213
- function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log } = {}) {
239
+ function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
214
240
  if (env && env.BONGOS_SKIP_CORE_SYNC) return null;
215
241
  const runningPkg = readJson(path.join(coreRoot, 'package.json'), fsImpl);
216
242
  if (!runningPkg || runningPkg.name !== CORE_PKG) return null; // fast path: the core repo itself
@@ -227,7 +253,7 @@ function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.e
227
253
  say(remedyLine({ ...plan, hasLock: fsImpl.existsSync(path.join(root, 'package-lock.json')) }));
228
254
  return 1;
229
255
  }
230
- return syncAndRerun(plan, { argv, run, fsImpl, log: say, env });
256
+ return syncAndRerun(plan, { argv, run, fsImpl, log: say, env, ...(npmCli !== undefined ? { npmCli } : {}) });
231
257
  }
232
258
 
233
- module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, ensurePinnedCore };
259
+ module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, healScriptsLink, ensurePinnedCore };
package/src/module-api.js CHANGED
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
75
75
  // MAJOR (see allowBoxScope below): passes the request through untouched.
76
76
  function deprecatedNoopMiddleware(_req, _res, next) { next(); }
77
77
 
78
- const CORE_VERSION = '1.21.147'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
78
+ const CORE_VERSION = '1.21.148'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
79
79
 
80
80
  // A namespaced logger so a module's log lines are attributable + consistent.
81
81
  // Usage: const log = api.logger('discord'); log.info('mounted');
@@ -0,0 +1,287 @@
1
+ // tests/core_origin_follow.mjs — the laptop picks up a core move the hub made (task 1004732)
2
+ //
3
+ // The bug: when the hub moves a hosted project's core it commits the new pin on the box
4
+ // and pushes it to the project's GitHub repo. The owner's laptop checkout never heard of
5
+ // it, so the next session and the next ship ran — and merged against — the old pin.
6
+ //
7
+ // Each case is a REAL temp git setup: a bare "GitHub" origin, a "box" clone that makes
8
+ // the hub's pin commit and pushes it, and a "laptop" clone that is the checkout under
9
+ // test. npm is stubbed (it lays down a fake installed core); git is real.
10
+ //
11
+ // Run: node tests/core_origin_follow.mjs
12
+
13
+ import { strict as assert } from 'node:assert';
14
+ import { createRequire } from 'node:module';
15
+ import fs from 'node:fs';
16
+ import os from 'node:os';
17
+ import path from 'node:path';
18
+ import { spawnSync } from 'node:child_process';
19
+
20
+ const require = createRequire(import.meta.url);
21
+ const follow = require('../scripts/gds/core-origin-follow.js');
22
+ const pinSync = require('../scripts/gds/core-pin-sync.js');
23
+
24
+ let passed = 0;
25
+ let failed = 0;
26
+ function test(name, fn) {
27
+ try { fn(); passed++; console.log(` ok ${name}`); }
28
+ catch (err) { failed++; console.log(` FAIL ${name}`); console.log(` ${err.stack || err.message}`); }
29
+ }
30
+
31
+ function git(cwd, ...args) {
32
+ const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], { cwd, encoding: 'utf8' });
33
+ assert.equal(r.status, 0, `git ${args.join(' ')}: ${r.stderr}`);
34
+ return r.stdout;
35
+ }
36
+
37
+ const OLD = '1.21.20';
38
+ const NEW = '1.21.115';
39
+
40
+ function writePin(dir, spec, version) {
41
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ name: 'toast', private: true, dependencies: { '@bongos/core': spec } }, null, 2) + '\n');
42
+ fs.writeFileSync(path.join(dir, 'package-lock.json'), JSON.stringify({
43
+ name: 'toast', lockfileVersion: 3,
44
+ packages: { 'node_modules/@bongos/core': { version, resolved: spec.startsWith('file:') ? spec : `https://registry.npmjs.org/@bongos/core/-/core-${version}.tgz` } },
45
+ }, null, 2) + '\n');
46
+ }
47
+
48
+ // A fake installed core in `root`: what npm would leave behind.
49
+ function installCore(root, version) {
50
+ const core = path.join(root, 'node_modules', '@bongos', 'core');
51
+ fs.rmSync(path.join(root, 'node_modules'), { recursive: true, force: true });
52
+ fs.mkdirSync(path.join(core, 'bin'), { recursive: true });
53
+ fs.mkdirSync(path.join(core, 'scripts', 'gds'), { recursive: true });
54
+ fs.writeFileSync(path.join(core, 'package.json'), JSON.stringify({ name: '@bongos/core', version }));
55
+ fs.writeFileSync(path.join(core, 'bin', 'bongos.js'), '');
56
+ fs.writeFileSync(path.join(core, 'scripts', 'gds', 'fitness.js'), '');
57
+ return core;
58
+ }
59
+
60
+ // origin (bare) ← box pushes the hub's move; laptop is a clone made BEFORE the move.
61
+ // `move` makes the hub's commit: vendored tgz pin → exact registry pin, tgz removed.
62
+ function setup({ move = true, alsoOnBox = null } = {}) {
63
+ const base = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-'));
64
+ const origin = path.join(base, 'origin.git');
65
+ const box = path.join(base, 'box');
66
+ const laptop = path.join(base, 'laptop');
67
+ git(base, 'init', '-q', '--bare', '-b', 'main', origin);
68
+ fs.mkdirSync(box);
69
+ git(box, 'init', '-q', '-b', 'main');
70
+ writePin(box, `file:vendor/bongos-core-${OLD}.tgz`, OLD);
71
+ fs.mkdirSync(path.join(box, 'vendor'));
72
+ fs.writeFileSync(path.join(box, 'vendor', `bongos-core-${OLD}.tgz`), 'old-tarball');
73
+ fs.writeFileSync(path.join(box, '.gitignore'), 'node_modules/\n');
74
+ git(box, 'add', '-A');
75
+ git(box, 'commit', '-qm', 'init: vendored core');
76
+ git(box, 'remote', 'add', 'origin', origin);
77
+ git(box, 'push', '-q', 'origin', 'main');
78
+ git(base, 'clone', '-q', origin, laptop);
79
+ // The laptop has the old core installed and the untracked, excluded scripts link.
80
+ installCore(laptop, OLD);
81
+ fs.symlinkSync(path.join(laptop, 'node_modules', '@bongos', 'core', 'scripts'), path.join(laptop, 'scripts'), 'junction');
82
+ fs.appendFileSync(path.join(laptop, '.git', 'info', 'exclude'), 'scripts\n');
83
+ if (move) {
84
+ writePin(box, NEW, NEW);
85
+ git(box, 'rm', '-q', `vendor/bongos-core-${OLD}.tgz`);
86
+ if (alsoOnBox) alsoOnBox(box);
87
+ git(box, 'add', '-A');
88
+ git(box, 'commit', '-qm', `core: upgrade @bongos/core to ${NEW}`);
89
+ git(box, 'push', '-q', 'origin', 'main');
90
+ }
91
+ // The core the laptop runs (node_modules/@bongos/core) — the dispatcher's REPO_ROOT.
92
+ const coreRoot = path.join(laptop, 'node_modules', '@bongos', 'core');
93
+ return { base, origin, box, laptop, coreRoot };
94
+ }
95
+
96
+ // git → real; npm → lay down the pinned core; the re-run of bongos → recorded.
97
+ function stubRun(root) {
98
+ const calls = { installs: [], reruns: [], git: 0 };
99
+ const run = (cmd, args, opts) => {
100
+ if (cmd === 'git') { calls.git++; return spawnSync(cmd, args, opts); }
101
+ if (cmd === 'npm' || String(args[0] || '').endsWith('npm-cli.js')) {
102
+ calls.installs.push(cmd === 'npm' ? args : args.slice(1));
103
+ const lock = JSON.parse(fs.readFileSync(path.join(root, 'package-lock.json'), 'utf8'));
104
+ installCore(root, lock.packages['node_modules/@bongos/core'].version);
105
+ return { status: 0 };
106
+ }
107
+ if (String(args[0] || '').endsWith('bongos.js')) { calls.reruns.push(args.slice(1)); return { status: 0 }; }
108
+ return { status: 1 };
109
+ };
110
+ return { run, calls };
111
+ }
112
+
113
+ const pinOf = (dir) => JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8')).dependencies['@bongos/core'];
114
+ const head = (dir) => git(dir, 'rev-parse', 'HEAD').trim();
115
+
116
+ test('clean main: says the project moved, fast-forwards, reinstalls, re-runs, scripts link resolves', () => {
117
+ const { laptop, coreRoot } = setup();
118
+ const { run, calls } = stubRun(laptop);
119
+ const logs = [];
120
+ const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
121
+ assert.equal(code, 0, 'the re-run exit code: ' + logs.join('\n'));
122
+ assert.ok(logs.some((l) => l.includes(`upgraded to core ${NEW} on GitHub`) && l.includes(OLD)), logs.join('\n'));
123
+ assert.equal(pinOf(laptop), NEW, 'fast-forwarded onto the hub commit');
124
+ assert.equal(calls.installs.length, 1, 'npm ran once');
125
+ assert.equal(calls.installs[0][0], 'ci');
126
+ assert.deepEqual(calls.reruns, [['start']], 'start re-ran under the new core');
127
+ assert.ok(fs.existsSync(path.join(laptop, 'scripts', 'gds', 'fitness.js')), 'the scripts link still resolves');
128
+ const installed = JSON.parse(fs.readFileSync(path.join(laptop, 'node_modules', '@bongos', 'core', 'package.json'), 'utf8'));
129
+ assert.equal(installed.version, NEW);
130
+ });
131
+
132
+ test('vendored → registry pin: no tgz left referenced after the follow', () => {
133
+ const { laptop, coreRoot } = setup();
134
+ const { run } = stubRun(laptop);
135
+ follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'ship', '7'], verb: 'ship', cwd: laptop, env: {}, run, log: () => {}, npmCli: null });
136
+ assert.equal(pinOf(laptop), NEW);
137
+ for (const f of ['package.json', 'package-lock.json']) {
138
+ assert.ok(!/bongos-core-[\d.]+\.tgz/.test(fs.readFileSync(path.join(laptop, f), 'utf8')), `${f} still names a tarball`);
139
+ }
140
+ assert.ok(!fs.existsSync(path.join(laptop, 'vendor', `bongos-core-${OLD}.tgz`)), 'the hub removed the old tarball; the follow carried that');
141
+ });
142
+
143
+ test('dirty tree: reports the move only — no fast-forward, no install', () => {
144
+ const { laptop, coreRoot } = setup();
145
+ fs.writeFileSync(path.join(laptop, '.gitignore'), 'node_modules/\nlocal-edit\n');
146
+ const before = head(laptop);
147
+ const { run, calls } = stubRun(laptop);
148
+ const logs = [];
149
+ const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
150
+ assert.equal(code, null, 'carry on with the command');
151
+ assert.ok(logs.some((l) => l.includes(`upgraded to core ${NEW} on GitHub`)), logs.join('\n'));
152
+ assert.ok(logs.some((l) => /uncommitted/.test(l)), 'says why it was not applied: ' + logs.join('\n'));
153
+ assert.equal(head(laptop), before);
154
+ assert.equal(pinOf(laptop), `file:vendor/bongos-core-${OLD}.tgz`);
155
+ assert.equal(calls.installs.length, 0);
156
+ assert.match(fs.readFileSync(path.join(laptop, '.gitignore'), 'utf8'), /local-edit/, 'the edit is untouched');
157
+ });
158
+
159
+ test('feature branch: reports only, never moves the branch', () => {
160
+ const { laptop, coreRoot } = setup();
161
+ git(laptop, 'checkout', '-qb', 'task-49');
162
+ const before = head(laptop);
163
+ const { run, calls } = stubRun(laptop);
164
+ const logs = [];
165
+ const r = follow.followOriginPin({ coreRoot, cwd: laptop, env: {}, run, log: (s) => logs.push(s) });
166
+ assert.equal(r.action, 'reported');
167
+ assert.ok(logs.some((l) => l.includes('task-49')), logs.join('\n'));
168
+ assert.equal(head(laptop), before);
169
+ assert.equal(calls.installs.length, 0);
170
+ });
171
+
172
+ // The fast-forward installs and RUNS what it brings in, so only the hub's pin-only commit
173
+ // shape onto a published release is applied unattended; anything wider is reported.
174
+ function expectReportedOnly(alsoOnBox, why) {
175
+ const { laptop, coreRoot } = setup({ alsoOnBox });
176
+ const before = head(laptop);
177
+ const { run, calls } = stubRun(laptop);
178
+ const logs = [];
179
+ const code = follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s), npmCli: null });
180
+ assert.equal(code, null);
181
+ assert.ok(logs.some((l) => l.includes('upgraded to core') ), logs.join('\n'));
182
+ assert.ok(logs.some((l) => why.test(l)), `names why (${why}): ` + logs.join('\n'));
183
+ assert.equal(head(laptop), before, 'not fast-forwarded');
184
+ assert.equal(calls.installs.length, 0, 'nothing installed');
185
+ }
186
+
187
+ test('a move that carries other code too is reported, never applied', () => {
188
+ expectReportedOnly((box) => fs.writeFileSync(path.join(box, 'postinstall.js'), 'evil()'), /other changes besides the core move/);
189
+ });
190
+
191
+ test('a move whose package.json changes more than the core pin is reported, never applied', () => {
192
+ expectReportedOnly((box) => {
193
+ const p = JSON.parse(fs.readFileSync(path.join(box, 'package.json'), 'utf8'));
194
+ p.scripts = { prestart: 'evil' };
195
+ fs.writeFileSync(path.join(box, 'package.json'), JSON.stringify(p, null, 2) + '\n');
196
+ }, /package\.json changed beyond the core pin/);
197
+ });
198
+
199
+ test('a move onto a tarball from GitHub (not a published release) is reported, never applied', () => {
200
+ // the pin alone names a tarball (the spec gate) …
201
+ expectReportedOnly((box) => writePin(box, `file:vendor/bongos-core-${NEW}.tgz`, NEW), /not a published release/);
202
+ // … and a tarball file brought in with it is caught by the file gate first
203
+ expectReportedOnly((box) => {
204
+ writePin(box, `file:vendor/bongos-core-${NEW}.tgz`, NEW);
205
+ fs.mkdirSync(path.join(box, 'vendor'), { recursive: true });
206
+ fs.writeFileSync(path.join(box, 'vendor', `bongos-core-${NEW}.tgz`), 'unreleased');
207
+ }, /other changes besides the core move/);
208
+ });
209
+
210
+ test('a lockfile that resolves from outside the public registry is reported, never applied', () => {
211
+ expectReportedOnly((box) => {
212
+ const lock = JSON.parse(fs.readFileSync(path.join(box, 'package-lock.json'), 'utf8'));
213
+ lock.packages['node_modules/left-pad'] = { version: '1.0.0', resolved: 'https://evil.example/left-pad.tgz' };
214
+ fs.writeFileSync(path.join(box, 'package-lock.json'), JSON.stringify(lock, null, 2) + '\n');
215
+ }, /source outside the public registry/);
216
+ });
217
+
218
+ test('the core repo itself: a no-op, without a single git spawn', () => {
219
+ const { laptop } = setup();
220
+ const coreRepo = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-core-'));
221
+ fs.writeFileSync(path.join(coreRepo, 'package.json'), JSON.stringify({ name: 'cloud-bongos', version: '0.0.2' }));
222
+ const { run, calls } = stubRun(laptop);
223
+ const logs = [];
224
+ assert.equal(follow.ensureCurrentCore({ coreRoot: coreRepo, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
225
+ assert.equal(calls.git, 0);
226
+ assert.deepEqual(logs, []);
227
+ });
228
+
229
+ test('origin unreadable: silent no-op', () => {
230
+ const { laptop, coreRoot, origin } = setup();
231
+ // Drop the remote-tracking ref and point origin at nothing: the fetch fails and there is no origin/main.
232
+ git(laptop, 'update-ref', '-d', 'refs/remotes/origin/main');
233
+ git(laptop, 'remote', 'set-url', 'origin', origin + '-gone');
234
+ const before = head(laptop);
235
+ const { run, calls } = stubRun(laptop);
236
+ const logs = [];
237
+ assert.equal(follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
238
+ assert.deepEqual(logs, [], 'nothing said');
239
+ assert.equal(head(laptop), before);
240
+ assert.equal(calls.installs.length, 0);
241
+ });
242
+
243
+ test('no move on origin: silent, and start pays no install', () => {
244
+ const { laptop, coreRoot } = setup({ move: false });
245
+ const { run, calls } = stubRun(laptop);
246
+ const logs = [];
247
+ assert.equal(follow.ensureCurrentCore({ coreRoot, argv: ['node', 'bongos', 'start'], verb: 'start', cwd: laptop, env: {}, run, log: (s) => logs.push(s) }), null);
248
+ assert.deepEqual(logs, []);
249
+ assert.equal(calls.installs.length, 0);
250
+ });
251
+
252
+ test('a pin the LAPTOP moved (not pushed yet) is not reported as a hub move', () => {
253
+ const { laptop, coreRoot } = setup({ move: false });
254
+ writePin(laptop, '1.21.99', '1.21.99');
255
+ git(laptop, 'commit', '-qam', 'local pin bump');
256
+ const { run } = stubRun(laptop);
257
+ const logs = [];
258
+ const r = follow.followOriginPin({ coreRoot, cwd: laptop, env: {}, run, log: (s) => logs.push(s) });
259
+ assert.equal(r.action, 'none');
260
+ assert.deepEqual(logs, []);
261
+ });
262
+
263
+ test('a scripts link left dangling after the install is re-pointed at the installed core', () => {
264
+ const { laptop } = setup({ move: false });
265
+ const link = path.join(laptop, 'scripts');
266
+ fs.rmSync(link, { force: true, recursive: false });
267
+ const gone = path.join(laptop, 'old-core-scripts');
268
+ fs.mkdirSync(gone);
269
+ fs.symlinkSync(gone, link, 'junction');
270
+ fs.rmSync(gone, { recursive: true });
271
+ const logs = [];
272
+ assert.equal(pinSync.healScriptsLink(laptop, { log: (s) => logs.push(s) }), 'repointed');
273
+ assert.ok(fs.existsSync(path.join(link, 'gds', 'fitness.js')));
274
+ // a real scripts/ directory (the core repo shape) is never touched
275
+ const real = fs.mkdtempSync(path.join(os.tmpdir(), 'core-origin-follow-real-'));
276
+ fs.mkdirSync(path.join(real, 'scripts'));
277
+ assert.equal(pinSync.healScriptsLink(real, { log: () => {} }), 'not-a-link');
278
+ });
279
+
280
+ test('the dispatcher runs the follow before ship and start', () => {
281
+ const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
282
+ assert.match(src, /verb === 'ship' \|\| verb === 'start'/);
283
+ assert.match(src, /require\('\.\.\/scripts\/gds\/core-origin-follow\.js'\)\.ensureCurrentCore/);
284
+ });
285
+
286
+ console.log(`\n${passed} passed, ${failed} failed`);
287
+ if (failed) process.exit(1);
@@ -228,9 +228,14 @@ test('ensurePinnedCore end to end: deleting the lockfile or adding an .npmrc on
228
228
  }
229
229
  });
230
230
 
231
- test('the dispatcher runs the check for ship only', () => {
231
+ test('the dispatcher runs the check for ship (and start only after a followed core move)', () => {
232
+ // Since task 1004732 the dispatcher goes through core-origin-follow.ensureCurrentCore,
233
+ // which calls ensurePinnedCore for `ship` always and for `start` only after it
234
+ // fast-forwarded a hub pin move (tests/core_origin_follow.mjs drives both).
232
235
  const src = fs.readFileSync(new URL('../bin/bongos.js', import.meta.url), 'utf8');
233
- assert.match(src, /if \(!COMPILED && verb === 'ship'\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-pin-sync\.js'\)\.ensurePinnedCore/);
236
+ assert.match(src, /if \(!COMPILED && \(verb === 'ship' \|\| verb === 'start'\)\) \{\s*const synced = require\('\.\.\/scripts\/gds\/core-origin-follow\.js'\)\.ensureCurrentCore/);
237
+ const follow = fs.readFileSync(new URL('../scripts/gds/core-origin-follow.js', import.meta.url), 'utf8');
238
+ assert.match(follow, /if \(verb !== 'ship' && followed\.action !== 'fast-forwarded'\) return null;\s*return pinSync\.ensurePinnedCore/);
234
239
  });
235
240
 
236
241
  console.log(`\n${passed} passed, ${failed} failed`);