@codeyam-editor/codeyam-editor 0.1.7 → 0.1.9
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.js +477 -41
- package/npm/editor-dev.js +24 -2
- package/npm/editor.js +15 -5
- package/npm/fleet-add-failure.js +406 -25
- package/npm/fleet-branch.js +124 -1
- package/npm/fleet-build-data.js +12 -1
- package/npm/fleet-cleanup.js +84 -3
- package/npm/fleet-datastore-health.js +107 -0
- package/npm/fleet-memory.js +29 -4
- package/npm/fleet-queue.js +141 -0
- package/npm/fleet-revision.js +108 -0
- package/npm/fleet-run-setup.js +66 -0
- package/npm/fleet-session-activity.js +162 -0
- package/npm/fleet-vm-access.js +162 -0
- package/npm/fleet-vm-readiness.js +57 -16
- package/npm/ingress-invoke.js +46 -2
- package/npm/launch-config.js +169 -2
- package/npm/scenario-check.js +708 -35
- package/npm/scenario-handlers.js +18 -0
- package/npm/scenario-interactivity.js +477 -23
- package/npm/scenario-issues.js +72 -0
- package/npm/scenario-mocks.js +245 -39
- package/npm/scenario-playwright.js +137 -26
- package/npm/utils.js +262 -22
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-Cu1tSN-o.js → ContainerInspector-C7I9wctz.js} +1 -1
- package/ui/dist/assets/EditorShell-9aYMeOdZ.js +330 -0
- package/ui/dist/assets/EditorShell-CE8iG_EM.css +1 -0
- package/ui/dist/assets/{ProjectLauncherScreen-Dmm6Z0mv.js → ProjectLauncherScreen-E0NYYiwy.js} +1 -1
- package/ui/dist/assets/ScenarioDataPanel-CLwkrex0.js +18 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-BqMxWVKW.js → ScenarioDataPanelFullPage-DT0TZC4M.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-Bsarhcrl.js +21 -0
- package/ui/dist/assets/index-BRoAenc1.css +1 -0
- package/ui/dist/assets/index-iMR5siiG.js +27 -0
- package/ui/dist/assets/useServerIdentity-CVuHTzjg.js +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/preview-subdir-demo/index.html +62 -0
- package/ui/dist/preview-subdir-demo/sibling.html +48 -0
- package/ui/dist/assets/EditorShell--V2ExYQ6.js +0 -260
- package/ui/dist/assets/EditorShell-DP5UY20G.css +0 -1
- package/ui/dist/assets/ScenarioDataPanel-DjprZq2L.js +0 -9
- package/ui/dist/assets/ScenarioRenderer-DqLVVMYS.js +0 -21
- package/ui/dist/assets/index-CjxWhkSy.css +0 -1
- package/ui/dist/assets/index-VuyqBbGJ.js +0 -27
- package/ui/dist/assets/useServerIdentity-BdXy8TQv.js +0 -1
package/npm/ingress-invoke.js
CHANGED
|
@@ -78,7 +78,15 @@ function regionFromZone(zone) {
|
|
|
78
78
|
* stand. An optional `slug` becomes `VM_SLUG`, which makes the script route the
|
|
79
79
|
* VM's custom `<slug>.<surface>.<domain>` host (the same slug must be passed on
|
|
80
80
|
* `down` so the matching host rule is removed); omit it to keep the numeric
|
|
81
|
-
* `vm-<N>` host.
|
|
81
|
+
* `vm-<N>` host.
|
|
82
|
+
*
|
|
83
|
+
* `vmAccess` (an array or comma-separated string, `up` only) becomes VM_ACCESS —
|
|
84
|
+
* the Add's INTENDED principal set, which the script's `apply_default_iap_access`
|
|
85
|
+
* grants instead of reading the per-VM sidecar. Passing it is what stops the
|
|
86
|
+
* ingress path granting a reused slot's prior tenant that the very next step then
|
|
87
|
+
* revokes as stale (VMs 4/5/6, 2026-08-09). Omit it (or pass an empty list) to
|
|
88
|
+
* leave the script's sidecar-read fallback in place — the standalone/reconcile
|
|
89
|
+
* path, where no Add is deciding the intent. */
|
|
82
90
|
function vmIngressInvocation({
|
|
83
91
|
scriptPath,
|
|
84
92
|
vmNumber,
|
|
@@ -89,6 +97,7 @@ function vmIngressInvocation({
|
|
|
89
97
|
appPort,
|
|
90
98
|
instancePrefix,
|
|
91
99
|
slug,
|
|
100
|
+
vmAccess,
|
|
92
101
|
} = {}) {
|
|
93
102
|
if (!scriptPath) throw new Error('vmIngressInvocation requires scriptPath');
|
|
94
103
|
if (action !== 'up' && action !== 'down') {
|
|
@@ -105,6 +114,8 @@ function vmIngressInvocation({
|
|
|
105
114
|
if (appPort != null && String(appPort).trim() !== '') env.APP_PORT = String(appPort);
|
|
106
115
|
if (instancePrefix) env.INSTANCE_PREFIX = instancePrefix;
|
|
107
116
|
if (slug != null && String(slug).trim() !== '') env.VM_SLUG = String(slug).trim();
|
|
117
|
+
const access = (Array.isArray(vmAccess) ? vmAccess.join(',') : String(vmAccess == null ? '' : vmAccess)).trim();
|
|
118
|
+
if (action === 'up' && access !== '') env.VM_ACCESS = access;
|
|
108
119
|
return { command: 'bash', args: [scriptPath, String(n), action], env };
|
|
109
120
|
}
|
|
110
121
|
|
|
@@ -427,6 +438,13 @@ function detachedAddTailInvocation({ nodePath, scriptPath, cfg, appPort } = {})
|
|
|
427
438
|
return { command: nodePath, args: [scriptPath, 'wire-add-tail'], env };
|
|
428
439
|
}
|
|
429
440
|
|
|
441
|
+
/** Basename prefix of the in-flight marker, shared by the path BUILDER below and
|
|
442
|
+
* the `inflightMarkerVmNumber` PARSER beneath it so the naming has exactly one
|
|
443
|
+
* owner. The two must agree exactly: the parser is how the dashboard counts live
|
|
444
|
+
* tails fleet-wide, and a prefix that drifted on one side only would silently
|
|
445
|
+
* count zero — which reads identically to a quiet fleet. */
|
|
446
|
+
const INFLIGHT_MARKER_BASENAME_PREFIX = 'cy-ingress-inflight-vm-';
|
|
447
|
+
|
|
430
448
|
/** The per-VM marker file the detached add-tail holds while its wiring is still
|
|
431
449
|
* running, so the dashboard's auto-rewire gather skips a VM whose FIRST wiring
|
|
432
450
|
* attempt is in flight (never firing a competing `vm-ingress.sh up`). Pure path
|
|
@@ -438,7 +456,32 @@ function ingressInflightMarkerPath(dir, vmNumber) {
|
|
|
438
456
|
if (!Number.isInteger(n) || n <= 0) {
|
|
439
457
|
throw new Error(`ingressInflightMarkerPath requires a positive VM number, got: ${vmNumber}`);
|
|
440
458
|
}
|
|
441
|
-
return `${dir.replace(/\/+$/, '')}
|
|
459
|
+
return `${dir.replace(/\/+$/, '')}/${INFLIGHT_MARKER_BASENAME_PREFIX}${n}`;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Inverse of `ingressInflightMarkerPath`'s basename: given ONE directory entry
|
|
463
|
+
* name, the VM number it is an in-flight marker for, or null if it is not one.
|
|
464
|
+
*
|
|
465
|
+
* This is what lets the dashboard count live wiring tails FLEET-WIDE by scanning
|
|
466
|
+
* the marker directory, rather than per-VM against a roster — the population that
|
|
467
|
+
* matters is exactly the one a roster lags behind, since a batch of adds is when
|
|
468
|
+
* VMs appear and their tails outlive their jobs. That count feeds both the
|
|
469
|
+
* heavy-job concurrency budget and the memory guard's restart refusal.
|
|
470
|
+
*
|
|
471
|
+
* Deliberately strict, because the marker directory is `os.tmpdir()` and the
|
|
472
|
+
* add's own siblings live there too: `cy-ingress-failed-vm-3` (a different
|
|
473
|
+
* marker), `cy-wire-add-vm-5.log` (the tail's log), and any `<marker>.tmp`.
|
|
474
|
+
* Counting one of those as a live tail would charge a phantom slot against a cap
|
|
475
|
+
* of 4 and, worse, hold the memory self-restart off forever. Accepts exactly what
|
|
476
|
+
* the builder emits: a positive integer with no leading zeros and nothing after.
|
|
477
|
+
*
|
|
478
|
+
* Pure; no I/O — the caller does the readdir. */
|
|
479
|
+
function inflightMarkerVmNumber(fileName) {
|
|
480
|
+
if (typeof fileName !== 'string') return null;
|
|
481
|
+
if (!fileName.startsWith(INFLIGHT_MARKER_BASENAME_PREFIX)) return null;
|
|
482
|
+
const rest = fileName.slice(INFLIGHT_MARKER_BASENAME_PREFIX.length);
|
|
483
|
+
if (!/^[1-9][0-9]*$/.test(rest)) return null;
|
|
484
|
+
return Number(rest);
|
|
442
485
|
}
|
|
443
486
|
|
|
444
487
|
/** The per-VM marker file the detached add-tail drops when its converge loop
|
|
@@ -762,6 +805,7 @@ module.exports = {
|
|
|
762
805
|
ingressConvergeDecision,
|
|
763
806
|
detachedAddTailInvocation,
|
|
764
807
|
ingressInflightMarkerPath,
|
|
808
|
+
inflightMarkerVmNumber,
|
|
765
809
|
ingressFailedMarkerPath,
|
|
766
810
|
isWiringInflight,
|
|
767
811
|
iapAccessInvocation,
|
package/npm/launch-config.js
CHANGED
|
@@ -119,6 +119,79 @@ function validateLaunchConfig(cfg) {
|
|
|
119
119
|
return { ok: true };
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** The branch refs a launch config asks git to resolve, as
|
|
123
|
+
* `[{ field, repo, branch }]` — one entry per branch-carrying field that is
|
|
124
|
+
* actually populated. This is the input to a ref-existence preflight; it lives
|
|
125
|
+
* beside normalizeLaunchConfig (rather than in a second parser at the add
|
|
126
|
+
* endpoint or in cloud.js) because it reads the SAME canonical field names that
|
|
127
|
+
* normalizer produces, so a field rename can never leave the two disagreeing.
|
|
128
|
+
*
|
|
129
|
+
* BOTH fields are reported, deliberately. In the 2026-08-25 incident the dead
|
|
130
|
+
* ref sat in `clientBranch` AND `editorBranch`, and it was `clientBranch` that
|
|
131
|
+
* actually killed the bootstrap — an editor-only check would have missed it.
|
|
132
|
+
*
|
|
133
|
+
* A greenfield add (`newProjectName`, no `clientRepo`) has no client repo to
|
|
134
|
+
* resolve a branch against, so its clientBranch is skipped rather than probed
|
|
135
|
+
* against a repo URL that does not exist. `editorRepo` is passed in because the
|
|
136
|
+
* launch config carries an editor BRANCH but not an editor REPO — the repo is
|
|
137
|
+
* the caller's (cloud:up resolves it via resolveEditorConfig; the dashboard
|
|
138
|
+
* holds it in REPO). Pure; no I/O. */
|
|
139
|
+
function branchRefTargets(cfg, { editorRepo } = {}) {
|
|
140
|
+
const c = cfg || {};
|
|
141
|
+
const targets = [];
|
|
142
|
+
if (c.clientRepo && c.clientBranch) {
|
|
143
|
+
targets.push({ field: "clientBranch", repo: c.clientRepo, branch: c.clientBranch });
|
|
144
|
+
}
|
|
145
|
+
if (editorRepo && c.editorBranch) {
|
|
146
|
+
targets.push({ field: "editorBranch", repo: editorRepo, branch: c.editorBranch });
|
|
147
|
+
}
|
|
148
|
+
return targets;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Partition branchRefTargets by whether each ref still resolves, using an
|
|
152
|
+
* INJECTED probe so the decision logic is unit-tested with no network and no
|
|
153
|
+
* git. `refExists({ repo, branch })` returns true when the ref is present,
|
|
154
|
+
* false when it is provably gone.
|
|
155
|
+
*
|
|
156
|
+
* A probe that cannot reach the remote at all must NOT report false — a network
|
|
157
|
+
* or credential fault is not evidence a branch was deleted, and treating it as
|
|
158
|
+
* such would refuse every add the moment GitHub blipped. The probe signals that
|
|
159
|
+
* by returning `null`/`undefined`, which this treats as "unknown, let it
|
|
160
|
+
* through": the classifier arm downstream still catches a genuinely dead ref,
|
|
161
|
+
* at the cost this preflight normally saves. Fail-open is the right default
|
|
162
|
+
* here precisely because the preflight is an OPTIMIZATION over a failure that
|
|
163
|
+
* is already handled, never the only line of defence. Pure; no I/O. */
|
|
164
|
+
function missingBranchRefs(targets, refExists) {
|
|
165
|
+
const missing = [];
|
|
166
|
+
for (const t of targets || []) {
|
|
167
|
+
if (refExists(t) === false) missing.push(t);
|
|
168
|
+
}
|
|
169
|
+
return missing;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Operator-facing copy for a failed branch-ref preflight. States the OUTCOME
|
|
173
|
+
* first (nothing was created), then the mechanism, then the remedy — and never
|
|
174
|
+
* the "a partial instance may exist — reconcile or destroy it" language the
|
|
175
|
+
* generic provisioning failure uses, which is what made the real incident
|
|
176
|
+
* confusing: at preflight time no instance, disk, NEG or backend service has
|
|
177
|
+
* been created, so there is nothing to reconcile and nothing to destroy.
|
|
178
|
+
*
|
|
179
|
+
* Names which FIELD carried the dead ref, because the two are configured in
|
|
180
|
+
* different places and an operator who only corrects one re-issues a request
|
|
181
|
+
* that fails identically. Pure; no I/O. */
|
|
182
|
+
function missingBranchRefMessage(missing) {
|
|
183
|
+
const list = (missing || [])
|
|
184
|
+
.map((m) => `${m.field}: branch '${m.branch}' is not on ${m.repo}`)
|
|
185
|
+
.join("; ");
|
|
186
|
+
return (
|
|
187
|
+
`Branch no longer exists on origin — ${list}. ` +
|
|
188
|
+
"Nothing was provisioned: this is checked before any instance is created, so " +
|
|
189
|
+
"there is no VM, disk or backend service to clean up. This repo prunes branches " +
|
|
190
|
+
"when they merge, so a stored branch name expires silently. Re-add on a live " +
|
|
191
|
+
"branch (the current fleet branch, or any branch that still exists on origin)."
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
|
|
122
195
|
/** Pre-flight the operator-host CONFIG a dashboard Add depends on, BEFORE
|
|
123
196
|
* spawning the ~20-min cloud:up. Pure classifier: it takes the ALREADY-resolved
|
|
124
197
|
* zone, project, and GitHub token (the side-effecting resolution — env lookups,
|
|
@@ -328,6 +401,48 @@ function decideAddSlot({ maxConcurrency, activeCount = 0 } = {}) {
|
|
|
328
401
|
return Number(activeCount) < cap ? "start" : "queue";
|
|
329
402
|
}
|
|
330
403
|
|
|
404
|
+
/** How many concurrency slots the fleet's heavy work is ACTUALLY occupying —
|
|
405
|
+
* the `activeCount` `decideAddSlot` above should be judging against.
|
|
406
|
+
*
|
|
407
|
+
* Two populations, not one. `runningVms` are the VMs with a heavy job in the
|
|
408
|
+
* `running` state; `wiringVms` are the VMs whose Add has already settled at
|
|
409
|
+
* container-healthy but whose DETACHED wire-add-tail is still spawning gcloud
|
|
410
|
+
* children for another 5-10 minutes. Charging only the first is what let a
|
|
411
|
+
* six-VM batch put 32 `gcloud.py compute` children on a 16-vCPU host at load
|
|
412
|
+
* 38.23 against a cap of 4: each Add released its slot the instant the job
|
|
413
|
+
* settled, while its real work ran on invisibly.
|
|
414
|
+
*
|
|
415
|
+
* A VM present in BOTH lists is charged ONCE, and that is the subtle part.
|
|
416
|
+
* `cmdUp` writes the in-flight marker BEFORE the Add settles, so every add has
|
|
417
|
+
* a window where its job is still `running` and its marker already exists;
|
|
418
|
+
* summing the two lists would charge that Add two of the four slots for the
|
|
419
|
+
* overlap. Counting the union instead makes an Add hold exactly one slot
|
|
420
|
+
* continuously across the job -> tail handoff, which is the property the cap
|
|
421
|
+
* needs in both directions — never over-charging during the overlap, never
|
|
422
|
+
* releasing early after it.
|
|
423
|
+
*
|
|
424
|
+
* Non-integer, non-positive, and duplicate entries are dropped from either
|
|
425
|
+
* list, so a caller's readdir glitch or a malformed job key degrades toward
|
|
426
|
+
* under-counting (admitting work) rather than manufacturing a phantom slot
|
|
427
|
+
* that would freeze admission forever.
|
|
428
|
+
*
|
|
429
|
+
* Pure: pulls no globals, has no side effects. */
|
|
430
|
+
function heavyChargeCount({ runningVms = [], wiringVms = [] } = {}) {
|
|
431
|
+
const numbers = (list) => {
|
|
432
|
+
const out = new Set();
|
|
433
|
+
for (const raw of Array.isArray(list) ? list : []) {
|
|
434
|
+
const n = Number(raw);
|
|
435
|
+
if (Number.isInteger(n) && n > 0) out.add(n);
|
|
436
|
+
}
|
|
437
|
+
return out;
|
|
438
|
+
};
|
|
439
|
+
const running = numbers(runningVms);
|
|
440
|
+
const wiring = numbers(wiringVms);
|
|
441
|
+
let untrackedTails = 0;
|
|
442
|
+
for (const n of wiring) if (!running.has(n)) untrackedTails += 1;
|
|
443
|
+
return running.size + untrackedTails;
|
|
444
|
+
}
|
|
445
|
+
|
|
331
446
|
/** Closed set of "heavy" action names — those that invoke `npm run cloud:up`
|
|
332
447
|
* (over gcloud-ssh) and so contend for the shared spawn budget that prevents
|
|
333
448
|
* the dashboard's launchd-managed node process from OOMing under burst. The
|
|
@@ -480,11 +595,42 @@ function recentlyLaunchedWithin(ts, nowMs, graceMs) {
|
|
|
480
595
|
* variant(@branch) fallback.
|
|
481
596
|
* - source "live" when editorIdentity drove editorLabel, else
|
|
482
597
|
* "history" — the affordance the card uses to mark inferred
|
|
483
|
-
* data.
|
|
598
|
+
* data.
|
|
599
|
+
* - deployedBranch the branch the fleet ADVANCED this VM to, from the editor's
|
|
600
|
+
* `editorDeployment` (which reads the record the refresh script
|
|
601
|
+
* writes at install time); null when the editor reports none.
|
|
602
|
+
* - deployedTag the full artifact tag behind that branch, for the hover.
|
|
603
|
+
* - deployedSource "live" when the editor reported a deployment, else
|
|
604
|
+
* "unknown".
|
|
605
|
+
* - deployedState the card's three-way presentation verdict, resolved HERE
|
|
606
|
+
* rather than in the browser so the rule is unit-tested in one
|
|
607
|
+
* place and index.html carries no copy of it (the compromise
|
|
608
|
+
* groupVmsByProject's own comment apologizes for, avoidable
|
|
609
|
+
* here because this value rides in the /data payload):
|
|
610
|
+
* "agrees" deployment and provenance name the same branch
|
|
611
|
+
* "differs" they disagree — the EXPECTED plan-only-retag
|
|
612
|
+
* state, to be shown as a pair, never badged
|
|
613
|
+
* "unknown" no deployment reported
|
|
614
|
+
*
|
|
615
|
+
* editorLabel and deployedBranch answer DIFFERENT questions and are routinely
|
|
616
|
+
* expected to disagree. editorLabel is build provenance — what source the
|
|
617
|
+
* binary was compiled from — and stays exactly as it was; it is the right
|
|
618
|
+
* answer to "what code is this actually running?" and is what fragility #33's
|
|
619
|
+
* fix was built for. deployedBranch is the deployment target. After a plan-only
|
|
620
|
+
* advance, where fleet-advance-to.sh correctly retags the base branch's CI
|
|
621
|
+
* binary, provenance `main` + deployment `editor-improvements-78` is the
|
|
622
|
+
* CORRECT state, so the card must present the pair rather than badge it.
|
|
623
|
+
*
|
|
624
|
+
* A missing deployment resolves to null / "unknown" and is NEVER filled in from
|
|
625
|
+
* cfg.editorBranch, though that value is right there: it is written at
|
|
626
|
+
* reconfigure and is not updated by a binary refresh, so it is stale for
|
|
627
|
+
* precisely this question — the same live-over-history discipline the rest of
|
|
628
|
+
* this function applies, and the same `source` honesty. Pure; no I/O. */
|
|
484
629
|
function resolveCardIdentity(live, historyCfg) {
|
|
485
630
|
const cfg = historyCfg || {};
|
|
486
631
|
const lcp = live && live.clientProject;
|
|
487
632
|
const lei = live && live.editorIdentity;
|
|
633
|
+
const led = live && live.editorDeployment;
|
|
488
634
|
|
|
489
635
|
const historyName = cfg.clientRepo ? repoBasename(cfg.clientRepo) : (cfg.newProjectName || "");
|
|
490
636
|
const project = (lcp && lcp.name) || historyName || null;
|
|
@@ -505,7 +651,24 @@ function resolveCardIdentity(live, historyCfg) {
|
|
|
505
651
|
source = "history";
|
|
506
652
|
}
|
|
507
653
|
|
|
508
|
-
|
|
654
|
+
const deployedBranch = (led && led.branch) || null;
|
|
655
|
+
const deployedTag = (led && led.tag) || null;
|
|
656
|
+
const deployedSource = deployedBranch || deployedTag ? "live" : "unknown";
|
|
657
|
+
let deployedState;
|
|
658
|
+
if (!deployedBranch) deployedState = "unknown";
|
|
659
|
+
else if (deployedBranch === editorLabel) deployedState = "agrees";
|
|
660
|
+
else deployedState = "differs";
|
|
661
|
+
|
|
662
|
+
return {
|
|
663
|
+
project,
|
|
664
|
+
clientBranch,
|
|
665
|
+
editorLabel,
|
|
666
|
+
source,
|
|
667
|
+
deployedBranch,
|
|
668
|
+
deployedTag,
|
|
669
|
+
deployedSource,
|
|
670
|
+
deployedState,
|
|
671
|
+
};
|
|
509
672
|
}
|
|
510
673
|
|
|
511
674
|
/** Sticky-merge a VM poll's `clientProject` against the last-known-good value
|
|
@@ -1173,6 +1336,9 @@ module.exports = {
|
|
|
1173
1336
|
EMPTY_CONFIG,
|
|
1174
1337
|
normalizeLaunchConfig,
|
|
1175
1338
|
validateLaunchConfig,
|
|
1339
|
+
branchRefTargets,
|
|
1340
|
+
missingBranchRefs,
|
|
1341
|
+
missingBranchRefMessage,
|
|
1176
1342
|
addPreflight,
|
|
1177
1343
|
isVariantProvisionable,
|
|
1178
1344
|
isVariantRebuildable,
|
|
@@ -1181,6 +1347,7 @@ module.exports = {
|
|
|
1181
1347
|
cloudUpArgs,
|
|
1182
1348
|
classifyContainerWait,
|
|
1183
1349
|
decideAddSlot,
|
|
1350
|
+
heavyChargeCount,
|
|
1184
1351
|
HEAVY_ACTIONS,
|
|
1185
1352
|
queuedStatusForAction,
|
|
1186
1353
|
pushHistory,
|