@codeyam-editor/codeyam-editor 0.1.4 → 0.1.5

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 (66) hide show
  1. package/npm/cloud-branch-switch.js +1 -1
  2. package/npm/cloud.js +1415 -73
  3. package/npm/container.js +3 -0
  4. package/npm/disk-pressure.js +78 -0
  5. package/npm/editor-dev.js +96 -15
  6. package/npm/editor.js +85 -37
  7. package/npm/fleet-auth-health.js +75 -0
  8. package/npm/fleet-auth.js +6 -1
  9. package/npm/fleet-build-data.js +51 -2
  10. package/npm/fleet-cleanup.js +118 -7
  11. package/npm/fleet-destroy-safety.js +55 -0
  12. package/npm/fleet-effect-status.js +33 -32
  13. package/npm/fleet-host.js +74 -0
  14. package/npm/fleet-job-badge.js +1 -1
  15. package/npm/fleet-launchd.js +171 -0
  16. package/npm/fleet-live-status.js +123 -0
  17. package/npm/fleet-plans.js +135 -0
  18. package/npm/fleet-prewarm.js +165 -0
  19. package/npm/fleet-public-url.js +160 -0
  20. package/npm/fleet-queue.js +38 -1
  21. package/npm/fleet-reconcile.js +43 -1
  22. package/npm/fleet-reset.js +298 -13
  23. package/npm/fleet-roster.js +6 -8
  24. package/npm/fleet-self-update.js +142 -0
  25. package/npm/fleet-setup-steps.js +105 -0
  26. package/npm/fleet-sleep-status.js +33 -0
  27. package/npm/fleet-strand.js +150 -0
  28. package/npm/fleet-stuck-status.js +111 -0
  29. package/npm/fleet-vm-access.js +234 -0
  30. package/npm/fleet-vm-readiness.js +104 -0
  31. package/npm/fleet-vm-slugs.js +116 -0
  32. package/npm/google-ingress.js +211 -0
  33. package/npm/ingress-invoke.js +292 -0
  34. package/npm/launch-config.js +204 -3
  35. package/npm/scenario-check.js +603 -32
  36. package/npm/scenario-handlers.js +78 -12
  37. package/npm/scenario-metrics.js +116 -1
  38. package/npm/scenario-mocks.js +20 -0
  39. package/npm/scenario-playwright.js +366 -29
  40. package/npm/script-drift.js +81 -0
  41. package/npm/utils.js +329 -50
  42. package/npm/vm-urls.js +69 -8
  43. package/package.json +5 -5
  44. package/ui/dist/assets/ContainerInspector-D7VZkkaj.js +2 -0
  45. package/ui/dist/assets/EditorShell-DHhIaotd.css +1 -0
  46. package/ui/dist/assets/EditorShell-r-796N_W.js +191 -0
  47. package/ui/dist/assets/{Markdown-BHBqz-Mj.js → Markdown-BoLHHFXH.js} +1 -1
  48. package/ui/dist/assets/ProjectLauncherScreen-Ck-XWkQN.js +1 -0
  49. package/ui/dist/assets/ScenarioDataPanel-B9iDSUpA.js +5 -0
  50. package/ui/dist/assets/ScenarioDataPanelFullPage-CBLTGC4-.js +1 -0
  51. package/ui/dist/assets/ScenarioRenderer-BhLfT-nX.js +21 -0
  52. package/ui/dist/assets/StepsSimulator-BFldWyoX.js +1 -0
  53. package/ui/dist/assets/index-CIXZkwfB.css +1 -0
  54. package/ui/dist/assets/{index-BuK0lizt.js → index-QsOiOG9x.js} +9 -9
  55. package/ui/dist/assets/useEvents-DHkLJZR4.js +1 -0
  56. package/ui/dist/codeyam-mark.svg +4 -0
  57. package/ui/dist/codeyam-wordmark.svg +5 -0
  58. package/ui/dist/index.html +2 -2
  59. package/ui/dist/assets/ContainerInspector-C8EkH4PP.js +0 -2
  60. package/ui/dist/assets/EditorShell-tMMdarZD.js +0 -187
  61. package/ui/dist/assets/ScenarioDataPanel-xWYGZOpo.js +0 -5
  62. package/ui/dist/assets/ScenarioDataPanelFullPage-5G6KuZgw.js +0 -1
  63. package/ui/dist/assets/ScenarioRenderer-CSdhX1Ak.js +0 -1
  64. package/ui/dist/assets/StepsSimulator-CRtmlyNZ.js +0 -1
  65. package/ui/dist/assets/index-BGqairFV.css +0 -1
  66. package/ui/dist/assets/useEvents-DggWrtHe.js +0 -1
