🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@codeyam-editor/codeyam-editor

Package Overview
Dependencies
Maintainers
1
Versions
32
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@codeyam-editor/codeyam-editor - npm Package Compare versions

Comparing version
0.1.0-staging.gac4ca32
to
0.1.0-staging.gc446a6f
+155
npm/fleet-add-failure.js
'use strict';
const fc = require('./fleet-cleanup');
// npm/fleet-add-failure.js — pure classifier for a FAILED fleet-dashboard `add`
// provision. Before this, every failed add rendered an identical red "add error"
// badge whose only action was "Clear badge", and a restart mid-add collapsed the
// message to a content-free `(re-attached after restart — detached provision
// exited 1)`. The real cause — captured to the provision transcript on disk — was
// thrown away. This happened live with VM9/VM10: both aborted at the streamed-
// script drift guard because a bootstrap script had uncommitted local edits, but
// the operator saw only "exited 1" and had to dig through /tmp/dash-add-vm-N.log.
//
// classifyAddFailure turns a transcript tail + exit code into a differentiated,
// ACTIONABLE verdict: a plain-language headline, a concrete recovery instruction,
// and — crucially — a `retryable` flag that is true ONLY for failures where the
// classifier can PROVE nothing was provisioned, so a one-click retry can never
// silently orphan or duplicate a half-provisioned GCP box. The decision logic
// lives here as a pure module (like fleet-job-badge.js::canClearBadge) so the
// whole failure matrix is unit-tested without a live VM; server.js is the thin
// I/O shell that gathers the transcript and turns the verdict into a card + a
// server-re-guarded retry action.
// The SINGLE source of truth for which categories are safe to auto-retry: only
// failures where nothing was provisioned. `retryable` on every returned verdict
// is stamped from this Set, so the invariant `retryable === has(category)` holds
// by construction and the server can re-guard a retry against the same Set.
const ADD_FAILURE_RETRYABLE = new Set(['preflight-drift', 'instance-vanished']);
// Collect the ` - <path>` bullets the drift guard prints AFTER the "following
// files differ" header (see npm/cloud.js), stopping at the first non-bullet line
// so an unrelated "- " elsewhere in the transcript can't leak in. Returns [] when
// no drift block is present.
function extractDriftFiles(logTail) {
const lines = String(logTail || '').split('\n');
const files = [];
let inBlock = false;
for (const line of lines) {
if (!inBlock) {
if (/following files differ/i.test(line)) inBlock = true;
continue;
}
const m = line.match(/^\s+-\s+(.+?)\s*$/);
if (m) { files.push(m[1]); continue; }
break; // first non-bullet after the header ends the block
}
return files;
}
// Pure transform: the last `maxLines` non-empty lines of a transcript, each
// right-trimmed. Extracted from readProvisionTail (server.js) so the aggregation
// is unit-tested and that function stays a thin filesystem-boundary call. Returns
// '' for empty/whitespace-only input.
function lastNonEmptyLines(text, maxLines = 50) {
const lines = String(text || '')
.split('\n')
.map((l) => l.replace(/\s+$/, ''))
.filter((l) => l.length > 0)
.slice(-maxLines);
return lines.join('\n');
}
// Build a verdict, stamping `retryable` from the single-source-of-truth Set so it
// can never drift from the category.
function buildFailureVerdict(category, headline, recovery, extra) {
const out = { category, headline, recovery, retryable: ADD_FAILURE_RETRYABLE.has(category) };
if (extra && extra.files) out.files = extra.files;
return out;
}
// Classify a failed add from its transcript tail + exit code.
//
// logTail the last lines of the provision transcript (string; '' when the
// log was unreadable — must never throw)
// exitCode the provision's exit code (number; used for signal-kill detection)
//
// Returns { category, headline, recovery, retryable, files? }.
//
// Detection is ORDER-SENSITIVE. The provision shell echoes cloud:up's tail, so a
// drift / instance-vanished / container-stuck transcript ALSO contains the
// generic `cloud:up FAILED (exit N)` line. Each specific (and every retryable)
// signature is therefore tested BEFORE the generic cloud-up-failed signature, or
// a retryable drift would misclassify as a non-retryable cloud:up failure and the
// operator would lose the safe one-click recovery.
function classifyAddFailure({ logTail, exitCode } = {}) {
const log = String(logTail || '');
// 1. preflight-drift — the streamed-script drift guard aborted BEFORE any
// gcloud call, so nothing was provisioned. Retryable once the file is fixed.
// We match the unambiguous abort header / verdict label, NOT the bare
// `CODEYAM_SKIP_SCRIPT_DRIFT_CHECK` token: that token ALSO appears in the
// "bypass active — skipping drift check" warning, so keying on it would
// misclassify a bypass-active add that later failed for a real reason as a
// (retryable!) drift — inviting an unsafe retry. The header below is printed
// only on a genuine drift abort.
if (/Streamed scripts carry unreviewed local content/i.test(log)
|| /UNCOMMITTED LOCAL EDITS/.test(log)) {
const files = extractDriftFiles(log);
const fileList = files.length ? files.join(', ') : 'the streamed scripts';
const fix = files.length
? `Commit & push them, or discard with \`git checkout -- ${files.join(' ')}\`, then Retry add.`
: 'Commit & push them, or discard the local edits, then Retry add.';
return buildFailureVerdict('preflight-drift', 'Blocked by local edits',
`Uncommitted local edits in ${fileList} tripped the streamed-script drift guard, which aborts before any cloud resource is created. ${fix}`,
{ files });
}
// 2. instance-vanished — the GCP instance is confirmed gone; nothing to orphan.
if (/instance .* is gone/i.test(log)) {
return buildFailureVerdict('instance-vanished', 'Instance never came up',
'The GCP instance is gone — provisioning failed before the container started, so nothing was left behind. Retrying the add is safe.');
}
// 3. container-stuck — the instance exists but the editor container never
// became healthy, so a half-provisioned box may exist. Not retryable.
if (/container never came up/i.test(log)) {
return buildFailureVerdict('container-stuck', 'Container never became healthy',
'The instance came up but the editor container never became healthy — it may be half-provisioned. Reconcile or destroy this VM before re-adding.');
}
// 4. cloud-up-failed — a generic cloud:up failure, reached ONLY when the drift /
// vanished / stuck signatures above did not match. A partial instance may
// exist, so not retryable.
if (/cloud:up FAILED \(exit/i.test(log)) {
return buildFailureVerdict('cloud-up-failed', 'Provisioning failed',
'cloud:up failed during provisioning, so a partial instance may exist. Reconcile or destroy this VM before re-adding.');
}
// 5. interrupted — a signal kill (OOM 137, SIGTERM 143, …) or an explicit
// Killed / out-of-memory marker. The real VM state is unknown. Not retryable.
if (fc.isSignalKillExit(Number(exitCode)) || /\bKilled\b/.test(log) || /Out of memory/i.test(log)) {
return buildFailureVerdict('interrupted', 'Provisioning interrupted',
'The provision was interrupted (killed or out of memory), so the VM state is unknown. Reconcile or destroy this VM before re-adding.');
}
// 6. unknown — fallback. Never invite an unproven retry.
return buildFailureVerdict('unknown', 'Add failed',
'The add failed for an unclassified reason — inspect the transcript below, then reconcile or destroy this VM before re-adding.');
}
// Pure guard for the operator "Retry add" action: given a VM number and its
// carried job, decide whether a one-click retry is SAFE. Retry is allowed ONLY
// for a terminal add-error whose classifier verdict is retryable (a category
// where nothing was provisioned). Returns { ok, reason } — reason is a stable
// code the server shell (actionRetryAdd) maps to an operator message. Mirrors
// fleet-job-badge.js::canClearBadge so the handler stays a thin I/O shell and the
// safety decision is unit-tested without a live dashboard.
function canRetryAdd({ n, job } = {}) {
if (!Number.isInteger(n) || n < 1) return { ok: false, reason: 'invalid-vm' };
if (!job || job.action !== 'add' || job.state !== 'error') return { ok: false, reason: 'no-failed-add' };
if (!job.retryable) return { ok: false, reason: 'not-retryable' };
return { ok: true, reason: null };
}
module.exports = { classifyAddFailure, extractDriftFiles, lastNonEmptyLines, canRetryAdd, ADD_FAILURE_RETRYABLE };
'use strict';
// Self-healing cycle guard for the Fleet Dashboard's slow effect poll
// (scripts/operator/fleet-dashboard/server.js requires this). No gcloud/SSH
// I/O — this module owns the control-flow guarantee ONLY; the server keeps its
// gather edges and worker pools.
//
// The bug this fixes: `pollEffects` guards against overlap with a
// `effectPollRunning` flag (two concurrent full cycles would double the gcloud
// load), releasing it in a `finally` that runs only AFTER
// `await Promise.all(...)` of the reachable + launcher-strand sweeps. On
// 2026-07-09 a single probe's callback never fired (an SSH/docker-exec against
// VM-8, whose GCP instance had been deleted mid-session), so that awaited
// promise never resolved, the `finally` never ran, and `effectPollRunning`
// stayed `true` forever — every subsequent 45s tick early-returned and fleet
// readiness (iapAccess, backendHealth, readiness, launcher-strand self-heal)
// froze fleet-wide until the dashboard was restarted.
//
// Two guarantees make that freeze impossible:
// makeGuardedCycle — the no-overlap guard is kept (it's correct), but its
// release is made unconditional: the returned promise ALWAYS settles, either
// when the tasks finish or when a whole-cycle watchdog fires, and the guard
// flag is cleared in a `finally` either way. A hung task can no longer strand
// the next tick.
// withTimeout — wraps a single probe promise so a never-firing callback
// resolves to a safe fallback verdict instead of hanging the sweep.
// Wrap a promise so it can never hang the caller: resolve to `fallback` if
// `promise` has not settled within `ms`, and also on rejection (a probe that
// throws must degrade gracefully, never reject the whole sweep). `ms <= 0`
// disables the timeout and just awaits the promise (rejections still fold to
// `fallback`), so a caller can opt a leg out without branching. Whichever of
// (settle, timeout) happens first wins; the other is a no-op.
function withTimeout(promise, ms, fallback) {
const settled = Promise.resolve(promise).catch(() => fallback);
if (!(ms > 0)) return settled;
return new Promise((resolve) => {
let done = false;
const finish = (val) => { if (!done) { done = true; resolve(val); } };
// NOT unref'd: this timer is load-bearing — it resolves the promise the
// caller awaits, so letting the event loop exit before it fires would leave
// the await hung. It is cleared the instant the real promise settles, so it
// holds the loop open only for the (short) timeout window.
const timer = setTimeout(() => finish(fallback), ms);
settled.then((val) => { clearTimeout(timer); finish(val); });
});
}
// Build a guarded cycle runner. `run(tasks)` executes an array of zero-arg async
// task factories (or bare promises) under a no-overlap guard:
// - while a cycle is in flight, a second `run(...)` returns `{ skipped: true }`
// immediately without launching the tasks — the no-overlap contract.
// - the returned promise ALWAYS settles: when the tasks finish, or — if
// `watchdogMs > 0` — when the whole-cycle watchdog fires first (returning
// `{ wedged: true }`). Either way the guard flag is cleared in a `finally`,
// so the NEXT tick can always run. A never-resolving task therefore cannot
// permanently wedge the loop (the 2026-07-09 freeze).
// `onWedge(elapsedMs)` (optional) is called when the watchdog fires, so the
// caller can log a wedge warning. A task that rejects is swallowed (one bad probe
// must not fail the whole cycle); the caller composes concurrency/ordering.
function makeGuardedCycle(opts) {
const o = opts || {};
const watchdogMs = o.watchdogMs > 0 ? o.watchdogMs : 0;
const onWedge = typeof o.onWedge === 'function' ? o.onWedge : null;
let running = false;
const run = async (tasks) => {
if (running) return { skipped: true };
running = true;
try {
const work = Promise.all((tasks || []).map((t) => (typeof t === 'function' ? t() : t)))
.then(() => false, () => false); // settle to `wedged=false`; swallow a rejecting probe
let wedged = false;
if (watchdogMs > 0) {
wedged = await new Promise((resolve) => {
// NOT unref'd: this watchdog resolves the promise the caller awaits, so
// it must keep the loop alive until it fires (or the work settles first
// and clears it). Unref'ing it would let a standalone await hang.
const timer = setTimeout(() => resolve(true), watchdogMs);
work.then((w) => { clearTimeout(timer); resolve(w); });
});
} else {
await work;
}
if (wedged && onWedge) { try { onWedge(watchdogMs); } catch (e) { /* logging must never re-wedge */ } }
return { skipped: false, wedged };
} finally {
// Unconditional release — the whole point. Whether the tasks finished or
// the watchdog fired, the guard is cleared so the next tick proceeds. Any
// still-pending task keeps running detached, but holds no guard.
running = false;
}
};
return { run };
}
// A roster entry whose Add errored out is an `add-failed` ghost: its GCP instance
// may be half-created or already reconciled away, so an SSH/docker-exec strand
// probe against it is the exact call that hung the 2026-07-09 sweep (VM-8). The
// strand selection skips it — a failed Add is owned by the reachable / vanish
// reconcilers, not the launcher-strand self-heal. `withTimeout` (above) is the
// general backstop for any hung probe; this removes the doomed probe at the
// source for the named trigger.
function isAddFailedGhost(job) {
return !!(job && job.state === 'error' && job.action === 'add');
}
module.exports = {
withTimeout,
makeGuardedCycle,
isAddFailedGhost,
};
import{j as e}from"./markdown-v0F0UEt3.js";import{b as a}from"./react-CSS0HapR.js";import{u as O}from"./useEvents-DHkLJZR4.js";import{b as E}from"./index-iE2jFez3.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}=O(),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 k({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:k,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(k,{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(`
`)})}),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(k,{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}=O(),[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(k,{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}=O(),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}=O();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,k 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};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

import{j as c}from"./markdown-v0F0UEt3.js";import{b as o}from"./react-CSS0HapR.js";import{S as l}from"./ScenarioDataPanel-BU2tHhl4.js";function m({slug:t}){const[r,i]=o.useState(void 0);return o.useEffect(()=>{let n=!1;return fetch(`/api/scenarios/${encodeURIComponent(t)}`).then(e=>e.ok?e.json():null).then(e=>{if(n||!e)return;const a=typeof e=="object"&&e!==null&&"name"in e?String(e.name):void 0;i(a),a&&(document.title=`${a} · data`)}).catch(()=>{}),()=>{n=!0}},[t]),c.jsx(l,{slug:t,scenarioName:r,variant:"fullPage"})}const p=Object.freeze(Object.defineProperty({__proto__:null,ScenarioDataPanelFullPage:m},Symbol.toStringTag,{value:"Module"}));export{m as S,p as _};

Sorry, the diff of this file is too big to display

+92
-7
"use strict";
/**
* Launcher-side mirror of `is_empty_project_dir` in
* Launcher-side mirrors of `is_empty_project_dir` and
* `looks_like_onboardable_project` in
* `crates/codeyam-editor/src/commands/init.rs`. The Rust side decides
* which `/codeyam-…` skill the init binary points at; this JS mirror
* decides the same for the launcher's post-launch tail banner. Keep
* both in sync — the test `empty-project-dir.test.js` and Rust's
* `is_empty_project_dir_ignores_hidden_entries` /
* `is_empty_project_dir_detects_visible_entry` pin the contract.
* which `/codeyam-…` skill the init binary points at; these JS mirrors
* decide the same for the launcher's post-launch tail banner. Keep both
* predicates in sync with Rust — the test `empty-project-dir.test.js`
* and Rust's `is_empty_project_dir_*` / `looks_like_onboardable_project_*`
* tests pin the contracts.
*

@@ -22,2 +23,3 @@ * A "fresh" project directory has no non-hidden entries — only things

const fs = require("node:fs");
const path = require("node:path");

@@ -34,2 +36,85 @@ function isEmptyProjectDir(projectDir) {

module.exports = { isEmptyProjectDir };
// Root files that signal a real project: dependency/build manifests,
// lockfiles, and the static-site `index.html`. Mirror of the Rust
// `MANIFEST_MARKERS` list.
const MANIFEST_MARKERS = new Set([
"package.json",
"Cargo.toml",
"pyproject.toml",
"requirements.txt",
"setup.py",
"go.mod",
"Gemfile",
"pom.xml",
"build.gradle",
"build.gradle.kts",
"composer.json",
"pubspec.yaml",
"Package.swift",
"mix.exs",
"deno.json",
"CMakeLists.txt",
"Makefile",
"tsconfig.json",
"index.html",
]);
// Subdirectories that signal an organized source tree. Mirror of the Rust
// `SOURCE_DIR_MARKERS` list.
const SOURCE_DIR_MARKERS = new Set([
"src",
"app",
"lib",
"pages",
"components",
"crates",
"packages",
"source",
"include",
]);
/**
* Does this directory contain a *project* worth onboarding, as opposed to
* being empty or holding only a stray file or two? Mirror of Rust
* `looks_like_onboardable_project`. A folder "contains a project" when it
* holds a recognized manifest / lockfile, a `.csproj` / `.xcodeproj`, a root
* `index.html`, OR an organized source subdirectory. Intentionally
* marker-based, not file-count based: a home dir full of loose files is still
* not a project. A read error returns false (not a project → the
* auto-init/open path), matching Rust's `Err(_) => false`.
*/
function looksLikeOnboardableProject(projectDir) {
let entries;
try {
entries = fs.readdirSync(projectDir, { withFileTypes: true });
} catch {
return false;
}
for (const entry of entries) {
const name = entry.name;
if (MANIFEST_MARKERS.has(name)) {
return true;
}
if (name.endsWith(".csproj") || name.endsWith(".xcodeproj")) {
return true;
}
// Source-tree markers only count as directories, not like-named files.
if (SOURCE_DIR_MARKERS.has(name)) {
let isDir = entry.isDirectory();
// A symlinked source dir surfaces as a symlink Dirent; resolve it.
if (!isDir && entry.isSymbolicLink()) {
try {
isDir = fs.statSync(path.join(projectDir, name)).isDirectory();
} catch {
isDir = false;
}
}
if (isDir) {
return true;
}
}
}
return false;
}
module.exports = { isEmptyProjectDir, looksLikeOnboardableProject };

@@ -37,2 +37,28 @@ 'use strict';

/**
* Indexes the reachable VMs by their fleet machine name, so a claim that only
* carries `claimedBy.machine` (and no resolvable sessionId) can still attribute
* to a VM. A VM's machine name is `<instancePrefix><n>` (the GCE instance name
* the editor stamps into CODEYAM_MACHINE), so the map is derived, not probed.
*
* This is the fallback for backend/HTTP-"Run" claims: the editor stamps
* `claimedBy.sessionId` only for a PTY-attached `plan-select`, so a
* server-driven autonomous claim has an empty sessionId and never resolves via
* {@link indexVmsBySessionId}. Its machine name always resolves.
* @param {Array|Object} vms The merged VM states on the dashboard.
* @param {string} instancePrefix The fleet instance-name prefix (INSTANCE_PREFIX).
* @returns {Object} A map of machine name -> VM number (n).
*/
function indexVmsByMachine(vms, instancePrefix) {
const map = {};
if (!vms || !instancePrefix) return map;
const list = Array.isArray(vms) ? vms : Object.values(vms);
for (const vm of list) {
if (vm && vm.n !== undefined) {
map[`${instancePrefix}${vm.n}`] = vm.n;
}
}
return map;
}
/**
* Computes which dependencies are unsatisfied (i.e. not in the completedSlugs set).

@@ -56,5 +82,7 @@ * @param {string[]} dependsOn Prerequisite slugs.

* @param {Object} params.vmsBySessionId Map of sessionId -> VM number.
* @param {Object} [params.vmsByMachine] Map of machine name -> VM number, the
* fallback for backend claims whose sessionId is empty.
* @returns {Object[]} Annotated plan rows.
*/
function resolvePlanRows({ plans, claims, completedSlugs, vmsBySessionId }) {
function resolvePlanRows({ plans, claims, completedSlugs, vmsBySessionId, vmsByMachine }) {
const list = Array.isArray(plans) ? plans : [];

@@ -81,4 +109,10 @@ const claimsList = Array.isArray(claims) ? claims : [];

const sessionId = claimedBy.sessionId || claimedBy.session_id || null;
const vm = (sessionId && vmsBySessionId && vmsBySessionId[sessionId] !== undefined) ? vmsBySessionId[sessionId] : null;
// Prefer the authoritative live sessionId match; fall back to the claim's
// machine name (backend claims have an empty sessionId, so this is the
// only key that resolves them to a VM).
let vm = (sessionId && vmsBySessionId && vmsBySessionId[sessionId] !== undefined) ? vmsBySessionId[sessionId] : null;
if (vm === null && claimedBy.machine && vmsByMachine && vmsByMachine[claimedBy.machine] !== undefined) {
vm = vmsByMachine[claimedBy.machine];
}
claimInfo = {

@@ -242,2 +276,3 @@ vm,

indexVmsBySessionId,
indexVmsByMachine,
unsatisfiedDeps,

@@ -244,0 +279,0 @@ resolvePlanRows,

@@ -25,4 +25,7 @@ 'use strict';

// { "vms": { "<n>": { status, branch, freshAt, claimedAt } } }
// status: "pool" idle, claim-ready
// "assigned" claimed by an operator, rebranded to `branch`
// status: "pool" idle, claim-ready (authenticated + push-ready)
// "pending-auth" reachable + LB-wired but awaiting operator auth;
// counts toward the provisioning target (so the
// keeper converges) but is NOT claimable
// "assigned" claimed by an operator, rebranded to `branch`
// branch: the branch the VM is on (base branch while pooled)

@@ -49,3 +52,17 @@ // freshAt: ISO timestamp of the last successful auth/health refresh —

const ASSIGNED = 'assigned';
// A VM that passed every readiness item EXCEPT provider auth (reachable,
// on-branch, broker up, editor+preview 200, git-push-ready). Provisioning has
// converged — the VM exists and is LB-wired — so it counts toward the refill
// target and the keeper stops churning; but it is never claimable, so the claim
// path still hands out only fully-authenticated VMs (the 2026-06-29 guarantee).
const PENDING_AUTH = 'pending-auth';
// Canonical status coercion: an unrecognized status is not a pool member.
function coerceStatus(status) {
if (status === ASSIGNED) return ASSIGNED;
if (status === PENDING_AUTH) return PENDING_AUTH;
if (status === POOL) return POOL;
return null;
}
// ── Pure state helpers ──────────────────────────────────────────────────────

@@ -66,3 +83,3 @@

const e = vms[key] || {};
const status = e.status === ASSIGNED ? ASSIGNED : e.status === POOL ? POOL : null;
const status = coerceStatus(e.status);
if (!status) continue;

@@ -97,2 +114,9 @@ out.vms[n] = {

// VMs provisioned + LB-wired but awaiting operator auth. They count toward the
// refill target (provisioning has converged) yet are excluded from poolMembers,
// so selectForClaim never hands one out.
function pendingAuthMembers(state) {
return membersByStatus(state, PENDING_AUTH);
}
// True when a pool entry's auth/health is older than `ttlMs` (its token may

@@ -116,8 +140,12 @@ // have lapsed). An entry with no `freshAt` is treated as stale — "freshness

// How many NEW pool VMs the keeper must provision to reach `target` idle
// members. Assigned VMs do not count toward the pool — once claimed they're
// gone from the warm set until released — so a claim naturally triggers a
// refill. Never negative (an over-target pool is shrunk by policy, not here).
// How many NEW VMs the keeper must provision to reach `target`. `have` counts
// BOTH claim-ready pool members AND pending-auth members: provisioning has
// converged once N reachable+wired VMs exist, regardless of auth, so the deficit
// closes and the keeper stops churning (the 2026-07-09 endless-refill fix). Only
// the provisioning denominator widens — poolMembers (claim eligibility) is
// untouched, so a claim still selects only fully-authenticated VMs. Assigned VMs
// do not count (claimed → gone from the warm set until released). Never negative
// (an over-target pool is shrunk by policy, not here).
function refillCount(state, target) {
const have = poolMembers(state).length;
const have = poolMembers(state).length + pendingAuthMembers(state).length;
return Math.max(0, target - have);

@@ -149,12 +177,14 @@ }

// Register (or overwrite) a VM in the pool state. The keeper calls this after
// provisioning a fresh READY VM (status=pool) and the claim path can re-add an
// assigned VM. `freshAt` defaults to now for a pool member so a just-verified
// VM isn't immediately stale.
// provisioning: status=pool for a fully-READY VM, status=pending-auth for one
// that is reachable+wired but still awaiting operator auth; the claim path can
// re-add an assigned VM. `freshAt` is stamped to now for a pool OR pending-auth
// member (both were just verified live) so neither is immediately stale; an
// unknown status coerces to pool.
function applyAdd(state, n, status, branch, nowIso) {
const next = normalizeState(state);
const st = status === ASSIGNED ? ASSIGNED : POOL;
const st = coerceStatus(status) || POOL;
next.vms[n] = {
status: st,
branch: branch || null,
freshAt: st === POOL ? nowIso : (next.vms[n] && next.vms[n].freshAt) || null,
freshAt: st === ASSIGNED ? (next.vms[n] && next.vms[n].freshAt) || null : nowIso,
claimedAt: st === ASSIGNED ? nowIso : null,

@@ -197,2 +227,14 @@ };

// Promote a pending-auth VM to a claim-ready pool member — the dashboard calls
// this the instant an operator completes that VM's OAuth, so it becomes
// claimable with NO re-provision. `freshAt` resets to now (it was just proven
// authenticated). A no-op unless the VM is currently pending-auth, so it is safe
// to call for any authenticated VM (an already-pooled or absent VM is untouched).
function applyPromote(state, n, nowIso) {
const next = normalizeState(state);
if (!next.vms[n] || next.vms[n].status !== PENDING_AUTH) return next;
next.vms[n] = { ...next.vms[n], status: POOL, freshAt: nowIso, claimedAt: null };
return next;
}
// ── Atomic file layer ───────────────────────────────────────────────────────

@@ -270,5 +312,7 @@

ASSIGNED,
PENDING_AUTH,
normalizeState,
poolMembers,
assignedMembers,
pendingAuthMembers,
isStale,

@@ -282,2 +326,3 @@ staleMembers,

applyMarkFresh,
applyPromote,
withLock,

@@ -324,2 +369,5 @@ readState,

return 0;
case 'pending-auth-members':
process.stdout.write(`${pendingAuthMembers(readState()).join(' ')}\n`);
return 0;
case 'refill-count': {

@@ -370,4 +418,6 @@ const target = Number(flags.target);

}
const status = flags.status === ASSIGNED ? ASSIGNED : POOL;
withLock(() => writeState(applyAdd(readState(), vm, status, flags.branch, now)));
// applyAdd coerces the status (pool | pending-auth | assigned; unknown →
// pool), so pass the raw flag through — this is how the keeper adds a
// reachable-but-unauthed VM with `--status pending-auth`.
withLock(() => writeState(applyAdd(readState(), vm, flags.status, flags.branch, now)));
return 0;

@@ -402,5 +452,14 @@ }

}
case 'promote': {
const vm = Number(flags.vm);
if (!Number.isInteger(vm) || vm <= 0) {
process.stderr.write('fleet-pool: promote needs --vm <pos int>\n');
return 2;
}
withLock(() => writeState(applyPromote(readState(), vm, now)));
return 0;
}
default:
process.stderr.write(
'fleet-pool: usage: <list|pool-members|assigned-members|refill-count|stale|claim|add|release|evict|mark-fresh> [--flags]\n',
'fleet-pool: usage: <list|pool-members|assigned-members|pending-auth-members|refill-count|stale|claim|add|release|evict|mark-fresh|promote> [--flags]\n',
);

@@ -407,0 +466,0 @@ return 2;

@@ -122,2 +122,35 @@ 'use strict';

/** Aggregate the per-surface IAP verdicts (editor + preview) into a single fleet
* verdict. PURE — takes `{ editor, preview }`, each a `classifyIapAccess`
* result, and returns:
* 'unknown' — either surface's read errored (absence of evidence,
* never manufactured as a fault).
* 'no-accessor' — the EDITOR backend has no accessor: the URL the
* operator opens 51s for everyone. The primary gap
* (dominates a preview gap — the editor is unusable).
* 'preview-no-accessor' — the editor grants access but the PREVIEW backend has
* no accessor. The exact shape that hid this bug: the
* editor loads, but the framed Live Preview (served
* from the separate preview host) is blank for every
* authorized user because nobody is granted there.
* 'granted' — BOTH surfaces authorize someone.
*
* The preview verdict is optional: an older dashboard that only gathered the
* editor surface passes `preview === undefined`, and the result collapses to the
* single-surface classification (never a phantom preview gap). */
function aggregateIapAccess({ editor, preview } = {}) {
// Absence of evidence on either surface is never a fault.
if (editor === 'unknown' || preview === 'unknown') return 'unknown';
// The editor is the URL the operator opens; if IT lacks an accessor, that is
// the primary gap and dominates any preview verdict.
if (editor !== 'granted') return 'no-accessor';
// Editor granted. With no preview verdict gathered, fall back to the
// editor-only 'granted' (backward-compatible with single-surface dashboards).
if (preview === undefined || preview === null) return 'granted';
// Editor granted but the preview host authorizes nobody — the framed preview
// is blank for authorized users even though the editor works.
if (preview !== 'granted') return 'preview-no-accessor';
return 'granted';
}
// The fleet minimum backend-service `timeoutSec` a WebSocket needs to survive

@@ -269,2 +302,3 @@ // behind the External HTTPS LB. The LB severs a backend connection at

classifyIapAccess,
aggregateIapAccess,
classifyWsTimeout,

@@ -271,0 +305,0 @@ FLEET_WS_TIMEOUT_MIN_SEC,

@@ -77,2 +77,59 @@ 'use strict';

module.exports = { decideAutoReleaseGoneHead, computeReorderTargets };
/** Move one uuid within a desired order array.
*
* The staged-draft panel shuffles a working uuid order before it is saved.
* `delta > 0` raises priority (moves the entry TOWARD the head / index 0);
* `delta < 0` lowers it (toward the tail). The target index is clamped to the
* array bounds, so a nudge past an end is a no-op and a full-span negative
* delta lands the entry at the very tail — which is exactly how Demote
* (head → back of the line) is expressed: `nudgeQueueOrder(order, head, -(N-1))`.
* Mirrors the editor UI's `useBranchQueueDraft.nudge` (arrayMove(from, from-delta)).
*
* `order` is a uuid array; a uuid not present is returned unchanged. Pure — the
* input array is never mutated.
*/
function nudgeQueueOrder(order, uuid, delta) {
const arr = (order || []).slice();
const from = arr.findIndex((u) => u === uuid);
if (from < 0 || !delta) return arr;
let to = from - delta; // +delta → toward head; -delta → toward tail
if (to < 0) to = 0;
if (to > arr.length - 1) to = arr.length - 1;
if (to === from) return arr;
const [moved] = arr.splice(from, 1);
arr.splice(to, 0, moved);
return arr;
}
/** Merge a fresh poll's membership into a staged draft order without discarding
* the operator's in-progress reorder.
*
* - uuids still present live keep their DRAFT position (the working order wins);
* - draft uuids that vanished upstream are dropped;
* - live uuids not yet in the draft are appended at the tail so a sibling
* session's newly-enqueued entry doesn't silently disappear.
*
* Pure port of the editor UI's `reconcileDraft` order rule (uuid-only, no meta).
* Both arguments are uuid arrays; returns the merged uuid array. Never mutates
* its inputs.
*/
function reconcileQueueDraft(draftOrder, liveOrder) {
const live = liveOrder || [];
const liveSet = new Set(live);
const seen = new Set();
const out = [];
for (const u of draftOrder || []) {
if (liveSet.has(u) && !seen.has(u)) { out.push(u); seen.add(u); }
}
for (const u of live) {
if (!seen.has(u)) { out.push(u); seen.add(u); }
}
return out;
}
module.exports = {
decideAutoReleaseGoneHead,
computeReorderTargets,
nudgeQueueOrder,
reconcileQueueDraft,
};

@@ -127,7 +127,44 @@ 'use strict';

// Read the standing default-operator principal(s) from `.codeyam/cloud.json`'s
// `vmAccessDefault` key — the durable per-project config that resolves the default
// operator even when a launchd/systemd env was never re-baked with
// `VM_ACCESS_DEFAULT`. Returns the raw config value ('' when the file is missing or
// malformed, or the key is absent / not a string) so a broken cloud.json is treated
// as "no config" and never throws. Split out (like defaultGcloudAccountExec) so the
// cloud.json layer of resolveDefaultOperator is injectable/unit-testable without a
// real file. `.codeyam/cloud.json` is resolved via `rootDir` — the same repo root
// the access sidecar and other state files use. `cloudJsonPath` is a parameter
// (defaulting to that resolved path) so the reader is unit-testable against a temp
// file, the same injectable-for-testability posture as defaultGcloudAccountExec.
// The path also honors `VM_ACCESS_CLOUD_JSON` — the same env-override isolation
// posture as `VM_ACCESS_FILE` above — so a test process (or an operator) can point
// this read at a throwaway path and never pick up the developer's real
// `.codeyam/cloud.json` `vmAccessDefault`.
function defaultCloudConfigRead(
cloudJsonPath = process.env.VM_ACCESS_CLOUD_JSON || path.join(rootDir, '.codeyam', 'cloud.json'),
) {
try {
const raw = fs.readFileSync(cloudJsonPath, 'utf8');
const data = JSON.parse(raw);
if (data && typeof data === 'object' && !Array.isArray(data)) {
return typeof data.vmAccessDefault === 'string' ? data.vmAccessDefault : '';
}
} catch { /* missing or malformed cloud.json → no config */ }
return '';
}
/** Resolve the standing default-operator principal(s) that must be granted IAP
* access whenever a VM's IAP is enabled — the fix for "IAP enabled but nobody
* granted" (the Error-51-by-default footgun). Config first:
* 1. `VM_ACCESS_DEFAULT` (the config knob) — normalized and returned.
* 2. Fallback: the active gcloud account, so a HUMAN operator running these
* granted" (the Error-51-by-default footgun). Config first, most-specific to
* least:
* 1. `VM_ACCESS_DEFAULT` (the env knob) — normalized and returned.
* 2. `.codeyam/cloud.json`'s `vmAccessDefault` — the DURABLE per-project config,
* consulted when the env var is unset. This closes the gap that made every
* fleet Add grant nobody: the dashboard/Add runs as the automation SERVICE
* ACCOUNT, and with no `VM_ACCESS_DEFAULT` in the (un-re-baked) launchd env
* the resolver used to fall straight through to the gcloud fallback and skip
* the SA — resolving `[]`. Reading cloud.json here means every caller
* (iap-access.sh, cloud.js, the dashboard) inherits the default operator
* without an env re-bake.
* 3. Fallback: the active gcloud account, so a HUMAN operator running these
* scripts locally gets access with no extra config. A SERVICE-ACCOUNT

@@ -137,7 +174,8 @@ * session (the automation SA, whose account ends in `gserviceaccount.com`)

* and the human/ops identity it should grant is unknowable from the SA
* session, so config (`VM_ACCESS_DEFAULT`) is REQUIRED there.
* session, so config (env or cloud.json) is REQUIRED there.
* Returns a normalized principal array (possibly empty — callers treat empty as
* "no default operator configured" and keep their prior behavior). `exec` is
* injectable so the gcloud fallback is unit-testable. */
function resolveDefaultOperator({ env = process.env, exec } = {}) {
* "no default operator configured" and keep their prior behavior). `exec` and
* `readCloudConfig` are injectable so the gcloud and cloud.json fallbacks are
* unit-testable without a real gcloud / file. */
function resolveDefaultOperator({ env = process.env, exec, readCloudConfig } = {}) {
const configured = env[DEFAULT_OPERATOR_ENV];

@@ -147,2 +185,8 @@ if (configured != null && String(configured).trim() !== '') {

}
let fromCloud = '';
try { fromCloud = String((readCloudConfig || defaultCloudConfigRead)() || '').trim(); }
catch { fromCloud = ''; }
if (fromCloud !== '') {
try { return normalizeAccessList(fromCloud); } catch { return []; }
}
let account = '';

@@ -155,2 +199,24 @@ try { account = String((exec || defaultGcloudAccountExec)() || '').trim(); }

/** The loud degraded reason(s) to attach to a freshly-added VM whose IAP would
* authorize NOBODY. Fires only for a SUCCESSFUL `add` (`ok`) in LB mode (`lbMode`
* — a FLEET_DOMAIN is configured so the VM's public URL is IAP-gated) when the
* default operator resolved empty (`resolvedOperatorCount === 0`): the automation-
* SA identity with no `VM_ACCESS_DEFAULT` env AND no `.codeyam/cloud.json`
* `vmAccessDefault` fallback (the 2026-07-09 footgun). In that case the URL comes
* up authorizing nobody yet the add reads as "done" — so we surface the exact
* remedy on the card instead of a silently gated URL. `resolvedOperatorCount` is
* `null` when the caller skipped the (gcloud) resolve because the cheap gates
* already failed; the `!== 0` test covers both null and a positive count. Returns a
* single actionable reason or [] so the dashboard can spread it into
* `degradedReasons` with no conditional at the call site. Pure (no I/O): the string
* lives in exactly one place and the trigger is unit-testable. */
function noDefaultOperatorDegradedReasons({ ok, action, lbMode, resolvedOperatorCount, vmNumber } = {}) {
if (!ok || action !== 'add' || !lbMode || resolvedOperatorCount !== 0) return [];
return [
'iap-default-operator-missing: IAP default operator not configured — set '
+ 'VM_ACCESS_DEFAULT or cloud.json vmAccessDefault (the public URL will '
+ `authorize nobody; run scripts/gcp/iap-access.sh ${vmNumber} grant after configuring).`,
];
}
/** Union the default-operator principal(s) into an access list, deduped and

@@ -292,2 +358,4 @@ * order-preserving (explicit collaborators first, the operator appended if not

resolveDefaultOperator,
defaultCloudConfigRead,
noDefaultOperatorDegradedReasons,
withDefaultOperator,

@@ -294,0 +362,0 @@ parseProjectMembers,

@@ -39,8 +39,16 @@ 'use strict';

// until re-wired. The state that had NO signal before.
// 'iap-access-missing' — LB mode, the backend service is IAP-enabled but has NO
// accessor binding, so IAP authorizes nobody: the public URL
// returns "Error code 51" for everyone even when the backend is
// healthy. Distinct from 'half-wired' (there the backend is
// 'iap-access-missing' — LB mode, the EDITOR backend service is IAP-enabled but
// has NO accessor binding, so IAP authorizes nobody: the public
// URL returns "Error code 51" for everyone even when the backend
// is healthy. Distinct from 'half-wired' (there the backend is
// absent; here it exists and serves, but nobody's granted) — the
// exact "wired + IAP-enabled but 51s in the browser" gap.
// 'preview-iap-access-missing' — LB mode, the editor surface grants access but
// the dedicated PREVIEW backend has no accessor binding. The
// editor URL works, but the framed Live Preview (served from the
// separate preview host on client-routed stacks) is blank for
// every authorized user because nobody is granted THERE. A
// softer, non-URL-blocking warning than 'iap-access-missing'
// (the editor link is live) — the monitoring blind spot the
// editor-only IAP probe used to hide.
// 'starting' — LB mode, the editor backend exists but is UNHEALTHY (LB has

@@ -80,2 +88,3 @@ // not yet marked it HEALTHY). The transient 503 case; flips to

'iap-access-missing',
'preview-iap-access-missing',
'starting',

@@ -210,2 +219,10 @@ 'broker-down',

if (usability === 'needs-login') return 'needs-login';
// 6b. Preview-host IAP gap: the editor surface grants access (the URL routes
// and the box is otherwise ready) but the dedicated preview backend has no
// accessor, so the framed Live Preview is blank for authorized users. A
// softer warning than the editor 'iap-access-missing' — it does NOT block
// the editor link (see URL_BLOCKED_READINESS) — so it is checked last, just
// before 'ready'. Only an EXPLICIT 'preview-no-accessor' verdict fires;
// absence/'unknown' is never a fault. Gated on lbMode like its editor twin.
if (c.lbMode && v.iapAccess === 'preview-no-accessor') return 'preview-iap-access-missing';
// 7. Reachable, broker up, WS carries frames, agent usable, and (LB mode) the

@@ -212,0 +229,0 @@ // editor backend is HEALTHY — genuinely ready: green, clickable, the public

@@ -164,2 +164,3 @@ #!/usr/bin/env node

isDeclaredErrorMock,
unmockedRouteFrom,
} = require("./scenario-mocks");

@@ -713,3 +714,16 @@

let interactionRetried = false;
// Non-fatal warnings raised while resolving interaction/flow targets — e.g. a
// substring text match that hit more than one element. Surfaced on the result
// so the agent sees the ambiguity instead of a silently-wrong first-match.
const interactionWarnings = [];
const issues = [];
// Actionable set for self-healing: same-origin routes that returned 4xx with
// no scenario mock. A hermetic-proxy/upstream 404 is a *successful* HTTP
// response with status 404 (not a Playwright `requestfailed`), so the only
// place with method + path + status together is the `page.on("response")`
// collector below. De-duped by `"<METHOD> <path>"`; surfaced through
// `buildResult` so the Rust failure message can name each unmocked route and
// `stub-unmocked-routes` can add stub mocks for them.
const unmockedRoutes = [];
const unmockedRouteKeys = new Set();
// Origin of the captured page, used to classify failed requests: only a

@@ -798,2 +812,26 @@ // failure on the page's OWN origin should fail the capture (see

page.on("response", (response) => {
// Collect same-origin 4xx responses that no scenario mock covers — the set
// a stub mock would fix. Mirrors the console-guard predicates exactly so the
// reported set matches the guard's tolerances: a scenario that provokes
// errors by design (`expectedConsoleErrors`) reports none, and a route the
// scenario already declares an error mock for is the mock's intended 4xx,
// not a missing mock.
if (expectedConsoleErrors) return;
const req = response.request();
const reqUrl = req.url();
// Same-origin only — mirror the console/requestfailed origin gate so a
// cross-origin sub-resource 4xx is never reported as an unmocked route.
if (!isCaptureFatalRequestFailure(reqUrl, appOrigin)) return;
// The status-threshold, declared-mock, and path-extraction decision is the
// pure `unmockedRouteFrom` helper; the listener keeps only the stateful
// same-origin gate above and the dedup below.
const route = unmockedRouteFrom(req.method(), reqUrl, response.status(), httpMocks);
if (!route) return;
const key = `${route.method} ${route.path}`;
if (unmockedRouteKeys.has(key)) return;
unmockedRouteKeys.add(key);
unmockedRoutes.push(route);
});
page.on("requestfailed", (request) => {

@@ -1026,2 +1064,3 @@ // Per-scenario allowance: the broken-image fallback scenario's `<img>` 404

harnessOrigin: resolvedHarnessOrigin,
warnings: interactionWarnings,
});

@@ -1037,3 +1076,5 @@ } else if (config.interaction) {

// capture with the candidate-labels hint — never a silent blank shot.
await performInteraction(frame, config.interaction);
await performInteraction(frame, config.interaction, {
warnings: interactionWarnings,
});
await waitForStablePage(page, frame, 5000, loadingMarkers);

@@ -1048,2 +1089,4 @@

await new Promise((resolve) => setTimeout(resolve, 500));
// The retry re-resolves the same target; suppress its ambiguity warning
// (already captured on the first attempt) to avoid a duplicate.
await performInteraction(frame, config.interaction);

@@ -1067,2 +1110,3 @@ await waitForStablePage(page, frame, 5000, loadingMarkers);

loadingMarkers,
warnings: interactionWarnings,
});

@@ -1095,2 +1139,3 @@ }

url: frame.url() || url,
unmockedRoutes,
});

@@ -1103,2 +1148,10 @@

// Surface any non-fatal interaction/flow warnings (e.g. an ambiguous
// substring text match) so the CLI can print them and the agent can switch
// to a precise selector. Only attached when present — a clean run is
// byte-for-byte unchanged.
if (interactionWarnings.length > 0) {
result.warnings = interactionWarnings.slice();
}
// `capture-state` mode: attach the read-only page-state snapshot so the

@@ -1131,2 +1184,3 @@ // backend can report localStorage + rendered text. Off by default, so a

url,
unmockedRoutes,
});

@@ -1133,0 +1187,0 @@ } finally {

@@ -101,6 +101,12 @@ const { createIssue } = require("./scenario-issues");

// Ignore known dev-server WebSocket/HMR errors from Vite proxy
// Ignore known dev-server WebSocket/HMR errors from Vite proxy, plus the
// crxjs/Vite dynamic-import reload race: dev loaders `import()` the app entry
// with a `?t=<timestamp>` cache-buster, and a rapid scenario-reactivation
// reload aborts the in-flight import, logging "TypeError: Failed to fetch
// dynamically imported module" (Chrome) or "error loading dynamically
// imported module" (Vite). Benign — the entry reloads on the next nav.
if (
text.includes("WebSocket connection to") ||
text.includes("Unsupported Media Type")
text.includes("Unsupported Media Type") ||
text.includes("dynamically imported module")
) {

@@ -126,3 +132,9 @@ return null;

return createIssue("console", text);
// Attach the offending resource URL the console message already exposes, so
// the capture-failure message can name the unmocked route (e.g.
// `GET /api/questions/<id>/research`) instead of an opaque "Failed to load
// resource". `message.location().url` is the resource that produced the
// error; `format_issue` renders it as `[<url>]` when populated.
const url = message.location && message.location().url;
return createIssue("console", text, url ? { url } : {});
}

@@ -129,0 +141,0 @@

@@ -20,3 +20,10 @@ function createIssue(kind, message, extra = {}) {

function buildResult({ loaded, hasContent, issues, outputPath, url }) {
function buildResult({
loaded,
hasContent,
issues,
outputPath,
url,
unmockedRoutes = [],
}) {
return {

@@ -29,2 +36,7 @@ ok: loaded && hasContent && issues.length === 0,

issues,
// Diagnostic-only: same-origin 4xx routes with no scenario mock. Does NOT
// affect `ok` — the paired console error already fails the capture; this is
// the actionable route list the failure message and `stub-unmocked-routes`
// consume. Defaults to `[]` so callers that omit it are unchanged.
unmockedRoutes,
};

@@ -31,0 +43,0 @@ }

@@ -1,18 +0,94 @@

function normalizeMockCandidates(url) {
// Route matcher shared, semantically, with the injected live-preview shim
// (crates/proxy-http/src/fetch_patch.js::matchUrl / matchPath / matchQuerySpec).
// This is a VERBATIM port of that shim's matcher — the two interception paths
// MUST agree on which requests a mock route covers. When they drifted (the
// shim gained `:param` + query-spec parity but this capture-harness matcher was
// left doing exact string matching), a parameterized or query-spec'd mock
// rendered in the Live Preview but escaped Playwright's `page.route` during
// capture and rendered an empty list. `scenario-mocks.test.js` pins the two to
// identical verdicts on a shared fixture table so a future divergence is a
// test failure, not another lost build session.
//
// `:param` path segments match any single non-empty segment; a `?k=v` / `?k=*`
// query-spec suffix constrains the query as a subset match (`k=*` matches any
// value; an empty spec matches any query). Full-URL patterns (`http://…`) match
// by prefix on the raw URL. Mirrors crates/mock-engine's server matcher.
function matchUrl(pattern, url) {
// Full-URL patterns (external APIs) match by prefix on the raw URL.
if (pattern.startsWith("http://") || pattern.startsWith("https://")) {
return url.indexOf(pattern) === 0;
}
let reqPath = url;
let reqQuery = "";
try {
const parsed = new URL(url);
return [url, `${parsed.pathname}${parsed.search}`, parsed.pathname];
const u = new URL(url);
reqPath = u.pathname;
reqQuery = u.search.replace(/^\?/, "");
} catch {
return [url];
const qi = url.indexOf("?");
if (qi >= 0) {
reqPath = url.slice(0, qi);
reqQuery = url.slice(qi + 1);
}
}
const patParts = pattern.split("?");
if (!matchPath(patParts[0], reqPath)) return false;
return matchQuerySpec(patParts.length > 1 ? patParts[1] : "", reqQuery);
}
// Segment-wise path match with `:param` wildcards. A `:name` segment matches
// any single non-empty segment; every other segment must be equal, and the
// segment counts must match — no implicit prefix match, same as the server.
function matchPath(pattern, path) {
const pp = pattern.split("/");
const rp = path.split("/");
if (pp.length !== rp.length) return false;
for (let i = 0; i < pp.length; i++) {
if (pp[i].charAt(0) === ":") {
if (rp[i] === "") return false;
continue;
}
if (pp[i] !== rp[i]) return false;
}
return true;
}
// Query-spec match. An empty spec matches any query (a path-only route still
// matches a request that carries query params). Otherwise every `k=v` pair in
// the spec must be present in the request query; `k=*` matches any value.
function matchQuerySpec(spec, query) {
if (!spec) return true;
const want = new URLSearchParams(spec);
const have = new URLSearchParams(query);
let ok = true;
want.forEach((v, k) => {
const got = have.get(k);
if (got === null) {
ok = false;
return;
}
if (v !== "*" && got !== v) ok = false;
});
return ok;
}
// Split a mock key (`"<METHOD> <route>"`, e.g. `"GET /api/plans"`) into its
// method and route pattern. Returns null for a malformed key with no separator.
function splitMockKey(key) {
const spaceIdx = key.indexOf(" ");
if (spaceIdx === -1) return null;
return { method: key.slice(0, spaceIdx), route: key.slice(spaceIdx + 1) };
}
function findHttpMock(httpMocks, request) {
const method = request.method().toUpperCase();
const candidates = normalizeMockCandidates(request.url());
for (const candidate of candidates) {
const key = `${method} ${candidate}`;
if (httpMocks[key]) {
return httpMocks[key];
}
const url = request.url();
for (const [key, mock] of Object.entries(httpMocks)) {
const parsed = splitMockKey(key);
if (!parsed) continue;
// Method stays an exact check; only the route is matched by pattern.
if (parsed.method.toUpperCase() !== method) continue;
if (matchUrl(parsed.route, url)) return mock;
}

@@ -22,4 +98,4 @@ return null;

// The set of path/URL targets declared by the mock keys. A key is
// `"<METHOD> <target>"` (e.g. `"GET /api/plans"`); we strip the method so the
// The set of path/URL route patterns declared by the mock keys. A key is
// `"<METHOD> <route>"` (e.g. `"GET /api/plans"`); we strip the method so the
// route matcher can decide whether a request *could* match any mock without

@@ -30,5 +106,5 @@ // knowing the method (the handler re-checks method via findHttpMock).

for (const key of Object.keys(httpMocks)) {
const spaceIdx = key.indexOf(" ");
if (spaceIdx === -1) continue;
targets.add(key.slice(spaceIdx + 1));
const parsed = splitMockKey(key);
if (!parsed) continue;
targets.add(parsed.route);
}

@@ -38,9 +114,12 @@ return targets;

// True when a request URL matches one of the declared mock targets. Accepts
// either a string or a WHATWG URL (Playwright's URL-matcher passes a URL).
// True when a request URL matches one of the declared mock route patterns under
// the shared route matcher (`:param` + query-spec parity), not exact string
// equality. Accepts either a string or a WHATWG URL (Playwright's URL-matcher
// passes a URL).
function requestTargetsMock(targets, url) {
const href = typeof url === "string" ? url : url.href;
return normalizeMockCandidates(href).some((candidate) =>
targets.has(candidate),
);
for (const target of targets) {
if (matchUrl(target, href)) return true;
}
return false;
}

@@ -112,8 +191,9 @@

function isDeclaredErrorMock(httpMocks, url) {
const candidates = normalizeMockCandidates(url);
for (const [key, mock] of Object.entries(httpMocks || {})) {
const spaceIdx = key.indexOf(" ");
if (spaceIdx === -1) continue;
const target = key.slice(spaceIdx + 1);
if (!candidates.includes(target)) continue;
const parsed = splitMockKey(key);
if (!parsed) continue;
// Method-agnostic on purpose: console errors carry no HTTP method, so any
// method's error mock on a matching route counts. Route matching uses the
// shared matcher so a `:param`/query-spec error mock is recognised too.
if (!matchUrl(parsed.route, url)) continue;
if ((mock.status || 200) >= 400) return true;

@@ -124,4 +204,26 @@ }

// Given a same-origin response's method/url/status and the scenario's declared
// mocks, return the `{method, path, status}` to record as an unmocked route, or
// `null` if it should be ignored: a sub-4xx response, or one already covered by
// a declared error mock (status >= 400, whose 4xx is the mock's intended
// behavior). The caller applies the same-origin and `expectedConsoleErrors`
// gates before calling. `path` is the URL pathname — the route half of the
// `"<METHOD> <path>"` stub-mock key; a malformed URL falls back to the raw
// string so the key is still well-formed.
function unmockedRouteFrom(method, url, status, httpMocks) {
if (status < 400) return null;
if (isDeclaredErrorMock(httpMocks, url)) return null;
let path = url;
try {
path = new URL(url).pathname;
} catch {
/* malformed URL — fall back to the raw string for the stub key */
}
return { method: String(method).toUpperCase(), path, status };
}
module.exports = {
normalizeMockCandidates,
matchUrl,
matchPath,
matchQuerySpec,
findHttpMock,

@@ -132,2 +234,3 @@ mockedTargets,

isDeclaredErrorMock,
unmockedRouteFrom,
};

@@ -707,2 +707,34 @@ const {

// Describe the elements a substring text match resolved, so an ambiguity
// warning can name the competing controls (e.g. the preset button "Bet" vs the
// disclosure button "…or bet on"). Reads each matched element's inner text via
// Playwright's `allInnerTexts`, caps the list at 5, and trims each to a single
// short line. Pure read; degrades to a count-only description if the locator
// API or the page can't produce texts.
async function describeMatchCandidates(baseLocator, matchCount) {
const CAP = 5;
const MAX_LEN = 60;
let texts = [];
try {
if (typeof baseLocator.allInnerTexts === "function") {
texts = await baseLocator.allInnerTexts();
}
} catch (_) {
texts = [];
}
const described = texts
.map((t) => String(t).replace(/\s+/g, " ").trim())
.filter((t) => t.length > 0)
.slice(0, CAP)
.map((t) => (t.length > MAX_LEN ? `${t.slice(0, MAX_LEN)}…` : t))
.map((t) => `"${t}"`);
if (described.length === 0) {
return [`${matchCount} elements (text unavailable)`];
}
if (matchCount > described.length) {
described.push(`…and ${matchCount - described.length} more`);
}
return described;
}
// Drive a single user-style interaction against the settled frame before the

@@ -718,12 +750,27 @@ // screenshot, so an interactive state (expanded accordion, open modal, filled

// failed capture with an actionable message, never a silent blank screenshot.
async function performInteraction(frame, interaction, { timeoutMs = 5000 } = {}) {
//
// Text matching is a case-insensitive SUBSTRING, so a label can match more than
// one element. When it does, acting on `.first()` silently may hit the wrong
// control (observed: a `"Bet"` click matched a disclosure button described "or
// bet on" and toggled the panel shut). Rather than change the matching
// semantics, push an actionable warning naming every candidate into the
// optional `warnings` array so the agent can switch to an exact selector; a
// zero-match likewise throws an error that spells out the substring caveat and
// the exact-selector / URL-query-param alternatives.
async function performInteraction(
frame,
interaction,
{ timeoutMs = 5000, warnings } = {},
) {
const { action, selector, text, value } = interaction || {};
let locator;
let baseLocator;
let targetDesc;
let matchedByText = false;
if (typeof text === "string" && text.length > 0) {
locator = frame.getByText(text, { exact: false }).first();
baseLocator = frame.getByText(text, { exact: false });
targetDesc = `text "${text}"`;
matchedByText = true;
} else if (typeof selector === "string" && selector.length > 0) {
locator = frame.locator(selector).first();
baseLocator = frame.locator(selector);
targetDesc = `selector "${selector}"`;

@@ -736,3 +783,4 @@ } else {

const matchCount = await locator.count();
const locator = baseLocator.first();
const matchCount = await baseLocator.count();
if (matchCount === 0) {

@@ -744,6 +792,24 @@ const candidates = await collectInteractiveLabels(frame);

`preview-interact: no element matched ${targetDesc}. ` +
`Candidate interactive labels: ${candidateList}`,
`Text is matched as a case-insensitive SUBSTRING, so a misspelled or ` +
`over-specific label matches nothing — prefer an exact/role/testid CSS ` +
`selector (e.g. {"selector":"[data-testid=\\"save\\"]"}) for a precise ` +
`target. Many filter/status/sort states are also reachable directly via a ` +
`URL query param (e.g. add "?status=active" to the path) with no ` +
`interaction at all. Candidate interactive labels: ${candidateList}`,
);
}
// Ambiguity warning: a substring text match that resolves >1 element acts on
// the first, which may not be the one intended. Name the competing elements
// instead of silently proceeding. Only fires for text matches — an explicit
// selector that matches many is the caller's deliberate choice.
if (matchedByText && matchCount > 1 && Array.isArray(warnings)) {
warnings.push(
`preview-interact: ${targetDesc} matched ${matchCount} elements — acting on the first. ` +
`Text matching is substring-based, so this may not be the element you meant. ` +
`Candidates: ${(await describeMatchCandidates(baseLocator, matchCount)).join(" | ")}. ` +
`Use an exact/role/testid selector to disambiguate.`,
);
}
try {

@@ -836,2 +902,3 @@ switch (action) {

settle = waitForStablePage,
warnings,
} = {},

@@ -841,3 +908,3 @@ ) {

try {
await performInteraction(frame, interactions[i], { timeoutMs });
await performInteraction(frame, interactions[i], { timeoutMs, warnings });
} catch (err) {

@@ -874,2 +941,3 @@ // Prefix the failing step's index so a miss in a multi-step sequence is

collectInteractiveLabels,
describeMatchCandidates,
performInteraction,

@@ -876,0 +944,0 @@ waitForPredicate,

@@ -107,2 +107,30 @@ const { execSync, spawn, spawnSync } = require("child_process");

/**
* Whether this install is a dev checkout (the source tree) rather than an
* npm-installed package. Signalled by a `Cargo.toml` at `rootDir` — present in
* the git checkout, absent in the npm install dir. The single source of truth
* for the checkout-vs-installed distinction; both `ensureBinary` (build-from-
* source vs error) and `serverSpawnEnv` (distribution-channel gating) read it.
* Accepts an explicit dir so it is unit-testable against a temp tree.
*/
function isDevCheckout(dir = rootDir) {
return fs.existsSync(path.join(dir, "Cargo.toml"));
}
/**
* Env for a launched editor-server, tagging the distribution channel so the
* server's update-availability check knows an npm install can be upgraded via
* `npm install -g …@latest`. Pure: returns a NEW object, never mutates the
* input.
*
* Installed (non-dev-checkout) → set `CODEYAM_DISTRIBUTION=npm`, so the server
* polls the npm registry and surfaces the upgrade banner. Dev checkout → leave
* the env untouched (any pre-existing value from the parent env is preserved),
* so a `codeyam-editor` run from source is never nagged to "upgrade".
*/
function serverSpawnEnv(baseEnv, { devCheckout } = {}) {
if (devCheckout) return { ...baseEnv };
return { ...baseEnv, CODEYAM_DISTRIBUTION: "npm" };
}
/**
* Return the binary path, building from source if we're in a dev checkout.

@@ -124,5 +152,5 @@ *

const isDevCheckout = fs.existsSync(path.join(rootDir, "Cargo.toml"));
const devCheckout = isDevCheckout();
if (!isDevCheckout) {
if (!devCheckout) {
const info = platformPackageInfo();

@@ -1056,3 +1084,7 @@ if (!info) {

["start", "--no-open", "--port", String(port)],
buildServerSpawnOptions({ cwd: process.cwd(), env: process.env, logFd }),
buildServerSpawnOptions({
cwd: process.cwd(),
env: serverSpawnEnv(process.env, { devCheckout: isDevCheckout() }),
logFd,
}),
);

@@ -1198,2 +1230,4 @@ if (logFd !== null) {

ensureBinary,
isDevCheckout,
serverSpawnEnv,
staleBinaryBanner,

@@ -1200,0 +1234,0 @@ enforceFreshBinary,

+5
-5
{
"name": "@codeyam-editor/codeyam-editor",
"version": "0.1.0-staging.gac4ca32",
"version": "0.1.0-staging.gc446a6f",
"description": "Language-agnostic managed execution sandbox for scenario-driven development",

@@ -12,6 +12,6 @@ "bin": {

"optionalDependencies": {
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.0-staging.gac4ca32",
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.0-staging.gac4ca32",
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.0-staging.gac4ca32",
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.0-staging.gac4ca32"
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.0-staging.gc446a6f",
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.0-staging.gc446a6f",
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.0-staging.gc446a6f",
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.0-staging.gc446a6f"
},

@@ -18,0 +18,0 @@ "keywords": [

@@ -8,3 +8,3 @@ <!DOCTYPE html>

<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<script type="module" crossorigin src="/assets/index-B7_KRKaQ.js"></script>
<script type="module" crossorigin src="/assets/index-iE2jFez3.js"></script>
<link rel="modulepreload" crossorigin href="/assets/react-CSS0HapR.js">

@@ -11,0 +11,0 @@ <link rel="modulepreload" crossorigin href="/assets/markdown-v0F0UEt3.js">

import{j as e}from"./markdown-v0F0UEt3.js";import{b as a}from"./react-CSS0HapR.js";import{u as O}from"./useEvents-DHkLJZR4.js";import{b as E}from"./index-B7_KRKaQ.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}=O(),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 k({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:k,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(k,{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(`
`)})}),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(k,{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}=O(),[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(k,{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}=O(),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}=O();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,k 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};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

import{j as c}from"./markdown-v0F0UEt3.js";import{b as o}from"./react-CSS0HapR.js";import{S as l}from"./ScenarioDataPanel-DuU3_hdh.js";function m({slug:t}){const[r,i]=o.useState(void 0);return o.useEffect(()=>{let n=!1;return fetch(`/api/scenarios/${encodeURIComponent(t)}`).then(e=>e.ok?e.json():null).then(e=>{if(n||!e)return;const a=typeof e=="object"&&e!==null&&"name"in e?String(e.name):void 0;i(a),a&&(document.title=`${a} · data`)}).catch(()=>{}),()=>{n=!0}},[t]),c.jsx(l,{slug:t,scenarioName:r,variant:"fullPage"})}const p=Object.freeze(Object.defineProperty({__proto__:null,ScenarioDataPanelFullPage:m},Symbol.toStringTag,{value:"Module"}));export{m as S,p as _};

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display