@codeyam-editor/codeyam-editor 0.1.0-staging.01efa1a

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.
Files changed (56) hide show
  1. package/npm/cli.js +19 -0
  2. package/npm/cloud-branch-switch.js +442 -0
  3. package/npm/cloud.js +2184 -0
  4. package/npm/container.js +54 -0
  5. package/npm/editor-dev.js +459 -0
  6. package/npm/editor.js +272 -0
  7. package/npm/empty-project-dir.js +33 -0
  8. package/npm/fleet-auth.js +111 -0
  9. package/npm/fleet-branch.js +49 -0
  10. package/npm/fleet-broker-death.js +44 -0
  11. package/npm/fleet-broker-debounce.js +63 -0
  12. package/npm/fleet-broker-status.js +45 -0
  13. package/npm/fleet-build-data.js +81 -0
  14. package/npm/fleet-cleanup.js +297 -0
  15. package/npm/fleet-effect-status.js +179 -0
  16. package/npm/fleet-env.js +125 -0
  17. package/npm/fleet-grouping.js +56 -0
  18. package/npm/fleet-job-badge.js +83 -0
  19. package/npm/fleet-memory.js +157 -0
  20. package/npm/fleet-plans.js +96 -0
  21. package/npm/fleet-pool-policy.js +78 -0
  22. package/npm/fleet-pool.js +401 -0
  23. package/npm/fleet-queue.js +40 -0
  24. package/npm/fleet-reconcile.js +93 -0
  25. package/npm/fleet-reset.js +222 -0
  26. package/npm/fleet-roster.js +81 -0
  27. package/npm/launch-config.js +717 -0
  28. package/npm/open.js +15 -0
  29. package/npm/preflight-guidance.js +77 -0
  30. package/npm/scenario-check.js +604 -0
  31. package/npm/scenario-handlers.js +82 -0
  32. package/npm/scenario-interactivity.js +245 -0
  33. package/npm/scenario-issues.js +35 -0
  34. package/npm/scenario-metrics.js +108 -0
  35. package/npm/scenario-mocks.js +107 -0
  36. package/npm/scenario-playwright.js +534 -0
  37. package/npm/utils.js +823 -0
  38. package/npm/vm-urls.js +117 -0
  39. package/package.json +32 -0
  40. package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +2 -0
  41. package/ui/dist/assets/EditorShell-By-2rl7D.js +187 -0
  42. package/ui/dist/assets/Markdown-BHBqz-Mj.js +1 -0
  43. package/ui/dist/assets/ScenarioDataPanel-Cg8zOFNZ.js +5 -0
  44. package/ui/dist/assets/ScenarioDataPanel-DYP7pi_n.css +32 -0
  45. package/ui/dist/assets/ScenarioDataPanelFullPage-WOUYbba7.js +1 -0
  46. package/ui/dist/assets/ScenarioRenderer-CBTlIVPs.js +1 -0
  47. package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +1 -0
  48. package/ui/dist/assets/index-DzoF9RKw.js +27 -0
  49. package/ui/dist/assets/index-ckz94rKI.css +1 -0
  50. package/ui/dist/assets/markdown-v0F0UEt3.js +37 -0
  51. package/ui/dist/assets/react-CSS0HapR.js +28 -0
  52. package/ui/dist/assets/useEvents-DggWrtHe.js +1 -0
  53. package/ui/dist/assets/xterm--24IGk-x.js +9 -0
  54. package/ui/dist/favicon.ico +0 -0
  55. package/ui/dist/favicon.png +0 -0
  56. package/ui/dist/index.html +16 -0
