@codeyam-editor/codeyam-editor 0.1.5 → 0.1.6

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.
Files changed (76) hide show
  1. package/npm/cli.js +5 -1
  2. package/npm/cloud.js +1884 -210
  3. package/npm/codesign-plan.js +68 -0
  4. package/npm/empty-project-dir.js +92 -7
  5. package/npm/fleet-add-failure.js +252 -0
  6. package/npm/fleet-asset-version.js +37 -0
  7. package/npm/fleet-auth-health.js +143 -5
  8. package/npm/fleet-build-data.js +60 -5
  9. package/npm/fleet-cleanup.js +114 -9
  10. package/npm/fleet-destroy-safety.js +230 -13
  11. package/npm/fleet-early-roster.js +87 -0
  12. package/npm/fleet-effect-poll.js +110 -0
  13. package/npm/fleet-env.js +86 -0
  14. package/npm/fleet-ghcr-secret.js +129 -0
  15. package/npm/fleet-git-maintenance.js +142 -0
  16. package/npm/fleet-host-op-limiter.js +68 -0
  17. package/npm/fleet-iap-token.js +165 -0
  18. package/npm/fleet-live-status.js +40 -8
  19. package/npm/fleet-memory.js +245 -9
  20. package/npm/fleet-plans.js +98 -4
  21. package/npm/fleet-pool.js +75 -16
  22. package/npm/fleet-prewarm.js +87 -11
  23. package/npm/fleet-project-drift.js +58 -0
  24. package/npm/fleet-public-url.js +234 -0
  25. package/npm/fleet-queue.js +58 -1
  26. package/npm/fleet-reachability-debounce.js +69 -0
  27. package/npm/fleet-reachability.js +80 -0
  28. package/npm/fleet-reset.js +32 -0
  29. package/npm/fleet-running-plan.js +85 -0
  30. package/npm/fleet-session-token-cache.js +65 -0
  31. package/npm/fleet-setup-steps.js +19 -12
  32. package/npm/fleet-status-refs.js +96 -0
  33. package/npm/fleet-strand.js +39 -8
  34. package/npm/fleet-stuck-status.js +13 -7
  35. package/npm/fleet-vm-access.js +170 -0
  36. package/npm/fleet-vm-probe-target.js +282 -0
  37. package/npm/fleet-vm-readiness.js +136 -3
  38. package/npm/google-ingress.js +15 -0
  39. package/npm/ingress-invoke.js +478 -0
  40. package/npm/launch-config.js +182 -6
  41. package/npm/scenario-check.js +282 -10
  42. package/npm/scenario-handlers.js +85 -27
  43. package/npm/scenario-interactivity.js +145 -6
  44. package/npm/scenario-issues.js +13 -1
  45. package/npm/scenario-mocks.js +129 -26
  46. package/npm/scenario-playwright.js +286 -12
  47. package/npm/script-drift.js +72 -1
  48. package/npm/utils.js +37 -3
  49. package/package.json +5 -5
  50. package/ui/dist/assets/{ContainerInspector-D7VZkkaj.js → ContainerInspector-DgM0pTAx.js} +1 -1
  51. package/ui/dist/assets/EditorShell-B7Q06uJ_.js +252 -0
  52. package/ui/dist/assets/EditorShell-DP5UY20G.css +1 -0
  53. package/ui/dist/assets/{Markdown-BoLHHFXH.js → Markdown-Chgdf3PW.js} +1 -1
  54. package/ui/dist/assets/ProjectLauncherScreen-BRrEr8Ng.js +1 -0
  55. package/ui/dist/assets/ScenarioDataPanel-BeQfa28g.js +9 -0
  56. package/ui/dist/assets/{ScenarioDataPanel-DYP7pi_n.css → ScenarioDataPanel-C1LukYI6.css} +1 -1
  57. package/ui/dist/assets/{ScenarioDataPanelFullPage-CBLTGC4-.js → ScenarioDataPanelFullPage-BLqRCxQX.js} +1 -1
  58. package/ui/dist/assets/ScenarioRenderer-CIAxfHPh.js +21 -0
  59. package/ui/dist/assets/{StepsSimulator-BFldWyoX.js → StepsSimulator-CymGgfbu.js} +1 -1
  60. package/ui/dist/assets/index-BygCiJSb.css +1 -0
  61. package/ui/dist/assets/{index-QsOiOG9x.js → index-DCHqRrwO.js} +4 -4
  62. package/ui/dist/assets/{markdown-v0F0UEt3.js → markdown-C9tbsVHX.js} +1 -1
  63. package/ui/dist/assets/react-nrLBr15I.js +28 -0
  64. package/ui/dist/assets/{useEvents-DHkLJZR4.js → useEvents-S3Qk1Y_d.js} +1 -1
  65. package/ui/dist/assets/useServerIdentity-B7rI0u3O.js +1 -0
  66. package/ui/dist/index.html +4 -4
  67. package/ui/dist/open-source/codeyam-counter.png +0 -0
  68. package/ui/dist/open-source/el-carot.png +0 -0
  69. package/ui/dist/open-source/tabcommand.png +0 -0
  70. package/ui/dist/assets/EditorShell-DHhIaotd.css +0 -1
  71. package/ui/dist/assets/EditorShell-r-796N_W.js +0 -191
  72. package/ui/dist/assets/ProjectLauncherScreen-Ck-XWkQN.js +0 -1
  73. package/ui/dist/assets/ScenarioDataPanel-B9iDSUpA.js +0 -5
  74. package/ui/dist/assets/ScenarioRenderer-BhLfT-nX.js +0 -21
  75. package/ui/dist/assets/index-CIXZkwfB.css +0 -1
  76. package/ui/dist/assets/react-CSS0HapR.js +0 -28
