@codeyam-editor/codeyam-editor 0.1.3 → 0.1.5
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/npm/cloud-branch-switch.js +1 -1
- package/npm/cloud.js +1693 -96
- package/npm/container.js +14 -0
- package/npm/disk-pressure.js +167 -0
- package/npm/editor-dev.js +107 -15
- package/npm/editor.js +97 -37
- package/npm/fleet-auth-health.js +75 -0
- package/npm/fleet-auth.js +6 -1
- package/npm/fleet-build-data.js +130 -0
- package/npm/fleet-cleanup.js +145 -7
- package/npm/fleet-destroy-safety.js +55 -0
- package/npm/fleet-effect-status.js +33 -32
- package/npm/fleet-host.js +74 -0
- package/npm/fleet-job-badge.js +46 -2
- package/npm/fleet-launchd.js +171 -0
- package/npm/fleet-live-status.js +123 -0
- package/npm/fleet-plans.js +231 -0
- package/npm/fleet-prewarm.js +165 -0
- package/npm/fleet-public-url.js +160 -0
- package/npm/fleet-queue.js +38 -1
- package/npm/fleet-reconcile.js +100 -1
- package/npm/fleet-reset.js +298 -13
- package/npm/fleet-roster.js +6 -8
- package/npm/fleet-self-update.js +142 -0
- package/npm/fleet-setup-steps.js +105 -0
- package/npm/fleet-sleep-status.js +33 -0
- package/npm/fleet-strand.js +150 -0
- package/npm/fleet-stuck-status.js +111 -0
- package/npm/fleet-vm-access.js +234 -0
- package/npm/fleet-vm-readiness.js +104 -0
- package/npm/fleet-vm-slugs.js +116 -0
- package/npm/google-ingress.js +211 -0
- package/npm/ingress-invoke.js +292 -0
- package/npm/launch-config.js +310 -4
- package/npm/scenario-check.js +684 -33
- package/npm/scenario-handlers.js +94 -12
- package/npm/scenario-metrics.js +116 -1
- package/npm/scenario-mocks.js +20 -0
- package/npm/scenario-playwright.js +366 -29
- package/npm/script-drift.js +81 -0
- package/npm/utils.js +453 -43
- package/npm/vm-urls.js +69 -8
- package/package.json +5 -5
- package/ui/dist/assets/ContainerInspector-D7VZkkaj.js +2 -0
- package/ui/dist/assets/EditorShell-DHhIaotd.css +1 -0
- package/ui/dist/assets/EditorShell-r-796N_W.js +191 -0
- package/ui/dist/assets/{Markdown-BHBqz-Mj.js → Markdown-BoLHHFXH.js} +1 -1
- package/ui/dist/assets/ProjectLauncherScreen-Ck-XWkQN.js +1 -0
- package/ui/dist/assets/ScenarioDataPanel-B9iDSUpA.js +5 -0
- package/ui/dist/assets/ScenarioDataPanelFullPage-CBLTGC4-.js +1 -0
- package/ui/dist/assets/ScenarioRenderer-BhLfT-nX.js +21 -0
- package/ui/dist/assets/StepsSimulator-BFldWyoX.js +1 -0
- package/ui/dist/assets/index-CIXZkwfB.css +1 -0
- package/ui/dist/assets/{index-KDFAgt2e.js → index-QsOiOG9x.js} +9 -9
- package/ui/dist/assets/useEvents-DHkLJZR4.js +1 -0
- package/ui/dist/codeyam-mark.svg +4 -0
- package/ui/dist/codeyam-wordmark.svg +5 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +0 -2
- package/ui/dist/assets/EditorShell-CGsBSA6u.js +0 -186
- package/ui/dist/assets/ScenarioDataPanel-nuLXJzLf.js +0 -5
- package/ui/dist/assets/ScenarioDataPanelFullPage-Bnghtj8a.js +0 -1
- package/ui/dist/assets/ScenarioRenderer-B_Za9jL8.js +0 -1
- package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +0 -1
- package/ui/dist/assets/index-Bih6HJ_u.css +0 -1
- package/ui/dist/assets/useEvents-DggWrtHe.js +0 -1
package/npm/fleet-reset.js
CHANGED
|
@@ -123,11 +123,15 @@ function codeyamBrokerEnsureStep(E) {
|
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
// Build the in-container bash for "Reset client" — hard-clean the CLIENT
|
|
126
|
-
// project (/workspace) on ANY VM shape. The decisions
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
//
|
|
126
|
+
// project (/workspace) on ANY VM shape, advancing to origin tip. The decisions
|
|
127
|
+
// encoded here:
|
|
128
|
+
// - Advance to ORIGIN tip: fetch + `reset --hard origin/<branch>` (the same
|
|
129
|
+
// shape resetEditorStateScript uses), so the working tree ends clean AT the
|
|
130
|
+
// origin tip. This discards uncommitted churn AND unpushed commits — more
|
|
131
|
+
// destructive than the old reset-to-HEAD; the card's confirm dialog warns
|
|
132
|
+
// about the unpushed-commit loss. A greenfield local-only repo (no origin)
|
|
133
|
+
// falls back to a bare `git reset --hard` to HEAD. The non-destructive
|
|
134
|
+
// ff-to-tip-that-keeps-the-session path is the separate "Update client".
|
|
131
135
|
// - `git reset --hard` leaves untracked files, so node_modules + a per-VM
|
|
132
136
|
// .env survive (no forced re-install / lost secrets).
|
|
133
137
|
// - Stop the broker FIRST so abandoned agent WIP can't be re-written to disk
|
|
@@ -145,18 +149,34 @@ function resetClientScript({ E, gitReseedBody }) {
|
|
|
145
149
|
'echo ">> [1/5] stopping agent session"',
|
|
146
150
|
`(${E} editor pty-broker stop || true)`,
|
|
147
151
|
'(git config --local --unset-all credential.helper || true)',
|
|
148
|
-
'
|
|
149
|
-
|
|
152
|
+
'if git remote | grep -qx origin; then ' +
|
|
153
|
+
'BR=$(git branch --show-current); ' +
|
|
154
|
+
'echo ">> [2/5] advancing client to origin tip (git fetch + reset --hard origin/$BR; discards uncommitted AND unpushed; keeps untracked files)"; ' +
|
|
155
|
+
'git reset --hard && git fetch origin "$BR" && git reset --hard "origin/$BR"; ' +
|
|
156
|
+
'else echo ">> [2/5] no origin remote (greenfield) — git reset --hard to HEAD"; git reset --hard; fi',
|
|
150
157
|
'echo ">> [3/5] deep-cleaning .codeyam (clears ignored runtime state; keeps editor.local.json + server-state.json)"',
|
|
151
158
|
...codeyamDeepCleanSteps(E),
|
|
152
159
|
`${E} editor session-reset`,
|
|
153
|
-
|
|
160
|
+
// Self-host: client === editor, so a client reset advances /workspace (the
|
|
161
|
+
// editor source) and MUST rebuild before restart or the binary↔source
|
|
162
|
+
// mismatch loops. No-op off self-host (the client /workspace is the client
|
|
163
|
+
// app, not the editor source). See selfHostRebuildStep.
|
|
164
|
+
selfHostRebuildStep(E),
|
|
165
|
+
'echo ">> [4/5] restarting server to respawn the broker"',
|
|
154
166
|
`(${E} editor restart-server --force-in-container || true)`,
|
|
155
167
|
// Belt-and-suspenders: ensure a broker is live even on an older binary
|
|
156
168
|
// whose restart-server doesn't yet respawn it. Idempotent / status-gated.
|
|
157
169
|
codeyamBrokerEnsureStep(E),
|
|
158
170
|
'echo ">> [5/5] re-seeding git credential cache"',
|
|
159
171
|
`( ( ${gitReseedBody} ) || echo "WARN git cache re-seed failed" )`,
|
|
172
|
+
// Terminal contract guard: the reset PROMISES idle, so verify it and end any
|
|
173
|
+
// agent that slipped back after the restart/broker-ensure. No-op when the
|
|
174
|
+
// reset already reached idle. See verifyIdleOrEndSessionStep.
|
|
175
|
+
verifyIdleOrEndSessionStep({ E }),
|
|
176
|
+
// Verify the editor came back SERVING THE PROJECT on :14199 (not stranded in
|
|
177
|
+
// launcher mode); on a self-host strand emit CY_NEEDS_RECONFIGURE so the
|
|
178
|
+
// dashboard auto-falls-back to reconfigure.
|
|
179
|
+
verifyServingProjectOrFlagReconfigureStep(),
|
|
160
180
|
].join(' && ');
|
|
161
181
|
}
|
|
162
182
|
|
|
@@ -174,7 +194,12 @@ function resetClientScript({ E, gitReseedBody }) {
|
|
|
174
194
|
// `git reset --hard` / `rm -rf .codeyam`. The composite action relies on this
|
|
175
195
|
// same gate as its safety contract — a failed rebuild that leaves no binary
|
|
176
196
|
// will short-circuit the trailing recipe before any wipe happens.
|
|
177
|
-
|
|
197
|
+
// `rebuildOnSelfHost` (default true): rebuild the binary before the final
|
|
198
|
+
// restart when this is a self-host VM (see selfHostRebuildStep). The composite
|
|
199
|
+
// update-reset-everything passes `false` because its leading
|
|
200
|
+
// rebuildBinaryHostScript already rebuilt — a second cargo build would be a
|
|
201
|
+
// ~5-8 min no-op.
|
|
202
|
+
function resetEditorStateScript({ E, rebuildOnSelfHost = true } = {}) {
|
|
178
203
|
return [
|
|
179
204
|
'cd /workspace',
|
|
180
205
|
`command -v ${E} >/dev/null`,
|
|
@@ -203,20 +228,280 @@ function resetEditorStateScript({ E }) {
|
|
|
203
228
|
'echo ">> [3/4] deep-cleaning .codeyam (clears ignored state; keeps editor.local.json + server-state.json)"',
|
|
204
229
|
...codeyamDeepCleanSteps(E),
|
|
205
230
|
`${E} editor session-reset`,
|
|
206
|
-
//
|
|
207
|
-
//
|
|
208
|
-
//
|
|
209
|
-
|
|
231
|
+
// Self-host VMs build the binary from /workspace, so a reset that advanced
|
|
232
|
+
// source MUST rebuild before restarting or the binary↔source mismatch sends
|
|
233
|
+
// the editor into a stale-binary reexec loop. No-op off self-host (the
|
|
234
|
+
// binary is shipped). Skipped when the composite already rebuilt.
|
|
235
|
+
...(rebuildOnSelfHost ? [selfHostRebuildStep(E)] : []),
|
|
236
|
+
// Restart the server in place to respawn the broker we stopped (now on a
|
|
237
|
+
// binary that matches the advanced source on self-host): restart-server
|
|
238
|
+
// SIGUSR1s pid 1 and the docker-exec returns cleanly, so `|| true` keeps a
|
|
239
|
+
// broker-down VM green.
|
|
240
|
+
'echo ">> [4/4] restarting server to respawn the broker"',
|
|
210
241
|
`(${E} editor restart-server --force-in-container || true)`,
|
|
211
242
|
// Belt-and-suspenders: ensure a broker is live even on an older binary
|
|
212
243
|
// whose restart-server doesn't yet respawn it. Idempotent / status-gated.
|
|
213
244
|
codeyamBrokerEnsureStep(E),
|
|
245
|
+
// Terminal contract guard: the reset PROMISES idle, so verify it and end any
|
|
246
|
+
// agent that slipped back after the restart/broker-ensure. No-op when the
|
|
247
|
+
// reset already reached idle. See verifyIdleOrEndSessionStep.
|
|
248
|
+
verifyIdleOrEndSessionStep({ E }),
|
|
249
|
+
// Verify the editor came back SERVING THE PROJECT on :14199 (not stranded in
|
|
250
|
+
// launcher mode on an ephemeral port); on a self-host strand emit
|
|
251
|
+
// CY_NEEDS_RECONFIGURE so the dashboard auto-falls-back to reconfigure.
|
|
252
|
+
verifyServingProjectOrFlagReconfigureStep(),
|
|
214
253
|
].join(' && ');
|
|
215
254
|
}
|
|
216
255
|
|
|
256
|
+
// Build the in-container bash for "End session" — the MINIMAL "stop the agent"
|
|
257
|
+
// primitive. Unlike the reset family this does NOT advance source, deep-clean
|
|
258
|
+
// .codeyam, or rebuild: it kills the live agent process and clears the
|
|
259
|
+
// session-specific state files, taking the VM from a live `waiting`/`building`
|
|
260
|
+
// session back to a true `idle` in seconds.
|
|
261
|
+
//
|
|
262
|
+
// The recipe takes the VM to idle (`hasSession:false`) while leaving a BARE
|
|
263
|
+
// broker daemon ready for the next run:
|
|
264
|
+
// - `pty-broker stop` SIGTERMs the broker daemon, which kills the live agent.
|
|
265
|
+
// Fail-soft (`|| true`): a VM with no broker running legitimately exits
|
|
266
|
+
// non-zero (crates/codeyam-editor/src/commands/editor/pty_broker.rs), and a
|
|
267
|
+
// no-op end must not abort the chain.
|
|
268
|
+
// - `session-reset` deletes the session-specific state files (no resume
|
|
269
|
+
// target) so the still-running editor server's view drops to no-session.
|
|
270
|
+
// - codeyamBrokerEnsureStep respawns a BARE daemon (no agent attached) — only
|
|
271
|
+
// if none is reachable. Order matters: stop → reset → ensure, so the
|
|
272
|
+
// ensure can never re-bind stale session state.
|
|
273
|
+
//
|
|
274
|
+
// Why ensure a bare daemon instead of leaving the broker down: End session
|
|
275
|
+
// means "no agent session," NOT "no broker at all." The Build-tab terminal
|
|
276
|
+
// WebSocket handler does NOT respawn the broker on connect — if the daemon is
|
|
277
|
+
// down, `attach_or_spawn` fails with "PTY broker unreachable"
|
|
278
|
+
// (crates/control-api/src/terminal.rs). So leaving the broker down means the
|
|
279
|
+
// next Run / Build-tab open errors until the watchdog catches up. A bare daemon
|
|
280
|
+
// (broker alive, zero sessions) still reports `hasSession:false` → status
|
|
281
|
+
// `idle`, and lets the next `attach_or_spawn` attach instantly and spawn a
|
|
282
|
+
// fresh agent. The builder unit test asserts the chain ends with the daemon
|
|
283
|
+
// ensure and contains NO `restart-server`.
|
|
284
|
+
//
|
|
285
|
+
// CRITICAL: there is NO `restart-server` here. `restart-server` deliberately
|
|
286
|
+
// reattaches the surviving/respawned broker session and brings the agent back
|
|
287
|
+
// (crates/codeyam-editor/src/commands/editor/restart_server.rs), which would
|
|
288
|
+
// undo the end. codeyamBrokerEnsureStep, by contrast, never attaches an agent —
|
|
289
|
+
// it only spawns a bare daemon.
|
|
290
|
+
function endSessionScript({ E }) {
|
|
291
|
+
return [
|
|
292
|
+
'cd /workspace',
|
|
293
|
+
// Binary-existence gate FIRST, mirroring the reset builders.
|
|
294
|
+
`command -v ${E} >/dev/null`,
|
|
295
|
+
`(${E} editor pty-broker stop || true)`,
|
|
296
|
+
`${E} editor session-reset`,
|
|
297
|
+
// Respawn a BARE daemon (no agent) so the next Run / Build-tab open attaches
|
|
298
|
+
// instantly. Status-gated + idempotent, and NOT restart-server (no resume).
|
|
299
|
+
codeyamBrokerEnsureStep(E),
|
|
300
|
+
].join(' && ');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Build the TERMINAL "verify idle, end the agent if one slipped back" step,
|
|
304
|
+
// appended as the LAST step of every reset recipe. This is the contract fix for
|
|
305
|
+
// `vm: Resets Must Land at Idle`: the reset actions ("Reset client", "Reset
|
|
306
|
+
// editor", "Reset all + rebuild") all promise to drop the agent and return the
|
|
307
|
+
// VM to a fresh-provision state, but in practice a live Claude agent came back
|
|
308
|
+
// resumed at the Plan step after the reset's trailing restart-server +
|
|
309
|
+
// codeyamBrokerEnsureStep — so the dashboard card read `waiting` instead of
|
|
310
|
+
// `idle` (observed live on VM-4).
|
|
311
|
+
//
|
|
312
|
+
// Diagnosis note (the fix is MECHANISM-AGNOSTIC on purpose): the reset already
|
|
313
|
+
// STOPS the broker before the wipe (codeyamDeepCleanSteps) and runs
|
|
314
|
+
// session-reset, so the agent is killed mid-reset; it reappears only because of
|
|
315
|
+
// the trailing restart-server + broker-ensure — the same two steps
|
|
316
|
+
// endSessionScript deliberately OMITS to land at idle (see its idle-end
|
|
317
|
+
// invariant test). The EXACT re-attach path — restart-server's in-place SIGUSR1
|
|
318
|
+
// reexec resuming a session, the broker-ensure respawn the server then attaches
|
|
319
|
+
// an agent to, or the editor server auto-resuming a workflow on startup — was
|
|
320
|
+
// NOT reproduced on a scratch cloud VM in the authoring environment, so rather
|
|
321
|
+
// than guess at one we make the reset TERMINALLY VERIFY idle regardless of which
|
|
322
|
+
// path fires: read /api/session-info from the now-running server and, if it
|
|
323
|
+
// still reports an active session (`hasSession: true`) OR a live build-slot
|
|
324
|
+
// broker agent (`agentBrokerLive: true`), end the session with the proven
|
|
325
|
+
// endSessionScript primitive (`pty-broker stop` + `session-reset`, with NO
|
|
326
|
+
// further restart). The EITHER condition exactly mirrors the editor UI's
|
|
327
|
+
// Build-dot clear condition in useBuildSessionLive (the dot clears only when
|
|
328
|
+
// `!hasSession && !agentBrokerLive`): a reset that left a live build-slot agent
|
|
329
|
+
// in the broker — the `fresh_branch_surfaces_agent_broker_live_true` server
|
|
330
|
+
// shape, `hasSession:false` + `agentBrokerLive:true` — previously slipped past a
|
|
331
|
+
// `hasSession`-only check, reading "idle" on the dashboard while the editor UI
|
|
332
|
+
// Build dot stayed lit. Gating on either signal closes that gap (and
|
|
333
|
+
// `pty-broker stop` is what drops `agentBrokerLive` to false). Robust even if a
|
|
334
|
+
// future change reintroduces a resume path, and a no-op on the happy path where
|
|
335
|
+
// the reset already reached idle (the bare broker daemon from
|
|
336
|
+
// codeyamBrokerEnsureStep stays up — which reads idle, both signals false).
|
|
337
|
+
//
|
|
338
|
+
// Stack assumption: in-container the editor server's control port is the
|
|
339
|
+
// preserved server-state.json `controlPort` (rewritten by restart-server),
|
|
340
|
+
// falling back to the standard 14199 — the same port the rebuild verify curls
|
|
341
|
+
// (scripts/operator/fleet-dashboard/server.js). server-state.json survives the
|
|
342
|
+
// deep-clean wipe (codeyamDeepCleanSteps cp's it aside and restores it).
|
|
343
|
+
//
|
|
344
|
+
// SETTLE-WINDOW POLL (vm: Resets Stay at Idle After restart-server Reattach):
|
|
345
|
+
// a single point-in-time probe loses a race. The reset family ends with
|
|
346
|
+
// `restart-server --force-in-container`, which is DESIGNED to reattach the
|
|
347
|
+
// surviving broker session and bring the agent back asynchronously
|
|
348
|
+
// (crates/codeyam-editor/src/commands/editor/restart_server.rs:29-33) — a beat
|
|
349
|
+
// AFTER this terminal guard's first curl already saw `hasSession:false` and
|
|
350
|
+
// declared idle. Observed live on VM-3/VM-6 (2026-06-22): the reset-client job
|
|
351
|
+
// logged `>> reset reached idle` then ~90s later `/data` reported
|
|
352
|
+
// `status=building, hasSession=true`. The one-shot check won the probe but lost
|
|
353
|
+
// the race. endSessionScript lands at idle reliably ONLY because it omits
|
|
354
|
+
// restart-server; the reset NEEDS restart-server to respawn the broker / advance
|
|
355
|
+
// the binary, so it must DEFEAT the reattach rather than avoid it.
|
|
356
|
+
//
|
|
357
|
+
// The fix keeps every property of the single-probe guard above (mechanism-
|
|
358
|
+
// agnostic `hasSession || agentBrokerLive` grep, the reused endSessionScript
|
|
359
|
+
// primitive with NO restart-server, the `( …; true )` non-fatal wrapper, the
|
|
360
|
+
// server-state.json controlPort resolution) and turns "checked once, too early"
|
|
361
|
+
// into "checked until stable": a bounded loop probes /api/session-info
|
|
362
|
+
// PROBE_COUNT times (~1s apart), and on ANY probe where a session or live broker
|
|
363
|
+
// agent reappears it ends it with endSessionScript (`pty-broker stop` +
|
|
364
|
+
// `session-reset`, NEVER another restart) and keeps probing — so a late reattach,
|
|
365
|
+
// or even a second one within the window, is each caught. The window outlasts
|
|
366
|
+
// restart-server's async reattach (a few seconds) without stretching the reset.
|
|
367
|
+
// On the happy path (already idle and stays idle) every probe reads idle, zero
|
|
368
|
+
// endSession calls fire, and it ends with the same `reset reached idle` line as
|
|
369
|
+
// before — a no-op, same as the old single probe.
|
|
370
|
+
//
|
|
371
|
+
// Wrapped in `( …; true )` so a curl/parse hiccup mid-window can never abort the
|
|
372
|
+
// reset's &&-chain, exactly like codeyamBrokerEnsureStep.
|
|
373
|
+
function verifyIdleOrEndSessionStep({ E, controlPort = 14199 }) {
|
|
374
|
+
const end = endSessionScript({ E });
|
|
375
|
+
// Settle-window budget. Kept as named locals so the builder test can assert the
|
|
376
|
+
// loop is bounded and a tuning change is a one-line edit. PROBE_COUNT probes
|
|
377
|
+
// PROBE_SLEEP_SECS apart ⇒ ~7s window — long enough to outlast restart-server's
|
|
378
|
+
// async reattach, short enough not to stretch the reset job.
|
|
379
|
+
const PROBE_COUNT = 8;
|
|
380
|
+
const PROBE_SLEEP_SECS = 1;
|
|
381
|
+
const probe = `si=$(curl -sf "http://127.0.0.1:$PORT/api/session-info" 2>/dev/null || true)`;
|
|
382
|
+
const sessionLiveGate = `printf '%s' "$si" | grep -qE '"(hasSession|agentBrokerLive)"[[:space:]]*:[[:space:]]*true'`;
|
|
383
|
+
const reattachMsg =
|
|
384
|
+
'">> reset left a session or live broker agent attached — ending the session to land at idle"';
|
|
385
|
+
return (
|
|
386
|
+
`( PORT=$(grep -oE '"controlPort"[^0-9]*[0-9]+' .codeyam/server-state.json 2>/dev/null | grep -oE '[0-9]+$' | head -1); ` +
|
|
387
|
+
`PORT=\${PORT:-${controlPort}}; ` +
|
|
388
|
+
// Settle window: probe up to PROBE_COUNT times; end any session/agent that
|
|
389
|
+
// reattaches on each probe and keep polling, so a late (or double) reattach
|
|
390
|
+
// after restart-server is caught instead of slipping past a single probe.
|
|
391
|
+
`i=0; while [ "$i" -lt ${PROBE_COUNT} ]; do ` +
|
|
392
|
+
`${probe}; ` +
|
|
393
|
+
`if ${sessionLiveGate}; then echo ${reattachMsg}; ${end}; fi; ` +
|
|
394
|
+
`i=$((i+1)); [ "$i" -lt ${PROBE_COUNT} ] && sleep ${PROBE_SLEEP_SECS}; ` +
|
|
395
|
+
`done; ` +
|
|
396
|
+
// Confirming trailing probe — the window has now outlasted the async
|
|
397
|
+
// reattach, so this read is the authoritative idle verdict. End once more if
|
|
398
|
+
// a session somehow survived the final loop iteration; otherwise declare idle.
|
|
399
|
+
`${probe}; ` +
|
|
400
|
+
`if ${sessionLiveGate}; then echo ${reattachMsg}; ${end}; ` +
|
|
401
|
+
`else echo ">> reset reached idle (no session, no live broker agent)"; fi; true )`
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// REBUILD-BEFORE-RESTART on a self-host VM. The reset family advances source to
|
|
406
|
+
// origin tip; on a self-host VM (the client project IS codeyam-editor, so
|
|
407
|
+
// /workspace/Cargo.toml exists) the binary is BUILT from /workspace, so a reset
|
|
408
|
+
// that advances source without rebuilding deterministically produces a
|
|
409
|
+
// binary↔source mismatch — and the editor, deciding it's stale, enters a
|
|
410
|
+
// reexec/rebuild loop. So before the final restart we rebuild the binary in
|
|
411
|
+
// place (`rebuild-self`, the same mechanism rebuildBinaryHostScript uses). The
|
|
412
|
+
// self-host axis is detected at RUNTIME with the same `/workspace/Cargo.toml`
|
|
413
|
+
// probe rebuildBinaryHostScript uses, so the NON-self-host path — where the
|
|
414
|
+
// editor binary is shipped, not built from the client repo — keeps its fast
|
|
415
|
+
// "(no rebuild)" restart untouched.
|
|
416
|
+
//
|
|
417
|
+
// Stack assumption: only a self-host VM has /workspace/Cargo.toml AND builds its
|
|
418
|
+
// own binary; a normal client project's /workspace is the client's app, never
|
|
419
|
+
// the editor source, so the probe is false and this is a silent no-op there.
|
|
420
|
+
//
|
|
421
|
+
// Fail-soft: a `rebuild-self` failure `|| echo WARN`s rather than aborting the
|
|
422
|
+
// reset's &&-chain — restarting on the prior (un-advanced-binary) build is
|
|
423
|
+
// recoverable (the restart-server full-restart fallback makes the stale-binary
|
|
424
|
+
// loop non-fatal), and the trailing serving-probe still flags reconfigure if the
|
|
425
|
+
// editor doesn't come back. The whole step is wrapped so it can never break the
|
|
426
|
+
// chain.
|
|
427
|
+
function selfHostRebuildStep(E) {
|
|
428
|
+
return (
|
|
429
|
+
'( if test -f /workspace/Cargo.toml; then ' +
|
|
430
|
+
'echo ">> self-host VM — rebuilding binary before restart so it matches the advanced source (cargo ~5-8 min)"; ' +
|
|
431
|
+
`${E} editor rebuild-self --force-in-container || echo "WARN self-host rebuild-self failed — restart may hit the stale-binary reexec path"; ` +
|
|
432
|
+
'fi; true )'
|
|
433
|
+
);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// POST-RESET SERVING PROBE — verify the editor came back SERVING THE PROJECT on
|
|
437
|
+
// :14199, and on a self-host VM flag an auto-reconfigure if it didn't. The
|
|
438
|
+
// Project Launcher binds an EPHEMERAL port (never :14199 — see
|
|
439
|
+
// crates/codeyam-editor/src/commands/launcher/server.rs `TcpListener::bind(…, 0)`),
|
|
440
|
+
// so a launcher-mode strand — or a server that never came back — shows up
|
|
441
|
+
// identically: :14199 stops answering /api/health. (The serve-on-:14199 fix in
|
|
442
|
+
// start.rs::decide_launch prevents the strand at the source; this is the
|
|
443
|
+
// belt-and-suspenders that turns a residual strand into an automatic recovery
|
|
444
|
+
// instead of a hand-run reconfigure.)
|
|
445
|
+
//
|
|
446
|
+
// On a self-host VM that isn't serving :14199, emit the `CY_NEEDS_RECONFIGURE`
|
|
447
|
+
// marker the dashboard's job-completion handler reacts to by auto-running
|
|
448
|
+
// reconfigure (the proven container-recreate recovery). Off self-host we only
|
|
449
|
+
// WARN: reconfigure's container recreate is the wrong hammer for a shipped-binary
|
|
450
|
+
// client VM, so the operator decides.
|
|
451
|
+
//
|
|
452
|
+
// Reuses the server-state.json controlPort resolution (falling back to 14199)
|
|
453
|
+
// exactly like verifyIdleOrEndSessionStep, and is wrapped in `( …; true )` so a
|
|
454
|
+
// curl hiccup can never abort the reset's &&-chain.
|
|
455
|
+
function verifyServingProjectOrFlagReconfigureStep({ controlPort = 14199 } = {}) {
|
|
456
|
+
// ~30s window — long enough for restart-server's async respawn to bind the
|
|
457
|
+
// port, short enough not to stretch the reset job.
|
|
458
|
+
const PROBE_COUNT = 15;
|
|
459
|
+
const PROBE_SLEEP_SECS = 2;
|
|
460
|
+
// VERIFY-AFTER-REBUILD (VM-4 2026-06-25): a rebuild that produces a binary and
|
|
461
|
+
// answers /api/health on :14199 is NOT proof the fix took — the launcher, when
|
|
462
|
+
// it lands from the boot decision, binds the CONTROL port (so `npm run editor`
|
|
463
|
+
// finds it) and ALSO serves /api/health, so /api/health alone can't tell a
|
|
464
|
+
// serving editor from a launcher strand sitting ON :14199. /api/projects is
|
|
465
|
+
// launcher-ONLY (the editor serves /api/session-info, never /api/projects) and
|
|
466
|
+
// returns JSON, so a launcher signature ON the control port means the strand
|
|
467
|
+
// SURVIVED the rebuild — treat it as a FAILURE (emit the reconfigure marker),
|
|
468
|
+
// never as success. This makes "serves the PROJECT on :14199" the hard
|
|
469
|
+
// post-condition, not merely "something answers /api/health".
|
|
470
|
+
const launcherOnPort =
|
|
471
|
+
'body=$(curl -sf --max-time 2 "http://127.0.0.1:$PORT/api/projects" 2>/dev/null); ' +
|
|
472
|
+
'case "$body" in "["*|"{"*) true;; *) false;; esac';
|
|
473
|
+
return (
|
|
474
|
+
`( PORT=$(grep -oE '"controlPort"[^0-9]*[0-9]+' .codeyam/server-state.json 2>/dev/null | grep -oE '[0-9]+$' | head -1); ` +
|
|
475
|
+
`PORT=\${PORT:-${controlPort}}; ` +
|
|
476
|
+
`serving=0; launchered=0; i=0; while [ "$i" -lt ${PROBE_COUNT} ]; do ` +
|
|
477
|
+
'if curl -sf "http://127.0.0.1:$PORT/api/health" >/dev/null 2>&1; then ' +
|
|
478
|
+
`if ${launcherOnPort}; then launchered=1; else serving=1; break; fi; fi; ` +
|
|
479
|
+
`i=$((i+1)); sleep ${PROBE_SLEEP_SECS}; done; ` +
|
|
480
|
+
'if [ "$serving" = "1" ]; then echo ">> editor serving project on :$PORT"; ' +
|
|
481
|
+
'elif test -f /workspace/Cargo.toml; then ' +
|
|
482
|
+
'echo "CY_NEEDS_RECONFIGURE self-host editor not serving project on :$PORT after reset ($([ "$launchered" = "1" ] && echo "launcher-mode strand ON the control port — rebuild did not adopt the guard" || echo "launcher-mode strand or server down") ) — falling back to reconfigure"; ' +
|
|
483
|
+
'else echo "WARN editor not serving project on :$PORT after reset — check the VM"; fi; true )'
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// Pure predicate the dashboard's job-completion handler uses to decide whether a
|
|
488
|
+
// finished reset must auto-fall-back to reconfigure: true iff the job log
|
|
489
|
+
// carries the `CY_NEEDS_RECONFIGURE` marker emitted by
|
|
490
|
+
// verifyServingProjectOrFlagReconfigureStep (self-host, didn't come back serving
|
|
491
|
+
// :14199). Kept here as a pure function of the log string so the decision is
|
|
492
|
+
// unit-tested without the gcloud/docker dispatch.
|
|
493
|
+
function needsReconfigureFallback(log) {
|
|
494
|
+
return typeof log === 'string' && log.includes('CY_NEEDS_RECONFIGURE');
|
|
495
|
+
}
|
|
496
|
+
|
|
217
497
|
module.exports = {
|
|
218
498
|
codeyamDeepCleanSteps,
|
|
219
499
|
codeyamBrokerEnsureStep,
|
|
500
|
+
selfHostRebuildStep,
|
|
501
|
+
verifyServingProjectOrFlagReconfigureStep,
|
|
502
|
+
needsReconfigureFallback,
|
|
220
503
|
resetClientScript,
|
|
221
504
|
resetEditorStateScript,
|
|
505
|
+
endSessionScript,
|
|
506
|
+
verifyIdleOrEndSessionStep,
|
|
222
507
|
};
|
package/npm/fleet-roster.js
CHANGED
|
@@ -31,16 +31,14 @@ function normalizeRoster(arr) {
|
|
|
31
31
|
return [...new Set((arr || []).filter(Number.isInteger))].sort((a, b) => a - b);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
// Read the roster, returning a normalized integer array
|
|
35
|
-
//
|
|
36
|
-
// `readRoster`/`loadVmsState` duplicates shared) so a truncated write or an
|
|
37
|
-
// absent file never throws into a consumer's poll loop.
|
|
34
|
+
// Read the roster, returning a normalized integer array, or null if the file is
|
|
35
|
+
// missing or malformed.
|
|
38
36
|
function readRoster() {
|
|
39
37
|
try {
|
|
40
38
|
const data = JSON.parse(fs.readFileSync(VMS_STATE_FILE, 'utf8'));
|
|
41
39
|
if (Array.isArray(data)) return normalizeRoster(data);
|
|
42
|
-
} catch { /* missing or malformed
|
|
43
|
-
return
|
|
40
|
+
} catch { /* missing or malformed */ }
|
|
41
|
+
return null;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
44
|
// Persist `arr` as the roster, atomically: write a temp file then `rename` it
|
|
@@ -62,13 +60,13 @@ function writeRoster(arr) {
|
|
|
62
60
|
// onSuccess rosters again — is a harmless no-op, which is exactly what closes
|
|
63
61
|
// the race where one writer's roster write could fail.
|
|
64
62
|
function addToRoster(n) {
|
|
65
|
-
return writeRoster([...readRoster(), n]);
|
|
63
|
+
return writeRoster([...(readRoster() || []), n]);
|
|
66
64
|
}
|
|
67
65
|
|
|
68
66
|
// Remove `n` from the roster, returning the new roster. Removing an absent
|
|
69
67
|
// entry is a no-op.
|
|
70
68
|
function removeFromRoster(n) {
|
|
71
|
-
return writeRoster(readRoster().filter((x) => x !== n));
|
|
69
|
+
return writeRoster((readRoster() || []).filter((x) => x !== n));
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
module.exports = {
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
// Pure decision + bash-script composition for the dashboard's SELF-update action
|
|
3
|
+
// (scripts/operator/fleet-dashboard/server.js requires this). Mirrors the
|
|
4
|
+
// fleet-branch.js / fleet-reset.js pattern: string + decision logic only, no I/O
|
|
5
|
+
// (no git exec, no env detection, no `exec`), so the self-update *recipe* — what
|
|
6
|
+
// the detached bash actually does to pull the dashboard's own checkout to tip and
|
|
7
|
+
// which supervisor it restarts through — stays unit-testable in isolation
|
|
8
|
+
// (npm/fleet-self-update.test.js). server.js owns the I/O: current-branch read,
|
|
9
|
+
// `id -u`, FLEET_ON_GCE detection, and the detached `exec`.
|
|
10
|
+
//
|
|
11
|
+
// The dashboard's Node half is plain source (it `require`s server.js +
|
|
12
|
+
// npm/fleet-*.js from REPO_ROOT at boot, no compile step), so refreshing *it* is
|
|
13
|
+
// just "pull new source + restart the node process". Both supervisors auto-respawn
|
|
14
|
+
// on exit (systemd Restart=always, launchd KeepAlive), so the restart is a clean
|
|
15
|
+
// hand-off. But the dashboard also depends on the Rust `codeyam-editor` binary at
|
|
16
|
+
// runtime — `pollPlans` shells out to `codeyam-editor editor plans-snapshot` for the
|
|
17
|
+
// Queued-Plans origin view — so a self-update that advances editor source past a
|
|
18
|
+
// changed/new subcommand must rebuild the binary too, or the JS moves while a stale
|
|
19
|
+
// binary keeps erroring. buildSelfUpdateScript therefore rebuilds the binary, but
|
|
20
|
+
// only when editor source actually changed across the update and fail-soft so a
|
|
21
|
+
// build problem never blocks the restart.
|
|
22
|
+
|
|
23
|
+
const { cleanBranch } = require('./fleet-branch');
|
|
24
|
+
|
|
25
|
+
// Single-quote a value for safe bash interpolation — identical to server.js's
|
|
26
|
+
// `shq`, duplicated here so this module stays a pure, dependency-light leaf (it
|
|
27
|
+
// must not require the untestable server shell).
|
|
28
|
+
function shq(s) {
|
|
29
|
+
return `'${String(s).replace(/'/g, `'\\''`)}'`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// A branch name we're willing to hand to `git checkout` / `git reset --hard`.
|
|
33
|
+
// git's own ref rules are broader, but the dashboard only ever targets ordinary
|
|
34
|
+
// fleet branches, so we accept a conservative charset (letters, digits, and the
|
|
35
|
+
// `._/-` that real branch names use) and reject everything else. This is the
|
|
36
|
+
// belt to shq's suspenders: even though every interpolated value is quoted, an
|
|
37
|
+
// up-front reject turns an injection attempt / typo into a clear `{ok:false}`
|
|
38
|
+
// instead of a script that runs and fails deep in the detached child.
|
|
39
|
+
function isSafeBranch(b) {
|
|
40
|
+
return /^[A-Za-z0-9._/-]+$/.test(b) && !b.includes('..');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Decide whether a self-update request is actionable and on which branch.
|
|
44
|
+
// - `alreadyRunning` (a self-update already in flight) is refused first, so the
|
|
45
|
+
// in-flight guard is part of the tested decision rather than action branching.
|
|
46
|
+
// - A blank requested branch falls back to the dashboard's current branch
|
|
47
|
+
// (the modal prefills the current branch, but an operator can clear it).
|
|
48
|
+
// - An unsafe / empty resulting branch is rejected with a clear `msg`.
|
|
49
|
+
// Pure; no I/O. Returns `{ ok, branch, msg }`. Keeping the guard + resolution +
|
|
50
|
+
// validation here lets server.js's actionUpdateDashboard stay a thin I/O shell.
|
|
51
|
+
function decideSelfUpdate({ requestedBranch, currentBranch, alreadyRunning } = {}) {
|
|
52
|
+
if (alreadyRunning) {
|
|
53
|
+
return { ok: false, msg: 'dashboard update already running' };
|
|
54
|
+
}
|
|
55
|
+
const branch = cleanBranch(requestedBranch) || cleanBranch(currentBranch);
|
|
56
|
+
if (!branch) {
|
|
57
|
+
return { ok: false, msg: 'self-update: no branch given and current branch is unknown' };
|
|
58
|
+
}
|
|
59
|
+
if (!isSafeBranch(branch)) {
|
|
60
|
+
return { ok: false, msg: `self-update: invalid branch name '${branch}'` };
|
|
61
|
+
}
|
|
62
|
+
return { ok: true, branch, msg: `updating dashboard to ${branch}` };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The supervisor-appropriate restart command. The dashboard restarting itself
|
|
66
|
+
// kills the very process serving the request, so this runs inside the detached
|
|
67
|
+
// child (see buildSelfUpdateScript). Selection:
|
|
68
|
+
// - GCE operator host (systemd) → `sudo systemctl restart codeyam-dashboard`
|
|
69
|
+
// (needs a passwordless sudoers drop-in; see the plan's provisioning note).
|
|
70
|
+
// - laptop (launchd) → `launchctl kickstart -k gui/<uid>/<label>`
|
|
71
|
+
// - bare-nohup fallback → `<repoRoot>/scripts/operator/fleet-dashboard/start.sh`
|
|
72
|
+
// The non-GCE path tries launchd first and falls back to start.sh in one `||`
|
|
73
|
+
// chain, so a laptop with no launchd agent (a bare `nohup node server.js`) still
|
|
74
|
+
// restarts. start.sh is idempotent (kills the port, re-execs node).
|
|
75
|
+
function restartCommand({ onGce, launchdUid, label, repoRoot }) {
|
|
76
|
+
if (onGce) return 'sudo systemctl restart codeyam-dashboard';
|
|
77
|
+
const target = shq(`gui/${launchdUid}/${label}`);
|
|
78
|
+
const startSh = shq(`${repoRoot}/scripts/operator/fleet-dashboard/start.sh`);
|
|
79
|
+
return `launchctl kickstart -k ${target} || ${startSh}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Compose the bash the detached child runs: pull the dashboard's own checkout to
|
|
83
|
+
// the tip of `branch`, conditionally rebuild the editor binary, then restart
|
|
84
|
+
// through the environment-appropriate supervisor. Hard-reset semantics
|
|
85
|
+
// (operator's choice): always land exactly on `origin/<branch>`, discarding any
|
|
86
|
+
// uncommitted/divergent state on the dashboard checkout. Bails loudly if the
|
|
87
|
+
// branch doesn't exist on origin (nothing is reset/restarted).
|
|
88
|
+
//
|
|
89
|
+
// Rebuild gating (between the reset and the restart): record the pre-reset HEAD,
|
|
90
|
+
// compare it to the post-reset HEAD restricted to the editor source paths, and
|
|
91
|
+
// rebuild only when something under `crates`/`Cargo.toml`/`Cargo.lock` actually
|
|
92
|
+
// moved — a JS-only update must not pay a multi-minute Rust compile. The rebuild
|
|
93
|
+
// is additionally gated on `codeyam-editor` being on PATH so a host without the
|
|
94
|
+
// toolchain skips cleanly, and is fail-soft (`|| echo …`) so a build failure can't
|
|
95
|
+
// abort the `set -e` script before the restart: a degraded-but-running dashboard
|
|
96
|
+
// beats a wedged one. The restart stays last and unconditional.
|
|
97
|
+
//
|
|
98
|
+
// Every interpolated value is shq-quoted. Pure; no I/O.
|
|
99
|
+
function buildSelfUpdateScript({ repoRoot, branch, onGce, launchdUid, label } = {}) {
|
|
100
|
+
const R = shq(repoRoot);
|
|
101
|
+
const B = shq(branch);
|
|
102
|
+
const originRef = shq(`origin/${branch}`);
|
|
103
|
+
const restart = restartCommand({ onGce, launchdUid, label, repoRoot });
|
|
104
|
+
return [
|
|
105
|
+
'set -e',
|
|
106
|
+
`echo ">> self-update: fetching origin in ${repoRoot}"`,
|
|
107
|
+
`git -C ${R} fetch origin`,
|
|
108
|
+
// Verify origin/<branch> exists BEFORE touching the working tree, so a typo'd
|
|
109
|
+
// / unknown branch bails with nothing reset or restarted.
|
|
110
|
+
`git -C ${R} rev-parse --verify ${originRef} >/dev/null 2>&1 || ` +
|
|
111
|
+
`{ echo "self-update: origin/${branch} not found" >&2; exit 1; }`,
|
|
112
|
+
// Record the editor-source revision BEFORE the reset, so we can tell after
|
|
113
|
+
// whether the editor binary needs rebuilding. Read-only; touches nothing.
|
|
114
|
+
`OLD=$(git -C ${R} rev-parse HEAD)`,
|
|
115
|
+
`echo ">> self-update: checking out ${branch}"`,
|
|
116
|
+
// DWIM creates a local tracking branch when one doesn't exist yet; a no-op
|
|
117
|
+
// when already on it.
|
|
118
|
+
`git -C ${R} checkout ${B}`,
|
|
119
|
+
`echo ">> self-update: hard-reset to origin/${branch} (discards local changes)"`,
|
|
120
|
+
`git -C ${R} reset --hard ${originRef}`,
|
|
121
|
+
`NEW=$(git -C ${R} rev-parse HEAD)`,
|
|
122
|
+
// Rebuild the editor binary, but only when (a) editor source actually moved
|
|
123
|
+
// and (b) the binary is on PATH. The `git diff --quiet … -- crates …` exits 0
|
|
124
|
+
// when nothing under the editor source paths changed → skip (the fast JS-only
|
|
125
|
+
// path). `command -v` skips a toolchain-less host. The rebuild itself is
|
|
126
|
+
// fail-soft so a build failure still falls through to the restart. Each guard
|
|
127
|
+
// sits in an `if`/`elif` condition, where a non-zero exit is allowed under
|
|
128
|
+
// `set -e`.
|
|
129
|
+
`if git -C ${R} diff --quiet "$OLD" "$NEW" -- crates Cargo.toml Cargo.lock; then ` +
|
|
130
|
+
`echo ">> self-update: skipping rebuild (no editor source change)"; ` +
|
|
131
|
+
`elif ! command -v codeyam-editor >/dev/null 2>&1; then ` +
|
|
132
|
+
`echo ">> self-update: skipping rebuild (codeyam-editor not on PATH)"; ` +
|
|
133
|
+
`else echo ">> self-update: editor source changed — rebuilding binary"; ` +
|
|
134
|
+
`codeyam-editor editor rebuild-self || ` +
|
|
135
|
+
`echo ">> self-update: rebuild-self FAILED — continuing to restart with the existing binary" >&2; ` +
|
|
136
|
+
`fi`,
|
|
137
|
+
`echo ">> self-update: restarting dashboard"`,
|
|
138
|
+
restart,
|
|
139
|
+
].join('\n');
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = { shq, isSafeBranch, decideSelfUpdate, restartCommand, buildSelfUpdateScript };
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// npm/fleet-setup-steps.js — partition a VM's degradedReasons into expected
|
|
4
|
+
// first-run SETUP steps vs genuine ATTENTION problems for the fleet dashboard
|
|
5
|
+
// (vm: Dashboard 'Finishing setup' state for expected add steps).
|
|
6
|
+
//
|
|
7
|
+
// A clean greenfield Add lands with a wall of amber `⚠ Add degraded` badges
|
|
8
|
+
// because every unfinished post-provision wiring step is funneled into one flat
|
|
9
|
+
// `degradedReasons` string array and rendered identically. There is no
|
|
10
|
+
// distinction between *expected first-run setup that just hasn't finished yet*
|
|
11
|
+
// (Claude login not seeded, ingress host-rule not wired, project dir still the
|
|
12
|
+
// scaffold `workspace`, no start command on a brand-new project) and *something
|
|
13
|
+
// that actually broke* (broker death, half-provisioned orphan, off-branch
|
|
14
|
+
// drift). This module draws that line: it parses the existing reason-code
|
|
15
|
+
// prefixes and classifies each reason as a calm `setup` step or an `attention`
|
|
16
|
+
// problem, keeping the rules in a tested pure function the way fleet-job-badge.js
|
|
17
|
+
// / fleet-public-url.js do — server.js stays thin and the card colors finally
|
|
18
|
+
// tell the truth (blue = still wiring up, amber = look at this).
|
|
19
|
+
//
|
|
20
|
+
// Stack note: this is fleet-operator infrastructure (the GCE fleet that runs
|
|
21
|
+
// codeyam-editor itself), GCP-specific by design — the same posture as the
|
|
22
|
+
// scripts it backstops. The classification is pure string parsing, no network,
|
|
23
|
+
// so it unit-tests with no fixtures.
|
|
24
|
+
|
|
25
|
+
// The setup-step codes this module emits. A single source so server.js and the
|
|
26
|
+
// card UI agree on the vocabulary.
|
|
27
|
+
const SETUP_CODES = ['auth', 'ingress', 'project-dir', 'start-command'];
|
|
28
|
+
|
|
29
|
+
// The scaffold default project directory a brand-new (not-yet-created) VM runs
|
|
30
|
+
// under. project-dir-drift AWAY from this is a real problem (two real project
|
|
31
|
+
// names disagree); drift while still ON it is just "the greenfield project
|
|
32
|
+
// hasn't been created yet" — an expected setup step.
|
|
33
|
+
const SCAFFOLD_PROJECT_DIR = 'workspace';
|
|
34
|
+
|
|
35
|
+
// Parse `project-dir-drift (expected: X, running: Y)` into its two names.
|
|
36
|
+
// Returns { expected, running } with null fields when the shape doesn't match.
|
|
37
|
+
function parseDriftReason(reason) {
|
|
38
|
+
const m = /expected:\s*([^,]+?)\s*,\s*running:\s*([^)]+?)\s*\)/.exec(reason);
|
|
39
|
+
return m ? { expected: m[1].trim(), running: m[2].trim() } : { expected: null, running: null };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The re-wire command the ingress-unwired reason embeds after "Re-wire: " (see
|
|
43
|
+
// fleet-public-url.js::ingressUnwiredReason). Surfaced as the setup step's
|
|
44
|
+
// inline action so the calm checklist row carries the exact command, not just a
|
|
45
|
+
// label. null when the reason carries no command tail.
|
|
46
|
+
function parseIngressAction(reason) {
|
|
47
|
+
const m = /Re-wire:\s*(.+)$/.exec(reason);
|
|
48
|
+
return m ? m[1].trim() : null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Partition degradedReasons into { setupSteps, attentionReasons }.
|
|
52
|
+
//
|
|
53
|
+
// setupSteps ordered [{ code, label, detail, action? }] — expected
|
|
54
|
+
// first-run wiring the card renders as a calm checklist.
|
|
55
|
+
// attentionReasons the original reason strings that signal a genuine
|
|
56
|
+
// problem — rendered amber with the existing ⚠ treatment.
|
|
57
|
+
//
|
|
58
|
+
// Unknown reason codes default to attentionReasons: an unrecognized signal is
|
|
59
|
+
// surfaced loudly, never silently hidden in the calm checklist.
|
|
60
|
+
function classifyReasons(degradedReasons = []) {
|
|
61
|
+
const setupSteps = [];
|
|
62
|
+
const attentionReasons = [];
|
|
63
|
+
const list = Array.isArray(degradedReasons) ? degradedReasons : [];
|
|
64
|
+
for (const raw of list) {
|
|
65
|
+
const reason = String(raw);
|
|
66
|
+
if (reason.startsWith('claude-auth-not-seeded')) {
|
|
67
|
+
setupSteps.push({ code: 'auth', label: 'Claude login', detail: reason });
|
|
68
|
+
} else if (reason.startsWith('ingress-unwired')) {
|
|
69
|
+
setupSteps.push({
|
|
70
|
+
code: 'ingress',
|
|
71
|
+
label: 'Public URL wiring',
|
|
72
|
+
detail: reason,
|
|
73
|
+
action: parseIngressAction(reason),
|
|
74
|
+
});
|
|
75
|
+
} else if (reason.startsWith('project-dir-drift')) {
|
|
76
|
+
const { expected, running } = parseDriftReason(reason);
|
|
77
|
+
if (running === SCAFFOLD_PROJECT_DIR) {
|
|
78
|
+
setupSteps.push({
|
|
79
|
+
code: 'project-dir',
|
|
80
|
+
label: expected ? `Create project ${expected}` : 'Create project',
|
|
81
|
+
detail: reason,
|
|
82
|
+
});
|
|
83
|
+
} else {
|
|
84
|
+
// Drift between two REAL project names is a genuine problem, not setup.
|
|
85
|
+
attentionReasons.push(reason);
|
|
86
|
+
}
|
|
87
|
+
} else if (reason.startsWith('start-command')) {
|
|
88
|
+
setupSteps.push({ code: 'start-command', label: 'Start command', detail: reason });
|
|
89
|
+
} else {
|
|
90
|
+
attentionReasons.push(reason);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return { setupSteps, attentionReasons };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// True when a VM's reasons are ALL expected setup steps with zero genuine
|
|
97
|
+
// problems — the gate for the calm `finishing-setup` status. Requires at least
|
|
98
|
+
// one setup step (an empty reason list is not "finishing setup", it's just a
|
|
99
|
+
// healthy VM with nothing left to do) and zero attention reasons.
|
|
100
|
+
function hasOnlySetup(degradedReasons = []) {
|
|
101
|
+
const { setupSteps, attentionReasons } = classifyReasons(degradedReasons);
|
|
102
|
+
return setupSteps.length > 0 && attentionReasons.length === 0;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = { classifyReasons, hasOnlySetup, SETUP_CODES, SCAFFOLD_PROJECT_DIR };
|