@canaryai/cli 0.2.8 → 0.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/README.md +77 -92
  2. package/dist/chunk-CEW4BDXD.js +186 -0
  3. package/dist/chunk-CEW4BDXD.js.map +1 -0
  4. package/dist/chunk-ERSNYLMZ.js +229 -0
  5. package/dist/chunk-ERSNYLMZ.js.map +1 -0
  6. package/dist/{chunk-FK3EZADZ.js → chunk-MSMC6UXW.js} +2021 -873
  7. package/dist/chunk-MSMC6UXW.js.map +1 -0
  8. package/dist/{chunk-K2OB72B6.js → chunk-Q7WFBG5C.js} +2 -2
  9. package/dist/{debug-workflow-55G4Y6YT.js → debug-workflow-53ULOFJC.js} +57 -36
  10. package/dist/debug-workflow-53ULOFJC.js.map +1 -0
  11. package/dist/{docs-RPFT7ZJB.js → docs-BEE3LOCO.js} +2 -2
  12. package/dist/{feature-flag-2FDSKOVX.js → feature-flag-CYTDV4ZB.js} +3 -2
  13. package/dist/{feature-flag-2FDSKOVX.js.map → feature-flag-CYTDV4ZB.js.map} +1 -1
  14. package/dist/index.js +72 -137
  15. package/dist/index.js.map +1 -1
  16. package/dist/init-M6I3MG3D.js +146 -0
  17. package/dist/init-M6I3MG3D.js.map +1 -0
  18. package/dist/{issues-6ZDNDSD6.js → issues-NLM72HLU.js} +3 -2
  19. package/dist/{issues-6ZDNDSD6.js.map → issues-NLM72HLU.js.map} +1 -1
  20. package/dist/{knobs-MZRTYS3P.js → knobs-O35GAU5M.js} +3 -2
  21. package/dist/{knobs-MZRTYS3P.js.map → knobs-O35GAU5M.js.map} +1 -1
  22. package/dist/list-4K4EIGAT.js +57 -0
  23. package/dist/list-4K4EIGAT.js.map +1 -0
  24. package/dist/local-NHXXPHZ3.js +63 -0
  25. package/dist/local-NHXXPHZ3.js.map +1 -0
  26. package/dist/{local-browser-X7J27IGS.js → local-browser-VAZORCO3.js} +3 -3
  27. package/dist/login-ZLP64YQP.js +130 -0
  28. package/dist/login-ZLP64YQP.js.map +1 -0
  29. package/dist/mcp-ZF5G5DCB.js +377 -0
  30. package/dist/mcp-ZF5G5DCB.js.map +1 -0
  31. package/dist/{record-4OX7HXWQ.js → record-V6QKFFH3.js} +133 -72
  32. package/dist/record-V6QKFFH3.js.map +1 -0
  33. package/dist/{release-L4IXOHDF.js → release-7TI7EIGD.js} +8 -4
  34. package/dist/release-7TI7EIGD.js.map +1 -0
  35. package/dist/session-UGNJXRUW.js +819 -0
  36. package/dist/session-UGNJXRUW.js.map +1 -0
  37. package/dist/skill-ORWAPBDW.js +424 -0
  38. package/dist/skill-ORWAPBDW.js.map +1 -0
  39. package/dist/{src-I4EXB5OD.js → src-4VIDSK4A.js} +18 -2
  40. package/dist/start-E532F3BU.js +112 -0
  41. package/dist/start-E532F3BU.js.map +1 -0
  42. package/dist/workflow-HXIUXRFI.js +613 -0
  43. package/dist/workflow-HXIUXRFI.js.map +1 -0
  44. package/package.json +1 -1
  45. package/dist/chunk-6WWHXWCS.js +0 -65
  46. package/dist/chunk-6WWHXWCS.js.map +0 -1
  47. package/dist/chunk-DXIAHB72.js +0 -340
  48. package/dist/chunk-DXIAHB72.js.map +0 -1
  49. package/dist/chunk-FK3EZADZ.js.map +0 -1
  50. package/dist/debug-workflow-55G4Y6YT.js.map +0 -1
  51. package/dist/mcp-4JVLADZL.js +0 -688
  52. package/dist/mcp-4JVLADZL.js.map +0 -1
  53. package/dist/record-4OX7HXWQ.js.map +0 -1
  54. package/dist/release-L4IXOHDF.js.map +0 -1
  55. /package/dist/{chunk-K2OB72B6.js.map → chunk-Q7WFBG5C.js.map} +0 -0
  56. /package/dist/{docs-RPFT7ZJB.js.map → docs-BEE3LOCO.js.map} +0 -0
  57. /package/dist/{local-browser-X7J27IGS.js.map → local-browser-VAZORCO3.js.map} +0 -0
  58. /package/dist/{src-I4EXB5OD.js.map → src-4VIDSK4A.js.map} +0 -0
@@ -1,8 +1,9 @@
1
1
  import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