@@ -1,15 +1,59 @@
1
1
  const { createIssue } = require("./scenario-issues");
2
2
 
3
- function getInitScript() {
4
- return `
5
- window.__codeyamUnhandledRejections = [];
6
- window.addEventListener("unhandledrejection", (event) => {
7
- const reason = event.reason;
8
- const message =
9
- reason instanceof Error ? reason.message : String(reason);
10
- window.__codeyamUnhandledRejections.push(message);
11
- });
3
+ // Known substrings that mean the app refused to initialize because it was loaded
4
+ // from an *insecure context* — a bare dotted-quad IP rather than HTTPS or the
5
+ // hostname `localhost`. This is a whole CLASS of failure (Sveltia CMS's
6
+ // "only works with HTTPS or localhost", anything gating on `isSecureContext`,
7
+ // `crypto.subtle`, service workers, `Secure` cookies, WebAuthn), not one app.
8
+ // Matching reclassifies the error as an actionable `insecure-host` advisory
9
+ // instead of a generic console/page failure that surfaces as a bare
10
+ // `screenshot=null` with no explanation. Substrings are matched
11
+ // case-insensitively; keep the set small and well-known.
12
+ const INSECURE_CONTEXT_SIGNATURES = [
13
+ "only works with https or localhost",
14
+ "issecurecontext",
15
+ "secure context",
16
+ "requires https",
17
+ ];
18
+
19
+ // The single actionable message the `insecure-host` advisory carries. Names the
20
+ // class of failure and the fix (the preview origin is `localhost` by default;
21
+ // stubborn apps can opt into HTTPS) rather than echoing one app's raw error.
22
+ const INSECURE_HOST_ADVISORY_MESSAGE =
23
+ "The app refused to run because it was loaded from an insecure context " +
24
+ "(a bare IP). It requires a secure context — HTTPS or the hostname " +
25
+ "`localhost`. The preview origin is `localhost` by default; if this persists, " +
26
+ "the app may require HTTPS even on localhost — enable `proxy.httpsPreview` in " +
27
+ ".codeyam/editor.json.";
28
+
29
+ // Reclassify an error string as an `insecure-host` advisory when it matches a
30
+ // known insecure-context signature, else null. Pure; the original text is kept
31
+ // as the `contextSnippet` so the operator can still see what the app logged.
32
+ function insecureContextAdvisory(text) {
33
+ if (typeof text !== "string" || text.length === 0) return null;
34
+ const lower = text.toLowerCase();
35
+ const matched = INSECURE_CONTEXT_SIGNATURES.some((sig) => lower.includes(sig));
36
+ if (!matched) return null;
37
+ return createIssue("insecure-host", INSECURE_HOST_ADVISORY_MESSAGE, {
38
+ contextSnippet: text,
39
+ });
40
+ }
12
41
 
42
+ // `allowWebSocket` keeps the real `WebSocket` for scenarios that script a
43
+ // `/ws/terminal` transcript (or a WS stream): those captures NEED the socket to
44
+ // connect so the server replays the scripted agent state into the frame. The
45
+ // stub below exists to silence the live terminal's reconnect loop on EVERY
46
+ // OTHER capture (a component that opens `/ws/terminal` with no scripted
47
+ // playback would otherwise screenshot the "Reconnecting…" overlay); for a
48
+ // scripted scenario the server holds the socket open after replay, so there is
49
+ // no reconnect spam to silence and stubbing only hides the very state we are
50
+ // trying to capture. The flag is computed by the capture orchestrator from the
51
+ // scenario's `mocks.transcripts` / `mocks.streams` — see
52
+ // `scenarioScriptsLiveSocket` in scenario-check.js.
53
+ function getInitScript(allowWebSocket = false) {
54
+ const webSocketStub = allowWebSocket
55
+ ? ""
56
+ : `
13
57
  // Stub WebSocket during capture to prevent terminal reconnection spam.
14
58
  window.WebSocket = class StubWebSocket {
15
59
  static CONNECTING = 0;
@@ -32,14 +76,28 @@ function getInitScript() {
32
76
  if (this.onclose) this.onclose(new CloseEvent("close"));
33
77
  }, 0);
34
78
  }
