@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.
- package/.bongos-core.json +320 -465
- package/.claude/skills/feedback/SKILL.md +2 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +4 -60
- package/clients/bongos-client/index.cjs +4 -60
- package/clients/bongos-client/index.d.ts +5 -90
- package/clients/bongos-client/index.mjs +4 -60
- package/config/branding.neutral.json +1 -5
- package/config/modules.neutral.json +2 -3
- package/config/scheduled-routines.json +0 -2
- package/docs/adr/0145-free-hosted-project-tier-isolation-and-domain-separation.md +1 -0
- package/docs/adr/0285-a-shared-box-holds-about-twelve-projects-per-gb-and-memory-is-the-wall.md +1 -0
- package/docs/adr/0323-hosting-is-three-shapes-and-we-are-not-the-landlord.md +1 -1
- package/docs/adr/0327-a-cloud-host-runs-on-the-owners-account-and-the-key-is-borrowed.md +1 -1
- package/docs/adr/0341-the-page-is-the-unit-of-tweak-mode.md +6 -2
- package/docs/adr/0345-we-host-every-projects-hall-and-its-app-deploys-where-the-owner-chooses.md +94 -0
- package/docs/adr/README.md +2 -1
- package/docs/api/openapi.json +171 -1661
- package/docs/api-reference.md +9 -41
- package/docs/architecture.md +16 -1
- package/docs/branding-contract.md +0 -3
- package/docs/copy-inventory.md +454 -479
- package/docs/copy-registry.json +746 -978
- package/docs/file-map.md +5 -34
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +13 -1
- package/docs/onboarding/diagrams/04-architecture.mmd +11 -22
- package/docs/onboarding/diagrams/README.md +3 -3
- package/docs/onboarding/diagrams/assertions.json +2 -22
- package/docs/onboarding/primer.md +9 -28
- package/docs/page-inventory.json +4 -1
- package/docs/page-readings.json +1032 -1077
- package/docs/recipes/render-pilot.md +29 -10
- package/migrations/core_257_module_store_registry.sql +87 -0
- package/migrations/core_258_drop_dev_box_tables.sql +55 -0
- package/modules/agents/module.json +1 -0
- package/modules/autonomy/module.json +1 -0
- package/modules/builder-settings/module.json +1 -0
- package/modules/builder-settings/render-prefs.js +4 -5
- package/modules/copy-desk/docx.js +78 -5
- package/modules/copy-desk/module.json +1 -0
- package/modules/copy-desk/routes/copy-desk.js +9 -9
- package/modules/copy-desk/tests/copy_docx.mjs +107 -1
- package/modules/copy-desk/tests/fixtures/word-docx.mjs +30 -0
- package/modules/discord/module.json +1 -0
- package/modules/economy/module.json +1 -0
- package/modules/economy/routes/credits.js +1 -2
- package/modules/government/catalog.js +13 -17
- package/modules/government/migrations/government_019_retire_box_permissions.sql +33 -0
- package/modules/government/module.json +1 -0
- package/modules/government/protected-surfaces.json +3 -11
- package/modules/government/resolver.js +3 -46
- package/modules/government/routes/government.js +0 -2
- package/modules/government/session-scopes.js +43 -83
- package/modules/government/session-scopes.json +8 -18
- package/modules/grading/module.json +1 -0
- package/modules/hall-ui/module.json +1 -0
- package/modules/hall-ui/public/board-lib.js +2 -2
- package/modules/hall-ui/public/builders.js +29 -40
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/collab.js +1 -1
- package/modules/hall-ui/public/diagrams.html +3 -3
- package/modules/hall-ui/public/dom-utils.js +1 -1
- package/modules/hall-ui/public/gate.js +2 -2
- package/modules/hall-ui/public/gate.states.json +1 -1
- package/modules/hall-ui/public/hall-render.js +43 -79
- package/modules/hall-ui/public/index.html +5 -1
- package/modules/hall-ui/public/modules.js +1 -0
- package/modules/hall-ui/public/oversight.css +5 -5
- package/modules/hall-ui/public/palette.js +0 -11
- package/modules/hall-ui/public/primer.js +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-sessions.js +1 -1
- package/modules/hall-ui/public/settings.css +0 -1
- package/modules/hall-ui/public/settings.html +4 -33
- package/modules/hall-ui/public/settings.js +3 -33
- package/modules/hall-ui/public/settings.states.json +2 -2
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +10 -13
- package/modules/hall-ui/public/task.html +4 -1
- package/modules/hall-ui/public/task.js +36 -1
- package/modules/hall-ui/public/tweak-editor-lib.js +18 -1
- package/modules/hall-ui/public/tweak-editor.css +16 -0
- package/modules/hall-ui/public/tweak-editor.html +26 -3
- package/modules/hall-ui/public/tweak-editor.js +32 -1
- package/modules/hall-ui/public/work.js +1 -1
- package/modules/hall-ui/records/tweak-editor.md +1 -0
- package/modules/ideas/module.json +1 -0
- package/modules/lifecycle/db-claim-reads.js +1 -58
- package/modules/lifecycle/db-ship.js +0 -5
- package/modules/lifecycle/db.js +0 -4
- package/modules/lifecycle/kickoff-checklist.js +0 -4
- package/modules/lifecycle/lifecycle.js +0 -8
- package/modules/lifecycle/module.json +1 -0
- package/modules/lifecycle/routes/tasks.js +5 -7
- package/modules/memory/module.json +1 -0
- package/modules/npm-release/module.json +1 -0
- package/modules/onboarding/module.json +1 -0
- package/modules/onboarding/onboarding-state.js +17 -36
- package/modules/onboarding/routes/access-requests.js +3 -3
- package/modules/platform-identity/module.json +1 -0
- package/modules/platform-identity/platform-identity.js +13 -5
- package/modules/platform-identity/routes/sso.js +2 -2
- package/modules/platform-identity/tests/platform-identity.mjs +17 -3
- package/modules/provisioning/module.json +1 -0
- package/modules/provisioning/provisioning.js +2 -2
- package/modules/provisioning/starter-bundles.js +5 -25
- package/modules/public-landing/module.json +1 -0
- package/modules/public-landing/public/assets/cosmos.css +1 -1
- package/modules/public-landing/public/contact.html +1 -1
- package/modules/public-landing/public/privacy.html +1 -1
- package/modules/public-landing/public/projects.html +101 -32
- package/modules/public-landing/public/projects.probes.json +2 -2
- package/modules/public-landing/public/projects.states.json +4 -3
- package/modules/public-landing/public/terms.html +1 -1
- package/modules/security/module.json +1 -0
- package/modules/sessions/module.json +1 -0
- package/modules/specialities/module.json +1 -0
- package/modules/status-ui/module.json +1 -0
- package/modules/ui-design/module.json +1 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +84 -0
- package/scripts/gds/artifact-format.js +203 -0
- package/scripts/gds/audit-authorship.sh +1 -13
- package/scripts/gds/claim.js +0 -15
- package/scripts/gds/cli-lib.js +8 -8
- package/scripts/gds/diagram-facts.js +12 -23
- package/scripts/gds/do-api.js +13 -80
- package/scripts/gds/doc-cli-guard.js +2 -0
- package/scripts/gds/doctor.js +1 -1
- package/scripts/gds/feedback-latest.js +139 -9
- package/scripts/gds/fitness-checks-identity.js +2 -7
- package/scripts/gds/fitness-checks-write-validation.js +4 -0
- package/scripts/gds/fitness-ratchets.js +4 -4
- package/scripts/gds/fitness.js +10 -10
- package/scripts/gds/gen-diagrams.js +49 -71
- package/scripts/gds/http-api-client.js +12 -19
- package/scripts/gds/init.js +1 -1
- package/scripts/gds/lint-infra-exec.js +1 -1
- package/scripts/gds/local-preview-lib.js +8 -39
- package/scripts/gds/local-preview.js +2 -15
- package/scripts/gds/migration-namespace.js +3 -3
- package/scripts/gds/module-artifact.js +212 -0
- package/scripts/gds/module.js +95 -6
- package/scripts/gds/package-core.js +5 -116
- package/scripts/gds/provision-config.js +2 -2
- package/scripts/gds/provision-net.js +2 -3
- package/scripts/gds/provision.js +9 -9
- package/scripts/gds/publish-manifest.js +1 -2
- package/scripts/gds/regen-instance-docs.js +1 -1
- package/scripts/gds/run-unit-tests.js +2 -16
- package/scripts/gds/sandbox-stage.js +39 -341
- package/scripts/gds/ship-flow.js +6 -6
- package/scripts/gds/ship-land.js +3 -4
- package/scripts/gds/ship-regen.js +10 -41
- package/scripts/gds/ship.js +4 -24
- package/scripts/gds/start.js +0 -15
- package/scripts/hall-preview/README.md +5 -5
- package/scripts/hall-preview/server.js +6 -6
- package/scripts/render-diagrams.sh +1 -1
- package/src/bongos/api-errors.js +1 -1
- package/src/bongos/auth-github.js +2 -3
- package/src/bongos/auth.js +51 -126
- package/src/bongos/db-kernel.js +2 -3
- package/src/bongos/db.js +5 -28
- package/src/bongos/module-scope-map.js +14 -29
- package/src/bongos/module-store.js +141 -0
- package/src/bongos/route-rank-check.js +3 -16
- package/src/bongos/routes/auth.js +0 -116
- package/src/bongos/routes/builders.js +5 -15
- package/src/bongos/routes/instance.js +8 -14
- package/src/bongos/routes/me.js +6 -22
- package/src/bongos/routes/modules.js +83 -0
- package/src/bongos/routes.js +5 -0
- package/src/module-api.js +13 -6
- package/src/module-loader/catalog.js +3 -0
- package/src/module-loader/loader.js +4 -4
- package/src/module-loader/manifest-schema.js +15 -7
- package/src/module-seams.js +2 -3
- package/src/modules.js +42 -14
- package/tests/agents_spend_guard.mjs +1 -1
- package/tests/api_client.mjs +0 -15
- package/tests/auth_page_gate.mjs +10 -10
- package/tests/canonical_profile_url.mjs +1 -3
- package/tests/claim_action.mjs +4 -1
- package/tests/claim_from_task_and_home.mjs +127 -0
- package/tests/consumer_layout_boot.mjs +2 -0
- package/tests/copy_desk_page_docx.mjs +33 -2
- package/tests/core_upgrade_runner.mjs +6 -6
- package/tests/deploy_divergence_line.mjs +11 -11
- package/tests/diagram_facts_offset.mjs +1 -1
- package/tests/do_api.mjs +106 -0
- package/tests/feedback_latest.mjs +122 -0
- package/tests/fitness.mjs +44 -42
- package/tests/gate_approvals.mjs +0 -1
- package/tests/go_live.mjs +3 -3
- package/tests/government_abuse_matrix.mjs +20 -19
- package/tests/government_ownership_scope.mjs +33 -35
- package/tests/government_parity.mjs +2 -2
- package/tests/government_protected_surfaces.mjs +56 -37
- package/tests/government_require_permission.mjs +3 -3
- package/tests/government_seed.mjs +37 -4
- package/tests/government_session_scope.mjs +92 -366
- package/tests/hall_error_envelope.mjs +4 -0
- package/tests/hall_landing_boot.mjs +4 -1
- package/tests/hall_palette.mjs +1 -16
- package/tests/hall_record_world.mjs +6 -0
- package/tests/hall_tweak_editor.mjs +52 -1
- package/tests/host_topology_skips.mjs +1 -2
- package/tests/html_comment_nesting.mjs +137 -0
- package/tests/infra_exec_paths.mjs +9 -10
- package/tests/init.mjs +4 -5
- package/tests/instance_manifest.mjs +18 -25
- package/tests/kickoff_checklist.mjs +1 -1
- package/tests/lib_sh_resolution.mjs +0 -221
- package/tests/local_preview.mjs +0 -12
- package/tests/module-scope-map.mjs +9 -9
- package/tests/module_catalog.mjs +1 -1
- package/tests/module_cli.mjs +15 -15
- package/tests/module_contributions.mjs +6 -6
- package/tests/module_loader.mjs +6 -6
- package/tests/module_manifest.mjs +13 -4
- package/tests/module_store_publish.mjs +307 -0
- package/tests/module_store_publish_route.mjs +121 -0
- package/tests/module_store_registry_migration.mjs +70 -0
- package/tests/modules.mjs +56 -26
- package/tests/onboarding_route_signals.mjs +21 -31
- package/tests/onboarding_state.mjs +35 -49
- package/tests/permission_path.mjs +0 -3
- package/tests/platform_boot.mjs +1 -2
- package/tests/profile_route.mjs +1 -6
- package/tests/projects_hub.mjs +5 -5
- package/tests/projects_hub_app_step.mjs +138 -0
- package/tests/projects_hub_module_picker.mjs +2 -2
- package/tests/projects_hub_pre_uat.mjs +4 -5
- package/tests/provision.mjs +7 -7
- package/tests/provisioning_starter_bundles.mjs +0 -22
- package/tests/publish_branch_route.mjs +10 -18
- package/tests/publish_manifest.mjs +1 -2
- package/tests/rank_tier_single_source.mjs +1 -13
- package/tests/repo_map.mjs +3 -4
- package/tests/runner_drift.mjs +3 -21
- package/tests/sandbox_stage.mjs +57 -390
- package/tests/session_rename_fallback.mjs +0 -21
- package/tests/ship_error_shape.mjs +1 -1
- package/tests/task_visual_slots.mjs +31 -0
- package/tests/terms_acceptance.mjs +1 -1
- package/tests/upgrade.mjs +4 -4
- package/tests/watch_sealed_floor.mjs +0 -1
- package/tests/wizard_draft_resume.mjs +2 -2
- package/tests/wizard_intent_resume.mjs +21 -10
- package/tests/wizard_preselect_why.mjs +18 -18
- package/docs/onboarding/browser-terminal-guide.md +0 -73
- package/docs/recipes/managed-settings-remote-control.md +0 -416
- package/modules/dev-box/CLAUDE.md +0 -15
- package/modules/dev-box/box-access.js +0 -623
- package/modules/dev-box/box-credential.js +0 -94
- package/modules/dev-box/box-onboard.js +0 -486
- package/modules/dev-box/boxes.js +0 -1038
- package/modules/dev-box/db.js +0 -39
- package/modules/dev-box/module.json +0 -19
- package/modules/dev-box/routes/box.js +0 -1223
- package/scripts/gds/box-auth-check.js +0 -160
- package/scripts/gds/box-infra.js +0 -321
- package/scripts/gds/box-sync.js +0 -216
- package/scripts/gds/box.js +0 -1332
- package/scripts/gds/cf-tunnel.js +0 -558
- package/scripts/gds/smoke-box.sh +0 -150
- package/scripts/gds/tree-preflight.js +0 -241
- package/src/bongos/app-pair.js +0 -172
- package/tests/app_pair.mjs +0 -168
- package/tests/box_access.mjs +0 -578
- package/tests/box_auth_check.mjs +0 -114
- package/tests/box_code_staleness.mjs +0 -129
- package/tests/box_connect_e2e.mjs +0 -196
- package/tests/box_cost.mjs +0 -70
- package/tests/box_credential.mjs +0 -125
- package/tests/box_dns_repoint.mjs +0 -153
- package/tests/box_env.mjs +0 -64
- package/tests/box_host_keys.mjs +0 -126
- package/tests/box_infra_resolve.mjs +0 -246
- package/tests/box_onboard.mjs +0 -317
- package/tests/box_scope_session.mjs +0 -119
- package/tests/box_sweep_docs.mjs +0 -119
- package/tests/box_sync.mjs +0 -55
- package/tests/box_sync_scope_report.mjs +0 -126
- package/tests/box_task_scope.mjs +0 -169
- package/tests/box_task_scope_pause.mjs +0 -66
- package/tests/box_terminal_ssrf.mjs +0 -96
- package/tests/boxes.mjs +0 -1585
- package/tests/cf_ruleset_preflight.mjs +0 -142
- package/tests/cf_tunnel.mjs +0 -496
- package/tests/docs_scrubber_damage.mjs +0 -111
- package/tests/module_scope_active_claims.mjs +0 -94
- package/tests/ship_api_client_pathspec.mjs +0 -100
- 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
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
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
|
|
11
|
-
//
|
|
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,
|
|
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
|
|
4
|
-
//
|
|
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:
|
|
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:
|
|
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 `
|
|
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 };
|
package/scripts/gds/module.js
CHANGED
|
@@ -102,12 +102,12 @@ function envPrefix() {
|
|
|
102
102
|
// Pure builders (exported for tests — no I/O, no network)
|
|
103
103
|
// ---------------------------------------------------------------------------
|
|
104
104
|
|
|
105
|
-
//
|
|
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
|
-
//
|
|
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. "
|
|
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,
|