2
2
  import {
3
3
  apiRequest,
4
- fetchList
5
- } from "./chunk-6WWHXWCS.js";
4
+ downloadStorageState,
5
+ selectCredential
6
+ } from "./chunk-ERSNYLMZ.js";
6
7
  import {
7
8
  getArgValue,
8
9
  hasFlag,
@@ -16,7 +17,6 @@ import "./chunk-VKVL7WBN.js";
16
17
  // src/record.ts
17
18
  import fs from "fs/promises";
18
19
  import path from "path";
19
- import readline from "readline";
20
20
  import process from "process";
21
21
 
22
22
  // src/record-interaction-script.ts
@@ -29,6 +29,37 @@ var INTERACTION_CAPTURE_SCRIPT = `
29
29
  window.__canaryRecordedEvents.push(event);
30
30
  }
31
31
 
32
+ // --- Input debouncing ---
33
+ // Accumulate input per element, flush after 500ms idle as a single event
34
+ var inputTimers = new Map();
35
+ var inputState = new Map();
36
+
37
+ function getElementKey(el) {
38
+ return (el.id || '') + '|' + (el.getAttribute('aria-label') || '') + '|' + (el.tagName || '') + '|' + (el.getAttribute('name') || '');
39
+ }
40
+
41
+ function flushInput(key) {
42
+ var state = inputState.get(key);
43
+ if (!state) return;
44
+ inputTimers.delete(key);
45
+ inputState.delete(key);
46
+ push({
47
+ type: 'input',
48
+ ts: state.ts,
49
+ tagName: state.tagName,
50
+ id: state.id || undefined,
51
+ inputType: state.inputType || undefined,
52
+ ariaLabel: state.ariaLabel || undefined,
53
+ value: state.value,
54
+ isFinal: true,
55
+ });
56
+ }
57
+
58
+ function flushAllInputs() {
59
+ inputTimers.forEach(function(timer) { clearTimeout(timer); });
60
+ inputState.forEach(function(_, key) { flushInput(key); });
61
+ }
62
+
32
63
  document.addEventListener('click', function(e) {
33
64
  var el = e.target;
34
65
  push({
@@ -49,8 +80,14 @@ var INTERACTION_CAPTURE_SCRIPT = `
49
80
  document.addEventListener('input', function(e) {
50
81
  var el = e.target;
51
82
  var isPassword = el.type === 'password';
52
- push({
53
- type: 'input',
83
+ var key = getElementKey(el);
84
+
85
+ // Clear previous timer for this element
86
+ var prevTimer = inputTimers.get(key);
87
+ if (prevTimer) clearTimeout(prevTimer);
88
+
89
+ // Update accumulated state
90
+ inputState.set(key, {
54
91
  ts: Date.now(),
55
92
  tagName: el.tagName || '',
56
93
  id: el.id || undefined,
@@ -58,6 +95,38 @@ var INTERACTION_CAPTURE_SCRIPT = `
58
95
  ariaLabel: el.getAttribute('aria-label') || undefined,
59
96
  value: isPassword ? '***' : (el.value || ''),
60
97
  });
98
+
99
+ // Schedule flush after 500ms idle
100
+ inputTimers.set(key, setTimeout(function() { flushInput(key); }, 500));
101
+ }, true);
102
+
103
+ // --- Change events for selects, checkboxes, radios ---
104
+ document.addEventListener('change', function(e) {
105
+ var el = e.target;
106
+ var tagName = el.tagName || '';
107
+ var inputType = (el.type || '').toLowerCase();
108
+
109
+ // Only capture selects, checkboxes, and radios (text inputs are handled by input debouncing)
110
+ var isSelect = tagName === 'SELECT';
111
+ var isCheckOrRadio = inputType === 'checkbox' || inputType === 'radio';
112
+ if (!isSelect && !isCheckOrRadio) return;
113
+
114
+ var value;
115
+ if (isCheckOrRadio) {
116
+ value = el.checked ? 'checked' : 'unchecked';
117
+ } else {
118
+ value = el.value || '';
119
+ }
120
+
121
+ push({
122
+ type: 'change',
123
+ ts: Date.now(),
124
+ tagName: tagName,
125
+ id: el.id || undefined,
126
+ inputType: inputType || undefined,
127
+ ariaLabel: el.getAttribute('aria-label') || undefined,
128
+ value: value,
129
+ });
61
130
  }, true);
62
131
 
63
132
  document.addEventListener('keydown', function(e) {
@@ -70,43 +139,52 @@ var INTERACTION_CAPTURE_SCRIPT = `
70
139
  }
71
140
  }, true);
72
141
 
73
- window.addEventListener('beforeunload', function() {
142
+ // --- SPA navigation: patch pushState/replaceState ---
143
+ var origPushState = history.pushState;
144
+ var origReplaceState = history.replaceState;
145
+
146
+ history.pushState = function() {
147
+ origPushState.apply(this, arguments);
74
148
  push({
75
149
  type: 'navigation',
76
150
  ts: Date.now(),
77
151
  url: window.location.href,
152
+ navigationType: 'pushState',
78
153
  });
79
- });
154
+ };
155
+
156
+ history.replaceState = function() {
157
+ origReplaceState.apply(this, arguments);
158
+ push({
159
+ type: 'navigation',
160
+ ts: Date.now(),
161
+ url: window.location.href,
162
+ navigationType: 'replaceState',
163
+ });
164
+ };
80
165
 
81
166
  window.addEventListener('popstate', function() {
82
167
  push({
83
168
  type: 'navigation',
84
169
  ts: Date.now(),
85
170
  url: window.location.href,
171
+ navigationType: 'popstate',
172
+ });
173
+ });
174
+
175
+ window.addEventListener('beforeunload', function() {
176
+ flushAllInputs();
177
+ push({
178
+ type: 'navigation',
179
+ ts: Date.now(),
180
+ url: window.location.href,
181
+ navigationType: 'beforeunload',
86
182
  });
87
183
  });
88
184
  })();
89
185
  `;
90
186
 
91
187
  // src/record.ts
92
- async function promptSelection(items, prompt) {
93
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
94
- return new Promise((resolve) => {
95
- for (let i = 0; i < items.length; i++) {
96
- console.log(` ${i + 1}. ${items[i]}`);
97
- }
98
- rl.question(`
99
- ${prompt} `, (answer) => {
100
- rl.close();
101
- const idx = parseInt(answer, 10) - 1;
102
- if (isNaN(idx) || idx < 0 || idx >= items.length) {
103
- console.error("Invalid selection.");
104
- process.exit(1);
105
- }
106
- resolve(idx);
107
- });
108
- });
109
- }
110
188
  async function runRecord(argv) {
111
189
  if (hasFlag(argv, "--help", "-h")) {
112
190
  console.log(
@@ -133,49 +211,22 @@ async function runRecord(argv) {
133
211
  const outputDir = getArgValue(argv, "--output");
134
212
  const skipUpload = hasFlag(argv, "--no-upload");
135
213
  console.log("Fetching credentials...");
136
- const credentials = await fetchList(
137
- config.apiUrl,
138
- config.token,
139
- "/org/credentials",
140
- "items"
141
- );
142
- if (credentials.length === 0) {
143
- console.error("No credentials found. Create one first in the web app.");
214
+ const credential = await selectCredential(config.apiUrl, config.token, credentialArg);
215
+ if (!credential) {
144
216
  process.exit(1);
145
217
  }
146
- let credential;
147
- if (credentialArg) {
148
- const found = credentials.find((c) => c.id === credentialArg);
149
- if (!found) {
150
- console.error(`Credential not found: ${credentialArg}`);
151
- process.exit(1);
152
- }
153
- credential = found;
154
- } else {
155
- console.log("\nSelect a credential:\n");
156
- const labels = credentials.map(
157
- (c) => `${c.name} (${c.propertyName ?? c.propertyId})`
158
- );
159
- const idx = await promptSelection(labels, "Enter number:");
160
- credential = credentials[idx];
161
- }
162
218
  console.log(`Using credential: ${credential.name}`);
163
219
  const propertyId = credential.propertyId;
164
220
  let storageStatePath;
165
221
  if (credential.storageStateS3Key) {
166
222
  console.log("Downloading storage state...");
167
- const tmpFile = path.join(getCanaryTmpDir(), `canary-ss-${Date.now()}.json`);
168
- const res = await fetch(
169
- `${config.apiUrl}/org/properties/${propertyId}/credentials/${credential.id}/storage-state/download`,
170
- {
171
- headers: { Authorization: `Bearer ${config.token}` },
172
- redirect: "follow"
173
- }
174
- );
175
- if (res.ok) {
176
- const body = await res.text();
177
- await fs.writeFile(tmpFile, body, "utf-8");
178
- storageStatePath = tmpFile;
223
+ storageStatePath = await downloadStorageState({
224
+ apiUrl: config.apiUrl,
225
+ token: config.token,
226
+ propertyId: credential.propertyId,
227
+ credentialId: credential.id
228
+ });
229
+ if (storageStatePath) {
179
230
  console.log("Storage state loaded.");
180
231
  } else {
181
232
  console.warn("Could not download storage state, continuing without it.");
@@ -196,7 +247,7 @@ async function runRecord(argv) {
196
247
  console.warn("No start URL provided and no login URL on credential. Browser will open to about:blank.");
197
248
  }
198
249
  console.log("Launching browser...");
199
- const { PlaywrightClient, consoleLogger, captureElementAtPoint } = await import("./src-I4EXB5OD.js");
250
+ const { PlaywrightClient, consoleLogger, captureElementAtPoint } = await import("./src-4VIDSK4A.js");
200
251
  const videoDir = path.join(getCanaryTmpDir(), `canary-record-video-${Date.now()}`);
201
252
  await fs.mkdir(videoDir, { recursive: true });
202
253
  const client = new PlaywrightClient({ logger: consoleLogger });
@@ -222,7 +273,20 @@ async function runRecord(argv) {
222
273
  const startedAt = /* @__PURE__ */ new Date();
223
274
  const events = [];
224
275
  let running = true;
225
- let snapshotCounter = 0;
276
+ const currentUrl = page.url();
277
+ const initialUrlEvent = {
278
+ type: "initial-url",
279
+ ts: Date.now(),
280
+ url: currentUrl
281
+ };
282
+ try {
283
+ const snapshot = await page._snapshotForAI({ mode: "full" });
284
+ initialUrlEvent.snapshot = snapshot;
285
+ } catch {
286
+ }
287
+ events.push(initialUrlEvent);
288
+ process.stdout.write(` [${events.length}] initial-url \u2192 ${currentUrl}
289
+ `);
226
290
  console.log("\nRecording started. Interact with the browser.");
227
291
  console.log("Press Ctrl+C to stop recording.\n");
228
292
  const pollInterval = setInterval(async () => {
@@ -244,13 +308,10 @@ async function runRecord(argv) {
244
308
  }
245
309
  } catch {
246
310
  }
247
- snapshotCounter++;
248
- if (snapshotCounter % 3 === 1) {
249
- try {
250
- const snapshot = await page._snapshotForAI({ mode: "full" });
251
- clickEvt.snapshot = snapshot;
252
- } catch {
253
- }
311
+ try {
312
+ const snapshot = await page._snapshotForAI({ mode: "full" });
313
+ clickEvt.snapshot = snapshot;
314
+ } catch {
254
315
  }
255
316
  }
256
317
  if (evt.type === "navigation") {
@@ -261,7 +322,7 @@ async function runRecord(argv) {
261
322
  }
262
323
  }
263
324
  events.push(evt);
264
- const label = evt.type === "click" ? `click (${evt.tagName})` : evt.type === "input" ? `input` : evt.type === "navigation" ? `navigation \u2192 ${evt.url}` : `keydown: ${evt.key}`;
325
+ const label = evt.type === "click" ? `click (${evt.tagName})` : evt.type === "input" ? `input` : evt.type === "change" ? `change (${evt.tagName})` : evt.type === "navigation" ? `navigation \u2192 ${evt.url}` : evt.type === "initial-url" ? `initial-url \u2192 ${evt.url}` : `keydown: ${evt.key}`;
265
326
  process.stdout.write(` [${events.length}] ${label}
266
327
  `);
267
328
  }
@@ -333,4 +394,4 @@ Output directory: ${outDir}`);
333
394
  export {
334
395
  runRecord
335
396
  };
336
- //# sourceMappingURL=record-4OX7HXWQ.js.map
397
+ //# sourceMappingURL=record-V6QKFFH3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/record.ts","../src/record-interaction-script.ts"],"sourcesContent":["/**\n * `canary record` — Record browser interactions for flow creation.\n *\n * Launches a headed browser, injects an in-page capture script,\n * and records user interactions (clicks, inputs, navigation) enriched\n * with accessibility snapshots and element info. On stop, bundles\n * everything as JSONL + video and uploads to the API.\n *\n * @module record\n */\n\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { getCanaryTmpDir } from \"@chatsdet/tmp\";\nimport process from \"node:process\";\nimport { resolveConfig, getArgValue, hasFlag } from \"./auth.js\";\nimport { apiRequest, selectCredential, downloadStorageState } from \"./cli-helpers.js\";\nimport type { CredentialListItem } from \"./cli-helpers.js\";\nimport { INTERACTION_CAPTURE_SCRIPT } from \"./record-interaction-script.js\";\n\n/** Minimal recording event types (mirrors @chatsdet/types/recording) */\ninterface RecordingClickEvent {\n type: \"click\";\n ts: number;\n x: number;\n y: number;\n tagName: string;\n id?: string;\n testId?: string;\n ariaLabel?: string;\n role?: string;\n textContent?: string;\n className?: string;\n elementInfo?: Record<string, unknown>;\n snapshot?: string;\n}\n\ntype RecordingEvent =\n | RecordingClickEvent\n | { type: \"input\"; ts: number; tagName: string; id?: string; inputType?: string; ariaLabel?: string; value: string; isFinal?: boolean }\n | { type: \"change\"; ts: number; tagName: string; id?: string; inputType?: string; ariaLabel?: string; value: string }\n | { type: \"keydown\"; ts: number; key: string }\n | { type: \"navigation\"; ts: number; url: string; snapshot?: string; navigationType?: string }\n | { type: \"initial-url\"; ts: number; url: string; snapshot?: string };\n\ninterface CredentialDetail {\n id: string;\n name: string;\n loginUrl?: string;\n storageStateS3Key?: string | null;\n}\n\ninterface Property {\n id: string;\n name: string;\n baseUrl?: string;\n}\n\nexport async function runRecord(argv: string[]): Promise<void> {\n if (hasFlag(argv, \"--help\", \"-h\")) {\n console.log(\n [\n \"Usage: canary record [options]\",\n \"\",\n \"Record browser interactions for flow creation.\",\n \"\",\n \"Options:\",\n \" --credential <id> Credential ID (skip interactive selection)\",\n \" --url <startUrl> URL to navigate to after launch\",\n \" --output <dir> Local output directory (default: temp dir)\",\n \" --env <env> Environment (local, dev, prod)\",\n \" --no-upload Save locally only, skip API upload\",\n \"\",\n \"Press Ctrl+C to stop recording.\",\n ].join(\"\\n\")\n );\n return;\n }\n\n const config = await resolveConfig(argv);\n const credentialArg = getArgValue(argv, \"--credential\");\n const startUrl = getArgValue(argv, \"--url\");\n const outputDir = getArgValue(argv, \"--output\");\n const skipUpload = hasFlag(argv, \"--no-upload\");\n\n // 1. Fetch and select credential\n console.log(\"Fetching credentials...\");\n const credential = await selectCredential(config.apiUrl, config.token, credentialArg);\n if (!credential) {\n process.exit(1);\n }\n\n console.log(`Using credential: ${credential.name}`);\n\n const propertyId = credential.propertyId;\n\n // 3. Download storage state if available\n let storageStatePath: string | undefined;\n if (credential.storageStateS3Key) {\n console.log(\"Downloading storage state...\");\n storageStatePath = await downloadStorageState({\n apiUrl: config.apiUrl,\n token: config.token,\n propertyId: credential.propertyId,\n credentialId: credential.id,\n });\n if (storageStatePath) {\n console.log(\"Storage state loaded.\");\n } else {\n console.warn(\"Could not download storage state, continuing without it.\");\n }\n }\n\n // 4. Get credential detail for loginUrl\n let loginUrl = credential.loginUrl;\n if (!loginUrl) {\n const detail = await apiRequest<{ ok: boolean; credential?: CredentialDetail }>(\n config.apiUrl,\n config.token,\n \"GET\",\n `/org/properties/${propertyId}/credentials/${credential.id}`\n );\n loginUrl = detail.credential?.loginUrl ?? undefined;\n }\n\n // 5. Determine start URL\n const navigateUrl = startUrl ?? loginUrl;\n if (!navigateUrl) {\n console.warn(\"No start URL provided and no login URL on credential. Browser will open to about:blank.\");\n }\n\n // 6. Launch browser\n console.log(\"Launching browser...\");\n\n // Lazy-load playwright-dependent modules\n const { PlaywrightClient, consoleLogger, captureElementAtPoint } = await import(\"@chatsdet/browser-core\");\n\n const videoDir = path.join(getCanaryTmpDir(), `canary-record-video-${Date.now()}`);\n await fs.mkdir(videoDir, { recursive: true });\n\n const client = new PlaywrightClient({ logger: consoleLogger });\n await client.connect({\n browserMode: \"headed\",\n storageStatePath,\n recordVideo: { dir: videoDir },\n });\n\n const page = await client.getPageForReplay();\n if (!page) {\n console.error(\"Failed to get browser page.\");\n await client.disconnect();\n process.exit(1);\n }\n\n // 7. Inject interaction capture script\n await page.addInitScript(INTERACTION_CAPTURE_SCRIPT);\n await page.evaluate(INTERACTION_CAPTURE_SCRIPT);\n\n // 8. Navigate\n if (navigateUrl) {\n console.log(`Navigating to ${navigateUrl}`);\n await page.goto(navigateUrl, { waitUntil: \"domcontentloaded\", timeout: 30_000 }).catch(() => {\n console.warn(\"Navigation timed out or failed, continuing anyway.\");\n });\n }\n\n const startedAt = new Date();\n const events: RecordingEvent[] = [];\n let running = true;\n\n // Emit initial-url event so we know the starting page\n const currentUrl = page.url();\n const initialUrlEvent: RecordingEvent = {\n type: \"initial-url\",\n ts: Date.now(),\n url: currentUrl,\n };\n try {\n const snapshot = await (page as unknown as { _snapshotForAI(opts: { mode: string }): Promise<string> })._snapshotForAI({ mode: \"full\" });\n (initialUrlEvent as { snapshot?: string }).snapshot = snapshot;\n } catch {\n // Snapshot may fail\n }\n events.push(initialUrlEvent);\n process.stdout.write(` [${events.length}] initial-url → ${currentUrl}\\n`);\n\n console.log(\"\\nRecording started. Interact with the browser.\");\n console.log(\"Press Ctrl+C to stop recording.\\n\");\n\n // 9. Poll loop — collect events from the page\n const pollInterval = setInterval(async () => {\n if (!running) return;\n try {\n const raw = await page.evaluate(() => {\n const evts = (window as unknown as { __canaryRecordedEvents?: unknown[] }).__canaryRecordedEvents ?? [];\n (window as unknown as { __canaryRecordedEvents: unknown[] }).__canaryRecordedEvents = [];\n return evts;\n });\n\n if (!Array.isArray(raw) || raw.length === 0) return;\n\n for (const evt of raw as RecordingEvent[]) {\n // Enrich click events with element info and periodic snapshots\n if (evt.type === \"click\") {\n const clickEvt = evt as RecordingClickEvent;\n try {\n const info = await captureElementAtPoint(page, clickEvt.x, clickEvt.y);\n if (info) {\n clickEvt.elementInfo = info as unknown as Record<string, unknown>;\n }\n } catch {\n // Element may have disappeared\n }\n\n // Snapshot every click for full page context\n try {\n const snapshot = await (page as unknown as { _snapshotForAI(opts: { mode: string }): Promise<string> })._snapshotForAI({ mode: \"full\" });\n clickEvt.snapshot = snapshot;\n } catch {\n // Snapshot may fail during navigation\n }\n }\n\n // Enrich navigation events with snapshot\n if (evt.type === \"navigation\") {\n try {\n const snapshot = await (page as unknown as { _snapshotForAI(opts: { mode: string }): Promise<string> })._snapshotForAI({ mode: \"full\" });\n (evt as { snapshot?: string }).snapshot = snapshot;\n } catch {\n // Snapshot may fail during navigation\n }\n }\n\n events.push(evt);\n const label =\n evt.type === \"click\"\n ? `click (${(evt as RecordingClickEvent).tagName})`\n : evt.type === \"input\"\n ? `input`\n : evt.type === \"change\"\n ? `change (${(evt as { tagName: string }).tagName})`\n : evt.type === \"navigation\"\n ? `navigation → ${(evt as { url: string }).url}`\n : evt.type === \"initial-url\"\n ? `initial-url → ${(evt as { url: string }).url}`\n : `keydown: ${(evt as { key: string }).key}`;\n process.stdout.write(` [${events.length}] ${label}\\n`);\n }\n } catch {\n // Page may have been closed\n }\n }, 500);\n\n // 10. Handle Ctrl+C\n const cleanup = async () => {\n if (!running) return;\n running = false;\n clearInterval(pollInterval);\n\n const endedAt = new Date();\n console.log(`\\nRecording stopped. ${events.length} events captured.`);\n\n // Save video\n console.log(\"Saving video...\");\n const videoResult = await client.saveVideo();\n await client.disconnect();\n\n // Clean up temp storage state\n if (storageStatePath) {\n await fs.unlink(storageStatePath).catch(() => {});\n }\n\n // Prepare output directory\n const outDir = outputDir ?? path.join(getCanaryTmpDir(), `canary-recording-${Date.now()}`);\n await fs.mkdir(outDir, { recursive: true });\n\n // Write events JSONL\n const eventsPath = path.join(outDir, \"events.jsonl\");\n const lines = events.map((e) => JSON.stringify(e)).join(\"\\n\");\n await fs.writeFile(eventsPath, lines, \"utf-8\");\n console.log(`Events saved: ${eventsPath}`);\n\n // Copy video if available\n let videoPath: string | undefined;\n if (videoResult?.videoPath) {\n videoPath = path.join(outDir, \"video.webm\");\n await fs.copyFile(videoResult.videoPath, videoPath);\n console.log(`Video saved: ${videoPath}`);\n }\n\n // Upload to API\n if (!skipUpload && events.length > 0) {\n console.log(\"Uploading recording...\");\n try {\n const formData = new FormData();\n formData.append(\"propertyId\", propertyId);\n formData.append(\"credentialId\", credential.id);\n\n const eventsBlob = new Blob([lines], { type: \"application/x-ndjson\" });\n formData.append(\"events\", eventsBlob, \"events.jsonl\");\n\n if (videoPath) {\n const videoBuffer = await fs.readFile(videoPath);\n const videoBlob = new Blob([videoBuffer], { type: \"video/webm\" });\n formData.append(\"video\", videoBlob, \"video.webm\");\n }\n\n const res = await fetch(`${config.apiUrl}/org/recordings/upload`, {\n method: \"POST\",\n headers: { Authorization: `Bearer ${config.token}` },\n body: formData,\n });\n\n const json = (await res.json()) as {\n ok: boolean;\n recordingId?: string;\n error?: string;\n };\n\n if (json.ok) {\n console.log(`Recording uploaded. ID: ${json.recordingId}`);\n } else {\n console.error(`Upload failed: ${json.error ?? \"Unknown error\"}`);\n }\n } catch (err) {\n console.error(\"Upload failed:\", err instanceof Error ? err.message : err);\n }\n }\n\n console.log(`\\nOutput directory: ${outDir}`);\n process.exit(0);\n };\n\n process.on(\"SIGINT\", () => void cleanup());\n process.on(\"SIGTERM\", () => void cleanup());\n\n // Also stop if the page is closed\n page.on(\"close\", () => void cleanup());\n}\n","/**\n * In-page interaction capture script injected via addInitScript.\n * Captures user interactions to window.__canaryRecordedEvents.\n *\n * @module record-interaction-script\n */\n\nexport const INTERACTION_CAPTURE_SCRIPT = `\n(function() {\n if (window.__canaryRecordedEvents) return;\n window.__canaryRecordedEvents = [];\n\n function push(event) {\n window.__canaryRecordedEvents.push(event);\n }\n\n // --- Input debouncing ---\n // Accumulate input per element, flush after 500ms idle as a single event\n var inputTimers = new Map();\n var inputState = new Map();\n\n function getElementKey(el) {\n return (el.id || '') + '|' + (el.getAttribute('aria-label') || '') + '|' + (el.tagName || '') + '|' + (el.getAttribute('name') || '');\n }\n\n function flushInput(key) {\n var state = inputState.get(key);\n if (!state) return;\n inputTimers.delete(key);\n inputState.delete(key);\n push({\n type: 'input',\n ts: state.ts,\n tagName: state.tagName,\n id: state.id || undefined,\n inputType: state.inputType || undefined,\n ariaLabel: state.ariaLabel || undefined,\n value: state.value,\n isFinal: true,\n });\n }\n\n function flushAllInputs() {\n inputTimers.forEach(function(timer) { clearTimeout(timer); });\n inputState.forEach(function(_, key) { flushInput(key); });\n }\n\n document.addEventListener('click', function(e) {\n var el = e.target;\n push({\n type: 'click',\n ts: Date.now(),\n x: e.clientX,\n y: e.clientY,\n tagName: el.tagName || '',\n id: el.id || undefined,\n testId: el.getAttribute('data-testid') || undefined,\n ariaLabel: el.getAttribute('aria-label') || undefined,\n role: el.getAttribute('role') || undefined,\n textContent: (el.textContent || '').slice(0, 200).trim() || undefined,\n className: el.className && typeof el.className === 'string' ? el.className.slice(0, 200) : undefined,\n });\n }, true);\n\n document.addEventListener('input', function(e) {\n var el = e.target;\n var isPassword = el.type === 'password';\n var key = getElementKey(el);\n\n // Clear previous timer for this element\n var prevTimer = inputTimers.get(key);\n if (prevTimer) clearTimeout(prevTimer);\n\n // Update accumulated state\n inputState.set(key, {\n ts: Date.now(),\n tagName: el.tagName || '',\n id: el.id || undefined,\n inputType: el.type || undefined,\n ariaLabel: el.getAttribute('aria-label') || undefined,\n value: isPassword ? '***' : (el.value || ''),\n });\n\n // Schedule flush after 500ms idle\n inputTimers.set(key, setTimeout(function() { flushInput(key); }, 500));\n }, true);\n\n // --- Change events for selects, checkboxes, radios ---\n document.addEventListener('change', function(e) {\n var el = e.target;\n var tagName = el.tagName || '';\n var inputType = (el.type || '').toLowerCase();\n\n // Only capture selects, checkboxes, and radios (text inputs are handled by input debouncing)\n var isSelect = tagName === 'SELECT';\n var isCheckOrRadio = inputType === 'checkbox' || inputType === 'radio';\n if (!isSelect && !isCheckOrRadio) return;\n\n var value;\n if (isCheckOrRadio) {\n value = el.checked ? 'checked' : 'unchecked';\n } else {\n value = el.value || '';\n }\n\n push({\n type: 'change',\n ts: Date.now(),\n tagName: tagName,\n id: el.id || undefined,\n inputType: inputType || undefined,\n ariaLabel: el.getAttribute('aria-label') || undefined,\n value: value,\n });\n }, true);\n\n document.addEventListener('keydown', function(e) {\n if (e.key === 'Enter' || e.key === 'Tab' || e.key === 'Escape') {\n push({\n type: 'keydown',\n ts: Date.now(),\n key: e.key,\n });\n }\n }, true);\n\n // --- SPA navigation: patch pushState/replaceState ---\n var origPushState = history.pushState;\n var origReplaceState = history.replaceState;\n\n history.pushState = function() {\n origPushState.apply(this, arguments);\n push({\n type: 'navigation',\n ts: Date.now(),\n url: window.location.href,\n navigationType: 'pushState',\n });\n };\n\n history.replaceState = function() {\n origReplaceState.apply(this, arguments);\n push({\n type: 'navigation',\n ts: Date.now(),\n url: window.location.href,\n navigationType: 'replaceState',\n });\n };\n\n window.addEventListener('popstate', function() {\n push({\n type: 'navigation',\n ts: Date.now(),\n url: window.location.href,\n navigationType: 'popstate',\n });\n });\n\n window.addEventListener('beforeunload', function() {\n flushAllInputs();\n push({\n type: 'navigation',\n ts: Date.now(),\n url: window.location.href,\n navigationType: 'beforeunload',\n });\n });\n})();\n`;\n"],"mappings":"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","names":[]}
@@ -154,12 +154,16 @@ async function handleRun(argv, apiUrl, token) {
154
154
  console.log(` Testers: ${run.testersCompleted}/${run.testersSpawned} completed, ${run.testersFailed} failed`);
155
155
  console.log(` Regressions: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed, ${run.regressionTestsFailed} failed`);
156
156
  console.log(` Issues found: ${run.issuesFound}`);
157
- if (run.status === "completed") {
158
- console.log("\nRelease QA PASSED");
157
+ if (run.status === "failed" || run.status === "canceled" || run.status === "timeout") {
158
+ console.error(`
159
+ Release QA FAILED (${run.status})`);
160
+ process.exit(1);
161
+ } else if (run.regressionTestsFailed === 0) {
162
+ console.log("\nRelease QA PASSED (0 regression failures)");
159
163
  process.exit(0);
160
164
  } else {
161
165
  console.error(`
162
- Release QA FAILED (${run.status})`);
166
+ Release QA FAILED (${run.regressionTestsFailed} regression test(s) failed)`);
163
167
  process.exit(1);
164
168
  }
165
169
  }
@@ -217,4 +221,4 @@ async function runRelease(argv) {
217
221
  export {
218
222
  runRelease
219
223
  };
220
- //# sourceMappingURL=release-L4IXOHDF.js.map
224
+ //# sourceMappingURL=release-7TI7EIGD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/release.ts"],"sourcesContent":["/**\n * CLI Release QA Management\n *\n * Trigger, poll, and check status of Release QA runs from CI/CD pipelines.\n * Used by the scheduled-release GitHub Actions workflow to gate deployments.\n */\n\nimport process from \"node:process\";\nimport { resolveConfig, getArgValue, hasFlag } from \"./auth.js\";\n\ntype TriggerResponse = {\n ok: boolean;\n releaseRunId?: string;\n jobId?: string;\n error?: string;\n};\n\ntype RunStatusResponse = {\n ok: boolean;\n run?: {\n id: string;\n status: string;\n triggerSource: string;\n cutoffReason: string | null;\n fromSha: string | null;\n toSha: string | null;\n commitsAnalyzed: number;\n testersSpawned: number;\n testersCompleted: number;\n testersFailed: number;\n issuesFound: number;\n regressionTestsTotal: number;\n regressionTestsPassed: number;\n regressionTestsFailed: number;\n startedAt: string | null;\n finishedAt: string | null;\n createdAt: string;\n };\n error?: string;\n};\n\n/** Terminal statuses that stop polling */\nconst TERMINAL_STATUSES = new Set([\n \"completed\",\n \"completed_with_errors\",\n \"failed\",\n \"canceled\",\n \"timeout\",\n]);\n\nasync function handleTrigger(argv: string[], apiUrl: string, token: string): Promise<void> {\n const propertyId = getArgValue(argv, \"--property-id\");\n if (!propertyId) {\n console.error(\"Error: Missing --property-id <uuid>.\");\n console.error(\"Usage: canary release trigger --property-id <uuid> [--credential-ids <uuid,...>]\");\n process.exit(1);\n }\n\n const credentialIdsRaw = getArgValue(argv, \"--credential-ids\");\n const body: Record<string, unknown> = { propertyId };\n if (credentialIdsRaw) {\n body.credentialIds = credentialIdsRaw.split(\",\").map((id) => id.trim());\n }\n\n const res = await fetch(`${apiUrl}/api/v1/release-qa/trigger`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Check your API token.\");\n process.exit(1);\n }\n\n const json = (await res.json()) as TriggerResponse;\n\n if (!json.ok) {\n console.error(`Error: ${json.error}`);\n process.exit(1);\n }\n\n console.log(`Release QA run triggered`);\n console.log(` Run ID: ${json.releaseRunId}`);\n console.log(` Job ID: ${json.jobId}`);\n}\n\nasync function fetchRunStatus(\n apiUrl: string,\n token: string,\n runId: string\n): Promise<RunStatusResponse> {\n const res = await fetch(`${apiUrl}/api/v1/release-qa/runs/${encodeURIComponent(runId)}`, {\n headers: { Authorization: `Bearer ${token}` },\n });\n\n if (res.status === 401) {\n console.error(\"Error: Unauthorized. Check your API token.\");\n process.exit(1);\n }\n\n return (await res.json()) as RunStatusResponse;\n}\n\nasync function handleStatus(argv: string[], apiUrl: string, token: string): Promise<void> {\n const runId = argv[0];\n if (!runId || runId.startsWith(\"--\")) {\n console.error(\"Error: Missing run ID.\");\n console.error(\"Usage: canary release status <run-id>\");\n process.exit(1);\n }\n\n const jsonOutput = hasFlag(argv, \"--json\");\n const json = await fetchRunStatus(apiUrl, token, runId);\n\n if (!json.ok) {\n console.error(`Error: ${json.error}`);\n process.exit(1);\n }\n\n const run = json.run!;\n\n if (jsonOutput) {\n console.log(JSON.stringify(run, null, 2));\n return;\n }\n\n console.log(`Release QA Run: ${run.id}`);\n console.log(` Status: ${run.status}`);\n console.log(` Trigger: ${run.triggerSource}`);\n console.log(` Commits analyzed: ${run.commitsAnalyzed}`);\n console.log(` Testers: ${run.testersCompleted}/${run.testersSpawned} completed, ${run.testersFailed} failed`);\n console.log(` Issues found: ${run.issuesFound}`);\n console.log(` Regression tests: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed, ${run.regressionTestsFailed} failed`);\n if (run.startedAt) console.log(` Started: ${run.startedAt}`);\n if (run.finishedAt) console.log(` Finished: ${run.finishedAt}`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\nasync function handleRun(argv: string[], apiUrl: string, token: string): Promise<void> {\n const propertyId = getArgValue(argv, \"--property-id\");\n if (!propertyId) {\n console.error(\"Error: Missing --property-id <uuid>.\");\n console.error(\n \"Usage: canary release run --property-id <uuid> [--timeout 3600] [--poll-interval 30]\"\n );\n process.exit(1);\n }\n\n const timeoutSec = parseInt(getArgValue(argv, \"--timeout\") ?? \"3600\", 10);\n const pollIntervalSec = parseInt(getArgValue(argv, \"--poll-interval\") ?? \"30\", 10);\n\n const credentialIdsRaw = getArgValue(argv, \"--credential-ids\");\n const body: Record<string, unknown> = { propertyId };\n if (credentialIdsRaw) {\n body.credentialIds = credentialIdsRaw.split(\",\").map((id) => id.trim());\n }\n\n // Trigger\n console.log(\"Triggering Release QA run...\");\n const triggerRes = await fetch(`${apiUrl}/api/v1/release-qa/trigger`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${token}`,\n \"Content-Type\": \"application/json\",\n },\n body: JSON.stringify(body),\n });\n\n if (triggerRes.status === 401) {\n console.error(\"Error: Unauthorized. Check your API token.\");\n process.exit(1);\n }\n\n const triggerJson = (await triggerRes.json()) as TriggerResponse;\n\n if (!triggerJson.ok) {\n console.error(`Error triggering run: ${triggerJson.error}`);\n process.exit(1);\n }\n\n const runId = triggerJson.releaseRunId!;\n console.log(`Run started: ${runId}`);\n\n // Poll\n const deadline = Date.now() + timeoutSec * 1000;\n let lastStatus = \"\";\n\n while (Date.now() < deadline) {\n await sleep(pollIntervalSec * 1000);\n\n let statusJson: RunStatusResponse;\n try {\n statusJson = await fetchRunStatus(apiUrl, token, runId);\n } catch (err) {\n console.error(`Warning: Failed to fetch status, retrying... (${err})`);\n continue;\n }\n\n if (!statusJson.ok) {\n console.error(`Error: ${statusJson.error}`);\n process.exit(1);\n }\n\n const run = statusJson.run!;\n const statusLine = `[${run.status}] testers: ${run.testersCompleted}/${run.testersSpawned}, regressions: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed`;\n\n if (run.status !== lastStatus) {\n console.log(statusLine);\n lastStatus = run.status;\n } else {\n // Print progress on same status change in metrics\n console.log(statusLine);\n }\n\n if (TERMINAL_STATUSES.has(run.status)) {\n console.log(\"\");\n console.log(`Run finished: ${run.status}`);\n console.log(` Testers: ${run.testersCompleted}/${run.testersSpawned} completed, ${run.testersFailed} failed`);\n console.log(` Regressions: ${run.regressionTestsPassed}/${run.regressionTestsTotal} passed, ${run.regressionTestsFailed} failed`);\n console.log(` Issues found: ${run.issuesFound}`);\n\n // Gate deployment solely on regression test failures.\n // Other issues (failed testers, build errors, cutoffs) are visible in the\n // UI but should not block deploys when all regressions pass.\n if (run.status === \"failed\" || run.status === \"canceled\" || run.status === \"timeout\") {\n // Run didn't complete normally — no reliable regression data\n console.error(`\\nRelease QA FAILED (${run.status})`);\n process.exit(1);\n } else if (run.regressionTestsFailed === 0) {\n console.log(\"\\nRelease QA PASSED (0 regression failures)\");\n process.exit(0);\n } else {\n console.error(`\\nRelease QA FAILED (${run.regressionTestsFailed} regression test(s) failed)`);\n process.exit(1);\n }\n }\n }\n\n console.error(`\\nTimeout: Release QA did not complete within ${timeoutSec}s`);\n process.exit(1);\n}\n\nfunction printReleaseHelp(): void {\n console.log(\n [\n \"Usage: canary release <sub-command> [options]\",\n \"\",\n \"Sub-commands:\",\n \" trigger --property-id <uuid> [--credential-ids <uuid,...>]\",\n \" Trigger a Release QA run\",\n \" status <run-id> [--json] Check run status\",\n \" run --property-id <uuid> [options] Trigger and poll until complete\",\n \"\",\n \"Run options:\",\n \" --timeout <seconds> Max wait time (default: 3600)\",\n \" --poll-interval <secs> Poll frequency (default: 30)\",\n \" --credential-ids <ids> Comma-separated credential UUIDs\",\n \"\",\n \"Options:\",\n \" --env <env> Target environment (prod, dev, local)\",\n \" --api-url <url> API URL override\",\n \" --token <key> API token override (or set CANARY_API_TOKEN)\",\n ].join(\"\\n\")\n );\n}\n\nexport async function runRelease(argv: string[]): Promise<void> {\n const [subCommand, ...rest] = argv;\n\n if (!subCommand || subCommand === \"help\" || hasFlag(argv, \"--help\", \"-h\")) {\n printReleaseHelp();\n return;\n }\n\n const { apiUrl, token } = await resolveConfig(argv);\n\n switch (subCommand) {\n case \"trigger\":\n await handleTrigger(rest, apiUrl, token);\n break;\n case \"status\":\n await handleStatus(rest, apiUrl, token);\n break;\n case \"run\":\n await handleRun(rest, apiUrl, token);\n break;\n default:\n console.error(`Unknown sub-command: ${subCommand}`);\n printReleaseHelp();\n process.exit(1);\n }\n}\n"],"mappings":"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","names":[]}