@codeyam-editor/codeyam-editor 0.1.15 → 0.1.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/npm/app-port-drift.js +23 -0
- package/npm/fleet-add-failure-evidence.js +309 -0
- package/npm/fleet-add-failure.js +51 -311
- package/npm/fleet-build-data.js +47 -20
- package/npm/fleet-preview-health.js +156 -0
- package/npm/fleet-revision.js +50 -0
- package/npm/fleet-sticky-fields.js +124 -0
- package/npm/launch-config.js +43 -142
- package/npm/scenario-check.js +68 -383
- package/npm/scenario-issues.js +47 -0
- package/npm/scenario-launch.js +349 -0
- package/npm/scenario-playwright.js +42 -321
- package/npm/scenario-targeting.js +338 -0
- package/npm/vm-forwards.js +158 -0
- package/package.json +5 -5
- package/ui/dist/assets/{ContainerInspector-Ceop11Fv.js → ContainerInspector-Cig2PDjT.js} +1 -1
- package/ui/dist/assets/{EditorShell-DjI5cEF_.js → EditorShell-DOuUJNSm.js} +76 -74
- package/ui/dist/assets/{ScenarioDataPanel-D-nWT7xD.js → ScenarioDataPanel-BRUYhl1o.js} +10 -10
- package/ui/dist/assets/{ScenarioDataPanelFullPage-ZNFZcTQU.js → ScenarioDataPanelFullPage-DjwRM8HJ.js} +1 -1
- package/ui/dist/assets/ScenarioRenderer-CcbccxCW.js +26 -0
- package/ui/dist/assets/{index-ClrdYzlw.js → index-Bkrhkuf5.js} +6 -6
- package/ui/dist/assets/index-Dw4EB88N.css +1 -0
- package/ui/dist/index.html +2 -2
- package/ui/dist/assets/ScenarioRenderer-BW6QLGFW.js +0 -26
- package/ui/dist/assets/index-B3SMxYUB.css +0 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
// Interaction targeting for the capture harness: turn a step's visible-text or
|
|
2
|
+
// CSS-selector target into the one element to act on, explain a miss in terms
|
|
3
|
+
// the author can act on, and measure where a pointer step lands inside its
|
|
4
|
+
// target. Split out of scenario-playwright.js, which drives the interactions
|
|
5
|
+
// themselves; everything here reads the page and never acts on it.
|
|
6
|
+
|
|
7
|
+
// The CSS selector for "clickable / interactive" elements — buttons, links,
|
|
8
|
+
// role=button, form controls, <summary>, and anything with an onclick. Shared
|
|
9
|
+
// by the candidate-label collector (below) and the text-target resolver
|
|
10
|
+
// (`resolveTextTarget`), which prefers an interactive element over a plain
|
|
11
|
+
// text node when a click/press label matches both.
|
|
12
|
+
const INTERACTIVE_SELECTOR =
|
|
13
|
+
"button, a[href], [role=button], input, select, textarea, summary, [onclick]";
|
|
14
|
+
|
|
15
|
+
// Collect up to 20 distinct visible labels of interactive elements on the
|
|
16
|
+
// page — buttons, links, role=button, form controls, <summary>, and anything
|
|
17
|
+
// with an onclick. Used to build an ACTIONABLE error when an interaction's
|
|
18
|
+
// target matches nothing: the agent reliably knows a label it rendered, so
|
|
19
|
+
// listing the real candidates turns a silent blank capture into a "did you
|
|
20
|
+
// mean one of these?" hint. Pure read (no clicks); falls back to value /
|
|
21
|
+
// aria-label / placeholder when an element has no text.
|
|
22
|
+
async function collectInteractiveLabels(frame) {
|
|
23
|
+
return frame.evaluate((selector) => {
|
|
24
|
+
const nodes = Array.from(document.querySelectorAll(selector));
|
|
25
|
+
const labels = nodes
|
|
26
|
+
.map((node) => {
|
|
27
|
+
const text =
|
|
28
|
+
(node.innerText || node.textContent || "").trim() ||
|
|
29
|
+
(typeof node.value === "string" ? node.value.trim() : "") ||
|
|
30
|
+
(node.getAttribute && node.getAttribute("aria-label")) ||
|
|
31
|
+
(node.getAttribute && node.getAttribute("placeholder")) ||
|
|
32
|
+
"";
|
|
33
|
+
return String(text).trim();
|
|
34
|
+
})
|
|
35
|
+
.filter((label) => label.length > 0);
|
|
36
|
+
return Array.from(new Set(labels)).slice(0, 20);
|
|
37
|
+
}, INTERACTIVE_SELECTOR);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// How many candidate labels an error message may echo. Deliberately tighter
|
|
41
|
+
// than `collectInteractiveLabels`' own 20-label read cap: the labels are the
|
|
42
|
+
// USER'S page content, so every one printed is a line of their app leaked into
|
|
43
|
+
// the terminal. Enough to answer "did you mean one of these?", not enough to
|
|
44
|
+
// transcribe the page.
|
|
45
|
+
const CANDIDATE_LABEL_CAP = 8;
|
|
46
|
+
|
|
47
|
+
// Render the candidate labels for an error message, capped and counted. Shared
|
|
48
|
+
// by the no-match and action-failed paths so the echo policy lives in one place.
|
|
49
|
+
async function candidateLabelSummary(frame) {
|
|
50
|
+
const candidates = await collectInteractiveLabels(frame);
|
|
51
|
+
if (candidates.length === 0) return "(none found on page)";
|
|
52
|
+
const shown = candidates.slice(0, CANDIDATE_LABEL_CAP);
|
|
53
|
+
const rest = candidates.length - shown.length;
|
|
54
|
+
return rest > 0 ? `${shown.join(", ")} (+${rest} more)` : shown.join(", ");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Compose the "no element matched" error, branched on the form the caller
|
|
58
|
+
// ACTUALLY passed. Pure and string-returning so both branches are asserted
|
|
59
|
+
// without a browser.
|
|
60
|
+
//
|
|
61
|
+
// The substring advice ("prefer an exact CSS selector") only applies to a text
|
|
62
|
+
// label. Printing it at a caller who already passed `#id` contradicts its own
|
|
63
|
+
// input and reads as misdirection — the failure mode that sent two sessions
|
|
64
|
+
// hunting a selector bug when the page had simply not hydrated. The candidate
|
|
65
|
+
// labels ride with the text branch for the same reason: they answer "did you
|
|
66
|
+
// mean one of these?", a question only a text match can have asked, and every
|
|
67
|
+
// label printed is a line of the user's own page echoed into the terminal.
|
|
68
|
+
//
|
|
69
|
+
// A text miss on a canvas-dominated page is not a spelling problem: the label
|
|
70
|
+
// is painted into the canvas and is not page text at all, so an empty
|
|
71
|
+
// candidate list would only send the author hunting a typo. That branch names
|
|
72
|
+
// the canvas and the position form instead.
|
|
73
|
+
function noMatchGuidance({ targetDesc, matchedByText, candidateLabels, canvasDominated = false }) {
|
|
74
|
+
const queryParamHint =
|
|
75
|
+
`Many filter/status/sort states are also reachable directly via a ` +
|
|
76
|
+
`URL query param (e.g. add "?status=active" to the path) with no ` +
|
|
77
|
+
`interaction at all.`;
|
|
78
|
+
if (matchedByText && canvasDominated) {
|
|
79
|
+
return (
|
|
80
|
+
`preview-interact: no element matched ${targetDesc}, and this page is ` +
|
|
81
|
+
`canvas-dominated (it paints into a canvas with no visible page text), ` +
|
|
82
|
+
`so the label is painted on the canvas and cannot be matched as text. ` +
|
|
83
|
+
`Target the canvas by position instead: ` +
|
|
84
|
+
`{"selector":"canvas","position":{"x":120,"y":40}} — a point in CSS ` +
|
|
85
|
+
`pixels from the canvas's top-left corner, read off the frame captured ` +
|
|
86
|
+
`just before this step.`
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
if (matchedByText) {
|
|
90
|
+
return (
|
|
91
|
+
`preview-interact: no element matched ${targetDesc}. ` +
|
|
92
|
+
`Text is matched as a case-insensitive SUBSTRING, so a misspelled or ` +
|
|
93
|
+
`over-specific label matches nothing — prefer an exact/role/testid CSS ` +
|
|
94
|
+
`selector (e.g. {"selector":"[data-testid=\\"save\\"]"}) for a precise ` +
|
|
95
|
+
`target. ${queryParamHint} ` +
|
|
96
|
+
`Candidate interactive labels: ${candidateLabels}`
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
return (
|
|
100
|
+
`preview-interact: no element matched ${targetDesc}. ` +
|
|
101
|
+
`That is already an exact CSS selector, so nothing on the page matches ` +
|
|
102
|
+
`it as written — confirm the element is present in THIS state (it may be ` +
|
|
103
|
+
`behind a tab, dialog, or conditional render), that the id/attribute is ` +
|
|
104
|
+
`spelled as the markup emits it, and that the page rendered and hydrated ` +
|
|
105
|
+
`at all. ${queryParamHint}`
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Describe the elements a substring text match resolved, so an ambiguity
|
|
110
|
+
// warning can name the competing controls (e.g. the preset button "Bet" vs the
|
|
111
|
+
// disclosure button "…or bet on"). Reads each matched element's inner text via
|
|
112
|
+
// Playwright's `allInnerTexts`, caps the list at 5, and trims each to a single
|
|
113
|
+
// short line. Pure read; degrades to a count-only description if the locator
|
|
114
|
+
// API or the page can't produce texts.
|
|
115
|
+
async function describeMatchCandidates(baseLocator, matchCount) {
|
|
116
|
+
const CAP = 5;
|
|
117
|
+
const MAX_LEN = 60;
|
|
118
|
+
let texts = [];
|
|
119
|
+
try {
|
|
120
|
+
if (typeof baseLocator.allInnerTexts === "function") {
|
|
121
|
+
texts = await baseLocator.allInnerTexts();
|
|
122
|
+
}
|
|
123
|
+
} catch (_) {
|
|
124
|
+
texts = [];
|
|
125
|
+
}
|
|
126
|
+
const described = texts
|
|
127
|
+
.map((t) => String(t).replace(/\s+/g, " ").trim())
|
|
128
|
+
.filter((t) => t.length > 0)
|
|
129
|
+
.slice(0, CAP)
|
|
130
|
+
.map((t) => (t.length > MAX_LEN ? `${t.slice(0, MAX_LEN)}…` : t))
|
|
131
|
+
.map((t) => `"${t}"`);
|
|
132
|
+
if (described.length === 0) {
|
|
133
|
+
return [`${matchCount} elements (text unavailable)`];
|
|
134
|
+
}
|
|
135
|
+
if (matchCount > described.length) {
|
|
136
|
+
described.push(`…and ${matchCount - described.length} more`);
|
|
137
|
+
}
|
|
138
|
+
return described;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Drive a single user-style interaction against the settled frame before the
|
|
142
|
+
// screenshot, so an interactive state (expanded accordion, open modal, filled
|
|
143
|
+
// field) can be captured without editing app source.
|
|
144
|
+
//
|
|
145
|
+
// The target is matched by visible `text` (preferred — the agent reliably
|
|
146
|
+
// knows the label it rendered) or a CSS `selector`. `action` is click / fill /
|
|
147
|
+
// press; `value` carries the text for `fill` or the key for `press` (e.g.
|
|
148
|
+
// `Enter`). On a no-match target this THROWS with the list of candidate
|
|
149
|
+
// interactive labels — the capture script's outer catch turns that into a
|
|
150
|
+
// failed capture with an actionable message, never a silent blank screenshot.
|
|
151
|
+
//
|
|
152
|
+
// Resolve a visible-text target to the element most likely intended, instead
|
|
153
|
+
// of blindly taking the first substring hit. A bare
|
|
154
|
+
// `getByText(text, { exact: false })` is a case-insensitive SUBSTRING match, so
|
|
155
|
+
// a label can land on a plain text node or a placeholder rather than the chip /
|
|
156
|
+
// button the agent meant (observed: a `"Japan"` click hit placeholder text, not
|
|
157
|
+
// the chip; a `"Bet"` click hit a disclosure button described "or bet on"). We
|
|
158
|
+
// try increasingly-loose locators in priority order and take the first that
|
|
159
|
+
// matches anything:
|
|
160
|
+
// 1. exact + interactive (for click/press)
|
|
161
|
+
// 2. exact
|
|
162
|
+
// 3. substring + interactive (for click/press)
|
|
163
|
+
// 4. substring
|
|
164
|
+
// so an exact, clickable element wins over a substring plain-text node. Returns
|
|
165
|
+
// the winning tier's locator plus its match count; when nothing matches at all,
|
|
166
|
+
// returns the substring locator (count 0) so the caller's zero-match error path
|
|
167
|
+
// still fires with its candidate-label hint.
|
|
168
|
+
async function resolveTextTarget(frame, text, action) {
|
|
169
|
+
const preferInteractive = action === "click" || action === "press";
|
|
170
|
+
const exactBase = frame.getByText(text, { exact: true });
|
|
171
|
+
const substrBase = frame.getByText(text, { exact: false });
|
|
172
|
+
|
|
173
|
+
const tiers = [];
|
|
174
|
+
// `.and()` (Playwright ≥1.34) intersects two locators to the elements
|
|
175
|
+
// matching both — here, the text element that is ALSO interactive. Guard on
|
|
176
|
+
// its presence so a locator without `.and` degrades to text-only tiers.
|
|
177
|
+
if (preferInteractive && typeof exactBase.and === "function") {
|
|
178
|
+
const interactive = frame.locator(INTERACTIVE_SELECTOR);
|
|
179
|
+
tiers.push(exactBase.and(interactive));
|
|
180
|
+
tiers.push(exactBase);
|
|
181
|
+
tiers.push(substrBase.and(interactive));
|
|
182
|
+
tiers.push(substrBase);
|
|
183
|
+
} else {
|
|
184
|
+
tiers.push(exactBase);
|
|
185
|
+
tiers.push(substrBase);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
for (const loc of tiers) {
|
|
189
|
+
const count = await loc.count();
|
|
190
|
+
if (count > 0) {
|
|
191
|
+
return { baseLocator: loc, matchCount: count };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return { baseLocator: substrBase, matchCount: 0 };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Pick the single element to act on from a resolved locator. When more than one
|
|
198
|
+
// candidate remains within the winning tier, prefer a VISIBLE element over a
|
|
199
|
+
// hidden one rather than acting on raw `.first()` (a hidden duplicate — an
|
|
200
|
+
// off-screen menu clone, an aria-hidden mirror — is almost never the intended
|
|
201
|
+
// target). Degrades to `.first()` when the locator API lacks a visible filter
|
|
202
|
+
// or no candidate is visible.
|
|
203
|
+
async function pickBestCandidate(baseLocator, matchCount) {
|
|
204
|
+
if (matchCount <= 1) {
|
|
205
|
+
return baseLocator.first();
|
|
206
|
+
}
|
|
207
|
+
try {
|
|
208
|
+
if (typeof baseLocator.filter === "function") {
|
|
209
|
+
const visible = baseLocator.filter({ visible: true });
|
|
210
|
+
if ((await visible.count()) > 0) {
|
|
211
|
+
return visible.first();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (_) {
|
|
215
|
+
// Locator API without a `{ visible: true }` filter — fall through.
|
|
216
|
+
}
|
|
217
|
+
return baseLocator.first();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// The page that owns `frame` — its `mouse` and `keyboard` are page-level, and a
|
|
221
|
+
// locator's `boundingBox()` is already in that page's viewport coordinates, so
|
|
222
|
+
// a point measured from the box can be handed to `page.mouse` unchanged.
|
|
223
|
+
function pageOf(frame) {
|
|
224
|
+
return typeof frame.page === "function" ? frame.page() : frame;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// `drag` / `wheel` need the target's box to turn a target-relative point into a
|
|
228
|
+
// viewport point. A target that is not rendered has no box, and acting at
|
|
229
|
+
// (0,0) instead would silently drive whatever is in the corner.
|
|
230
|
+
async function targetBox(locator, targetDesc) {
|
|
231
|
+
if (typeof locator.scrollIntoViewIfNeeded === "function") {
|
|
232
|
+
await locator.scrollIntoViewIfNeeded().catch(() => {});
|
|
233
|
+
}
|
|
234
|
+
const box = await locator.boundingBox();
|
|
235
|
+
if (!box) {
|
|
236
|
+
throw new Error(`${targetDesc} has no bounding box (not rendered or not visible)`);
|
|
237
|
+
}
|
|
238
|
+
return box;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// A target-relative point resolved to viewport coordinates, plus whether it
|
|
242
|
+
// lies inside the box. Positions are CSS pixels from the target's top-left, so
|
|
243
|
+
// a point measured off a device-pixel screenshot overshoots the box and would
|
|
244
|
+
// drive whatever sits beside the target; `inside` lets the caller refuse it.
|
|
245
|
+
// No point means the centre, which is always inside.
|
|
246
|
+
function pointIn(box, point) {
|
|
247
|
+
if (point && typeof point.x === "number" && typeof point.y === "number") {
|
|
248
|
+
const inside =
|
|
249
|
+
point.x >= 0 && point.y >= 0 && point.x < box.width && point.y < box.height;
|
|
250
|
+
return { x: box.x + point.x, y: box.y + point.y, inside };
|
|
251
|
+
}
|
|
252
|
+
return { x: box.x + box.width / 2, y: box.y + box.height / 2, inside: true };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// The page's device pixel ratio — how many screenshot pixels make one CSS
|
|
256
|
+
// pixel. `null` when the frame cannot answer, so a report never guesses.
|
|
257
|
+
async function devicePixelRatioOf(frame) {
|
|
258
|
+
try {
|
|
259
|
+
const ratio = await frame.evaluate(() => window.devicePixelRatio);
|
|
260
|
+
return typeof ratio === "number" && ratio > 0 ? ratio : null;
|
|
261
|
+
} catch (_) {
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// Why a point is not where its author thought: name the target's real CSS size
|
|
267
|
+
// and, when screenshots are scaled, the divisor that converts a coordinate
|
|
268
|
+
// measured off one into the CSS pixels a position is written in.
|
|
269
|
+
function outsideTargetMessage(label, point, targetDesc, box, devicePixelRatio) {
|
|
270
|
+
const scale =
|
|
271
|
+
devicePixelRatio && devicePixelRatio !== 1
|
|
272
|
+
? `screenshots are ${devicePixelRatio}× device pixels — divide screenshot coordinates by ${devicePixelRatio}`
|
|
273
|
+
: "positions are CSS pixels from the target's top-left";
|
|
274
|
+
const size = `${Math.round(box.width)}×${Math.round(box.height)} CSS px`;
|
|
275
|
+
return `${label} (${point.x},${point.y}) is outside ${targetDesc} (${size}; ${scale})`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// A start point outside the target can never act on it, so it is refused —
|
|
279
|
+
// loudly, like a target that matches nothing. Flagged so `performInteraction`
|
|
280
|
+
// rethrows it without the candidate-labels hint, which is about a missing
|
|
281
|
+
// target, not a misplaced point.
|
|
282
|
+
async function outsideTargetError(frame, action, label, point, targetDesc, box) {
|
|
283
|
+
const error = new Error(
|
|
284
|
+
`preview-interact: action "${action}" refused: ` +
|
|
285
|
+
outsideTargetMessage(label, point, targetDesc, box, await devicePixelRatioOf(frame)),
|
|
286
|
+
);
|
|
287
|
+
error.outsideTarget = true;
|
|
288
|
+
return error;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// Where a pointer step landed, in the target's own CSS-pixel frame, beside the
|
|
292
|
+
// target's size — what a report needs to answer "where did the pointer go?"
|
|
293
|
+
// without opening a captured frame. `end` is a drag's release point.
|
|
294
|
+
function pointerGeometry(box, start, end) {
|
|
295
|
+
const relative = (p) => ({ x: Math.round(p.x - box.x), y: Math.round(p.y - box.y) });
|
|
296
|
+
const geometry = {
|
|
297
|
+
targetBox: { width: Math.round(box.width), height: Math.round(box.height) },
|
|
298
|
+
landedAt: relative(start),
|
|
299
|
+
};
|
|
300
|
+
if (end) geometry.landedTo = relative(end);
|
|
301
|
+
return geometry;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// `click` / `hover` hand `position` to Playwright, which does not check that it
|
|
305
|
+
// lies inside the target — so measure the box first and refuse a point outside
|
|
306
|
+
// it. A position-less step aims at the centre; its box is best-effort (no box
|
|
307
|
+
// means no geometry, and Playwright's own actionability error stands).
|
|
308
|
+
async function locatorPointGeometry(frame, locator, targetDesc, action, position) {
|
|
309
|
+
if (!position) {
|
|
310
|
+
if (typeof locator.boundingBox !== "function") return undefined;
|
|
311
|
+
const box = await locator.boundingBox().catch(() => null);
|
|
312
|
+
return box ? pointerGeometry(box, pointIn(box)) : undefined;
|
|
313
|
+
}
|
|
314
|
+
const box = await targetBox(locator, targetDesc);
|
|
315
|
+
const at = pointIn(box, position);
|
|
316
|
+
if (!at.inside) {
|
|
317
|
+
throw await outsideTargetError(frame, action, "position", position, targetDesc, box);
|
|
318
|
+
}
|
|
319
|
+
return pointerGeometry(box, at);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
module.exports = {
|
|
323
|
+
INTERACTIVE_SELECTOR,
|
|
324
|
+
collectInteractiveLabels,
|
|
325
|
+
candidateLabelSummary,
|
|
326
|
+
noMatchGuidance,
|
|
327
|
+
describeMatchCandidates,
|
|
328
|
+
resolveTextTarget,
|
|
329
|
+
pickBestCandidate,
|
|
330
|
+
pageOf,
|
|
331
|
+
targetBox,
|
|
332
|
+
pointIn,
|
|
333
|
+
devicePixelRatioOf,
|
|
334
|
+
outsideTargetMessage,
|
|
335
|
+
outsideTargetError,
|
|
336
|
+
pointerGeometry,
|
|
337
|
+
locatorPointGeometry,
|
|
338
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// npm/vm-forwards.js — the operator host's local plumbing for one fleet VM: the
|
|
2
|
+
// split IAP ssh forwards (editor reachability and live preview) and the
|
|
3
|
+
// self-match-safe `pkill` sweeps that reap them, the cloudflared quick-tunnel and
|
|
4
|
+
// a wedged `cloud:up`. Every builder returns a shell string; none runs anything.
|
|
5
|
+
// Split out of launch-config.js, which re-exports these for the dashboard.
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
// Assemble a `pkill -f` whose ERE is ANCHORED at the target's own executable:
|
|
9
|
+
// `^`, an optional directory prefix, then `exe`. Every sweep builder in this
|
|
10
|
+
// module routes through here so the anchor is structural rather than remembered
|
|
11
|
+
// — an unanchored `pkill -f` matches the FULL command line, which for the fleet
|
|
12
|
+
// dashboard's `sh -c '<add script>'` wrapper includes the script text itself.
|
|
13
|
+
// See the "Self-match-safe process sweeps" block below for the full failure
|
|
14
|
+
// mode. `argv` is an ERE fragment matched after the executable; escape any
|
|
15
|
+
// literal dots in it.
|
|
16
|
+
function _anchoredSweep({ exe, argv } = {}) {
|
|
17
|
+
return `pkill -f "^([^ ]*/)?${exe} ${argv}" 2>/dev/null`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Build the `nohup … gcloud compute ssh … -N -L …` shell string that opens the
|
|
21
|
+
* EDITOR reachability forward (`editorPort` → VM:14199) as its OWN ssh process,
|
|
22
|
+
* logging to `/tmp/ssh-tunnel-vm-<n>.log`. The editor forward is the dashboard's
|
|
23
|
+
* reachability signal — the probe to `PORT_BASE+N` connects through it — so it
|
|
24
|
+
* must have a lifetime INDEPENDENT of the preview forward (whose port legitimately
|
|
25
|
+
* changes as the app's dev server is resolved). Splitting the two forwards into
|
|
26
|
+
* separate ssh processes is what makes a preview re-wire non-disruptive: a
|
|
27
|
+
* preview-port change restarts only the preview tunnel and never blips :14199, so
|
|
28
|
+
* the reachability probe can't flip the VM to a false "unreachable" (the
|
|
29
|
+
* VM-1/2/3 flapping observed 2026-06-22). Pairs with `previewTunnelCmd`; the two
|
|
30
|
+
* builders are the single source of truth for the attach (addVm) and re-wire call
|
|
31
|
+
* sites so neither forward's `-L` spec can drift back into the other's process.
|
|
32
|
+
* Pure; no I/O. */
|
|
33
|
+
function editorTunnelCmd({ gcloud, inst, zone, editorPort, n } = {}) {
|
|
34
|
+
return `nohup ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} -- -N -L ${editorPort}:127.0.0.1:14199 > /tmp/ssh-tunnel-vm-${n}.log 2>&1 &`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Build the `nohup … gcloud compute ssh … -N -L …` shell string that opens the
|
|
38
|
+
* LIVE-PREVIEW forward (`previewPort` → VM:<appPort>) as its OWN ssh process,
|
|
39
|
+
* logging to `/tmp/ssh-preview-vm-<n>.log`. The preview forward's target port
|
|
40
|
+
* changes whenever the app's dev-server port is (re)resolved, so it lives apart
|
|
41
|
+
* from the editor reachability forward (`editorTunnelCmd`): a preview re-wire
|
|
42
|
+
* pkills + respawns ONLY this process, leaving the editor :14199 forward — and
|
|
43
|
+
* the reachability probe riding it — untouched. The preview local port is unique
|
|
44
|
+
* per VM and never collides with the editor port. Sweep it through
|
|
45
|
+
* `previewForwardSweepCmd` — the bare `pkill -f "<previewPort>:127.0.0.1:"` this
|
|
46
|
+
* comment used to call "targets exactly this process" did NOT: it also matched
|
|
47
|
+
* every `sh -c '<add script>'` wrapper carrying that literal. Pure; no I/O. */
|
|
48
|
+
function previewTunnelCmd({ gcloud, inst, zone, previewPort, appPort, n } = {}) {
|
|
49
|
+
return `nohup ${gcloud} compute ssh ${inst} --tunnel-through-iap --zone=${zone} -- -N -L ${previewPort}:127.0.0.1:${appPort} > /tmp/ssh-preview-vm-${n}.log 2>&1 &`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ── Self-match-safe process sweeps ─────────────────────────────────────────
|
|
53
|
+
*
|
|
54
|
+
* Every builder below emits a `pkill -f` whose match expression is ANCHORED at
|
|
55
|
+
* the TARGET's executable — a leading `^`, an optional directory prefix, then
|
|
56
|
+
* the program name (`ssh`, `node`, `cloudflared`). That anchor is the entire
|
|
57
|
+
* point of these builders, not decoration.
|
|
58
|
+
*
|
|
59
|
+
* `pkill -f` matches the FULL command line. The fleet dashboard runs an Add as
|
|
60
|
+
* `/bin/sh -c '<the entire add script text>'`, and that script text EMBEDS these
|
|
61
|
+
* very sweep lines — so an unanchored pattern like `pkill -f
|
|
62
|
+
* "14199:127.0.0.1:14199"` is a substring of the wrapper's own argv. It SIGTERMs
|
|
63
|
+
* the wrapper that is running it, and every concurrent SIBLING add wrapper too.
|
|
64
|
+
* The wrapper then dies from an uncaught signal, so its EXIT trap never writes
|
|
65
|
+
* the `add-<n>.exit` marker that `fc.readExitMarker` / `fc.reattachVerdict` exist
|
|
66
|
+
* to consume; `startJob`'s exec callback reports a red "Add failed" on a VM that
|
|
67
|
+
* is fine; and the detached `cloud:up` it started — whose own argv does NOT carry
|
|
68
|
+
* the pattern — is orphaned to init and keeps provisioning, after which
|
|
69
|
+
* stranded-reconcile declares the VM half-provisioned and stacks a SECOND
|
|
70
|
+
* `cloud:up` on top of it. That is the 2026-07-31 VM-3..8 batch, whose containers
|
|
71
|
+
* were Up and answering /api/health 200 throughout.
|
|
72
|
+
*
|
|
73
|
+
* Excluding `$$` / `$PPID` does NOT fix this: the processes being wrongly killed
|
|
74
|
+
* are SIBLINGS, not self or parent. The pattern itself must be incapable of
|
|
75
|
+
* matching a `sh -c '<script>'` command line, and anchoring at the executable is
|
|
76
|
+
* what guarantees that — a wrapper's argv always begins with the shell, never
|
|
77
|
+
* with `ssh` / `node` / `cloudflared`.
|
|
78
|
+
*
|
|
79
|
+
* Same trap, same lesson as `npm/cloud.js` bootstrapStatusCommand (:1446) and
|
|
80
|
+
* projectBootstrapStatusCommand (:1760), which document it explicitly ("the
|
|
81
|
+
* status command's own argv contains the bootstrap path, so a `pgrep -f` on it
|
|
82
|
+
* would match this very probe") and dodge it with a PID file + `kill -0`. A PID
|
|
83
|
+
* file cannot replace THESE sweeps: they deliberately target STRAY processes left
|
|
84
|
+
* by a prior add, a crashed watchdog respawn, or an earlier container generation
|
|
85
|
+
* — processes this script never owned and holds no PID for. So the sweep stays
|
|
86
|
+
* pattern-based; only the pattern's blast radius changes.
|
|
87
|
+
*
|
|
88
|
+
* Keep every sweep in this family behind one of these builders. Re-inlining a
|
|
89
|
+
* bare `pkill -f` at a call site is exactly how the bug got in the first time. */
|
|
90
|
+
|
|
91
|
+
/** Sweep a stray EDITOR reachability forward — the `-L <localPort>:127.0.0.1:14199`
|
|
92
|
+
* ssh process `editorTunnelCmd` opens.
|
|
93
|
+
*
|
|
94
|
+
* Targets the `ssh` CHILD, not the `gcloud compute ssh` python parent. The child
|
|
95
|
+
* is what binds the local listening socket, so killing it is what actually
|
|
96
|
+
* releases the port; the parent is only waiting on it and exits on its own once
|
|
97
|
+
* it goes. Only the child's argv starts with `ssh`, which is also what makes the
|
|
98
|
+
* anchor safe. Pure; no I/O. */
|
|
99
|
+
function editorForwardSweepCmd({ localPort } = {}) {
|
|
100
|
+
return _anchoredSweep({ exe: "ssh", argv: `.*-L ${localPort}:127\\.0\\.0\\.1:14199` });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Sweep a stray LIVE-PREVIEW forward — the `-L <localPort>:127.0.0.1:<appPort>`
|
|
104
|
+
* ssh process `previewTunnelCmd` opens. The remote port is left open because the
|
|
105
|
+
* app's dev-server port legitimately drifts; the per-VM preview local port is the
|
|
106
|
+
* identifying half and never collides with the editor port. Same ssh-child
|
|
107
|
+
* targeting as `editorForwardSweepCmd`. Pure; no I/O. */
|
|
108
|
+
function previewForwardSweepCmd({ localPort } = {}) {
|
|
109
|
+
return _anchoredSweep({ exe: "ssh", argv: `.*-L ${localPort}:127\\.0\\.0\\.1:` });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Sweep a wedged `cloud:up` provision for one instance.
|
|
113
|
+
*
|
|
114
|
+
* Targets the `node npm/cloud.js up --instance-name <inst>` process, NOT the
|
|
115
|
+
* `npm run cloud:up` parent — call sites already hold the npm child's PID and
|
|
116
|
+
* `kill` it directly; this reaps the node grandchild that PID does not cover.
|
|
117
|
+
* `--instance-name` is the first pair `cloudUpArgs` emits, so the literal stays
|
|
118
|
+
* adjacent to `up`. Pure; no I/O. */
|
|
119
|
+
function cloudUpSweepCmd({ inst } = {}) {
|
|
120
|
+
return _anchoredSweep({ exe: "node", argv: `.*cloud\\.js up --instance-name ${inst}` });
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Sweep the cloudflared quick-tunnel fronting a VM's editor port.
|
|
124
|
+
*
|
|
125
|
+
* Anchored for the same reason as its siblings, with one extra bite: this sweep
|
|
126
|
+
* runs inside a `sh -c` compound built by `exec()`, so unanchored it matched its
|
|
127
|
+
* OWN parent shell — killing the compound before the editor/preview sweeps that
|
|
128
|
+
* follow it on the same line ever ran. Pure; no I/O. */
|
|
129
|
+
function cloudflaredTunnelSweepCmd({ localPort } = {}) {
|
|
130
|
+
return _anchoredSweep({ exe: "cloudflared", argv: `tunnel --url http://localhost:${localPort}` });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Build the destroy-time local reap compound for one VM: free the editor port,
|
|
134
|
+
* then sweep the cloudflared quick-tunnel and BOTH split ssh forwards.
|
|
135
|
+
*
|
|
136
|
+
* Lives here rather than inline in `cleanLocalVmArtifacts` because the ORDERING
|
|
137
|
+
* is load-bearing and was previously untestable: the whole compound runs as one
|
|
138
|
+
* `sh -c`, so the unanchored cloudflared sweep used to match that shell's own
|
|
139
|
+
* argv and kill it BEFORE the two forward sweeps behind it ever ran — meaning a
|
|
140
|
+
* destroy silently orphaned both forwards. Pure; no I/O. */
|
|
141
|
+
function localVmReapCmd({ port, previewPort } = {}) {
|
|
142
|
+
return [
|
|
143
|
+
`lsof -ti:${port} 2>/dev/null | xargs -r kill`,
|
|
144
|
+
cloudflaredTunnelSweepCmd({ localPort: port }),
|
|
145
|
+
editorForwardSweepCmd({ localPort: port }),
|
|
146
|
+
previewForwardSweepCmd({ localPort: previewPort }),
|
|
147
|
+
].join("; ");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
module.exports = {
|
|
151
|
+
editorTunnelCmd,
|
|
152
|
+
previewTunnelCmd,
|
|
153
|
+
editorForwardSweepCmd,
|
|
154
|
+
previewForwardSweepCmd,
|
|
155
|
+
cloudUpSweepCmd,
|
|
156
|
+
cloudflaredTunnelSweepCmd,
|
|
157
|
+
localVmReapCmd,
|
|
158
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codeyam-editor/codeyam-editor",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"description": "Language-agnostic managed execution sandbox for scenario-driven development",
|
|
5
5
|
"bin": {
|
|
6
6
|
"codeyam-editor": "./npm/cli.js"
|
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
"playwright": "^1.58.2"
|
|
10
10
|
},
|
|
11
11
|
"optionalDependencies": {
|
|
12
|
-
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.
|
|
13
|
-
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.
|
|
14
|
-
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.
|
|
15
|
-
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.
|
|
12
|
+
"@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.16",
|
|
13
|
+
"@codeyam-editor/codeyam-editor-darwin-x64": "0.1.16",
|
|
14
|
+
"@codeyam-editor/codeyam-editor-linux-x64": "0.1.16",
|
|
15
|
+
"@codeyam-editor/codeyam-editor-win32-x64": "0.1.16"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"codeyam",
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as O}from"./useEvents-DNb1olaj.js";import{m as E}from"./index-
|
|
1
|
+
import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as O}from"./useEvents-DNb1olaj.js";import{m as E}from"./index-Bkrhkuf5.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(`
|
|
2
2
|
`)})}),s&&e.jsx(_,{label:"Request Body",children:e.jsx("pre",{style:M,children:typeof t.requestBody=="string"?t.requestBody:JSON.stringify(t.requestBody,null,2)})}),l&&e.jsx(_,{label:"Response Body",children:e.jsx("pre",{style:M,children:typeof t.responseBody=="string"?t.responseBody:JSON.stringify(t.responseBody,null,2)})}),r.length===0&&!o&&!s&&!l&&e.jsx("div",{style:{color:"#8b949e",fontSize:12},children:"No additional details available for this request"})]})}const le=Object.freeze(Object.defineProperty({__proto__:null,DetailSection:_,InspectorTrafficDetail:B},Symbol.toStringTag,{value:"Module"}));function L({event:t}){const[r,o]=a.useState(!1),s=new Date(t.timestampMs).toLocaleTimeString(),l=t.mocked;return e.jsxs(e.Fragment,{children:[e.jsxs("tr",{onClick:()=>o(!r),style:{borderBottom:"1px solid #21262d",background:l?"transparent":"rgba(210, 153, 34, 0.05)",cursor:"pointer"},children:[e.jsxs("td",{style:{...h,color:"#8b949e",textAlign:"center"},children:[e.jsx("span",{style:{display:"inline-block",width:12,marginRight:4,fontSize:10,color:"#8b949e",transition:"transform 0.15s",transform:r?"rotate(90deg)":"rotate(0deg)"},children:"▶"}),s]}),e.jsx("td",{style:{...h,textAlign:"center"},children:e.jsx(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};
|