@bongos/core 1.19.1072 → 1.19.1074

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.
Files changed (297) hide show
  1. package/.bongos-core.json +320 -465
  2. package/.claude/skills/feedback/SKILL.md +2 -2
  3. package/clients/bongos-client/README.md +1 -1
  4. package/clients/bongos-client/bongos-client.global.js +4 -60
  5. package/clients/bongos-client/index.cjs +4 -60
  6. package/clients/bongos-client/index.d.ts +5 -90
  7. package/clients/bongos-client/index.mjs +4 -60
  8. package/config/branding.neutral.json +1 -5
  9. package/config/modules.neutral.json +2 -3
  10. package/config/scheduled-routines.json +0 -2
  11. package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
  12. package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
  13. package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
  14. package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
  15. package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
  16. package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
  17. package/docs/adr/README.md +2 -1
  18. package/docs/api/openapi.json +171 -1661
  19. package/docs/api-reference.md +9 -41
  20. package/docs/architecture.md +16 -1
  21. package/docs/branding-contract.md +0 -3
  22. package/docs/copy-inventory.md +454 -479
  23. package/docs/copy-registry.json +746 -978
  24. package/docs/file-map.md +5 -34
  25. package/docs/module-api-changelog.md +4 -0
  26. package/docs/modules-contract.md +13 -1
  27. package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
  28. package/docs/onboarding/diagrams/README.md +3 -3
  29. package/docs/onboarding/diagrams/assertions.json +2 -22
  30. package/docs/onboarding/primer.md +9 -28
  31. package/docs/page-inventory.json +4 -1
  32. package/docs/page-readings.json +1032 -1077
  33. package/docs/recipes/render-pilot.md +29 -10
  34. package/migrations/core_257_module_store_registry.sql +87 -0
  35. package/migrations/core_258_drop_dev_box_tables.sql +55 -0
  36. package/modules/agents/module.json +1 -0
  37. package/modules/autonomy/module.json +1 -0
  38. package/modules/builder-settings/module.json +1 -0
  39. package/modules/builder-settings/render-prefs.js +4 -5
  40. package/modules/copy-desk/docx.js +78 -5
  41. package/modules/copy-desk/module.json +1 -0
  42. package/modules/copy-desk/routes/copy-desk.js +9 -9
  43. package/modules/copy-desk/tests/copy_docx.mjs +107 -1
  44. package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
  45. package/modules/discord/module.json +1 -0
  46. package/modules/economy/module.json +1 -0
  47. package/modules/economy/routes/credits.js +1 -2
  48. package/modules/government/catalog.js +13 -17
  49. package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
  50. package/modules/government/module.json +1 -0
  51. package/modules/government/protected-surfaces.json +3 -11
  52. package/modules/government/resolver.js +3 -46
  53. package/modules/government/routes/government.js +0 -2
  54. package/modules/government/session-scopes.js +43 -83
  55. package/modules/government/session-scopes.json +8 -18
  56. package/modules/grading/module.json +1 -0
  57. package/modules/hall-ui/module.json +1 -0
  58. package/modules/hall-ui/public/board-lib.js +2 -2
  59. package/modules/hall-ui/public/builders.js +29 -40
  60. package/modules/hall-ui/public/collab.html +1 -1
  61. package/modules/hall-ui/public/collab.js +1 -1
  62. package/modules/hall-ui/public/diagrams.html +3 -3
  63. package/modules/hall-ui/public/dom-utils.js +1 -1
  64. package/modules/hall-ui/public/gate.js +2 -2
  65. package/modules/hall-ui/public/gate.states.json +1 -1
  66. package/modules/hall-ui/public/hall-render.js +43 -79
  67. package/modules/hall-ui/public/index.html +5 -1
  68. package/modules/hall-ui/public/modules.js +1 -0
  69. package/modules/hall-ui/public/oversight.css +5 -5
  70. package/modules/hall-ui/public/palette.js +0 -11
  71. package/modules/hall-ui/public/primer.js +1 -1
  72. package/modules/hall-ui/public/sessions.html +1 -1
  73. package/modules/hall-ui/public/settings-sessions.js +1 -1
  74. package/modules/hall-ui/public/settings.css +0 -1
  75. package/modules/hall-ui/public/settings.html +4 -33
  76. package/modules/hall-ui/public/settings.js +3 -33
  77. package/modules/hall-ui/public/settings.states.json +2 -2
  78. package/modules/hall-ui/public/studio.html +1 -1
  79. package/modules/hall-ui/public/style.css +10 -13
  80. package/modules/hall-ui/public/task.html +4 -1
  81. package/modules/hall-ui/public/task.js +36 -1
  82. package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
  83. package/modules/hall-ui/public/tweak-editor.css +16 -0
  84. package/modules/hall-ui/public/tweak-editor.html +26 -3
  85. package/modules/hall-ui/public/tweak-editor.js +32 -1
  86. package/modules/hall-ui/public/work.js +1 -1
  87. package/modules/hall-ui/records/tweak-editor.md +1 -0
  88. package/modules/ideas/module.json +1 -0
  89. package/modules/lifecycle/db-claim-reads.js +1 -58
  90. package/modules/lifecycle/db-ship.js +0 -5
  91. package/modules/lifecycle/db.js +0 -4
  92. package/modules/lifecycle/kickoff-checklist.js +0 -4
  93. package/modules/lifecycle/lifecycle.js +0 -8
  94. package/modules/lifecycle/module.json +1 -0
  95. package/modules/lifecycle/routes/tasks.js +5 -7
  96. package/modules/memory/module.json +1 -0
  97. package/modules/npm-release/module.json +1 -0
  98. package/modules/onboarding/module.json +1 -0
  99. package/modules/onboarding/onboarding-state.js +17 -36
  100. package/modules/onboarding/routes/access-requests.js +3 -3
  101. package/modules/platform-identity/module.json +1 -0
  102. package/modules/platform-identity/platform-identity.js +13 -5
  103. package/modules/platform-identity/routes/sso.js +2 -2
  104. package/modules/platform-identity/tests/platform-identity.mjs +17 -3
  105. package/modules/provisioning/module.json +1 -0
  106. package/modules/provisioning/provisioning.js +2 -2
  107. package/modules/provisioning/starter-bundles.js +5 -25
  108. package/modules/public-landing/module.json +1 -0
  109. package/modules/public-landing/public/assets/cosmos.css +1 -1
  110. package/modules/public-landing/public/contact.html +1 -1
  111. package/modules/public-landing/public/privacy.html +1 -1
  112. package/modules/public-landing/public/projects.html +101 -32
  113. package/modules/public-landing/public/projects.probes.json +2 -2
  114. package/modules/public-landing/public/projects.states.json +4 -3
  115. package/modules/public-landing/public/terms.html +1 -1
  116. package/modules/security/module.json +1 -0
  117. package/modules/sessions/module.json +1 -0
  118. package/modules/specialities/module.json +1 -0
  119. package/modules/status-ui/module.json +1 -0
  120. package/modules/ui-design/module.json +1 -0
  121. package/package-lock.json +2 -2
  122. package/package.json +1 -1
  123. package/release-notes.json +84 -0
  124. package/scripts/gds/artifact-format.js +203 -0
  125. package/scripts/gds/audit-authorship.sh +1 -13
  126. package/scripts/gds/claim.js +0 -15
  127. package/scripts/gds/cli-lib.js +8 -8
  128. package/scripts/gds/diagram-facts.js +12 -23
  129. package/scripts/gds/do-api.js +13 -80
  130. package/scripts/gds/doc-cli-guard.js +2 -0
  131. package/scripts/gds/doctor.js +1 -1
  132. package/scripts/gds/feedback-latest.js +139 -9
  133. package/scripts/gds/fitness-checks-identity.js +2 -7
  134. package/scripts/gds/fitness-checks-write-validation.js +4 -0
  135. package/scripts/gds/fitness-ratchets.js +4 -4
  136. package/scripts/gds/fitness.js +10 -10
  137. package/scripts/gds/gen-diagrams.js +49 -71
  138. package/scripts/gds/http-api-client.js +12 -19
  139. package/scripts/gds/init.js +1 -1
  140. package/scripts/gds/lint-infra-exec.js +1 -1
  141. package/scripts/gds/local-preview-lib.js +8 -39
  142. package/scripts/gds/local-preview.js +2 -15
  143. package/scripts/gds/migration-namespace.js +3 -3
  144. package/scripts/gds/module-artifact.js +212 -0
  145. package/scripts/gds/module.js +95 -6
  146. package/scripts/gds/package-core.js +5 -116
  147. package/scripts/gds/provision-config.js +2 -2
  148. package/scripts/gds/provision-net.js +2 -3
  149. package/scripts/gds/provision.js +9 -9
  150. package/scripts/gds/publish-manifest.js +1 -2
  151. package/scripts/gds/regen-instance-docs.js +1 -1
  152. package/scripts/gds/run-unit-tests.js +2 -16
  153. package/scripts/gds/sandbox-stage.js +39 -341
  154. package/scripts/gds/ship-flow.js +6 -6
  155. package/scripts/gds/ship-land.js +3 -4
  156. package/scripts/gds/ship-regen.js +10 -41
  157. package/scripts/gds/ship.js +4 -24
  158. package/scripts/gds/start.js +0 -15
  159. package/scripts/hall-preview/README.md +5 -5
  160. package/scripts/hall-preview/server.js +6 -6
  161. package/scripts/render-diagrams.sh +1 -1
  162. package/src/bongos/api-errors.js +1 -1
  163. package/src/bongos/auth-github.js +2 -3
  164. package/src/bongos/auth.js +51 -126
  165. package/src/bongos/db-kernel.js +2 -3
  166. package/src/bongos/db.js +5 -28
  167. package/src/bongos/module-scope-map.js +14 -29
  168. package/src/bongos/module-store.js +141 -0
  169. package/src/bongos/route-rank-check.js +3 -16
  170. package/src/bongos/routes/auth.js +0 -116
  171. package/src/bongos/routes/builders.js +5 -15
  172. package/src/bongos/routes/instance.js +8 -14
  173. package/src/bongos/routes/me.js +6 -22
  174. package/src/bongos/routes/modules.js +83 -0
  175. package/src/bongos/routes.js +5 -0
  176. package/src/module-api.js +13 -6
  177. package/src/module-loader/catalog.js +3 -0
  178. package/src/module-loader/loader.js +4 -4
  179. package/src/module-loader/manifest-schema.js +15 -7
  180. package/src/module-seams.js +2 -3
  181. package/src/modules.js +42 -14
  182. package/tests/agents_spend_guard.mjs +1 -1
  183. package/tests/api_client.mjs +0 -15
  184. package/tests/auth_page_gate.mjs +10 -10
  185. package/tests/canonical_profile_url.mjs +1 -3
  186. package/tests/claim_action.mjs +4 -1
  187. package/tests/claim_from_task_and_home.mjs +127 -0
  188. package/tests/consumer_layout_boot.mjs +2 -0
  189. package/tests/copy_desk_page_docx.mjs +33 -2
  190. package/tests/core_upgrade_runner.mjs +6 -6
  191. package/tests/deploy_divergence_line.mjs +11 -11
  192. package/tests/diagram_facts_offset.mjs +1 -1
  193. package/tests/do_api.mjs +106 -0
  194. package/tests/feedback_latest.mjs +122 -0
  195. package/tests/fitness.mjs +44 -42
  196. package/tests/gate_approvals.mjs +0 -1
  197. package/tests/go_live.mjs +3 -3
  198. package/tests/government_abuse_matrix.mjs +20 -19
  199. package/tests/government_ownership_scope.mjs +33 -35
  200. package/tests/government_parity.mjs +2 -2
  201. package/tests/government_protected_surfaces.mjs +56 -37
  202. package/tests/government_require_permission.mjs +3 -3
  203. package/tests/government_seed.mjs +37 -4
  204. package/tests/government_session_scope.mjs +92 -366
  205. package/tests/hall_error_envelope.mjs +4 -0
  206. package/tests/hall_landing_boot.mjs +4 -1
  207. package/tests/hall_palette.mjs +1 -16
  208. package/tests/hall_record_world.mjs +6 -0
  209. package/tests/hall_tweak_editor.mjs +52 -1
  210. package/tests/host_topology_skips.mjs +1 -2
  211. package/tests/html_comment_nesting.mjs +137 -0
  212. package/tests/infra_exec_paths.mjs +9 -10
  213. package/tests/init.mjs +4 -5
  214. package/tests/instance_manifest.mjs +18 -25
  215. package/tests/kickoff_checklist.mjs +1 -1
  216. package/tests/lib_sh_resolution.mjs +0 -221
  217. package/tests/local_preview.mjs +0 -12
  218. package/tests/module-scope-map.mjs +9 -9
  219. package/tests/module_catalog.mjs +1 -1
  220. package/tests/module_cli.mjs +15 -15
  221. package/tests/module_contributions.mjs +6 -6
  222. package/tests/module_loader.mjs +6 -6
  223. package/tests/module_manifest.mjs +13 -4
  224. package/tests/module_store_publish.mjs +307 -0
  225. package/tests/module_store_publish_route.mjs +121 -0
  226. package/tests/module_store_registry_migration.mjs +70 -0
  227. package/tests/modules.mjs +56 -26
  228. package/tests/onboarding_route_signals.mjs +21 -31
  229. package/tests/onboarding_state.mjs +35 -49
  230. package/tests/permission_path.mjs +0 -3
  231. package/tests/platform_boot.mjs +1 -2
  232. package/tests/profile_route.mjs +1 -6
  233. package/tests/projects_hub.mjs +5 -5
  234. package/tests/projects_hub_app_step.mjs +138 -0
  235. package/tests/projects_hub_module_picker.mjs +2 -2
  236. package/tests/projects_hub_pre_uat.mjs +4 -5
  237. package/tests/provision.mjs +7 -7
  238. package/tests/provisioning_starter_bundles.mjs +0 -22
  239. package/tests/publish_branch_route.mjs +10 -18
  240. package/tests/publish_manifest.mjs +1 -2
  241. package/tests/rank_tier_single_source.mjs +1 -13
  242. package/tests/repo_map.mjs +3 -4
  243. package/tests/runner_drift.mjs +3 -21
  244. package/tests/sandbox_stage.mjs +57 -390
  245. package/tests/session_rename_fallback.mjs +0 -21
  246. package/tests/ship_error_shape.mjs +1 -1
  247. package/tests/task_visual_slots.mjs +31 -0
  248. package/tests/terms_acceptance.mjs +1 -1
  249. package/tests/upgrade.mjs +4 -4
  250. package/tests/watch_sealed_floor.mjs +0 -1
  251. package/tests/wizard_draft_resume.mjs +2 -2
  252. package/tests/wizard_intent_resume.mjs +21 -10
  253. package/tests/wizard_preselect_why.mjs +18 -18
  254. package/docs/onboarding/browser-terminal-guide.md +0 -73
  255. package/docs/recipes/managed-settings-remote-control.md +0 -416
  256. package/modules/dev-box/CLAUDE.md +0 -15
  257. package/modules/dev-box/box-access.js +0 -623
  258. package/modules/dev-box/box-credential.js +0 -94
  259. package/modules/dev-box/box-onboard.js +0 -486
  260. package/modules/dev-box/boxes.js +0 -1038
  261. package/modules/dev-box/db.js +0 -39
  262. package/modules/dev-box/module.json +0 -19
  263. package/modules/dev-box/routes/box.js +0 -1223
  264. package/scripts/gds/box-auth-check.js +0 -160
  265. package/scripts/gds/box-infra.js +0 -321
  266. package/scripts/gds/box-sync.js +0 -216
  267. package/scripts/gds/box.js +0 -1332
  268. package/scripts/gds/cf-tunnel.js +0 -558
  269. package/scripts/gds/smoke-box.sh +0 -150
  270. package/scripts/gds/tree-preflight.js +0 -241
  271. package/src/bongos/app-pair.js +0 -172
  272. package/tests/app_pair.mjs +0 -168
  273. package/tests/box_access.mjs +0 -578
  274. package/tests/box_auth_check.mjs +0 -114
  275. package/tests/box_code_staleness.mjs +0 -129
  276. package/tests/box_connect_e2e.mjs +0 -196
  277. package/tests/box_cost.mjs +0 -70
  278. package/tests/box_credential.mjs +0 -125
  279. package/tests/box_dns_repoint.mjs +0 -153
  280. package/tests/box_env.mjs +0 -64
  281. package/tests/box_host_keys.mjs +0 -126
  282. package/tests/box_infra_resolve.mjs +0 -246
  283. package/tests/box_onboard.mjs +0 -317
  284. package/tests/box_scope_session.mjs +0 -119
  285. package/tests/box_sweep_docs.mjs +0 -119
  286. package/tests/box_sync.mjs +0 -55
  287. package/tests/box_sync_scope_report.mjs +0 -126
  288. package/tests/box_task_scope.mjs +0 -169
  289. package/tests/box_task_scope_pause.mjs +0 -66
  290. package/tests/box_terminal_ssrf.mjs +0 -96
  291. package/tests/boxes.mjs +0 -1585
  292. package/tests/cf_ruleset_preflight.mjs +0 -142
  293. package/tests/cf_tunnel.mjs +0 -496
  294. package/tests/docs_scrubber_damage.mjs +0 -111
  295. package/tests/module_scope_active_claims.mjs +0 -94
  296. package/tests/ship_api_client_pathspec.mjs +0 -100
  297. package/tests/tree_preflight.mjs +0 -243