35
- };
79
+ };`;
80
+ return `
81
+ window.__codeyamUnhandledRejections = [];
82
+ window.addEventListener("unhandledrejection", (event) => {
83
+ const reason = event.reason;
84
+ const message =
85
+ reason instanceof Error ? reason.message : String(reason);
86
+ window.__codeyamUnhandledRejections.push(message);
87
+ });
88
+ ${webSocketStub}
36
89
  `;
37
90
  }
38
91
 
39
92
  function handleConsoleMessage(message) {
40
93
  if (message.type() !== "error") return null;
41
94
  const text = message.text();
42
-
95
+
96
+ // An insecure-context refusal wins over the generic `console` classification:
97
+ // it is the targeted, actionable signal for the secure-context app class.
98
+ const advisory = insecureContextAdvisory(text);
99
+ if (advisory) return advisory;
100
+
43
101
  // Ignore known dev-server WebSocket/HMR errors from Vite proxy
44
102
  if (
45
103
  text.includes("WebSocket connection to") ||
@@ -68,7 +126,12 @@ function handleConsoleMessage(message) {
68
126
  }
69
127
 
70
128
  function handlePageError(error) {
71
- return createIssue("pageerror", error.message || String(error));
129
+ const text = error.message || String(error);
130
+ // A secure-context guard often throws at boot rather than logging — surface
131
+ // the same actionable advisory for the pageerror path.
132
+ const advisory = insecureContextAdvisory(text);
133
+ if (advisory) return advisory;
134
+ return createIssue("pageerror", text);
72
135
  }
73
136
 
74
137
  function handleRequestFailed(request) {
@@ -95,4 +158,7 @@ module.exports = {
95
158
  handleConsoleMessage,
96
159
  handlePageError,
97
160
  handleRequestFailed,
161
+ insecureContextAdvisory,
162
+ INSECURE_CONTEXT_SIGNATURES,
163
+ INSECURE_HOST_ADVISORY_MESSAGE,
98
164
  };
@@ -25,6 +25,34 @@ function hasLoadingMarkers(text, extraMarkers = []) {
25
25
 
26
26
  function hasRenderableContent(state) {
27
27
  if (!state) return false;
28
+
29
+ // Reveal-suppression guard. A scroll-gated entrance animation starts its
30
+ // content at `opacity:0` and reveals it via an IntersectionObserver wired
31
+ // from the app shell. Captured in isolation (no shell), the observer never
32
+ // fires, so the text renders into the DOM — `bodyTextLength`/`rootTextLength`
33
+ // are non-zero — yet stays visually invisible and the screenshot is blank.
34
+ // `visibleTextLength` is the length of text actually PAINTED to the frame
35
+ // (computed-style opacity/visibility aware). When the DOM carries text but
36
+ // none of it is visible AND there is no visible media, the frame is
37
+ // near-blank: report no content so the blank gate rejects it instead of
38
+ // waving through an empty shell. `visibleTextLength` is optional — older
39
+ // capture configs (and stubbed test targets) omit it, in which case the
40
+ // original DOM-presence behavior below is preserved byte-for-byte.
41
+ const domTextLength = Math.max(
42
+ state.bodyTextLength || 0,
43
+ state.rootTextLength || 0,
44
+ );
45
+ const hasVisibleMedia =
46
+ (state.loadedImageCount || 0) > 0 || (state.mediaBboxCount || 0) > 0;
47
+ if (
48
+ typeof state.visibleTextLength === "number" &&
49
+ domTextLength > 0 &&
50
+ state.visibleTextLength === 0 &&
51
+ !hasVisibleMedia
52
+ ) {
53
+ return false;
54
+ }
55
+
28
56
  if (
29
57
  state.rootChildCount > 0 ||
30
58
  state.rootTextLength > 0 ||
@@ -40,7 +68,17 @@ function hasRenderableContent(state) {
40
68
  function describeBlankReason(state) {
41
69
  if (!state) return "no content state collected";
42
70
  const parts = [];
43
- if (!(state.bodyTextLength > 0)) {
71
+ // Distinguish "no text in the DOM at all" from "text rendered but nothing is
72
+ // visible" — the latter is the reveal-suppression symptom (opacity:0
73
+ // entrance content whose observer never fired in isolation), which points
74
+ // the author at a different fix than a genuinely empty page.
75
+ if (
76
+ typeof state.visibleTextLength === "number" &&
77
+ state.bodyTextLength > 0 &&
78
+ state.visibleTextLength === 0
79
+ ) {
80
+ parts.push("text rendered but not visible (reveal-suppressed?)");
81
+ } else if (!(state.bodyTextLength > 0)) {
44
82
  parts.push("no text");
45
83
  }
46
84
  const imageCount = state.imageCount || 0;
@@ -69,6 +107,16 @@ const ERROR_PATTERNS = [
69
107
  "Scenario Error",
70
108
  ];
71
109
 
110
+ // The DOM attribute codeyam's own ScenarioRenderer stamps on its error and
111
+ // seed-error fallback frames. Error detection anchors on this marker rather
112
+ // than scanning the whole page body for ERROR_PATTERNS: a legitimately-mocked
113
+ // scenario whose *content* quotes one of those harness phrases (a journal entry
114
+ // describing the error component, say) must not be flagged as a failed capture.
115
+ // Arbitrary client apps that never render a codeyam ScenarioRenderer never emit
116
+ // the marker, so their pages are treated as healthy regardless of body text —
117
+ // these patterns were always codeyam-harness-specific, never generic.
118
+ const SCENARIO_ERROR_MARKER = "data-codeyam-scenario-error";
119
+
72
120
  function hasErrorPatterns(text) {
73
121
  return ERROR_PATTERNS.some((pattern) => text.includes(pattern));
74
122
  }
@@ -95,14 +143,81 @@ function buildErrorContextSnippet(text, pattern) {
95
143
  return `${prefix}${slice}${suffix}`;
96
144
  }
97
145
 
146
+ // Classify a scenario-error fallback into a `{ matchedPattern, contextSnippet }`
147
+ // from the marker info collected off the page. `marker` is the
148
+ // `{ reason, text }` read from the `[data-codeyam-scenario-error]` element, or
149
+ // null when no such element is present. Returns null when there is no marked
150
+ // fallback — the page is healthy and no error-content failure is raised.
151
+ //
152
+ // When the marked fallback text contains a known ERROR_PATTERN (the render-error
153
+ // path renders `Component "X" not found in registry`), that pattern is named so
154
+ // the downstream Rust classifier can still extract the component; otherwise the
155
+ // marker's `reason` attribute classifies it (the seed-error fallback, whose
156
+ // "Seed Error" copy is intentionally not an ERROR_PATTERN). Because the text is
157
+ // scoped to the fallback element — not the whole body — content elsewhere on the
158
+ // page can quote these phrases freely without tripping detection.
159
+ function findScenarioError(marker) {
160
+ // The capture passes the `frame.evaluate` result of querying the marker
161
+ // element: either null (no fallback rendered) or a `{reason, text}` object.
162
+ // Anything else (a bare string, a primitive) is not a marked error — guard
163
+ // so a non-object never reads as a present marker and false-flags the page.
164
+ if (!marker || typeof marker !== "object") return null;
165
+ const text = marker.text || "";
166
+ const reason = marker.reason || "";
167
+ const matchedPattern = findErrorPattern(text) || reason || "scenario error";
168
+ const contextSnippet =
169
+ buildErrorContextSnippet(text, matchedPattern) ||
170
+ (text ? text.replace(/\s+/g, " ").trim().slice(0, 200) : null);
171
+ return { matchedPattern, contextSnippet };
172
+ }
173
+
174
+ // Build the non-blocking "this page looks client-fetched" advisory from the two
175
+ // settle signals the capture already computes: `stableOutcome` from
176
+ // waitForStablePage (did the page settle, and was a loading marker still up at
177
+ // the cap?) and `networkOutcome` from waitForNetworkQuiet (did in-flight network
178
+ // go quiet, or did the bounded wait cap out?). Captures settle within a bounded
179
+ // window so a never-idle stream can't hang them — which means a slower
180
+ // client-side fetch lands AFTER the window and the screenshot catches the
181
+ // loading/initial state instead of the populated one. When either signal fires,
182
+ // name the cause and the two reliable fixes (server-render the data, or drive a
183
+ // props-driven isolated component scenario) so the agent reaches for those up
184
+ // front instead of rediscovering the constraint by trial and error. Returns the
185
+ // advisory string, or null when the page settled cleanly (the SSR / props-driven
186
+ // happy path — no advisory, so a healthy capture stays noise-free).
187
+ function buildSettleAdvisory(stableOutcome, networkOutcome) {
188
+ const causes = [];
189
+ if (
190
+ stableOutcome &&
191
+ stableOutcome.stabilized === false &&
192
+ stableOutcome.hadLoadingMarkers === true
193
+ ) {
194
+ causes.push("a loading marker was still on the page");
195
+ }
196
+ if (networkOutcome && networkOutcome.quiet === false) {
197
+ causes.push("network requests were still in flight");
198
+ }
199
+ if (causes.length === 0) return null;
200
+ return (
201
+ `When this page was captured, ${causes.join(" and ")} — it likely fetches ` +
202
+ `its data on the client. Captures settle within a bounded window, so data ` +
203
+ `that arrives via a client-side fetch after that window renders as the ` +
204
+ `loading/initial state, not the populated state. To capture a populated ` +
205
+ `state, server-render the data (SSR/RSC) or drive a props-driven isolated ` +
206
+ `component scenario.`
207
+ );
208
+ }
209
+
98
210
  module.exports = {
99
211
  hasLoadingMarkers,
100
212
  hasRenderableContent,
213
+ buildSettleAdvisory,
101
214
  describeBlankReason,
102
215
  shouldStopWaitingForImages,
103
216
  hasErrorPatterns,
104
217
  findErrorPattern,
218
+ findScenarioError,
105
219
  buildErrorContextSnippet,
106
220
  ERROR_PATTERNS,
221
+ SCENARIO_ERROR_MARKER,
107
222
  ERROR_CONTEXT_RADIUS,
108
223
  };
@@ -98,10 +98,30 @@ async function attachHttpMocks(page, httpMocks) {
98
98
  });
99
99
  }
100
100
 
101
+
102
+ // True when a console "Failed to load resource" error at `url` corresponds to
103
+ // a mock this scenario DECLARED with an error status (>= 400). An intentional
104
+ // error-state scenario (e.g. a History tab mocking `GET /api/history` -> 500)
105
+ // must not fail its own capture on the console noise its mock deliberately
106
+ // produces. Console errors carry no HTTP method, so any method's mock on a
107
+ // matching target counts.
108
+ function isDeclaredErrorMock(httpMocks, url) {
109
+ const candidates = normalizeMockCandidates(url);
110
+ for (const [key, mock] of Object.entries(httpMocks || {})) {
111
+ const spaceIdx = key.indexOf(" ");
112
+ if (spaceIdx === -1) continue;
113
+ const target = key.slice(spaceIdx + 1);
114
+ if (!candidates.includes(target)) continue;
115
+ if ((mock.status || 200) >= 400) return true;
116
+ }
117
+ return false;
118
+ }
119
+
101
120
  module.exports = {
102
121
  normalizeMockCandidates,
103
122
  findHttpMock,
104
123
  mockedTargets,
105
124
  requestTargetsMock,
106
125
  attachHttpMocks,
126
+ isDeclaredErrorMock,
107
127
  };