@@ -0,0 +1,222 @@
1
+ 'use strict';
2
+ // Pure bash-script builders for the fleet dashboard's RESET actions
3
+ // (scripts/operator/fleet-dashboard/server.js requires this). String
4
+ // construction only — no I/O, no gcloud/docker exec — so the reset *recipe*
5
+ // (what the remote bash actually does) stays unit-testable in isolation
6
+ // (npm/fleet-reset.test.js), exactly like launch-config.js / fleet-env.js.
7
+ // The dispatch (gcloud ssh → docker exec → startJob) stays in server.js as
8
+ // platform-glue; only the decision-bearing script shape lives here.
9
+
10
+ // The `.codeyam` deep-clean preserve→pause→wipe→restore sequence, shared by every
11
+ // reset that hard-cleans a /workspace. `git reset --hard` + `session-reset`
12
+ // leave the git-IGNORED runtime state behind (workflow step files,
13
+ // scenario-*.js, server-state, logs, caches), so we explicitly wipe and
14
+ // restore the tracked `.codeyam`. Two gitignored files MUST survive the wipe:
15
+ // - editor.local.json — previewOrigin + the agent API key (a blind wipe
16
+ // breaks Live Preview + auth).
17
+ // - server-state.json — the running pid + control port the restart's verify
18
+ // reads.
19
+ // Both are cp'd aside, `.codeyam` is wiped, restored from the branch (tracked,
20
+ // e.g. self-host) or re-`init`'d (greenfield, where `.codeyam` is gitignored),
21
+ // then the two files are copied back. Returned as step strings the caller
22
+ // joins into its own `&&`-chain (the surrounding `( …; true )` guards keep each
23
+ // best-effort cp from breaking the chain when a file is absent).
24
+ //
25
+ // First step: release any commit-queue entries this VM owns. The wipe
26
+ // regenerates `.codeyam/run/queue-origin-id`, which would otherwise orphan
27
+ // the prior session's entry (different origin_id, same machine — invisible
28
+ // to the two-key adoption path until the next enqueue's same-machine guard
29
+ // reaps it). Routing through `branch-queue release-mine` makes the
30
+ // queue-side cleanup intentional and visible in the queue log instead.
31
+ // Best-effort: a no-remote / queue-disabled project legitimately exits
32
+ // non-zero, and we should not block the reset.
33
+ //
34
+ // Before the wipe we QUIESCE ALL WRITERS: park the editor server
35
+ // (`stop-server --hold-for-restart`) AND stop the PTY broker
36
+ // (`pty-broker stop`). Parking only the editor server is insufficient — the
37
+ // broker is a SEPARATE process (and hooks / agent subprocesses can fire), so
38
+ // it keeps re-creating `.codeyam/logs/` and `.codeyam/run/` between when the
39
+ // wipe enumerates the dir and when it removes it. That race surfaced live
40
+ // (VM-1, 2026-06-01) as `rm: cannot remove '.codeyam': Directory not empty`,
41
+ // aborting the whole reset. Both quiesce calls are fail-soft `|| true` so a
42
+ // server/broker that's already dead (e.g. the reset is rerun after a partial
43
+ // failure) doesn't block the chain.
44
+ //
45
+ // The wipe itself is RETRY-RESILIENT instead of a single `rm -rf .codeyam`
46
+ // that aborts the &&-chain on one transient re-creation: a bounded loop of
47
+ // `find .codeyam -mindepth 1 -delete` empties the contents and tolerates a
48
+ // brief re-create, then proceeds even if `.codeyam` is still non-empty after
49
+ // best-effort (a soft retry, never a hard failure — `git checkout`/`init`
50
+ // repopulates over whatever remains). Emptying contents (not removing the dir)
51
+ // also sidesteps the `rmdir` race entirely.
52
+ //
53
+ // The destructive region (park → broker-stop → wipe → restore) is made
54
+ // FAILURE-SAFE with an EXIT trap installed before the park and disarmed after
55
+ // a successful restore. If ANY step in that region aborts the chain, the trap
56
+ // still restores tracked `.codeyam` (or re-inits), copies the two preserved
57
+ // infra files back, and runs `restart-server --force-in-container` before
58
+ // exiting non-zero — so a failed deep-clean self-recovers to a working
59
+ // (un-reset, but UP) VM instead of leaving it parked-and-down with a
60
+ // half-wiped `.codeyam`, and still reports the failure. The happy path is
61
+ // unchanged: the restore steps run, the trap is disarmed, and the caller's
62
+ // own trailing `restart-server` resurrects the parked server.
63
+ function codeyamDeepCleanSteps(E) {
64
+ // Restore primitives shared by the happy path and the trap recovery body.
65
+ const restoreCodeyam = `git checkout HEAD -- .codeyam 2>/dev/null || ${E} init`;
66
+ const restoreElj =
67
+ 'test -f /tmp/cy-elj.bak && mkdir -p .codeyam && cp /tmp/cy-elj.bak .codeyam/editor.local.json';
68
+ const restoreSs =
69
+ 'test -f /tmp/cy-ss.bak && mkdir -p .codeyam && cp /tmp/cy-ss.bak .codeyam/server-state.json';
70
+ // Recovery body: restore .codeyam + the two infra backups, then resurrect the
71
+ // parked server. Runs ONLY on the failure path (the happy path's own restore
72
+ // steps remove the backups; recovery here intentionally leaves them in place
73
+ // because on a mid-region abort those steps never ran).
74
+ const recover =
75
+ `${restoreCodeyam}; ( ${restoreElj}; true ); ( ${restoreSs}; true ); ` +
76
+ `${E} editor restart-server --force-in-container || true`;
77
+ return [
78
+ `( ${E} editor branch-queue release-mine --yes 2>/dev/null; true )`,
79
+ '( cp .codeyam/editor.local.json /tmp/cy-elj.bak 2>/dev/null; true )',
80
+ '( cp .codeyam/server-state.json /tmp/cy-ss.bak 2>/dev/null; true )',
81
+ // A brace group (NOT a subshell) so the function + trap persist into the
82
+ // current shell where the rest of the &&-chain runs.
83
+ `{ __cy_recover() { ${recover}; }; trap '__cy_recover; exit 1' EXIT; }`,
84
+ `(${E} editor stop-server --hold-for-restart --force-in-container || true)`,
85
+ `(${E} editor pty-broker stop || true)`,
86
+ '( for i in 1 2 3 4 5; do find .codeyam -mindepth 1 -delete 2>/dev/null; [ -z "$(ls -A .codeyam 2>/dev/null)" ] && break; sleep 0.2; done; true )',
87
+ `( ${restoreCodeyam} )`,
88
+ `( ${restoreElj} && rm -f /tmp/cy-elj.bak; true )`,
89
+ `( ${restoreSs} && rm -f /tmp/cy-ss.bak; true )`,
90
+ // Restore succeeded — disarm the failure-safe trap so normal completion
91
+ // (and the caller's trailing session-reset + restart) doesn't re-trigger it.
92
+ 'trap - EXIT',
93
+ ];
94
+ }
95
+
96
+ // Belt-and-suspenders broker-ensure, appended after the trailing
97
+ // `restart-server` in every reset recipe. The PRIMARY fix lives in the
98
+ // editor server's `ensure_broker_running`: it now `rm -f`s the stale
99
+ // socket and retries the spawn, so the reexec'd `start` (which
100
+ // restart-server triggers) reliably respawns the broker with no
101
+ // down-window. This step is the safety net for an OLDER in-container
102
+ // binary whose `restart-server` predates that fix — it mirrors the
103
+ // vm-watchdog's recovery so a reset still ends broker-up regardless of
104
+ // which binary is installed.
105
+ //
106
+ // Shape: status-gated and idempotent. `pty-broker status` exits non-zero
107
+ // only when no broker is reachable; ONLY then does the `||` branch fire
108
+ // (`rm -f` the stale socket — mirroring the watchdog — then `nohup` a
109
+ // detached daemon). When a broker is already alive the status succeeds
110
+ // and the respawn branch never runs, so a healthy broker is NEVER killed.
111
+ // Wrapped in `( …; true )` so it can never break the reset's &&-chain
112
+ // (a down broker that fails to respawn must not abort the rest of the
113
+ // reset). The socket glob + `pty-broker daemon` invocation match the
114
+ // watchdog (`scripts/operator/vm-watchdog.sh`) and the per-project
115
+ // `$TMPDIR`/`/tmp` socket layout (`socket_path_for`).
116
+ function codeyamBrokerEnsureStep(E) {
117
+ return (
118
+ `( ${E} editor pty-broker status >/dev/null 2>&1 || ` +
119
+ `( rm -f /tmp/codeyam-pty-broker-*.sock; ` +
120
+ `nohup ${E} editor pty-broker daemon </dev/null >/tmp/broker-spawn.log 2>&1 & disown ); ` +
121
+ 'true )'
122
+ );
123
+ }
124
+
125
+ // Build the in-container bash for "Reset client" — hard-clean the CLIENT
126
+ // project (/workspace) on ANY VM shape. The decisions encoded here:
127
+ // - Reset to HEAD, NOT origin: `git reset --hard` (bare) discards uncommitted
128
+ // churn but PRESERVES committed work. Advancing to origin tip is the
129
+ // separate "Update client" action — this must never `git fetch`/reset to
130
+ // `origin/<branch>`.
131
+ // - `git reset --hard` leaves untracked files, so node_modules + a per-VM
132
+ // .env survive (no forced re-install / lost secrets).
133
+ // - Stop the broker FIRST so abandoned agent WIP can't be re-written to disk
134
+ // mid-reset; drop a stale LOCAL credential.helper (fragility #29) so the
135
+ // re-seed + the agent's git ops fall back to the working system helper.
136
+ // - Re-seed the git credential cache AFTER the restart so the agent can
137
+ // pull/push immediately; the re-seed is a self-contained subshell whose
138
+ // own `|| echo` never masks an earlier &&-chain failure.
139
+ // `E` is the in-container editor binary; `gitReseedBody` is the re-seed bash
140
+ // body (passed in so this stays a pure function of its inputs).
141
+ function resetClientScript({ E, gitReseedBody }) {
142
+ return [
143
+ 'cd /workspace',
144
+ `command -v ${E} >/dev/null`,
145
+ 'echo ">> [1/5] stopping agent session"',
146
+ `(${E} editor pty-broker stop || true)`,
147
+ '(git config --local --unset-all credential.helper || true)',
148
+ 'echo ">> [2/5] discarding uncommitted changes (git reset --hard to HEAD; keeps committed work + untracked files)"',
149
+ 'git reset --hard',
150
+ 'echo ">> [3/5] deep-cleaning .codeyam (clears ignored runtime state; keeps editor.local.json + server-state.json)"',
151
+ ...codeyamDeepCleanSteps(E),
152
+ `${E} editor session-reset`,
153
+ 'echo ">> [4/5] restarting server to respawn the broker (no rebuild)"',
154
+ `(${E} editor restart-server --force-in-container || true)`,
155
+ // Belt-and-suspenders: ensure a broker is live even on an older binary
156
+ // whose restart-server doesn't yet respawn it. Idempotent / status-gated.
157
+ codeyamBrokerEnsureStep(E),
158
+ 'echo ">> [5/5] re-seeding git credential cache"',
159
+ `( ( ${gitReseedBody} ) || echo "WARN git cache re-seed failed" )`,
160
+ ].join(' && ');
161
+ }
162
+
163
+ // Build the in-container bash for "Reset editor" — the editor-state recipe:
164
+ // stop the agent, drop a stale local credential.helper, advance the VM's branch
165
+ // to origin tip (skipped on a greenfield local-only repo), deep-clean .codeyam
166
+ // (preserving editor.local.json + server-state.json via codeyamDeepCleanSteps),
167
+ // run session-reset, and restart the server. This is the SAME &&-chain
168
+ // previously inlined in actionResetEditorSource in scripts/operator/
169
+ // fleet-dashboard/server.js — extracted so the standalone Reset editor action
170
+ // AND the composite Update & Reset Everything action share one definition and
171
+ // it gets unit-test coverage like the other reset builders.
172
+ //
173
+ // Binary-existence gate FIRST so a broken binary aborts BEFORE the destructive
174
+ // `git reset --hard` / `rm -rf .codeyam`. The composite action relies on this
175
+ // same gate as its safety contract — a failed rebuild that leaves no binary
176
+ // will short-circuit the trailing recipe before any wipe happens.
177
+ function resetEditorStateScript({ E }) {
178
+ return [
179
+ 'cd /workspace',
180
+ `command -v ${E} >/dev/null`,
181
+ // Tolerate a missing broker: `pty-broker stop` exits non-zero when none is
182
+ // running; the subshell + `|| true` keeps it non-fatal without swallowing
183
+ // the binary-existence gate above.
184
+ 'echo ">> [1/4] stopping agent session + clearing workflow state"',
185
+ `(${E} editor pty-broker stop || true)`,
186
+ // Drop a stale LOCAL credential.helper (fragility #29) so `git fetch` falls
187
+ // back to the working system helper. No-op when none is set.
188
+ '(git config --local --unset-all credential.helper || true)',
189
+ // Advance the VM's own branch to origin (skip on a greenfield local-only repo
190
+ // with no remote). fetch + `reset --hard origin/<branch>`, NOT `pull --ff-only`
191
+ // (which dies "Cannot fast-forward to multiple branches" on some VM states).
192
+ 'if git remote | grep -qx origin; then ' +
193
+ 'BR=$(git branch --show-current); ' +
194
+ 'echo ">> [2/4] advancing source on $BR to origin tip"; ' +
195
+ 'git reset --hard && git fetch origin "$BR" && git reset --hard "origin/$BR"; ' +
196
+ 'else echo ">> [2/4] no origin remote (greenfield project) — skipping source advance"; fi',
197
+ // Deep-clean .codeyam: `git reset --hard` + session-reset leave git-IGNORED /
198
+ // untracked state (workflow + design-step state, server-state, logs, caches).
199
+ // Preserve editor.local.json (previewOrigin + agent API key — a blind wipe
200
+ // breaks Live Preview + auth) and server-state.json (runtime pid + control
201
+ // port the restart's verify reads), wipe .codeyam, restore it from the branch
202
+ // (tracked, e.g. self-host) or re-init (greenfield), then put both files back.
203
+ 'echo ">> [3/4] deep-cleaning .codeyam (clears ignored state; keeps editor.local.json + server-state.json)"',
204
+ ...codeyamDeepCleanSteps(E),
205
+ `${E} editor session-reset`,
206
+ // No rebuild here (that's Rebuild binary). Restart the server in place to
207
+ // respawn the broker we stopped: restart-server SIGUSR1s pid 1 and the
208
+ // docker-exec returns cleanly, so `|| true` keeps a broker-down VM green.
209
+ 'echo ">> [4/4] restarting server to respawn the broker (no rebuild)"',
210
+ `(${E} editor restart-server --force-in-container || true)`,
211
+ // Belt-and-suspenders: ensure a broker is live even on an older binary
212
+ // whose restart-server doesn't yet respawn it. Idempotent / status-gated.
213
+ codeyamBrokerEnsureStep(E),
214
+ ].join(' && ');
215
+ }
216
+
217
+ module.exports = {
218
+ codeyamDeepCleanSteps,
219
+ codeyamBrokerEnsureStep,
220
+ resetClientScript,
221
+ resetEditorStateScript,
222
+ };
@@ -0,0 +1,81 @@
1
+ 'use strict';
2
+
3
+ // npm/fleet-roster.js — the fleet roster (`.codeyam/logs/fleet-vms.json`) is a
4
+ // shared contract: the dashboard, the CLI provisioner (`cloud:up`), the
5
+ // `cloud:branch-switch` orchestrator, the vm-watchdog, and the bash tunnel
6
+ // publishers (`vm-urls.sh` / `cloudflared-monitor.sh`) all read it to learn
7
+ // which VMs exist. Before improve38 the *writer* lived only inside the dashboard
8
+ // process (`server.js::saveVmsState`), so a VM provisioned by the CLI never
9
+ // appeared until someone hand-edited the file. This module is the single
10
+ // read+write seam every Node consumer shares, so the writer is no longer locked
11
+ // inside the dashboard.
12
+ //
13
+ // File format (unchanged): a JSON array of VM-number integers. Readers tolerate
14
+ // a missing or malformed file by treating it as the empty roster.
15
+ //
16
+ // Env override: `VMS_STATE_FILE` relocates the file (honored by server.js and
17
+ // cloud-branch-switch.js already); resolved once at module load.
18
+
19
+ const fs = require('fs');
20
+ const path = require('path');
21
+
22
+ const { rootDir } = require('./utils');
23
+
24
+ const VMS_STATE_FILE = process.env.VMS_STATE_FILE
25
+ || path.join(rootDir, '.codeyam', 'logs', 'fleet-vms.json');
26
+
27
+ // Canonical roster shape: a sorted, de-duped set of integers. Every read and
28
+ // write funnels through here so on-disk content stays normalized regardless of
29
+ // which consumer last wrote it (or hand-edited it).
30
+ function normalizeRoster(arr) {
31
+ return [...new Set((arr || []).filter(Number.isInteger))].sort((a, b) => a - b);
32
+ }
33
+
34
+ // Read the roster, returning a normalized integer array. A missing or malformed
35
+ // file is treated as the empty roster `[]` (the documented contract the previous
36
+ // `readRoster`/`loadVmsState` duplicates shared) so a truncated write or an
37
+ // absent file never throws into a consumer's poll loop.
38
+ function readRoster() {
39
+ try {
40
+ const data = JSON.parse(fs.readFileSync(VMS_STATE_FILE, 'utf8'));
41
+ if (Array.isArray(data)) return normalizeRoster(data);
42
+ } catch { /* missing or malformed — treated as empty */ }
43
+ return [];
44
+ }
45
+
46
+ // Persist `arr` as the roster, atomically: write a temp file then `rename` it
47
+ // over the target so a concurrent reader never observes a half-written file.
48
+ // The temp name carries the pid so two *processes* writing at once (CLI +
49
+ // dashboard) don't clobber each other's temp file before their renames land.
50
+ // Returns the normalized roster that was written.
51
+ function writeRoster(arr) {
52
+ const next = normalizeRoster(arr);
53
+ fs.mkdirSync(path.dirname(VMS_STATE_FILE), { recursive: true });
54
+ const tmp = `${VMS_STATE_FILE}.${process.pid}.tmp`;
55
+ fs.writeFileSync(tmp, JSON.stringify(next));
56
+ fs.renameSync(tmp, VMS_STATE_FILE); // atomic replace
57
+ return next;
58
+ }
59
+
60
+ // Append `n` to the roster if absent (idempotent), returning the new roster.
61
+ // A double-roster — e.g. the CLI rosters on `cloud:up` and the dashboard's Add
62
+ // onSuccess rosters again — is a harmless no-op, which is exactly what closes
63
+ // the race where one writer's roster write could fail.
64
+ function addToRoster(n) {
65
+ return writeRoster([...readRoster(), n]);
66
+ }
67
+
68
+ // Remove `n` from the roster, returning the new roster. Removing an absent
69
+ // entry is a no-op.
70
+ function removeFromRoster(n) {
71
+ return writeRoster(readRoster().filter((x) => x !== n));
72
+ }
73
+
74
+ module.exports = {
75
+ VMS_STATE_FILE,
76
+ normalizeRoster,
77
+ readRoster,
78
+ writeRoster,
79
+ addToRoster,
80
+ removeFromRoster,
81
+ };