@@ -1,15 +1,13 @@
1
1
  // scripts/gds/local-preview-lib.js — core of the LOCAL sandbox launcher (task 1056).
2
2
  //
3
- // Gives a builder working on a LOCAL full clone (laptop) the same private game
4
- // preview a dev box gets at sandbox-<login>.example.com (ADR 0044/0046/0052)
5
- // — but served at http://localhost:<port> on their OWN machine: no Cloudflare
6
- // tunnel, no public URL, $0. It runs the ADR 0052 game-only entry
7
- // (src/preview-server.js), which serves ONLY the playable game (no /api/bongos, no
8
- // BuildersHall), degraded-DB exactly like the box sandbox.
3
+ // Gives a builder working on a LOCAL full clone a private game preview (ADR
4
+ // 0044/0046/0052) served at http://localhost:<port> on their OWN machine: no
5
+ // public URL, $0. It runs the ADR 0052 game-only entry (src/preview-server.js),
6
+ // which serves ONLY the playable game (no /api/bongos, no BuildersHall),
7
+ // degraded-DB.
9
8
  //
10
- // The preview is a long-running DETACHED background process (the laptop analogue
11
- // of the box's box-game-preview.service systemd unit). The edit loop mirrors the
12
- // box: edit -> restart -> refresh the browser tab.
9
+ // The preview is a long-running DETACHED background process. The edit loop is:
10
+ // edit -> restart -> refresh the browser tab.
13
11
  //