@@ -336,6 +336,110 @@ async function forceFinalVisualState(target) {
336
336
  });
337
337
  }
338
338
 
339
+ // Center the isolated-component wrapper in the viewport before the shot.
340
+ //
341
+ // The capture pipeline always shoots the full viewport (`page.screenshot({
342
+ // fullPage: false })`) and never an element clip, so an isolation page that
343
+ // does not center its own component strands it in the top-left corner of every
344
+ // frame. Next.js does this in its isolation layout and the query-param stacks
345
+ // do it in `.codeyam/harness/isolate.tsx`, but the server-rendered scaffolds
346
+ // historically did not — and neither do pages hand-authored before that was
347
+ // fixed. This pass closes the gap at capture time, for any stack.
348
+ //
349
+ // It MEASURES rather than assumes: a wrapper that is already centered is left
350
+ // alone, so pages that already do the right thing are byte-identical. The
351
+ // `#codeyam-capture` marker (the cross-stack isolation convention; expo emits
352
+ // it via `nativeID`) is the gate, so an ordinary route capture is never
353
+ // touched. Centering is applied to `<body>` and to each ancestor strictly
354
+ // between body and the wrapper — never to the wrapper itself, which frequently
355
+ // carries the component's own padding/background and must keep its own box.
356
+ // Each axis independently falls back to `flex-start` when the wrapper overflows
357
+ // the viewport, so an oversized component loses its bottom/right edge rather
358
+ // than being clipped symmetrically out of its top/left one.
359
+ //
360
+ // Idempotent (a single injected style id), best-effort, and returns a small
361
+ // result object describing what it did.
362
+ async function centerCaptureWrapper(target) {
363
+ return target.evaluate(() => {
364
+ const STYLE_ID = "__codeyam_center_capture";
365
+ const ANCESTOR_CLASS = "__codeyam-center-ancestor";
366
+ // Subpixel layout and fractional scrollbar widths make exact gap equality
367
+ // unreliable; a couple of px of slop keeps already-centered pages on the
368
+ // no-op branch.
369
+ const TOLERANCE_PX = 2;
370
+
371
+ if (typeof document.getElementById !== "function") {
372
+ return { applied: false, reason: "no-dom" };
373
+ }
374
+ if (document.getElementById(STYLE_ID)) {
375
+ return { applied: false, reason: "already-applied" };
376
+ }
377
+ const wrapper = document.getElementById("codeyam-capture");
378
+ if (!wrapper || typeof wrapper.getBoundingClientRect !== "function") {
379
+ return { applied: false, reason: "no-capture-marker" };
380
+ }
381
+
382
+ const rect = wrapper.getBoundingClientRect();
383
+ const viewportWidth = window.innerWidth;
384
+ const viewportHeight = window.innerHeight;
385
+
386
+ const leftGap = rect.left;
387
+ const rightGap = viewportWidth - rect.right;
388
+ const topGap = rect.top;
389
+ const bottomGap = viewportHeight - rect.bottom;
390
+
391
+ const fitsHorizontally = rect.width <= viewportWidth;
392
+ const fitsVertically = rect.height <= viewportHeight;
393
+ const centeredHorizontally =
394
+ fitsHorizontally && Math.abs(leftGap - rightGap) <= TOLERANCE_PX;
395
+ const centeredVertically =
396
+ fitsVertically && Math.abs(topGap - bottomGap) <= TOLERANCE_PX;
397
+
398
+ if (centeredHorizontally && centeredVertically) {
399
+ return { applied: false, reason: "already-centered" };
400
+ }
401
+
402
+ const horizontal = fitsHorizontally ? "center" : "flex-start";
403
+ const vertical = fitsVertically ? "center" : "flex-start";
404
+
405
+ // Stamp the ancestor chain so the injected rule targets exactly the
406
+ // elements between <body> and the wrapper and cannot leak elsewhere.
407
+ let node = wrapper.parentElement;
408
+ while (node && node !== document.body) {
409
+ if (node.classList && typeof node.classList.add === "function") {
410
+ node.classList.add(ANCESTOR_CLASS);
411
+ }
412
+ node = node.parentElement;
413
+ }
414
+
415
+ if (typeof document.createElement !== "function") {
416
+ return { applied: false, reason: "no-dom" };
417
+ }
418
+ const style = document.createElement("style");
419
+ style.id = STYLE_ID;
420
+ style.textContent =
421
+ "body, ." +
422
+ ANCESTOR_CLASS +
423
+ " {" +
424
+ "margin: 0;" +
425
+ "min-height: 100vh;" +
426
+ "display: flex;" +
427
+ "align-items: " +
428
+ vertical +
429
+ ";" +
430
+ "justify-content: " +
431
+ horizontal +
432
+ ";" +
433
+ "}";
434
+ const head = document.head || document.documentElement;
435
+ if (!head || typeof head.appendChild !== "function") {
436
+ return { applied: false, reason: "no-dom" };
437
+ }
438
+ head.appendChild(style);
439
+ return { applied: true, horizontal, vertical };
440
+ });
441
+ }
442
+
339
443
  async function collectImageStates(target) {
340
444
  return target.evaluate(() =>
341
445
  Array.from(document.images || []).map((img) => ({
@@ -575,9 +679,31 @@ async function loadScenarioInIframe(
575
679
  // and the nested scenario iframe inherits it (the Sveltia-class fix). The
576
680
  // inner iframe `src` is still `url`, so the document-response probe above is
577
681
  // unchanged.
578
- await page.goto(buildHarnessUrl(resolvedHarnessOrigin, url, background), {
682
+ const harnessUrl = buildHarnessUrl(resolvedHarnessOrigin, url, background);
683
+ const harnessResponse = await page.goto(harnessUrl, {
579
684
  waitUntil: "domcontentloaded",
580
685
  });
686
+ // Fail loudly on a fail-closed harness-auth rejection BEFORE the blind 30s
687
+ // `#scenario-frame` wait below. On a non-loopback (0.0.0.0 / cloud) bind the
688
+ // harness control-API route is session-token-gated; a generated
689
+ // `.codeyam/capture.js` (or an editor binary) that predates the `cy_session`
690
+ // cookie injection navigates without a token -> the server refuses the
691
+ // request with 401 -> the harness document never renders -> every component
692
+ // scenario times out on `#scenario-frame` with a generic Playwright error
693
+ // that hides the real cause. Surfacing the 401 here turns that blind timeout
694
+ // into a one-line root cause on the very first failing capture, naming both
695
+ // fixes. (Route/page scenarios navigate top-level to the un-gated app proxy,
696
+ // so they never hit this path.)
697
+ if (harnessResponse && harnessResponse.status() === 401) {
698
+ throw new Error(
699
+ `Capture harness route ${harnessUrl} returned 401 (fail-closed ` +
700
+ `session-token auth on a non-loopback bind). The capture script did not ` +
701
+ `send the \`cy_session\` token — most often a generated ` +
702
+ `\`.codeyam/capture.js\` (or an editor binary) that predates session-token ` +
703
+ `injection. Fixes: refresh the editor binary on this VM, or set ` +
704
+ `\`CODEYAM_INSECURE_BIND=1\` and restart the editor.`,
705
+ );
706
+ }
581
707
  } else {
582
708
  // Degraded fallback: no resolvable harness origin (server-state missing), so
583
709
  // use the legacy in-page harness. The top-level document is then
@@ -677,6 +803,14 @@ async function loadScenarioTopLevel(
677
803
  }
678
804
  }
679
805
 
806
+ // The CSS selector for "clickable / interactive" elements — buttons, links,
807
+ // role=button, form controls, <summary>, and anything with an onclick. Shared
808
+ // by the candidate-label collector (below) and the text-target resolver
809
+ // (`resolveTextTarget`), which prefers an interactive element over a plain
810
+ // text node when a click/press label matches both.
811
+ const INTERACTIVE_SELECTOR =
812
+ "button, a[href], [role=button], input, select, textarea, summary, [onclick]";
813
+
680
814
  // Collect up to 20 distinct visible labels of interactive elements on the
681
815
  // page — buttons, links, role=button, form controls, <summary>, and anything
682
816
  // with an onclick. Used to build an ACTIONABLE error when an interaction's
@@ -685,9 +819,7 @@ async function loadScenarioTopLevel(
685
819
  // mean one of these?" hint. Pure read (no clicks); falls back to value /
686
820
  // aria-label / placeholder when an element has no text.
687
821
  async function collectInteractiveLabels(frame) {
688
- return frame.evaluate(() => {
689
- const selector =
690
- "button, a[href], [role=button], input, select, textarea, summary, [onclick]";
822
+ return frame.evaluate((selector) => {
691
823
  const nodes = Array.from(document.querySelectorAll(selector));
692
824
  const labels = nodes
693
825
  .map((node) => {
@@ -701,7 +833,39 @@ async function collectInteractiveLabels(frame) {
701
833
  })
702
834
  .filter((label) => label.length > 0);
703
835
  return Array.from(new Set(labels)).slice(0, 20);
704
- });
836
+ }, INTERACTIVE_SELECTOR);
837
+ }
838
+
839
+ // Describe the elements a substring text match resolved, so an ambiguity
840
+ // warning can name the competing controls (e.g. the preset button "Bet" vs the
841
+ // disclosure button "…or bet on"). Reads each matched element's inner text via
842
+ // Playwright's `allInnerTexts`, caps the list at 5, and trims each to a single
843
+ // short line. Pure read; degrades to a count-only description if the locator
844
+ // API or the page can't produce texts.
845
+ async function describeMatchCandidates(baseLocator, matchCount) {
846
+ const CAP = 5;
847
+ const MAX_LEN = 60;
848
+ let texts = [];
849
+ try {
850
+ if (typeof baseLocator.allInnerTexts === "function") {
851
+ texts = await baseLocator.allInnerTexts();
852
+ }
853
+ } catch (_) {
854
+ texts = [];
855
+ }
856
+ const described = texts
857
+ .map((t) => String(t).replace(/\s+/g, " ").trim())
858
+ .filter((t) => t.length > 0)
859
+ .slice(0, CAP)
860
+ .map((t) => (t.length > MAX_LEN ? `${t.slice(0, MAX_LEN)}…` : t))
861
+ .map((t) => `"${t}"`);
862
+ if (described.length === 0) {
863
+ return [`${matchCount} elements (text unavailable)`];
864
+ }
865
+ if (matchCount > described.length) {
866
+ described.push(`…and ${matchCount - described.length} more`);
867
+ }
868
+ return described;
705
869
  }
706
870
 
707
871
  // Drive a single user-style interaction against the settled frame before the
@@ -714,16 +878,103 @@ async function collectInteractiveLabels(frame) {
714
878
  // `Enter`). On a no-match target this THROWS with the list of candidate
715
879
  // interactive labels — the capture script's outer catch turns that into a
716
880
  // failed capture with an actionable message, never a silent blank screenshot.
717
- async function performInteraction(frame, interaction, { timeoutMs = 5000 } = {}) {
881
+ //
882
+ // Resolve a visible-text target to the element most likely intended, instead
883
+ // of blindly taking the first substring hit. A bare
884
+ // `getByText(text, { exact: false })` is a case-insensitive SUBSTRING match, so
885
+ // a label can land on a plain text node or a placeholder rather than the chip /
886
+ // button the agent meant (observed: a `"Japan"` click hit placeholder text, not
887
+ // the chip; a `"Bet"` click hit a disclosure button described "or bet on"). We
888
+ // try increasingly-loose locators in priority order and take the first that
889
+ // matches anything:
890
+ // 1. exact + interactive (for click/press)
891
+ // 2. exact
892
+ // 3. substring + interactive (for click/press)
893
+ // 4. substring
894
+ // so an exact, clickable element wins over a substring plain-text node. Returns
895
+ // the winning tier's locator plus its match count; when nothing matches at all,
896
+ // returns the substring locator (count 0) so the caller's zero-match error path
897
+ // still fires with its candidate-label hint.
898
+ async function resolveTextTarget(frame, text, action) {
899
+ const preferInteractive = action === "click" || action === "press";
900
+ const exactBase = frame.getByText(text, { exact: true });
901
+ const substrBase = frame.getByText(text, { exact: false });
902
+
903
+ const tiers = [];
904
+ // `.and()` (Playwright ≥1.34) intersects two locators to the elements
905
+ // matching both — here, the text element that is ALSO interactive. Guard on
906
+ // its presence so a locator without `.and` degrades to text-only tiers.
907
+ if (preferInteractive && typeof exactBase.and === "function") {
908
+ const interactive = frame.locator(INTERACTIVE_SELECTOR);
909
+ tiers.push(exactBase.and(interactive));
910
+ tiers.push(exactBase);
911
+ tiers.push(substrBase.and(interactive));
912
+ tiers.push(substrBase);
913
+ } else {
914
+ tiers.push(exactBase);
915
+ tiers.push(substrBase);
916
+ }
917
+
918
+ for (const loc of tiers) {
919
+ const count = await loc.count();
920
+ if (count > 0) {
921
+ return { baseLocator: loc, matchCount: count };
922
+ }
923
+ }
924
+ return { baseLocator: substrBase, matchCount: 0 };
925
+ }
926
+
927
+ // Pick the single element to act on from a resolved locator. When more than one
928
+ // candidate remains within the winning tier, prefer a VISIBLE element over a
929
+ // hidden one rather than acting on raw `.first()` (a hidden duplicate — an
930
+ // off-screen menu clone, an aria-hidden mirror — is almost never the intended
931
+ // target). Degrades to `.first()` when the locator API lacks a visible filter
932
+ // or no candidate is visible.
933
+ async function pickBestCandidate(baseLocator, matchCount) {
934
+ if (matchCount <= 1) {
935
+ return baseLocator.first();
936
+ }
937
+ try {
938
+ if (typeof baseLocator.filter === "function") {
939
+ const visible = baseLocator.filter({ visible: true });
940
+ if ((await visible.count()) > 0) {
941
+ return visible.first();
942
+ }
943
+ }
944
+ } catch (_) {
945
+ // Locator API without a `{ visible: true }` filter — fall through.
946
+ }
947
+ return baseLocator.first();
948
+ }
949
+
950
+ // Text matching prefers an EXACT, INTERACTIVE element over a looser substring /
951
+ // plain-text hit (see `resolveTextTarget`), so the first attempt lands on the
952
+ // chip or button the agent meant. When a genuine ambiguity survives that
953
+ // resolution (>1 candidate in the winning tier), we still act — on the best
954
+ // visible candidate — but push an actionable warning naming every candidate
955
+ // into the optional `warnings` array so the agent can switch to an exact
956
+ // selector; a zero-match throws an error that spells out the substring caveat
957
+ // and the exact-selector / URL-query-param alternatives.
958
+ async function performInteraction(
959
+ frame,
960
+ interaction,
961
+ { timeoutMs = 5000, warnings } = {},
962
+ ) {
718
963
  const { action, selector, text, value } = interaction || {};
719
964
 
720
- let locator;
965
+ let baseLocator;
721
966
  let targetDesc;
967
+ let matchedByText = false;
968
+ let matchCount;
722
969
  if (typeof text === "string" && text.length > 0) {
723
- locator = frame.getByText(text, { exact: false }).first();
970
+ const resolved = await resolveTextTarget(frame, text, action);
971
+ baseLocator = resolved.baseLocator;
972
+ matchCount = resolved.matchCount;
724
973
  targetDesc = `text "${text}"`;
974
+ matchedByText = true;
725
975
  } else if (typeof selector === "string" && selector.length > 0) {
726
- locator = frame.locator(selector).first();
976
+ baseLocator = frame.locator(selector);
977
+ matchCount = await baseLocator.count();
727
978
  targetDesc = `selector "${selector}"`;
728
979
  } else {
729
980
  throw new Error(
@@ -731,14 +982,34 @@ async function performInteraction(frame, interaction, { timeoutMs = 5000 } = {})
731
982
  );
732
983
  }
733
984
 
734
- const matchCount = await locator.count();
735
985
  if (matchCount === 0) {
736
986
  const candidates = await collectInteractiveLabels(frame);
737
987
  const candidateList =
738
988
  candidates.length > 0 ? candidates.join(", ") : "(none found on page)";
739
989
  throw new Error(
740
990
  `preview-interact: no element matched ${targetDesc}. ` +
741
- `Candidate interactive labels: ${candidateList}`,
991
+ `Text is matched as a case-insensitive SUBSTRING, so a misspelled or ` +
992
+ `over-specific label matches nothing — prefer an exact/role/testid CSS ` +
993
+ `selector (e.g. {"selector":"[data-testid=\\"save\\"]"}) for a precise ` +
994
+ `target. Many filter/status/sort states are also reachable directly via a ` +
995
+ `URL query param (e.g. add "?status=active" to the path) with no ` +
996
+ `interaction at all. Candidate interactive labels: ${candidateList}`,
997
+ );
998
+ }
999
+
1000
+ const locator = await pickBestCandidate(baseLocator, matchCount);
1001
+
1002
+ // Ambiguity warning: a text target that still resolves >1 element within the
1003
+ // winning tier acts on the best visible candidate, which may not be the one
1004
+ // intended. Name the competing elements instead of silently proceeding. Only
1005
+ // fires for text matches — an explicit selector that matches many is the
1006
+ // caller's deliberate choice.
1007
+ if (matchedByText && matchCount > 1 && Array.isArray(warnings)) {
1008
+ warnings.push(
1009
+ `preview-interact: ${targetDesc} matched ${matchCount} elements — acting on the best visible candidate. ` +
1010
+ `Text resolution prefers an exact, interactive match, but several remain, so this may not be the element you meant. ` +
1011
+ `Candidates: ${(await describeMatchCandidates(baseLocator, matchCount)).join(" | ")}. ` +
1012
+ `Use an exact/role/testid selector to disambiguate.`,
742
1013
  );
743
1014
  }
744
1015
 
@@ -829,11 +1100,12 @@ async function performInteractionSequence(
829
1100
  settleMs = 5000,
830
1101
  loadingMarkers,
831
1102
  settle = waitForStablePage,
1103
+ warnings,
832
1104
  } = {},
833
1105
  ) {
834
1106
  for (let i = 0; i < interactions.length; i += 1) {
835
1107
  try {
836
- await performInteraction(frame, interactions[i], { timeoutMs });
1108
+ await performInteraction(frame, interactions[i], { timeoutMs, warnings });
837
1109
  } catch (err) {
838
1110
  // Prefix the failing step's index so a miss in a multi-step sequence is
839
1111
  // locatable, matching the model-side `interactions[i]` validator.
@@ -856,6 +1128,7 @@ module.exports = {
856
1128
  scrollThroughDocument,
857
1129
  collectVisibleTextLength,
858
1130
  forceFinalVisualState,
1131
+ centerCaptureWrapper,
859
1132
  collectImageStates,
860
1133
  waitForImagesSettled,
861
1134
  waitForAnimationsSettled,
@@ -865,6 +1138,7 @@ module.exports = {
865
1138
  loadScenarioInIframe,
866
1139
  loadScenarioTopLevel,
867
1140
  collectInteractiveLabels,
1141
+ describeMatchCandidates,
868
1142
  performInteraction,
869
1143
  waitForPredicate,
870
1144
  performInteractionSequence,
@@ -76,6 +76,77 @@ function classifyScriptDrift({ files }) {
76
76
  return { verdict: 'clean', staleFiles: [] };
77
77
  }
78
78
 
79
+ /**
80
+ * Decide, for one streamed provisioning script, whether the VM should receive the
81
+ * canonical remote-tip bytes or the local on-disk copy. The remote tip is the
82
+ * reviewed, canonical source: a checkout that is merely *behind* origin (a normal,
83
+ * expected consequence of an active fleet landing commits to these scripts) then
84
+ * streams exactly the bytes the drift guard expects — never stale on-disk bytes,
85
+ * never a false STALE abort. The local copy wins only when the operator authored
86
+ * content the remote tip does not have — an uncommitted (dirty) edit or a committed
87
+ * local-ahead change (developer mode / diverged) — or when the remote facts are
88
+ * unavailable (offline fallback, so an offline operator can still provision).
89
+ *
90
+ * @param {Object} file
91
+ * @param {string|null} file.localHash
92
+ * @param {string|null} file.remoteHash null/undefined ⇒ remote unreachable
93
+ * @param {string|null} file.baseHash
94
+ * @param {boolean} file.dirty
95
+ * @returns {'remote' | 'local'}
96
+ */
97
+ function streamedScriptSource({ localHash, remoteHash, baseHash, dirty }) {
98
+ // Remote unreachable / ref missing → the only bytes we have are on disk.
99
+ if (remoteHash === null || remoteHash === undefined) {
100
+ return 'local';
101
+ }
102
+ // Uncommitted local edit → the operator is provisioning unreviewed bytes on
103
+ // purpose (developer mode, or an explicit CODEYAM_SKIP_SCRIPT_DRIFT_CHECK bypass).
104
+ if (dirty) {
105
+ return 'local';
106
+ }
107
+ // Local content the remote tip does not have (committed local-ahead, or a genuine
108
+ // divergence): local differs from the remote tip AND from the merge base → honor
109
+ // the local commit rather than silently overwriting it with the remote tip.
110
+ if (localHash !== remoteHash && localHash !== baseHash) {
111
+ return 'local';
112
+ }
113
+ // Clean, or merely behind the remote tip → stream the canonical remote-tip bytes.
114
+ return 'remote';
115
+ }
116
+
117
+ /**
118
+ * Decide what the streamed-script drift preflight should do, from the classifier
119
+ * verdict plus which streamed files carry uncommitted edits. A checkout that is
120
+ * merely *behind* origin (verdict 'stale') is benign — the add streams the
121
+ * canonical remote-tip bytes (see streamedScriptSource), so it never blocks. The
122
+ * guard fires ('block') only for local content the remote tip does not have: an
123
+ * uncommitted (dirty) edit, or a genuine divergence. A committed local-ahead
124
+ * change is developer mode — warn, don't block.
125
+ *
126
+ * @param {Object} o
127
+ * @param {string} o.verdict classifyScriptDrift verdict
128
+ * @param {string[]} [o.staleFiles] files the classifier flagged (the diverged set)
129
+ * @param {string[]} [o.dirtyFiles] streamed files with uncommitted local edits
130
+ * @returns {{ block: boolean, reason: 'clean'|'behind'|'local-ahead'|'dirty'|'diverged', blockedFiles: string[] }}
131
+ */
132
+ function scriptDriftGuardAction({ verdict, staleFiles = [], dirtyFiles = [] }) {
133
+ if (verdict === 'diverged') {
134
+ return { block: true, reason: 'diverged', blockedFiles: staleFiles };
135
+ }
136
+ if (dirtyFiles.length > 0) {
137
+ return { block: true, reason: 'dirty', blockedFiles: dirtyFiles };
138
+ }
139
+ if (verdict === 'stale') {
140
+ return { block: false, reason: 'behind', blockedFiles: [] };
141
+ }
142
+ if (verdict === 'local-ahead') {
143
+ return { block: false, reason: 'local-ahead', blockedFiles: [] };
144
+ }
145
+ return { block: false, reason: 'clean', blockedFiles: [] };
146
+ }
147
+
79
148
  module.exports = {
80
- classifyScriptDrift
149
+ classifyScriptDrift,
150
+ streamedScriptSource,
151
+ scriptDriftGuardAction
81
152
  };
package/npm/utils.js CHANGED
@@ -103,6 +103,34 @@ function findBinary() {
103
103
  return null;
104
104
  }
105
105
 
106
+ /**
107
+ * Whether this install is a dev checkout (the source tree) rather than an
108
+ * npm-installed package. Signalled by a `Cargo.toml` at `rootDir` — present in
109
+ * the git checkout, absent in the npm install dir. The single source of truth
110
+ * for the checkout-vs-installed distinction; both `ensureBinary` (build-from-
111
+ * source vs error) and `serverSpawnEnv` (distribution-channel gating) read it.
112
+ * Accepts an explicit dir so it is unit-testable against a temp tree.
113
+ */
114
+ function isDevCheckout(dir = rootDir) {
115
+ return fs.existsSync(path.join(dir, "Cargo.toml"));
116
+ }
117
+
118
+ /**
119
+ * Env for a launched editor-server, tagging the distribution channel so the
120
+ * server's update-availability check knows an npm install can be upgraded via
121
+ * `npm install -g …@latest`. Pure: returns a NEW object, never mutates the
122
+ * input.
123
+ *
124
+ * Installed (non-dev-checkout) → set `CODEYAM_DISTRIBUTION=npm`, so the server
125
+ * polls the npm registry and surfaces the upgrade banner. Dev checkout → leave
126
+ * the env untouched (any pre-existing value from the parent env is preserved),
127
+ * so a `codeyam-editor` run from source is never nagged to "upgrade".
128
+ */
129
+ function serverSpawnEnv(baseEnv, { devCheckout } = {}) {
130
+ if (devCheckout) return { ...baseEnv };
131
+ return { ...baseEnv, CODEYAM_DISTRIBUTION: "npm" };
132
+ }
133
+
106
134
  /**
107
135
  * Return the binary path, building from source if we're in a dev checkout.
108
136
  *
@@ -120,9 +148,9 @@ function ensureBinary() {
120
148
  const existing = findBinary();
121
149
  if (existing) return existing;
122
150
 
123
- const isDevCheckout = fs.existsSync(path.join(rootDir, "Cargo.toml"));
151
+ const devCheckout = isDevCheckout();
124
152
 
125
- if (!isDevCheckout) {
153
+ if (!devCheckout) {
126
154
  const info = platformPackageInfo();
127
155
  if (!info) {
128
156
  console.error(
@@ -1051,7 +1079,11 @@ async function ensureServer(port = 14199) {
1051
1079
  const child = spawn(
1052
1080
  binary,
1053
1081
  ["start", "--no-open", "--port", String(port)],
1054
- buildServerSpawnOptions({ cwd: process.cwd(), env: process.env, logFd }),
1082
+ buildServerSpawnOptions({
1083
+ cwd: process.cwd(),
1084
+ env: serverSpawnEnv(process.env, { devCheckout: isDevCheckout() }),
1085
+ logFd,
1086
+ }),
1055
1087
  );
1056
1088
  if (logFd !== null) {
1057
1089
  try {
@@ -1192,6 +1224,8 @@ module.exports = {
1192
1224
  findBrokerBinary,
1193
1225
  verifyBrokerBinaryPresent,
1194
1226
  ensureBinary,
1227
+ isDevCheckout,
1228
+ serverSpawnEnv,
1195
1229
  staleBinaryBanner,
1196
1230
  enforceFreshBinary,
1197
1231
  uiDistDir,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codeyam-editor/codeyam-editor",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
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.5",
13
- "@codeyam-editor/codeyam-editor-darwin-x64": "0.1.5",
14
- "@codeyam-editor/codeyam-editor-linux-x64": "0.1.5",
15
- "@codeyam-editor/codeyam-editor-win32-x64": "0.1.5"
12
+ "@codeyam-editor/codeyam-editor-darwin-arm64": "0.1.6",
13
+ "@codeyam-editor/codeyam-editor-darwin-x64": "0.1.6",
14
+ "@codeyam-editor/codeyam-editor-linux-x64": "0.1.6",
15
+ "@codeyam-editor/codeyam-editor-win32-x64": "0.1.6"
16
16
  },
17
17
  "keywords": [
18
18
  "codeyam",
@@ -1,2 +1,2 @@
1
- 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-QsOiOG9x.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(`
1
+ import{j as e}from"./markdown-C9tbsVHX.js";import{b as a}from"./react-nrLBr15I.js";import{u as O}from"./useEvents-S3Qk1Y_d.js";import{b as E}from"./index-DCHqRrwO.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};