@codeyam-editor/codeyam-editor 0.1.12-staging.g83e4a0c → 0.1.12

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.
@@ -84,6 +84,13 @@ function buildVmEnvelope(v, ctx) {
84
84
  step: v.step != null ? v.step : null,
85
85
  mode: v.mode || null,
86
86
  label: v.label || null,
87
+ // What the last closed cycle was, { feature, closedAt }, from the editor's
88
+ // `resumeMode: "completed"` shape. Null when the cycle is open, when it
89
+ // closed through `assist-complete` (which writes no marker), or on an editor
90
+ // that predates the field. The card names the finished feature from it.
91
+ cycleClosed: v.cycleClosed
92
+ ? { feature: v.cycleClosed.feature || null, closedAt: v.cycleClosed.closedAt || null }
93
+ : null,
87
94
  status: ctx.status,
88
95
  orphan: ctx.orphan || null,
89
96
  degradedReasons: ctx.degradedReasons || [],
@@ -184,7 +191,7 @@ function buildVmEnvelope(v, ctx) {
184
191
  // if you do one without the other.
185
192
  const REQUIRED_CARD_FIELDS = [
186
193
  'n', 'url', 'previewUrl', 'appPort', 'previewServing', 'previewIngress', 'reachable', 'reachableSource', 'hasSession', 'brokerLive', 'brokerStatus',
187
- 'awaitingInput', 'needsCleanup', 'feature', 'selectedPlan', 'step', 'mode', 'label', 'status', 'orphan',
194
+ 'awaitingInput', 'needsCleanup', 'feature', 'selectedPlan', 'step', 'mode', 'label', 'cycleClosed', 'status', 'orphan',
188
195
  'degradedReasons', 'setupSteps', 'attentionReasons',
189
196
  'isHead', 'headHolderUnreachable', 'queuePos', 'queueLen',
190
197
  'job', 'config', 'project', 'variant', 'cardIdentity', 'brokerHistory',
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const faf = require('./fleet-add-failure');
4
+
3
5
  // npm/fleet-job-badge.js — pure verdict for the fleet dashboard's failed-job
4
6
  // badge reconciliation (improve39). A VM's last terminal-error heavy job
5
7
  // (add/error, reset-all error, reset-client error, …) used to stick on the
@@ -96,4 +98,33 @@ function canClearBadge({ rostered, job } = {}) {
96
98
  return { ok: true, reason: null, cleanup: 'badge-only' };
97
99
  }
98
100
 
99
- module.exports = { shouldReconcileFailedBadge, shouldClearReconciledBadge, canClearBadge };
101
+ // The short label the card prints ahead of a terminal job's headline — the
102
+ // "<action> <state>" prefix, derived from the classifier's verdict rather than
103
+ // from the coarse `job.state` alone.
104
+ //
105
+ // job the carried job record ({ action, state, category, ... })
106
+ // wiringDeadlineBlown the add's wiring tail gave up (the calm-hold release)
107
+ //
108
+ // A terminal add error is NOT one fact. The classifier (npm/fleet-add-failure.js)
109
+ // splits it into outcomes PROVEN bad — which keep "add failed" — and the calm
110
+ // CALM_ADD_CATEGORIES side, where nothing proves the VM dead. Printing "add error"
111
+ // on the calm side produced a card contradicting itself: "add error · Add outcome
112
+ // unconfirmed", on a branch that is deliberately not retryable, so it named a
113
+ // fault and offered no fix (three healthy VMs, 2026-09-28). The calm label is
114
+ // gated on holdConvergingBadge — the SAME predicate deriveStatus uses to pick
115
+ // add-verifying/add-converging over add-failed — so the label and the status dot
116
+ // can never disagree about whether this is a failure.
117
+ //
118
+ // Only the LABEL changes. `job.state` stays 'error', so canRetryAdd,
119
+ // canClearBadge and shouldReconcileFailedBadge keep gating on it unchanged.
120
+ function jobBadgeLabel({ job, wiringDeadlineBlown } = {}) {
121
+ if (!job) return '';
122
+ if (job.action === 'add' && job.state === 'error') {
123
+ return faf.holdConvergingBadge({ category: job.category, wiringDeadlineBlown })
124
+ ? 'add unconfirmed'
125
+ : 'add failed';
126
+ }
127
+ return `${job.action} ${job.state}`;
128
+ }
129
+
130
+ module.exports = { shouldReconcileFailedBadge, shouldClearReconciledBadge, canClearBadge, jobBadgeLabel };
@@ -31,6 +31,17 @@
31
31
  // mid-feature post-restart agent is not terminal (`needsCleanup` falsy) and stays
32
32
  // 'building', preserving the 2026-06-20 decoupled-agent fix above.
33
33
  //
34
+ // The closed-cycle refinement (fleet--fix-a-finished-vm-is-never-flagged): both
35
+ // `feature-complete` and `assist-complete` delete `editor-step.json` but keep the
36
+ // saved build-agent session, so /api/session-info answers with the
37
+ // `resumeMode: "completed"` shape — `hasSession: true`, no awaiting flag, no
38
+ // `needsCleanup`. That shape used to fall straight into the `hasSession ⇒
39
+ // 'building'` line, so a finished VM read "building" forever and was never
40
+ // flagged. We key on `resumeMode`, not on the `cycleClosed` marker, because
41
+ // only `feature-complete` writes the marker. No broker check is needed to tell
42
+ // a parked agent from a working one: the moment the agent takes a new request
43
+ // it enters triage, which writes step state and retires the completed shape.
44
+ //
34
45
  // Returns 'waiting' | 'building' | 'session-done' | 'idle' for a reachable VM, or
35
46
  // `null` when the VM is unreachable — the caller maps null to its own
36
47
  // asleep/unreachable verdict (it owns the running-fleet snapshot this module
@@ -43,6 +54,8 @@ function liveSessionStatus(v, brokerStatus) {
43
54
  // asleep/unreachable mapping — a stale broker signal must not mask it.
44
55
  if (!v.reachable) return null;
45
56
  if (v.hasSession && v.awaitingUserInput) return awaitingVerdict(v);
57
+ // The cycle closed and nothing has started since — finished, review it.
58
+ if (v.resumeMode === 'completed') return 'session-done';
46
59
  if (v.hasSession) return 'building';
47
60
  // No editor-owned session, but a live agent may still be working in the PTY
48
61
  // broker (decoupled lifetime — see docs/process-model.md). The broker
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+ // Push alerts for the Fleet Dashboard: one phone notification each time a VM
3
+ // becomes "needs you" — its cycle finished, or it has sat on a gate past the
4
+ // needs-you threshold (npm/fleet-stuck-status.js decides both). server.js does
5
+ // the I/O; the decision here is pure so the dedupe rules are unit-testable.
6
+ //
7
+ // Why the dashboard and not each VM: the operator host already sees the whole
8
+ // fleet, so one topic in the operator's own `$HOME/.codeyam/notify.env` covers
9
+ // every VM. Provisioning the topic onto each VM was rejected — a client VM's
10
+ // container must never hold the operator's topic.
11
+ //
12
+ // Dedupe: each VM's last alert is remembered as a KEY naming the episode — the
13
+ // cycle's close time for a finished VM, the step's start time for a waiting
14
+ // one. A 3s poll, a dashboard restart (the sent-set is persisted by the
15
+ // caller), or a status flap back to the same episode never re-sends; a new
16
+ // cycle or a new gate carries a new key and does.
17
+
18
+ // The episode a needs-you card belongs to. An assist cycle writes no close
19
+ // marker, so its key is just 'done:' — see the building rule in
20
+ // decideNotifications for how that episode ends.
21
+ function episodeKey(env) {
22
+ if (env.status === 'session-done') {
23
+ return 'done:' + ((env.cycleClosed && env.cycleClosed.closedAt) || '');
24
+ }
25
+ return 'waiting:' + (env.startedAt || '');
26
+ }
27
+
28
+ function alertFor(env, key) {
29
+ const vm = 'VM-' + env.n;
30
+ if (env.status === 'session-done') {
31
+ const feature = (env.cycleClosed && env.cycleClosed.feature) || env.feature;
32
+ return {
33
+ n: env.n,
34
+ key,
35
+ title: vm + ' finished',
36
+ message: feature ? vm + ' finished: ' + feature : vm + ' finished its work and is ready for review',
37
+ clickUrl: env.url || null,
38
+ };
39
+ }
40
+ const what = env.label || env.feature;
41
+ return {
42
+ n: env.n,
43
+ key,
44
+ title: vm + ' is waiting on you',
45
+ message: what ? vm + ' is waiting on you: ' + what : vm + ' is waiting on you',
46
+ clickUrl: env.url || null,
47
+ };
48
+ }
49
+
50
+ // decideNotifications(prevSent, envelopes) -> { toSend, nextSent, changed }
51
+ //
52
+ // prevSent — { [n]: { key } } persisted from the previous call.
53
+ // envelopes — the per-VM card envelopes buildData produced this poll.
54
+ //
55
+ // Rules:
56
+ // - A needs-you card whose episode key differs from the remembered one is
57
+ // sent, and its key remembered.
58
+ // - A needs-you card whose key matches is skipped.
59
+ // - A card that is NOT needs-you keeps its remembered key, so a flap through
60
+ // unreachable/reconnecting back into the same episode stays quiet.
61
+ // - Except: a card back to 'building' after an assist cycle (key 'done:')
62
+ // forgets it. That key cannot tell one assist cycle from the next, so the
63
+ // return to work is the only thing that ends the episode.
64
+ // - A VM absent from `envelopes` (destroyed) is forgotten.
65
+ // - `changed` says whether nextSent differs from prevSent, so the caller
66
+ // writes the persisted copy only when there is something new to write.
67
+ function decideNotifications(prevSent, envelopes) {
68
+ const prev = prevSent || {};
69
+ const toSend = [];
70
+ const nextSent = {};
71
+ for (const env of envelopes || []) {
72
+ if (!env || env.n == null) continue;
73
+ const remembered = prev[env.n] || null;
74
+ const needsYou = !!(env.attention && env.attention.needsYou);
75
+ if (needsYou) {
76
+ const key = episodeKey(env);
77
+ if (!remembered || remembered.key !== key) toSend.push(alertFor(env, key));
78
+ nextSent[env.n] = { key };
79
+ continue;
80
+ }
81
+ if (remembered && !(env.status === 'building' && remembered.key === 'done:')) {
82
+ nextSent[env.n] = remembered;
83
+ }
84
+ }
85
+ const changed = JSON.stringify(nextSent) !== JSON.stringify(prev);
86
+ return { toSend, nextSent, changed };
87
+ }
88
+
89
+ // The dashboard log line for one send attempt, so the operator can see in the
90
+ // dashboard log which alerts went out and why one did not.
91
+ function formatSendResult(item, result) {
92
+ if (result.ok) return `[notify] VM-${item.n}: ${item.title}`;
93
+ return `[notify] VM-${item.n} push failed: HTTP ${result.status}${result.error ? ' ' + result.error : ''}`;
94
+ }
95
+
96
+ // Parse the operator's notify config the same way .claude/hooks/claude-notify.sh
97
+ // resolves it: an exported NTFY_TOPIC wins, else `$HOME/.codeyam/notify.env`
98
+ // (KEY=value lines, as `editor notify-setup` writes them). Returns null when no
99
+ // topic is configured — the caller then sends nothing, silently, like the hook.
100
+ function resolveNotifyConfig(env, notifyEnvText) {
101
+ const e = env || {};
102
+ const fileVals = {};
103
+ for (const raw of String(notifyEnvText || '').split('\n')) {
104
+ const line = raw.trim().replace(/^export\s+/, '');
105
+ const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line);
106
+ if (!m) continue;
107
+ fileVals[m[1]] = m[2].trim().replace(/^(['"])(.*)\1$/, '$2');
108
+ }
109
+ const topic = e.NTFY_TOPIC || fileVals.NTFY_TOPIC || '';
110
+ if (!topic) return null;
111
+ const server = e.NTFY_SERVER || fileVals.NTFY_SERVER || 'https://ntfy.sh';
112
+ return { topic, server: server.replace(/\/+$/, '') };
113
+ }
114
+
115
+ // Send one alert to ntfy with the same headers claude-notify.sh uses. Resolves
116
+ // to { ok, status } and never rejects: a failed push is logged by the caller,
117
+ // never allowed to break a poll.
118
+ async function sendNtfy(cfg, item, fetchImpl) {
119
+ const doFetch = fetchImpl || fetch;
120
+ const headers = { Title: item.title };
121
+ if (item.clickUrl) headers.Click = item.clickUrl;
122
+ try {
123
+ const res = await doFetch(cfg.server + '/' + cfg.topic, { method: 'POST', headers, body: item.message });
124
+ return { ok: res.status === 200, status: res.status };
125
+ } catch (e) {
126
+ return { ok: false, status: null, error: e && e.message };
127
+ }
128
+ }
129
+
130
+ module.exports = { decideNotifications, formatSendResult, resolveNotifyConfig, sendNtfy };
@@ -262,7 +262,64 @@ function queueWriteBlockReason(memo = {}) {
262
262
  : `commit queue read is failing${why} — the displayed order cannot be trusted; fix the read before changing the queue`;
263
263
  }
264
264
 
265
+ /** Should this scheduled queue tick do any work at all?
266
+ *
267
+ * The scheduled poll used to fetch and exec `branch-queue status` every
268
+ * QUEUE_POLL_MS whether or not anyone had the dashboard open. On 2026-09-27
269
+ * that was 13,120 binary starts (~4.7h of process time) across one 13h audit
270
+ * window, competing with the audit for CPU. When no
271
+ * browser has requested `/data` for `idleAfterMs`, the tick runs at most once
272
+ * per `idleIntervalMs` — slow enough to cost nothing, fast enough that the
273
+ * gone-head auto-release (which rides this poll) still fires within a minute.
274
+ *
275
+ * `lastViewerAt` / `lastTickAt` are epoch ms or null. A null `lastViewerAt`
276
+ * (nobody has looked since boot) counts as idle; a null `lastTickAt` always
277
+ * ticks, so the boot read is never skipped. Pure; no I/O. */
278
+ function shouldRunQueueTick({ now, lastViewerAt, lastTickAt, idleAfterMs, idleIntervalMs } = {}) {
279
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
280
+ const t = num(now);
281
+ const tick = num(lastTickAt);
282
+ if (t === null || tick === null) return true;
283
+ const viewer = num(lastViewerAt);
284
+ const watched = viewer !== null && t - viewer < (num(idleAfterMs) || 0);
285
+ if (watched) return true;
286
+ return t - tick >= (num(idleIntervalMs) || 0);
287
+ }
288
+
289
+ /** Does a freshly fetched queue-ref snapshot need a `branch-queue status` exec?
290
+ *
291
+ * `branch-queue status` is a pure projection of `refs/codeyam/queue/*`, so
292
+ * when those refs are byte-identical to the ones behind the last SUCCESSFUL
293
+ * read, the exec can only return the answer already in the memo — and each
294
+ * exec is a full binary start plus a second network fetch of the same refs.
295
+ * Skipping it is what turns a per-tick process start into a per-change one.
296
+ *
297
+ * Always exec when:
298
+ * - the fingerprint could not be computed (`null` — never trust a failed
299
+ * local read as "unchanged");
300
+ * - no successful read has happened (`memo.readAt` null) or the last read
301
+ * FAILED (`memo.error`) — a cached answer must never paper over an error,
302
+ * the 2026-08-14 frozen-panel lesson mergeQueuePoll records;
303
+ * - the refs moved since the fingerprint the memo was read against;
304
+ * - the last exec is older than `maxAgeMs` — a safety net so a projection
305
+ * that ever grows a non-ref input cannot freeze the panel indefinitely.
306
+ *
307
+ * Returns { exec: boolean, reason: string }. Pure; no I/O. */
308
+ function decideQueueStatusExec({ fingerprint, memo = {}, lastExecAt, now, maxAgeMs } = {}) {
309
+ if (typeof fingerprint !== 'string') return { exec: true, reason: 'fingerprint-unavailable' };
310
+ if (!(typeof memo.readAt === 'string' && memo.readAt)) return { exec: true, reason: 'never-read' };
311
+ if (typeof memo.error === 'string' && memo.error) return { exec: true, reason: 'last-read-failed' };
312
+ if (memo.fingerprint !== fingerprint) return { exec: true, reason: 'refs-changed' };
313
+ const at = (typeof lastExecAt === 'number' && Number.isFinite(lastExecAt)) ? lastExecAt : null;
314
+ const clock = (typeof now === 'number' && Number.isFinite(now)) ? now : null;
315
+ if (at === null || clock === null) return { exec: true, reason: 'max-age' };
316
+ if (clock - at >= (typeof maxAgeMs === 'number' ? maxAgeMs : 0)) return { exec: true, reason: 'max-age' };
317
+ return { exec: false, reason: 'refs-unchanged' };
318
+ }
319
+
265
320
  module.exports = {
321
+ shouldRunQueueTick,
322
+ decideQueueStatusExec,
266
323
  decideAutoReleaseGoneHead,
267
324
  computeReorderTargets,
268
325
  nudgeQueueOrder,
@@ -50,6 +50,14 @@
50
50
  // repopulates over whatever remains). Emptying contents (not removing the dir)
51
51
  // also sidesteps the `rmdir` race entirely.
52
52
  //
53
+ // The wipe NEVER descends into `.codeyam/agent-home`. Beside the two gitignored
54
+ // infra files preserved above, it is the third thing that must survive: when
55
+ // the editor relocates Claude's sign-in store into the project it lives at
56
+ // `.codeyam/agent-home/claude`, and it holds a live sign-in no restore step
57
+ // could recreate — wiping it strands the VM at `claude /login`. It is excluded
58
+ // rather than round-tripped through a world-readable `/tmp` like the two files;
59
+ // an untouched directory needs no restore, so the trap recovery is unchanged.
60
+ //
53
61
  // The destructive region (park → broker-stop → wipe → restore) is made
54
62
  // FAILURE-SAFE with an EXIT trap installed before the park and disarmed after
55
63
  // a successful restore. If ANY step in that region aborts the chain, the trap
@@ -83,7 +91,11 @@ function codeyamDeepCleanSteps(E) {
83
91
  `{ __cy_recover() { ${recover}; }; trap '__cy_recover; exit 1' EXIT; }`,
84
92
  `(${E} editor stop-server --hold-for-restart --force-in-container || true)`,
85
93
  `(${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 )',
94
+ // Spares `.codeyam/agent-home` (see the header): `! -path` rather than
95
+ // `-prune`, because `-delete` implies `-depth`, under which `-prune` is a
96
+ // no-op. The done-check ignores it too, or a relocated VM would always
97
+ // spin the full retry budget.
98
+ '( for i in 1 2 3 4 5; do find .codeyam -mindepth 1 ! -path .codeyam/agent-home ! -path ".codeyam/agent-home/*" -delete 2>/dev/null; [ -z "$(ls -A .codeyam 2>/dev/null | grep -vx agent-home)" ] && break; sleep 0.2; done; true )',
87
99
  `( ${restoreCodeyam} )`,
88
100
  `( ${restoreElj} && rm -f /tmp/cy-elj.bak; true )`,
89
101
  `( ${restoreSs} && rm -f /tmp/cy-ss.bak; true )`,
@@ -78,6 +78,11 @@ function elapsedMs(iso, nowMs) {
78
78
  // longer drives this: liveSessionStatus already collapses an idle awaiting
79
79
  // agent to 'idle' and a blocked one to 'waiting', so the status IS the
80
80
  // discriminator.
81
+ // - A 'session-done' VM (its cycle closed) is "needs you" IMMEDIATELY, with
82
+ // no threshold: the work is finished and the operator reviewing it is the
83
+ // next move. Its `level` stays 'ok' — a finished VM is never slow or stuck.
84
+ // `needsYouElapsedMs` counts from `doneAt` (the cycle's close time) when the
85
+ // editor reports one, else null.
81
86
  // - Missing/unparseable stepStartedAt degrades to stepElapsedMs:null,
82
87
  // level:'ok', needsYou:false (older editor, no crash).
83
88
  function deriveAttention(opts) {
@@ -95,6 +100,9 @@ function deriveAttention(opts) {
95
100
  // its threshold. An 'idle' VM never reaches this branch.
96
101
  needsYouElapsedMs = stepElapsed;
97
102
  needsYou = stepElapsed != null && stepElapsed >= thresholds.needsYouMs;
103
+ } else if (o.status === 'session-done') {
104
+ needsYou = true;
105
+ needsYouElapsedMs = elapsedMs(o.doneAt, nowMs);
98
106
  } else if (o.status === 'building' && stepElapsed != null) {
99
107
  if (stepElapsed >= thresholds.stuckMs) level = 'stuck';
100
108
  else if (stepElapsed >= thresholds.slowMs) level = 'slow';
@@ -12,6 +12,7 @@ const { CANONICAL_EDITOR_REPO, EDITOR_VARIANTS, repoBasename, AGENT_ENV_VARS } =
12
12
  const { normalizeVmSlug } = require("./vm-urls");
13
13
  const { normalizeAccessList } = require("./fleet-vm-access");
14
14
  const { normalizeRepoUrl } = require("./fleet-env");
15
+ const { posix: posixPath } = require("node:path");
15
16
 
16
17
  /** The build agents a VM's editor terminal can run, as the keys of cloud.js's
17
18
  * AGENT_ENV_VARS. Deriving the selectable set from that map keeps it a single
@@ -1029,11 +1030,58 @@ function credentialShapeCheck(file) {
1029
1030
  return CREDENTIAL_SHAPE_PATTERNS.map((p) => `grep -Eq '${p}' ${file}`).join(" && ");
1030
1031
  }
1031
1032
 
1033
+ /** Where Claude's sign-in store lives in the container: the editor may relocate
1034
+ * it from the home store into the project. `RELOCATED_STORE_REL` MUST track
1035
+ * Rust's `RELOCATED_STORE_DIR` (crates/control-api/src/claude_store_relocation.rs);
1036
+ * it is resolved against the container's fixed app root `/workspace`. */
1037
+ const RELOCATED_STORE_REL = ".codeyam/agent-home/claude";
1038
+ const RELOCATED_STORE_DIR = `/workspace/${RELOCATED_STORE_REL}`;
1039
+ const HOME_STORE_DIR = "/root/.claude";
1040
+
1041
+ /** Shell step that `cd`s into the directory holding Claude's `name` file —
1042
+ * `.credentials.json` or the onboarding `.claude.json` — so the caller then
1043
+ * names the file by basename. The relocated store wins when it exists; else the
1044
+ * home store. `.claude.json` mirrors the Rust `claude_json_path_at` asymmetry:
1045
+ * relocated → INSIDE the store, home → `/root/.claude.json` (beside the store,
1046
+ * never `/root/.claude/.claude.json`).
1047
+ *
1048
+ * A `cd`, not a `$(…)` path, on purpose: the copy steps run through the VM-host
1049
+ * shell inside a double-quoted `bash -c "…"`, which would expand a command
1050
+ * substitution on the HOST (see `verifyBeforeMoveSink`). This contains no `$`
1051
+ * and no `"`, so it survives every quoting layer and resolves in the container.
1052
+ * The single definition of the store location, shared by the integrity probe,
1053
+ * the donor read and the copy sinks so they can never disagree. `dirs`
1054
+ * overrides the container locations (tests point it at temp dirs). Pure. */
1055
+ function claudeStoreCdStep(name, dirs = {}) {
1056
+ const { relocated = RELOCATED_STORE_DIR, home = HOME_STORE_DIR } = dirs;
1057
+ const homeDir = name === ".claude.json" ? posixPath.dirname(home) : home;
1058
+ return `if [ -d ${relocated} ]; then cd ${relocated}; else cd ${homeDir}; fi`;
1059
+ }
1060
+
1061
+ /** Shell assignment `STORE=project|home` naming which Claude store is live in
1062
+ * the container, for the dashboard gather. No token is read. `relocated`
1063
+ * overrides the container location for tests. Pure. */
1064
+ function claudeStoreProbe(relocated = RELOCATED_STORE_DIR) {
1065
+ return `STORE=$(test -d ${relocated} && echo project || echo home)`;
1066
+ }
1067
+
1068
+ /** Parse the gather's `STORE` marker: `"project"` / `"home"`, else `null` — an
1069
+ * absent marker (older gather, probe never ran) is unknown, never a fabricated
1070
+ * `home`. Pure. */
1071
+ function parseClaudeStore(marker) {
1072
+ return marker === "project" || marker === "home" ? marker : null;
1073
+ }
1074
+
1032
1075
  /** Shell assignment `CRED=1|0` for the donor-integrity probe: `1` when the
1033
- * credential at `file` passes `credentialShapeCheck`, `0` when it fails or is
1034
- * missing (either way the copy sink would refuse it). Pure. */
1035
- function credentialIntegrityProbe(file) {
1036
- return `CRED=$( (${credentialShapeCheck(file)}) 2>/dev/null && echo 1 || echo 0)`;
1076
+ * credential passes `credentialShapeCheck`, `0` when it fails or is missing
1077
+ * (either way the copy sink would refuse it). With no `file`, the credential is
1078
+ * resolved via `claudeStoreCdStep` — relocated store first, then home — inside a
1079
+ * subshell, so the caller's cwd is untouched; `dirs` passes through to it. Pure. */
1080
+ function credentialIntegrityProbe(file, dirs) {
1081
+ const check = file
1082
+ ? credentialShapeCheck(file)
1083
+ : `${claudeStoreCdStep(".credentials.json", dirs)} && ${credentialShapeCheck(".credentials.json")}`;
1084
+ return `CRED=$( (${check}) 2>/dev/null && echo 1 || echo 0)`;
1037
1085
  }
1038
1086
 
1039
1087
  /** Parse the probe's `CRED` marker into the tri-state integrity verdict:
@@ -1087,13 +1135,17 @@ function donorSkippedLine(donor) {
1087
1135
  function verifyBeforeMoveSink(
1088
1136
  container,
1089
1137
  target,
1090
- { decode = true, requireCredentialShape = false } = {},
1138
+ { decode = true, requireCredentialShape = false, cdStep = "" } = {},
1091
1139
  ) {
1092
1140
  const tmp = `${target}.cytmp`;
1093
1141
  const read = decode ? "base64 -d" : "cat";
1094
1142
  const shape = requireCredentialShape ? ` && ${credentialShapeCheck(tmp)}` : "";
1143
+ // `cdStep` (e.g. `claudeStoreCdStep`) resolves the target's directory in the
1144
+ // container first, so a relative `target` lands in the live store; a failed
1145
+ // `cd` aborts before anything is written.
1146
+ const cd = cdStep ? `${cdStep} || exit 1; ` : "";
1095
1147
  const script =
1096
- `umask 077; ${read} > ${tmp}; ` +
1148
+ `${cd}umask 077; ${read} > ${tmp}; ` +
1097
1149
  `if [ -s ${tmp} ]${shape}; then mv ${tmp} ${target} && chmod 600 ${target}; ` +
1098
1150
  `else rm -f ${tmp}; exit 1; fi`;
1099
1151
  return `docker exec -i ${container} bash -c "${script}"`;
@@ -1108,8 +1160,8 @@ function verifyBeforeMoveSink(
1108
1160
  *
1109
1161
  * Donor mode (the default) copies, container→container from the donor VM:
1110
1162
  * 1. host-file git credentials → `$HOME/.codeyam-cloud-git-credentials`
1111
- * 2. claude `.credentials.json` → `/root/.claude/.credentials.json`
1112
- * 3. claude `.claude.json` → `/root/.claude.json`
1163
+ * 2. claude `.credentials.json` → the target's live store (`claudeStoreCdStep`)
1164
+ * 3. claude `.claude.json` → the target's live onboarding dotfile
1113
1165
  * Step 3 (onboarding state — `hasCompletedOnboarding` + sibling keys) is what
1114
1166
  * keeps the first interactive `claude` TUI from popping the welcome "Select
1115
1167
  * login method" menu; without it the non-interactive `claude --print` probe
@@ -1134,19 +1186,27 @@ function buildCredsSteps({ mode, refInst = "", inst, container, gcloud, zone, n,
1134
1186
  // non-empty-but-corrupt donor must not clobber a working target. The
1135
1187
  // `.claude.json` (onboarding-state) copy has a different shape, so it keeps
1136
1188
  // the plain non-empty guard.
1137
- const claudeSink = verifyBeforeMoveSink(container, "/root/.claude/.credentials.json", {
1189
+ // Both ends follow Claude's store wherever the editor put it (relocated into
1190
+ // the project, or the home store): the donor is read from its live store and
1191
+ // the target written into ITS live store, via the one `claudeStoreCdStep`.
1192
+ const claudeSink = verifyBeforeMoveSink(container, ".credentials.json", {
1138
1193
  requireCredentialShape: true,
1194
+ cdStep: claudeStoreCdStep(".credentials.json"),
1195
+ });
1196
+ const claudeJsonSink = verifyBeforeMoveSink(container, ".claude.json", {
1197
+ cdStep: claudeStoreCdStep(".claude.json"),
1139
1198
  });
1140
- const claudeJsonSink = verifyBeforeMoveSink(container, "/root/.claude.json");
1199
+ const donorRead = (name) =>
1200
+ `docker exec ${container} bash -c "${claudeStoreCdStep(name)} && base64 ${name}"`;
1141
1201
  const loginHint = `run: docker exec -it ${container} claude /login on ${inst}`;
1142
1202
  if (mode === "donor") {
1143
1203
  return [
1144
1204
  `echo "copying git creds from ${refInst}..."`,
1145
1205
  `${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command='cat $HOME/.codeyam-cloud-git-credentials' 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command='umask 077; cat > $HOME/.codeyam-cloud-git-credentials' 2>/dev/null || echo "CY_DEGRADED git creds copy failed on VM-${n} — re-copy creds from a live donor (the agent can't git pull/push until then)"`,
1146
1206
  `echo "copying claude auth from ${refInst}${acctNote} (container->container)..."`,
1147
- `${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command='docker exec ${container} base64 /root/.claude/.credentials.json' 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command=${_shq(claudeSink)} 2>/dev/null && echo "claude auth copied" || echo "CY_DEGRADED claude-auth-not-seeded: claude auth copy failed on VM-${n} (empty/unreadable/malformed donor — existing creds left untouched, not overwritten with a corrupt credential) — ${loginHint}"`,
1207
+ `${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command=${_shq(donorRead(".credentials.json"))} 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command=${_shq(claudeSink)} 2>/dev/null && echo "claude auth copied" || echo "CY_DEGRADED claude-auth-not-seeded: claude auth copy failed on VM-${n} (empty/unreadable/malformed donor — existing creds left untouched, not overwritten with a corrupt credential) — ${loginHint}"`,
1148
1208
  `echo "copying claude.json (onboarding state) from ${refInst} (container->container)..."`,
1149
- `${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command='docker exec ${container} base64 /root/.claude.json' 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command=${_shq(claudeJsonSink)} 2>/dev/null && echo "claude.json copied" || echo "CY_DEGRADED claude-auth-not-seeded: claude.json copy failed on VM-${n} — first interactive claude launch may show 'Select login method' menu; ${loginHint} once to complete onboarding"`,
1209
+ `${gcloud} compute ssh ${refInst} --tunnel-through-iap --zone=${zone} --command=${_shq(donorRead(".claude.json"))} 2>/dev/null | ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} --command=${_shq(claudeJsonSink)} 2>/dev/null && echo "claude.json copied" || echo "CY_DEGRADED claude-auth-not-seeded: claude.json copy failed on VM-${n} — first interactive claude launch may show 'Select login method' menu; ${loginHint} once to complete onboarding"`,
1150
1210
  ];
1151
1211
  }
1152
1212
  if (mode === "host-stash") {
@@ -1532,6 +1592,11 @@ module.exports = {
1532
1592
  CREDENTIAL_SHAPE_PATTERNS,
1533
1593
  credentialShapeCheck,
1534
1594
  credentialIntegrityProbe,
1595
+ RELOCATED_STORE_REL,
1596
+ HOME_STORE_DIR,
1597
+ claudeStoreCdStep,
1598
+ claudeStoreProbe,
1599
+ parseClaudeStore,
1535
1600
  parseCredentialIntact,
1536
1601
  donorSkippedLine,
1537
1602
  buildClaudeJsonPreserveSteps,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeyam-editor/codeyam-editor",
3
- "version": "0.1.12-staging.g83e4a0c",
3
+ "version": "0.1.12",
4
4
  "description": "Language-agnostic managed execution sandbox for scenario-driven development",
5
5
  "bin": {
6
6
  "codeyam-editor": "./npm/cli.js"
@@ -9,10 +9,10 @@
9
9
  "playwright": "^1.58.2"
10
10
  },
11
11
  "optionalDependencies": {
12
- "@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.12-staging.g83e4a0c",
13
- "@codeyam-editor/codeyam-editor-darwin-x64": "0.1.12-staging.g83e4a0c",
14
- "@codeyam-editor/codeyam-editor-linux-x64": "0.1.12-staging.g83e4a0c",
15
- "@codeyam-editor/codeyam-editor-win32-x64": "0.1.12-staging.g83e4a0c"
12
+ "@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.12",
13
+ "@codeyam-editor/codeyam-editor-darwin-x64": "0.1.12",
14
+ "@codeyam-editor/codeyam-editor-linux-x64": "0.1.12",
15
+ "@codeyam-editor/codeyam-editor-win32-x64": "0.1.12"
16
16
  },
17
17
  "keywords": [
18
18
  "codeyam",
@@ -1,2 +1,2 @@
1
- import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as k}from"./useEvents-S3Qk1Y_d.js";import{k as E}from"./index-ngNVAM6P.js";function P({connected:t}){return e.jsx("span",{className:`inline-block rounded-full px-2 py-0.5 text-xs font-medium text-[var(--text-primary)] ${t?"bg-[var(--accent-green)]":"bg-[var(--accent-red)]"}`,children:t?"connected":"reconnecting…"})}const re=Object.freeze(Object.defineProperty({__proto__:null,ConnectionBadge:P},Symbol.toStringTag,{value:"Module"}));function N(){const[t,r]=a.useState(null),[o,s]=a.useState(!0),{events:l}=k(),n=a.useCallback(async()=>{try{const c=await(await fetch("/api/inspector/status")).json();r(c)}catch{}finally{s(!1)}},[]);return a.useEffect(()=>{n()},[n]),a.useEffect(()=>{const i=l[l.length-1];i&&i.type==="scenario_switch"&&n()},[l,n]),{scenario:(t==null?void 0:t.scenario)??null,previewScenario:(t==null?void 0:t.previewScenario)??null,routes:(t==null?void 0:t.routes)??[],routeCount:(t==null?void 0:t.routeCount)??0,loading:o,refetch:n}}function v({title:t}){return e.jsx("h2",{style:{fontSize:14,fontWeight:600,color:"#f0f6fc",margin:"0 0 8px 0",borderBottom:"1px solid #30363d",paddingBottom:8},children:t})}function f({color:t,children:r}){return e.jsx("span",{style:{background:`${t}22`,color:t,border:`1px solid ${t}55`,borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,letterSpacing:"0.5px",whiteSpace:"nowrap"},children:r})}const $={GET:"#D7FF63",POST:"#238636",PUT:"#d29922",DELETE:"#da3633",PATCH:"#bc8cff"};function O({method:t}){const r=$[t.toUpperCase()]??"#8b949e";return e.jsx("span",{style:{color:r,fontWeight:600,fontSize:11,minWidth:48,display:"inline-block"},children:t.toUpperCase()})}function C({color:t,children:r}){return e.jsx("div",{style:{padding:"12px 16px",borderRadius:6,border:`1px solid ${t}44`,background:`${t}11`,color:t},children:r})}const u={padding:"8px 12px",textAlign:"center",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},h={padding:"6px 12px"},S={background:"#161b22",border:"1px solid #30363d",borderRadius:6,color:"#c9d1d9",padding:"6px 12px",fontSize:13,fontFamily:"inherit"},R={...S,appearance:"none",paddingRight:24,backgroundImage:`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238b949e' d='M6 8L1 3h10z'/%3E%3C/svg%3E")`,backgroundRepeat:"no-repeat",backgroundPosition:"right 8px center"},oe=Object.freeze(Object.defineProperty({__proto__:null,Badge:f,MethodBadge:O,SectionHeader:v,StatusBox:C,inputStyle:S,selectStyle:R,tdStyle:h,thStyle:u},Symbol.toStringTag,{value:"Module"}));function z({routes:t}){return e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d"},children:[e.jsx("th",{style:u,children:"Method"}),e.jsx("th",{style:{...u,textAlign:"left"},children:"Pattern"}),e.jsx("th",{style:u,children:"Status"})]})}),e.jsx("tbody",{children:t.map((r,o)=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(O,{method:r.method})}),e.jsx("td",{style:h,children:r.pattern}),e.jsx("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:r.status})]},o))})]})}const se=Object.freeze(Object.defineProperty({__proto__:null,InspectorRouteTable:z},Symbol.toStringTag,{value:"Module"}));function A(){const{scenario:t,previewScenario:r,routes:o,routeCount:s,loading:l}=N();return e.jsxs("section",{children:[e.jsx(v,{title:"Active Scenario"}),l?e.jsx(C,{color:"#8b949e",children:"Loading..."}):t?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#238636",children:"ACTIVE"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:t.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",t.slug,")"]})]}),e.jsxs("div",{style:{color:"#8b949e",fontSize:12},children:[s," mock route",s!==1?"s":""," loaded"]}),o.length>0&&e.jsx(z,{routes:o})]}):r?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#D7FF63",children:"PREVIEW"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:r.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",r.slug,")"]})]}),e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"Component preview — no HTTP mocks active"})]}):e.jsx(C,{color:"#d29922",children:"No scenario active — all requests pass through to upstream"})]})}const ne=Object.freeze(Object.defineProperty({__proto__:null,InspectorScenarioSection:A},Symbol.toStringTag,{value:"Module"}));function F({textUrlFilter:t,onTextUrlFilterChange:r,methodFilter:o,onMethodFilterChange:s,sourceFilter:l,onSourceFilterChange:n}){return e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12,alignItems:"center"},children:[e.jsx("input",{type:"text",placeholder:"Filter by URL...",value:t,onChange:i=>r(i.target.value),style:{...S,flex:1,minWidth:0}}),e.jsxs("select",{value:o,onChange:i=>s(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Methods"}),e.jsx("option",{value:"GET",children:"GET"}),e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"}),e.jsx("option",{value:"DELETE",children:"DELETE"}),e.jsx("option",{value:"PATCH",children:"PATCH"})]}),e.jsxs("select",{value:l,onChange:i=>n(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Sources"}),e.jsx("option",{value:"MOCKED",children:"Mocked"}),e.jsx("option",{value:"PASSTHROUGH",children:"Passthrough"})]})]})}const ie=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficFilterBar:F},Symbol.toStringTag,{value:"Module"})),G={color:"#8b949e",fontSize:11,fontWeight:600,textTransform:"uppercase",letterSpacing:"0.5px",marginBottom:4},M={background:"#161b22",border:"1px solid #30363d",borderRadius:4,padding:"8px 12px",margin:0,fontSize:11,color:"#c9d1d9",fontFamily:"monospace",maxHeight:200,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"};function _({label:t,children:r}){return e.jsxs("div",{style:{marginBottom:12},children:[e.jsx("div",{style:G,children:t}),r]})}function B({event:t}){let r=[];try{const n=new URL(t.url,"http://localhost");r=Array.from(n.searchParams.entries())}catch{}const o=t.requestHeaders&&Object.keys(t.requestHeaders).length>0,s=t.requestBody!=null,l=t.responseBody!=null;return e.jsxs("div",{style:{background:"#0d1117",padding:"12px 16px",borderTop:"1px solid #30363d"},children:[r.length>0&&e.jsx(_,{label:"Query Parameters",children:e.jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"2px 12px",fontSize:12},children:r.map(([n,i],c)=>e.jsxs("div",{style:{display:"contents"},children:[e.jsx("span",{style:{color:"#D7FF63",fontFamily:"monospace"},children:n}),e.jsx("span",{style:{color:"#c9d1d9",fontFamily:"monospace"},children:i})]},c))})}),o&&e.jsx(_,{label:"Request Headers",children:e.jsx("pre",{style:M,children:Object.entries(t.requestHeaders).map(([n,i])=>`${n}: ${i}`).join(`
1
+ import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as k}from"./useEvents-S3Qk1Y_d.js";import{k as E}from"./index-maa9OUlU.js";function P({connected:t}){return e.jsx("span",{className:`inline-block rounded-full px-2 py-0.5 text-xs font-medium text-[var(--text-primary)] ${t?"bg-[var(--accent-green)]":"bg-[var(--accent-red)]"}`,children:t?"connected":"reconnecting…"})}const re=Object.freeze(Object.defineProperty({__proto__:null,ConnectionBadge:P},Symbol.toStringTag,{value:"Module"}));function N(){const[t,r]=a.useState(null),[o,s]=a.useState(!0),{events:l}=k(),n=a.useCallback(async()=>{try{const c=await(await fetch("/api/inspector/status")).json();r(c)}catch{}finally{s(!1)}},[]);return a.useEffect(()=>{n()},[n]),a.useEffect(()=>{const i=l[l.length-1];i&&i.type==="scenario_switch"&&n()},[l,n]),{scenario:(t==null?void 0:t.scenario)??null,previewScenario:(t==null?void 0:t.previewScenario)??null,routes:(t==null?void 0:t.routes)??[],routeCount:(t==null?void 0:t.routeCount)??0,loading:o,refetch:n}}function v({title:t}){return e.jsx("h2",{style:{fontSize:14,fontWeight:600,color:"#f0f6fc",margin:"0 0 8px 0",borderBottom:"1px solid #30363d",paddingBottom:8},children:t})}function f({color:t,children:r}){return e.jsx("span",{style:{background:`${t}22`,color:t,border:`1px solid ${t}55`,borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,letterSpacing:"0.5px",whiteSpace:"nowrap"},children:r})}const $={GET:"#D7FF63",POST:"#238636",PUT:"#d29922",DELETE:"#da3633",PATCH:"#bc8cff"};function O({method:t}){const r=$[t.toUpperCase()]??"#8b949e";return e.jsx("span",{style:{color:r,fontWeight:600,fontSize:11,minWidth:48,display:"inline-block"},children:t.toUpperCase()})}function C({color:t,children:r}){return e.jsx("div",{style:{padding:"12px 16px",borderRadius:6,border:`1px solid ${t}44`,background:`${t}11`,color:t},children:r})}const u={padding:"8px 12px",textAlign:"center",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},h={padding:"6px 12px"},S={background:"#161b22",border:"1px solid #30363d",borderRadius:6,color:"#c9d1d9",padding:"6px 12px",fontSize:13,fontFamily:"inherit"},R={...S,appearance:"none",paddingRight:24,backgroundImage:`url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%238b949e' d='M6 8L1 3h10z'/%3E%3C/svg%3E")`,backgroundRepeat:"no-repeat",backgroundPosition:"right 8px center"},oe=Object.freeze(Object.defineProperty({__proto__:null,Badge:f,MethodBadge:O,SectionHeader:v,StatusBox:C,inputStyle:S,selectStyle:R,tdStyle:h,thStyle:u},Symbol.toStringTag,{value:"Module"}));function z({routes:t}){return e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d"},children:[e.jsx("th",{style:u,children:"Method"}),e.jsx("th",{style:{...u,textAlign:"left"},children:"Pattern"}),e.jsx("th",{style:u,children:"Status"})]})}),e.jsx("tbody",{children:t.map((r,o)=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(O,{method:r.method})}),e.jsx("td",{style:h,children:r.pattern}),e.jsx("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:r.status})]},o))})]})}const se=Object.freeze(Object.defineProperty({__proto__:null,InspectorRouteTable:z},Symbol.toStringTag,{value:"Module"}));function A(){const{scenario:t,previewScenario:r,routes:o,routeCount:s,loading:l}=N();return e.jsxs("section",{children:[e.jsx(v,{title:"Active Scenario"}),l?e.jsx(C,{color:"#8b949e",children:"Loading..."}):t?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#238636",children:"ACTIVE"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:t.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",t.slug,")"]})]}),e.jsxs("div",{style:{color:"#8b949e",fontSize:12},children:[s," mock route",s!==1?"s":""," loaded"]}),o.length>0&&e.jsx(z,{routes:o})]}):r?e.jsxs("div",{style:{display:"flex",flexDirection:"column",gap:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8},children:[e.jsx(f,{color:"#D7FF63",children:"PREVIEW"}),e.jsx("span",{style:{color:"#f0f6fc",fontWeight:500},children:r.name}),e.jsxs("span",{style:{color:"#8b949e"},children:["(",r.slug,")"]})]}),e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"Component preview — no HTTP mocks active"})]}):e.jsx(C,{color:"#d29922",children:"No scenario active — all requests pass through to upstream"})]})}const ne=Object.freeze(Object.defineProperty({__proto__:null,InspectorScenarioSection:A},Symbol.toStringTag,{value:"Module"}));function F({textUrlFilter:t,onTextUrlFilterChange:r,methodFilter:o,onMethodFilterChange:s,sourceFilter:l,onSourceFilterChange:n}){return e.jsxs("div",{style:{display:"flex",gap:8,marginBottom:12,alignItems:"center"},children:[e.jsx("input",{type:"text",placeholder:"Filter by URL...",value:t,onChange:i=>r(i.target.value),style:{...S,flex:1,minWidth:0}}),e.jsxs("select",{value:o,onChange:i=>s(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Methods"}),e.jsx("option",{value:"GET",children:"GET"}),e.jsx("option",{value:"POST",children:"POST"}),e.jsx("option",{value:"PUT",children:"PUT"}),e.jsx("option",{value:"DELETE",children:"DELETE"}),e.jsx("option",{value:"PATCH",children:"PATCH"})]}),e.jsxs("select",{value:l,onChange:i=>n(i.target.value),style:R,children:[e.jsx("option",{value:"ALL",children:"All Sources"}),e.jsx("option",{value:"MOCKED",children:"Mocked"}),e.jsx("option",{value:"PASSTHROUGH",children:"Passthrough"})]})]})}const ie=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficFilterBar:F},Symbol.toStringTag,{value:"Module"})),G={color:"#8b949e",fontSize:11,fontWeight:600,textTransform:"uppercase",letterSpacing:"0.5px",marginBottom:4},M={background:"#161b22",border:"1px solid #30363d",borderRadius:4,padding:"8px 12px",margin:0,fontSize:11,color:"#c9d1d9",fontFamily:"monospace",maxHeight:200,overflow:"auto",whiteSpace:"pre-wrap",wordBreak:"break-all"};function _({label:t,children:r}){return e.jsxs("div",{style:{marginBottom:12},children:[e.jsx("div",{style:G,children:t}),r]})}function B({event:t}){let r=[];try{const n=new URL(t.url,"http://localhost");r=Array.from(n.searchParams.entries())}catch{}const o=t.requestHeaders&&Object.keys(t.requestHeaders).length>0,s=t.requestBody!=null,l=t.responseBody!=null;return e.jsxs("div",{style:{background:"#0d1117",padding:"12px 16px",borderTop:"1px solid #30363d"},children:[r.length>0&&e.jsx(_,{label:"Query Parameters",children:e.jsx("div",{style:{display:"grid",gridTemplateColumns:"auto 1fr",gap:"2px 12px",fontSize:12},children:r.map(([n,i],c)=>e.jsxs("div",{style:{display:"contents"},children:[e.jsx("span",{style:{color:"#D7FF63",fontFamily:"monospace"},children:n}),e.jsx("span",{style:{color:"#c9d1d9",fontFamily:"monospace"},children:i})]},c))})}),o&&e.jsx(_,{label:"Request Headers",children:e.jsx("pre",{style:M,children:Object.entries(t.requestHeaders).map(([n,i])=>`${n}: ${i}`).join(`
2
2
  `)})}),s&&e.jsx(_,{label:"Request Body",children:e.jsx("pre",{style:M,children:typeof t.requestBody=="string"?t.requestBody:JSON.stringify(t.requestBody,null,2)})}),l&&e.jsx(_,{label:"Response Body",children:e.jsx("pre",{style:M,children:typeof t.responseBody=="string"?t.responseBody:JSON.stringify(t.responseBody,null,2)})}),r.length===0&&!o&&!s&&!l&&e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"No additional details available for this request"})]})}const le=Object.freeze(Object.defineProperty({__proto__:null,DetailSection:_,InspectorTrafficDetail:B},Symbol.toStringTag,{value:"Module"}));function L({event:t}){const[r,o]=a.useState(!1),s=new Date(t.timestampMs).toLocaleTimeString(),l=t.mocked;return e.jsxs(e.Fragment,{children:[e.jsxs("tr",{onClick:()=>o(!r),style:{borderBottom:"1px solid #21262d",background:l?"transparent":"rgba(210, 153, 34, 0.05)",cursor:"pointer"},children:[e.jsxs("td",{style:{...h,color:"#8b949e",textAlign:"center"},children:[e.jsx("span",{style:{display:"inline-block",width:12,marginRight:4,fontSize:10,color:"#8b949e",transition:"transform 0.15s",transform:r?"rotate(90deg)":"rotate(0deg)"},children:"▶"}),s]}),e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(O,{method:t.method})}),e.jsx("td",{style:{...h,maxWidth:400,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"},children:t.url}),e.jsx("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:t.status??"---"}),e.jsxs("td",{style:{...h,textAlign:"center",color:"#8b949e"},children:[t.durationMs,"ms"]}),e.jsx("td",{style:{...h,textAlign:"center"},children:l?t.mockSource==="default"?e.jsx(f,{color:"#1f6feb",children:"MOCKED (default)"}):e.jsx(f,{color:"#238636",children:"MOCKED (scenario)"}):e.jsx(f,{color:"#d29922",children:"PASSTHROUGH"})})]}),r&&e.jsx("tr",{children:e.jsx("td",{colSpan:6,style:{padding:0},children:e.jsx(B,{event:t})})})]})}const ae=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficRow:L},Symbol.toStringTag,{value:"Module"}));function D({events:t}){return e.jsx("div",{style:{maxHeight:300,overflowY:"auto",border:"1px solid #30363d",borderRadius:6},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",position:"sticky",top:0,background:"#0d1117"},children:[e.jsx("th",{style:u,children:"Time"}),e.jsx("th",{style:u,children:"Method"}),e.jsx("th",{style:{...u,textAlign:"left"},children:"URL"}),e.jsx("th",{style:u,children:"Status"}),e.jsx("th",{style:u,children:"Duration"}),e.jsx("th",{style:u,children:"Source"})]})}),e.jsx("tbody",{children:t.map(r=>e.jsx(L,{event:r},r.id))})]})})}const ce=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficTable:D},Symbol.toStringTag,{value:"Module"}));function K(t){if(t===void 0)return"checked";if(t==="checked")return"excluded"}function I({chips:t,chipStates:r,onChipStatesChange:o}){if(t.length===0)return null;const s=n=>{const i=new Map(r),c=K(i.get(n));c===void 0?i.delete(n):i.set(n,c),o(i)},l=()=>{o(new Map)};return e.jsxs("div",{style:{display:"flex",flexWrap:"wrap",gap:6,marginBottom:12},children:[r.size>0&&e.jsx("button",{onClick:l,style:{background:"#da363322",color:"#da3633",border:"1px solid #da363355",borderRadius:4,padding:"2px 8px",fontSize:11,fontWeight:600,cursor:"pointer",fontFamily:"inherit"},children:"Clear all"}),t.map(({path:n,count:i})=>{const c=r.get(n),d=c==="checked",g=c==="excluded";let m="#30363d",b="#8b949e",T="#30363d",y="";return d?(m="#D7FF6322",b="#D7FF63",T="#D7FF6355",y="✓ "):g&&(m="#da363322",b="#da3633",T="#da363355",y="✗ "),e.jsxs("button",{onClick:()=>s(n),style:{background:m,color:b,border:`1px solid ${T}`,borderRadius:4,padding:"2px 8px",fontSize:11,fontFamily:"monospace",cursor:"pointer",whiteSpace:"nowrap"},children:[y,n,e.jsxs("span",{style:{marginLeft:4,opacity:.6,fontFamily:"inherit"},children:["(",i,")"]})]},n)})]})}const de=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficUrlChips:I},Symbol.toStringTag,{value:"Module"}));function H(){const{events:t}=k(),[r,o]=a.useState(""),[s,l]=a.useState(()=>new Map),[n,i]=a.useState("ALL"),[c,d]=a.useState("ALL"),g=t.filter(p=>p.type==="http_request"),m=a.useMemo(()=>{const p=new Map;for(const x of g){const j=E(x.url);p.set(j,(p.get(j)||0)+1)}return Array.from(p.entries()).sort((x,j)=>j[1]-x[1]).slice(0,10).map(([x,j])=>({path:x,count:j}))},[g]),b=a.useMemo(()=>Array.from(s.values()).some(p=>p==="checked"),[s]),y=g.filter(p=>{if(r&&!p.url.toLowerCase().includes(r.toLowerCase()))return!1;const x=E(p.url);return!(s.get(x)==="excluded"||b&&s.get(x)!=="checked"||n!=="ALL"&&p.method.toUpperCase()!==n||c==="MOCKED"&&!p.mocked||c==="PASSTHROUGH"&&p.mocked)}).slice(-50).reverse();return e.jsxs("section",{children:[e.jsx(v,{title:"Live Traffic"}),e.jsx(F,{textUrlFilter:r,onTextUrlFilterChange:o,methodFilter:n,onMethodFilterChange:i,sourceFilter:c,onSourceFilterChange:p=>d(p)}),e.jsx(I,{chips:m,chipStates:s,onChipStatesChange:l}),y.length===0?e.jsx(C,{color:"#8b949e",children:g.length>0?"No matching traffic — try adjusting the filters":"No HTTP traffic yet — requests will appear here in real-time"}):e.jsx(D,{events:y})]})}const pe=Object.freeze(Object.defineProperty({__proto__:null,InspectorTrafficSection:H},Symbol.toStringTag,{value:"Module"}));function J(){const[t,r]=a.useState(null),[o,s]=a.useState(!1);return{explain:a.useCallback(async(n,i)=>{s(!0);try{const d=await(await fetch("/api/inspector/explain",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:n,url:i})})).json();r(d)}catch{}finally{s(!1)}},[]),result:t,loading:o}}function U({result:t}){return e.jsxs("div",{style:{border:"1px solid #30363d",borderRadius:6,padding:12},children:[e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,marginBottom:12},children:[e.jsxs("span",{style:{color:"#f0f6fc",fontWeight:500},children:[t.method," ",t.url]}),t.wouldMatch?e.jsx(f,{color:"#238636",children:"WOULD MATCH"}):e.jsx(f,{color:"#da3633",children:"NO MATCH"})]}),t.attempts.length===0?e.jsx("div",{style:{color:"#8b949e"},children:"No routes loaded to match against"}):e.jsx("div",{style:{display:"flex",flexDirection:"column",gap:6},children:t.attempts.map((r,o)=>e.jsxs("div",{style:{display:"flex",alignItems:"center",gap:8,padding:"4px 8px",borderRadius:4,background:r.failure?"transparent":"rgba(35, 134, 54, 0.1)"},children:[e.jsx("span",{style:{color:r.failure?"#da3633":"#238636",fontWeight:600,width:16},children:r.failure?"x":"v"}),e.jsx(O,{method:r.method}),e.jsx("span",{children:r.pattern}),r.failure&&e.jsx("span",{style:{color:"#8b949e",fontSize:11},children:r.failure.reason==="methodMismatch"?`method: expected ${r.failure.expected}, got ${r.failure.actual}`:`path "${r.failure.path}" does not match "${r.failure.pattern}"`})]},o))})]})}const ue=Object.freeze(Object.defineProperty({__proto__:null,InspectorExplainerResult:U},Symbol.toStringTag,{value:"Module"}));function W(){const{explain:t,result:r,loading:o}=J(),[s,l]=a.useState("GET"),[n,i]=a.useState(""),c=d=>{d.preventDefault(),n.trim()&&t(s,n.trim())};return e.jsxs("section",{children:[e.jsx(v,{title:"Match Explainer"}),e.jsxs("form",{onSubmit:c,style:{display:"flex",gap:8,marginBottom:12},children:[e.jsx("select",{value:s,onChange:d=>l(d.target.value),style:{...S,width:100},children:["GET","POST","PUT","DELETE","PATCH"].map(d=>e.jsx("option",{value:d,children:d},d))}),e.jsx("input",{type:"text",value:n,onChange:d=>i(d.target.value),placeholder:"/api/users",style:{...S,flex:1}}),e.jsx("button",{type:"submit",disabled:o||!n.trim(),style:{background:"#238636",color:"#fff",border:"none",borderRadius:6,padding:"6px 16px",cursor:"pointer",fontSize:12,fontWeight:500,opacity:o||!n.trim()?.5:1},children:o?"Checking...":"Explain"})]}),r&&e.jsx(U,{result:r})]})}const he=Object.freeze(Object.defineProperty({__proto__:null,InspectorExplainerSection:W},Symbol.toStringTag,{value:"Module"})),w={"/api/health":"Health check — confirms the proxy and mock engine are running","/api/events":"SSE stream of real-time events (traffic, errors) powering this Inspector","/api/config":"Returns current CodeYam configuration for the project","/api/editor-dev-server":"Polls/controls the dev server status (start, stop, restart)","/api/inspector/status":"Returns active scenario and loaded mock routes","/api/inspector/explain":"Tests which mock route would match a given method + URL","/api/scenarios":"Lists all defined scenarios with metadata","/api/tests":"Returns the AI-maintained test registry joined with per-test-evidence, grouped by file","/api/test-results":"Returns latest test execution results","/api/screenshots/*":"Serves captured scenario screenshots","/api/session-info":"Returns current editor session metadata","/api/data-structure":"Returns the app's data structure definition","/api/dependency-graph":"Returns the project's component dependency graph","/api/app-route-entities":"Lists app routes and their associated entities","/api/glossary":"Returns the project's component glossary","/api/step":"Returns current editor workflow step","/api/editor-commands":"Lists available editor commands"};function V(t){if(w[t])return w[t];for(const[r,o]of Object.entries(w))if(r.endsWith("/*")){const s=r.slice(0,-1);if(t.startsWith(s))return o}}function Y(t){const r=new Set([...t,...Object.keys(w)]),o=[];for(const s of Array.from(r).sort()){if(s.endsWith("/*"))continue;const l=V(s);o.push({path:s,description:l??"Application endpoint"})}return o}function q(){const{events:t}=k(),r=a.useMemo(()=>{const o=[];for(const s of t)s.type==="http_request"&&o.push(E(s.url));return Y(o)},[t]);return r.length===0?null:e.jsxs("section",{children:[e.jsx(v,{title:"URL Glossary"}),e.jsx("div",{style:{border:"1px solid #30363d",borderRadius:6,overflow:"hidden"},children:e.jsxs("table",{style:{width:"100%",borderCollapse:"collapse",fontSize:12},children:[e.jsx("thead",{children:e.jsxs("tr",{style:{borderBottom:"1px solid #30363d",background:"#161b22"},children:[e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px",width:"30%"},children:"Path"}),e.jsx("th",{style:{padding:"6px 12px",textAlign:"left",color:"#8b949e",fontWeight:500,fontSize:11,textTransform:"uppercase",letterSpacing:"0.5px"},children:"Description"})]})}),e.jsx("tbody",{children:r.map(({path:o,description:s})=>e.jsxs("tr",{style:{borderBottom:"1px solid #21262d"},children:[e.jsx("td",{style:{padding:"4px 12px",fontFamily:"monospace",color:"#D7FF63",fontSize:11},children:o}),e.jsx("td",{style:{padding:"4px 12px",color:"#8b949e"},children:s})]},o))})]})})]})}const xe=Object.freeze(Object.defineProperty({__proto__:null,InspectorUrlGlossary:q},Symbol.toStringTag,{value:"Module"}));function Q(){const{connected:t}=k();return e.jsxs("div",{style:{background:"#0d1117",color:"#c9d1d9",minHeight:"100vh",fontFamily:"'SF Mono', 'Fira Code', 'Cascadia Code', Menlo, monospace",fontSize:13,padding:24,display:"flex",flexDirection:"column",gap:24},children:[e.jsxs("header",{style:{display:"flex",alignItems:"center",gap:12},children:[e.jsx("h1",{style:{fontSize:18,fontWeight:600,color:"#f0f6fc",margin:0},children:"Container Inspector"}),e.jsx("span",{style:{color:"#8b949e",fontSize:12},children:"Read-only view of proxy and mock engine state"}),e.jsx(P,{connected:t})]}),e.jsx(A,{}),e.jsx(H,{}),e.jsx(W,{}),e.jsx(q,{})]})}const fe=Object.freeze(Object.defineProperty({__proto__:null,ContainerInspector:Q},Symbol.toStringTag,{value:"Module"}));export{f as B,Q as C,_ as D,L as I,O as M,v as S,xe as _,C as a,q as b,I as c,D as d,F as e,B as f,U as g,W as h,H as i,z as j,A as k,P as l,de as m,ce as n,pe as o,ae as p,ie as q,le as r,ne as s,u as t,se as u,oe as v,he as w,ue as x,fe as y,re as z};