14
12
  // Everything is pure + injectable (deps.fs / deps.spawn / deps.kill /
15
13
  // deps.fetchImpl / deps.env / deps.*Path) so tests/local_preview.mjs can exercise
@@ -25,18 +23,6 @@ const path = require('node:path');
25
23
  const DEFAULT_PORT = 3100;
26
24
  const HOST = '127.0.0.1';
27
25
 
28
- // Box-only markers (ADR 0031/0044). ANY one ⇒ this is a provisioned dev box, NOT
29
- // a laptop. Deliberately broader than /etc/otb/box.env alone: a box whose box.env
30
- // was lost or is unreadable must still be recognized as a box, never mistaken for
31
- // a laptop — because the laptop path hands back a http://localhost preview URL,
32
- // which is unreachable from the builder's own browser when the server is on a
33
- // remote box (the task-1443 footgun: "the link is giving me localhost"). box.env
34
- // is the canonical marker; box-preview (the preview control command) and the
35
- // box-game-preview.service unit are written on every box and survive an
36
- // unreadable/empty box.env, so they're the resilient backstops.
37
- const BOX_ENV_PATH = '/etc/otb/box.env';
38
- const BOX_PREVIEW_WRAPPER = '/usr/local/bin/box-preview';
39
- const BOX_SERVICE_UNIT = '/etc/systemd/system/box-game-preview.service';
40
26
  // scripts/gds/ -> repo root. Absolute + cwd-independent so staging works no
