@bongos/core 1.24.57 → 1.24.59
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 +77 -67
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +0 -2
- package/clients/bongos-client/index.cjs +0 -2
- package/clients/bongos-client/index.d.ts +1 -3
- package/clients/bongos-client/index.mjs +0 -2
- package/docs/adr/0398-every-hosted-project-takes-its-own-update-and-nobody-asks-for-one.md +45 -0
- package/docs/adr/README.md +1 -0
- package/docs/api/openapi.json +1 -37
- package/docs/api-reference.md +2 -3
- package/docs/architecture.md +1 -1
- package/docs/copy-inventory.md +38 -44
- package/docs/copy-registry.json +44 -108
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-readings.json +5 -5
- package/docs/recipes/ops-gotchas.md +2 -2
- package/modules/hall-ui/public/deploy-handoff.js +11 -3
- package/modules/hall-ui/public/deploy.js +10 -3
- package/modules/hall-ui/public/settings-software-update.js +4 -71
- package/modules/hall-ui/public/update-pill.js +6 -31
- package/modules/status-ui/public/style.css +5 -2
- package/modules/ui-design/public/brand/png-sources.json +10 -0
- package/modules/ui-design/public/brand.css +6 -0
- package/modules/ui-design/public/brand.html +2 -2
- package/modules/ui-design/scripts/gen-design-reference.js +2 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-docs/index.html +3 -1
- package/release-notes.json +16 -0
- package/scripts/bongos/design-generators-fresh.js +14 -6
- package/scripts/bongos/gen-brand.js +82 -36
- package/scripts/bongos/provision-core-upgrade.js +30 -1
- package/scripts/bongos/provision-units.js +18 -0
- package/scripts/hall-preview/server.js +0 -3
- package/scripts/status-mirror-sync.js +4 -1
- package/src/bongos/routes/auth.js +4 -9
- package/src/bongos/routes/core-update.js +36 -76
- package/src/bongos/routes/feedback.js +1 -2
- package/src/bongos/serve-internal.js +5 -70
- package/src/bongos/sign-in-pages.js +20 -4
- package/src/brand-mark.js +10 -1
- package/src/error-page.js +11 -10
- package/src/module-api.js +1 -1
- package/src/theme-markup.js +81 -0
- package/tests/brand_mark.mjs +11 -1
- package/tests/brand_surfaces.mjs +5 -2
- package/tests/deploy_handoff.mjs +23 -2
- package/tests/deploy_page_projects.mjs +35 -3
- package/tests/design_counts_ratchet.mjs +38 -0
- package/tests/design_reference.mjs +1 -1
- package/tests/provision_settings_apply.mjs +52 -0
- package/tests/software_update.mjs +35 -5
- package/tests/update_pill.mjs +9 -5
- package/tests/wordmark_and_faces.mjs +111 -0
- package/src/bongos/upgrade-request.js +0 -158
- package/tests/upgrade_request.mjs +0 -348
|
@@ -66,49 +66,12 @@
|
|
|
66
66
|
</div>`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// "Ask for this upgrade" (task 1004595). A project the hub's operator runs cannot take an
|
|
70
|
-
// update itself, so instead of a dead end the page offers one press that reaches the people
|
|
71
|
-
// who run its server, and then says that it was asked. The server decides whether it is
|
|
72
|
-
// offered (`ask.available`) and remembers the request (`ask.requested`).
|
|
73
69
|
function longDate(iso) {
|
|
74
70
|
const d = new Date(iso);
|
|
75
71
|
if (!Number.isFinite(d.getTime())) return null;
|
|
76
72
|
try { return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); } catch (e) { return d.toISOString().slice(0, 10); }
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
// Asked, and still waiting: for this very version, or recently enough that asking again
|
|
80
|
-
// would only be refused.
|
|
81
|
-
function askPending(ask, offered) {
|
|
82
|
-
const r = ask && ask.requested;
|
|
83
|
-
if (!r) return false;
|
|
84
|
-
if (r.version === offered) return true;
|
|
85
|
-
const again = Date.parse(ask.ask_again_after);
|
|
86
|
-
return Number.isFinite(again) && Date.now() < again;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function askHtml(ask, offered, { lead }) {
|
|
90
|
-
const team = ask.hub_host ? `the team at ${escapeHtml(ask.hub_host)}` : 'the team that runs this project\'s server';
|
|
91
|
-
if (askPending(ask, offered)) {
|
|
92
|
-
const r = ask.requested;
|
|
93
|
-
const when = longDate(r.requested_at);
|
|
94
|
-
return `<div class="su-door su-ask">
|
|
95
|
-
<p class="panel-status panel-status--ok" role="status">Upgrade requested${when ? ` on ${escapeHtml(when)}` : ''}${r.by ? ` by @${escapeHtml(r.by)}` : ''}, for version ${mono(r.version)}. ${team.charAt(0).toUpperCase() + team.slice(1)} has your request. This page shows the new version once it is installed.</p>
|
|
96
|
-
</div>`;
|
|
97
|
-
}
|
|
98
|
-
const last = ask.requested && longDate(ask.requested.requested_at);
|
|
99
|
-
const before = last ? ` You last asked on ${escapeHtml(last)}, for ${mono(ask.requested.version)}.` : '';
|
|
100
|
-
const intro = lead
|
|
101
|
-
? `<p class="panel-hint">This project runs on a server that ${team} looks after, so they install its updates. Ask them for this one and they are told which version you want.${before}</p>`
|
|
102
|
-
: `<p class="panel-hint">If that page does not let you in, ask ${team} to install it for you.${before}</p>`;
|
|
103
|
-
return `<div class="su-door su-ask">
|
|
104
|
-
${intro}
|
|
105
|
-
<div class="row-end">
|
|
106
|
-
<button type="button" class="pbtn ${lead ? 'pbtn--save' : 'pbtn--quiet'}" id="su-ask">Ask for this upgrade</button>
|
|
107
|
-
<span class="panel-hint" id="su-ask-status" role="status"></span>
|
|
108
|
-
</div>
|
|
109
|
-
</div>`;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
75
|
// Where the Update button goes, and what it says it will do. Only a plain same-origin path
|
|
113
76
|
// or an http(s) URL is ever put in an href; anything else draws no button.
|
|
114
77
|
function doorHtml(data, offered) {
|
|
@@ -116,9 +79,9 @@
|
|
|
116
79
|
if (!data.can_update) {
|
|
117
80
|
return '<p class="panel-hint su-door">Only the people allowed to upgrade this project see an Update button here.</p>';
|
|
118
81
|
}
|
|
119
|
-
|
|
82
|
+
// No request-the-upgrade button any more (task 1005128): a project the hub hosts always has
|
|
83
|
+
// the hub's door, so `cli` is only ever a server its owner runs themselves.
|
|
120
84
|
if (door.kind === 'cli') {
|
|
121
|
-
if (ask.available) return askHtml(ask, offered, { lead: true });
|
|
122
85
|
return '<p class="panel-hint su-door">This project is upgraded on the server that runs it: <code>bongos upgrade</code> there takes this version, with the same health check and rollback.</p>';
|
|
123
86
|
}
|
|
124
87
|
const url = String(door.url || '');
|
|
@@ -134,7 +97,7 @@
|
|
|
134
97
|
return `<div class="row-end su-door">
|
|
135
98
|
<a class="pbtn pbtn--save" id="su-update" href="${escapeHtml(url)}">Update to ${escapeHtml(offered)}</a>
|
|
136
99
|
<span class="panel-hint">Takes a few minutes. The project restarts once, checks itself, and goes back to the version it runs now if it does not come back healthy.${where ? ` A small window from ${escapeHtml(where)} opens for a moment to start it.` : ''}</span>
|
|
137
|
-
</div
|
|
100
|
+
</div>`;
|
|
138
101
|
}
|
|
139
102
|
|
|
140
103
|
// One entry: its text, and — only in the hall whose ledger the ids belong to, which is
|
|
@@ -223,8 +186,7 @@
|
|
|
223
186
|
html += row('Newest for this project', `${mono(u.running)} — this project is up to date.`, '<span class="su-pill su-pill--ok">Up to date</span>');
|
|
224
187
|
} else {
|
|
225
188
|
const when = ago(u.offered_published_at);
|
|
226
|
-
|
|
227
|
-
html += row('Newest for this project', `${mono(u.offered)}${when ? ` — published ${escapeHtml(when)}` : ''}`, `<span class="su-pill">${onHold ? 'On hold' : pending ? 'Requested' : 'Update ready'}</span>`);
|
|
189
|
+
html += row('Newest for this project', `${mono(u.offered)}${when ? ` — published ${escapeHtml(when)}` : ''}`, `<span class="su-pill">${onHold ? 'On hold' : 'Update ready'}</span>`);
|
|
228
190
|
// No button to take again the very version that was just rolled back here (task 1004730).
|
|
229
191
|
if (!onHold) html += doorHtml(data, u.offered);
|
|
230
192
|
html += whatsNewHtml(u.whats_new, (data.door || {}).kind === 'local');
|
|
@@ -237,9 +199,6 @@
|
|
|
237
199
|
body.innerHTML = html + historyHtml(data.history);
|
|
238
200
|
if (note) note.textContent = notes.join(' ');
|
|
239
201
|
|
|
240
|
-
const askBtn = byId('su-ask');
|
|
241
|
-
if (askBtn) askBtn.addEventListener('click', () => askForUpgrade(data, askBtn));
|
|
242
|
-
|
|
243
202
|
// The same press as the pill's Update now; only when it took the update in place is the
|
|
244
203
|
// link's own navigation stopped.
|
|
245
204
|
const updateBtn = byId('su-update');
|
|
@@ -259,32 +218,6 @@
|
|
|
259
218
|
}
|
|
260
219
|
}
|
|
261
220
|
|
|
262
|
-
// One press: the server sends the request and remembers it; the panel redraws from its
|
|
263
|
-
// answer. "Asked recently" carries the stored request, so it redraws as pending too.
|
|
264
|
-
async function askForUpgrade(data, btn) {
|
|
265
|
-
const status = byId('su-ask-status');
|
|
266
|
-
btn.disabled = true;
|
|
267
|
-
btn.textContent = 'Asking…';
|
|
268
|
-
if (status) status.textContent = '';
|
|
269
|
-
try {
|
|
270
|
-
const res = await api.request('POST', `${API}/software-update/request`, { body: {} });
|
|
271
|
-
const err = (res.data && res.data.error) || {};
|
|
272
|
-
const request = (res.data && res.data.request) || (err.details && err.details.request);
|
|
273
|
-
if ((res.ok || res.status === 429) && request) {
|
|
274
|
-
const again = new Date(Date.parse(request.requested_at) + 24 * 3600 * 1000).toISOString();
|
|
275
|
-
render({ ...data, ask: { ...data.ask, requested: request, ask_again_after: again } });
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
// Every refusal the server sends carries a sentence; a bare code is only ever a
|
|
279
|
-
// proxy or a crash, so the status stands in for it.
|
|
280
|
-
throw new Error(err.message && err.message !== err.code ? err.message : `HTTP ${res.status}`);
|
|
281
|
-
} catch (e) {
|
|
282
|
-
btn.disabled = false;
|
|
283
|
-
btn.textContent = 'Ask for this upgrade';
|
|
284
|
-
if (status) status.textContent = `Could not send the request: ${String(e.message || 'unknown error').replace(/\.$/, '')}.`;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
221
|
async function load() {
|
|
289
222
|
if (asked) return;
|
|
290
223
|
asked = true;
|
|
@@ -23,7 +23,6 @@
|
|
|
23
23
|
// with &handoff=1, this page tells it the version over postMessage, and the hub
|
|
24
24
|
// posts the move with the owner's OWN hub session (deploy-handoff.js). This
|
|
25
25
|
// project's server gains no capability; the only thing that crosses is a version.
|
|
26
|
-
// ask a hosted project the hub's operator runs: "Ask for this update" (task 1004595).
|
|
27
26
|
// cli a self-hosted instance: no button; the card names the command.
|
|
28
27
|
//
|
|
29
28
|
// PROGRESS IS READ, NEVER GUESSED (deploy.js's rule). The page watches its own public
|
|
@@ -41,9 +40,10 @@
|
|
|
41
40
|
var RUN_KEY = 'bongos.update.run';
|
|
42
41
|
var MSG = 'bongos-update'; // the tag deploy-handoff.js answers to; both sides check it
|
|
43
42
|
var VERSION_RE = /^\d+\.\d+\.\d+$/;
|
|
44
|
-
// A hub door as the server words it (core-update.js
|
|
45
|
-
//
|
|
46
|
-
|
|
43
|
+
// A hub door as the server words it (core-update.js updateDoor): an origin and
|
|
44
|
+
// /deploy?project=<slug>, or /deploy?host=<address> for a hosted project whose server never
|
|
45
|
+
// learned its slug (task 1005128). Anything else draws no button.
|
|
46
|
+
var HUB_DOOR_RE = /^(https?:\/\/[^\s"'<>/]+)\/deploy\?(?:project|host)=[A-Za-z0-9._%-]+$/;
|
|
47
47
|
var SETTINGS_HREF = '/settings#software-update';
|
|
48
48
|
var HIGHLIGHTS = 3;
|
|
49
49
|
var LINE_MAX = 120;
|
|
@@ -101,14 +101,13 @@
|
|
|
101
101
|
function doorPlan(sw) {
|
|
102
102
|
if (!sw || sw.can_update !== true) return { kind: 'none' };
|
|
103
103
|
var door = sw.door || {};
|
|
104
|
-
var ask = sw.ask || {};
|
|
105
104
|
if (door.kind === 'local' && door.url === '/deploy') return { kind: 'local' };
|
|
106
105
|
if (door.kind === 'hub') {
|
|
107
106
|
var m = HUB_DOOR_RE.exec(String(door.url || ''));
|
|
108
|
-
if (!m) return
|
|
107
|
+
if (!m) return { kind: 'none' };
|
|
109
108
|
return { kind: 'hub', url: door.url, origin: m[1] };
|
|
110
109
|
}
|
|
111
|
-
if (door.kind === 'cli') return
|
|
110
|
+
if (door.kind === 'cli') return { kind: 'cli' };
|
|
112
111
|
return { kind: 'none' };
|
|
113
112
|
}
|
|
114
113
|
|
|
@@ -263,13 +262,6 @@
|
|
|
263
262
|
note = '<p class="upd-card__note">Takes a few minutes. The project restarts once, and goes back to the version it runs now on its own if the new one is not healthy.' +
|
|
264
263
|
(host ? ' A small window from ' + esc(host) + ' opens for a moment to start it.' : '') + '</p>';
|
|
265
264
|
action = '<button type="button" class="upd-btn upd-btn--go" id="upd-go">Update now</button>';
|
|
266
|
-
} else if (plan.kind === 'ask') {
|
|
267
|
-
var ask = state.sw.ask || {};
|
|
268
|
-
var team = ask.hub_host ? 'the team at ' + ask.hub_host : 'the team that runs this project\'s server';
|
|
269
|
-
var asked = ask.requested && ask.requested.version === state.offered;
|
|
270
|
-
note = '<p class="upd-card__note">' + esc(asked ? 'You asked ' + team + ' for this update. This shows the new version once it is installed.'
|
|
271
|
-
: 'This project runs on a server that ' + team + ' looks after, so they install its updates.') + '</p>';
|
|
272
|
-
if (!asked) action = '<button type="button" class="upd-btn upd-btn--go" id="upd-ask">Ask for this update</button>';
|
|
273
265
|
} else if (plan.kind === 'cli') {
|
|
274
266
|
note = '<p class="upd-card__note">This project is updated on the server that runs it: <code>bongos upgrade</code> there takes this version, and puts the old one back if it fails.</p>';
|
|
275
267
|
} else {
|
|
@@ -294,7 +286,6 @@
|
|
|
294
286
|
var on = function (id, fn) { var el = c.querySelector('#' + id); if (el) el.addEventListener('click', fn); };
|
|
295
287
|
on('upd-close', closeCard);
|
|
296
288
|
on('upd-go', function () { updateNow(); });
|
|
297
|
-
on('upd-ask', function (e) { askForIt(e.currentTarget); });
|
|
298
289
|
on('upd-reload', reload);
|
|
299
290
|
on('upd-open', function () { updateNow(); });
|
|
300
291
|
}
|
|
@@ -565,22 +556,6 @@
|
|
|
565
556
|
if (root.location && root.location.reload) root.location.reload();
|
|
566
557
|
}
|
|
567
558
|
|
|
568
|
-
// "Ask for this update" (task 1004595): one press reaches the people who run the server.
|
|
569
|
-
function askForIt(btn) {
|
|
570
|
-
btn.disabled = true;
|
|
571
|
-
btn.textContent = 'Asking…';
|
|
572
|
-
state.api.request('POST', '/api/bongos/software-update/request', { body: {} }).then(function (res) {
|
|
573
|
-
var data = (res && res.data) || {};
|
|
574
|
-
var request = data.request || (data.error && data.error.details && data.error.details.request);
|
|
575
|
-
if (!((res.ok || res.status === 429) && request)) throw new Error(errorOf(res, 'Could not send the request.'));
|
|
576
|
-
state.sw.ask = Object.assign({}, state.sw.ask, { requested: request });
|
|
577
|
-
drawCard();
|
|
578
|
-
}).then(null, function (e) {
|
|
579
|
-
state.error = String((e && e.message) || 'Could not send the request.');
|
|
580
|
-
drawCard();
|
|
581
|
-
});
|
|
582
|
-
}
|
|
583
|
-
|
|
584
559
|
/** shell.js hands over GET /core-update's answer; the pill decides what, if anything, to show. */
|
|
585
560
|
function mount(data, opts) {
|
|
586
561
|
state.api = opts && opts.api;
|
|
@@ -250,11 +250,14 @@ a:hover { text-decoration-color: currentColor; }
|
|
|
250
250
|
/* drawn from the one mark (modules/ui-design/public/brand/mark.svg, task 1005088) */
|
|
251
251
|
--mark-ink: var(--ink);
|
|
252
252
|
}
|
|
253
|
+
/* the wordmark: the pack's productName, lowercase and light, as the hall's lockup
|
|
254
|
+
(DESIGN.md "Naming"; task 1005135) */
|
|
253
255
|
.topbar__name {
|
|
254
256
|
font-family: var(--font-display);
|
|
255
|
-
font-size: var(--ds-text-
|
|
256
|
-
font-weight:
|
|
257
|
+
font-size: var(--ds-text-lg);
|
|
258
|
+
font-weight: 300;
|
|
257
259
|
letter-spacing: -0.01em;
|
|
260
|
+
text-transform: lowercase;
|
|
258
261
|
color: var(--ink);
|
|
259
262
|
white-space: nowrap;
|
|
260
263
|
overflow: hidden;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"apple-touch-icon.png": {
|
|
3
|
+
"page": "b665654a1cdfa7fb91f9aef0ab4a06831922c2e12bd7eb3dea572dd0db4988a2",
|
|
4
|
+
"png": "953557984586881b25452b8642f595dfad39d17b86459cee5c5b080b74165362"
|
|
5
|
+
},
|
|
6
|
+
"og-card.png": {
|
|
7
|
+
"page": "5f7d738dd440ed4760017fe4e468ec8a593745536336f819d4a3bd2c32a5c38d",
|
|
8
|
+
"png": "e168fdd0f4ac94754af089e7240be7cbba46704b36a617252f2942c97e8835e2"
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -15,6 +15,11 @@ body[data-page="brand"] {
|
|
|
15
15
|
font-family: var(--font-body);
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/* The hall's sheet paints its plate veil as a fixed body::after over the page
|
|
19
|
+
(black in dark); the hall lifts its shell above it, and so does this page —
|
|
20
|
+
otherwise the whole reference reads through the veil, dimmed (task 1005135). */
|
|
21
|
+
body[data-page="brand"] > * { position: relative; z-index: 1; }
|
|
22
|
+
|
|
18
23
|
/* ---- the bar: the mark, the name, the sections ---- */
|
|
19
24
|
.ref-head {
|
|
20
25
|
box-sizing: border-box;
|
|
@@ -134,6 +139,7 @@ body[data-page="brand"] {
|
|
|
134
139
|
font-weight: 300;
|
|
135
140
|
font-size: var(--ds-text-headline);
|
|
136
141
|
letter-spacing: -0.01em;
|
|
142
|
+
text-transform: lowercase;
|
|
137
143
|
color: var(--ink);
|
|
138
144
|
}
|
|
139
145
|
.ref-lockup__mark { width: 72px; height: 72px; }
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
</head>
|
|
14
14
|
<body data-page="brand">
|
|
15
15
|
<header class="ref-head">
|
|
16
|
-
<a class="ref-head__brand" href="/"><svg class="ref-head__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="/modules/ui-design/public/brand/mark.svg#mark"/></svg><span>
|
|
16
|
+
<a class="ref-head__brand" href="/"><svg class="ref-head__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="/modules/ui-design/public/brand/mark.svg#mark"/></svg><span>{{productName}}</span></a>
|
|
17
17
|
</header>
|
|
18
18
|
<main class="ref-main">
|
|
19
19
|
<h1 class="ref-title">Design system</h1>
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
<div class="ref-lockup">
|
|
29
29
|
<svg class="ref-lockup__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="/modules/ui-design/public/brand/mark.svg#mark"/></svg>
|
|
30
|
-
<span class="ref-lockup__name">
|
|
30
|
+
<span class="ref-lockup__name">{{productName}}</span>
|
|
31
31
|
</div>
|
|
32
32
|
<div class="ref-naming">
|
|
33
33
|
<p><strong>The wordmark is lowercase; the name in a sentence is capitalised</strong> (owner, 2026-11-06):</p>
|
|
@@ -152,7 +152,6 @@ function build(src = read()) {
|
|
|
152
152
|
for (const cls of NEEDS) {
|
|
153
153
|
if (!new RegExp(cls.replace(/[.[\]-]/g, '\\$&') + '(?![a-z0-9_-])').test(allCss)) throw new Error(`the hall's sheets no longer define ${cls}`);
|
|
154
154
|
}
|
|
155
|
-
const product = (src.pack.identity && src.pack.identity.productName) || '';
|
|
156
155
|
const naming = readNaming(src.design);
|
|
157
156
|
const files = readBrandFiles(src.genBrand, SRC.brandDir);
|
|
158
157
|
const OTB = src.otb;
|
|
@@ -199,7 +198,7 @@ function build(src = read()) {
|
|
|
199
198
|
parts.push(section('ref-logo', 'Logo and name', 'The mark is one file every page draws from. The ring and the planet take the ink; the moon takes the accent.', `
|
|
200
199
|
<div class="ref-lockup">
|
|
201
200
|
<svg class="ref-lockup__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="${BRAND_URL}mark.svg#mark"/></svg>
|
|
202
|
-
<span class="ref-lockup__name"
|
|
201
|
+
<span class="ref-lockup__name">{{productName}}</span>
|
|
203
202
|
</div>
|
|
204
203
|
<div class="ref-naming">
|
|
205
204
|
${naming.lead.map((p) => `<p>${p}</p>`).join('\n ')}
|
|
@@ -315,7 +314,7 @@ function build(src = read()) {
|
|
|
315
314
|
</head>
|
|
316
315
|
<body data-page="brand">
|
|
317
316
|
<header class="ref-head">
|
|
318
|
-
<a class="ref-head__brand" href="/"><svg class="ref-head__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="${BRAND_URL}mark.svg#mark"/></svg><span
|
|
317
|
+
<a class="ref-head__brand" href="/"><svg class="ref-head__mark" viewBox="0 0 200 200" aria-hidden="true"><use href="${BRAND_URL}mark.svg#mark"/></svg><span>{{productName}}</span></a>
|
|
319
318
|
</header>
|
|
320
319
|
<main class="ref-main">
|
|
321
320
|
<h1 class="ref-title">Design system</h1>
|
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.24.
|
|
3
|
+
"version": "1.24.59",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.24.
|
|
9
|
+
"version": "1.24.59",
|
|
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.24.
|
|
3
|
+
"version": "1.24.59",
|
|
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/public-docs/index.html
CHANGED
|
@@ -54,6 +54,8 @@
|
|
|
54
54
|
}
|
|
55
55
|
#topbar .mark { width: 28px; height: 28px; flex-shrink: 0; --mark-ink: var(--ink); }
|
|
56
56
|
#topbar strong { font-family: var(--font-display); font-size: var(--ds-text-xl); font-weight: 500; }
|
|
57
|
+
/* the wordmark: the pack's productName, lowercase and light (DESIGN.md "Naming"; task 1005135) */
|
|
58
|
+
#topbar .wordmark { font-size: var(--ds-text-lg); font-weight: 300; letter-spacing: -0.01em; text-transform: lowercase; }
|
|
57
59
|
#topbar .muted {
|
|
58
60
|
font-family: var(--font-mono); font-size: var(--ds-text-2xs); letter-spacing: .12em;
|
|
59
61
|
text-transform: uppercase; color: var(--ink-soft);
|
|
@@ -72,7 +74,7 @@
|
|
|
72
74
|
<body>
|
|
73
75
|
<div id="topbar">
|
|
74
76
|
<svg class="mark" viewBox="0 0 200 200" aria-hidden="true"><use href="/modules/ui-design/public/brand/mark.svg#mark"/></svg>
|
|
75
|
-
<strong>{{productName}} — API</strong>
|
|
77
|
+
<strong><span class="wordmark">{{productName}}</span> — API</strong>
|
|
76
78
|
<span class="muted">OpenAPI 3.1</span>
|
|
77
79
|
<span class="spacer"></span>
|
|
78
80
|
<a href="/docs/openapi.json" download>Download spec</a>
|
package/release-notes.json
CHANGED
|
@@ -11313,5 +11313,21 @@
|
|
|
11313
11313
|
"id": "1005093",
|
|
11314
11314
|
"text": "Stylesheets can no longer gain new typed-in colours, text sizes, corners, shadows or breakpoints without the build failing, and the public design reference must be regenerated whenever its sources change."
|
|
11315
11315
|
}
|
|
11316
|
+
],
|
|
11317
|
+
"1.24.58": [
|
|
11318
|
+
{
|
|
11319
|
+
"id": "1005128",
|
|
11320
|
+
"text": "A project's owner now takes a new Bongos version with one Update button; the \"ask the team for this upgrade\" step is gone for every hosted project."
|
|
11321
|
+
}
|
|
11322
|
+
],
|
|
11323
|
+
"1.24.59": [
|
|
11324
|
+
{
|
|
11325
|
+
"id": "1005135",
|
|
11326
|
+
"text": "The name beside the logo now reads 'cloud bongos' in lowercase everywhere, taken from the project's branding, and the sign-in and error pages use the same fonts and colours as the rest of the site."
|
|
11327
|
+
},
|
|
11328
|
+
{
|
|
11329
|
+
"id": "1005133",
|
|
11330
|
+
"text": "The logo files and the link-preview pictures are now checked automatically, so a logo change can no longer ship with out-of-date copies; the docs now say plainly that the off-site status page is updated by hand."
|
|
11331
|
+
}
|
|
11316
11332
|
]
|
|
11317
11333
|
}
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
// check; kept in its own file because fitness.js sits at the 1,500-line budget.
|
|
5
5
|
//
|
|
6
6
|
// Both generated files read the shared token sheet (modules/ui-design/public/tokens.css) and the brand pack; the public
|
|
7
|
-
// reference also reads the hall's sheets and DESIGN.md.
|
|
7
|
+
// reference also reads the hall's sheets and DESIGN.md. The logo set (scripts/bongos/gen-brand.js)
|
|
8
|
+
// reads mark.svg and the pack, and checks its two PNGs by hash (task 1005133). A stylesheet edit without a
|
|
8
9
|
// regeneration used to surface only as a failing unit test; this names it in fitness.
|
|
9
10
|
// Skipped when the ui-design module is not in the checkout. `root` is injectable for
|
|
10
11
|
// tests/design_counts_ratchet.mjs, which runs it against a copied tree.
|
|
@@ -17,14 +18,21 @@ const { REPO_ROOT } = require('./fitness-lib.js');
|
|
|
17
18
|
|
|
18
19
|
function checkDesignGeneratorsFresh(root = REPO_ROOT) {
|
|
19
20
|
const violations = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
['gen-design-
|
|
21
|
+
const UI = ['modules', 'ui-design', 'scripts'];
|
|
22
|
+
for (const [dir, script, out, why, task] of [
|
|
23
|
+
[UI, 'gen-design-tokens.js', 'config/design-tokens.neutral.json', 'a design source (the token sheet, the brand pack, a hall sheet or DESIGN.md)', '1005082'],
|
|
24
|
+
[UI, 'gen-design-reference.js', 'modules/ui-design/public/brand.html (the public /brand page)', 'a design source (the token sheet, the brand pack, a hall sheet or DESIGN.md)', '1005083'],
|
|
25
|
+
// The logo and favicon set, and the two PNGs by hash (task 1005133): mark.svg or the
|
|
26
|
+
// pack's colours changed without a regeneration. A PNG needs `--png` (a system browser).
|
|
27
|
+
[['scripts', 'bongos'], 'gen-brand.js', 'the logo set in modules/ui-design/public/brand/', 'mark.svg or the brand pack', '1005133'],
|
|
23
28
|
]) {
|
|
24
|
-
const gen = path.join(root,
|
|
29
|
+
const gen = path.join(root, ...dir, script);
|
|
25
30
|
if (!fs.existsSync(gen)) continue;
|
|
26
31
|
try { execFileSync(process.execPath, [gen, '--check'], { cwd: root, stdio: 'pipe' }); }
|
|
27
|
-
catch
|
|
32
|
+
catch (e) {
|
|
33
|
+
const said = String((e && e.stderr) || '').trim().split('\n').filter(Boolean).join(' ');
|
|
34
|
+
violations.push(`${out} is STALE — ${why} changed without a regeneration. Run \`node ${[...dir, script].join('/')}\`${script === 'gen-brand.js' ? ' (with --png for a PNG)' : ''} and commit (task ${task}).${said ? ` [${said}]` : ''}`);
|
|
35
|
+
}
|
|
28
36
|
}
|
|
29
37
|
return violations;
|
|
30
38
|
}
|
|
@@ -17,17 +17,23 @@
|
|
|
17
17
|
// og-card.png 1200x630, the link-preview card every public page names (task 1005091):
|
|
18
18
|
// the dark mark and the lowercase wordmark on the void (--png; needs a browser)
|
|
19
19
|
//
|
|
20
|
-
// Deterministic text, so `--check` is a clean gate (tests/brand_mark.mjs runs it
|
|
21
|
-
// rendered by the system browser
|
|
20
|
+
// Deterministic text, so `--check` is a clean gate (tests/brand_mark.mjs runs it; fitness runs it
|
|
21
|
+
// through scripts/bongos/design-generators-fresh.js). The PNGs are rendered by the system browser
|
|
22
|
+
// and only written with --png; a browser's pixels are not byte-stable across machines, so they
|
|
23
|
+
// are checked by hash instead (task 1005133): --png records, in png-sources.json beside them,
|
|
24
|
+
// a hash of the exact page each was rendered from and a hash of the PNG written. --check fails
|
|
25
|
+
// when the page a PNG would be rendered from today differs from the one recorded (a source or a
|
|
26
|
+
// colour moved without a re-render), or when the PNG is not the one that render wrote.
|
|
22
27
|
//
|
|
23
28
|
// Usage:
|
|
24
29
|
// node scripts/bongos/gen-brand.js # write the SVG variants
|
|
25
30
|
// node scripts/bongos/gen-brand.js --png # … and render apple-touch-icon.png + og-card.png
|
|
26
|
-
// node scripts/bongos/gen-brand.js --check # write nothing; exit 2 if a variant is stale
|
|
31
|
+
// node scripts/bongos/gen-brand.js --check # write nothing; exit 2 if a variant or a PNG is stale
|
|
27
32
|
|
|
28
33
|
const fs = require('node:fs');
|
|
29
34
|
const path = require('node:path');
|
|
30
|
-
const
|
|
35
|
+
const crypto = require('node:crypto');
|
|
36
|
+
const { readMarkSource, markParts, markFile, faviconSvg } = require('../../src/brand-mark');
|
|
31
37
|
const { escapeHtml } = require('../../src/html-escape');
|
|
32
38
|
|
|
33
39
|
const ROOT = path.resolve(__dirname, '..', '..');
|
|
@@ -59,22 +65,71 @@ function ogCardHtml({ markSvg, name, fontLinks = [], fontFamily = 'sans-serif' }
|
|
|
59
65
|
'</body></html>';
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
|
|
68
|
+
// The page apple-touch-icon.png is rendered from: the light mark on the pack's ground. Pure.
|
|
69
|
+
function touchIconHtml({ markSvg, bg }) {
|
|
70
|
+
return `<html><body style="margin:0;background:${bg}">` +
|
|
71
|
+
`<img src="data:image/svg+xml;base64,${Buffer.from(markSvg).toString('base64')}" style="display:block;width:128px;height:128px;margin:26px">` +
|
|
72
|
+
'</body></html>';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Each PNG as the exact page and viewport it is rendered from, given the SVG variants it
|
|
76
|
+
// draws (the text gen-brand writes, not the files on disk, so a stale SVG shows here too).
|
|
77
|
+
function pngPages(files, pack) {
|
|
78
|
+
return {
|
|
79
|
+
'apple-touch-icon.png': {
|
|
80
|
+
width: 180, height: 180, wait: 'load',
|
|
81
|
+
html: touchIconHtml({ markSvg: files['mark-light.svg'], bg: pack.theme.ui.colors.bg }),
|
|
82
|
+
},
|
|
83
|
+
'og-card.png': {
|
|
84
|
+
width: 1200, height: 630, wait: 'networkidle',
|
|
85
|
+
html: ogCardHtml({
|
|
86
|
+
markSvg: files['mark-dark.svg'],
|
|
87
|
+
name: pack.identity.productName,
|
|
88
|
+
fontLinks: pack.theme.ui.fonts.links,
|
|
89
|
+
fontFamily: pack.theme.ui.fonts.display,
|
|
90
|
+
}),
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex');
|
|
96
|
+
// The hash of a render's input: the page and the viewport it is shot at.
|
|
97
|
+
function pageHash(p) { return sha256(JSON.stringify([p.width, p.height, p.html])); }
|
|
98
|
+
|
|
99
|
+
const PNG_MANIFEST = 'png-sources.json';
|
|
100
|
+
|
|
101
|
+
// The PNGs that do not match what --png would write today, each with the reason. Pure I/O
|
|
102
|
+
// read of the brand folder; writes nothing.
|
|
103
|
+
function stalePngs(dir, pages) {
|
|
104
|
+
let manifest = {};
|
|
105
|
+
try { manifest = JSON.parse(fs.readFileSync(path.join(dir, PNG_MANIFEST), 'utf8')); } catch { manifest = {}; }
|
|
106
|
+
const stale = [];
|
|
107
|
+
for (const [name, p] of Object.entries(pages)) {
|
|
108
|
+
const rec = manifest[name];
|
|
109
|
+
const file = path.join(dir, name);
|
|
110
|
+
if (!rec || !fs.existsSync(file)) { stale.push(`${name} (not recorded in ${PNG_MANIFEST})`); continue; }
|
|
111
|
+
if (rec.page !== pageHash(p)) stale.push(`${name} (its source page changed since it was rendered)`);
|
|
112
|
+
else if (rec.png !== sha256(fs.readFileSync(file))) stale.push(`${name} (the file is not the one --png wrote)`);
|
|
113
|
+
}
|
|
114
|
+
return stale;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function renderPngs(dir, pages) {
|
|
63
118
|
const { launchBrowser } = require('../../modules/ui-design/kit/lib'); // eslint-disable-line global-require
|
|
64
|
-
const pack = readPack();
|
|
65
119
|
const browser = await launchBrowser();
|
|
120
|
+
const manifest = {};
|
|
66
121
|
try {
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
await page.screenshot({ path: path.join(dir, 'og-card.png'), omitBackground: false });
|
|
122
|
+
for (const [name, p] of Object.entries(pages)) {
|
|
123
|
+
const page = await browser.newPage({ viewport: { width: p.width, height: p.height }, deviceScaleFactor: 1 });
|
|
124
|
+
await page.setContent(p.html);
|
|
125
|
+
await page.waitForLoadState(p.wait);
|
|
126
|
+
await page.evaluate(() => document.fonts.ready);
|
|
127
|
+
await page.screenshot({ path: path.join(dir, name), omitBackground: false });
|
|
128
|
+
await page.close();
|
|
129
|
+
manifest[name] = { page: pageHash(p), png: sha256(fs.readFileSync(path.join(dir, name))) };
|
|
130
|
+
}
|
|
77
131
|
} finally { await browser.close(); }
|
|
132
|
+
fs.writeFileSync(path.join(dir, PNG_MANIFEST), JSON.stringify(manifest, null, 2) + '\n');
|
|
78
133
|
}
|
|
79
134
|
|
|
80
135
|
// { filename: text } for every SVG variant. Pure given the source and the colours.
|
|
@@ -84,33 +139,21 @@ function brandFiles(src, colors) {
|
|
|
84
139
|
const mono = '#000000';
|
|
85
140
|
return {
|
|
86
141
|
'mark-light.svg': markFile(parts, { ink: colors.ink, dot: colors.accent, label: 'logo' }),
|
|
87
|
-
'mark-dark.svg': markFile(parts, { ink:
|
|
142
|
+
'mark-dark.svg': markFile(parts, { ink: colors.bgDeep, dot: colors.accent, label: 'logo' }),
|
|
88
143
|
'mark-mono.svg': markFile(parts, { ink: mono, dot: mono, label: 'logo' }),
|
|
89
144
|
'mark-body.svg': markFile(parts, { ink: mono, dot: mono, only: ['ring', 'disc'] }),
|
|
90
145
|
'mark-dot.svg': markFile(parts, { ink: mono, dot: mono, only: ['dot'] }),
|
|
91
|
-
'favicon.svg': faviconSvg({ ink: colors.ink, accent: colors.accent }, src) + '\n',
|
|
146
|
+
'favicon.svg': faviconSvg({ ink: colors.ink, accent: colors.accent, night: colors.bgDeep }, src) + '\n',
|
|
92
147
|
};
|
|
93
148
|
}
|
|
94
149
|
|
|
95
|
-
async function renderTouchIcon(dir, colors) {
|
|
96
|
-
const { launchBrowser } = require('../../modules/ui-design/kit/lib'); // eslint-disable-line global-require
|
|
97
|
-
const svg = fs.readFileSync(path.join(dir, 'mark-light.svg'), 'utf8');
|
|
98
|
-
const browser = await launchBrowser();
|
|
99
|
-
try {
|
|
100
|
-
const page = await browser.newPage({ viewport: { width: 180, height: 180 }, deviceScaleFactor: 1 });
|
|
101
|
-
await page.setContent(`<html><body style="margin:0;background:${colors.bg}">` +
|
|
102
|
-
`<img src="data:image/svg+xml;base64,${Buffer.from(svg).toString('base64')}" style="display:block;width:128px;height:128px;margin:26px">` +
|
|
103
|
-
'</body></html>');
|
|
104
|
-
await page.waitForLoadState('load');
|
|
105
|
-
await page.screenshot({ path: path.join(dir, 'apple-touch-icon.png'), omitBackground: false });
|
|
106
|
-
} finally { await browser.close(); }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
150
|
async function main() {
|
|
110
151
|
const args = process.argv.slice(2);
|
|
111
152
|
const dir = brandDir();
|
|
112
|
-
const
|
|
153
|
+
const pack = readPack();
|
|
154
|
+
const colors = pack.theme.ui.colors;
|
|
113
155
|
const files = brandFiles(readMarkSource({ root: ROOT }), colors);
|
|
156
|
+
const pages = pngPages(files, pack);
|
|
114
157
|
const stale = [];
|
|
115
158
|
for (const [name, text] of Object.entries(files)) {
|
|
116
159
|
const file = path.join(dir, name);
|
|
@@ -120,11 +163,14 @@ async function main() {
|
|
|
120
163
|
if (!args.includes('--check')) fs.writeFileSync(file, text);
|
|
121
164
|
}
|
|
122
165
|
if (args.includes('--check')) {
|
|
123
|
-
|
|
124
|
-
process.
|
|
166
|
+
const pngs = stalePngs(dir, pages);
|
|
167
|
+
if (stale.length) process.stderr.write(`gen-brand: stale — ${stale.join(', ')}. Run: node scripts/bongos/gen-brand.js\n`);
|
|
168
|
+
if (pngs.length) process.stderr.write(`gen-brand: stale PNG — ${pngs.join('; ')}. Run: node scripts/bongos/gen-brand.js --png\n`);
|
|
169
|
+
if (stale.length || pngs.length) process.exit(2);
|
|
170
|
+
process.stdout.write('gen-brand: the logo and favicon set match mark.svg, and the PNGs match their sources\n');
|
|
125
171
|
return;
|
|
126
172
|
}
|
|
127
|
-
if (args.includes('--png'))
|
|
173
|
+
if (args.includes('--png')) await renderPngs(dir, pages);
|
|
128
174
|
process.stdout.write(`gen-brand: ${stale.length ? `wrote ${stale.join(', ')}` : 'nothing to write'}${args.includes('--png') ? ' + apple-touch-icon.png + og-card.png' : ''}\n`);
|
|
129
175
|
}
|
|
130
176
|
|
|
@@ -338,6 +338,7 @@ async function coreUpgradeInstance(inst, deps, intent) {
|
|
|
338
338
|
log(` ✓ would then read ${versionUrl} on the box and require it to say ${to}`);
|
|
339
339
|
return { ok: true, served: null, preflight: verdict };
|
|
340
340
|
}
|
|
341
|
+
backfillHubUpgradeUrl(inst, deps, boxExec);
|
|
341
342
|
try {
|
|
342
343
|
log(` [upgrade] (in ${runDir}) ${cmd}`);
|
|
343
344
|
boxExec(cmd, { cwd: runDir });
|
|
@@ -672,6 +673,34 @@ function resolveRunAs(inst, deps, { probe: allowProbe = true } = {}) {
|
|
|
672
673
|
// HERE rather than left to instanceUser()'s own throw: that throw is correct today, but
|
|
673
674
|
// a call site whose safety depends on a helper's internals is one refactor away from a
|
|
674
675
|
// shell-injection path (and the apply leg's own guard sits at the route, not here).
|
|
676
|
+
// WHERE THIS PROJECT'S UPDATE BUTTON GOES, written on every move (task 1005128): a project
|
|
677
|
+
// provisioned before task 1004608 has no <PREFIX>_HUB_UPGRADE_URL line, and only a
|
|
678
|
+
// settings-apply ever wrote one. The move restarts the service, so the line is read the moment
|
|
679
|
+
// the new core comes up. BEST-EFFORT: it is written before the move and never decides it; an
|
|
680
|
+
// unreadable file, a refused write or a line already right changes nothing and fails nothing.
|
|
681
|
+
function backfillHubUpgradeUrl(inst, deps, boxExec) {
|
|
682
|
+
// The slug is interpolated into a shell command below, so it is checked HERE, at the point
|
|
683
|
+
// of use, not trusted to a caller that validated it earlier (the rule coreUpgradeInstance
|
|
684
|
+
// states for its own command).
|
|
685
|
+
if (!inst || !isValidSlug(inst.slug)) return false;
|
|
686
|
+
try {
|
|
687
|
+
const { hubUpgradeEnvPatch, webEnvPath } = require('./provision-units.js'); // eslint-disable-line global-require
|
|
688
|
+
const sudoP = deps.privileged ? 'sudo ' : '';
|
|
689
|
+
const read = boxExec(`${sudoP}cat ${webEnvPath(inst)}`, { allowFail: true });
|
|
690
|
+
if (!read || read.softFailed) return false;
|
|
691
|
+
const next = hubUpgradeEnvPatch(read.stdout, inst);
|
|
692
|
+
if (!next) return false;
|
|
693
|
+
const write = inst.hosting_shape === 'cloud-host' ? (deps.controlWriteFile || deps.writeFile) : deps.writeFile;
|
|
694
|
+
if (typeof write !== 'function') return false;
|
|
695
|
+
write(webEnvPath(inst), next, { mode: 0o600, sudo: true });
|
|
696
|
+
deps.log(' [config] wrote where this project\'s Update button goes into web.env (read on restart)');
|
|
697
|
+
return true;
|
|
698
|
+
} catch (e) {
|
|
699
|
+
deps.log(` [config] could not write the Update button's address (${(e && e.message) || e}); the move goes on`);
|
|
700
|
+
return false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
675
704
|
function upgradeInvocation(inst, to, { privileged = false, dryRun = false, requestedBy = '', automatic = false, selfRoot = selfInstanceRoot, runAs = null, migrateAs = null } = {}) {
|
|
676
705
|
if (!isValidSlug(inst && inst.slug)) {
|
|
677
706
|
throw new Error(`upgradeInvocation: refusing to build a shell command from an invalid slug ${JSON.stringify(inst && inst.slug)}`);
|
|
@@ -1164,4 +1193,4 @@ async function snapshotAllCoreVersions(deps) {
|
|
|
1164
1193
|
return { seen };
|
|
1165
1194
|
}
|
|
1166
1195
|
|
|
1167
|
-
module.exports = { pushUpgradePin, UPGRADE_SHAPES, instanceChannel, CHANGE_CAP, REPO_NOTES, repoNote, requestedByOf, parseChangeRecords, readInstalledNotes, readPackagedNotes, changesBetween, coreCheckoutCandidates, findCoreCheckout, gitFault, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, snapshotCoreVersions, snapshotAllCoreVersions };
|
|
1196
|
+
module.exports = { pushUpgradePin, UPGRADE_SHAPES, instanceChannel, CHANGE_CAP, REPO_NOTES, repoNote, requestedByOf, parseChangeRecords, readInstalledNotes, readPackagedNotes, changesBetween, coreCheckoutCandidates, findCoreCheckout, gitFault, resolveRunAs, versionCmd, parseServedVersion, parseSchemaPending, targetFault, parsePreflight, upgradeInvocation, preflightUpgrade, preflightCoreUpgrade, coreUpgradeInstance, backfillHubUpgradeUrl, snapshotCoreVersions, snapshotAllCoreVersions };
|