@bongos/core 1.19.331 → 1.19.333
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 +163 -43
- package/docs/api/openapi.json +2 -2
- package/docs/api-reference.md +1 -1
- package/docs/copy-inventory.md +202 -202
- package/docs/copy-registry.json +384 -384
- package/modules/grading/grader-config.js +98 -0
- package/modules/grading/grader-model.js +150 -0
- package/modules/grading/grader-prompt.js +139 -0
- package/modules/grading/grader-score.js +706 -0
- package/modules/grading/grader-subagent.js +328 -0
- package/modules/grading/grader.js +5 -1287
- package/modules/hall-ui/public/builders.js +19 -1182
- package/modules/hall-ui/public/goals-page.js +11 -997
- package/modules/hall-ui/public/goals-render.js +1032 -0
- package/modules/hall-ui/public/goals.html +4 -0
- package/modules/hall-ui/public/hall-render.js +1211 -0
- package/modules/hall-ui/public/index.html +6 -0
- package/modules/lifecycle/routes/goal-route-authz.js +400 -0
- package/modules/lifecycle/routes/goals.js +1 -367
- package/modules/lifecycle/routes/task-route-helpers.js +219 -0
- package/modules/lifecycle/routes/task-write-routes.js +542 -0
- package/modules/lifecycle/routes/tasks.js +3 -696
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +4 -0
- package/public-landing/projects.html +103 -23
- package/scripts/gds/box-infra.js +280 -0
- package/scripts/gds/box.js +1 -245
- package/scripts/gds/fitness-checks-identity.js +320 -0
- package/scripts/gds/fitness-checks-vocabulary.js +230 -0
- package/scripts/gds/fitness-checks-write-validation.js +144 -0
- package/scripts/gds/fitness-lib.js +82 -0
- package/scripts/gds/fitness.js +55 -668
- package/scripts/gds/provision-config.js +204 -0
- package/scripts/gds/provision-net.js +221 -0
- package/scripts/gds/provision-repo.js +849 -0
- package/scripts/gds/provision-units.js +262 -0
- package/scripts/gds/provision.js +4 -1395
- package/src/bongos/auth-admission.js +264 -0
- package/src/bongos/auth-config.js +151 -0
- package/src/bongos/auth-discord.js +198 -0
- package/src/bongos/auth-github.js +336 -0
- package/src/bongos/auth-idp.js +330 -0
- package/src/bongos/auth.js +5 -1100
- package/src/module-api.js +5 -1
- package/tests/auth_admission_gate.mjs +3 -1
- package/tests/est_advisory.mjs +3 -1
- package/tests/goal_advisory.mjs +3 -2
- package/tests/goal_criterion_confirm_ui.mjs +6 -0
- package/tests/goal_subtitle.mjs +5 -2
- package/tests/goals_card_view.mjs +10 -3
- package/tests/grader_generated_provenance.mjs +3 -1
- package/tests/hall_palette.mjs +3 -1
- package/tests/helpers.mjs +58 -25
- package/tests/lib_sh_resolution.mjs +3 -1
- package/tests/membership_kind_lockstep.mjs +3 -1
- package/tests/migration_allocation.mjs +5 -2
- package/tests/patch_gate_ordering.mjs +3 -1
- package/tests/projects_hub_pre_uat.mjs +53 -0
- package/tests/rank_tier_single_source.mjs +16 -3
- package/tests/reward_gate.mjs +3 -2
package/package-lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bongos/core",
|
|
3
|
-
"version": "1.19.
|
|
3
|
+
"version": "1.19.333",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@bongos/core",
|
|
9
|
-
"version": "1.19.
|
|
9
|
+
"version": "1.19.333",
|
|
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.19.
|
|
3
|
+
"version": "1.19.333",
|
|
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",
|
|
@@ -414,6 +414,10 @@ p.sec-p{font-size:var(--fs-lede);font-weight:500;color:var(--ink-soft);line-heig
|
|
|
414
414
|
/* ── page-level bits ── */
|
|
415
415
|
.alert{display:none;background:var(--err-soft);border:2px solid color-mix(in srgb,var(--err) 25%,transparent);color:var(--err);font-weight:600;font-size:14px;border-radius:14px;padding:12px 16px;margin-bottom:18px;}
|
|
416
416
|
.alert.on{display:block;}
|
|
417
|
+
/* the raw server text folds under the alert's plain sentence (task 1003244) */
|
|
418
|
+
.alert details{margin-top:8px;}
|
|
419
|
+
.alert summary{cursor:pointer;font-size:12.5px;font-weight:700;}
|
|
420
|
+
.alert details p{margin-top:6px;font-family:var(--font-mono);font-size:12px;font-weight:500;overflow-wrap:anywhere;}
|
|
417
421
|
.loading{font-size:14.5px;font-weight:600;color:var(--ink-faint);padding:14px 0;}
|
|
418
422
|
.empty{border:2px dashed var(--line);border-radius:18px;padding:22px;text-align:center;}
|
|
419
423
|
.empty p{font-size:14.5px;font-weight:600;color:var(--ink-soft);margin-bottom:14px;line-height:1.55;}
|
|
@@ -614,7 +614,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
614
614
|
stripFor/errMarkup/canRetry/canRevive the list card reads -->
|
|
615
615
|
<div id="mStrip"></div>
|
|
616
616
|
<div class="projMsg err" id="mErr" hidden></div>
|
|
617
|
-
<div class="projMsg" id="mMsg"></div>
|
|
617
|
+
<div class="projMsg" id="mMsg" role="status" aria-live="polite"></div>
|
|
618
618
|
<div class="actions" id="mActs" hidden></div>
|
|
619
619
|
</div>
|
|
620
620
|
|
|
@@ -683,7 +683,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
683
683
|
<div class="wrap viewWrap">
|
|
684
684
|
<div class="card">
|
|
685
685
|
<ol class="steps" aria-label="Creation steps">
|
|
686
|
-
<li class="now" data-step="1"><span class="snum">1</span><span class="slbl">Name</span></li>
|
|
686
|
+
<li class="now" data-step="1" aria-current="step"><span class="snum">1</span><span class="slbl">Name</span></li>
|
|
687
687
|
<li data-step="2"><span class="snum">2</span><span class="slbl">Type</span></li>
|
|
688
688
|
<li data-step="3"><span class="snum">3</span><span class="slbl">Home</span></li>
|
|
689
689
|
<li data-step="4"><span class="snum">4</span><span class="slbl">Address</span></li>
|
|
@@ -2400,16 +2400,27 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2400
2400
|
Six steps since task 1002772: gate 0 · name 1 · type 2 · home 3 ·
|
|
2401
2401
|
address 4 · review 5 · done 6 (the rail shows 1-5; done marks it all). */
|
|
2402
2402
|
var panels = { 0: 'panel-gate', 1: 'panel-name', 2: 'panel-type', 3: 'panel-home', 4: 'panel-address', 5: 'panel-review', 6: 'panel-done' };
|
|
2403
|
+
/* the rail at the done panel (task 1003244): "done" is the Create step
|
|
2404
|
+
still running — the marker stays on 5 until the project actually
|
|
2405
|
+
answers, and only then does the whole rail read finished. aria-current
|
|
2406
|
+
tells a screen reader the same thing. */
|
|
2407
|
+
function paintRail(step) {
|
|
2408
|
+
var landed = step === 6 && !!(live && live.instStatus === 'active');
|
|
2409
|
+
var cur = step === 6 ? 5 : step;
|
|
2410
|
+
root.querySelectorAll('.steps li').forEach(function (li) {
|
|
2411
|
+
var n = Number(li.getAttribute('data-step'));
|
|
2412
|
+
var now = !landed && n === cur;
|
|
2413
|
+
li.classList.toggle('now', now);
|
|
2414
|
+
li.classList.toggle('done', landed || n < cur);
|
|
2415
|
+
if (now) li.setAttribute('aria-current', 'step'); else li.removeAttribute('aria-current');
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2403
2418
|
function show(step, moveFocus) {
|
|
2404
2419
|
state.step = step;
|
|
2405
2420
|
Object.keys(panels).forEach(function (k) {
|
|
2406
2421
|
$(panels[k]).classList.toggle('on', Number(k) === step);
|
|
2407
2422
|
});
|
|
2408
|
-
|
|
2409
|
-
var n = Number(li.getAttribute('data-step'));
|
|
2410
|
-
li.classList.toggle('now', n === step);
|
|
2411
|
-
li.classList.toggle('done', n < step || step === 6);
|
|
2412
|
-
});
|
|
2423
|
+
paintRail(step);
|
|
2413
2424
|
saveDraft();
|
|
2414
2425
|
/* the done step takes the whole card — the bongo pair steps aside */
|
|
2415
2426
|
root.querySelector('.createSplit').classList.toggle('wizDone', step === 6);
|
|
@@ -2782,14 +2793,24 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2782
2793
|
}
|
|
2783
2794
|
|
|
2784
2795
|
/* ── create ── */
|
|
2796
|
+
/* the mapped codes are headline-only; anything else gets a fixed owner
|
|
2797
|
+
sentence with the raw text folded under Technical details (direction
|
|
2798
|
+
§5.1, task 1003244) — operator text is never the headline */
|
|
2785
2799
|
function friendlyCreateError(err) {
|
|
2786
2800
|
var code = err && err.data && err.data.error && (err.data.error.code || '');
|
|
2787
2801
|
if (code === 'slug_taken') return 'That handle is already taken by another project. Pick a different one.';
|
|
2788
2802
|
if (code === 'bad_slug') return 'The handle didn’t pass the server’s checks — 2–40 lowercase letters, numbers, or dashes.';
|
|
2789
|
-
|
|
2803
|
+
/* both server branches: a reserved hostname, and any name under the platform's own apex */
|
|
2804
|
+
if (code === 'domain_reserved') return 'That address belongs to the platform’s own domain — your project already gets its own address there from its handle. Use a domain you own instead.';
|
|
2790
2805
|
if (code === 'domain_taken') return 'That address is already held by another project. Pick a different handle or domain.';
|
|
2791
2806
|
if (code === 'bad_domain') return 'The domain didn’t parse as a real hostname — use something like myproject.example.com.';
|
|
2792
|
-
return '
|
|
2807
|
+
if (code === 'repo_scope_required') return 'GitHub hasn’t granted repository access for this session. Sign in again — it asks for repository access — and try once more.';
|
|
2808
|
+
if (code === 'request_failed') return 'The platform couldn’t file the request. Nothing was created — try again in a moment.';
|
|
2809
|
+
return '';
|
|
2810
|
+
}
|
|
2811
|
+
function createFailureMarkup(err) {
|
|
2812
|
+
return 'We couldn’t file the request just now. Nothing was created — you can try again.' +
|
|
2813
|
+
'<details><summary>Technical details</summary><p>' + esc(err && err.message ? err.message : 'no detail came back') + '</p></details>';
|
|
2793
2814
|
}
|
|
2794
2815
|
|
|
2795
2816
|
/* Fresh + repo access: auto-create the empty GitHub repo NOW (the commit moment),
|
|
@@ -2901,7 +2922,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2901
2922
|
$('h-home').className = 'hint err';
|
|
2902
2923
|
return;
|
|
2903
2924
|
}
|
|
2904
|
-
|
|
2925
|
+
var friendly = friendlyCreateError(err);
|
|
2926
|
+
if (friendly) alertBox.textContent = friendly;
|
|
2927
|
+
else alertBox.innerHTML = createFailureMarkup(err);
|
|
2905
2928
|
alertBox.classList.add('on');
|
|
2906
2929
|
});
|
|
2907
2930
|
});
|
|
@@ -2918,7 +2941,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
2918
2941
|
var GH_ICON = '<svg class="gh" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27s1.36.09 2 .27c1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8z"/></svg>';
|
|
2919
2942
|
|
|
2920
2943
|
var instanceId = null; /* set when a create lands / a resumed watch starts */
|
|
2921
|
-
var live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
|
|
2944
|
+
var live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
2922
2945
|
var planPromises = {}; /* mode|domain → Promise<steps[]> */
|
|
2923
2946
|
var lastStepsHtml = '';
|
|
2924
2947
|
var FALLBACK_HUMAN_KEYS = ['github-repo', 'oauth-app', 'device-flow', 'app-install', 'first-sign-in', 'enter-hall'];
|
|
@@ -3000,8 +3023,15 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3000
3023
|
return '<li class="sdone" data-onboard-step="oauth-app"><b>GitHub sign-in is wired</b><p>Credentials placed and verified — sign-in on <code style="font-family:var(--font-mono)">' + esc(domain) + '</code> is live.</p></li>';
|
|
3001
3024
|
}
|
|
3002
3025
|
if (live.manifest === 'error' || live.manifest === 'expired') {
|
|
3003
|
-
|
|
3004
|
-
|
|
3026
|
+
/* a plain sentence first; the raw route text folds under Technical
|
|
3027
|
+
details (task 1003244). The one refusal a founder can act on — no
|
|
3028
|
+
address yet — says where to go instead of what the server said. */
|
|
3029
|
+
var snag, raw = '';
|
|
3030
|
+
if (live.manifest === 'expired') snag = 'That setup link expired (they last 30 minutes) — mint a fresh one.';
|
|
3031
|
+
else if (live.manifestCode === 'domain_required') snag = 'This project needs an address first — attach one from <a href="?view=mine">your projects page</a>, then come back to this step.';
|
|
3032
|
+
else { snag = 'GitHub setup didn’t start. Nothing was changed on GitHub — try again.'; raw = live.manifestMsg; }
|
|
3033
|
+
return '<li data-onboard-step="oauth-app"><b>Wire GitHub sign-in</b><p>' + snag + '</p>' +
|
|
3034
|
+
(raw ? '<details><summary>Technical details</summary><p>' + esc(raw) + '</p></details>' : '') +
|
|
3005
3035
|
'<button type="button" class="ghbtn" data-action="manifest-start">' + GH_ICON + 'Set up GitHub sign-in</button>' + manualOauthDetails() + '</li>';
|
|
3006
3036
|
}
|
|
3007
3037
|
if (live.manifest === 'code_received') {
|
|
@@ -3186,6 +3216,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3186
3216
|
return res.json().catch(function () { return {}; }).then(function (d) {
|
|
3187
3217
|
if (!res.ok) {
|
|
3188
3218
|
var e = new Error((d.error && (d.error.message || d.error.code)) || ('HTTP ' + res.status));
|
|
3219
|
+
e.code = (d.error && d.error.code) || '';
|
|
3189
3220
|
throw e;
|
|
3190
3221
|
}
|
|
3191
3222
|
return d;
|
|
@@ -3206,6 +3237,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3206
3237
|
if (String(err && err.message) === 'signing in') return;
|
|
3207
3238
|
if (tab) { try { tab.close(); } catch (e) {} }
|
|
3208
3239
|
live.manifest = 'error'; live.manifestMsg = String((err && err.message) || 'could not start');
|
|
3240
|
+
live.manifestCode = String((err && err.code) || '');
|
|
3209
3241
|
renderSteps();
|
|
3210
3242
|
});
|
|
3211
3243
|
}
|
|
@@ -3380,7 +3412,10 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3380
3412
|
/* the checklist reflects live state (task 1002694): a status transition —
|
|
3381
3413
|
e.g. the scaffold landing — re-renders the steps (change-only, so the
|
|
3382
3414
|
5s tick never clobbers an open dropdown or mid-flight button) */
|
|
3383
|
-
if (inst && inst.status !== live.instStatus) {
|
|
3415
|
+
if (inst && inst.status !== live.instStatus) {
|
|
3416
|
+
live.instStatus = inst.status; renderSteps();
|
|
3417
|
+
if (state.step === 6) paintRail(6); /* the rail finishes only when the project answers */
|
|
3418
|
+
}
|
|
3384
3419
|
var terminal = renderProgress(inst);
|
|
3385
3420
|
if (!terminal) pollTimer = setTimeout(tick, 5000);
|
|
3386
3421
|
else stopPolling();
|
|
@@ -3412,7 +3447,7 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3412
3447
|
instanceId = null;
|
|
3413
3448
|
watchPending = false; manifestPending = false; manifestState = null;
|
|
3414
3449
|
manifestStartedAt = 0; manifestStale = false;
|
|
3415
|
-
live = { instStatus: null, manifest: null, manifestMsg: '', appSlug: null };
|
|
3450
|
+
live = { instStatus: null, manifest: null, manifestMsg: '', manifestCode: '', appSlug: null };
|
|
3416
3451
|
lastStepsHtml = '';
|
|
3417
3452
|
try { sessionStorage.removeItem(CREATED_KEY); sessionStorage.removeItem(DRAFT_KEY); } catch (e) {}
|
|
3418
3453
|
state = { step: 1, name: '', projType: null, mode: 'greenfield', owner: '', ownerTouched: false, repo: '', repoTouched: false, slug: '', slugTouched: false, domain: '', noAddress: false };
|
|
@@ -3882,7 +3917,9 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3882
3917
|
: '. The repository is still yours, and its database is kept — bringing it back takes one click. Or <a href="?view=new">start another</a>.</div>');
|
|
3883
3918
|
}
|
|
3884
3919
|
var lm = lastMsg[inst.id];
|
|
3885
|
-
|
|
3920
|
+
/* a live region: an action's result is announced where the pointer is,
|
|
3921
|
+
not only painted (task 1003244) */
|
|
3922
|
+
body += '<div class="projMsg' + (lm && lm.cls ? ' ' + lm.cls : '') + '" id="msg-' + iid + '" role="status" aria-live="polite">' + (lm ? lm.text : '') + '</div>';
|
|
3886
3923
|
/* every owned card opens its manage view (task 1003041) — a real link, so
|
|
3887
3924
|
middle/ctrl-click works; the delegated handler upgrades a plain click */
|
|
3888
3925
|
var acts = ['<a class="btn small ghost" data-manage="' + iid + '" href="?view=manage&id=' + iid + '">Manage</a>'];
|
|
@@ -3903,12 +3940,51 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
3903
3940
|
var keepOpen = !!(oldFold && oldFold.open);
|
|
3904
3941
|
var typed = oldInput && oldInput.value !== oldInput.defaultValue ? oldInput.value : '';
|
|
3905
3942
|
var hadFocus = !!oldInput && document.activeElement === oldInput;
|
|
3943
|
+
/* focus anywhere else on the card (a door, a confirm, the fold's summary)
|
|
3944
|
+
comes back to the same control — or the nearest one — after the swap;
|
|
3945
|
+
quietActions() leaves a note when a disabled button already let go */
|
|
3946
|
+
var wasInside = li.contains(document.activeElement) && document.activeElement !== li;
|
|
3947
|
+
var prefer = wasInside ? focusKeyOf(document.activeElement) : null;
|
|
3948
|
+
var focusBack = wasInside || li.hasAttribute('data-focus-back');
|
|
3949
|
+
li.removeAttribute('data-focus-back');
|
|
3906
3950
|
li.innerHTML = orbFor(inst) + '<div class="projBody">' + body + '</div>';
|
|
3907
3951
|
var fold = li.querySelector('details.attachFold');
|
|
3908
3952
|
var input = fold ? fold.querySelector('input') : null;
|
|
3909
3953
|
if (fold && keepOpen) fold.open = true;
|
|
3910
3954
|
if (input && typed) input.value = typed;
|
|
3911
3955
|
if (input && hadFocus) input.focus();
|
|
3956
|
+
else if (focusBack) focusCardControl(inst.id, prefer);
|
|
3957
|
+
}
|
|
3958
|
+
|
|
3959
|
+
/* ── keeping focus on the card through a re-render (task 1003244) ── */
|
|
3960
|
+
|
|
3961
|
+
/* which control had focus, as a selector the rebuilt card can answer;
|
|
3962
|
+
a confirm's Yes/Keep lands back on the door that opened it */
|
|
3963
|
+
function focusKeyOf(el) {
|
|
3964
|
+
if (!el || !el.getAttribute) return null;
|
|
3965
|
+
if (el.tagName === 'SUMMARY') return 'details.attachFold summary';
|
|
3966
|
+
if (el.hasAttribute('data-teardown-yes') || el.hasAttribute('data-teardown-no')) return '[data-teardown]';
|
|
3967
|
+
var keys = ['data-manage', 'data-attach', 'data-retry', 'data-revive', 'data-teardown'];
|
|
3968
|
+
for (var i = 0; i < keys.length; i++) if (el.hasAttribute(keys[i])) return '[' + keys[i] + ']';
|
|
3969
|
+
return null;
|
|
3970
|
+
}
|
|
3971
|
+
function focusCardControl(id, prefer) {
|
|
3972
|
+
var li = liFor(id);
|
|
3973
|
+
if (!li) return;
|
|
3974
|
+
var t = (prefer && li.querySelector(prefer)) ||
|
|
3975
|
+
li.querySelector('.actions a, .actions button') ||
|
|
3976
|
+
li.querySelector('details.attachFold summary');
|
|
3977
|
+
if (t) t.focus();
|
|
3978
|
+
}
|
|
3979
|
+
/* an action in flight: the row stays put and its buttons go quiet, so
|
|
3980
|
+
nothing under the pointer is deleted mid-click; a button that held
|
|
3981
|
+
focus lets go when disabled, so the card remembers to take it back */
|
|
3982
|
+
function quietActions(id) {
|
|
3983
|
+
var act = $('act-' + id);
|
|
3984
|
+
if (!act) return;
|
|
3985
|
+
var li = liFor(id);
|
|
3986
|
+
if (li && act.contains(document.activeElement)) li.setAttribute('data-focus-back', '');
|
|
3987
|
+
act.querySelectorAll('button').forEach(function (b) { b.disabled = true; });
|
|
3912
3988
|
}
|
|
3913
3989
|
|
|
3914
3990
|
function render(instances) {
|
|
@@ -4465,16 +4541,20 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
4465
4541
|
var act = $('act-' + id);
|
|
4466
4542
|
if (!act) return;
|
|
4467
4543
|
cardAsking = String(id);
|
|
4468
|
-
act.innerHTML = '<div class="confirmRow">' +
|
|
4544
|
+
act.innerHTML = '<div class="confirmRow" tabindex="-1">' +
|
|
4469
4545
|
'<span class="plain">Tear it down? ' + teardownTruth(byId[id]) + '</span>' +
|
|
4470
4546
|
'<button type="button" class="btn small danger" data-teardown-yes="' + esc(id) + '">Yes, tear it down</button>' +
|
|
4471
4547
|
'<button type="button" class="linklike" data-teardown-no="' + esc(id) + '">Keep it running</button></div>';
|
|
4548
|
+
/* the pressed button is gone — focus lands on the question itself, so
|
|
4549
|
+
the truth is read before Tab reaches Yes (task 1003244) */
|
|
4550
|
+
var row = act.firstChild;
|
|
4551
|
+
if (row && row.focus) row.focus();
|
|
4472
4552
|
}
|
|
4473
4553
|
function unAskTeardown(id) { cardAsking = null; var inst = byId[id]; if (inst) renderCard(inst); }
|
|
4474
4554
|
function doTeardown(id) {
|
|
4475
4555
|
cardAsking = null;
|
|
4476
|
-
|
|
4477
|
-
|
|
4556
|
+
quietActions(id);
|
|
4557
|
+
msg(id, 'Asking…');
|
|
4478
4558
|
fetch(API + '/provisioning/instances/' + encodeURIComponent(id) + '/teardown', {
|
|
4479
4559
|
method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: '{}'
|
|
4480
4560
|
}).then(function (res) {
|
|
@@ -4501,8 +4581,8 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
4501
4581
|
}).catch(function () {
|
|
4502
4582
|
msg(id, 'Couldn’t reach the platform — check your connection and try again.', 'err');
|
|
4503
4583
|
}).then(function () {
|
|
4504
|
-
/*
|
|
4505
|
-
|
|
4584
|
+
/* wake the quieted row even when the state didn't change (renderCard
|
|
4585
|
+
also hands focus back), then pick up whatever DID change */
|
|
4506
4586
|
if (byId[id]) renderCard(byId[id]);
|
|
4507
4587
|
watchOnce(id);
|
|
4508
4588
|
});
|
|
@@ -4515,8 +4595,8 @@ h1[tabindex="-1"]:focus:not(:focus-visible){outline:none;}
|
|
|
4515
4595
|
function reProvision(id, verb) {
|
|
4516
4596
|
var inst = byId[id];
|
|
4517
4597
|
if (!inst) return;
|
|
4518
|
-
|
|
4519
|
-
|
|
4598
|
+
quietActions(id);
|
|
4599
|
+
msg(id, 'Asking…');
|
|
4520
4600
|
var body = { slug: inst.slug, hosting_shape: inst.hosting_shape || 'standalone' };
|
|
4521
4601
|
if (inst.onboard_mode) body.onboard_mode = inst.onboard_mode;
|
|
4522
4602
|
if (inst.target_ref) body.target_ref = inst.target_ref;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// scripts/gds/box-infra.js — the dev-box CONFIG block plus everything that
|
|
2
|
+
// turns it into a bootable droplet: infra/ resolution, naming, the DNS hook,
|
|
3
|
+
// and cloud-init rendering.
|
|
4
|
+
//
|
|
5
|
+
// Carved out of scripts/gds/box.js by task 1003206 (goal 1000079, the R03
|
|
6
|
+
// de-monolith plan) to bring that file under the 1,500-line ratchet. A
|
|
7
|
+
// STRUCTURAL move: the lines below are box.js's originals unchanged, and box.js
|
|
8
|
+
// requires every name straight back, so the operator CLI is untouched.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const fs = require('node:fs');
|
|
13
|
+
const path = require('node:path');
|
|
14
|
+
const zlib = require('node:zlib');
|
|
15
|
+
const { execFileSync } = require('node:child_process');
|
|
16
|
+
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
17
|
+
const instanceConfig = require(path.join(REPO_ROOT, 'src/instance-config'));
|
|
18
|
+
const BOX_INFRA_TEMPLATE = 'builder-box-cloud-init.yaml';
|
|
19
|
+
|
|
20
|
+
function boxInfraCandidates({ env = process.env, repoRoot = REPO_ROOT } = {}) {
|
|
21
|
+
const out = [];
|
|
22
|
+
const seen = new Set();
|
|
23
|
+
const add = (d) => { if (d && !seen.has(d)) { seen.add(d); out.push(d); } };
|
|
24
|
+
// 1. explicit override: <PREFIX>_BOX_INFRA_DIR (operator escape hatch).
|
|
25
|
+
const override = instanceConfig.resolveEnv('BOX_INFRA_DIR', { env });
|
|
26
|
+
if (override) add(path.resolve(override));
|
|
27
|
+
// 2. the instance root via an EXPLICIT pointer (<PREFIX>_INSTANCE_ROOT env or a
|
|
28
|
+
// bongos.config.json {"instanceRoot":"."}) — where a vendored instance keeps infra/.
|
|
29
|
+
const inst = instanceConfig.resolveInstanceRootExplicit({ env });
|
|
30
|
+
if (inst) add(path.join(inst, 'infra'));
|
|
31
|
+
// 3. REPO_ROOT/infra — the dev single-checkout (and the ADR 0144 node_modules workaround).
|
|
32
|
+
add(path.join(repoRoot, 'infra'));
|
|
33
|
+
// 4. walk up out of node_modules from the core root, so a vendored
|
|
34
|
+
// …/node_modules/@bongos/core still finds its host instance's infra/ with no pointer set.
|
|
35
|
+
let dir = repoRoot;
|
|
36
|
+
for (let i = 0; i < 6; i += 1) {
|
|
37
|
+
const parent = path.dirname(dir);
|
|
38
|
+
if (parent === dir) break;
|
|
39
|
+
dir = parent;
|
|
40
|
+
add(path.join(dir, 'infra'));
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// The resolved infra/ dir (first candidate holding the template), or null when the
|
|
46
|
+
// box cloud-init cannot be located anywhere — the caller MUST fail loud on null
|
|
47
|
+
// rather than provision a box with empty user-data (ADR 0150).
|
|
48
|
+
function resolveBoxInfraDir(opts = {}) {
|
|
49
|
+
for (const c of boxInfraCandidates(opts)) {
|
|
50
|
+
try { if (fs.existsSync(path.join(c, BOX_INFRA_TEMPLATE))) return c; }
|
|
51
|
+
catch { /* unreadable candidate → try the next */ }
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// task 1002757 — the instance's own apex; null when unconfigured (callers refuse).
|
|
57
|
+
const { instanceApex } = require('./instance-apex');
|
|
58
|
+
|
|
59
|
+
const CONFIG = {
|
|
60
|
+
floor: process.env.BOX_PROVISION_MIN_RANK || 'xenos',
|
|
61
|
+
region: process.env.BOX_REGION || 'nyc3',
|
|
62
|
+
size: process.env.BOX_SIZE || 's-2vcpu-4gb',
|
|
63
|
+
baseImage: process.env.BOX_BASE_IMAGE || 'ubuntu-24-04-x64',
|
|
64
|
+
idleMinutes: numEnv('BOX_IDLE_MINUTES', 10),
|
|
65
|
+
dormantDays: numEnv('BOX_DORMANT_DAYS', 14),
|
|
66
|
+
// task 1002727 — how many days BEFORE the reclaim a builder gets warned.
|
|
67
|
+
dormantWarnLeadDays: numEnv('BOX_DORMANT_WARN_LEAD_DAYS', 3),
|
|
68
|
+
sshKeyIds: (process.env.BOX_SSH_KEY_IDS || '').split(',').map((s) => s.trim()).filter(Boolean),
|
|
69
|
+
dnsHook: process.env.BOX_DNS_HOOK || null,
|
|
70
|
+
tag: process.env.BOX_TAG || 'otb-builder-box',
|
|
71
|
+
// task 1002757 settled this file's old carve-out: an operator-plane DNS zone is
|
|
72
|
+
// still a FUNCTIONAL DEFAULT, and a founder literal aimed another operator's
|
|
73
|
+
// fleet at the founder's zone. Derived from the instance apex, null when
|
|
74
|
+
// unconfigured — hostnameFor() then refuses instead of minting a dead FQDN.
|
|
75
|
+
dnsZone: process.env.BOX_DNS_ZONE || (instanceApex() ? `dev.${instanceApex()}` : null),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Named-tunnel automation for the browser terminal (#771, ADR 0038 Part B).
|
|
79
|
+
// OPT-IN, default OFF — an unset BOX_NAMED_TUNNEL leaves the box on the tokenless
|
|
80
|
+
// quick tunnel (trycloudflare.com), the zero-secret #760 default. When the operator
|
|
81
|
+
// turns it on AND supplies a Cloudflare token with Tunnel + DNS scope (the same
|
|
82
|
+
// CLOUDFLARE_API_TOKEN box-dns uses) + the account id, every provision creates a
|
|
83
|
+
// stable term-<login>.<terminalZone> tunnel and bakes its connector token into the
|
|
84
|
+
// box; deprovision tears it down.
|
|
85
|
+
const CF = {
|
|
86
|
+
enabled: /^(1|true|yes)$/i.test(process.env.BOX_NAMED_TUNNEL || ''),
|
|
87
|
+
apiToken: process.env.CLOUDFLARE_API_TOKEN || null,
|
|
88
|
+
accountId: process.env.CLOUDFLARE_ACCOUNT_ID || null,
|
|
89
|
+
// The Cloudflare ZONE the record lives in (the registrable apex) — the terminal
|
|
90
|
+
// host is a record WITHIN it, matching box-dns-cloudflare.sh. Distinct from
|
|
91
|
+
// terminalZone (the host suffix). Both resolve from the instance (task 1002757).
|
|
92
|
+
zone: process.env.CLOUDFLARE_ZONE || instanceApex(),
|
|
93
|
+
// Host suffix for term-<login>.<terminalZone>. DEFAULTS TO THE APEX, NOT dev.*:
|
|
94
|
+
// the terminal rides a PROXIED tunnel, and free Universal SSL covers <apex> +
|
|
95
|
+
// *.<apex> (ONE label) but not a TWO-label host like term-x.dev.<apex>, which
|
|
96
|
+
// fails TLS at the edge (verified live, #771). An operator holding an Advanced
|
|
97
|
+
// Certificate for *.dev.<apex> can override via BOX_TERMINAL_ZONE. (The SSH
|
|
98
|
+
// A-record path stays on dev.* — proxied=false, so it needs no edge cert.)
|
|
99
|
+
terminalZone: process.env.BOX_TERMINAL_ZONE || instanceApex(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// Named-tunnel automation is live only when the operator opted in AND the
|
|
103
|
+
// credentials are present. Missing creds while enabled is a loud config error at
|
|
104
|
+
// provision time (see cmdProvision), not a silent fall-back, so an operator who
|
|
105
|
+
// meant to use named tunnels learns immediately.
|
|
106
|
+
function numEnv(name, dflt) {
|
|
107
|
+
const v = process.env[name];
|
|
108
|
+
if (v == null || v === '') return dflt;
|
|
109
|
+
const n = Number(v);
|
|
110
|
+
return Number.isFinite(n) && n > 0 ? n : dflt;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function hasFlag(args, name) { return args.includes(name); }
|
|
114
|
+
|
|
115
|
+
// Lazy + memoized DB + lib load so `--help` and arg errors don't require Postgres
|
|
116
|
+
// (this CLI runs on a non-droplet machine for dry-run planning of pure logic too):
|
|
117
|
+
// nothing is require()d until the first makeDo/makeCf/IIFE call, and repeat calls
|
|
118
|
+
// reuse the first resolution instead of re-running the six requires.
|
|
119
|
+
let _deps;
|
|
120
|
+
function shortStamp() {
|
|
121
|
+
return new Date().toISOString().replace(/[-:T.Z]/g, '').slice(2, 14);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function dropletName(login) { return `otb-box-${login}`; }
|
|
125
|
+
// Refuses on an unresolved zone (task 1002757) — `box-x.undefined` would mint a
|
|
126
|
+
// DNS record and a box that can never be reached.
|
|
127
|
+
function hostnameFor(login) {
|
|
128
|
+
if (!CONFIG.dnsZone) throw new Error('box: no DNS zone configured — set BOX_DNS_ZONE, or give this instance a domains.publicOrigin in config/branding.json (task 1002757).');
|
|
129
|
+
return `box-${login}.${CONFIG.dnsZone}`;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Run the optional DNS hook so the per-builder hostname tracks the new IP; with no
|
|
133
|
+
// hook configured, print the reminder — the stable-hostname CUTOVER is #599's, #598
|
|
134
|
+
// only records IP + hostname and offers the hook so an operator can wire DNS without
|
|
135
|
+
// editing this file. → { ok, error?, skipped? }. task 1180: callers MUST act on
|
|
136
|
+
// ok:false (box_events row + flag the box), never continue silently — that swallow
|
|
137
|
+
// left @kingaonline's A record pinned at a dead IP while provision reported success.
|
|
138
|
+
// The hook retries transient Cloudflare blips, so ok:false is a real, exhausted
|
|
139
|
+
// failure. `hook` is injectable so the contract is unit-testable (#1034).
|
|
140
|
+
function runDnsHook(hostname, ip, hook = CONFIG.dnsHook) {
|
|
141
|
+
if (!ip) return { ok: true, skipped: true };
|
|
142
|
+
if (!hook) {
|
|
143
|
+
console.log(` ↳ DNS: point ${hostname} → ${ip} (no BOX_DNS_HOOK set; #599 owns the cutover)`);
|
|
144
|
+
return { ok: true, skipped: true };
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
execFileSync('bash', ['-c', hook], {
|
|
148
|
+
cwd: REPO_ROOT, stdio: 'pipe',
|
|
149
|
+
env: { ...process.env, BOX_HOSTNAME: hostname, BOX_IP: ip },
|
|
150
|
+
});
|
|
151
|
+
console.log(` ↳ DNS hook updated ${hostname} → ${ip}`);
|
|
152
|
+
return { ok: true };
|
|
153
|
+
} catch (e) {
|
|
154
|
+
const msg = (e.stderr && e.stderr.toString().trim()) || e.message;
|
|
155
|
+
console.log(` ⚠ DNS hook failed: ${msg} (point ${hostname} → ${ip} manually)`);
|
|
156
|
+
return { ok: false, error: msg };
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Blackhole sentinel for a torn-down box's A record (idea 309). 192.0.2.1 is in
|
|
161
|
+
// TEST-NET-1 (RFC 5737) — reserved for documentation and GUARANTEED never to be a
|
|
162
|
+
// routable host, so no recycled DigitalOcean IP can ever land here. A perfect
|
|
163
|
+
// dead end to park a record at.
|
|
164
|
+
const DNS_BLACKHOLE_IP = '192.0.2.1';
|
|
165
|
+
|
|
166
|
+
// Park the per-builder A record at the blackhole sentinel on deprovision (idea 309,
|
|
167
|
+
// refining task 1180). 1180 DELETED the record so it couldn't dangle at a recycled
|
|
168
|
+
// DO IP, but delete-then-recreate across a reap→rebuild left an NXDOMAIN window that
|
|
169
|
+
// resolvers negative-cache for the zone's SOA neg-TTL (~30 min, not builder-editable)
|
|
170
|
+
// — a client querying then kept getting ENOTFOUND long after the box was back.
|
|
171
|
+
// REPOINTING to a non-routable sentinel keeps the name always-resolving (nothing to
|
|
172
|
+
// negative-cache) while preserving 1180's guarantee that a torn-down box's name can
|
|
173
|
+
// never dial a stranger's recycled IP; the next provision/wake PUTs the live IP
|
|
174
|
+
// in-place at the 60s TTL, no gap. Best-effort like the prior delete. Reuses
|
|
175
|
+
// runDnsHook's UPSERT path; `hook` injectable for tests. → { ok, error?, skipped? }
|
|
176
|
+
function runDnsHookBlackhole(hostname, hook = CONFIG.dnsHook) {
|
|
177
|
+
if (!hook || !hostname) return { ok: true, skipped: true };
|
|
178
|
+
return runDnsHook(hostname, DNS_BLACKHOLE_IP, hook);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Read the cloud-init template and substitute the repo URL from BOX_REPO_URL
|
|
182
|
+
// (kept out of the repo file so no GitHub identity is hardcoded — V3.R12 rule).
|
|
183
|
+
// An unset URL leaves the placeholder; the cloud-init then logs guidance rather
|
|
184
|
+
// than cloning, and #600 wires the scoped credential.
|
|
185
|
+
// Read + render the cloud-init. `authorizedKeysBlock` is the otb-managed block
|
|
186
|
+
// for the builder being provisioned (#685) — base64'd here and baked in so the
|
|
187
|
+
// FIRST SSH login works before the box has a GDS token. An empty/absent block
|
|
188
|
+
// renders an empty managed block (a keyless box; the operator is warned upstream).
|
|
189
|
+
function readCloudInit({ authorizedKeysBlock = '', boxEnv = '', gdsSession = '', infraDir = resolveBoxInfraDir() } = {}) {
|
|
190
|
+
// Fail loud, never provision a BARE box (ADR 0150). No infra/ located → there is
|
|
191
|
+
// no cloud-init to render, so THROW here rather than the old silent `return null`
|
|
192
|
+
// that let cmdProvision ship empty user-data (a box that boots as bare Ubuntu).
|
|
193
|
+
if (!infraDir) {
|
|
194
|
+
throw new Error(
|
|
195
|
+
`box cloud-init not found: no infra/${BOX_INFRA_TEMPLATE} in any candidate location `
|
|
196
|
+
+ `(set <PREFIX>_BOX_INFRA_DIR or <PREFIX>_INSTANCE_ROOT, or ship infra/). A box must `
|
|
197
|
+
+ `never be provisioned without user-data — it would boot bare (ADR 0150).`,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
const p = path.join(infraDir, BOX_INFRA_TEMPLATE);
|
|
201
|
+
// gzip+base64 a repo file into the cloud-init write_files (`encoding: gz+b64`).
|
|
202
|
+
// EVERY inlined infra file goes through this: cloud-init gunzips it back to the
|
|
203
|
+
// exact bytes on the box, so it is byte-identical to plain b64 but ~2-3x smaller,
|
|
204
|
+
// which keeps the rendered user-data well under DO's 64 KiB cap (#903; the source
|
|
205
|
+
// -fetch growth in task 1314 had pushed the all-b64-but-source-fetch render to
|
|
206
|
+
// 67 KB — gz+b64-ing the rest brings it back to ~51 KB, task 1321). Only the
|
|
207
|
+
// dynamic runtime values (authorized_keys, box.env, gds-session) stay plain b64
|
|
208
|
+
// below — they are minted per-provision, not read from infra/. A missing inlined
|
|
209
|
+
// file is COLLECTED and fails loud after the render (ADR 0150) — a half-wired box
|
|
210
|
+
// (that component silently never boots) is a bug, not something to warn past.
|
|
211
|
+
const missingInfra = [];
|
|
212
|
+
const fileGzB64 = (name) => {
|
|
213
|
+
try {
|
|
214
|
+
return zlib.gzipSync(fs.readFileSync(path.join(infraDir, name))).toString('base64');
|
|
215
|
+
} catch {
|
|
216
|
+
missingInfra.push(name);
|
|
217
|
+
return '';
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
221
|
+
// Inject the rank-gated source-fetch (#600) + authorized-keys-fetch (#685)
|
|
222
|
+
// scripts as gz+b64 (a single YAML-safe token — no indentation/escaping
|
|
223
|
+
// pitfalls; cloud-init gunzips encoding: gz+b64 back to the exact file), plus
|
|
224
|
+
// the baked authorized_keys block. ONE canonical copy of each lives in infra/.
|
|
225
|
+
const rendered = raw
|
|
226
|
+
.replace(/__REPO_URL__/g, process.env.BOX_REPO_URL || '')
|
|
227
|
+
.replace(/__BOX_SOURCE_FETCH_B64__/g, fileGzB64('box-source-fetch.sh'))
|
|
228
|
+
.replace(/__BOX_AUTHKEYS_FETCH_B64__/g, fileGzB64('box-authorized-keys-fetch.sh'))
|
|
229
|
+
.replace(/__BUILDER_AUTHORIZED_KEYS_B64__/g, Buffer.from(authorizedKeysBlock || '', 'utf8').toString('base64'))
|
|
230
|
+
// /etc/otb/box.env — empty by default (quick tunnel); the named-tunnel
|
|
231
|
+
// connector token + stable hostname when BOX_NAMED_TUNNEL is on (#771).
|
|
232
|
+
.replace(/__BOX_ENV_B64__/g, Buffer.from(boxEnv || '', 'utf8').toString('base64'))
|
|
233
|
+
// /root/.config/otb/gds-session.json — the builder's GDS session, minted at
|
|
234
|
+
// provision so box-source-fetch (#600) can authenticate on FIRST boot and
|
|
235
|
+
// clone the rank-scoped repo. Without it the box never clones (the #818
|
|
236
|
+
// chicken-and-egg: the session normally comes from /builder-setup, which
|
|
237
|
+
// lives in the un-cloned repo).
|
|
238
|
+
.replace(/__GDS_SESSION_B64__/g, Buffer.from(gdsSession || '', 'utf8').toString('base64'))
|
|
239
|
+
// #774: the browser-terminal units, inlined so they start at FIRST BOOT
|
|
240
|
+
// (independent of the repo clone). ONE canonical copy lives in infra/.
|
|
241
|
+
.replace(/__BOX_TERMINAL_SERVICE_B64__/g, fileGzB64('box-terminal.service'))
|
|
242
|
+
// #917: credential-drift watcher pair — restarts ttyd when the credential
|
|
243
|
+
// file lands/changes after the service started (the wake/first-boot race).
|
|
244
|
+
.replace(/__BOX_TERMINAL_CREDSYNC_SERVICE_B64__/g, fileGzB64('box-terminal-credsync.service'))
|
|
245
|
+
.replace(/__BOX_TERMINAL_CREDENTIAL_PATH_B64__/g, fileGzB64('box-terminal-credential.path'))
|
|
246
|
+
.replace(/__CLOUDFLARED_SERVICE_B64__/g, fileGzB64('cloudflared.service'))
|
|
247
|
+
.replace(/__CLOUDFLARED_RUN_B64__/g, fileGzB64('cloudflared-run.sh'))
|
|
248
|
+
// #893: the `close-box` teardown command, present at first boot so a bare
|
|
249
|
+
// browser-only box has it before any repo clone (box-source-fetch refreshes
|
|
250
|
+
// it on existing boxes). ONE canonical copy lives in infra/close-box.sh.
|
|
251
|
+
.replace(/__CLOSE_BOX_B64__/g, fileGzB64('close-box.sh'))
|
|
252
|
+
// task 986: the browser-terminal auto-launcher — runs `claude` on first
|
|
253
|
+
// connect so the builder skips the "type claude" step. box-source-fetch
|
|
254
|
+
// refreshes it on existing boxes. ONE canonical copy in infra/otb-box-launch.sh.
|
|
255
|
+
.replace(/__OTB_BOX_LAUNCH_B64__/g, fileGzB64('otb-box-launch.sh'));
|
|
256
|
+
if (missingInfra.length) {
|
|
257
|
+
throw new Error(
|
|
258
|
+
`box cloud-init is incomplete under ${infraDir}: missing ${missingInfra.join(', ')} `
|
|
259
|
+
+ `— a box would boot half-wired (ADR 0150). Fix the instance infra/ before provisioning.`,
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return rendered;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
module.exports = {
|
|
267
|
+
CF,
|
|
268
|
+
CONFIG,
|
|
269
|
+
boxInfraCandidates,
|
|
270
|
+
dropletName,
|
|
271
|
+
hasFlag,
|
|
272
|
+
hostnameFor,
|
|
273
|
+
instanceApex,
|
|
274
|
+
numEnv,
|
|
275
|
+
readCloudInit,
|
|
276
|
+
resolveBoxInfraDir,
|
|
277
|
+
runDnsHook,
|
|
278
|
+
runDnsHookBlackhole,
|
|
279
|
+
shortStamp,
|
|
280
|
+
};
|