@bongos/core 1.17.0 → 1.17.2
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 +363 -312
- package/.claude/skills/blocker-review/SKILL.md +1 -1
- package/.claude/skills/builder-claim/SKILL.md +2 -0
- package/.claude/skills/builder-exit/SKILL.md +2 -2
- package/.claude/skills/builder-redteam/SKILL.md +3 -3
- package/.claude/skills/builder-setup/SKILL.md +2 -2
- package/.claude/skills/builder-start/SKILL.md +1 -1
- package/.claude/skills/dev/SKILL.md +1 -1
- package/.claude/skills/idea-triage/SKILL.md +2 -2
- package/.claude/skills/ideate/SKILL.md +1 -1
- package/.claude/skills/merge-mode/SKILL.md +1 -1
- package/.claude/skills/new-project/SKILL.md +3 -0
- package/.claude/skills/paint/SKILL.md +1 -1
- package/CLAUDE.md +11 -238
- package/GOVERNANCE.md +1 -1
- package/README.md +1 -1
- package/bin/bongos.js +1 -0
- package/clients/bongos-client/README.md +4 -4
- package/clients/bongos-client/bongos-client.global.js +14 -1
- package/clients/bongos-client/index.cjs +14 -1
- package/clients/bongos-client/index.d.ts +19 -2
- package/clients/bongos-client/index.mjs +11 -1
- package/clients/bongos-client/package.json +1 -1
- package/docs/adr/0001-stack-choice-single-vps.md +1 -1
- package/docs/adr/0008-google-chat-oauth-user-auth.md +1 -1
- package/docs/adr/0019-red-team-bounty-payout-table.md +3 -3
- package/docs/adr/0020-security-model-public-stub.md +1 -1
- package/docs/adr/0023-kind-multipliers-and-idea-bonus.md +1 -1
- package/docs/adr/0024-cloneable-repo-local-first-memory.md +1 -1
- package/docs/adr/0026-bfg-memory-hygiene-and-cross-builder-learning.md +4 -4
- package/docs/adr/0027-bfg-session-inefficiency-evaluator.md +3 -3
- package/docs/adr/0030-builders-hall-live-updates.md +1 -1
- package/docs/adr/0031-cloud-dev-environments-for-builders.md +4 -4
- package/docs/adr/0032-discord-mirror-of-gds.md +2 -2
- package/docs/adr/0034-thetes-graduated-newcomer-rank.md +1 -1
- package/docs/adr/0044-mediterranean-palette-replacement.md +1 -1
- package/docs/adr/0044-per-box-live-game-preview.md +1 -1
- package/docs/adr/0054-session-token-reward-cost-plus.md +10 -10
- package/docs/adr/0057-container-cost-ledger.md +3 -3
- package/docs/adr/0062-medusa-instance-model-decoupling.md +4 -4
- package/docs/adr/0065-agpl-nonprofit-ai-first-governance.md +2 -2
- package/docs/adr/0085-remove-default-per-builder-spend-cap.md +1 -1
- package/docs/adr/0086-goal-scoped-work-hierarchy.md +1 -1
- package/docs/adr/0096-require-reward-before-workable.md +9 -9
- package/docs/adr/0097-retroactive-reward-backfill.md +3 -3
- package/docs/adr/0100-two-repo-instance-model.md +1 -1
- package/docs/adr/0101-goal-hierarchy-phase-5-judge-membership-reward.md +3 -3
- package/docs/adr/0103-core-first-extraction-cloud-bongos-trunk.md +3 -3
- package/docs/adr/0107-module-upstreaming-contribution-to-core-catalog.md +1 -1
- package/docs/adr/0120-pay-on-land-and-builder-owned-rebase-gate.md +6 -6
- package/docs/adr/0133-github-app-manifest-onboarding-signin.md +2 -0
- package/docs/adr/0134-private-first-npm-distribution.md +20 -6
- package/docs/adr/0135-module-upstream-submission-interim-queue.md +34 -0
- package/docs/adr/0136-update-channel-subscription-policy.md +51 -0
- package/docs/adr/0137-upstream-publish-policy-manual-owner-gated.md +40 -0
- package/docs/adr/0138-bundled-module-deps-declared-per-module.md +45 -0
- package/docs/adr/README.md +14 -10
- package/docs/api/openapi.json +315 -6
- package/docs/api-reference.md +10 -5
- package/docs/architecture.md +3 -3
- package/docs/branding-contract.md +5 -4
- package/docs/canonical-permissions.md +1 -1
- package/docs/design/vanilla-hall-ui-redesign-scope.md +1 -1
- package/docs/file-map.md +20 -19
- package/docs/handoff-template.md +2 -2
- package/docs/modules-contract.md +8 -1
- package/docs/onboarding/diagrams/README.md +7 -7
- package/docs/onboarding/drachmae-and-karma.md +18 -18
- package/docs/onboarding/primer.md +9 -9
- package/docs/recipes/audit-replay.md +2 -2
- package/docs/recipes/builder-box-lifecycle.md +2 -2
- package/docs/recipes/claude-design-loop.md +1 -1
- package/docs/recipes/instance-deploy.md +4 -4
- package/docs/recipes/instance-first-run.md +51 -0
- package/docs/recipes/ops-gotchas.md +1 -1
- package/docs/recipes/packaging-the-core.md +5 -5
- package/docs/recipes/private-npm-distribution.md +39 -12
- package/docs/recipes/self-host.md +1 -1
- package/docs/recipes/staging-environment.md +1 -1
- package/docs/recipes/upgrading-the-core.md +63 -8
- package/docs/research/cursor-competitive-analysis.md +1 -1
- package/docs/research/llm-model-agnostic-and-cheaper-grader.md +1 -1
- package/migrations/core_191_module_submissions.sql +45 -0
- package/modules/autonomy/CLAUDE.md +11 -45
- package/modules/builder-settings/CLAUDE.md +11 -54
- package/modules/dev-box/CLAUDE.md +11 -34
- package/modules/discord/CLAUDE.md +11 -46
- package/modules/discord/module.json +2 -1
- package/modules/economy/CLAUDE.md +11 -44
- package/modules/economy/achievements.js +9 -1
- package/modules/economy/credits.js +10 -1
- package/modules/grading/CLAUDE.md +11 -38
- package/modules/hall-ui/CLAUDE.md +11 -57
- package/modules/hall-ui/public/builders.js +71 -4
- package/modules/hall-ui/public/goal-inbox.js +191 -0
- package/modules/hall-ui/public/goals-page.js +227 -22
- package/modules/hall-ui/public/goals.html +16 -0
- package/modules/hall-ui/public/hall-widgets.js +14 -1
- package/modules/hall-ui/public/index.html +3 -0
- package/modules/hall-ui/public/new-project-wizard.js +6 -4
- package/modules/hall-ui/public/style.css +43 -0
- package/modules/ideas/CLAUDE.md +11 -33
- package/modules/lifecycle/CLAUDE.md +11 -59
- package/modules/lifecycle/routes/goals.js +80 -0
- package/modules/memory/CLAUDE.md +11 -36
- package/modules/onboarding/CLAUDE.md +11 -51
- package/modules/provisioning/CLAUDE.md +15 -0
- package/modules/provisioning/migrations/provisioning_001_tables.sql +160 -0
- package/modules/provisioning/migrations/provisioning_002_standalone_shape.sql +11 -0
- package/modules/provisioning/migrations/provisioning_003_github_app_manifest.sql +95 -0
- package/modules/provisioning/module.json +17 -0
- package/modules/provisioning/onboard-plan.js +37 -0
- package/modules/provisioning/provisioning.js +539 -0
- package/modules/provisioning/rate-limit.js +35 -0
- package/modules/provisioning/routes/provisioning.js +607 -0
- package/modules/provisioning/tests/provisioning.mjs +232 -0
- package/modules/provisioning/ui/.gitkeep +0 -0
- package/modules/security/CLAUDE.md +11 -40
- package/modules/security/secret-scrub.js +10 -0
- package/modules/sessions/CLAUDE.md +11 -35
- package/modules/status-ui/CLAUDE.md +11 -43
- package/package-lock.json +2 -2
- package/package.json +7 -2
- package/public-landing/create.html +58 -12
- package/public-landing/index.html +40 -21
- package/scripts/discord/README.md +1 -1
- package/scripts/gds/CLAUDE.md +11 -140
- package/scripts/gds/claim.js +98 -48
- package/scripts/gds/claude-materialize.js +174 -20
- package/scripts/gds/claude-md-template.js +84 -0
- package/scripts/gds/cli-lib.js +112 -2
- package/scripts/gds/cutover-codemod.js +241 -0
- package/scripts/gds/fitness.js +54 -0
- package/scripts/gds/gen-api-client.js +10 -10
- package/scripts/gds/init.js +112 -16
- package/scripts/gds/lint-infra-exec.js +70 -0
- package/scripts/gds/lint-workflow-exec.js +63 -0
- package/scripts/gds/migration-namespace.js +1 -1
- package/scripts/gds/mirror-lineage-check.js +14 -5
- package/scripts/gds/mirror-redact.js +8 -0
- package/scripts/gds/module.js +118 -4
- package/scripts/gds/npmrc.js +93 -0
- package/scripts/gds/onboard.js +17 -24
- package/scripts/gds/package-core.js +74 -17
- package/scripts/gds/provision.js +102 -17
- package/scripts/gds/publish-manifest.js +60 -2
- package/scripts/gds/run-routine.js +51 -13
- package/scripts/gds/run-unit-tests.js +4 -0
- package/scripts/gds/seed-adopt-tasks.js +1 -1
- package/scripts/gds/status.js +6 -1
- package/scripts/gds/task.js +166 -0
- package/scripts/gds/update-channel.js +186 -0
- package/scripts/gds/upgrade.js +172 -25
- package/src/bongos/CLAUDE.md +11 -60
- package/src/bongos/auth.js +24 -0
- package/src/bongos/module-submissions.js +56 -0
- package/src/bongos/route-rank-check.js +6 -0
- package/src/bongos/routes/CLAUDE.md +11 -39
- package/src/bongos/routes/auth.js +6 -1
- package/src/bongos/routes/modules.js +123 -21
- package/src/bongos/serve-internal.js +10 -1
- package/src/branding.js +1 -0
- package/src/instance-config.js +1 -1
- package/src/module-api.js +24 -1
- package/src/module-loader/loader.js +36 -4
- package/src/module-loader/manifest-schema.js +16 -1
- package/tests/branding.mjs +11 -0
- package/tests/claim_error_surface.mjs +107 -0
- package/tests/claude_materialize.mjs +125 -0
- package/tests/consumer_layout_boot.mjs +302 -0
- package/tests/cutover_codemod.mjs +184 -0
- package/tests/device_flow_start.mjs +96 -0
- package/tests/first_run_engine_check.mjs +79 -0
- package/tests/goal_membership_requests.mjs +33 -3
- package/tests/hook_core_resolve.mjs +125 -0
- package/tests/hook_user_agents.mjs +62 -0
- package/tests/infra_exec_paths.mjs +104 -0
- package/tests/init.mjs +106 -6
- package/tests/instance_mismatch_guard.mjs +101 -0
- package/tests/mirror_lineage_proof.mjs +14 -0
- package/tests/module_cli.mjs +130 -2
- package/tests/module_loader.mjs +42 -0
- package/tests/module_manifest.mjs +19 -0
- package/tests/module_submissions.mjs +135 -0
- package/tests/npmrc.mjs +105 -0
- package/tests/onboard.mjs +62 -0
- package/tests/package_core.mjs +76 -22
- package/tests/platform_boot.mjs +5 -2
- package/tests/provision.mjs +107 -8
- package/tests/provisioning_github_app_routes.mjs +44 -2
- package/tests/publish_manifest.mjs +52 -1
- package/tests/run_routine.mjs +26 -1
- package/tests/run_routine_two_root.mjs +77 -0
- package/tests/scrubber_corpus.mjs +17 -0
- package/tests/served_paths_shipped.mjs +2 -2
- package/tests/task_cli.mjs +99 -0
- package/tests/update_channel.mjs +186 -0
- package/tests/upgrade.mjs +220 -25
- package/tests/workflow_exec_paths.mjs +98 -0
- package/docs/design/container-exterior-tiles.md +0 -49
- package/docs/design/container-exterior.md +0 -81
- package/docs/design/container-interior.md +0 -77
- package/docs/design/world-map-scroll.md +0 -41
- package/docs/recipes/claude-gemini-bongos-doors.md +0 -155
- package/docs/recipes/cloudbongos-staging-gate.md +0 -139
- package/docs/recipes/cloudbongos-standup.md +0 -111
- package/docs/recipes/parallel-load-results-2026-05-30.md +0 -63
- package/docs/recipes/parallel-load-test.md +0 -32
- package/docs/recipes/pixel-art-pipeline.md +0 -411
- package/docs/recipes/staging-data.md +0 -50
- package/migrations/instance/018_pms_v3_planning_seed.sql +0 -131
- package/migrations/instance/080_gds_v4_planning_seed.sql +0 -75
- package/migrations/instance/110_medusa_planning_seed.sql +0 -70
- package/migrations/instance/120_bongos_v1_planning_seed.sql +0 -69
- package/migrations/instance/144_bongos_v1_module_system_criterion.sql +0 -26
- package/migrations/instance/159_bongos_v1_goal_hierarchy_criterion.sql +0 -29
- package/migrations/instance/172_v2_world_planning_seed.sql +0 -163
- package/migrations/instance/173_bongos_v1_c7_core_modularized.sql +0 -33
- package/migrations/instance/182_gdsv4_bongos_consolidation.sql +0 -132
- package/migrations/instance/183_gdsv4_carryover_themed_goals.sql +0 -116
- package/migrations/instance/186_bongos_v1_governance_planning_seed.sql +0 -69
- package/scripts/gds/art-stage.js +0 -152
- package/server.js +0 -212
- package/tests/art_key.mjs +0 -121
- package/tests/day_cycle.mjs +0 -197
- package/tests/snapshot_picker.mjs +0 -233
- package/tests/sprite_facing.mjs +0 -35
- package/tests/terrain_picker.mjs +0 -298
- package/tests/turn_in_place.mjs +0 -280
- package/tests/walkability.mjs +0 -188
|
@@ -23,9 +23,20 @@
|
|
|
23
23
|
const fs = require('fs');
|
|
24
24
|
const path = require('path');
|
|
25
25
|
const { execFileSync } = require('child_process');
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
const { resolveCoreRoot, resolveInstanceRoot } = require('../../src/instance-config');
|
|
27
|
+
|
|
28
|
+
// TWO roots (ADR 0108 configurable-root; task 2138). run-routine.js is a CORE script, so it
|
|
29
|
+
// ships alongside src/, scripts/gds/*, scripts/discord/*, and modules/ — all under
|
|
30
|
+
// resolveCoreRoot(), which is where core step scripts + the grader resolve from. The ROUTINES
|
|
31
|
+
// themselves are TOPOLOGY: `bongos init/upgrade` materializes .claude/scheduled-tasks/ INTO the
|
|
32
|
+
// instance (claude-materialize.js), so they're found under resolveInstanceRoot(). Today both
|
|
33
|
+
// roots are the one checkout — BYTE-IDENTICAL to the old `path.resolve(__dirname,'..','..')`;
|
|
34
|
+
// post-split TASKS_DIR points at the instance repo while core scripts resolve inside
|
|
35
|
+
// node_modules/@bongos/core. (Do NOT reintroduce a hardcoded __dirname/../.. — see scripts/gds/CLAUDE.md.)
|
|
36
|
+
const CORE_ROOT = resolveCoreRoot();
|
|
37
|
+
const INSTANCE_ROOT = resolveInstanceRoot();
|
|
38
|
+
const TASKS_DIR = path.join(INSTANCE_ROOT, '.claude', 'scheduled-tasks');
|
|
39
|
+
const DISCORD_POST = path.join(CORE_ROOT, 'scripts', 'discord', 'post.js');
|
|
29
40
|
const VALID_MODES = new Set(['deterministic', 'hybrid', 'judgment']);
|
|
30
41
|
const VALID_SINKS = new Set(['discord', 'gds', 'none']);
|
|
31
42
|
const ALERT_CHANNEL = process.env.OTB_ROUTINE_ALERT_CHANNEL || 'ship_feed';
|
|
@@ -104,10 +115,36 @@ function tokenize(cmd) {
|
|
|
104
115
|
return out;
|
|
105
116
|
}
|
|
106
117
|
|
|
118
|
+
function isNodeExec(a) {
|
|
119
|
+
const base = path.basename(String(a || '')).toLowerCase();
|
|
120
|
+
return a === process.execPath || base === 'node' || base === 'node.exe';
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Pure: resolve a routine step's argv so it runs in BOTH layouts (ADR 0108; task 2138).
|
|
124
|
+
// • `bongos exec <rel> [args]` (the materialized invocation form) → run <rel> under the CORE
|
|
125
|
+
// root, exactly like `bongos exec` itself does (bin/bongos.js resolveExecTarget).
|
|
126
|
+
// • `node <path> [args]` → resolve <path>: a `scripts/...` core path under coreRoot; any other
|
|
127
|
+
// relative path (notably the `.claude/scheduled-tasks/<name>/<x>.js` companions) under the
|
|
128
|
+
// instance root. Absolute paths and non-node commands pass through untouched.
|
|
129
|
+
// Commands come from committed SKILL.md (trusted), so this is path resolution, not sandboxing.
|
|
130
|
+
// Today coreRoot === instanceRoot (single checkout) so this is byte-identical to the old run;
|
|
131
|
+
// only after the split do the two roots diverge. Exported + unit-tested.
|
|
132
|
+
function resolveStepArgv(argv, { coreRoot = CORE_ROOT, instanceRoot = INSTANCE_ROOT } = {}) {
|
|
133
|
+
if (!argv || !argv.length) return argv || [];
|
|
134
|
+
if (argv[0] === 'bongos' && argv[1] === 'exec' && argv[2]) {
|
|
135
|
+
return [process.execPath, path.join(coreRoot, argv[2]), ...argv.slice(3)];
|
|
136
|
+
}
|
|
137
|
+
if (isNodeExec(argv[0]) && argv[1] && !path.isAbsolute(argv[1])) {
|
|
138
|
+
const base = argv[1].startsWith('scripts/') ? coreRoot : instanceRoot;
|
|
139
|
+
return [process.execPath, path.join(base, argv[1]), ...argv.slice(2)];
|
|
140
|
+
}
|
|
141
|
+
return argv;
|
|
142
|
+
}
|
|
143
|
+
|
|
107
144
|
function opsAlert(message) {
|
|
108
145
|
try {
|
|
109
|
-
execFileSync(process.execPath, [
|
|
110
|
-
{ cwd:
|
|
146
|
+
execFileSync(process.execPath, [DISCORD_POST, '--channel', ALERT_CHANNEL, String(message).slice(0, 1800)],
|
|
147
|
+
{ cwd: INSTANCE_ROOT, stdio: 'ignore' });
|
|
111
148
|
} catch (_) { /* best-effort: the alert must never mask the real failure */ }
|
|
112
149
|
}
|
|
113
150
|
|
|
@@ -116,19 +153,20 @@ function routeOutput(sink, stdout, routineName) {
|
|
|
116
153
|
if (text) console.log(text);
|
|
117
154
|
if (sink === 'discord' && text) {
|
|
118
155
|
try {
|
|
119
|
-
execFileSync(process.execPath, [
|
|
120
|
-
{ cwd:
|
|
156
|
+
execFileSync(process.execPath, [DISCORD_POST, '--channel', ALERT_CHANNEL, `[${routineName}] ${text}`.slice(0, 1800)],
|
|
157
|
+
{ cwd: INSTANCE_ROOT, stdio: 'ignore' });
|
|
121
158
|
} catch (_) { /* best-effort */ }
|
|
122
159
|
}
|
|
123
160
|
// sink 'gds' / 'none': the step's command performed its own GDS side effects.
|
|
124
161
|
}
|
|
125
162
|
|
|
126
163
|
function runStep(step, routineName, { dryRun }) {
|
|
127
|
-
const
|
|
128
|
-
if (!
|
|
164
|
+
const tokens = tokenize(step.command);
|
|
165
|
+
if (!tokens.length) return '';
|
|
129
166
|
if (dryRun) { console.log(`[dry-run] would run (${step.sink}): ${step.command}`); return ''; }
|
|
167
|
+
const argv = resolveStepArgv(tokens);
|
|
130
168
|
const out = execFileSync(argv[0], argv.slice(1), {
|
|
131
|
-
cwd:
|
|
169
|
+
cwd: INSTANCE_ROOT, // routines run in the instance repo (companions + git-touching steps)
|
|
132
170
|
encoding: 'utf8',
|
|
133
171
|
env: { ...process.env, OTB_COST_CRON_ROUTINE: routineName },
|
|
134
172
|
});
|
|
@@ -140,7 +178,7 @@ function runJudgment(routineName, skillBody, { dryRun }) {
|
|
|
140
178
|
if (dryRun) { console.log(`[dry-run] judgment mode → would spawn claude -p over ${routineName}/SKILL.md`); return; }
|
|
141
179
|
// Status quo: a wrapped session reading the SKILL body. Reuse the shared spawn.
|
|
142
180
|
const grader = require('../../modules/grading/grader');
|
|
143
|
-
return grader.runSubagent({ prompt: skillBody, opts: { cwd:
|
|
181
|
+
return grader.runSubagent({ prompt: skillBody, opts: { cwd: INSTANCE_ROOT } });
|
|
144
182
|
}
|
|
145
183
|
|
|
146
184
|
async function main() {
|
|
@@ -176,7 +214,7 @@ async function main() {
|
|
|
176
214
|
try { const j = JSON.parse(String(out).trim().split('\n').pop() || '{}'); if (j && j.needs_judgment) payload = j.needs_judgment; } catch (_) {}
|
|
177
215
|
if (payload) {
|
|
178
216
|
const grader = require('../../modules/grading/grader');
|
|
179
|
-
await grader.runSubagent({ prompt: String(payload), opts: { cwd:
|
|
217
|
+
await grader.runSubagent({ prompt: String(payload), opts: { cwd: INSTANCE_ROOT } });
|
|
180
218
|
}
|
|
181
219
|
}
|
|
182
220
|
}
|
|
@@ -194,4 +232,4 @@ if (require.main === module) {
|
|
|
194
232
|
main();
|
|
195
233
|
}
|
|
196
234
|
|
|
197
|
-
module.exports = { parseRoutineFrontmatter, tokenize, VALID_MODES, VALID_SINKS };
|
|
235
|
+
module.exports = { parseRoutineFrontmatter, tokenize, resolveStepArgv, isNodeExec, VALID_MODES, VALID_SINKS };
|
|
@@ -65,6 +65,10 @@ const SUPPORT_MODULES = new Set(['helpers']);
|
|
|
65
65
|
const INTEGRATION = new Set([
|
|
66
66
|
'asset_version', 'auth_flow_slice1', 'backfill_sessions', 'bfg', 'bfg_write',
|
|
67
67
|
'box_connect_e2e', 'box_onboard', 'boxes', 'cf_tunnel', 'cli_token_reissue',
|
|
68
|
+
// consumer_layout_boot builds the core .tgz, `npm install`s it, and boots a live
|
|
69
|
+
// platform-server — network + package build + a spawned server, run by its own
|
|
70
|
+
// .github/workflows/consumer-layout-boot.yml, not this DB-free gate (task 2142).
|
|
71
|
+
'consumer_layout_boot',
|
|
68
72
|
'discord_autojoin', 'discord_oauth', 'linkify_refs', 'memory_isolation',
|
|
69
73
|
'memory_pull', 'parallel-load', 'session_records', 'setup_smoke',
|
|
70
74
|
'status-probe', 'subdomain_auth',
|
|
@@ -19,7 +19,7 @@ const RUNBOOK_TASK = 1981; // goal-35 canonical runbook; A05 depends on it (exte
|
|
|
19
19
|
const TASKS = [
|
|
20
20
|
{ ref: 'A01', kind: 'feature', priority: 4, est: 180, crit: [CRIT], deps: [], extDeps: [],
|
|
21
21
|
title: 'bongos init --adopt: mode skeleton + additive config layering (no clobber)',
|
|
22
|
-
description: 'Add an `--adopt` mode to `scripts/gds/init.js` (ADR 0121 §Decision 1-2). Instead of the greenfield "stand up a NEW instance" path (which refuses where config/branding.json exists and whose --force OVERWRITES), --adopt LAYERS onto the existing tree: add the pinned @
|
|
22
|
+
description: 'Add an `--adopt` mode to `scripts/gds/init.js` (ADR 0121 §Decision 1-2). Instead of the greenfield "stand up a NEW instance" path (which refuses where config/branding.json exists and whose --force OVERWRITES), --adopt LAYERS onto the existing tree: add the pinned @bongos/core dependency + the lean config/ overrides + .claude/ ADDITIVELY. For any pre-existing file (package.json, an existing .claude/, a CI workflow) it MERGES or LEAVES-AND-REPORTS — it never overwrites, and it must NOT reuse the --force code path. Writes only the lean branding.json overrides (rest resolves from *.neutral.json). Serves criterion adopt-existing-repo. deps: none (the ADR-0108 configurable-root substrate is shipped).' },
|
|
23
23
|
{ ref: 'A02', kind: 'feature', priority: 4, est: 150, crit: [CRIT], deps: [], extDeps: [],
|
|
24
24
|
title: 'bongos init --adopt: detect the existing stack + layout',
|
|
25
25
|
description: 'Before writing anything, --adopt inspects the existing tree — language/runtime, package manager, test + CI setup, directory layout — so the enabled modules, the generated repo-map, and the core<->host fitness boundary (ADR 0062 §1) fit what is already there instead of assuming the reference layout (ADR 0121 §Decision 4). Output: a detected-stack summary consumed by A03 (real-state capture) and A04 (pre-flight). Serves criterion adopt-existing-repo. deps: none.' },
|
package/scripts/gds/status.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
//
|
|
20
20
|
// Exit codes: 0 ok · 1 network/api error · 2 bad invocation · 3 not found.
|
|
21
21
|
|
|
22
|
-
const { API_BASE, cliClient } = require('./cli-lib');
|
|
22
|
+
const { API_BASE, cliClient, assertInstanceMatch } = require('./cli-lib');
|
|
23
23
|
|
|
24
24
|
// The active internal version; override with GDS_STATUS_DEFAULT_VERSION (or pass
|
|
25
25
|
// --version). Bumped GDS-V4 → BONGOS-V1 (task 1709, ADR 0103): GDS-V4 was
|
|
@@ -319,6 +319,11 @@ async function main() {
|
|
|
319
319
|
console.log('Usage: node scripts/gds/status.js [--version ID] [Cn] [--json]');
|
|
320
320
|
process.exit(0);
|
|
321
321
|
}
|
|
322
|
+
// Fail closed if this repo's configured instance != the signed-in session's (task
|
|
323
|
+
// 2124 / demo-audit C2): fetchProgress uses a raw fetch(API_BASE/…) that bypasses the
|
|
324
|
+
// apiCall/cliClient guard, so without this `bongos status` would silently print another
|
|
325
|
+
// instance's progress. Skip only in --hook mode (the automated card path stays fail-open).
|
|
326
|
+
if (!opts.hook) await assertInstanceMatch();
|
|
322
327
|
const data = await fetchProgress(opts.version);
|
|
323
328
|
|
|
324
329
|
// Resolve the requested criterion (if any) once — every output mode needs it.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/gds/task.js — `bongos task` : create + show a Bongos task from the terminal.
|
|
3
|
+
//
|
|
4
|
+
// task 2123 (demo-audit fix C1). The bongos CLI had start/claim/ship/status/… but no
|
|
5
|
+
// way to CREATE a task — so seeding the first task (a new owner's opening move) meant
|
|
6
|
+
// hand-driving POST /api/bongos/tasks through `bongos exec scripts/gds/api.js`. This is
|
|
7
|
+
// the first-class front door.
|
|
8
|
+
//
|
|
9
|
+
// Usage:
|
|
10
|
+
// bongos task new --title "…" --description "…" [--kind feature] [--discipline engineer]
|
|
11
|
+
// [--priority 3] [--version <id>] [--goal <id>] [--criteria C1,C2]
|
|
12
|
+
// [--touches a/,b/] [--est-minutes N] [--credits N] [--ready] [--json]
|
|
13
|
+
// bongos task show <id> [--json]
|
|
14
|
+
//
|
|
15
|
+
// Task CREATION is Archon-gated SERVER-SIDE (creating work shapes scope, ADR 0016). This
|
|
16
|
+
// CLI adds no authority — a lower rank gets a clear 403 message pointing at idea capture.
|
|
17
|
+
|
|
18
|
+
const { cliClient, requireSession, arg, hasFlag } = require('./cli-lib');
|
|
19
|
+
|
|
20
|
+
// Valid enums (mirrors the server's task-classifier vocab; a friendly client-side check
|
|
21
|
+
// so an obvious typo fails before the round-trip — the server stays authoritative).
|
|
22
|
+
const VALID_KINDS = ['feature', 'bug', 'infra', 'refactor', 'spike', 'learning-capture', 'cleanup', 'decision', 'blocker-resolution', 'verify'];
|
|
23
|
+
const VALID_DISCIPLINES = ['engineer', 'artist', 'ideator', 'ui'];
|
|
24
|
+
|
|
25
|
+
function csv(v) {
|
|
26
|
+
return String(v || '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// PURE argv → { body, criteria, errors } — exported for the unit test (no network).
|
|
30
|
+
// `args` is the argv slice AFTER the `new` subcommand.
|
|
31
|
+
function buildTaskBody(args) {
|
|
32
|
+
const errors = [];
|
|
33
|
+
const title = arg('--title', args);
|
|
34
|
+
const description = arg('--description', args);
|
|
35
|
+
if (!title) errors.push('--title is required');
|
|
36
|
+
if (!description) errors.push('--description is required');
|
|
37
|
+
|
|
38
|
+
const kind = arg('--kind', args) || 'feature';
|
|
39
|
+
if (!VALID_KINDS.includes(kind)) errors.push(`--kind must be one of: ${VALID_KINDS.join(', ')}`);
|
|
40
|
+
const discipline = arg('--discipline', args) || 'engineer';
|
|
41
|
+
if (!VALID_DISCIPLINES.includes(discipline)) errors.push(`--discipline must be one of: ${VALID_DISCIPLINES.join(', ')}`);
|
|
42
|
+
|
|
43
|
+
const num = (flag) => { const v = arg(flag, args); return v == null ? undefined : Number(v); };
|
|
44
|
+
const priority = num('--priority');
|
|
45
|
+
if (priority !== undefined && (!Number.isInteger(priority) || priority < 1 || priority > 5)) {
|
|
46
|
+
errors.push('--priority must be an integer 1–5');
|
|
47
|
+
}
|
|
48
|
+
const goalRaw = arg('--goal', args);
|
|
49
|
+
const goalId = goalRaw == null ? undefined : Number(goalRaw);
|
|
50
|
+
if (goalRaw != null && (!Number.isInteger(goalId) || goalId <= 0)) errors.push('--goal must be a positive integer task-goal id');
|
|
51
|
+
|
|
52
|
+
const body = {
|
|
53
|
+
title,
|
|
54
|
+
description,
|
|
55
|
+
kind,
|
|
56
|
+
discipline,
|
|
57
|
+
status: hasFlag('--ready', args) ? 'ready' : 'backlog',
|
|
58
|
+
source: 'bongos-task-cli',
|
|
59
|
+
};
|
|
60
|
+
const version = arg('--version', args);
|
|
61
|
+
if (version) body.version_id = version;
|
|
62
|
+
if (goalId !== undefined) body.goal_id = goalId;
|
|
63
|
+
if (priority !== undefined) body.priority = priority;
|
|
64
|
+
const est = num('--est-minutes'); if (est !== undefined) body.est_minutes = est;
|
|
65
|
+
const md = num('--manual-degree'); if (md !== undefined) body.manual_degree = md;
|
|
66
|
+
const credits = num('--credits'); if (credits !== undefined) body.credits_reward = credits;
|
|
67
|
+
const touches = csv(arg('--touches', args)); if (touches.length) body.touches = touches;
|
|
68
|
+
const criteria = csv(arg('--criteria', args));
|
|
69
|
+
if (criteria.length) body.criterion_ids = criteria;
|
|
70
|
+
|
|
71
|
+
return { body, criteria, errors };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function usage() {
|
|
75
|
+
console.error('usage:');
|
|
76
|
+
console.error(' bongos task new --title "…" --description "…" [--kind feature] [--discipline engineer]');
|
|
77
|
+
console.error(' [--priority 3] [--version <id>] [--goal <id>] [--criteria C1,C2]');
|
|
78
|
+
console.error(' [--touches a/,b/] [--est-minutes N] [--credits N] [--ready] [--json]');
|
|
79
|
+
console.error(' bongos task show <id> [--json]');
|
|
80
|
+
console.error(`\n kinds: ${VALID_KINDS.join(' | ')}`);
|
|
81
|
+
console.error(` disciplines: ${VALID_DISCIPLINES.join(' | ')}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function hallLink(base, id) {
|
|
85
|
+
try { return `${new URL(base).origin}/builders#/task/${id}`; } catch { return `task #${id}`; }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// The version_id a task defaults to when --version is omitted: the most-recent
|
|
89
|
+
// BUILDING version (the capture.js precedent, so idea-capture and task-create agree).
|
|
90
|
+
// Returns null when none is building — the caller then requires an explicit --version.
|
|
91
|
+
async function resolveDefaultVersion(api) {
|
|
92
|
+
const r = await api.versions.getVersions();
|
|
93
|
+
if (!r.ok) return null;
|
|
94
|
+
const building = (r.data?.versions ?? []).filter((v) => v.status === 'building');
|
|
95
|
+
if (building.length === 0) return null;
|
|
96
|
+
building.sort((a, b) => {
|
|
97
|
+
const ta = a.started_at ? new Date(a.started_at).getTime() : 0;
|
|
98
|
+
const tb = b.started_at ? new Date(b.started_at).getTime() : 0;
|
|
99
|
+
return tb - ta;
|
|
100
|
+
});
|
|
101
|
+
return building[0].id;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function cmdNew(args) {
|
|
105
|
+
const { body, errors } = buildTaskBody(args);
|
|
106
|
+
if (errors.length) { console.error('task new: ' + errors.join('; ') + '\n'); usage(); process.exit(2); }
|
|
107
|
+
const api = await cliClient();
|
|
108
|
+
// version_id is required by the API; default to the active (building) version so a
|
|
109
|
+
// new owner never has to know the id, but keep --version as the explicit override.
|
|
110
|
+
if (!body.version_id) {
|
|
111
|
+
body.version_id = await resolveDefaultVersion(api);
|
|
112
|
+
if (!body.version_id) {
|
|
113
|
+
console.error('task new: could not resolve an active version — pass --version <id> (see `bongos status`).');
|
|
114
|
+
process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const r = await api.tasks.postTasks({ body });
|
|
118
|
+
if (r.status === 403) {
|
|
119
|
+
console.error('Creating a task is Archon-gated (creating work shapes scope). Your rank is lower, so the server declined.');
|
|
120
|
+
console.error('To propose it instead, capture an idea for triage:');
|
|
121
|
+
console.error(' bongos exec scripts/gds/capture.js "<your idea>"');
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
if (r.status !== 201 || !r.data?.task) {
|
|
125
|
+
console.error(`task new failed (${r.status}):`, r.data);
|
|
126
|
+
process.exit(1);
|
|
127
|
+
}
|
|
128
|
+
const task = r.data.task;
|
|
129
|
+
if (hasFlag('--json', args)) { console.log(JSON.stringify(task, null, 2)); return; }
|
|
130
|
+
const { API_BASE } = require('./cli-lib');
|
|
131
|
+
console.log(`✓ Created task #${task.id} [${task.status}] — ${task.title}`);
|
|
132
|
+
console.log(` kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}`);
|
|
133
|
+
console.log(` ${hallLink(API_BASE, task.id)}`);
|
|
134
|
+
if (task.status !== 'ready') console.log(` (status '${task.status}' — pass --ready to make it claimable now, deps permitting)`);
|
|
135
|
+
console.log(` next: bongos claim ${task.id}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function cmdShow(args) {
|
|
139
|
+
const id = Number(args[0]);
|
|
140
|
+
if (!Number.isInteger(id) || id <= 0) { console.error('usage: bongos task show <id>'); process.exit(2); }
|
|
141
|
+
const api = await cliClient();
|
|
142
|
+
const r = await api.tasks.getTasksId({ id });
|
|
143
|
+
if (r.status === 404) { console.error(`task #${id} not found.`); process.exit(1); }
|
|
144
|
+
if (r.status !== 200 || !r.data?.task) { console.error(`task show failed (${r.status}):`, r.data); process.exit(1); }
|
|
145
|
+
const task = r.data.task;
|
|
146
|
+
if (hasFlag('--json', args)) { console.log(JSON.stringify(task, null, 2)); return; }
|
|
147
|
+
console.log(`#${task.id} [${task.status}] — ${task.title}`);
|
|
148
|
+
console.log(`kind=${task.kind} · discipline=${task.discipline}${task.priority ? ` · P${task.priority}` : ''}${task.goal_id ? ` · goal ${task.goal_id}` : ''}${task.version_id ? ` · ${task.version_id}` : ''}`);
|
|
149
|
+
if (task.description) console.log(`\n${task.description}`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function main() {
|
|
153
|
+
const args = process.argv.slice(2);
|
|
154
|
+
const sub = args[0];
|
|
155
|
+
if (!sub || sub === 'help' || sub === '--help' || sub === '-h') { usage(); process.exit(sub ? 0 : 2); }
|
|
156
|
+
await requireSession();
|
|
157
|
+
if (sub === 'new') return cmdNew(args.slice(1));
|
|
158
|
+
if (sub === 'show') return cmdShow(args.slice(1));
|
|
159
|
+
console.error(`task: unknown subcommand "${sub}"\n`); usage(); process.exit(2);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (require.main === module) {
|
|
163
|
+
main().catch((err) => { console.error('fatal:', err); process.exit(1); });
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
module.exports = { buildTaskBody, VALID_KINDS, VALID_DISCIPLINES, csv, hallLink };
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
//
|
|
4
|
+
// scripts/gds/update-channel.js — the update-subscription POLICY core (task 2150, ADR 0136).
|
|
5
|
+
//
|
|
6
|
+
// "How does a consumer instance subscribe to regular @bongos/core updates?" The owner-chosen
|
|
7
|
+
// answer (2026-07-09): **patch-only by default, health-gated, auto-rollback, opt-in, autonomy-gated.**
|
|
8
|
+
// This module is the PURE, testable heart of that policy — the semver + channel math + the
|
|
9
|
+
// subscription-manifest loader. It launches nothing and mutates nothing; the scheduled routine
|
|
10
|
+
// (.claude/scheduled-tasks/core-update-subscription/subscribe.js) drives the actual `bongos upgrade`
|
|
11
|
+
// per subscribed instance using what this module resolves.
|
|
12
|
+
//
|
|
13
|
+
// A per-instance UPDATE CHANNEL bounds how far an unattended upgrade may jump:
|
|
14
|
+
// pinned — never auto-upgrade (manual `bongos upgrade` only).
|
|
15
|
+
// patch — take a higher PATCH within the same major.minor (1.17.1 → 1.17.4, never 1.18.0). DEFAULT.
|
|
16
|
+
// minor — take a higher MINOR within the same major (1.17.1 → 1.18.0, never 2.0.0).
|
|
17
|
+
// A MAJOR bump is NEVER automatic under any channel — a major is a breaking change that wants a human.
|
|
18
|
+
// Prereleases (1.17.2-rc.1) are never auto-target either — only stable releases.
|
|
19
|
+
//
|
|
20
|
+
// WHY the registry as the source (not vendored tarballs): a subscription needs a LIVE source of
|
|
21
|
+
// truth for "what's the newest published core?" — that's `npm view @bongos/core versions` against the
|
|
22
|
+
// private registry (ADR 0134, task 2090). The vendored path is for a one-off manual bump.
|
|
23
|
+
|
|
24
|
+
const fs = require('node:fs');
|
|
25
|
+
const path = require('node:path');
|
|
26
|
+
const { spawnSync } = require('node:child_process');
|
|
27
|
+
|
|
28
|
+
const CORE_PKG = '@bongos/core';
|
|
29
|
+
const VALID_CHANNELS = ['pinned', 'patch', 'minor'];
|
|
30
|
+
const DEFAULT_CHANNEL = 'patch';
|
|
31
|
+
// The subscription manifest an operator edits to opt instances in. Ships EMPTY (instances: []) in the
|
|
32
|
+
// core template — a fresh instance subscribes nothing until the operator lists a target here.
|
|
33
|
+
const SUBSCRIPTIONS_REL = path.join('config', 'update-subscriptions.json');
|
|
34
|
+
|
|
35
|
+
// ---- semver (stable x.y.z, optional prerelease tag) -------------------------
|
|
36
|
+
// A deliberately small parser — the repo avoids an npm-semver dependency in the CLI path (see
|
|
37
|
+
// upgrade.js versionFromDep). We only need compare + the major/minor/patch fields.
|
|
38
|
+
|
|
39
|
+
// Parse "1.17.2" / "v1.17.2" / "1.17.2-rc.1" → { major, minor, patch, pre } or null. `pre` is the
|
|
40
|
+
// prerelease tag ('' for a plain stable release), used only to EXCLUDE prereleases from auto-targets.
|
|
41
|
+
function parseSemver(v) {
|
|
42
|
+
const m = String(v == null ? '' : v).trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/);
|
|
43
|
+
if (!m) return null;
|
|
44
|
+
return { major: +m[1], minor: +m[2], patch: +m[3], pre: m[4] || '' };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isStable(v) {
|
|
48
|
+
const p = parseSemver(v);
|
|
49
|
+
return !!p && p.pre === '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Compare two version strings by (major, minor, patch). Returns -1 / 0 / 1. A stable release sorts
|
|
53
|
+
// ABOVE its own prereleases (1.17.2 > 1.17.2-rc.1), matching semver precedence — enough for "is this
|
|
54
|
+
// candidate newer than what's installed?" Unparseable inputs sort as -Infinity (never a target).
|
|
55
|
+
function compareSemver(a, b) {
|
|
56
|
+
const pa = parseSemver(a);
|
|
57
|
+
const pb = parseSemver(b);
|
|
58
|
+
if (!pa && !pb) return 0;
|
|
59
|
+
if (!pa) return -1;
|
|
60
|
+
if (!pb) return 1;
|
|
61
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
62
|
+
if (pa[k] !== pb[k]) return pa[k] < pb[k] ? -1 : 1;
|
|
63
|
+
}
|
|
64
|
+
// Same x.y.z: a prerelease is LOWER than the stable release. (Two prereleases: not ordered
|
|
65
|
+
// precisely here — we never auto-target a prerelease, so their relative order is immaterial.)
|
|
66
|
+
if (pa.pre && !pb.pre) return -1;
|
|
67
|
+
if (!pa.pre && pb.pre) return 1;
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function normalizeChannel(channel) {
|
|
72
|
+
const c = String(channel == null ? '' : channel).trim().toLowerCase();
|
|
73
|
+
return VALID_CHANNELS.includes(c) ? c : DEFAULT_CHANNEL;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Would `channel` permit an unattended jump from `installed` to `candidate`? True only when candidate
|
|
77
|
+
// is a STABLE release strictly newer than installed AND within the channel's blast radius. Any invalid
|
|
78
|
+
// input, an unknown installed version, a prerelease candidate, or a major bump ⇒ false (fail-closed).
|
|
79
|
+
function channelAllows(installed, candidate, channel) {
|
|
80
|
+
const ch = normalizeChannel(channel);
|
|
81
|
+
if (ch === 'pinned') return false;
|
|
82
|
+
const from = parseSemver(installed);
|
|
83
|
+
const to = parseSemver(candidate);
|
|
84
|
+
if (!from || !to) return false;
|
|
85
|
+
if (to.pre) return false; // never auto-target a prerelease
|
|
86
|
+
if (compareSemver(candidate, installed) <= 0) return false; // must be strictly newer
|
|
87
|
+
if (to.major !== from.major) return false; // a major bump is never automatic
|
|
88
|
+
if (ch === 'patch' && to.minor !== from.minor) return false; // patch stays on the same minor
|
|
89
|
+
return true; // 'minor' allows any higher minor within the major
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// The highest version in `available` that `channel` permits from `installed`, or null if none
|
|
93
|
+
// (nothing newer, pinned, only prereleases/majors ahead, or an unknown installed version). This is
|
|
94
|
+
// the one function the routine asks: "given where this instance is and what it subscribes to, what —
|
|
95
|
+
// if anything — should I upgrade it to right now?"
|
|
96
|
+
function resolveChannelTarget({ installed, channel, available } = {}) {
|
|
97
|
+
const list = Array.isArray(available) ? available : [];
|
|
98
|
+
let best = null;
|
|
99
|
+
for (const v of list) {
|
|
100
|
+
if (!channelAllows(installed, v, channel)) continue;
|
|
101
|
+
if (best === null || compareSemver(v, best) > 0) best = v;
|
|
102
|
+
}
|
|
103
|
+
return best;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- registry: what versions of the core are published? ---------------------
|
|
107
|
+
|
|
108
|
+
// List published versions of @bongos/core from the registry via `npm view … versions --json`
|
|
109
|
+
// (ADR 0134 — the private scoped package resolves through the instance's env-fed .npmrc, so run this
|
|
110
|
+
// with cwd inside an instance that has one + NPM_TOKEN in env). Fail-SOFT: any error returns
|
|
111
|
+
// { ok:false } so the routine skips that instance rather than crashing the whole sweep. `run` +
|
|
112
|
+
// `pkg` are injectable for tests.
|
|
113
|
+
function listAvailableVersions({ cwd = process.cwd(), pkg = CORE_PKG, run = spawnSync, env = process.env } = {}) {
|
|
114
|
+
let res;
|
|
115
|
+
try {
|
|
116
|
+
res = run('npm', ['view', pkg, 'versions', '--json'], { cwd, env, encoding: 'utf8' });
|
|
117
|
+
} catch (e) {
|
|
118
|
+
return { ok: false, error: e && e.message ? e.message : String(e), versions: [] };
|
|
119
|
+
}
|
|
120
|
+
if (!res || res.status !== 0) {
|
|
121
|
+
const reason = (res && (res.stderr || res.error && res.error.message)) || `exit ${res && res.status}`;
|
|
122
|
+
return { ok: false, error: String(reason).trim().split('\n')[0] || 'npm view failed', versions: [] };
|
|
123
|
+
}
|
|
124
|
+
return parseNpmViewVersions(res.stdout);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Parse `npm view … versions --json` stdout. npm prints a JSON ARRAY for a multi-version package, a
|
|
128
|
+
// bare JSON STRING for a single-version one, and nothing for an unpublished name. Normalizes all
|
|
129
|
+
// three to { ok:true, versions:[…] }. Exported + unit-tested (the shape has bitten callers before).
|
|
130
|
+
function parseNpmViewVersions(stdout) {
|
|
131
|
+
const s = String(stdout == null ? '' : stdout).trim();
|
|
132
|
+
if (!s) return { ok: true, versions: [] };
|
|
133
|
+
let parsed;
|
|
134
|
+
try { parsed = JSON.parse(s); } catch (e) { return { ok: false, error: `unparseable npm output: ${e.message}`, versions: [] }; }
|
|
135
|
+
if (Array.isArray(parsed)) return { ok: true, versions: parsed.map(String) };
|
|
136
|
+
if (typeof parsed === 'string') return { ok: true, versions: [parsed] };
|
|
137
|
+
return { ok: false, error: 'unexpected npm view shape', versions: [] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ---- subscription manifest --------------------------------------------------
|
|
141
|
+
|
|
142
|
+
// Read + normalize config/update-subscriptions.json — the operator-edited opt-in list of instances
|
|
143
|
+
// this control plane keeps up to date. Never throws: a missing/malformed file ⇒ an empty roster
|
|
144
|
+
// (subscribe nothing), because an unreadable manifest must not upgrade anything. Each entry is
|
|
145
|
+
// normalized to { slug, dir, service, healthUrl, channel } with the channel validated + defaulted.
|
|
146
|
+
function loadSubscriptions({ instanceDir = process.cwd(), fsImpl = fs, file = SUBSCRIPTIONS_REL } = {}) {
|
|
147
|
+
const abs = path.isAbsolute(file) ? file : path.join(instanceDir, file);
|
|
148
|
+
let raw;
|
|
149
|
+
try { raw = fsImpl.readFileSync(abs, 'utf8'); } catch (_) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: false }; }
|
|
150
|
+
let json;
|
|
151
|
+
try { json = JSON.parse(raw); } catch (e) { return { defaultChannel: DEFAULT_CHANNEL, instances: [], source: abs, present: true, error: `malformed JSON: ${e.message}` }; }
|
|
152
|
+
const defaultChannel = normalizeChannel(json.defaultChannel);
|
|
153
|
+
const rows = Array.isArray(json.instances) ? json.instances : [];
|
|
154
|
+
const instances = rows
|
|
155
|
+
.filter((r) => r && typeof r === 'object' && r.dir) // an entry without a dir can't be upgraded — drop it
|
|
156
|
+
.map((r) => ({
|
|
157
|
+
slug: String(r.slug || r.dir),
|
|
158
|
+
dir: String(r.dir),
|
|
159
|
+
service: r.service ? String(r.service) : null,
|
|
160
|
+
healthUrl: r.healthUrl ? String(r.healthUrl) : null,
|
|
161
|
+
channel: normalizeChannel(r.channel == null ? defaultChannel : r.channel),
|
|
162
|
+
// Per-instance env merged into the `bongos upgrade` child (e.g. PGDATABASE so the migrate step
|
|
163
|
+
// hits THIS instance's DB, not the control plane's — see the live-op learnings for demo). Only
|
|
164
|
+
// string values are kept; anything else is ignored.
|
|
165
|
+
env: (r.env && typeof r.env === 'object' && !Array.isArray(r.env))
|
|
166
|
+
? Object.fromEntries(Object.entries(r.env).filter(([, v]) => typeof v === 'string'))
|
|
167
|
+
: {},
|
|
168
|
+
}));
|
|
169
|
+
return { defaultChannel, instances, source: abs, present: true };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
CORE_PKG,
|
|
174
|
+
VALID_CHANNELS,
|
|
175
|
+
DEFAULT_CHANNEL,
|
|
176
|
+
SUBSCRIPTIONS_REL,
|
|
177
|
+
parseSemver,
|
|
178
|
+
isStable,
|
|
179
|
+
compareSemver,
|
|
180
|
+
normalizeChannel,
|
|
181
|
+
channelAllows,
|
|
182
|
+
resolveChannelTarget,
|
|
183
|
+
listAvailableVersions,
|
|
184
|
+
parseNpmViewVersions,
|
|
185
|
+
loadSubscriptions,
|
|
186
|
+
};
|