41
27
  // matter where ship.js is invoked from.
42
28
  const REPO_ROOT = path.resolve(__dirname, '..', '..');
@@ -62,22 +48,6 @@ function hasGameEntry(deps = {}) {
62
48
  return f.existsSync(previewEntry(deps));
63
49
  }
64
50
 
65
- // Is this a provisioned dev box (ADR 0031/0044), i.e. NOT a laptop? True if any
66
- // box-only marker is present. Pure + injectable (deps.fs + per-marker path
67
- // overrides) like everything else here, and honors the dep names sandbox-stage.js
68
- // already injects (deps.boxEnvPath / deps.previewWrapper) so a single call site
69
- // drives both. Used to keep the localhost local-preview path from EVER being
70
- // chosen on a box — see the BOX_* marker note above (task 1443).
71
- function onBox(deps = {}) {
72
- const f = deps.fs || fs;
73
- const markers = [
74
- deps.boxEnvPath || BOX_ENV_PATH,
75
- deps.previewWrapper || BOX_PREVIEW_WRAPPER,
76
- deps.boxServiceUnit || BOX_SERVICE_UNIT,
77
- ];
78
- return markers.some((p) => { try { return f.existsSync(p); } catch { return false; } });
79
- }
80
-
81
51
  function readPid(deps = {}) {
82
52
  const f = deps.fs || fs;
83
53
  try {
@@ -152,8 +122,7 @@ async function health(deps = {}) {
152
122
 
153
123
  module.exports = {
154
124
  DEFAULT_PORT, HOST, REPO_ROOT,
155
- BOX_ENV_PATH, BOX_PREVIEW_WRAPPER, BOX_SERVICE_UNIT,
156
125
  previewPort, localUrl, healthUrl, previewEntry, pidPath, logPath,
157
- hasGameEntry, onBox, readPid, pidAlive, isRunning,
126
+ hasGameEntry, readPid, pidAlive, isRunning,
158
127
  start, stop, restart, health,
159
128
  };
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // scripts/gds/local-preview.js — the builder's control for the LOCAL game
3
- // preview (task 1056), the laptop analogue of `box-preview` (ADR 0044) on a dev
4
- // box. Runs the ADR 0052 game-only entry privately at http://localhost:<port>.
3
+ // preview (task 1056, ADR 0044). Runs the ADR 0052 game-only entry privately at
4
+ // http://localhost:<port>.
5
5
  //
6
6
  // npm run preview # start it (then open the URL)
7
7
  // node scripts/gds/local-preview.js status is it up + the URL (default)
@@ -27,21 +27,8 @@ async function pollHealthy(ms = 20_000) {
27
27
 
28
28
  const HINT = `Open ${lib.localUrl()} — your PRIVATE copy of the game (degraded DB, nothing shipped).`;
29
29
 
30
- // This launcher is the LAPTOP analogue of box-preview. On a dev box it's the
31
- // wrong tool: it binds http://localhost, which the builder's browser can't reach
32
- // when the server is on a remote box — the "the link is giving me localhost"
33
- // footgun (task 1443). Warn loudly and point at the box's HOSTED sandbox; warn,
34
- // don't hard-fail, so an operator who really wants a box-local loopback still can.
35
- function warnIfOnBox() {
36
- if (!lib.onBox()) return;
37
- console.error('⚠ You appear to be on a dev box. This LOCAL preview binds http://localhost —');
38
- console.error(' your browser cannot reach the box\'s localhost. Your sandbox is the HOSTED url:');
39
- console.error(' run /builder-stage (or box-preview status ) to get https://sandbox-<login>.<host> and refresh that tab.');
40
- }
41
-
42
30
  (async () => {
43
31
  const cmd = process.argv[2] || 'status';
44
- if (cmd === 'start' || cmd === 'restart' || cmd === 'status' || cmd === 'url') warnIfOnBox();
45
32
  switch (cmd) {
46
33
  case 'start': {
47
34
  const r = lib.start();
@@ -14,7 +14,7 @@
14
14
  // DISCIPLINE. Every migration SOURCE owns a distinct filename namespace, so stems can
15
15
  // never collide across sources and each source's allocator counts only its own numbers:
16
16
  // • CORE (the @bongos/core package's migrations/*.sql) → core_NNN_slug.sql
17
- // • MODULE (modules/<key>/migrations/*.sql) → <key>_NNN_slug.sql (key: dev-box → dev_box_)
17
+ // • MODULE (modules/<key>/migrations/*.sql) → <key>_NNN_slug.sql (key: art-pipeline → art_pipeline_)
18
18
  // • INSTANCE (the host repo's migrations/instance/*.sql) → NNN_slug.sql (BARE — the instance owns the bare namespace)
19
19
  //
20
20
  // The core prefix ALONE is enough to break stem-equality with a bare instance file
@@ -42,7 +42,7 @@ const INSTANCE_NS = 'instance';
42
42
  // Reserved namespace words a module key may not be (they name the core/instance sources).
43
43
  const RESERVED_NS = new Set([CORE_NS, INSTANCE_NS]);
44
44
 
45
- // SQL/filename-safe namespace prefix for a module key: dev-box → dev_box (matches
45
+ // SQL/filename-safe namespace prefix for a module key: art-pipeline → art_pipeline (matches
46
46
  // fitness.js moduleNamespace + the existing economy_/lifecycle_ files).
47
47
  function sqlNamespace(moduleKey) {
48
48
  return String(moduleKey == null ? '' : moduleKey).replace(/-/g, '_');
@@ -70,7 +70,7 @@ const RENAMED_MODULE_PREFIXES = { government: new Set(['governance']) };
70
70
 
71
71
  const BARE_RE = /^(\d{3,})_(.+)\.sql$/;
72
72
  // A prefix is a lowercase word (letters/digits/underscores) starting with a letter,
73
- // then `_NNN_`. Lazy so `dev_box_003_x` splits as prefix=dev_box, num=003 (not dev/box).
73
+ // then `_NNN_`. Lazy so `art_pipeline_003_x` splits as prefix=art_pipeline, num=003 (not art/pipeline).
74
74
  const PREFIXED_RE = /^([a-z][a-z0-9_]*?)_(\d{3,})_(.+)\.sql$/;
75
75
 
76
76
  // Parse a migration basename into { prefix, number, slug, bare, ok }. A non-migration
@@ -0,0 +1,212 @@
1
+ // scripts/gds/module-artifact.js — pack and verify ONE module version for the
2
+ // Bongos-hosted module store (task 1004271, ADR 0338 D1).
3
+ //
4
+ // ADR 0338 settled "no second packaging format": a module version is a tarball plus a
5
+ // hash manifest in exactly the shape package-core.js emits for the core — per-file
6
+ // sha256, a compression-independent tree_sha256, a deterministic ustar+gzip — so the
7
+ // tar writer, reader and tree pin all come from artifact-format.js, the file the core
8
+ // release builds with. What differs is only WHAT is packed and what the manifest names:
9
+ //
10
+ // tarball package/<path inside modules/<key>/> (module.json at package/module.json)
11
+ // package/.bongos-module.json the inner manifest (below)
12
+ // manifest { artifact: 'bongos-module', manifest_schema, generator, module_key,
13
+ // module_version, core_version, built_at, file_count, tree_sha256,
14
+ // files: [{ path, mode, sha256 }] }
15
+ // side-car the same manifest + { tarball: { name, sha256, bytes } } (withTarballInfo)
16
+ //
17
+ // THE GATE. The core's isPublishable() is default-deny: a path ships only if the
18
+ // allowlist names it and the denylist does not. The allowlist names the CORE's own
19
+ // modules, so for a store module the allow side is "inside this module's own
20
+ // modules/<key>/ directory" and the deny side is the SAME repo-wide denylist
21
+ // (matchesDeny — game content, pixel art, copyrighted references, instance topology,
22
+ // secrets), plus credential-named files anywhere in the module (SECRET_FILE_RE). Anything the denylist catches refuses the whole publish rather than being
23
+ // silently dropped: a module that ships without a file it expected is a broken module.
24
+ //
25
+ // Both halves live here so the CLI (packModule) and the store route
26
+ // (verifyModuleArtifact) cannot disagree about the format. Pure apart from reading the
27
+ // module directory in packModule; never touches the network or the database.
28
+
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+ const { sha256hex, canonicalTreeHash, normalizeMode, buildTar, readTarAsync } = require('./artifact-format');
32
+ const { matchesDeny } = require('./publish-manifest');
33
+ const { validateManifest } = require('../../src/module-loader/manifest-schema');
34
+
35
+ const ARTIFACT_NAME = 'bongos-module';
36
+ const MANIFEST_SCHEMA = 1;
37
+ const TAR_ROOT = 'package/';
38
+ const INNER_MANIFEST_PATH = '.bongos-module.json';
39
+ const INNER_MANIFEST = TAR_ROOT + INNER_MANIFEST_PATH;
40
+ // Local-only files a module dir may hold that are never part of the module itself.
41
+ const NEVER_PACKED = new Set(['.upstream-signoff.json', INNER_MANIFEST_PATH]);
42
+ // The store's caps. A real feature module is tens of KB (the biggest bundled one packs
43
+ // to ~90 KB), so these are generous headroom — and deliberately small, because the
44
+ // store verifies an upload inside the web process: every byte is inflated and hashed
45
+ // there. 5 MB on the wire matches the upstreaming queue's SUBMISSION_MAX_BYTES; the
46
+ // unpacked cap bounds a gzip bomb.
47
+ const MAX_TARBALL_BYTES = 5 * 1024 * 1024;
48
+ const READ_LIMITS = { maxUnpackedBytes: 16 * 1024 * 1024, maxFiles: 2000 };
49
+
50
+ function tarballName(key, version) { return `${ARTIFACT_NAME}-${key}-${version}.tgz`; }
51
+
52
+ // Every file under dir, as module-relative forward-slash paths, sorted. Skips
53
+ // VCS/dependency noise the way listModuleFiles (module.js) does.
54
+ function listFiles(dir, rel = '') {
55
+ const out = [];
56
+ for (const entry of fs.readdirSync(path.join(dir, rel), { withFileTypes: true })) {
57
+ if (entry.name === 'node_modules' || entry.name === '.git') continue;
58
+ const r = rel ? `${rel}/${entry.name}` : entry.name;
59
+ if (entry.isDirectory()) out.push(...listFiles(dir, r));
60
+ else if (entry.isFile()) out.push(r);
61
+ }
62
+ return out.sort();
63
+ }
64
+
65
+ // A module-relative path is safe to unpack only if it stays inside the module.
66
+ function isSafeRelPath(p) {
67
+ return typeof p === 'string' && p.length > 0 && !p.startsWith('/') && !p.includes('\\')
68
+ && !/^[A-Za-z]:/.test(p) && !p.split('/').some((seg) => seg === '' || seg === '.' || seg === '..');
69
+ }
70
+
71
+ // The repo denylist anchors its secret entries at the root (`.env`, not
72
+ // `modules/x/.env`), which is right for the core tree and wrong for a module that
73
+ // travels on its own. So a store module is also refused any file whose NAME marks it
74
+ // as a credential, wherever it sits in the module.
75
+ const SECRET_FILE_RE = /^(?:\.env(?:\..*)?|\.npmrc|\.netrc|\.pgpass|id_(?:rsa|dsa|ecdsa|ed25519)(?:\.pub)?|.*\.(?:pem|key|p12|pfx|jks|keystore))$/i;
76
+
77
+ // The files the gate refuses, named at their canonical modules/<key>/ path.
78
+ function deniedFiles(key, relPaths) {
79
+ return relPaths
80
+ .filter((p) => matchesDeny(`modules/${key}/${p}`) || SECRET_FILE_RE.test(p.split('/').pop()))
81
+ .map((p) => `modules/${key}/${p}`);
82
+ }
83
+
84
+ function buildModuleManifest({ key, version, coreVersion, builtAtIso, files, treeHash }) {
85
+ return {
86
+ artifact: ARTIFACT_NAME,
87
+ manifest_schema: MANIFEST_SCHEMA,
88
+ generator: 'scripts/gds/module-artifact.js',
89
+ module_key: key,
90
+ module_version: version,
91
+ core_version: coreVersion,
92
+ built_at: builtAtIso,
93
+ file_count: files.length,
94
+ tree_sha256: treeHash,
95
+ files: files.map((f) => ({ path: f.path, mode: normalizeMode(f.mode), sha256: f.sha256 })),
96
+ };
97
+ }
98
+
99
+ // Pack modules/<key>/ into { tgz, manifest, sidecar } or throw with a plain reason.
100
+ // `modeOf` is injectable because Windows reports no executable bit.
101
+ function packModule(key, { modulesDir, now = () => new Date(), modeOf } = {}) {
102
+ const dir = path.join(modulesDir, key);
103
+ if (!fs.existsSync(dir)) throw new Error(`modules/${key} does not exist`);
104
+ let moduleJson;
105
+ try { moduleJson = JSON.parse(fs.readFileSync(path.join(dir, 'module.json'), 'utf8')); }
106
+ catch { throw new Error(`modules/${key}/module.json is missing or is not valid JSON`); }
107
+ const { valid, errors } = validateManifest(moduleJson);
108
+ if (!valid) throw new Error(`modules/${key}/module.json is invalid: ${errors.join('; ')}`);
109
+ if (moduleJson.key !== key) throw new Error(`modules/${key}/module.json names key "${moduleJson.key}"`);
110
+
111
+ const rel = listFiles(dir).filter((p) => !NEVER_PACKED.has(p));
112
+ const denied = deniedFiles(key, rel);
113
+ if (denied.length) {
114
+ throw new Error(`the publish denylist refuses: ${denied.join(', ')} — remove them from the module (ADR 0098)`);
115
+ }
116
+ const unsafe = rel.filter((p) => !isSafeRelPath(p));
117
+ if (unsafe.length) throw new Error(`unpackable file name(s): ${unsafe.join(', ')}`);
118
+
119
+ const files = [];
120
+ const entries = [];
121
+ for (const p of rel) {
122
+ const abs = path.join(dir, ...p.split('/'));
123
+ const buf = fs.readFileSync(abs);
124
+ const mode = normalizeMode(modeOf ? modeOf(abs) : (fs.statSync(abs).mode & 0o777).toString(8));
125
+ files.push({ path: p, mode, sha256: sha256hex(buf) });
126
+ entries.push({ name: TAR_ROOT + p, mode: parseInt(mode, 8), buf });
127
+ }
128
+ const manifest = buildModuleManifest({
129
+ key, version: moduleJson.version, coreVersion: moduleJson.coreVersion,
130
+ builtAtIso: now().toISOString(), files, treeHash: canonicalTreeHash(files),
131
+ });
132
+ entries.push({ name: INNER_MANIFEST, mode: 0o644, buf: Buffer.from(JSON.stringify(manifest, null, 2) + '\n') });
133
+ const tgz = buildTar(entries);
134
+ const sidecar = { ...manifest, tarball: { name: tarballName(key, manifest.module_version), sha256: sha256hex(tgz), bytes: tgz.length } };
135
+ return { tgz, manifest, sidecar, moduleJson };
136
+ }
137
+
138
+ // The store's check on an upload. Trusts NOTHING the client says beyond the bytes:
139
+ // every hash is recomputed, the inner manifest must describe exactly the files the
140
+ // tarball holds, module.json must validate and name the route's key, and the
141
+ // denylist runs again. Resolves { ok: true, ... } or { ok: false, code, message }.
142
+ // Async so the inflate runs off the event loop (readTarAsync); the rest is one pass
143
+ // over at most READ_LIMITS.maxUnpackedBytes.
144
+ async function verifyModuleArtifact(tgz, { key, maxBytes = MAX_TARBALL_BYTES } = {}) {
145
+ const fail = (code, message) => ({ ok: false, code, message });
146
+ if (!Buffer.isBuffer(tgz) || tgz.length === 0) return fail('empty_artifact', 'the upload is empty');
147
+ if (tgz.length > maxBytes) return fail('artifact_too_large', `the tarball is over ${maxBytes} bytes`);
148
+
149
+ let entries;
150
+ try { entries = await readTarAsync(tgz, READ_LIMITS); }
151
+ catch (e) { return fail('bad_tarball', e.message); }
152
+
153
+ const byName = new Map();
154
+ for (const e of entries) {
155
+ if (!e.name.startsWith(TAR_ROOT)) return fail('bad_tarball', `"${e.name}" is outside ${TAR_ROOT}`);
156
+ const rel = e.name.slice(TAR_ROOT.length);
157
+ if (!isSafeRelPath(rel)) return fail('bad_tarball', `"${e.name}" is not a safe path`);
158
+ if (byName.has(rel)) return fail('bad_tarball', `"${e.name}" appears twice`);
159
+ byName.set(rel, e);
160
+ }
161
+ const inner = byName.get(INNER_MANIFEST_PATH);
162
+ if (!inner) return fail('no_manifest', `the tarball carries no ${INNER_MANIFEST}`);
163
+ let manifest;
164
+ try { manifest = JSON.parse(inner.buf.toString('utf8')); }
165
+ catch { return fail('no_manifest', `${INNER_MANIFEST} is not valid JSON`); }
166
+ if (manifest.artifact !== ARTIFACT_NAME || manifest.manifest_schema !== MANIFEST_SCHEMA || !Array.isArray(manifest.files)) {
167
+ return fail('bad_manifest', `${INNER_MANIFEST} is not a ${ARTIFACT_NAME} schema ${MANIFEST_SCHEMA} manifest`);
168
+ }
169
+
170
+ // Recompute the pin from the bytes and hold the manifest to it, file by file.
171
+ byName.delete(INNER_MANIFEST_PATH);
172
+ const actual = [...byName.entries()]
173
+ .map(([p, e]) => ({ path: p, mode: normalizeMode(e.mode.toString(8)), sha256: sha256hex(e.buf) }))
174
+ .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
175
+ const declared = [...manifest.files].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
176
+ if (declared.length !== actual.length || manifest.file_count !== actual.length) {
177
+ return fail('hash_mismatch', `the manifest lists ${declared.length} file(s) but the tarball holds ${actual.length}`);
178
+ }
179
+ for (let i = 0; i < actual.length; i++) {
180
+ const a = actual[i]; const d = declared[i];
181
+ if (d.path !== a.path || normalizeMode(d.mode) !== a.mode || d.sha256 !== a.sha256) {
182
+ return fail('hash_mismatch', `"${a.path}" does not match its manifest entry`);
183
+ }
184
+ }
185
+ const treeSha = canonicalTreeHash(actual);
186
+ if (manifest.tree_sha256 !== treeSha) return fail('hash_mismatch', 'tree_sha256 does not match the files');
187
+
188
+ const denied = deniedFiles(key, actual.map((f) => f.path));
189
+ if (denied.length) return fail('denied_content', `the publish denylist refuses: ${denied.join(', ')}`);
190
+
191
+ const mj = byName.get('module.json');
192
+ if (!mj) return fail('bad_module', 'the tarball has no module.json');
193
+ let moduleJson;
194
+ try { moduleJson = JSON.parse(mj.buf.toString('utf8')); }
195
+ catch { return fail('bad_module', 'module.json is not valid JSON'); }
196
+ const v = validateManifest(moduleJson);
197
+ if (!v.valid) return fail('bad_module', `module.json is invalid: ${v.errors.join('; ')}`);
198
+ if (moduleJson.key !== key || manifest.module_key !== key) {
199
+ return fail('key_mismatch', `the tarball is module "${moduleJson.key}", not "${key}"`);
200
+ }
201
+ if (manifest.module_version !== moduleJson.version || manifest.core_version !== moduleJson.coreVersion) {
202
+ return fail('bad_manifest', 'the manifest version does not match module.json');
203
+ }
204
+
205
+ return {
206
+ ok: true, manifest, moduleJson,
207
+ version: moduleJson.version, coreVersion: moduleJson.coreVersion,
208
+ treeSha256: treeSha, tarballSha256: sha256hex(tgz), bytes: tgz.length,
209
+ };
210
+ }
211
+
212
+ module.exports = { MAX_TARBALL_BYTES, packModule, verifyModuleArtifact };
@@ -102,12 +102,12 @@ function envPrefix() {
102
102
  // Pure builders (exported for tests — no I/O, no network)
103
103
  // ---------------------------------------------------------------------------
104
104
 
105
- // dev-box -> "Dev Box"; economy -> "Economy". A readable default title.
105
+ // art-pipeline -> "Art Pipeline"; economy -> "Economy". A readable default title.
106
106
  function titleize(key) {
107
107
  return String(key).split(/[-_]+/).filter(Boolean)
108
108
  .map((w) => w[0].toUpperCase() + w.slice(1)).join(' ');
109
109
  }
110
- // dev-box -> devBox (for the generated route-factory function name).
110
+ // art-pipeline -> artPipeline (for the generated route-factory function name).
111
111
  function camel(key) {
112
112
  return String(key).replace(/[-_]+(.)/g, (_, c) => c.toUpperCase());
113
113
  }
@@ -125,6 +125,8 @@ function buildManifest(key, { core = coreVersion() } = {}) {
125
125
  key,
126
126
  title: titleize(key),
127
127
  description: `TODO: one sentence describing the ${key} module.`,
128
+ // The module's own version (task 1003781). A fresh scaffold is pre-release.
129
+ version: '0.1.0',
128
130
  coreVersion: `^${core}`,
129
131
  default: false,
130
132
  author: 'TODO: your name or builder handle',
@@ -265,7 +267,7 @@ function evaluateUpgrade({ rows = [], errors = [], core } = {}) {
265
267
 
266
268
  function scaffoldModule({ key, modulesDir = MODULES_DIR, core = coreVersion(), force = false } = {}) {
267
269
  if (!key || !KEY_RE.test(key)) {
268
- throw new Error(`invalid module key "${key}" — must match ${KEY_RE} (lowercase kebab-case, e.g. "dev-box")`);
270
+ throw new Error(`invalid module key "${key}" — must match ${KEY_RE} (lowercase kebab-case, e.g. "art-pipeline")`);
269
271
  }
270
272
  const dir = path.join(modulesDir, key);
271
273
  if (fs.existsSync(dir) && !force) {
@@ -411,14 +413,14 @@ function formatCatalog(rows, { catalogOnly = false, query = '', core = null } =
411
413
  return out.join('\n');
412
414
  }
413
415
 
414
- out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
416
+ out.push(` ${pad('', 5)}${pad('KEY', 20)}${pad('VERSION', 9)}${pad('SOURCE', 10)}${pad('AUTHOR', 20)}${pad('ORIGIN', 26)}${pad('LICENSE', 12)}UPKEEP`);
415
417
  for (const r of shown) {
416
418
  const mark = r.enabled ? '[x]' : '[ ]';
417
419
  // An inferred credit is marked so it never reads as something the author
418
420
  // typed — the catalog's whole job is honest attribution (ADR 0107 §2).
419
421
  const suffix = r.credit.inferred ? '*' : '';
420
422
  const upkeep = (r.maintenance && r.maintenance.status) || 'undeclared';
421
- out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
423
+ out.push(` ${pad(mark, 5)}${pad(clip(r.key, 19), 20)}${pad(clip(r.version || '—', 8), 9)}${pad(r.source, 10)}${pad(clip((r.credit.author || '—') + suffix, 19), 20)}${pad(clip((r.credit.origin || '—') + suffix, 25), 26)}${pad(clip(r.license || '—', 11), 12)}${upkeep}`);
422
424
  }
423
425
  out.push('');
424
426
  out.push(' [x] enabled here · [ ] available, not enabled');
@@ -776,6 +778,90 @@ async function cmdSubmit(args, {
776
778
  return 0;
777
779
  }
778
780
 
781
+ // ---------------------------------------------------------------------------
782
+ // bongos module publish <key> — put a module version in the store (ADR 0338 D1)
783
+ // ---------------------------------------------------------------------------
784
+
785
+ // `bongos module publish <key>` — task 1004271. Packs modules/<key>/ into the
786
+ // store's tarball + hash manifest (scripts/gds/module-artifact.js — the core
787
+ // release's own format, through the publish denylist), then uploads it to
788
+ // POST /store/modules/:key/versions, which re-verifies every hash itself and
789
+ // records the version. `--dry-run` packs and reports without uploading;
790
+ // `--out <dir>` also writes the tarball and its side-car manifest there.
791
+ // Distinct from `submit`, which proposes a module INTO core (ADR 0135).
792
+ async function cmdPublish(args, {
793
+ log = console.log, errlog = console.error,
794
+ modulesDir = MODULES_DIR, pack, upload, writeFile = fs.writeFileSync,
795
+ } = {}) {
796
+ const key = args.find((a, i) => !a.startsWith('-') && args[i - 1] !== '--out');
797
+ if (!key) {
798
+ errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
799
+ return 2;
800
+ }
801
+ const dryRun = args.includes('--dry-run');
802
+ const outIdx = args.indexOf('--out');
803
+ const outDir = outIdx !== -1 ? args[outIdx + 1] : null;
804
+ if (outIdx !== -1 && (!outDir || outDir.startsWith('--'))) {
805
+ errlog('usage: bongos module publish <key> [--dry-run] [--out <dir>]');
806
+ return 2;
807
+ }
808
+
809
+ const artifact = require('./module-artifact');
810
+ let packed;
811
+ try { packed = (pack || ((k) => artifact.packModule(k, { modulesDir })))(key); }
812
+ catch (e) { errlog(`REFUSING: ${e.message}`); return 1; }
813
+ const { tgz, sidecar } = packed;
814
+ log(`bongos module publish — "${key}" ${sidecar.module_version} (needs core ${sidecar.core_version})`);
815
+ log(` ✓ packed ${sidecar.file_count} file(s), ${tgz.length} bytes — tree ${sidecar.tree_sha256.slice(0, 12)}…`);
816
+ if (tgz.length > artifact.MAX_TARBALL_BYTES) {
817
+ errlog(`REFUSING: the tarball is ${tgz.length} bytes, over the store's ${artifact.MAX_TARBALL_BYTES}-byte cap.`);
818
+ return 1;
819
+ }
820
+
821
+ if (outDir) {
822
+ fs.mkdirSync(outDir, { recursive: true });
823
+ const tgzPath = path.join(outDir, sidecar.tarball.name);
824
+ writeFile(tgzPath, tgz);
825
+ writeFile(tgzPath.replace(/\.tgz$/, '.manifest.json'), JSON.stringify(sidecar, null, 2) + '\n');
826
+ log(` ✓ wrote ${tgzPath} (+ .manifest.json)`);
827
+ }
828
+ if (dryRun) {
829
+ log('\n--dry-run: nothing uploaded.');
830
+ return 0;
831
+ }
832
+
833
+ const doUpload = upload || (async (k, body) => {
834
+ // Lazy require — the same reason cmdSubmit gives: this file loads in the server
835
+ // process too, and cli-lib's session/fetch plumbing must never come with it.
836
+ const { readSessionToken, dispatchedFetch, noKeepAliveDispatcher, CLI_USER_AGENT } = require('./cli-lib');
837
+ const session = readSessionToken();
838
+ if (!session) throw new Error('no Bongos session — run `bongos setup` first');
839
+ const res = await dispatchedFetch(`${session.apiBase}/api/bongos/store/modules/${encodeURIComponent(k)}/versions`, {
840
+ method: 'POST',
841
+ headers: { 'Content-Type': 'application/gzip', Authorization: `Bearer ${session.token}`, 'User-Agent': CLI_USER_AGENT },
842
+ body,
843
+ dispatcher: noKeepAliveDispatcher(),
844
+ });
845
+ const text = await res.text();
846
+ let data;
847
+ try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
848
+ return { status: res.status, ok: res.ok, data };
849
+ });
850
+
851
+ let res;
852
+ try { res = await doUpload(key, tgz); }
853
+ catch (e) { errlog(`bongos module publish: upload failed — ${e.message}`); return 1; }
854
+ if (!res.ok) {
855
+ const err = res.data && (res.data.error || res.data);
856
+ const detail = (err && (err.message || err.code)) || JSON.stringify(res.data);
857
+ errlog(`\nREFUSING: the store rejected "${key}" ${sidecar.module_version} (HTTP ${res.status}): ${detail}`);
858
+ return 1;
859
+ }
860
+ const row = res.data && res.data.version;
861
+ log(`\n✓ Published "${key}" ${row ? row.version : sidecar.module_version} to the store${res.data && res.data.created_module ? ' — its first version, so you are its author' : ''}.`);
862
+ return 0;
863
+ }
864
+
779
865
  function printHelp(out = console.log) {
780
866
  out('bongos module — scaffold + manage Cloud Bongos modules\n');
781
867
  out('Usage:');
@@ -787,6 +873,8 @@ function printHelp(out = console.log) {
787
873
  out(' AGPL/publish pre-check before upstreaming + record a DCO-style sign-off');
788
874
  out(' bongos module submit <key> [--sign-off "Name <email>"]');
789
875
  out(' Pre-check + sign-off, then file into the rank-gated upstreaming review queue');
876
+ out(' bongos module publish <key> [--dry-run] [--out <dir>]');
877
+ out(' Pack a module version (tarball + hash manifest) and upload it to the module store');
790
878
  }
791
879
 
792
880
  async function main(argv) {
@@ -797,6 +885,7 @@ async function main(argv) {
797
885
  if (sub === 'upgrade') return cmdUpgrade(args.slice(1));
798
886
  if (sub === 'check') return cmdCheck(args.slice(1));
799
887
  if (sub === 'submit') return cmdSubmit(args.slice(1));
888
+ if (sub === 'publish') return cmdPublish(args.slice(1));
800
889
  if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
801
890
  printHelp();
802
891
  return sub ? 0 : 2;
@@ -807,7 +896,7 @@ async function main(argv) {
807
896
  }
808
897
 
809
898
  module.exports = {
810
- main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, printHelp,
899
+ main, cmdNew, cmdList, cmdUpgrade, cmdCheck, cmdSubmit, cmdPublish, printHelp,
811
900
  gatherCatalog, formatCatalog, ownProjectName,
812
901
  scaffoldModule, gatherUpgrade, evaluateUpgrade, buildManifest,
813
902
  formatMaintenanceHint, isPastRemoveAfter,