@bongos/core 1.19.576 → 1.19.577

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.19.576",
6
- "core_contract": "1.19.576",
7
- "source_commit": "a97463936d818b3a6be556837447748b3a2abaa1",
5
+ "core_version": "1.19.577",
6
+ "core_contract": "1.19.577",
7
+ "source_commit": "762e093b407c02f640fd7c4f28f35b4aba92b905",
8
8
  "source_ref": "HEAD",
9
- "built_at": "2026-09-07T14:24:36.180Z",
9
+ "built_at": "2026-09-07T16:20:42.564Z",
10
10
  "redaction": {
11
11
  "model": "docs-redacted+functional-verbatim",
12
- "docs_redacted": 450,
12
+ "docs_redacted": 451,
13
13
  "agent_docs_stubbed": 24,
14
- "functional_verbatim": 2044,
14
+ "functional_verbatim": 2046,
15
15
  "rules": 3,
16
16
  "gate_literals": 3,
17
17
  "gate": "passed"
18
18
  },
19
- "file_count": 2518,
20
- "tree_sha256": "9b56f557f5e1d8d63c314cb2990df89c75d7230c3f16bdd41416396d95e2b6bd",
19
+ "file_count": 2521,
20
+ "tree_sha256": "681bcb585fd2e75cf4e37463230354417f0f352734aa5057de24da8cfb8d8712",
21
21
  "files": [
22
22
  {
23
23
  "path": ".claude/skills/blocker-review/SKILL.md",
@@ -1804,10 +1804,15 @@
1804
1804
  "mode": "0000644",
1805
1805
  "sha256": "5ebfaa564a125f2c5bb0b391b8ff01914f147c15c9ef3f4caa91bafa8bd2e8ed"
1806
1806
  },
1807
+ {
1808
+ "path": "docs/adr/0258-the-public-cli-is-a-generated-client-package-not-the-published-core.md",
1809
+ "mode": "0000644",
1810
+ "sha256": "28f3644d3d9dcd2985b7e3f42cf2135af86203eee35b6e509d1ff98f630f1228"
1811
+ },
1807
1812
  {
1808
1813
  "path": "docs/adr/README.md",
1809
1814
  "mode": "0000644",
1810
- "sha256": "e9b47055a52c284035c201fae3f172215c746caabf938922821f4efcda2e8980"
1815
+ "sha256": "30bb35e65ebd2dc8c5eb0aad7e30d5875d0c764265f398547abd545b62ee7d7c"
1811
1816
  },
1812
1817
  {
1813
1818
  "path": "docs/api-reference.md",
@@ -2682,7 +2687,7 @@
2682
2687
  {
2683
2688
  "path": "docs/file-map.md",
2684
2689
  "mode": "0000644",
2685
- "sha256": "dd40cd29025789ec5a9447aa44ec6c7d3bee6aaae183c65713a6a3ca8ed8275b"
2690
+ "sha256": "f43e8946ecb5c96dc7af0bcee49010245df577eee62381701d2aef1c67455662"
2686
2691
  },
2687
2692
  {
2688
2693
  "path": "docs/handoff-template.md",
@@ -2692,7 +2697,7 @@
2692
2697
  {
2693
2698
  "path": "docs/module-api-changelog.md",
2694
2699
  "mode": "0000644",
2695
- "sha256": "8f122cc3f3ca35dcc6569b98bad34967f2a4bbf9ebcbc0e645ca2b1016245fbc"
2700
+ "sha256": "39e12981a4c704b133c9afc6c10cdb225fbe2cc882e667916eed2785ce0ae5ef"
2696
2701
  },
2697
2702
  {
2698
2703
  "path": "docs/modules-contract.md",
@@ -7552,12 +7557,12 @@
7552
7557
  {
7553
7558
  "path": "package-lock.json",
7554
7559
  "mode": "0000644",
7555
- "sha256": "8a58247d11bee28e30582d0860bd54cac15c8f3cb30753111063711a9369c390"
7560
+ "sha256": "b2a595c7b297963949c8f0c4d662c56d6ce4334a8f9e92231f47f0cd9233d624"
7556
7561
  },
7557
7562
  {
7558
7563
  "path": "package.json",
7559
7564
  "mode": "0000644",
7560
- "sha256": "e2f634e2e1deae3517a6b545c91a2b4818c51b04cb56daa4f1528b822cc1d14e"
7565
+ "sha256": "a2009a9bab221342b0b8b9c3c75a21d2ae02fb2fc281235f6fa7e17513a99ff8"
7561
7566
  },
7562
7567
  {
7563
7568
  "path": "public-docs/index.html",
@@ -7727,7 +7732,7 @@
7727
7732
  {
7728
7733
  "path": "scripts/gds/box-connect-lib.js",
7729
7734
  "mode": "0000644",
7730
- "sha256": "73172141b376910a6b6bc5a326e6bebae1fb8d0a80441e18c9f5f7acf689b67d"
7735
+ "sha256": "56c6f3eae23d427d9c74763521739bae5b704bcd02ad234e43a5434645657519"
7731
7736
  },
7732
7737
  {
7733
7738
  "path": "scripts/gds/box-infra.js",
@@ -7754,6 +7759,11 @@
7754
7759
  "mode": "0000644",
7755
7760
  "sha256": "6a696f9291da2d071d8a8aa0f92bf0be08445569a73196f9204f696bf6cb6cb5"
7756
7761
  },
7762
+ {
7763
+ "path": "scripts/gds/build-cli-package.js",
7764
+ "mode": "0000644",
7765
+ "sha256": "4b2428615e3c81c14a3b3144eca35fcb2c4523c4debef0f7d4596e5934857a6b"
7766
+ },
7757
7767
  {
7758
7768
  "path": "scripts/gds/bump-version.js",
7759
7769
  "mode": "0000644",
@@ -9257,7 +9267,7 @@
9257
9267
  {
9258
9268
  "path": "src/module-api.js",
9259
9269
  "mode": "0000644",
9260
- "sha256": "fb020c72dfb07b789b7e281ba766fe8f0f9c4fe67c359c8b5c9928c8e86b85da"
9270
+ "sha256": "a06af2e38ee9044c10f14f0ce1392ab85adb8058421222bf894f0f7ed6d77326"
9261
9271
  },
9262
9272
  {
9263
9273
  "path": "src/module-loader/catalog.js",
@@ -9839,6 +9849,11 @@
9839
9849
  "mode": "0000644",
9840
9850
  "sha256": "d0ea5b273c24527190f6ed01555ddb6b022a6ea94f3efcc7bdbd17f834c7d2ad"
9841
9851
  },
9852
+ {
9853
+ "path": "tests/cli_package.mjs",
9854
+ "mode": "0000644",
9855
+ "sha256": "a57516ed9e904cf1c86fe99688415f834dcae1a19f01b22e986f199fa5473258"
9856
+ },
9842
9857
  {
9843
9858
  "path": "tests/cli_token_reissue.mjs",
9844
9859
  "mode": "0000644",
@@ -0,0 +1,140 @@
1
+ # ADR 0258 — The public CLI is a generated client-only package, and its file list is proven by running it
2
+
3
+ - **Status:** accepted
4
+ - **Date:** 2026-09-07
5
+ - **Task:** [task 1003679](https://cloudbongos.com/builders#/task/1003679) (BONGOS-V1, goal 1000054 — *A newcomer can build without the UI*)
6
+ - **Deciders:** Claude, under the Archon's standing scope
7
+ - **Related:** [ADR 0083](<redacted>.md) (a module may import only `src/module-api.js` — the reason a static closure cannot answer this question) · [ADR 0099](<redacted>.md) (the lagged, redacted public mirror — still dormant) · [ADR 0108](<redacted>.md) (the core ships as a private versioned dependency) · [ADR 0118](<redacted>.md) (the generated client this package vendors) · [ADR 0161](<redacted>.md) (why the core's version moves many times a day)
8
+
9
+ ---
10
+
11
+ ## Context
12
+
13
+ The core ships as `@bongos/core`, which is **private**. That is deliberate (ADR 0108), and it
14
+ has an unintended consequence at the front door: a newcomer with no credential can install
15
+ nothing. There is no terminal path into an instance at all, so the web hall is the only way in.
16
+
17
+ The owner put it plainly: *"how am I supposed to easily take on tasks as a new builder? I should
18
+ be able to do everything without the UI."*
19
+
20
+ An open task, 1002025, proposed the obvious fix — publish the core to npm. That is the wrong
21
+ instrument on two counts. It publishes the **server**: routes, auth middleware, pool, migrations,
22
+ provisioning. And it bypasses ADR 0099's redaction pipeline, which is the mechanism that is
23
+ supposed to decide what leaves this repo and does not run until ~2026-11. The premise was also
24
+ stale (it named `@cloudbongos/core`; the package is `@bongos/core`).
25
+
26
+ Two refactors landed the night before this decision and are what make a narrower answer possible:
27
+ [task 1003676](https://cloudbongos.com/builders#/task/1003676) cut the one edge that dragged the
28
+ server into the CLI, and [task 1003677](https://cloudbongos.com/builders#/task/1003677) made
29
+ `src/module-api.js` resolve its kernel capabilities on read rather than on import.
30
+
31
+ ## Decision
32
+
33
+ **Generate a separate, public, client-only package — `@cloudbongos/cli` — from the core, and do
34
+ not publish the core.** `scripts/gds/build-cli-package.js` emits it; `tests/cli_package.mjs`
35
+ proves it.
36
+
37
+ Four parts of this are load-bearing.
38
+
39
+ ### 1. The name is scoped because the org already exists
40
+
41
+ The owner holds the npm org `cloudbongos`. On npm, org names and unscoped package names share one
42
+ namespace, so the bare name `cloudbongos` is **not publishable precisely because that org
43
+ exists** — npm reports it as invalid, which reads like the name is taken when in fact the owner
44
+ owns it. A public scoped package is free and `npx @cloudbongos/cli` is not a worse experience
45
+ than `npx cloudbongos`. (Registry checked 2026-09-07: `cloudbongos`, `@cloudbongos/cli` and
46
+ `@cloudbongos/core` all 404, search total 0.)
47
+
48
+ Related and worth writing down because it wasted the owner's time: **a package name cannot be
49
+ created or reserved on the npm website.** The name comes into existence on first `npm publish`.
50
+ Every field on the org settings pages operates on packages that already exist, which is why
51
+ "Add Existing Package" answered `Forbidden` for a package nobody had published.
52
+
53
+ ### 2. The file list is DECLARED, and proven by behaviour — not computed
54
+
55
+ The tempting approach is to derive the package from a static require-closure of the CLI entry
56
+ points. **That instrument does not work here, and cannot be made to.** `src/module-api.js` is the
57
+ published module doorway (ADR 0083): a module may import *only* it, so any script that touches a
58
+ module file statically reaches the doorway, and the doorway by design *names* every kernel
59
+ capability. A static walk therefore sees the whole server — 33 `src/` files from
60
+ `scripts/gds/start.js` alone — even though every one of those names is behind a lazy getter that
61
+ never resolves. Measured at runtime the same set is **one** `src/bongos/` file
62
+ (`api-prefix.js`, which imports nothing).
63
+
64
+ This is the same trap as task 1003677's original done-when ("zero `src/bongos` in the
65
+ require-closure"), which was unsatisfiable for exactly this reason: a getter still *contains* the
66
+ require text.
67
+
68
+ So `FILES` in the build script is an explicit, grouped, auditable list, and the test packs the
69
+ tarball, installs it into an empty directory **with no repo present**, and runs every advertised
70
+ verb. A verb may fail for want of a session or a network; it may not fail because a file is
71
+ missing. That test is the only thing that can prove the list complete, and it earned its keep
72
+ immediately — the first build shipped without `clients/bongos-client/index.mjs`, which
73
+ `cli-lib.js` reaches by a dynamic `import()` that no `require()`-based reasoning would ever see.
74
+ The build now refuses any relative `import()` target the manifest omits, and the generated
75
+ `package.json` `files` array is derived from the manifest rather than hand-listed (the hand-listed
76
+ one had already dropped `clients/`, producing a tarball that installed cleanly and died on first
77
+ use).
78
+
79
+ ### 3. `claim` and `ship` are absent, and the CLI says why
80
+
81
+ Writing code needs a real checkout and a worktree, so `claim`, `ship`, `dev`, `serve`, `module`,
82
+ `upgrade`, `onboard`, `doctor`, `exec` and `package-core` are not in the public CLI. A newcomer
83
+ who types one gets the reason and the next step — `bongos shell` opens a terminal on a cloud dev
84
+ box that already has the full CLI — rather than `unknown command`. A bare unknown-command error
85
+ teaches nothing, which is the precise failure this whole task exists to fix, so it would be
86
+ perverse to reintroduce it at the boundary.
87
+
88
+ The journey the package supports end to end is: `bongos login <instance>` → `bongos start` →
89
+ `bongos shell`. Steps one and two are what had no terminal path at all.
90
+
91
+ `onboard` is excluded for a second reason: it drags ten provisioning files that talk to
92
+ DigitalOcean and GitHub. It is owner tooling, and it is the widest redaction surface in the
93
+ candidate set.
94
+
95
+ ### 4. A redaction gate runs on every build, and it fails closed
96
+
97
+ The package is public, so the build scans the emitted tree and **refuses to emit** on a hit:
98
+ non-loopback IPv4 (RFC 5737 documentation ranges and link-local exempted — those can never name
99
+ real infrastructure, so they are the correct thing to write in an example), GitHub/npm token
100
+ shapes, private-key blocks, AWS keys, plus every domain and owner login it can read out of the
101
+ instance's own `config/branding.json`.
102
+
103
+ The needles come from host config rather than a list in the core, so the core carries no instance
104
+ identity (the ADR 0062 §7 split). A neutral core has no `config/branding.json`, so that half is
105
+ inert there — which is correct: the gate exists to stop someone building this package from an
106
+ *instance* checkout. `cloudbongos.com` is allowlisted as public by design, the way
107
+ `registry.npmjs.org` is baked into npm.
108
+
109
+ The generated `package.json` carries **no `repository` field**: the core repo is private, so a
110
+ repository URL would 404 for every user *and* put the owner's login into a public artifact for no
111
+ benefit. It gains one when ADR 0099's mirror lands.
112
+
113
+ ## Consequences
114
+
115
+ - 31 files, ~450 KB, 13 verbs, **one** runtime dependency (`undici`). Small enough to audit by
116
+ reading, which was the point.
117
+ - The package's version is independent of the core's. The core moves many times a day under
118
+ ADR 0161; a public CLI's contract should not.
119
+ - The generated tree is gitignored (`dist/`). It is rebuilt, never committed.
120
+ - **Publishing is the owner's action, not a builder's.** It is the owner's npm account, and a
121
+ public package name cannot be un-taken. The build stops at a packed tarball and hands over one
122
+ `npm publish --access public`.
123
+ - `@bongos/client` stays private and is **vendored** as a single file rather than declared as a
124
+ dependency — a public package that depended on a private one would be uninstallable for
125
+ everyone.
126
+ - Task 1002025 is superseded and should be closed against this ADR rather than done.
127
+
128
+ ## Rejected
129
+
130
+ - **Publishing the core** (task 1002025 as written) — ships the server and bypasses ADR 0099.
131
+ - **Deriving the package from a static require-closure** — structurally impossible past the
132
+ module doorway; see §2.
133
+ - **Waiting for the ADR 0099 mirror** — it is dormant until ~2026-11 and the front door is broken
134
+ now. The two are independent: this package is a build product, not a source release.
135
+ - **An unscoped `cloudbongos` package** — unpublishable while the owner's org of that name exists.
136
+ - **Depending on `@bongos/client`** — private, so the public package would not install.
137
+ - **Including `claim`/`ship` in a degraded form** — a half-working claim that cannot materialize
138
+ into a checkout is worse than one that explains where to go.
139
+ - **Shipping all of `src/`** to make lazy getters safe — reintroduces the server surface and the
140
+ redaction problem the narrow list exists to avoid.
@@ -349,3 +349,4 @@ This keeps the decision history honest and traceable.
349
349
 
350
350
  > ⚠️ **Numbering collisions are now machine-enforced, not narrated.** Twenty numbers are shared by two ADRs each — assigned in parallel sessions before anything checked. The files keep their filenames (renumbering would break every existing citation, and a number once assigned is never reused), and this table disambiguates each pair as `NNNN-a` / `NNNN-b`. The authoritative list is `LEGACY_DUPLICATE_ADRS` in [`scripts/gds/adr-namespace.js`](../../scripts/gds/adr-namespace.js), frozen by exact filename: a **new** collision — or a third file joining a legacy number, or a rename of either half — hard-fails the `unit` CI gate, as does an index row that points at no file or an ADR with no row. The list may only shrink. This paragraph used to narrate the collisions one by one and had fallen eight behind reality; see [ADR 0195](<redacted>.md) for why the check exists and why the pairs are grandfathered rather than renumbered.
351
351
  | 0257 | [**Auth resolves before the hall mounts anything, and a widget's boot read may never navigate** ([task 1003673](https://cloudbongos.com/builders#/task/1003673) · goal 1000063 — *The front door*). An invite-only instance could not admit its FIRST builder, and the symptom lied about where the fault was: an owner saw an empty Access-requests queue and no approve button, because **signing in does not file a request** — only the landing's *Request access* form does, and that form was unreachable. A signed-out visitor to `/builders` was bounced to GitHub, refused as a first-timer ("request access first"), and pointed back at `/builders` to be bounced again. Reproduced against `main`, not just an old pin. `builders.js` was already careful — `/me` is read `softAuth` and a 401 there renders the landing — but `DOMContentLoaded` called `mountHallWidgets()` FIRST, synchronously, and `goals.js`'s mount-time read of `/goals` goes through a `getJSON` with no `softAuth`, which answers 401 by NAVIGATING. The widget's read raced `renderLanding()` and won. **Decision: the front door settles which page this is before anything else runs** — `boot()` reads `/me` (soft), then either renders the landing and stops (nothing mounts, nothing else fetches) or mounts the hall, handing the resolved payload to `loadAll()` so the page still asks once. Widgets are member surfaces (`renderLanding` hid them all after the fact anyway), so not mounting them while signed out removes the CLASS rather than the one instance of it that was found; **a widget's own boot read may never navigate** is the belt beside it (`goals.js` reads soft and renders nothing without a session). A 401 during boot is not an instruction to go and sign in — only a user action is. `tests/hall_landing_boot.mjs` executes the real `builders.js` in a DOM stub and was confirmed to FAIL against the pre-fix file. Rejected: patching `goals.js` alone (it was merely first); making the 401 redirect soft everywhere (an expired session mid-visit SHOULD be sent to sign in — the line is boot vs user action); open enrollment as the cure (removes the gate [ADR 0050](<redacted>.md) chose deliberately instead of repairing the door).](<redacted>.md) | hall-ui / admission / front door |
352
+ | 0258 | [**The public CLI is a generated client-only package, and its file list is proven by running it** ([task 1003679](https://cloudbongos.com/builders#/task/1003679) · goal 1000054 — *A newcomer can build without the UI*). The core ships private as `@bongos/core` ([ADR 0108](<redacted>.md)), so a newcomer with no credential can install NOTHING and the web hall is the only way in — the owner's words: "how am I supposed to easily take on tasks as a new builder? I should be able to do everything without the UI." Open task 1002025 proposed publishing the core, which ships the SERVER and bypasses [ADR 0099](<redacted>.md)'s redaction pipeline (dormant until ~2026-11). **Decision: generate a separate public `@cloudbongos/cli` from the core and do not publish the core.** Four load-bearing parts. (1) SCOPED NAME, because npm shares one namespace between org names and unscoped packages — the bare `cloudbongos` is unpublishable *precisely because the owner owns that org*, which npm reports as "invalid" and reads like the name is taken; and a package name cannot be created on the npm website at all (it exists on first publish, which is why "Add Existing Package" answered `Forbidden`). (2) THE FILE LIST IS DECLARED AND PROVEN BY BEHAVIOUR, never computed — a static require-closure CANNOT answer this, because `src/module-api.js` is the doorway a module may only import ([ADR 0083](<redacted>.md)) and it *names* every kernel capability, so a static walk sees 33 `src/` files from `start.js` alone where runtime resolves **one** (`api-prefix.js`, which imports nothing) — the same trap as task 1003677's unsatisfiable done-when. So the test packs the tarball, installs it into an empty dir with NO repo, and runs every verb: it may fail for want of a session, never for a missing file. That caught two real holes on its first strengthened run — `clients/bongos-client/index.mjs`, reached by a dynamic `import()` no `require()` walk can see, and a hand-listed `files` array that dropped `clients/` and produced a tarball which installed cleanly then died on first use; both are now build-time refusals, and the `files` array is derived from the manifest. (3) `claim`/`ship`/`dev`/`serve`/`module`/`upgrade`/`onboard`/`doctor`/`exec`/`package-core` are ABSENT and the CLI says WHY plus the next step (`bongos shell` → a cloud box with the full CLI) — a bare `unknown command` teaches nothing, which is the exact failure being fixed. The supported journey is `login` → `start` → `shell`. (4) A REDACTION GATE fails the build closed on non-loopback IPv4 (RFC 5737 doc ranges exempt), token/key shapes, and every domain + owner login readable from the instance's own `config/branding.json` — needles come from host config so the core carries no instance identity ([ADR 0062 §7](<redacted>.md)); `cloudbongos.com` is allowlisted as public by design. No `repository` field while the core repo is private (it would 404 for every user and publish the owner's login for nothing). 31 files, one dependency (`undici`), version independent of the core's ([ADR 0161](<redacted>.md)). `@bongos/client` is VENDORED, not depended on — a public package depending on a private one is uninstallable. **The owner runs `npm publish`; a builder must not.** Task 1002025 is superseded. Rejected: publishing the core; deriving from a static closure (structurally impossible past the doorway); waiting for the mirror (dormant, and this is a build product not a source release); an unscoped name; depending on `@bongos/client`; a degraded `claim`; shipping all of `src/` to make lazy getters safe.](<redacted>.md) | cli / distribution / public surface |
package/docs/file-map.md CHANGED
@@ -167,6 +167,7 @@
167
167
  │ ├── ship.js ← resolve claim as shipped, award credits (`/builder-ship`); also uploads a secret-scrubbed session digest to the corpus (6D.1, ADR 0027); on a dev box runs the sandbox-first review gate before resolving (#927, ADR 0046)
168
168
  │ ├── ship-visual.js ← the OPTIONAL `--visual <image> [--visual-alt "…"]` leg of a ship (task 1003109): screens the file BEFORE the claim resolves (bad input costs no claim), uploads it AFTER (a failed picture never fails a ship)
169
169
  │ ├── package-core.js ← package the core as a versioned, installable artifact + pin manifest (`bongos package-core`; R84, ADR 0100 §1, [#1688](https://example.com/builders#/task/1688)): isPublishable() selection + mirror redaction + fail-closed no-leak gate → dist/bongos-core-<version>.{tgz,manifest.json}; version = src/module-api CORE_VERSION. Recipe: docs/recipes/packaging-the-core.md
170
+ │ ├── build-cli-package.js ← generate the PUBLIC, client-only `@cloudbongos/cli` npm package from this repo (task 1003679, ADR 0258): declared FILES manifest (no server/routes/auth/pool/provisioning) + generated dispatcher, README and package.json whose `files` array is DERIVED from the manifest; refuses to emit on a relative `import()` the manifest omits, and on a redaction hit (non-loopback IPv4, token/key shapes, instance domains + owner login read from config/branding.json). Sibling of package-core.js, opposite audience: that one packages the private core for an instance, this one packages the client surface for the public. Proven by tests/cli_package.mjs, which installs the packed tarball into an empty dir and runs every verb. `npm publish` is the OWNER's step, never a builder's → dist/cloudbongos-cli/
170
171
  │ ├── sandbox-stage.js ← stage the working tree on the live game preview (box OR local) for browser review before ship; resolvePreviewContext picks the context (`/builder-stage`; #927, ADR 0046; local: task 1056)
171
172
  │ ├── local-preview.js ← builder CLI for the LOCAL sandbox: start/stop/status/restart/logs/url of the game-only preview at http://localhost:3100 (`npm run preview`; task 1056)
172
173
  │ ├── local-preview-lib.js ← core of the local sandbox launcher (detached `node src/preview-server.js`, pid/health/port mgmt); shared by local-preview.js + sandbox-stage.js (task 1056)
@@ -1601,5 +1601,7 @@ is load-bearing: the script throws rather than guess if it is missing, and
1601
1601
  landed since 1.19.574 with no explicit bump. run 34076465465. (task 1002620)
1602
1602
  1.19.576 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1603
1603
  landed since 1.19.575 with no explicit bump. run 34132732488. (task 1002620)
1604
+ 1.19.577 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1605
+ landed since 1.19.576 with no explicit bump. run 34142767912. (task 1002620)
1604
1606
  ---------------------------------------------------------------------------
1605
1607
  ```
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.576",
3
+ "version": "1.19.577",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.576",
9
+ "version": "1.19.577",
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.19.576",
3
+ "version": "1.19.577",
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",
@@ -124,7 +124,7 @@ function knownHostsLineNamesAny(line, names) {
124
124
  // line for the same box.
125
125
  //
126
126
  // It used to compare field 1 to the hostname with `!==`, which missed the two forms
127
- // OpenSSH actually writes — `host,1.2.3.4` (what `StrictHostKeyChecking=accept-new`
127
+ // OpenSSH actually writes — `host,192.0.2.4` (what `StrictHostKeyChecking=accept-new`
128
128
  // records) and the hashed `|1|salt|hash`. Those lines survived the "replace", so
129
129
  // every re-pin ACCUMULATED another entry and the client kept verifying against the
130
130
  // first, now-wrong one: "Host denied (verification failed)" on a box that had just
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env node
2
+ // scripts/gds/build-cli-package.js — generate the public, client-only `@cloudbongos/cli`
3
+ // npm package from this repo (task 1003679, goal 1000054).
4
+ //
5
+ // WHY THIS EXISTS
6
+ // The core ships as `@bongos/core`, which is PRIVATE. That makes the front door unusable for
7
+ // anyone without a credential: a newcomer cannot install anything, so "take on a task" has no
8
+ // terminal path at all and the web hall is the only way in. This script cuts the CLI *surface*
9
+ // out of the core and emits a small, public, server-free package, so `npx @cloudbongos/cli
10
+ // login <instance>` works from a bare laptop.
11
+ //
12
+ // WHAT IT IS NOT
13
+ // It does NOT publish the core. No server, no routes, no auth middleware, no pool/db, no
14
+ // provisioning. Publishing the core would bypass the ADR 0099 redaction pipeline; this package
15
+ // carries a file list narrow enough to actually audit, and REDACTION_PATTERNS below fails the
16
+ // build if instance identity leaks into it.
17
+ //
18
+ // WHY THE FILE LIST IS DECLARED, NOT COMPUTED
19
+ // A static require-closure is unusable here. `src/module-api.js` is the published module doorway
20
+ // (ADR 0083) and by design *names* every kernel capability, so any static walk that reaches a
21
+ // module file appears to reach the whole server — even though the getters are lazy and never
22
+ // resolve. The honest instrument is behaviour: FILES is declared, and tests/cli_package.mjs packs
23
+ // the tarball, installs it into an empty directory with no repo present, and RUNS every verb.
24
+ // If a real code path needs a file that isn't here, that test fails with MODULE_NOT_FOUND.
25
+ //
26
+ // Usage:
27
+ // node scripts/gds/build-cli-package.js [--out dist/cloudbongos-cli] [--version 0.1.0] [--quiet]
28
+
29
+ 'use strict';
30
+
31
+ const fs = require('node:fs');
32
+ const path = require('node:path');
33
+
34
+ const REPO_ROOT = path.join(__dirname, '..', '..');
35
+
36
+ // The package's OWN version — deliberately independent of the core's. The core moves many times
37
+ // a day (CI auto-patch, ADR 0161); the CLI's public contract should not.
38
+ const PACKAGE_VERSION = '0.1.0';
39
+ const PACKAGE_NAME = '@cloudbongos/cli';
40
+
41
+ // ── What the package carries ────────────────────────────────────────────────────────────────
42
+ // Every entry is repo-relative and copied verbatim. Grouped by why it's here, because the
43
+ // grouping is the audit: anything that doesn't fit a group below does not belong in a public
44
+ // client package.
45
+ const FILES = [
46
+ // The shared CLI plumbing every verb goes through: session file, HTTP, error shapes.
47
+ 'scripts/gds/cli-lib.js',
48
+ 'scripts/gds/api.js',
49
+
50
+ // Account + sign-in. login.js is the whole point — GitHub device flow, no paste, no credential.
51
+ 'scripts/gds/login.js',
52
+ 'scripts/gds/reauth.js',
53
+ 'scripts/gds/setup.js',
54
+ 'scripts/gds/onboarding-config.js', // lazy dep of setup.js
55
+ 'scripts/gds/install-git-hooks.js', // lazy dep of setup.js
56
+ 'scripts/gds/preflight.js',
57
+
58
+ // Read the board and move your own work along.
59
+ 'scripts/gds/start.js',
60
+ 'scripts/gds/context-pack.js', // start.js renders through this
61
+ 'scripts/gds/status.js',
62
+ 'scripts/gds/task.js',
63
+ 'scripts/gds/recall.js',
64
+ 'scripts/gds/cost.js',
65
+ 'scripts/gds/release.js',
66
+
67
+ // Get onto a real environment without installing one: the cloud dev box.
68
+ 'scripts/gds/shell.js',
69
+ 'scripts/gds/code.js',
70
+ 'scripts/gds/box-connect-lib.js',
71
+
72
+ // Module files the above touch. Each imports nothing from the kernel at load time.
73
+ 'modules/lifecycle/task-classifier.js',
74
+ 'modules/lifecycle/task-visuals.js',
75
+ 'modules/economy/reward-policy.js',
76
+
77
+ // The only core files in the package: config/branding readers and one constant table.
78
+ // src/module-api.js is here because task-classifier.js reads branding through the doorway
79
+ // (ADR 0083 forbids a module importing src/branding directly). Its `branding` getter resolves
80
+ // against src/branding.js, which ships; the other getters point at server files that do not,
81
+ // and no client path touches them — the smoke test is what proves that.
82
+ 'src/branding.js',
83
+ 'src/instance-config.js',
84
+ 'src/module-seams.js',
85
+ 'src/module-api.js',
86
+ 'src/bongos/api-prefix.js',
87
+
88
+ // The generated typed API client (ADR 0118), which cli-lib.js reaches by a dynamic
89
+ // `import()` of this exact path. Vendored as a single file rather than depended on: it is
90
+ // published as `@bongos/client`, which is PRIVATE, so a public package that declared it as a
91
+ // dependency would be uninstallable for everyone. index.mjs is self-contained and imports
92
+ // nothing; the sibling package.json is deliberately NOT shipped (it carries
93
+ // publishConfig.access=restricted and a nested manifest only confuses packing).
94
+ 'clients/bongos-client/index.mjs',
95
+
96
+ // Neutral branding defaults, so the CLI has sane strings with no instance checkout present.
97
+ 'config/branding.neutral.json',
98
+ ];
99
+
100
+ // Paths that must NEVER appear in the generated tree, whatever FILES says. A belt-and-braces
101
+ // assertion against a future edit quietly widening the package back into the server.
102
+ const FORBIDDEN_PREFIXES = [
103
+ 'src/bongos/routes/',
104
+ 'src/server',
105
+ 'migrations/',
106
+ 'infra/',
107
+ 'public/',
108
+ 'modules/hall-ui/',
109
+ '.github/',
110
+ 'devbox-app/',
111
+ ];
112
+ const FORBIDDEN_BASENAMES = [
113
+ 'pool.js', 'auth.js', 'db.js', 'server.js', 'serve-internal.js',
114
+ 'logger.js', 'project-door.js', 'project-settings.js',
115
+ ];
116
+ // src/bongos/ is the kernel. Exactly one file from it is allowed, and it imports nothing.
117
+ const ALLOWED_SRC_BONGOS = new Set(['src/bongos/api-prefix.js']);
118
+
119
+ // ── The verbs the package advertises ────────────────────────────────────────────────────────
120
+ // `script` is resolved inside the package. Keep in lockstep with FILES.
121
+ const VERBS = {
122
+ login: { script: 'login.js', group: 'account', summary: 'Sign in to an instance (bongos login <url> — browser click, no paste)' },
123
+ reauth: { script: 'reauth.js', group: 'account', summary: 'Refresh your session' },
124
+ setup: { script: 'setup.js', group: 'account', summary: 'Finish builder setup on this instance (disciplines, consent)' },
125
+ box: { script: 'api.js', prefixArgs: ['POST', '/api/gds/box/ensure'], group: 'account', summary: 'Turn on / wake your cloud dev box' },
126
+ shell: { script: 'shell.js', group: 'account', summary: 'Open a terminal on your dev box' },
127
+ code: { script: 'code.js', group: 'account', summary: 'Open VS Code on your dev box over Remote-SSH' },
128
+
129
+ start: { script: 'start.js', group: 'work', summary: 'List tasks you can claim right now' },
130
+ status: { script: 'status.js', group: 'work', summary: 'Version / criterion progress (bongos status C8)', selfHelp: true },
131
+ task: { script: 'task.js', group: 'work', summary: 'Create or show a task', selfHelp: true },
132
+ recall: { script: 'recall.js', group: 'work', summary: 'Search the docs + project knowledge (bongos recall "x")' },
133
+ cost: { script: 'cost.js', group: 'work', summary: 'Log a cost entry' },
134
+ release: { script: 'release.js', group: 'work', summary: 'Cancel a claim — task returns to ready' },
135
+
136
+ api: { script: 'api.js', group: 'work', summary: 'Call any instance endpoint (bongos api GET /api/gds/me)' },
137
+ };
138
+
139
+ // Verbs that exist in the full core CLI and are deliberately absent here. The dispatcher prints
140
+ // the reason and the actual next step, because a newcomer hitting a bare "unknown command" learns
141
+ // nothing — that silence is the exact failure this whole task is fixing.
142
+ const CHECKOUT_ONLY = {
143
+ claim: 'Claiming a task writes code, so it needs a real checkout and a worktree.',
144
+ ship: 'Shipping grades, merges and deploys from a real checkout.',
145
+ dev: 'Runs an instance locally — needs the instance repo.',
146
+ serve: 'Runs an instance on a host — needs the instance repo.',
147
+ module: 'Scaffolds a module into an instance repo.',
148
+ upgrade: 'Moves an instance to a new core version.',
149
+ onboard: 'Provisions new infrastructure — owner tooling, not in the public CLI.',
150
+ doctor: 'Checks a local checkout toolchain.',
151
+ exec: 'Runs a core script from inside the core package.',
152
+ 'package-core': 'Packages the core — owner tooling.',
153
+ };
154
+
155
+ // ── Redaction gate ──────────────────────────────────────────────────────────────────────────
156
+ // Generic shapes only. Instance-specific needles are discovered at build time from the host's
157
+ // own config (below) so the core itself carries no instance identity — the ADR 0062 §7 split.
158
+ const REDACTION_PATTERNS = [
159
+ { name: 'non-loopback IPv4', re: /\b(?!127\.|0\.0\.0\.0|255\.)(?:\d{1,3}\.){3}\d{1,3}\b/g },
160
+ { name: 'github token', re: /\bgh[pousr]_[A-Za-z0-9]{16,}/g },
161
+ { name: 'github fine-grained token', re: /\bgithub_pat_[A-Za-z0-9_]{20,}/g },
162
+ { name: 'npm token', re: /\bnpm_[A-Za-z0-9]{30,}/g },
163
+ { name: 'private key block', re: /-----BEGIN [A-Z ]*PRIVATE KEY-----/g },
164
+ { name: 'aws access key', re: /\bAKIA[0-9A-Z]{16}\b/g },
165
+ ];
166
+
167
+ // Version strings (1.19.576) and dotted identifiers look like IPv4 to a loose regex; a real
168
+ // address has four all-numeric octets each <= 255 and is not a semver.
169
+ //
170
+ // The documentation ranges (RFC 5737) and link-local are exempt: they can never name real
171
+ // infrastructure, so they are the CORRECT thing to write in an example. Nothing else is
172
+ // exempt — private ranges included, because an internal address still leaks topology and
173
+ // deserves a human look before it goes public.
174
+ const DOC_IPV4 = [/^192\.0\.2\./, /^198\.51\.100\./, /^203\.0\.113\./, /^169\.254\./];
175
+
176
+ function isRealIPv4(s) {
177
+ const parts = s.split('.');
178
+ if (parts.length !== 4) return false;
179
+ if (!parts.every((p) => /^\d{1,3}$/.test(p) && Number(p) <= 255)) return false;
180
+ return !DOC_IPV4.some((re) => re.test(s));
181
+ }
182
+
183
+ // The project's own public identity. `cloudbongos.com` is the flagship instance and the CLI's
184
+ // documented default — the way `registry.npmjs.org` is baked into npm — so it is published on
185
+ // purpose and must not trip the gate. Anything NOT on this list that comes out of an instance's
186
+ // branding is treated as private identity and blocks the build.
187
+ const PUBLIC_BY_DESIGN = new Set(['cloudbongos.com']);
188
+
189
+ // Instance identity to refuse: the host's own domains and owner login, read from the instance's
190
+ // committed branding if this checkout has one. Absent (a neutral core), the gate still runs the
191
+ // generic patterns above.
192
+ function instanceNeedles() {
193
+ const needles = new Set();
194
+ const add = (v) => {
195
+ if (typeof v !== 'string') return;
196
+ const s = v.trim().toLowerCase();
197
+ if (s.length > 3 && !PUBLIC_BY_DESIGN.has(s)) needles.add(s);
198
+ };
199
+ try {
200
+ const b = JSON.parse(fs.readFileSync(path.join(REPO_ROOT, 'config', 'branding.json'), 'utf8'));
201
+ for (const v of Object.values(b.domains || {})) add(String(v).replace(/^https?:\/\//, '').replace(/\/.*$/, ''));
202
+ add(b.identity && b.identity.ownerLogin);
203
+ add(b.repo && b.repo.owner);
204
+ } catch (_) { /* neutral core: no instance branding, nothing to redact */ }
205
+ for (const extra of String(process.env.CLI_PACKAGE_FORBIDDEN || '').split(',')) add(extra);
206
+ return [...needles];
207
+ }
208
+
209
+ function scanRedaction(outDir, files) {
210
+ const needles = instanceNeedles();
211
+ const hits = [];
212
+ for (const rel of files) {
213
+ const abs = path.join(outDir, rel);
214
+ let text;
215
+ try { text = fs.readFileSync(abs, 'utf8'); } catch { continue; }
216
+ for (const { name, re } of REDACTION_PATTERNS) {
217
+ for (const m of text.matchAll(re)) {
218
+ if (name === 'non-loopback IPv4' && !isRealIPv4(m[0])) continue;
219
+ const line = text.slice(0, m.index).split('\n').length;
220
+ hits.push(`${rel}:${line} ${name}: ${m[0]}`);
221
+ }
222
+ }
223
+ const lower = text.toLowerCase();
224
+ for (const n of needles) {
225
+ let i = lower.indexOf(n);
226
+ while (i !== -1) {
227
+ hits.push(`${rel}:${text.slice(0, i).split('\n').length} instance identity: ${n}`);
228
+ i = lower.indexOf(n, i + n.length);
229
+ }
230
+ }
231
+ }
232
+ return hits;
233
+ }
234
+
235
+ // ── Generated files ─────────────────────────────────────────────────────────────────────────
236
+
237
+ function dispatcherSource() {
238
+ const verbs = JSON.stringify(VERBS, null, 2).replace(/\n/g, '\n');
239
+ const checkout = JSON.stringify(CHECKOUT_ONLY, null, 2).replace(/\n/g, '\n');
240
+ return `#!/usr/bin/env node
241
+ // bin/bongos.js — the public Cloud Bongos CLI.
242
+ //
243
+ // GENERATED by scripts/gds/build-cli-package.js in the Cloud Bongos core. Do not edit by hand:
244
+ // edit the generator and rebuild.
245
+ //
246
+ // A thin dispatcher over the client scripts in this package. It spawns each with process.execPath
247
+ // and forwards argv + the exit code; no auth or API logic lives here (that is cli-lib.js).
248
+
249
+ 'use strict';
250
+
251
+ const path = require('node:path');
252
+ const { spawnSync } = require('node:child_process');
253
+
254
+ const PKG_ROOT = path.join(__dirname, '..');
255
+ const SCRIPTS_DIR = path.join(PKG_ROOT, 'scripts', 'gds');
256
+ const VERSION = require('../package.json').version;
257
+
258
+ const VERBS = ${verbs};
259
+
260
+ const CHECKOUT_ONLY = ${checkout};
261
+
262
+ const GROUPS = [['account', 'Account & environment'], ['work', 'Work']];
263
+
264
+ function helpText() {
265
+ const lines = ['bongos — the Cloud Bongos CLI', '', 'Usage: bongos <command> [args]'];
266
+ lines.push('', 'First time here?');
267
+ lines.push(' bongos login https://your-instance.com sign in with a browser click');
268
+ lines.push(' bongos start see what you can pick up');
269
+ for (const [key, label] of GROUPS) {
270
+ lines.push('', label + ':');
271
+ for (const [verb, def] of Object.entries(VERBS)) {
272
+ if (def.group !== key) continue;
273
+ lines.push(' ' + verb.padEnd(10) + ' ' + def.summary);
274
+ }
275
+ }
276
+ lines.push('', ' help Show this help', ' version Show the bongos version');
277
+ lines.push('');
278
+ lines.push('Writing code (claim, ship) happens in a checkout. \`bongos shell\` opens one');
279
+ lines.push('on a cloud dev box with nothing to install locally.');
280
+ return lines.join('\\n');
281
+ }
282
+
283
+ function verbHelpText(verb, def) {
284
+ return [
285
+ 'bongos ' + verb + ' — ' + def.summary,
286
+ '',
287
+ 'Forwards to scripts/gds/' + def.script + '; extra arguments are passed through.',
288
+ 'Run \`bongos help\` to see every command.',
289
+ ].join('\\n');
290
+ }
291
+
292
+ // Returns the exit code (does not call process.exit, so it stays testable).
293
+ function main(argv) {
294
+ const args = argv.slice(2);
295
+ const verb = args[0];
296
+
297
+ if (!verb || verb === 'help' || verb === '--help' || verb === '-h') {
298
+ process.stdout.write(helpText() + '\\n');
299
+ return 0;
300
+ }
301
+ if (verb === 'version' || verb === '--version' || verb === '-v') {
302
+ process.stdout.write('bongos ' + VERSION + '\\n');
303
+ return 0;
304
+ }
305
+
306
+ if (CHECKOUT_ONLY[verb]) {
307
+ process.stderr.write(
308
+ 'bongos: "' + verb + '" is not in the public CLI.\\n ' + CHECKOUT_ONLY[verb] + '\\n\\n' +
309
+ 'To get a checkout without installing anything locally:\\n' +
310
+ ' bongos shell open a terminal on your cloud dev box\\n' +
311
+ ' bongos code open VS Code on it over Remote-SSH\\n\\n' +
312
+ 'The full CLI (including ' + verb + ') is already installed inside that box.\\n'
313
+ );
314
+ return 2;
315
+ }
316
+
317
+ const def = VERBS[verb];
318
+ if (!def) {
319
+ process.stderr.write('bongos: unknown command "' + verb + '"\\n\\n' + helpText() + '\\n');
320
+ return 2;
321
+ }
322
+
323
+ if (!def.selfHelp && args.slice(1).some((a) => a === '--help' || a === '-h')) {
324
+ process.stdout.write(verbHelpText(verb, def) + '\\n');
325
+ return 0;
326
+ }
327
+
328
+ const target = path.join(SCRIPTS_DIR, def.script);
329
+ const spawnArgs = [target, ...(def.prefixArgs || []), ...args.slice(1)];
330
+ const res = spawnSync(process.execPath, spawnArgs, { stdio: 'inherit' });
331
+ if (res.error) {
332
+ process.stderr.write('bongos: could not run ' + def.script + ': ' + res.error.message + '\\n');
333
+ return 1;
334
+ }
335
+ return res.status == null ? 1 : res.status;
336
+ }
337
+
338
+ if (require.main === module) process.exit(main(process.argv));
339
+ module.exports = { main, helpText, VERBS, CHECKOUT_ONLY };
340
+ `;
341
+ }
342
+
343
+ function packageJsonSource(version) {
344
+ // DERIVED from the manifest, never hand-listed. A hand-written `files` array silently drops
345
+ // whatever it forgets: the first build of this package copied clients/bongos-client/index.mjs
346
+ // correctly and then `npm pack` left it out, so the tarball installed and died on first use.
347
+ const roots = [...new Set(FILES.map((f) => f.split('/')[0] + '/'))].sort();
348
+ return JSON.stringify({
349
+ name: PACKAGE_NAME,
350
+ version,
351
+ description: 'The Cloud Bongos CLI — sign in to an instance, see what you can build, and get onto a dev box. Client only: no server.',
352
+ bin: { bongos: 'bin/bongos.js' },
353
+ files: ['bin/', ...roots, 'README.md'],
354
+ engines: { node: '>=20' },
355
+ dependencies: { undici: '^6.19.8' },
356
+ keywords: ['cloud-bongos', 'cli', 'ai-agents', 'build-platform'],
357
+ license: 'AGPL-3.0-or-later',
358
+ // No `repository` field on purpose. The core repo is private today, so a repository URL
359
+ // would 404 for every user of a public package AND put the owner's login in it for nothing.
360
+ // Point it at the redacted public mirror once that lands (ADR 0099).
361
+ homepage: 'https://cloudbongos.com',
362
+ bugs: { url: 'https://cloudbongos.com/builders' },
363
+ publishConfig: { access: 'public' },
364
+ }, null, 2) + '\n';
365
+ }
366
+
367
+ function readmeSource(version) {
368
+ return `# ${PACKAGE_NAME}
369
+
370
+ The [Cloud Bongos](https://cloudbongos.com) CLI. Sign in to an instance, see what you can pick
371
+ up, and get onto a dev box — from a terminal, with nothing installed.
372
+
373
+ ## Start here
374
+
375
+ \`\`\`sh
376
+ npx ${PACKAGE_NAME} login https://your-instance.com
377
+ npx ${PACKAGE_NAME} start
378
+ \`\`\`
379
+
380
+ \`login\` opens your browser, you click once, and the session is written to
381
+ \`~/.config/cloudbongos/\`. There is no token to copy and no password to set. If the instance
382
+ gates admission, \`login\` files your access request and the owner approves it.
383
+
384
+ Install it properly once you're past the first run:
385
+
386
+ \`\`\`sh
387
+ npm install -g ${PACKAGE_NAME}
388
+ bongos start
389
+ \`\`\`
390
+
391
+ ## Commands
392
+
393
+ Run \`bongos help\` for the current list. In short: \`login\`, \`reauth\`, \`setup\`, \`box\`,
394
+ \`shell\`, \`code\` for your account and environment; \`start\`, \`status\`, \`task\`, \`recall\`,
395
+ \`cost\`, \`release\`, \`api\` for the work.
396
+
397
+ \`bongos api\` is the escape hatch — it calls any endpoint on the instance as you, so anything
398
+ the web hall can do is reachable from the terminal:
399
+
400
+ \`\`\`sh
401
+ bongos api GET /api/gds/me
402
+ \`\`\`
403
+
404
+ ## Writing code
405
+
406
+ \`claim\` and \`ship\` need a real checkout, so they aren't in this package. The shortest path to
407
+ one is a cloud dev box, which needs nothing on your machine:
408
+
409
+ \`\`\`sh
410
+ bongos box # turn it on
411
+ bongos shell # a terminal on it, full CLI already installed
412
+ bongos code # or VS Code over Remote-SSH
413
+ \`\`\`
414
+
415
+ ## What this package is
416
+
417
+ The client surface of the Cloud Bongos core, and nothing else — no server, no routes, no
418
+ database, no provisioning. It is generated from the core by
419
+ \`scripts/gds/build-cli-package.js\` and verified by installing the packed tarball into an empty
420
+ directory and running every command.
421
+
422
+ Version ${version} · AGPL-3.0-or-later
423
+ `;
424
+ }
425
+
426
+ // ── Build ───────────────────────────────────────────────────────────────────────────────────
427
+
428
+ function build(opts = {}) {
429
+ const outDir = path.resolve(REPO_ROOT, opts.out || 'dist/cloudbongos-cli');
430
+ const version = opts.version || PACKAGE_VERSION;
431
+ const log = opts.quiet ? () => {} : (m) => process.stdout.write(m + '\n');
432
+
433
+ // Refuse a file list that contradicts the package's whole premise, before copying anything.
434
+ const violations = [];
435
+ for (const rel of FILES) {
436
+ if (rel.startsWith('src/bongos/') && !ALLOWED_SRC_BONGOS.has(rel)) {
437
+ violations.push(`${rel} — src/bongos/ is the kernel; only ${[...ALLOWED_SRC_BONGOS].join(', ')} may ship`);
438
+ }
439
+ if (FORBIDDEN_PREFIXES.some((p) => rel.startsWith(p))) violations.push(`${rel} — forbidden path prefix`);
440
+ if (FORBIDDEN_BASENAMES.includes(path.basename(rel)) && !ALLOWED_SRC_BONGOS.has(rel)) {
441
+ violations.push(`${rel} — forbidden filename (server surface)`);
442
+ }
443
+ if (!fs.existsSync(path.join(REPO_ROOT, rel))) violations.push(`${rel} — missing from the repo`);
444
+ }
445
+ if (violations.length) {
446
+ const err = new Error('cli package manifest rejected:\n ' + violations.join('\n '));
447
+ err.violations = violations;
448
+ throw err;
449
+ }
450
+
451
+ // Dynamic `import()` of a relative path is invisible to any require()-based reasoning, and
452
+ // that is not hypothetical: cli-lib.js reaches the generated API client that way, and the
453
+ // first build shipped without it. Every relative import target in a shipped file must itself
454
+ // be shipped, or the package installs fine and dies on first use.
455
+ const unshipped = [];
456
+ for (const rel of FILES) {
457
+ if (!/\.(js|mjs|cjs)$/.test(rel)) continue;
458
+ const text = fs.readFileSync(path.join(REPO_ROOT, rel), 'utf8');
459
+ for (const m of text.matchAll(/\bimport\(\s*['"](\.[^'"]+)['"]\s*\)/g)) {
460
+ const target = path.relative(REPO_ROOT, path.resolve(path.dirname(path.join(REPO_ROOT, rel)), m[1]));
461
+ if (!FILES.includes(target)) unshipped.push(`${rel} dynamically imports ${target}, which the manifest does not ship`);
462
+ }
463
+ }
464
+ if (unshipped.length) {
465
+ const err = new Error('cli package manifest incomplete:\n ' + unshipped.join('\n '));
466
+ err.violations = unshipped;
467
+ throw err;
468
+ }
469
+
470
+ fs.rmSync(outDir, { recursive: true, force: true });
471
+ fs.mkdirSync(outDir, { recursive: true });
472
+
473
+ for (const rel of FILES) {
474
+ const dest = path.join(outDir, rel);
475
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
476
+ fs.copyFileSync(path.join(REPO_ROOT, rel), dest);
477
+ }
478
+
479
+ fs.mkdirSync(path.join(outDir, 'bin'), { recursive: true });
480
+ fs.writeFileSync(path.join(outDir, 'bin', 'bongos.js'), dispatcherSource(), { mode: 0o755 });
481
+ fs.writeFileSync(path.join(outDir, 'package.json'), packageJsonSource(version));
482
+ fs.writeFileSync(path.join(outDir, 'README.md'), readmeSource(version));
483
+
484
+ const generated = [...FILES, 'bin/bongos.js', 'package.json', 'README.md'];
485
+ const leaks = scanRedaction(outDir, generated);
486
+ if (leaks.length) {
487
+ const err = new Error(
488
+ 'cli package redaction gate FAILED — refusing to emit:\n ' + leaks.join('\n ') +
489
+ '\n\nThis package is PUBLIC. Build it from the neutral core, not from an instance checkout' +
490
+ '\n(an instance\'s config/branding.json names its own domains and owner).'
491
+ );
492
+ err.leaks = leaks;
493
+ throw err;
494
+ }
495
+
496
+ const bytes = generated.reduce((n, rel) => {
497
+ try { return n + fs.statSync(path.join(outDir, rel)).size; } catch { return n; }
498
+ }, 0);
499
+
500
+ log(`${PACKAGE_NAME}@${version} → ${path.relative(REPO_ROOT, outDir)}`);
501
+ log(` ${generated.length} files · ${(bytes / 1024).toFixed(0)} KB · ${Object.keys(VERBS).length} verbs · 1 dependency (undici)`);
502
+ log(` redaction gate: clean (${instanceNeedles().length} instance needles checked)`);
503
+ return { outDir, version, files: generated, verbs: Object.keys(VERBS) };
504
+ }
505
+
506
+ module.exports = { build, FILES, VERBS, CHECKOUT_ONLY, FORBIDDEN_PREFIXES, FORBIDDEN_BASENAMES, ALLOWED_SRC_BONGOS, PACKAGE_NAME, PACKAGE_VERSION, scanRedaction, instanceNeedles };
507
+
508
+ if (require.main === module) {
509
+ const argv = process.argv.slice(2);
510
+ const flag = (name) => { const i = argv.indexOf(name); return i === -1 ? null : argv[i + 1]; };
511
+ try {
512
+ build({ out: flag('--out'), version: flag('--version'), quiet: argv.includes('--quiet') });
513
+ } catch (err) {
514
+ process.stderr.write(String(err.message) + '\n');
515
+ process.exit(1);
516
+ }
517
+ }
package/src/module-api.js CHANGED
@@ -55,7 +55,7 @@ const { buildInfo } = require('./build-info');
55
55
  // there. scripts/gds/bump-version.js still rewrites the literal below; it appends
56
56
  // the entry to that file. Look for a version's history there, not here.
57
57
  // ---------------------------------------------------------------------------
58
- const CORE_VERSION = '1.19.576'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
58
+ const CORE_VERSION = '1.19.577'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
59
59
 
60
60
  // A namespaced logger so a module's log lines are attributable + consistent.
61
61
  // Usage: const log = api.logger('dev-box'); log.info('mounted');
@@ -0,0 +1,217 @@
1
+ // tests/cli_package.mjs — the public @cloudbongos/cli package is client-only AND actually runs.
2
+ //
3
+ // Guards task 1003679. Two halves, and the second is the one that matters:
4
+ //
5
+ // 1. Shape: the generated tree carries no server surface, and the redaction gate really fails
6
+ // a build when instance identity or a credential shape appears in it.
7
+ //
8
+ // 2. BEHAVIOUR: pack the tarball, install it into an empty directory with NO repo present, and
9
+ // run every advertised verb. This is the only instrument that can prove the file list is
10
+ // complete. A static require-closure cannot: src/module-api.js is the module doorway
11
+ // (ADR 0083) and by design *names* every kernel capability, so a static walk sees the whole
12
+ // server even though the getters are lazy and never resolve. If a real code path needs a
13
+ // file the manifest omits, the verb exits with MODULE_NOT_FOUND and this test says so.
14
+
15
+ import test from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import fs from 'node:fs';
18
+ import os from 'node:os';
19
+ import path from 'node:path';
20
+ import { spawnSync } from 'node:child_process';
21
+ import { fileURLToPath } from 'node:url';
22
+
23
+ const REPO_ROOT = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
24
+ const BUILDER = path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js');
25
+
26
+ const mod = await import(path.join(REPO_ROOT, 'scripts', 'gds', 'build-cli-package.js'));
27
+ const { build, FILES, VERBS, CHECKOUT_ONLY, ALLOWED_SRC_BONGOS, PACKAGE_NAME } = mod.default ?? mod;
28
+
29
+ function tmpdir(tag) {
30
+ return fs.mkdtempSync(path.join(os.tmpdir(), `cli-pkg-${tag}-`));
31
+ }
32
+
33
+ // ── 1. Shape ────────────────────────────────────────────────────────────────────────────────
34
+
35
+ test('the generated package contains no server surface', () => {
36
+ const out = tmpdir('shape');
37
+ const { files } = build({ out, quiet: true });
38
+
39
+ const kernel = files.filter((f) => f.startsWith('src/bongos/'));
40
+ assert.deepEqual(kernel, [...ALLOWED_SRC_BONGOS],
41
+ `only ${[...ALLOWED_SRC_BONGOS].join(', ')} may ship from the kernel, got ${kernel.join(', ')}`);
42
+
43
+ for (const f of files) {
44
+ assert.ok(!f.startsWith('src/bongos/routes/'), `${f}: routes must never ship`);
45
+ assert.ok(!/(^|\/)(pool|db|server|serve-internal)\.js$/.test(f), `${f}: server surface must never ship`);
46
+ assert.ok(!f.startsWith('migrations/'), `${f}: migrations must never ship`);
47
+ assert.ok(!f.startsWith('infra/'), `${f}: infra must never ship`);
48
+ }
49
+ fs.rmSync(out, { recursive: true, force: true });
50
+ });
51
+
52
+ test('the one kernel file that ships imports nothing', () => {
53
+ const src = fs.readFileSync(path.join(REPO_ROOT, 'src', 'bongos', 'api-prefix.js'), 'utf8');
54
+ const rel = [...src.matchAll(/require\(\s*['"](\.[^'"]+)['"]\s*\)/g)].map((m) => m[1]);
55
+ assert.deepEqual(rel, [], `api-prefix.js must stay dependency-free, found: ${rel.join(', ')}`);
56
+ });
57
+
58
+ test('every file in the manifest exists', () => {
59
+ for (const rel of FILES) {
60
+ assert.ok(fs.existsSync(path.join(REPO_ROOT, rel)), `manifest names a missing file: ${rel}`);
61
+ }
62
+ });
63
+
64
+ test('every advertised verb maps to a script the manifest ships', () => {
65
+ for (const [verb, def] of Object.entries(VERBS)) {
66
+ assert.ok(FILES.includes(`scripts/gds/${def.script}`),
67
+ `verb "${verb}" runs scripts/gds/${def.script}, which the manifest does not ship`);
68
+ }
69
+ });
70
+
71
+ test('the redaction gate fails the build on an injected needle and on a credential shape', () => {
72
+ const out = tmpdir('gate');
73
+
74
+ // A domain that really is in the tree, supplied as forbidden → must refuse to emit.
75
+ const withNeedle = spawnSync(process.execPath, [BUILDER, '--out', out, '--quiet'], {
76
+ cwd: REPO_ROOT, encoding: 'utf8',
77
+ env: { ...process.env, CLI_PACKAGE_FORBIDDEN: 'cli-lib' },
78
+ });
79
+ assert.notEqual(withNeedle.status, 0, 'an instance needle present in the tree must fail the build');
80
+ assert.match(withNeedle.stderr, /redaction gate FAILED/);
81
+
82
+ // A planted credential shape must fail too, with no needle configured at all.
83
+ const planted = path.join(REPO_ROOT, 'scripts', 'gds', '.cli-pkg-gate-probe.js');
84
+ const victim = path.join(REPO_ROOT, 'scripts', 'gds', 'preflight.js');
85
+ const original = fs.readFileSync(victim, 'utf8');
86
+ try {
87
+ fs.writeFileSync(victim, `${original}\n// ghp_0123456789abcdefghijABCDEFGHIJ0123\n`);
88
+ const withToken = spawnSync(process.execPath, [BUILDER, '--out', out, '--quiet'], {
89
+ cwd: REPO_ROOT, encoding: 'utf8',
90
+ });
91
+ assert.notEqual(withToken.status, 0, 'a github token shape must fail the build');
92
+ assert.match(withToken.stderr, /github token/);
93
+ } finally {
94
+ fs.writeFileSync(victim, original);
95
+ fs.rmSync(planted, { force: true });
96
+ }
97
+ fs.rmSync(out, { recursive: true, force: true });
98
+ });
99
+
100
+ test('a clean build emits and reports itself', () => {
101
+ const out = tmpdir('clean');
102
+ const res = spawnSync(process.execPath, [BUILDER, '--out', out], { cwd: REPO_ROOT, encoding: 'utf8' });
103
+ assert.equal(res.status, 0, `clean build failed:\n${res.stderr}`);
104
+ assert.match(res.stdout, /redaction gate: clean/);
105
+ assert.ok(fs.existsSync(path.join(out, 'package.json')));
106
+ assert.ok(fs.existsSync(path.join(out, 'bin', 'bongos.js')));
107
+
108
+ const pkg = JSON.parse(fs.readFileSync(path.join(out, 'package.json'), 'utf8'));
109
+ assert.equal(pkg.name, PACKAGE_NAME);
110
+ assert.deepEqual(Object.keys(pkg.dependencies), ['undici'], 'exactly one runtime dependency');
111
+ assert.equal(pkg.bin.bongos, 'bin/bongos.js');
112
+ assert.equal(pkg.publishConfig.access, 'public');
113
+ assert.ok(!('repository' in pkg), 'no repository URL while the core repo is private');
114
+ fs.rmSync(out, { recursive: true, force: true });
115
+ });
116
+
117
+ // ── 2. Behaviour: install the tarball where there is no repo, and run it ─────────────────────
118
+
119
+ test('the packed tarball installs and every verb runs with no repo present', (t) => {
120
+ const buildOut = tmpdir('pack');
121
+ const home = tmpdir('home');
122
+ const site = tmpdir('site');
123
+
124
+ const built = spawnSync(process.execPath, [BUILDER, '--out', buildOut, '--quiet'], {
125
+ cwd: REPO_ROOT, encoding: 'utf8',
126
+ });
127
+ assert.equal(built.status, 0, `build failed:\n${built.stderr}`);
128
+
129
+ const packed = spawnSync('npm', ['pack', '--silent', '--pack-destination', site], {
130
+ cwd: buildOut, encoding: 'utf8',
131
+ });
132
+ assert.equal(packed.status, 0, `npm pack failed:\n${packed.stderr}`);
133
+ const tarball = fs.readdirSync(site).find((f) => f.endsWith('.tgz'));
134
+ assert.ok(tarball, `no tarball produced in ${site}`);
135
+
136
+ // An empty directory: no repo, no core, no config, and a HOME of its own so a real session
137
+ // file on this machine cannot make a verb look healthier than it is.
138
+ fs.writeFileSync(path.join(site, 'package.json'), JSON.stringify({ name: 'probe', private: true }));
139
+ const installed = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--silent', path.join(site, tarball)], {
140
+ cwd: site, encoding: 'utf8', env: { ...process.env, HOME: home },
141
+ });
142
+ assert.equal(installed.status, 0, `npm install failed:\n${installed.stderr}`);
143
+
144
+ const bongos = path.join(site, 'node_modules', '.bin', 'bongos');
145
+ assert.ok(fs.existsSync(bongos), 'the package did not install a `bongos` binary');
146
+
147
+ const runVerb = (args) => spawnSync(bongos, args, {
148
+ cwd: site, encoding: 'utf8', input: '',
149
+ env: { ...process.env, HOME: home, CLOUDBONGOS_API_BASE: 'http://127.0.0.1:45999', NO_COLOR: '1' },
150
+ });
151
+
152
+ // help and version must work with nothing configured at all.
153
+ const help = runVerb(['help']);
154
+ assert.equal(help.status, 0, `bongos help failed:\n${help.stderr}`);
155
+ assert.match(help.stdout, /bongos login https:\/\//, 'help must lead with how to sign in');
156
+
157
+ const version = runVerb(['version']);
158
+ assert.equal(version.status, 0);
159
+ assert.match(version.stdout, /^bongos \d+\.\d+\.\d+/m);
160
+
161
+ // Every advertised verb must actually LOAD its script. Args are chosen so the dispatcher
162
+ // cannot answer on the script's behalf — `--help` on a non-selfHelp verb is intercepted by
163
+ // the dispatcher and would leave the script untouched, which is how this test could pass
164
+ // while proving nothing. Each verb below reaches its own code, then fails on the dead API
165
+ // base or on missing arguments. Failing that way is fine; failing to find a file is not.
166
+ const LOAD_ARGS = {
167
+ login: ['login'],
168
+ reauth: ['reauth'],
169
+ setup: ['setup'],
170
+ box: ['box'],
171
+ shell: ['shell'],
172
+ code: ['code'],
173
+ start: ['start'],
174
+ status: ['status'],
175
+ task: ['task', 'show', '1'],
176
+ recall: ['recall', 'probe'],
177
+ cost: ['cost'],
178
+ release: ['release'],
179
+ api: ['api', 'GET', '/api/gds/me'],
180
+ };
181
+ assert.deepEqual(
182
+ Object.keys(LOAD_ARGS).sort(), Object.keys(VERBS).sort(),
183
+ 'every advertised verb needs an entry here, or it goes unexercised',
184
+ );
185
+
186
+ for (const [verb, args] of Object.entries(LOAD_ARGS)) {
187
+ const res = runVerb(args);
188
+ const all = `${res.stdout}\n${res.stderr}`;
189
+ assert.ok(!/Cannot find module|MODULE_NOT_FOUND|ERR_MODULE_NOT_FOUND/.test(all),
190
+ `bongos ${verb} hit a missing file — the manifest is incomplete:\n${all}`);
191
+ assert.ok(!/ERR_REQUIRE_ESM|SyntaxError|ReferenceError/.test(all),
192
+ `bongos ${verb} failed to load:\n${all}`);
193
+ // `TypeError: fetch failed` is what an unreachable API base looks like and is expected
194
+ // here; any OTHER TypeError means the package itself is broken.
195
+ assert.ok(!/TypeError(?!: fetch failed)/.test(all),
196
+ `bongos ${verb} threw a real TypeError:\n${all}`);
197
+ assert.notEqual(res.status, null, `bongos ${verb} was killed by a signal:\n${all}`);
198
+ // The dispatcher's own help card is proof the script was never reached.
199
+ assert.ok(!/^bongos \w+ — .*\nForwards to scripts/m.test(all),
200
+ `bongos ${verb} was answered by the dispatcher instead of loading:\n${all}`);
201
+ }
202
+
203
+ // The excluded verbs must teach the next step, not print a bare "unknown command".
204
+ for (const verb of Object.keys(CHECKOUT_ONLY)) {
205
+ const res = runVerb([verb]);
206
+ const all = `${res.stdout}\n${res.stderr}`;
207
+ assert.match(all, /is not in the public CLI/, `bongos ${verb} must say why it is absent`);
208
+ assert.match(all, /bongos shell/, `bongos ${verb} must point at a way to get a checkout`);
209
+ assert.ok(!/unknown command/.test(all), `bongos ${verb} must not read as a typo`);
210
+ }
211
+
212
+ // An unknown verb still behaves like one.
213
+ const bogus = runVerb(['definitely-not-a-verb']);
214
+ assert.match(bogus.stderr, /unknown command/);
215
+
216
+ for (const d of [buildOut, home, site]) fs.rmSync(d, { recursive: true, force: true });
217
+ });