@codeyam-editor/codeyam-editor 0.1.0-staging.4d8da9a → 0.1.0-staging.6200ab1
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 +352 -50
- package/npm/disk-pressure.js +17 -0
- package/npm/editor-dev.js +63 -13
- package/npm/editor.js +57 -26
- package/npm/fleet-auth.js +6 -1
- package/npm/fleet-cleanup.js +52 -4
- package/npm/fleet-effect-status.js +33 -32
- package/npm/fleet-host.js +74 -0
- package/npm/fleet-job-badge.js +1 -1
- package/npm/fleet-queue.js +38 -1
- package/npm/fleet-reset.js +117 -7
- package/npm/fleet-roster.js +6 -8
- package/npm/fleet-self-update.js +105 -0
- package/npm/fleet-sleep-status.js +33 -0
- package/npm/google-ingress.js +143 -0
- package/npm/launch-config.js +17 -3
- package/npm/scenario-check.js +363 -14
- package/npm/scenario-handlers.js +54 -2
- package/npm/scenario-metrics.js +37 -0
- package/npm/scenario-mocks.js +20 -0
- package/npm/scenario-playwright.js +236 -29
- package/npm/script-drift.js +81 -0
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-C8EkH4PP.js → ContainerInspector-DhJ-Oh-9.js} +2 -2
- package/ui/dist/assets/EditorShell-08glcZVy.css +1 -0
- package/ui/dist/assets/EditorShell-BPSWpFGH.js +189 -0
- package/ui/dist/assets/ScenarioDataPanel-961YITEd.js +5 -0
- package/ui/dist/assets/{ScenarioDataPanelFullPage-5G6KuZgw.js → ScenarioDataPanelFullPage-DHncFah1.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-zZfej9Vx.js +21 -0
- package/ui/dist/assets/index-DiezX3Bw.css +1 -0
- package/ui/dist/assets/{index-BuK0lizt.js → index-W4-0pn6S.js} +9 -9
- 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/EditorShell-tMMdarZD.js +0 -187
- package/ui/dist/assets/ScenarioDataPanel-xWYGZOpo.js +0 -5
- package/ui/dist/assets/ScenarioRenderer-CSdhX1Ak.js +0 -1
- package/ui/dist/assets/index-BGqairFV.css +0 -1
package/npm/cloud.js
CHANGED
|
@@ -352,11 +352,21 @@ function resolveSeedPreviewOrigin(
|
|
|
352
352
|
_managedPreviewOrigin: MANAGED_PREVIEW_ORIGIN,
|
|
353
353
|
};
|
|
354
354
|
}
|
|
355
|
-
// Quick-tunnel / domain-free mode:
|
|
356
|
-
//
|
|
357
|
-
//
|
|
358
|
-
|
|
359
|
-
|
|
355
|
+
// Quick-tunnel / domain-free mode: support both 'local' (default) and 'tunnel' modes.
|
|
356
|
+
// In 'local' mode, we use http://localhost:<15172 + VM_NUMBER> directly.
|
|
357
|
+
// In 'tunnel' mode, we prefer the operator's published quick-tunnel URL.
|
|
358
|
+
const mode = env.PREVIEW_ORIGIN_MODE || "local";
|
|
359
|
+
if (mode === "local" && vm != null) {
|
|
360
|
+
// http://localhost inside an https:// editor page is allowed.
|
|
361
|
+
// Browsers treat http://localhost as a potentially-trustworthy origin,
|
|
362
|
+
// so the mixed-content blocker does not apply.
|
|
363
|
+
const previewPort = 15172 + vm;
|
|
364
|
+
return {
|
|
365
|
+
previewOrigin: `http://localhost:${previewPort}`,
|
|
366
|
+
_managedPreviewOrigin: MANAGED_PREVIEW_ORIGIN,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
// Quick-tunnel 'tunnel' mode fallback / non-fleet project fallback:
|
|
360
370
|
const previewKey = vm != null ? `${vm}-preview` : null;
|
|
361
371
|
const tunnelUrl =
|
|
362
372
|
previewKey &&
|
|
@@ -411,6 +421,46 @@ function isRottedManagedPreviewOrigin(localJson, sameOriginSafe) {
|
|
|
411
421
|
);
|
|
412
422
|
}
|
|
413
423
|
|
|
424
|
+
// A loopback / localhost previewOrigin: `http://127.0.0.1:<port>`,
|
|
425
|
+
// `http://localhost:<port>`, `http://[::1]:<port>`, with or without a port,
|
|
426
|
+
// path, or query. These are reachable only from the editor host itself (or a
|
|
427
|
+
// laptop `-L` forward), never from a remote browser — exactly the dead-end the
|
|
428
|
+
// dedicated_preview_origin fallback lands on when the seed was wrong-moded.
|
|
429
|
+
const LOOPBACK_ORIGIN_RE =
|
|
430
|
+
/^https?:\/\/(?:127\.0\.0\.1|localhost|\[::1\])(?:[:/?#]|$)/i;
|
|
431
|
+
|
|
432
|
+
/** Decide whether a VM's current `editor.local.json` carries a WRONG-MODE
|
|
433
|
+
* previewOrigin for a stack that is NOT same-origin-safe (Next.js etc.) and so
|
|
434
|
+
* must be self-healed to a dedicated origin. This is the inverse of
|
|
435
|
+
* `isRottedManagedPreviewOrigin`: it fires when the stack.json race froze the
|
|
436
|
+
* same-origin-safe default (or a loopback bootstrap) onto a client-routed
|
|
437
|
+
* stack — the VM7 failure where the iframe shows `127.0.0.1 refused to
|
|
438
|
+
* connect`. True iff BOTH hold:
|
|
439
|
+
* - the stack is NOT same-origin-safe (a same-origin-safe stack legitimately
|
|
440
|
+
* keeps the `/__codeyam_preview` subpath — never heal it; that is
|
|
441
|
+
* `isRottedManagedPreviewOrigin`'s job in the other direction); and
|
|
442
|
+
* - the `previewOrigin` is the same-origin subpath sentinel, any other
|
|
443
|
+
* relative path, or a loopback/localhost origin — none of which a remote
|
|
444
|
+
* browser can load for a dedicated-origin stack. A real dedicated origin
|
|
445
|
+
* (an absolute non-loopback URL — quick-tunnel or named-tunnel host) is
|
|
446
|
+
* left alone, so re-running the heal once a good origin is in place is a
|
|
447
|
+
* no-op.
|
|
448
|
+
* Symmetric with `isRottedManagedPreviewOrigin` and tested as a pair. Pure —
|
|
449
|
+
* `localJson` is a parsed editor.local.json object. */
|
|
450
|
+
function isWrongModePreviewOrigin(localJson, sameOriginSafe) {
|
|
451
|
+
if (sameOriginSafe) return false;
|
|
452
|
+
if (!localJson || typeof localJson !== "object") return false;
|
|
453
|
+
const origin = localJson.previewOrigin;
|
|
454
|
+
if (typeof origin !== "string" || origin.length === 0) return false;
|
|
455
|
+
// The reserved same-origin subpath, or any relative path (no scheme): these
|
|
456
|
+
// recurse into the editor SPA for a client-routed stack.
|
|
457
|
+
if (origin === SAME_ORIGIN_PREVIEW_PATH) return true;
|
|
458
|
+
if (!/^[a-z][a-z0-9+.-]*:\/\//i.test(origin)) return true;
|
|
459
|
+
// An absolute loopback/localhost origin: reachable only from the host, dead
|
|
460
|
+
// for a remote browser.
|
|
461
|
+
return LOOPBACK_ORIGIN_RE.test(origin);
|
|
462
|
+
}
|
|
463
|
+
|
|
414
464
|
/** Read the operator-laptop cloudflared-urls.json sidecar. Missing/unparseable
|
|
415
465
|
* → {} (the monitor may not have run yet; the seed falls back to localhost). */
|
|
416
466
|
function readCloudflaredUrls() {
|
|
@@ -491,9 +541,25 @@ function editorImageBootstrapArgs(cfg) {
|
|
|
491
541
|
* so a stale value — e.g. a prior greenfield's `newProject` left in cloud.json
|
|
492
542
|
* — can't override the new intent. Without this, launching `--repo X` after a
|
|
493
543
|
* greenfield run silently provisions the greenfield project (resolveProject
|
|
494
|
-
* prioritizes newProject). Mutates and returns cfg.
|
|
495
|
-
|
|
496
|
-
|
|
544
|
+
* prioritizes newProject). Mutates and returns cfg.
|
|
545
|
+
*
|
|
546
|
+
* `prev` is the pre-merge persisted config (default `{}`). When `--repo`
|
|
547
|
+
* SWITCHES to a different repo and no `--branch` was passed this run, the
|
|
548
|
+
* inherited client branch is dropped: a persisted branch belongs to the OLD
|
|
549
|
+
* repo and may not exist on the new one. (The harvardintech VM was torn down
|
|
550
|
+
* because a `--repo harvardintech` re-up reused `editor-improvements-48` — a
|
|
551
|
+
* branch that lives only on the editor repo — and `git clone --branch` failed.)
|
|
552
|
+
* An explicit `--branch` this run always wins (the drop is gated on
|
|
553
|
+
* `!flags.branch`), and a same-repo re-up keeps its persisted branch — that is
|
|
554
|
+
* the legitimate "set the branch once, reuse it" workflow. `sameRepo` is used
|
|
555
|
+
* for the comparison so `.git`/case differences don't trigger a spurious drop. */
|
|
556
|
+
function reconcileProjectExclusivity(cfg, flags = {}, prev = {}) {
|
|
557
|
+
if (flags.repo) {
|
|
558
|
+
delete cfg.newProject;
|
|
559
|
+
if (!flags.branch && prev.repo && !sameRepo(prev.repo, flags.repo)) {
|
|
560
|
+
delete cfg.branch;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
497
563
|
if (flags.newProject) { delete cfg.repo; delete cfg.branch; }
|
|
498
564
|
return cfg;
|
|
499
565
|
}
|
|
@@ -546,14 +612,27 @@ function loadCloudConfig(projectDir) {
|
|
|
546
612
|
try {
|
|
547
613
|
return JSON.parse(raw);
|
|
548
614
|
} catch (e) {
|
|
549
|
-
|
|
615
|
+
try {
|
|
616
|
+
raw = fs.readFileSync(p, "utf8");
|
|
617
|
+
return JSON.parse(raw);
|
|
618
|
+
} catch (e2) {
|
|
619
|
+
throw new Error(`Failed to parse ${p}: ${e2.message}`);
|
|
620
|
+
}
|
|
550
621
|
}
|
|
551
622
|
}
|
|
552
623
|
|
|
553
624
|
function saveCloudConfig(projectDir, cfg) {
|
|
554
625
|
const p = cloudConfigPath(projectDir);
|
|
555
|
-
|
|
556
|
-
fs.
|
|
626
|
+
const dir = path.dirname(p);
|
|
627
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
628
|
+
const tmpPath = path.join(dir, `cloud.json.${process.pid}.tmp`);
|
|
629
|
+
try {
|
|
630
|
+
fs.writeFileSync(tmpPath, JSON.stringify(cfg, null, 2) + "\n");
|
|
631
|
+
fs.renameSync(tmpPath, p);
|
|
632
|
+
} catch (e) {
|
|
633
|
+
fs.rmSync(tmpPath, { force: true });
|
|
634
|
+
throw e;
|
|
635
|
+
}
|
|
557
636
|
}
|
|
558
637
|
|
|
559
638
|
/** Build the JSON patch to merge into the VM's `editor.local.json` so the
|
|
@@ -837,7 +916,15 @@ function loadOrInitCloudConfig(projectDir, flags) {
|
|
|
837
916
|
// `cloud:up` (zero-arg, reading cloud.json) would silently skip its tunnel.
|
|
838
917
|
const persist = (c) => { const { noTunnel, ...rest } = c; return rest; };
|
|
839
918
|
if (existing) {
|
|
840
|
-
const merged = reconcileProjectExclusivity({ ...existing, ...flags }, flags);
|
|
919
|
+
const merged = reconcileProjectExclusivity({ ...existing, ...flags }, flags, existing);
|
|
920
|
+
const { isPlaceholderProject } = require("./launch-config");
|
|
921
|
+
if (isPlaceholderProject(merged.project)) {
|
|
922
|
+
const configPath = cloudConfigPath(projectDir);
|
|
923
|
+
throw new Error(
|
|
924
|
+
`Failed to load cloud config: project '${merged.project}' in ${configPath} is a test-fixture placeholder. ` +
|
|
925
|
+
`Please configure a real GCP project or set the GCP_PROJECT environment variable.`
|
|
926
|
+
);
|
|
927
|
+
}
|
|
841
928
|
const toSave = persist(merged);
|
|
842
929
|
if (JSON.stringify(toSave) !== JSON.stringify(existing)) {
|
|
843
930
|
saveCloudConfig(projectDir, toSave);
|
|
@@ -847,6 +934,15 @@ function loadOrInitCloudConfig(projectDir, flags) {
|
|
|
847
934
|
|
|
848
935
|
const cfg = reconcileProjectExclusivity({ ...flags }, flags);
|
|
849
936
|
cfg.project = cfg.project || gcloudGetValue("config-get-project");
|
|
937
|
+
|
|
938
|
+
const { isPlaceholderProject } = require("./launch-config");
|
|
939
|
+
if (isPlaceholderProject(cfg.project)) {
|
|
940
|
+
throw new Error(
|
|
941
|
+
`Failed to load cloud config: resolved project '${cfg.project}' is a test-fixture placeholder. ` +
|
|
942
|
+
`Please configure a real GCP project or set the GCP_PROJECT environment variable.`
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
850
946
|
cfg.zone = cfg.zone || gcloudGetValue("config-get-zone");
|
|
851
947
|
cfg.agent = cfg.agent || "claude";
|
|
852
948
|
agentEnvVarName(cfg.agent);
|
|
@@ -1651,27 +1747,43 @@ async function seedPreviewOrigin(cfg, appPort = VITE_PORT) {
|
|
|
1651
1747
|
);
|
|
1652
1748
|
|
|
1653
1749
|
// Skip-on-already-set guard, EXCEPT for self-heal. A non-empty previewOrigin
|
|
1654
|
-
// is normally a user pin or a good prior seed and is left untouched.
|
|
1655
|
-
//
|
|
1656
|
-
//
|
|
1657
|
-
//
|
|
1658
|
-
//
|
|
1659
|
-
//
|
|
1660
|
-
//
|
|
1661
|
-
//
|
|
1750
|
+
// is normally a user pin or a good prior seed and is left untouched. There are
|
|
1751
|
+
// TWO symmetric heal exceptions, both of which the frozen skip-guard would
|
|
1752
|
+
// otherwise pin forever:
|
|
1753
|
+
// 1. a ROTTED managed cloudflared quick-tunnel URL on a same-origin-safe VM
|
|
1754
|
+
// (the VM-1/2/6 leftover from before the improve38 same-origin
|
|
1755
|
+
// migration): that frozen URL points at a dead rotated tunnel (NXDOMAIN /
|
|
1756
|
+
// blank preview), so we heal it back to the same-origin path; and
|
|
1757
|
+
// 2. a WRONG-MODE previewOrigin on a NOT-same-origin-safe VM (the VM7 race:
|
|
1758
|
+
// the first seed ran before onboarding wrote stack.json, defaulted to
|
|
1759
|
+
// `sameOriginSafe: true`, and seeded `/__codeyam_preview` — which a
|
|
1760
|
+
// remote browser then loads as `127.0.0.1 refused to connect`). Once
|
|
1761
|
+
// stack.json exists, heal it forward to the dedicated origin.
|
|
1762
|
+
// Because every dashboard action (Reset / Update / Add / bootstrap-recovery)
|
|
1763
|
+
// re-runs this seed over cloud:up, either heal reaches every touch-point the
|
|
1764
|
+
// moment stack.json is present — closing the race without re-ordering
|
|
1765
|
+
// provisioning, and without a separate per-VM hand-fix.
|
|
1662
1766
|
const originAlreadySet =
|
|
1663
1767
|
typeof current.previewOrigin === "string" && current.previewOrigin.length > 0;
|
|
1664
1768
|
const healing =
|
|
1665
|
-
originAlreadySet &&
|
|
1769
|
+
originAlreadySet &&
|
|
1770
|
+
(isRottedManagedPreviewOrigin(current, sameOriginSafe) ||
|
|
1771
|
+
isWrongModePreviewOrigin(current, sameOriginSafe));
|
|
1666
1772
|
if (originAlreadySet && !healing) {
|
|
1667
1773
|
return;
|
|
1668
1774
|
}
|
|
1669
1775
|
|
|
1670
|
-
// On heal,
|
|
1671
|
-
//
|
|
1672
|
-
//
|
|
1673
|
-
// the
|
|
1674
|
-
|
|
1776
|
+
// On heal, reconcile the `_managedPreviewOrigin` marker to the healed value's
|
|
1777
|
+
// own shape. The rotted→same-origin heal (seed carries NO marker) DROPS any
|
|
1778
|
+
// stale marker (deepMergeJson deletes a key whose overlay value is null) — the
|
|
1779
|
+
// constant same-origin path has nothing for the monitor to manage, exactly
|
|
1780
|
+
// matching the manual `config-override --unset _managedPreviewOrigin` fix. The
|
|
1781
|
+
// wrong-mode→dedicated heal (seed carries the marker) KEEPS it — that origin
|
|
1782
|
+
// IS managed (the monitor's gated rotation push owns it), so the marker must
|
|
1783
|
+
// survive so the next respawn recognises the value as ours.
|
|
1784
|
+
const overlay = healing
|
|
1785
|
+
? { ...seed, _managedPreviewOrigin: seed._managedPreviewOrigin ?? null }
|
|
1786
|
+
: seed;
|
|
1675
1787
|
console.error(
|
|
1676
1788
|
healing
|
|
1677
1789
|
? `Healing rotted managed previewOrigin (${current.previewOrigin}) → ${seed.previewOrigin} in ${remotePath}...`
|
|
@@ -1691,6 +1803,82 @@ async function seedPreviewOrigin(cfg, appPort = VITE_PORT) {
|
|
|
1691
1803
|
}
|
|
1692
1804
|
}
|
|
1693
1805
|
|
|
1806
|
+
/** READY-probe auto-heal: after the editor container is verifiably Up, ask the
|
|
1807
|
+
* running editor's own `/api/config` for its stack verdict and effective
|
|
1808
|
+
* preview origin, and heal a wrong-moded seed in place — never failing the add.
|
|
1809
|
+
*
|
|
1810
|
+
* The race this closes (VM7, 2026-06-10): the pre-container `seedPreviewOrigin`
|
|
1811
|
+
* at line ~1906 runs BEFORE onboarding writes stack.json, so it defaults to
|
|
1812
|
+
* `sameOriginSafe: true` and seeds the `/__codeyam_preview` subpath. For a
|
|
1813
|
+
* client-routed stack (Next.js) the editor then reports `previewSameOriginSafe:
|
|
1814
|
+
* false` but, with only the subpath in editor.local.json, falls its
|
|
1815
|
+
* `dedicated_preview_origin` back to `http://127.0.0.1:<appPort>` — which a
|
|
1816
|
+
* remote browser renders as "refused to connect". If stack.json appeared since
|
|
1817
|
+
* the first seed (the editor reads it live from disk), we re-run the seed — now
|
|
1818
|
+
* the wrong-mode heal fires (`isWrongModePreviewOrigin`) — and POST
|
|
1819
|
+
* `/api/editor-reload-config` so the running server serves the healed origin
|
|
1820
|
+
* with no restart.
|
|
1821
|
+
*
|
|
1822
|
+
* Best-effort and non-fatal by contract: a probe failure, a still-absent
|
|
1823
|
+
* stack.json (verdict stays `true`), or a heal that can only reach a loopback
|
|
1824
|
+
* bootstrap origin (the operator's `<N>-preview` tunnel not published yet) all
|
|
1825
|
+
* leave the add green. The cloudflared-monitor's periodic tick (section 2) is
|
|
1826
|
+
* the durable owner that heals post-add flips — onboarding finishing minutes
|
|
1827
|
+
* after READY. */
|
|
1828
|
+
async function healPreviewOriginAtReady(cfg, appPort = VITE_PORT) {
|
|
1829
|
+
const probe = runGcloud(
|
|
1830
|
+
buildGcloudArgs("ssh-probe", cfg, {
|
|
1831
|
+
command: `curl -fsS --max-time 5 http://localhost:${EDITOR_PORT}/api/config 2>/dev/null || echo '{}'`,
|
|
1832
|
+
}),
|
|
1833
|
+
{ stdio: ["ignore", "pipe", "ignore"] },
|
|
1834
|
+
);
|
|
1835
|
+
if (probe.status !== 0 || typeof probe.stdout !== "string") {
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
let config;
|
|
1839
|
+
try {
|
|
1840
|
+
config = JSON.parse(probe.stdout);
|
|
1841
|
+
} catch {
|
|
1842
|
+
return;
|
|
1843
|
+
}
|
|
1844
|
+
if (!config || typeof config !== "object") return;
|
|
1845
|
+
|
|
1846
|
+
// The editor's own verdict (section 3's `/api/config` field) plus the
|
|
1847
|
+
// wrong-mode predicate (a subpath / relative / loopback origin on a
|
|
1848
|
+
// not-same-origin-safe stack) — the exact dead-end the dedicated-origin
|
|
1849
|
+
// fallback lands on. Same-origin-safe stacks (verdict true) never enter here.
|
|
1850
|
+
if (config.previewSameOriginSafe !== false) return;
|
|
1851
|
+
if (!isWrongModePreviewOrigin(config, false)) return;
|
|
1852
|
+
|
|
1853
|
+
console.error(
|
|
1854
|
+
`READY auto-heal: ${cfg.instanceName} reports a not-same-origin-safe stack ` +
|
|
1855
|
+
`but a wrong-mode previewOrigin (${config.previewOrigin}); re-seeding...`,
|
|
1856
|
+
);
|
|
1857
|
+
// Re-run the seed: stack.json is now readable, so the wrong-mode heal writes
|
|
1858
|
+
// the dedicated origin into editor.local.json (a published `<N>-preview`
|
|
1859
|
+
// tunnel URL, else the localhost bootstrap for the window before the monitor's
|
|
1860
|
+
// first tick).
|
|
1861
|
+
await seedPreviewOrigin(cfg, appPort);
|
|
1862
|
+
// Tell the running server to re-read editor.local.json — no restart (the
|
|
1863
|
+
// footgun that killed VM7's live Claude session).
|
|
1864
|
+
const reload = runGcloud(
|
|
1865
|
+
buildGcloudArgs("ssh-probe", cfg, {
|
|
1866
|
+
command: `curl -fsS --max-time 5 -X POST http://localhost:${EDITOR_PORT}/api/editor-reload-config 2>/dev/null || true`,
|
|
1867
|
+
}),
|
|
1868
|
+
{ stdio: ["ignore", "inherit", "inherit"] },
|
|
1869
|
+
);
|
|
1870
|
+
if (reload.status !== 0) {
|
|
1871
|
+
console.error(
|
|
1872
|
+
` Warning: heal wrote editor.local.json but /api/editor-reload-config did ` +
|
|
1873
|
+
`not return cleanly — the next monitor tick or cloud:up touch-point will reconcile.`,
|
|
1874
|
+
);
|
|
1875
|
+
}
|
|
1876
|
+
console.error(
|
|
1877
|
+
` Live Preview origin healed. If the operator <N>-preview tunnel is not ` +
|
|
1878
|
+
`published yet, the cloudflared-monitor's periodic tick provides the ongoing heal.`,
|
|
1879
|
+
);
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1694
1882
|
/** Write the chosen agent provider and local API keys into editor.local.json on
|
|
1695
1883
|
* the VM BEFORE compose up so the file exists when the bind-mount happens —
|
|
1696
1884
|
* the provider seed MUST run before ensureEditorContainerUp, because the editor
|
|
@@ -1709,9 +1897,12 @@ async function seedAgentApiKey(cfg) {
|
|
|
1709
1897
|
if (!hasKey) {
|
|
1710
1898
|
console.error(
|
|
1711
1899
|
`\nNo ${agentEnvVarName(cfg.agent)} set locally — seeding provider only.\n` +
|
|
1712
|
-
`Authenticate ${cfg.agent}
|
|
1713
|
-
`
|
|
1714
|
-
|
|
1900
|
+
`Authenticate ${cfg.agent} on the VM by either:\n` +
|
|
1901
|
+
` • exporting ${agentEnvVarName(cfg.agent)} locally and re-running cloud:up, or\n` +
|
|
1902
|
+
` • pasting the key into the Build tab's auth gate in the editor.\n` +
|
|
1903
|
+
`Either path now flows the key through to the agent process on the VM.\n` +
|
|
1904
|
+
`Interactive browser-based OAuth login (e.g. \`codex login\`) is not yet ` +
|
|
1905
|
+
`supported headlessly on the VM.\n`,
|
|
1715
1906
|
);
|
|
1716
1907
|
}
|
|
1717
1908
|
const repoName = resolveProject(cfg).name;
|
|
@@ -1787,22 +1978,18 @@ async function ensureAgentCliInstalled(cfg) {
|
|
|
1787
1978
|
`AGENT="${agent}"`,
|
|
1788
1979
|
"",
|
|
1789
1980
|
"if command -v \"$BINARY\" >/dev/null 2>&1; then",
|
|
1790
|
-
"
|
|
1791
|
-
"
|
|
1792
|
-
" echo \" ✓ $BINARY already installed and executable at /usr/local/bin/claude\"",
|
|
1793
|
-
" exit 0",
|
|
1794
|
-
" fi",
|
|
1795
|
-
" else",
|
|
1796
|
-
" echo \" ✓ $BINARY already installed on PATH\"",
|
|
1797
|
-
" exit 0",
|
|
1798
|
-
" fi",
|
|
1981
|
+
" echo \" ✓ $BINARY already installed on PATH\"",
|
|
1982
|
+
" exit 0",
|
|
1799
1983
|
"fi",
|
|
1800
1984
|
"",
|
|
1801
1985
|
"echo \"Installing $PACKAGE globally in container...\"",
|
|
1802
1986
|
"if [ \"$AGENT\" = \"claude\" ]; then",
|
|
1803
1987
|
" rm -rf /usr/lib/node_modules/@anthropic-ai/.claude-code-*",
|
|
1804
1988
|
" npm install -g \"$PACKAGE\"",
|
|
1805
|
-
"
|
|
1989
|
+
" RESOLVED=$(find /usr/lib/node_modules/@anthropic-ai -name claude -type f -path '*linux-x64*' 2>/dev/null | head -1)",
|
|
1990
|
+
" if [ -n \"$RESOLVED\" ]; then",
|
|
1991
|
+
" ln -sf \"$RESOLVED\" /usr/local/bin/claude",
|
|
1992
|
+
" fi",
|
|
1806
1993
|
"else",
|
|
1807
1994
|
" npm install -g \"$PACKAGE\"",
|
|
1808
1995
|
"fi",
|
|
@@ -1864,19 +2051,117 @@ function promptForInstanceName(expected) {
|
|
|
1864
2051
|
});
|
|
1865
2052
|
}
|
|
1866
2053
|
|
|
2054
|
+
const STREAMED_SCRIPTS = ["scripts/gcp-bootstrap.sh", "scripts/gcp-project-bootstrap.sh"];
|
|
2055
|
+
|
|
2056
|
+
/** Check if the streamed bootstrap scripts have drifted from the remote repository
|
|
2057
|
+
* and abort provisioning if they are out of date to prevent silent container/VM failures. */
|
|
2058
|
+
function checkScriptDrift(cfg) {
|
|
2059
|
+
if (process.env.CODEYAM_SKIP_SCRIPT_DRIFT_CHECK === "1") {
|
|
2060
|
+
console.error(" [Warning] CODEYAM_SKIP_SCRIPT_DRIFT_CHECK=1 active. Skipping streamed-script drift preflight check.");
|
|
2061
|
+
return;
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
const { editorBranch = "main" } = resolveEditorConfig(cfg);
|
|
2065
|
+
|
|
2066
|
+
console.error(`Checking streamed-script drift against origin/${editorBranch}...`);
|
|
2067
|
+
|
|
2068
|
+
// 1. git fetch origin <editorBranch>
|
|
2069
|
+
const fetchResult = spawnSync("git", ["fetch", "origin", editorBranch], { encoding: "utf8" });
|
|
2070
|
+
if (fetchResult.status !== 0) {
|
|
2071
|
+
console.error(` [Warning] Failed to fetch origin ${editorBranch} (${(fetchResult.stderr || "").trim() || "unknown error"}). Skipping drift check.`);
|
|
2072
|
+
return;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
// 2. Find the merge base between HEAD and origin/<editorBranch>
|
|
2076
|
+
const mbResult = spawnSync("git", ["merge-base", "HEAD", `origin/${editorBranch}`], { encoding: "utf8" });
|
|
2077
|
+
let mergeBase = null;
|
|
2078
|
+
if (mbResult.status === 0) {
|
|
2079
|
+
mergeBase = mbResult.stdout.trim();
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
const filesData = [];
|
|
2083
|
+
|
|
2084
|
+
for (const scriptPath of STREAMED_SCRIPTS) {
|
|
2085
|
+
// a. localHash
|
|
2086
|
+
const localHashResult = spawnSync("git", ["hash-object", scriptPath], { encoding: "utf8" });
|
|
2087
|
+
if (localHashResult.status !== 0) {
|
|
2088
|
+
console.error(` [Warning] Failed to compute local hash for ${scriptPath}. Skipping drift check.`);
|
|
2089
|
+
return;
|
|
2090
|
+
}
|
|
2091
|
+
const localHash = localHashResult.stdout.trim();
|
|
2092
|
+
|
|
2093
|
+
// b. remoteHash (from origin/<editorBranch>)
|
|
2094
|
+
let remoteHash = null;
|
|
2095
|
+
const remoteHashResult = spawnSync("git", ["rev-parse", `origin/${editorBranch}:${scriptPath}`], { encoding: "utf8" });
|
|
2096
|
+
if (remoteHashResult.status === 0) {
|
|
2097
|
+
remoteHash = remoteHashResult.stdout.trim();
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
// c. baseHash (from mergeBase)
|
|
2101
|
+
let baseHash = null;
|
|
2102
|
+
if (mergeBase) {
|
|
2103
|
+
const baseHashResult = spawnSync("git", ["rev-parse", `${mergeBase}:${scriptPath}`], { encoding: "utf8" });
|
|
2104
|
+
if (baseHashResult.status === 0) {
|
|
2105
|
+
baseHash = baseHashResult.stdout.trim();
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
// d. dirty status
|
|
2110
|
+
const statusResult = spawnSync("git", ["status", "--porcelain", scriptPath], { encoding: "utf8" });
|
|
2111
|
+
const dirty = statusResult.status === 0 && statusResult.stdout.trim() !== "";
|
|
2112
|
+
|
|
2113
|
+
filesData.push({
|
|
2114
|
+
path: scriptPath,
|
|
2115
|
+
localHash,
|
|
2116
|
+
remoteHash,
|
|
2117
|
+
baseHash,
|
|
2118
|
+
dirty
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
|
|
2122
|
+
const { classifyScriptDrift } = require("./script-drift");
|
|
2123
|
+
const verdictResult = classifyScriptDrift({ files: filesData });
|
|
2124
|
+
|
|
2125
|
+
if (verdictResult.verdict === "stale" || verdictResult.verdict === "diverged") {
|
|
2126
|
+
console.error("");
|
|
2127
|
+
console.error("================================================================================");
|
|
2128
|
+
console.error(`✗ ERROR: Streamed scripts have drifted from origin/${editorBranch}!`);
|
|
2129
|
+
console.error(` Verdict: ${verdictResult.verdict.toUpperCase()}`);
|
|
2130
|
+
console.error(" The following files are out of date on your machine:");
|
|
2131
|
+
for (const f of verdictResult.staleFiles) {
|
|
2132
|
+
console.error(` - ${f}`);
|
|
2133
|
+
}
|
|
2134
|
+
console.error("");
|
|
2135
|
+
console.error(" To proceed safely, please update your local checkout first:");
|
|
2136
|
+
console.error(` git pull --no-rebase origin ${editorBranch}`);
|
|
2137
|
+
console.error("");
|
|
2138
|
+
console.error(" If you must bypass this check (emergency provisioning), set:");
|
|
2139
|
+
console.error(" CODEYAM_SKIP_SCRIPT_DRIFT_CHECK=1");
|
|
2140
|
+
console.error("================================================================================");
|
|
2141
|
+
console.error("");
|
|
2142
|
+
process.exit(1);
|
|
2143
|
+
} else if (verdictResult.verdict === "local-ahead") {
|
|
2144
|
+
console.error(" [Warning] Streaming locally-modified scripts to VM (developer mode).");
|
|
2145
|
+
} else {
|
|
2146
|
+
console.error(" ✓ Streamed scripts are in sync with remote.");
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
|
|
1867
2150
|
// =============================================================================
|
|
1868
2151
|
// Subcommand handlers
|
|
1869
2152
|
// =============================================================================
|
|
1870
2153
|
|
|
1871
|
-
async function cmdUp(args) {
|
|
2154
|
+
async function cmdUp(args, projectDir = rootDir) {
|
|
1872
2155
|
ensureGcloud();
|
|
1873
2156
|
const flags = parseFlags(args);
|
|
1874
|
-
const cfg = loadOrInitCloudConfig(
|
|
2157
|
+
const cfg = loadOrInitCloudConfig(projectDir, flags);
|
|
1875
2158
|
// Fail loud BEFORE creating a GCE instance if an unprovisionable editor
|
|
1876
2159
|
// variant was requested — otherwise we'd spin up a VM, build a dev image,
|
|
1877
2160
|
// and silently ignore the operator's staging/production intent.
|
|
1878
2161
|
assertVariantProvisionable(resolveEditorConfig(cfg).editorVariant);
|
|
1879
2162
|
|
|
2163
|
+
checkScriptDrift(cfg);
|
|
2164
|
+
|
|
1880
2165
|
await ensureFirewallRule(cfg);
|
|
1881
2166
|
const { createdThisRun } = await ensureInstance(cfg);
|
|
1882
2167
|
|
|
@@ -1904,6 +2189,18 @@ async function cmdUp(args) {
|
|
|
1904
2189
|
// us — a run reported success with no container). A failure here is a loud
|
|
1905
2190
|
// non-zero exit, which the dashboard fail-fast-on-nonzero path acts on.
|
|
1906
2191
|
await assertEditorContainerRunning(cfg);
|
|
2192
|
+
// Now that the editor answers on its own surface, ask it whether the
|
|
2193
|
+
// pre-container seed wrong-moded the previewOrigin (the stack.json race) and
|
|
2194
|
+
// heal it in place. Best-effort, never fatal — wrapped so a probe hiccup
|
|
2195
|
+
// can't abort an otherwise-green add.
|
|
2196
|
+
try {
|
|
2197
|
+
await healPreviewOriginAtReady(cfg, appPort);
|
|
2198
|
+
} catch (healErr) {
|
|
2199
|
+
console.error(
|
|
2200
|
+
` Warning: READY preview-origin auto-heal probe failed (${healErr.message}); ` +
|
|
2201
|
+
`the cloudflared-monitor tick will reconcile.`,
|
|
2202
|
+
);
|
|
2203
|
+
}
|
|
1907
2204
|
} catch (err) {
|
|
1908
2205
|
if (createdThisRun) {
|
|
1909
2206
|
console.error(
|
|
@@ -1940,10 +2237,10 @@ async function cmdUp(args) {
|
|
|
1940
2237
|
attachKeepAliveAndTeardown(tunnelChild);
|
|
1941
2238
|
}
|
|
1942
2239
|
|
|
1943
|
-
async function cmdTunnel(args) {
|
|
2240
|
+
async function cmdTunnel(args, projectDir = rootDir) {
|
|
1944
2241
|
ensureGcloud();
|
|
1945
2242
|
const flags = parseFlags(args);
|
|
1946
|
-
const existing = loadCloudConfig(
|
|
2243
|
+
const existing = loadCloudConfig(projectDir);
|
|
1947
2244
|
if (!existing) {
|
|
1948
2245
|
throw new Error(
|
|
1949
2246
|
"No .codeyam/cloud.json found. Run `npm run cloud:up -- --repo <url>` first.",
|
|
@@ -1971,10 +2268,10 @@ async function cmdTunnel(args) {
|
|
|
1971
2268
|
attachKeepAliveAndTeardown(tunnelChild);
|
|
1972
2269
|
}
|
|
1973
2270
|
|
|
1974
|
-
async function cmdStop(args) {
|
|
2271
|
+
async function cmdStop(args, projectDir = rootDir) {
|
|
1975
2272
|
ensureGcloud();
|
|
1976
2273
|
parseFlags(args);
|
|
1977
|
-
const cfg = loadCloudConfig(
|
|
2274
|
+
const cfg = loadCloudConfig(projectDir);
|
|
1978
2275
|
if (!cfg) {
|
|
1979
2276
|
throw new Error("No .codeyam/cloud.json found. Nothing to stop.");
|
|
1980
2277
|
}
|
|
@@ -1990,10 +2287,10 @@ async function cmdStop(args) {
|
|
|
1990
2287
|
);
|
|
1991
2288
|
}
|
|
1992
2289
|
|
|
1993
|
-
async function cmdDestroy(args) {
|
|
2290
|
+
async function cmdDestroy(args, projectDir = rootDir) {
|
|
1994
2291
|
ensureGcloud();
|
|
1995
2292
|
parseFlags(args);
|
|
1996
|
-
const cfg = loadCloudConfig(
|
|
2293
|
+
const cfg = loadCloudConfig(projectDir);
|
|
1997
2294
|
if (!cfg) {
|
|
1998
2295
|
throw new Error("No .codeyam/cloud.json found. Nothing to destroy.");
|
|
1999
2296
|
}
|
|
@@ -2027,17 +2324,17 @@ async function cmdDestroy(args) {
|
|
|
2027
2324
|
}
|
|
2028
2325
|
|
|
2029
2326
|
try {
|
|
2030
|
-
fs.unlinkSync(cloudConfigPath(
|
|
2327
|
+
fs.unlinkSync(cloudConfigPath(projectDir));
|
|
2031
2328
|
console.error("Deleted .codeyam/cloud.json");
|
|
2032
2329
|
} catch (e) {
|
|
2033
2330
|
if (e.code !== "ENOENT") throw e;
|
|
2034
2331
|
}
|
|
2035
2332
|
}
|
|
2036
2333
|
|
|
2037
|
-
async function cmdStatus(args) {
|
|
2334
|
+
async function cmdStatus(args, projectDir = rootDir) {
|
|
2038
2335
|
ensureGcloud();
|
|
2039
2336
|
parseFlags(args);
|
|
2040
|
-
const cfg = loadCloudConfig(
|
|
2337
|
+
const cfg = loadCloudConfig(projectDir);
|
|
2041
2338
|
if (!cfg) {
|
|
2042
2339
|
console.error("No .codeyam/cloud.json found. Run `npm run cloud:up` first.");
|
|
2043
2340
|
return;
|
|
@@ -2131,6 +2428,7 @@ module.exports = {
|
|
|
2131
2428
|
resolveSeedPreviewOrigin,
|
|
2132
2429
|
previewSameOriginSafeFromStack,
|
|
2133
2430
|
isRottedManagedPreviewOrigin,
|
|
2431
|
+
isWrongModePreviewOrigin,
|
|
2134
2432
|
resolveEditorConfig,
|
|
2135
2433
|
validateEditorVariant,
|
|
2136
2434
|
assertVariantProvisionable,
|
|
@@ -2143,9 +2441,11 @@ module.exports = {
|
|
|
2143
2441
|
cloudConfigPath,
|
|
2144
2442
|
loadCloudConfig,
|
|
2145
2443
|
saveCloudConfig,
|
|
2444
|
+
loadOrInitCloudConfig,
|
|
2146
2445
|
buildEditorLocalEnvPatch,
|
|
2147
2446
|
buildEditorLocalSeedWriteCommand,
|
|
2148
2447
|
seedPreviewOrigin,
|
|
2448
|
+
healPreviewOriginAtReady,
|
|
2149
2449
|
buildGcloudArgs,
|
|
2150
2450
|
sshScriptCommand,
|
|
2151
2451
|
bootstrapRunnerScript,
|
|
@@ -2167,6 +2467,8 @@ module.exports = {
|
|
|
2167
2467
|
ensureInstance,
|
|
2168
2468
|
ensureBootstrapped,
|
|
2169
2469
|
assertEditorContainerRunning,
|
|
2470
|
+
checkScriptDrift,
|
|
2471
|
+
STREAMED_SCRIPTS,
|
|
2170
2472
|
// side-effecting handlers
|
|
2171
2473
|
cmdUp,
|
|
2172
2474
|
cmdTunnel,
|
package/npm/disk-pressure.js
CHANGED
|
@@ -80,10 +80,27 @@ function parseDfUsedPct(dfOutput) {
|
|
|
80
80
|
return null;
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
+
// Classify a disk-guard sidecar age (updatedAt vs current time).
|
|
84
|
+
// If the sidecar is older than 600s (10 min), classify it as stale.
|
|
85
|
+
function classifySidecarAge(updatedAt, currentEpoch) {
|
|
86
|
+
if (typeof updatedAt !== 'number' || typeof currentEpoch !== 'number') return 'stale';
|
|
87
|
+
const age = currentEpoch - updatedAt;
|
|
88
|
+
if (age > 600) return 'stale';
|
|
89
|
+
return 'healthy';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Map or classify a general disk state level including 'unknown-exec-failed'
|
|
93
|
+
function mapDiskStateLevel(usedPct, hasExecError) {
|
|
94
|
+
if (hasExecError) return 'unknown-exec-failed';
|
|
95
|
+
return classifyDiskPressure(usedPct);
|
|
96
|
+
}
|
|
97
|
+
|
|
83
98
|
module.exports = {
|
|
84
99
|
DISK_WARN_PCT,
|
|
85
100
|
DISK_CRITICAL_PCT,
|
|
86
101
|
PRUNABLE_CACHE_DIRS,
|
|
87
102
|
classifyDiskPressure,
|
|
88
103
|
parseDfUsedPct,
|
|
104
|
+
classifySidecarAge,
|
|
105
|
+
mapDiskStateLevel,
|
|
89
106
|
};
|