@bongos/core 1.21.146 → 1.21.148
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bongos-core.json +98 -68
- package/bin/bongos.js +5 -2
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +6 -2
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +86 -7
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +17 -15
- package/docs/copy-registry.json +37 -19
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +70 -70
- package/modules/builder-settings/core-move-seen.js +41 -0
- package/modules/builder-settings/routes/settings.js +22 -0
- package/modules/builder-settings/settings.js +5 -0
- package/modules/hall-ui/public/atlas.html +1 -1
- package/modules/hall-ui/public/blockers.html +1 -1
- package/modules/hall-ui/public/board-room.html +1 -1
- package/modules/hall-ui/public/city.html +1 -1
- package/modules/hall-ui/public/collab.html +1 -1
- package/modules/hall-ui/public/commands.html +1 -1
- package/modules/hall-ui/public/copy-desk.html +1 -1
- package/modules/hall-ui/public/core-move-notice.js +70 -0
- package/modules/hall-ui/public/deploy.html +1 -1
- package/modules/hall-ui/public/diagrams.html +1 -1
- package/modules/hall-ui/public/drachmae.html +1 -1
- package/modules/hall-ui/public/fleet.html +1 -1
- package/modules/hall-ui/public/gate.html +1 -1
- package/modules/hall-ui/public/goal-map.html +1 -1
- package/modules/hall-ui/public/goals.html +1 -1
- package/modules/hall-ui/public/government.html +1 -1
- package/modules/hall-ui/public/idea.html +1 -1
- package/modules/hall-ui/public/ideas.html +1 -1
- package/modules/hall-ui/public/index.html +1 -1
- package/modules/hall-ui/public/modules.html +1 -1
- package/modules/hall-ui/public/my-agents.html +1 -1
- package/modules/hall-ui/public/primer.html +1 -1
- package/modules/hall-ui/public/profile.html +1 -1
- package/modules/hall-ui/public/project-settings.html +1 -1
- package/modules/hall-ui/public/ranks.html +1 -1
- package/modules/hall-ui/public/roadmap.html +1 -1
- package/modules/hall-ui/public/roster.html +1 -1
- package/modules/hall-ui/public/sessions.html +1 -1
- package/modules/hall-ui/public/settings-software-update.js +24 -2
- package/modules/hall-ui/public/settings.html +2 -2
- package/modules/hall-ui/public/shell.js +8 -0
- package/modules/hall-ui/public/street.html +1 -1
- package/modules/hall-ui/public/studio.html +1 -1
- package/modules/hall-ui/public/style.css +11 -0
- package/modules/hall-ui/public/task.html +1 -1
- package/modules/hall-ui/public/thinking.html +1 -1
- package/modules/hall-ui/public/tweak-editor.html +1 -1
- package/modules/hall-ui/public/watch.html +1 -1
- package/modules/hall-ui/public/work.html +1 -1
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/core-origin-follow.js +185 -0
- package/scripts/gds/core-pin-sync.js +31 -5
- package/scripts/gds/provision-core-upgrade.js +9 -3
- package/src/bongos/core-moves.js +155 -0
- package/src/bongos/routes/core-update.js +43 -3
- package/src/module-api.js +1 -1
- package/tests/core_move_notice.mjs +356 -0
- package/tests/core_origin_follow.mjs +287 -0
- package/tests/core_pin_sync.mjs +7 -2
- package/tests/nav_permission_atoms.mjs +3 -2
|
@@ -167,7 +167,7 @@
|
|
|
167
167
|
chrome exists before the page script runs. -->
|
|
168
168
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
169
169
|
<script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
|
|
170
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
170
|
+
<script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
|
|
171
171
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
172
172
|
<script src="/builders/palette.js" defer></script>
|
|
173
173
|
<!-- shared frontend toolkit (window.OTB) — loaded before watch.js, which pulls from it. -->
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
time so the chrome exists before the deferred page scripts execute. -->
|
|
174
174
|
<script src="/builders/shell-trail.js?v=2026-10-08-city-tab"></script>
|
|
175
175
|
<script src="/builders/craft-mode.js?v=2026-10-09-living3"></script>
|
|
176
|
-
<script src="/builders/shell.js?v=2026-10-
|
|
176
|
+
<script src="/builders/shell.js?v=2026-10-18-core-move1"></script>
|
|
177
177
|
<!-- jump palette (Ctrl/Cmd-K). Optional by design: without it the shell's slot stays hidden. -->
|
|
178
178
|
<script src="/builders/palette.js" defer></script>
|
|
179
179
|
<!-- shared frontend toolkit (window.OTB) — loaded FIRST so work.js can pull from it. -->
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.148",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.21.
|
|
9
|
+
"version": "1.21.148",
|
|
10
10
|
"license": "AGPL-3.0-or-later",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"express": "^4.21.2",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.148",
|
|
4
4
|
"description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"main": "src/platform-server.js",
|
package/release-notes.json
CHANGED
|
@@ -9890,5 +9890,17 @@
|
|
|
9890
9890
|
"id": "1004730",
|
|
9891
9891
|
"text": "When an automatic core update fails on a hosted project and is rolled back, that version is not retried over and over, and the project's owner now sees in their own Settings and update banner which version was tried, that it w"
|
|
9892
9892
|
}
|
|
9893
|
+
],
|
|
9894
|
+
"1.21.147": [
|
|
9895
|
+
{
|
|
9896
|
+
"id": "1004731",
|
|
9897
|
+
"text": "After a project's software is updated, its owner now sees a one-time note saying which version it moved to, when, and what changed, and the Software update page lists the recent updates, including any that failed and were undo"
|
|
9898
|
+
}
|
|
9899
|
+
],
|
|
9900
|
+
"1.21.148": [
|
|
9901
|
+
{
|
|
9902
|
+
"id": "1004732",
|
|
9903
|
+
"text": "When the hub upgrades a project's core, your laptop now notices at the next session start or ship, says so in one line, and on a clean main catches up and reinstalls the new core by itself."
|
|
9904
|
+
}
|
|
9893
9905
|
]
|
|
9894
9906
|
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// scripts/gds/core-origin-follow.js — the laptop follows a core move the hub made (task 1004732)
|
|
2
|
+
//
|
|
3
|
+
// WHY. When the hub moves a hosted project's core (provision-core-upgrade.js), it commits
|
|
4
|
+
// the new pin — package.json + lockfile, usually a vendored `file:vendor/bongos-core-X.tgz`
|
|
5
|
+
// rewritten to an exact registry version — on the box and pushes it to the project's
|
|
6
|
+
// GitHub repo. The owner's laptop checkout never hears of it: the next session runs the
|
|
7
|
+
// old core, and the next ship merges against the old pin. Seen on toast (1.21.20 on the
|
|
8
|
+
// laptop while fixes sat in newer releases).
|
|
9
|
+
//
|
|
10
|
+
// THE FIX. Before `bongos start` (an instance's session start: its CLAUDE.md opens every
|
|
11
|
+
// session with it, and a package-installed instance carries no SessionStart hooks) and
|
|
12
|
+
// before `bongos ship`, the dispatcher calls ensureCurrentCore(). It fetches origin/main
|
|
13
|
+
// (bounded; allowed to fail) and compares the checkout's @bongos/core pin with
|
|
14
|
+
// origin/main's. When ORIGIN moved it, the builder gets one plain line saying so. On a
|
|
15
|
+
// clean `main` with no local commits, the checkout is fast-forwarded and the pinned core
|
|
16
|
+
// is installed through core-pin-sync.js (task 1004632) — whose gates (the pin's shape,
|
|
17
|
+
// and the pin having landed on main) still decide what may be installed — then the
|
|
18
|
+
// command re-runs under the new core. A dirty tree, a feature branch or local commits get
|
|
19
|
+
// the line and the command to run, never a forced change. So does a move that is not
|
|
20
|
+
// pin-only (notPinOnly): the fast-forward installs and runs what it brings in, so it is
|
|
21
|
+
// taken only for the hub's commit shape — the pin, the lockfile, a removed tarball — onto
|
|
22
|
+
// a published registry release; anything wider waits for the owner to look.
|
|
23
|
+
//
|
|
24
|
+
// Silent and fail-open: in the core repo itself (the running package is not
|
|
25
|
+
// @bongos/core) it returns before any git spawn; outside git, with no pin, when origin
|
|
26
|
+
// cannot be read, or when the pins already agree it says nothing. A pin the LOCAL side
|
|
27
|
+
// moved (origin's pin is still the one this branch forked from) is not a hub move.
|
|
28
|
+
|
|
29
|
+
'use strict';
|
|
30
|
+
|
|
31
|
+
const fs = require('node:fs');
|
|
32
|
+
const path = require('node:path');
|
|
33
|
+
const { spawnSync } = require('node:child_process');
|
|
34
|
+
const pinSync = require('./core-pin-sync.js');
|
|
35
|
+
|
|
36
|
+
const FETCH_TIMEOUT_MS = 5000;
|
|
37
|
+
|
|
38
|
+
function readJson(file, fsImpl = fs) {
|
|
39
|
+
try { return JSON.parse(String(fsImpl.readFileSync(file, 'utf8')).replace(/^/, '')); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function gitOut(args, cwd, run, extra = {}) {
|
|
43
|
+
try {
|
|
44
|
+
const r = run('git', args, { cwd, encoding: 'utf8', ...extra });
|
|
45
|
+
return r && r.status === 0 ? String(r.stdout || '').trim() : null;
|
|
46
|
+
} catch { return null; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function specOf(pkg) {
|
|
50
|
+
if (!pkg) return null;
|
|
51
|
+
return (pkg.dependencies || {})[pinSync.CORE_PKG] || (pkg.devDependencies || {})[pinSync.CORE_PKG] || null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// The core pin package.json carries at `rev`, or null when that file cannot be read there.
|
|
55
|
+
function specAt(root, rev, run) {
|
|
56
|
+
const body = gitOut(['show', `${rev}:package.json`], root, run, { maxBuffer: 8 * 1024 * 1024 });
|
|
57
|
+
if (body == null) return null;
|
|
58
|
+
try { return specOf(JSON.parse(body.replace(/^/, ''))); } catch { return null; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const shown = (spec) => pinSync.versionFromSpec(spec) || spec;
|
|
62
|
+
|
|
63
|
+
// Why the move cannot be applied to this checkout automatically, or null when it can.
|
|
64
|
+
function blockedReason({ branch, dirty, ahead }) {
|
|
65
|
+
if (!branch) return 'this checkout is not on a branch';
|
|
66
|
+
if (branch !== 'main') return `you are on branch ${branch}, which picks the move up when it merges main`;
|
|
67
|
+
if (dirty == null || dirty !== '') return 'this checkout has uncommitted changes';
|
|
68
|
+
if (!Number.isFinite(ahead) || ahead !== 0) return 'main has local commits GitHub does not have';
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const REMEDY = 'On a clean main: `git pull --ff-only origin main`, then `npm ci`.';
|
|
73
|
+
const REVIEW_REMEDY = 'Review what GitHub has (`git log HEAD..origin/main`), then `git pull --ff-only origin main` and `npm ci`.';
|
|
74
|
+
const REGISTRY = 'https://registry.npmjs.org/';
|
|
75
|
+
const EXACT_SEMVER = /^\d+\.\d+\.\d+(-[A-Za-z0-9.]+)?$/;
|
|
76
|
+
|
|
77
|
+
function jsonAt(root, rev, file, run) {
|
|
78
|
+
const body = gitOut(['show', `${rev}:${file}`], root, run, { maxBuffer: 64 * 1024 * 1024 });
|
|
79
|
+
if (body == null) return null;
|
|
80
|
+
try { return JSON.parse(body.replace(/^/, '')); } catch { return null; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// The fast-forward installs and then RUNS what it brings in, unattended — so it is taken
|
|
84
|
+
// only when what GitHub has is the hub's pin move and nothing else (the shape
|
|
85
|
+
// provision-core-upgrade.js pushes). Anything wider is reported, never applied. Returns
|
|
86
|
+
// null when the move is pin-only, else the reason it is not.
|
|
87
|
+
// - the only files that differ are package.json, package-lock.json and a removed
|
|
88
|
+
// vendored tarball;
|
|
89
|
+
// - package.json differs ONLY in the @bongos/core spec;
|
|
90
|
+
// - the new spec is an exact version, i.e. a published release (a tarball brought in
|
|
91
|
+
// from GitHub is code nobody released);
|
|
92
|
+
// - the lockfile pins that version from the public registry, and every lockfile
|
|
93
|
+
// source it adds is the public registry too.
|
|
94
|
+
function notPinOnly(root, run) {
|
|
95
|
+
const diff = gitOut(['diff', '--name-status', '--no-renames', 'HEAD', 'origin/main'], root, run, { maxBuffer: 16 * 1024 * 1024 });
|
|
96
|
+
if (diff == null) return 'the change on GitHub could not be read';
|
|
97
|
+
for (const line of diff.split('\n').filter(Boolean)) {
|
|
98
|
+
const [status, file] = line.split('\t');
|
|
99
|
+
const ok = file === 'package.json' || file === 'package-lock.json' ||
|
|
100
|
+
(status === 'D' && /^vendor\/bongos-core-[^/]+\.tgz$/.test(file));
|
|
101
|
+
if (!ok) return 'GitHub has other changes besides the core move';
|
|
102
|
+
}
|
|
103
|
+
const before = jsonAt(root, 'HEAD', 'package.json', run);
|
|
104
|
+
const after = jsonAt(root, 'origin/main', 'package.json', run);
|
|
105
|
+
if (!before || !after) return 'package.json could not be read';
|
|
106
|
+
const spec = specOf(after);
|
|
107
|
+
const strip = (pkg) => {
|
|
108
|
+
const c = JSON.parse(JSON.stringify(pkg));
|
|
109
|
+
for (const k of ['dependencies', 'devDependencies']) if (c[k]) delete c[k][pinSync.CORE_PKG];
|
|
110
|
+
return JSON.stringify(c);
|
|
111
|
+
};
|
|
112
|
+
if (strip(before) !== strip(after)) return 'package.json changed beyond the core pin';
|
|
113
|
+
if (!EXACT_SEMVER.test(String(spec || ''))) return 'the new pin is not a published release';
|
|
114
|
+
const lock = jsonAt(root, 'origin/main', 'package-lock.json', run);
|
|
115
|
+
const entry = lock && lock.packages && lock.packages[`node_modules/${pinSync.CORE_PKG}`];
|
|
116
|
+
if (!entry || entry.version !== spec || !String(entry.resolved || '').startsWith(`${REGISTRY}${pinSync.CORE_PKG}/-/`)) {
|
|
117
|
+
return 'the lockfile does not pin that release from the public registry';
|
|
118
|
+
}
|
|
119
|
+
const prior = new Set(Object.values(((jsonAt(root, 'HEAD', 'package-lock.json', run) || {}).packages) || {}).map((p) => p && p.resolved));
|
|
120
|
+
for (const p of Object.values(lock.packages)) {
|
|
121
|
+
const r = p && p.resolved;
|
|
122
|
+
if (r && !prior.has(r) && !String(r).startsWith(REGISTRY)) return 'the lockfile adds a source outside the public registry';
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Returns { action: 'none' | 'reported' | 'fast-forwarded', reason, from?, to? }.
|
|
128
|
+
function followOriginPin({ coreRoot, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, fetchTimeoutMs = FETCH_TIMEOUT_MS } = {}) {
|
|
129
|
+
const none = (reason) => ({ action: 'none', reason });
|
|
130
|
+
if (env && (env.BONGOS_SKIP_CORE_SYNC || env[pinSync.SYNCED_ENV])) return none('skipped');
|
|
131
|
+
const running = readJson(path.join(coreRoot, 'package.json'), fsImpl);
|
|
132
|
+
if (!running || running.name !== pinSync.CORE_PKG) return none('core-repo'); // no git spawn
|
|
133
|
+
const top = gitOut(['rev-parse', '--show-toplevel'], cwd, run);
|
|
134
|
+
if (!top) return none('no-git');
|
|
135
|
+
const root = path.resolve(top);
|
|
136
|
+
const local = specOf(readJson(path.join(root, 'package.json'), fsImpl));
|
|
137
|
+
if (!local) return none('no-pin');
|
|
138
|
+
|
|
139
|
+
// Bounded, quiet, never prompts; a failure is fine — the ref below decides.
|
|
140
|
+
gitOut(['fetch', '--quiet', 'origin', 'main'], root, run, {
|
|
141
|
+
timeout: fetchTimeoutMs, env: { ...process.env, ...env, GIT_TERMINAL_PROMPT: '0' },
|
|
142
|
+
});
|
|
143
|
+
const origin = specAt(root, 'origin/main', run);
|
|
144
|
+
if (!origin) return none('origin-unreadable');
|
|
145
|
+
if (origin === local) return none('in-step');
|
|
146
|
+
const forkPoint = gitOut(['merge-base', 'HEAD', 'origin/main'], root, run);
|
|
147
|
+
if (forkPoint && specAt(root, forkPoint, run) === origin) return none('local-change');
|
|
148
|
+
|
|
149
|
+
const say = log || ((s) => process.stderr.write(s + '\n'));
|
|
150
|
+
const move = { from: local, to: origin };
|
|
151
|
+
say(`bongos: this project was upgraded to core ${shown(origin)} on GitHub (this checkout has ${shown(local)}).`);
|
|
152
|
+
const why = blockedReason({
|
|
153
|
+
branch: gitOut(['symbolic-ref', '--short', '-q', 'HEAD'], root, run),
|
|
154
|
+
dirty: gitOut(['status', '--porcelain', '--untracked-files=no'], root, run),
|
|
155
|
+
ahead: Number(gitOut(['rev-list', '--count', 'origin/main..HEAD'], root, run)),
|
|
156
|
+
});
|
|
157
|
+
if (why) {
|
|
158
|
+
say(` Not updated here: ${why}. ${REMEDY}`);
|
|
159
|
+
return { action: 'reported', reason: why, ...move };
|
|
160
|
+
}
|
|
161
|
+
const wider = notPinOnly(root, run);
|
|
162
|
+
if (wider) {
|
|
163
|
+
say(` Not updated automatically: ${wider}. ${REVIEW_REMEDY}`);
|
|
164
|
+
return { action: 'reported', reason: wider, ...move };
|
|
165
|
+
}
|
|
166
|
+
if (gitOut(['merge', '--ff-only', '--quiet', 'origin/main'], root, run) == null) {
|
|
167
|
+
say(` The fast-forward did not go through. ${REMEDY}`);
|
|
168
|
+
return { action: 'reported', reason: 'ff-failed', ...move };
|
|
169
|
+
}
|
|
170
|
+
say(' Fast-forwarded this checkout to it; installing that core next.');
|
|
171
|
+
return { action: 'fast-forwarded', reason: 'origin-moved', ...move };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// The dispatcher's one call, for `ship` and `start`. Returns an exit code when it handled
|
|
175
|
+
// the command (installed and re-ran, or refused), or null to run the verb as it is.
|
|
176
|
+
// `ship` always checks the running core against the pin (task 1004632); `start` only
|
|
177
|
+
// after a fast-forward actually moved the pin, so a normal session start pays no more
|
|
178
|
+
// than the fetch.
|
|
179
|
+
function ensureCurrentCore({ coreRoot, argv, verb, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
|
|
180
|
+
const followed = followOriginPin({ coreRoot, cwd, env, run, fsImpl, log });
|
|
181
|
+
if (verb !== 'ship' && followed.action !== 'fast-forwarded') return null;
|
|
182
|
+
return pinSync.ensurePinnedCore({ coreRoot, argv, cwd, env, run, fsImpl, log, ...(npmCli !== undefined ? { npmCli } : {}) });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
module.exports = { followOriginPin, ensureCurrentCore };
|
|
@@ -172,6 +172,28 @@ function unlinkLink(linkPath, { platform = process.platform, fsImpl = fs } = {})
|
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
174
|
|
|
175
|
+
// A vendored instance keeps an untracked `scripts -> node_modules/@bongos/core/scripts`
|
|
176
|
+
// link in its main checkout (grading needs it; task 1004728). After an install it must
|
|
177
|
+
// still resolve (task 1004732): a link left dangling is re-pointed at the installed core.
|
|
178
|
+
// Only ever a LINK — a real scripts/ directory (the core repo) is never touched.
|
|
179
|
+
// Returns 'absent' | 'not-a-link' | 'ok' | 'repointed' | 'unresolved'.
|
|
180
|
+
function healScriptsLink(root, { fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), platform = process.platform } = {}) {
|
|
181
|
+
const link = path.join(root, 'scripts');
|
|
182
|
+
let st = null;
|
|
183
|
+
try { st = fsImpl.lstatSync(link); } catch { return 'absent'; }
|
|
184
|
+
if (!st.isSymbolicLink()) return 'not-a-link';
|
|
185
|
+
if (fsImpl.existsSync(path.join(link, 'gds'))) return 'ok';
|
|
186
|
+
const target = path.join(root, 'node_modules', '@bongos', 'core', 'scripts');
|
|
187
|
+
const fix = `bongos: the \`scripts\` link in ${root} no longer resolves; point it at ${target}.`;
|
|
188
|
+
if (!fsImpl.existsSync(target)) { log(fix); return 'unresolved'; }
|
|
189
|
+
try {
|
|
190
|
+
unlinkLink(link, { platform, fsImpl });
|
|
191
|
+
fsImpl.symlinkSync(target, link, 'junction');
|
|
192
|
+
} catch { log(fix); return 'unresolved'; }
|
|
193
|
+
log('bongos: re-pointed the `scripts` link at the installed core.');
|
|
194
|
+
return 'repointed';
|
|
195
|
+
}
|
|
196
|
+
|
|
175
197
|
// Install the pinned core into `root` and re-run the same bongos command under it.
|
|
176
198
|
// Returns the exit code to use.
|
|
177
199
|
function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => process.stderr.write(s + '\n'), env = process.env, platform = process.platform, npmCli = npmCliPath(process.execPath, fsImpl) } = {}) {
|
|
@@ -192,10 +214,13 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
192
214
|
// npm's JS entry under this node — no shell. Fallback: `npm`, which on win32 is
|
|
193
215
|
// npm.cmd and needs a shell (Node refuses a .cmd without one); the args are fixed
|
|
194
216
|
// strings, never branch data.
|
|
217
|
+
// npm's output goes to stderr (task 1004732): `bongos start --widget` hands stdout to
|
|
218
|
+
// the renderer verbatim, so an install must never print there.
|
|
195
219
|
const cli = npmCli;
|
|
220
|
+
const stdio = ['ignore', 2, 2];
|
|
196
221
|
const inst = cli
|
|
197
|
-
? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio
|
|
198
|
-
: run('npm', installArgs(hasLock), { cwd: root, stdio
|
|
222
|
+
? run(process.execPath, [cli, ...installArgs(hasLock)], { cwd: root, stdio, env })
|
|
223
|
+
: run('npm', installArgs(hasLock), { cwd: root, stdio, env, ...(platform === 'win32' ? { shell: true } : {}) });
|
|
199
224
|
if (!inst || inst.error || inst.status !== 0) {
|
|
200
225
|
log(`bongos: installing core ${pinned} failed.`);
|
|
201
226
|
remedy();
|
|
@@ -203,6 +228,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
203
228
|
}
|
|
204
229
|
const bin = path.join(nm, '@bongos', 'core', 'bin', 'bongos.js');
|
|
205
230
|
if (!fsImpl.existsSync(bin)) { remedy(); return 1; }
|
|
231
|
+
healScriptsLink(root, { fsImpl, log, platform });
|
|
206
232
|
const res = run(process.execPath, [bin, ...argv.slice(2)], { stdio: 'inherit', env: { ...env, [SYNCED_ENV]: '1' } });
|
|
207
233
|
if (!res || res.error) return 1;
|
|
208
234
|
return res.status == null ? 1 : res.status;
|
|
@@ -210,7 +236,7 @@ function syncAndRerun(plan, { argv, run = spawnSync, fsImpl = fs, log = (s) => p
|
|
|
210
236
|
|
|
211
237
|
// The dispatcher's one call. Returns an exit code when it handled the command (synced
|
|
212
238
|
// and re-ran, or refused), or null to run the verb on the current core.
|
|
213
|
-
function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log } = {}) {
|
|
239
|
+
function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.env, run = spawnSync, fsImpl = fs, log, npmCli } = {}) {
|
|
214
240
|
if (env && env.BONGOS_SKIP_CORE_SYNC) return null;
|
|
215
241
|
const runningPkg = readJson(path.join(coreRoot, 'package.json'), fsImpl);
|
|
216
242
|
if (!runningPkg || runningPkg.name !== CORE_PKG) return null; // fast path: the core repo itself
|
|
@@ -227,7 +253,7 @@ function ensurePinnedCore({ coreRoot, argv, cwd = process.cwd(), env = process.e
|
|
|
227
253
|
say(remedyLine({ ...plan, hasLock: fsImpl.existsSync(path.join(root, 'package-lock.json')) }));
|
|
228
254
|
return 1;
|
|
229
255
|
}
|
|
230
|
-
return syncAndRerun(plan, { argv, run, fsImpl, log: say, env });
|
|
256
|
+
return syncAndRerun(plan, { argv, run, fsImpl, log: say, env, ...(npmCli !== undefined ? { npmCli } : {}) });
|
|
231
257
|
}
|
|
232
258
|
|
|
233
|
-
module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, ensurePinnedCore };
|
|
259
|
+
module.exports = { CORE_PKG, SYNCED_ENV, LANDED_REFS, pinLanded, npmCliPath, parseSpec, versionFromSpec, installArgs, pinnedCoreVersion, planCoreSync, remedyLine, syncAndRerun, healScriptsLink, ensurePinnedCore };
|
|
@@ -321,7 +321,10 @@ async function coreUpgradeInstance(inst, deps, intent) {
|
|
|
321
321
|
// the one field task 1004121 exists to keep identical. Skipped when !apply, where the
|
|
322
322
|
// command is printed and never run — a plan should not spend a subprocess per tick.
|
|
323
323
|
const { runAs, migrateAs } = resolveRunAs(inst, deps, { probe: apply });
|
|
324
|
-
|
|
324
|
+
// A move the hub queued by itself (provision-auto-upgrade.js) says so in the project's
|
|
325
|
+
// own ledger, so its hall can tell its owner "upgraded automatically" (task 1004731).
|
|
326
|
+
const automatic = !!intent && intent.requested_by === 'runner:auto-upgrade';
|
|
327
|
+
const { cmd, runDir } = upgradeInvocation(inst, to, { privileged, requestedBy, automatic, selfRoot, runAs, migrateAs });
|
|
325
328
|
|
|
326
329
|
if (!apply) {
|
|
327
330
|
// A --dry-run runner tick used to only PRINT this command. It now runs the CLI's own
|
|
@@ -668,7 +671,7 @@ function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
|
668
671
|
// HERE rather than left to instanceUser()'s own throw: that throw is correct today, but
|
|
669
672
|
// a call site whose safety depends on a helper's internals is one refactor away from a
|
|
670
673
|
// shell-injection path (and the apply leg's own guard sits at the route, not here).
|
|
671
|
-
function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', selfRoot = selfInstanceRoot, runAs = null, migrateAs = null } = {}) {
|
|
674
|
+
function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', automatic = false, selfRoot = selfInstanceRoot, runAs = null, migrateAs = null } = {}) {
|
|
672
675
|
if (!isValidSlug(inst && inst.slug)) {
|
|
673
676
|
throw new Error(`upgradeInvocation: refusing to build a shell command from an invalid slug ${JSON.stringify(inst && inst.slug)}`);
|
|
674
677
|
}
|
|
@@ -729,7 +732,10 @@ function upgradeInvocation(inst, to, { privileged = false, dryRun = false, reque
|
|
|
729
732
|
// token; the upgrade process cannot (task 1004065, pushUpgradePin).
|
|
730
733
|
hubPushesPin(inst) ? '--pin-no-push' : '',
|
|
731
734
|
requestedBy ? `--requested-by ${requestedBy}` : '',
|
|
732
|
-
requestedBy ? '--upgrade-source owner-control' : ''
|
|
735
|
+
requestedBy ? '--upgrade-source owner-control' : '',
|
|
736
|
+
// The ledger's existing mark for an unattended move (the self-host sweep writes
|
|
737
|
+
// `auto-subscription (<rule>)`); one shell-safe word here (task 1004731).
|
|
738
|
+
automatic && !requestedBy ? '--note auto-subscription' : ''];
|
|
733
739
|
return { cmd: [...shared, ...tail].filter(Boolean).join(' '), runDir: REPO_ROOT, instanceDir, runAs: account, migrateAs: dbAccount };
|
|
734
740
|
}
|
|
735
741
|
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/bongos/core-moves.js — "you were upgraded, and this is what changed": what THIS
|
|
4
|
+
// project's own hall says after its core moved, and the short list of moves Settings →
|
|
5
|
+
// Software update keeps (task 1004731).
|
|
6
|
+
//
|
|
7
|
+
// WHY. Since task 1004729 the hub moves a hosted project's core on its own, by its update
|
|
8
|
+
// rule. The owner found out only by noticing a different number. The goal state is "a
|
|
9
|
+
// changelog is visible inside the update": the hall says once, after the move, which core
|
|
10
|
+
// it is on now, when it moved, and what the releases in between brought.
|
|
11
|
+
//
|
|
12
|
+
// NO NEW CHANNEL — the same reading task 1004730's hold makes (upgrade-hold.js). Every move,
|
|
13
|
+
// automatic or by hand, is `bongos upgrade` run against THIS project's database, and it
|
|
14
|
+
// writes one row to this project's own `core_upgrades` ledger: a landed move as from → to,
|
|
15
|
+
// a failed one as its `rolled_back:` row. And the release notes for every version up to
|
|
16
|
+
// the one now running ship INSIDE the running core (release-notes.json, task 1004218), so
|
|
17
|
+
// the entries between the old and the new version are read from this project's own disk.
|
|
18
|
+
// The hub is never asked.
|
|
19
|
+
//
|
|
20
|
+
// HOW IT WAS MOVED is only as precise as the ledger: `source = 'owner-control'` is the
|
|
21
|
+
// Update button (a person); a note carrying `auto-subscription` is an automatic move (the
|
|
22
|
+
// self-host sweep has always written it, and since this task the hub's automatic move
|
|
23
|
+
// passes it too, provision-core-upgrade.js); anything else was run on the server, and the
|
|
24
|
+
// ledger cannot say by whom, so it says "on the server" rather than guess. A failed
|
|
25
|
+
// automatic move keeps no note of its own (rollback() writes `rolled_back:` in the one note
|
|
26
|
+
// column), so it reads as "on the server" too.
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
const { notesBetween, RELEASE_NOTES_FILE } = require('../../scripts/gds/release-notes.js');
|
|
31
|
+
|
|
32
|
+
const VERSION_RE = /^\d{1,4}\.\d{1,5}\.\d{1,6}$/;
|
|
33
|
+
const ROLLED_BACK_RE = /^rolled_back:/;
|
|
34
|
+
const DOWNGRADE_RE = /^downgrade\b/;
|
|
35
|
+
const AUTO_RE = /auto-subscription/;
|
|
36
|
+
|
|
37
|
+
// A notice is about a move that just happened. A project whose last move was months ago
|
|
38
|
+
// (the ledger outlives this feature) is not told about it as news.
|
|
39
|
+
const NOTICE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
|
|
40
|
+
// The notice is a banner, not the changelog: a few lines, then "and N more" with the way
|
|
41
|
+
// to the full list.
|
|
42
|
+
const NOTICE_ENTRIES_MAX = 6;
|
|
43
|
+
// "A short history of moves."
|
|
44
|
+
const HISTORY_MAX = 8;
|
|
45
|
+
|
|
46
|
+
const MONTHS = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August',
|
|
47
|
+
'September', 'October', 'November', 'December'];
|
|
48
|
+
|
|
49
|
+
function isoOrNull(v) {
|
|
50
|
+
const d = v ? new Date(v) : null;
|
|
51
|
+
return d && Number.isFinite(d.getTime()) ? d.toISOString() : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// "15 October 2026", in UTC so the same move reads the same date for every reader and test.
|
|
55
|
+
function longDate(iso) {
|
|
56
|
+
const d = new Date(iso);
|
|
57
|
+
return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]} ${d.getUTCFullYear()}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How a ledger row's move was started: 'automatic' | 'by_hand' | 'on_server'. PURE. */
|
|
61
|
+
function moveHow(row) {
|
|
62
|
+
if (row && row.source === 'owner-control') return 'by_hand';
|
|
63
|
+
if (row && typeof row.note === 'string' && AUTO_RE.test(row.note)) return 'automatic';
|
|
64
|
+
return 'on_server';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** What a ledger row says happened: 'upgraded' | 'moved_back' | 'rolled_back'. PURE. */
|
|
68
|
+
function moveOutcome(row) {
|
|
69
|
+
const note = row && typeof row.note === 'string' ? row.note : '';
|
|
70
|
+
if (ROLLED_BACK_RE.test(note)) return 'rolled_back';
|
|
71
|
+
if (DOWNGRADE_RE.test(note)) return 'moved_back';
|
|
72
|
+
return 'upgraded';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The short history Settings → Software update shows, newest first, failed moves included.
|
|
77
|
+
* PURE over core_upgrades rows (newest first). `version` is the core the move went to, or
|
|
78
|
+
* for a rolled-back row the one it tried.
|
|
79
|
+
*/
|
|
80
|
+
function moveHistory(rows) {
|
|
81
|
+
if (!Array.isArray(rows)) return [];
|
|
82
|
+
return rows
|
|
83
|
+
.filter((r) => r && VERSION_RE.test(String(r.to_version || '')))
|
|
84
|
+
.slice(0, HISTORY_MAX)
|
|
85
|
+
.map((r) => ({
|
|
86
|
+
id: r.id == null ? null : String(r.id),
|
|
87
|
+
version: String(r.to_version),
|
|
88
|
+
from: VERSION_RE.test(String(r.from_version || '')) ? String(r.from_version) : null,
|
|
89
|
+
at: isoOrNull(r.applied_at),
|
|
90
|
+
how: moveHow(r),
|
|
91
|
+
outcome: moveOutcome(r),
|
|
92
|
+
}));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The one-time notice after a move, or null. PURE.
|
|
97
|
+
*
|
|
98
|
+
* Shown only while it is true and new: the newest ledger row is a move that went in (not a
|
|
99
|
+
* rollback), this server runs the version it went to (so "upgraded to X" is a fact: this
|
|
100
|
+
* answer is served by X), it is recent, and this builder has not seen it (`seenId`, the
|
|
101
|
+
* ledger row id they last acknowledged).
|
|
102
|
+
*
|
|
103
|
+
* @param {object[]} rows core_upgrades rows, newest first
|
|
104
|
+
* @param {string} running the core this server runs
|
|
105
|
+
* @param {object?} notes the running core's release-notes.json (null if unreadable)
|
|
106
|
+
* @param {string?} seenId the ledger row id this builder has acknowledged
|
|
107
|
+
* @param {number} now ms clock
|
|
108
|
+
*/
|
|
109
|
+
function moveNotice({ rows, running, notes = null, seenId = null, now = Date.now() }) {
|
|
110
|
+
const last = Array.isArray(rows) ? rows[0] : null;
|
|
111
|
+
if (!last || last.id == null) return null;
|
|
112
|
+
const id = String(last.id);
|
|
113
|
+
const to = String(last.to_version || '');
|
|
114
|
+
if (!VERSION_RE.test(to) || to !== String(running || '')) return null;
|
|
115
|
+
const outcome = moveOutcome(last);
|
|
116
|
+
if (outcome === 'rolled_back') return null;
|
|
117
|
+
if (seenId != null && String(seenId) === id) return null;
|
|
118
|
+
const at = isoOrNull(last.applied_at);
|
|
119
|
+
if (!at || now - Date.parse(at) > NOTICE_MAX_AGE_MS) return null;
|
|
120
|
+
const from = VERSION_RE.test(String(last.from_version || '')) ? String(last.from_version) : null;
|
|
121
|
+
const how = moveHow(last);
|
|
122
|
+
|
|
123
|
+
// The release-note entries between the old and the new version, from this core's own file.
|
|
124
|
+
let entries = null;
|
|
125
|
+
let more = 0;
|
|
126
|
+
if (outcome === 'upgraded' && from && notes) {
|
|
127
|
+
const r = notesBetween(notes, from, to);
|
|
128
|
+
if (r.entries) {
|
|
129
|
+
entries = r.entries.slice(0, NOTICE_ENTRIES_MAX);
|
|
130
|
+
more = r.entries.length - entries.length;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const verb = outcome === 'moved_back' ? 'Moved back' : 'Upgraded';
|
|
134
|
+
return {
|
|
135
|
+
id,
|
|
136
|
+
version: to,
|
|
137
|
+
from,
|
|
138
|
+
at,
|
|
139
|
+
how,
|
|
140
|
+
outcome,
|
|
141
|
+
message: `${verb}${how === 'automatic' ? ' automatically' : ''} to core ${to} on ${longDate(at)}.`,
|
|
142
|
+
entries,
|
|
143
|
+
more,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The running core's own release notes, or null. Never throws. The file sits at the package
|
|
149
|
+
* root beside package.json (release-notes.js); a core checkout run in place has none.
|
|
150
|
+
*/
|
|
151
|
+
function readInstalledReleaseNotes({ fsImpl = fs, root = path.join(__dirname, '..', '..') } = {}) {
|
|
152
|
+
try { return JSON.parse(fsImpl.readFileSync(path.join(root, RELEASE_NOTES_FILE), 'utf8')); } catch { return null; }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
module.exports = { moveHistory, moveNotice, readInstalledReleaseNotes };
|
|
@@ -35,6 +35,7 @@ const seams = require('../../module-seams');
|
|
|
35
35
|
const { loadIdpConfig } = require('../auth-config');
|
|
36
36
|
const upgradeRequest = require('../upgrade-request');
|
|
37
37
|
const { readUpgradeHold } = require('../upgrade-hold');
|
|
38
|
+
const { moveNotice, moveHistory, readInstalledReleaseNotes } = require('../core-moves');
|
|
38
39
|
|
|
39
40
|
// WHERE A HOSTED PROJECT'S OWNER TAKES THE UPGRADE (task 1004174, ADR 0339): the hub's
|
|
40
41
|
// own deploy door, scoped to this project — `<hub>/deploy?project=<slug>`. The act stays
|
|
@@ -222,6 +223,37 @@ async function safeHold(read, args) {
|
|
|
222
223
|
try { return (await read(args)) || null; } catch { return null; }
|
|
223
224
|
}
|
|
224
225
|
|
|
226
|
+
// "Upgraded to core X on <date>, and this is what changed" (task 1004731,
|
|
227
|
+
// src/bongos/core-moves.js): the one-time notice after a move and the short history of
|
|
228
|
+
// moves, both read from this project's own core_upgrades ledger, the notes from the running
|
|
229
|
+
// core's own release-notes.json. Gated like the ledger read (GET /core-upgrades): when and to
|
|
230
|
+
// what this box moved is a vulnerability-window fact (ADR 0043).
|
|
231
|
+
//
|
|
232
|
+
// WHO HAS SEEN IT is per builder and server-side, in the builder-settings module's
|
|
233
|
+
// behavior_prefs (core-move-seen.js), reached through its port: core never requires a module.
|
|
234
|
+
// Without the module the hall remembers it in the browser instead ('browser').
|
|
235
|
+
async function defaultSeenMove(req) {
|
|
236
|
+
const port = seams.resolveOptional('builder-settings');
|
|
237
|
+
if (!port || typeof port.coreMoveSeen !== 'function' || typeof port.getBuilderBehaviorPrefs !== 'function') {
|
|
238
|
+
return { remember: 'browser', seen: null };
|
|
239
|
+
}
|
|
240
|
+
return { remember: 'server', seen: port.coreMoveSeen(await port.getBuilderBehaviorPrefs(req.builder && req.builder.id)) };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// The notice for this viewer, or null. Fail-soft: a ledger, notes file or seen state that
|
|
244
|
+
// cannot be read is no notice, never a failed banner read.
|
|
245
|
+
async function safeMoved({ req, running, upgrades, releaseNotes, seenMove }) {
|
|
246
|
+
try {
|
|
247
|
+
const [rows, seen] = await Promise.all([upgrades({ limit: 1 }), seenMove(req)]);
|
|
248
|
+
const notice = moveNotice({ rows, running, notes: releaseNotes(), seenId: seen && seen.seen });
|
|
249
|
+
return notice ? { ...notice, remember: seen && seen.remember === 'server' ? 'server' : 'browser' } : null;
|
|
250
|
+
} catch { return null; }
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function safeHistory(upgrades) {
|
|
254
|
+
try { return moveHistory(await upgrades({ limit: 8 })); } catch { return null; }
|
|
255
|
+
}
|
|
256
|
+
|
|
225
257
|
function buildCoreUpdateRouter({
|
|
226
258
|
coreUpdate = readCoreUpdate,
|
|
227
259
|
selfChannel = storedSelfChannel,
|
|
@@ -231,6 +263,8 @@ function buildCoreUpdateRouter({
|
|
|
231
263
|
doorInputs = () => ({ provisioning: provisioningEnabled(), idp: safeIdpConfig(), hubUpgradeUrl: safeHubUpgradeUrl() }),
|
|
232
264
|
askDeps = defaultAskDeps,
|
|
233
265
|
upgradeHold = readUpgradeHold,
|
|
266
|
+
releaseNotes = readInstalledReleaseNotes,
|
|
267
|
+
seenMove = defaultSeenMove,
|
|
234
268
|
} = {}) {
|
|
235
269
|
contributeCoreUpdateToDocket();
|
|
236
270
|
const router = express.Router();
|
|
@@ -247,7 +281,7 @@ function buildCoreUpdateRouter({
|
|
|
247
281
|
// grader caught here, and it is invisible to any test that does not drive a
|
|
248
282
|
// real request through the assembled router; tests/core_update_banner.mjs now
|
|
249
283
|
// does, and fails if this call is mis-shaped again.
|
|
250
|
-
asyncHandler('GET /core-update', async (
|
|
284
|
+
asyncHandler('GET /core-update', async (req, res) => {
|
|
251
285
|
// Fail-soft on the version read, exactly as GET /version does: an instance
|
|
252
286
|
// that cannot name its own core reports `no_answer` and renders no banner.
|
|
253
287
|
let running = null;
|
|
@@ -256,7 +290,11 @@ function buildCoreUpdateRouter({
|
|
|
256
290
|
// own links to Settings → Software update, and the hand-off lives on GET /software-update.
|
|
257
291
|
const { channel, source } = await resolveChannel(selfChannel);
|
|
258
292
|
const update = await coreUpdate({ running, channel });
|
|
259
|
-
|
|
293
|
+
const [held, moved] = await Promise.all([
|
|
294
|
+
safeHold(upgradeHold, { running, offered: update && update.reading === 'ok' ? update.latest : null }),
|
|
295
|
+
safeMoved({ req, running, upgrades, releaseNotes, seenMove }),
|
|
296
|
+
]);
|
|
297
|
+
res.json({ ok: true, update: { ...update, channel_source: source }, held, moved });
|
|
260
298
|
}, { errorCode: 'core_update_failed', message: false }),
|
|
261
299
|
);
|
|
262
300
|
|
|
@@ -295,7 +333,9 @@ function buildCoreUpdateRouter({
|
|
|
295
333
|
const door = updateDoor(doorInputs());
|
|
296
334
|
const ask = await readAsk({ update, door, deps: askDeps() });
|
|
297
335
|
const held = await safeHold(upgradeHold, { running, offered: update && update.reading === 'ok' ? update.offered : null });
|
|
298
|
-
|
|
336
|
+
// The history only for a viewer who holds core.pin.move (see safeMoved's gate note).
|
|
337
|
+
const history = can === true ? await safeHistory(upgrades) : null;
|
|
338
|
+
res.json({ ok: true, update, door, can_update: can === true, ask, held, history });
|
|
299
339
|
}, { errorCode: 'software_update_failed', message: false }),
|
|
300
340
|
);
|
|
301
341
|
|
package/src/module-api.js
CHANGED
|
@@ -75,7 +75,7 @@ const { responsibilityFor, ROLE_RESPONSIBILITIES } = require('./role-responsibil
|
|
|
75
75
|
// MAJOR (see allowBoxScope below): passes the request through untouched.
|
|
76
76
|
function deprecatedNoopMiddleware(_req, _res, next) { next(); }
|
|
77
77
|
|
|
78
|
-
const CORE_VERSION = '1.21.
|
|
78
|
+
const CORE_VERSION = '1.21.148'; // CI auto-patch carrier (ADR 0161); changelog: docs/module-api-changelog.md
|
|
79
79
|
|
|
80
80
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
81
81
|
// Usage: const log = api.logger('discord'); log.info('mounted');
|