@canaryai/cli 0.2.9 → 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.
- package/dist/{chunk-C2PGZRYK.js → chunk-CEW4BDXD.js} +26 -7
- package/dist/chunk-CEW4BDXD.js.map +1 -0
- package/dist/chunk-ERSNYLMZ.js +229 -0
- package/dist/chunk-ERSNYLMZ.js.map +1 -0
- package/dist/{chunk-XGO62PO2.js → chunk-MSMC6UXW.js} +198 -11
- package/dist/{chunk-XGO62PO2.js.map → chunk-MSMC6UXW.js.map} +1 -1
- package/dist/{chunk-LC7ZVXPH.js → chunk-Q7WFBG5C.js} +2 -2
- package/dist/{debug-workflow-I3F36JBL.js → debug-workflow-53ULOFJC.js} +4 -4
- package/dist/{docs-REHST3YB.js → docs-BEE3LOCO.js} +2 -2
- package/dist/{feature-flag-3HB5NTMY.js → feature-flag-CYTDV4ZB.js} +2 -2
- package/dist/index.js +61 -139
- package/dist/index.js.map +1 -1
- package/dist/init-M6I3MG3D.js +146 -0
- package/dist/init-M6I3MG3D.js.map +1 -0
- package/dist/{issues-YU57CHXS.js → issues-NLM72HLU.js} +2 -2
- package/dist/{knobs-QJ4IBLCT.js → knobs-O35GAU5M.js} +2 -2
- package/dist/list-4K4EIGAT.js +57 -0
- package/dist/list-4K4EIGAT.js.map +1 -0
- package/dist/local-NHXXPHZ3.js +63 -0
- package/dist/local-NHXXPHZ3.js.map +1 -0
- package/dist/{local-browser-MKTJ36KY.js → local-browser-VAZORCO3.js} +3 -3
- package/dist/login-ZLP64YQP.js +130 -0
- package/dist/login-ZLP64YQP.js.map +1 -0
- package/dist/{mcp-ZOKM2AUE.js → mcp-ZF5G5DCB.js} +4 -126
- package/dist/mcp-ZF5G5DCB.js.map +1 -0
- package/dist/{record-TNDBT3NY.js → record-V6QKFFH3.js} +6 -47
- package/dist/record-V6QKFFH3.js.map +1 -0
- package/dist/{release-L4IXOHDF.js → release-7TI7EIGD.js} +8 -4
- package/dist/release-7TI7EIGD.js.map +1 -0
- package/dist/{session-RNLKFS2Z.js → session-UGNJXRUW.js} +138 -70
- package/dist/session-UGNJXRUW.js.map +1 -0
- package/dist/skill-ORWAPBDW.js +424 -0
- package/dist/skill-ORWAPBDW.js.map +1 -0
- package/dist/{src-2WSMYBMJ.js → src-4VIDSK4A.js} +2 -2
- package/dist/start-E532F3BU.js +112 -0
- package/dist/start-E532F3BU.js.map +1 -0
- package/dist/workflow-HXIUXRFI.js +613 -0
- package/dist/workflow-HXIUXRFI.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-C2PGZRYK.js.map +0 -1
- package/dist/chunk-DXIAHB72.js +0 -340
- package/dist/chunk-DXIAHB72.js.map +0 -1
- package/dist/chunk-QLFSJG5O.js +0 -93
- package/dist/chunk-QLFSJG5O.js.map +0 -1
- package/dist/mcp-ZOKM2AUE.js.map +0 -1
- package/dist/record-TNDBT3NY.js.map +0 -1
- package/dist/release-L4IXOHDF.js.map +0 -1
- package/dist/session-RNLKFS2Z.js.map +0 -1
- package/dist/skill-CZ7SHI3P.js +0 -156
- package/dist/skill-CZ7SHI3P.js.map +0 -1
- /package/dist/{chunk-LC7ZVXPH.js.map → chunk-Q7WFBG5C.js.map} +0 -0
- /package/dist/{debug-workflow-I3F36JBL.js.map → debug-workflow-53ULOFJC.js.map} +0 -0
- /package/dist/{docs-REHST3YB.js.map → docs-BEE3LOCO.js.map} +0 -0
- /package/dist/{feature-flag-3HB5NTMY.js.map → feature-flag-CYTDV4ZB.js.map} +0 -0
- /package/dist/{issues-YU57CHXS.js.map → issues-NLM72HLU.js.map} +0 -0
- /package/dist/{knobs-QJ4IBLCT.js.map → knobs-O35GAU5M.js.map} +0 -0
- /package/dist/{local-browser-MKTJ36KY.js.map → local-browser-VAZORCO3.js.map} +0 -0
- /package/dist/{src-2WSMYBMJ.js.map → src-4VIDSK4A.js.map} +0 -0
|
@@ -2,8 +2,8 @@ import { createRequire as __cr } from "module"; const require = __cr(import.meta
|
|
|
2
2
|
import {
|
|
3
3
|
apiRequest,
|
|
4
4
|
downloadStorageState,
|
|
5
|
-
|
|
6
|
-
} from "./chunk-
|
|
5
|
+
selectCredential
|
|
6
|
+
} from "./chunk-ERSNYLMZ.js";
|
|
7
7
|
import {
|
|
8
8
|
getArgValue,
|
|
9
9
|
hasFlag,
|
|
@@ -17,7 +17,6 @@ import "./chunk-VKVL7WBN.js";
|
|
|
17
17
|
// src/record.ts
|
|
18
18
|
import fs from "fs/promises";
|
|
19
19
|
import path from "path";
|
|
20
|
-
import readline from "readline";
|
|
21
20
|
import process from "process";
|
|
22
21
|
|
|
23
22
|
// src/record-interaction-script.ts
|
|
@@ -186,24 +185,6 @@ var INTERACTION_CAPTURE_SCRIPT = `
|
|
|
186
185
|
`;
|
|
187
186
|
|
|
188
187
|
// src/record.ts
|
|
189
|
-
async function promptSelection(items, prompt) {
|
|
190
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
191
|
-
return new Promise((resolve) => {
|
|
192
|
-
for (let i = 0; i < items.length; i++) {
|
|
193
|
-
console.log(` ${i + 1}. ${items[i]}`);
|
|
194
|
-
}
|
|
195
|
-
rl.question(`
|
|
196
|
-
${prompt} `, (answer) => {
|
|
197
|
-
rl.close();
|
|
198
|
-
const idx = parseInt(answer, 10) - 1;
|
|
199
|
-
if (isNaN(idx) || idx < 0 || idx >= items.length) {
|
|
200
|
-
console.error("Invalid selection.");
|
|
201
|
-
process.exit(1);
|
|
202
|
-
}
|
|
203
|
-
resolve(idx);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
}
|
|
207
188
|
async function runRecord(argv) {
|
|
208
189
|
if (hasFlag(argv, "--help", "-h")) {
|
|
209
190
|
console.log(
|
|
@@ -230,32 +211,10 @@ async function runRecord(argv) {
|
|
|
230
211
|
const outputDir = getArgValue(argv, "--output");
|
|
231
212
|
const skipUpload = hasFlag(argv, "--no-upload");
|
|
232
213
|
console.log("Fetching credentials...");
|
|
233
|
-
const
|
|
234
|
-
|
|
235
|
-
config.token,
|
|
236
|
-
"/org/credentials",
|
|
237
|
-
"items"
|
|
238
|
-
);
|
|
239
|
-
if (credentials.length === 0) {
|
|
240
|
-
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) {
|
|
241
216
|
process.exit(1);
|
|
242
217
|
}
|
|
243
|
-
let credential;
|
|
244
|
-
if (credentialArg) {
|
|
245
|
-
const found = credentials.find((c) => c.id === credentialArg);
|
|
246
|
-
if (!found) {
|
|
247
|
-
console.error(`Credential not found: ${credentialArg}`);
|
|
248
|
-
process.exit(1);
|
|
249
|
-
}
|
|
250
|
-
credential = found;
|
|
251
|
-
} else {
|
|
252
|
-
console.log("\nSelect a credential:\n");
|
|
253
|
-
const labels = credentials.map(
|
|
254
|
-
(c) => `${c.name} (${c.propertyName ?? c.propertyId})`
|
|
255
|
-
);
|
|
256
|
-
const idx = await promptSelection(labels, "Enter number:");
|
|
257
|
-
credential = credentials[idx];
|
|
258
|
-
}
|
|
259
218
|
console.log(`Using credential: ${credential.name}`);
|
|
260
219
|
const propertyId = credential.propertyId;
|
|
261
220
|
let storageStatePath;
|
|
@@ -288,7 +247,7 @@ async function runRecord(argv) {
|
|
|
288
247
|
console.warn("No start URL provided and no login URL on credential. Browser will open to about:blank.");
|
|
289
248
|
}
|
|
290
249
|
console.log("Launching browser...");
|
|
291
|
-
const { PlaywrightClient, consoleLogger, captureElementAtPoint } = await import("./src-
|
|
250
|
+
const { PlaywrightClient, consoleLogger, captureElementAtPoint } = await import("./src-4VIDSK4A.js");
|
|
292
251
|
const videoDir = path.join(getCanaryTmpDir(), `canary-record-video-${Date.now()}`);
|
|
293
252
|
await fs.mkdir(videoDir, { recursive: true });
|
|
294
253
|
const client = new PlaywrightClient({ logger: consoleLogger });
|
|
@@ -435,4 +394,4 @@ Output directory: ${outDir}`);
|
|
|
435
394
|
export {
|
|
436
395
|
runRecord
|
|
437
396
|
};
|
|
438
|
-
//# sourceMappingURL=record-
|
|
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 === "
|
|
158
|
-
console.
|
|
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.
|
|
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-
|
|
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":[]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createRequire as __cr } from "module"; const require = __cr(import.meta.url);
|
|
2
2
|
import {
|
|
3
3
|
downloadStorageState,
|
|
4
|
-
|
|
5
|
-
} from "./chunk-
|
|
4
|
+
selectCredential
|
|
5
|
+
} from "./chunk-ERSNYLMZ.js";
|
|
6
6
|
import {
|
|
7
7
|
callTool,
|
|
8
8
|
createSession,
|
|
@@ -10,8 +10,9 @@ import {
|
|
|
10
10
|
deleteSession,
|
|
11
11
|
getSession,
|
|
12
12
|
listSessions,
|
|
13
|
-
resolveTargetSession
|
|
14
|
-
|
|
13
|
+
resolveTargetSession,
|
|
14
|
+
swapSessionContext
|
|
15
|
+
} from "./chunk-CEW4BDXD.js";
|
|
15
16
|
import {
|
|
16
17
|
getArgValue,
|
|
17
18
|
hasFlag,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
BrowserToolExecutor,
|
|
22
23
|
PlaywrightClient,
|
|
23
24
|
dispatchBrowserTool
|
|
24
|
-
} from "./chunk-
|
|
25
|
+
} from "./chunk-MSMC6UXW.js";
|
|
25
26
|
import "./chunk-XAA5VQ5N.js";
|
|
26
27
|
import {
|
|
27
28
|
consoleLogger
|
|
@@ -29,57 +30,15 @@ import {
|
|
|
29
30
|
import "./chunk-VKVL7WBN.js";
|
|
30
31
|
|
|
31
32
|
// src/session/index.ts
|
|
32
|
-
import
|
|
33
|
+
import process2 from "process";
|
|
33
34
|
|
|
34
35
|
// src/session/credentials.ts
|
|
35
|
-
import process2 from "process";
|
|
36
36
|
async function resolveCredential(argv, credentialArg) {
|
|
37
37
|
const { apiUrl, token } = await resolveConfig(argv);
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
token,
|
|
41
|
-
"/org/credentials",
|
|
42
|
-
"credentials"
|
|
43
|
-
);
|
|
44
|
-
if (credentials.length === 0) {
|
|
45
|
-
console.error("No credentials found for this organization.");
|
|
38
|
+
const credential = await selectCredential(apiUrl, token, credentialArg);
|
|
39
|
+
if (!credential) {
|
|
46
40
|
return null;
|
|
47
41
|
}
|
|
48
|
-
let credential;
|
|
49
|
-
if (!credentialArg) {
|
|
50
|
-
console.log("Select a credential:");
|
|
51
|
-
for (let i = 0; i < credentials.length; i++) {
|
|
52
|
-
const c = credentials[i];
|
|
53
|
-
const propLabel = c.propertyName ? ` (${c.propertyName})` : "";
|
|
54
|
-
console.log(` ${i + 1}. ${c.name}${propLabel}`);
|
|
55
|
-
}
|
|
56
|
-
const readline = await import("readline");
|
|
57
|
-
const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
|
|
58
|
-
const answer = await new Promise((resolve) => {
|
|
59
|
-
rl.question("> ", (ans) => {
|
|
60
|
-
rl.close();
|
|
61
|
-
resolve(ans.trim());
|
|
62
|
-
});
|
|
63
|
-
});
|
|
64
|
-
const idx = parseInt(answer, 10) - 1;
|
|
65
|
-
if (isNaN(idx) || idx < 0 || idx >= credentials.length) {
|
|
66
|
-
console.error("Invalid selection.");
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
credential = credentials[idx];
|
|
70
|
-
} else {
|
|
71
|
-
credential = credentials.find(
|
|
72
|
-
(c) => c.id === credentialArg || c.name.toLowerCase() === credentialArg.toLowerCase()
|
|
73
|
-
);
|
|
74
|
-
if (!credential) {
|
|
75
|
-
console.error(`Credential "${credentialArg}" not found.`);
|
|
76
|
-
console.error("Available credentials:");
|
|
77
|
-
for (const c of credentials) {
|
|
78
|
-
console.error(` - ${c.name} (${c.id})`);
|
|
79
|
-
}
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
42
|
console.log(`Fetching credential "${credential.name}"...`);
|
|
84
43
|
let storageStatePath;
|
|
85
44
|
if (credential.storageStateS3Key) {
|
|
@@ -188,9 +147,6 @@ async function handleCreateSession(body, res) {
|
|
|
188
147
|
browserMode: mode,
|
|
189
148
|
storageStatePath: params.storageStatePath
|
|
190
149
|
});
|
|
191
|
-
if (params.url) {
|
|
192
|
-
await client.navigate(params.url);
|
|
193
|
-
}
|
|
194
150
|
const executor = new BrowserToolExecutor(client, {
|
|
195
151
|
autoSnapshotAfterAction: true,
|
|
196
152
|
includeScreenshotWithSnapshot: true,
|
|
@@ -216,6 +172,11 @@ async function handleCreateSession(body, res) {
|
|
|
216
172
|
data: await toSessionInfo(session)
|
|
217
173
|
};
|
|
218
174
|
json(res, 201, result);
|
|
175
|
+
if (params.url) {
|
|
176
|
+
client.navigate(params.url).catch((err) => {
|
|
177
|
+
consoleLogger.warn(`Initial navigation to ${params.url} failed: ${err}`);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
219
180
|
} catch (err) {
|
|
220
181
|
const result = {
|
|
221
182
|
ok: false,
|
|
@@ -261,6 +222,44 @@ async function handleDeleteAllSessions(res) {
|
|
|
261
222
|
json(res, 200, { ok: true });
|
|
262
223
|
resetIdleTimer();
|
|
263
224
|
}
|
|
225
|
+
async function handleSwapContext(sessionId, body, res) {
|
|
226
|
+
const session = sessions.get(sessionId);
|
|
227
|
+
if (!session) {
|
|
228
|
+
json(res, 404, { ok: false, error: `Session "${sessionId}" not found` });
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const params = parseJson(body);
|
|
232
|
+
try {
|
|
233
|
+
await session.client.swapContext({
|
|
234
|
+
storageStatePath: params.storageStatePath
|
|
235
|
+
});
|
|
236
|
+
if (params.credentialName) {
|
|
237
|
+
session.credentialName = params.credentialName;
|
|
238
|
+
}
|
|
239
|
+
json(res, 200, { ok: true, data: await toSessionInfo(session) });
|
|
240
|
+
} catch (err) {
|
|
241
|
+
json(res, 500, {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: err instanceof Error ? err.message : String(err)
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function handleGetStorageState(sessionId, res) {
|
|
248
|
+
const session = sessions.get(sessionId);
|
|
249
|
+
if (!session) {
|
|
250
|
+
json(res, 404, { ok: false, error: `Session "${sessionId}" not found` });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const storageState = await session.client.getStorageState();
|
|
255
|
+
json(res, 200, { ok: true, data: storageState });
|
|
256
|
+
} catch (err) {
|
|
257
|
+
json(res, 500, {
|
|
258
|
+
ok: false,
|
|
259
|
+
error: err instanceof Error ? err.message : String(err)
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
264
263
|
async function handleToolCall(sessionId, toolName, body, res) {
|
|
265
264
|
const session = sessions.get(sessionId);
|
|
266
265
|
if (!session) {
|
|
@@ -316,6 +315,14 @@ async function handleRequest(req, res) {
|
|
|
316
315
|
await handleDeleteSession(segments[1], res);
|
|
317
316
|
return;
|
|
318
317
|
}
|
|
318
|
+
if (method === "POST" && segments[0] === "sessions" && segments[2] === "swap-context" && segments.length === 3) {
|
|
319
|
+
await handleSwapContext(segments[1], body, res);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (method === "GET" && segments[0] === "sessions" && segments[2] === "storage-state" && segments.length === 3) {
|
|
323
|
+
await handleGetStorageState(segments[1], res);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
319
326
|
if (method === "POST" && segments[0] === "sessions" && segments[2] === "tools" && segments.length === 4) {
|
|
320
327
|
await handleToolCall(segments[1], segments[3], body, res);
|
|
321
328
|
return;
|
|
@@ -352,6 +359,7 @@ async function cleanup() {
|
|
|
352
359
|
}
|
|
353
360
|
async function startDaemon() {
|
|
354
361
|
const server = createServer(handleRequest);
|
|
362
|
+
server.keepAliveTimeout = 0;
|
|
355
363
|
return new Promise((resolve, reject) => {
|
|
356
364
|
server.listen(0, "127.0.0.1", async () => {
|
|
357
365
|
const addr = server.address();
|
|
@@ -426,7 +434,7 @@ function printToolResult(result, jsonMode) {
|
|
|
426
434
|
}
|
|
427
435
|
if (!result.ok) {
|
|
428
436
|
console.error(`Error: ${result.error}`);
|
|
429
|
-
|
|
437
|
+
process2.exitCode = 1;
|
|
430
438
|
return;
|
|
431
439
|
}
|
|
432
440
|
if (result.text) {
|
|
@@ -449,7 +457,7 @@ async function handleStart(argv) {
|
|
|
449
457
|
const credentialArg = nextArg && !nextArg.startsWith("--") ? nextArg : void 0;
|
|
450
458
|
resolvedCred = await resolveCredential(argv, credentialArg);
|
|
451
459
|
if (!resolvedCred) {
|
|
452
|
-
|
|
460
|
+
process2.exitCode = 1;
|
|
453
461
|
return;
|
|
454
462
|
}
|
|
455
463
|
}
|
|
@@ -466,7 +474,7 @@ async function handleStart(argv) {
|
|
|
466
474
|
}
|
|
467
475
|
if (!result.ok) {
|
|
468
476
|
console.error(`Error: ${result.error}`);
|
|
469
|
-
|
|
477
|
+
process2.exitCode = 1;
|
|
470
478
|
return;
|
|
471
479
|
}
|
|
472
480
|
const session = result.data;
|
|
@@ -474,8 +482,9 @@ async function handleStart(argv) {
|
|
|
474
482
|
if (session.credentialName) {
|
|
475
483
|
console.log(` Credential: ${session.credentialName}`);
|
|
476
484
|
}
|
|
477
|
-
|
|
478
|
-
|
|
485
|
+
const displayUrl = url ?? session.url;
|
|
486
|
+
if (displayUrl) {
|
|
487
|
+
console.log(` URL: ${displayUrl}`);
|
|
479
488
|
}
|
|
480
489
|
}
|
|
481
490
|
async function handleList(argv) {
|
|
@@ -487,13 +496,13 @@ async function handleList(argv) {
|
|
|
487
496
|
}
|
|
488
497
|
if (!result.ok) {
|
|
489
498
|
console.error(`Error: ${result.error}`);
|
|
490
|
-
|
|
499
|
+
process2.exitCode = 1;
|
|
491
500
|
return;
|
|
492
501
|
}
|
|
493
502
|
printSessionTable(result.data ?? []);
|
|
494
503
|
}
|
|
495
504
|
async function handleStatus(argv) {
|
|
496
|
-
const sessionId = argv[0] && !argv[0].startsWith("--") ? argv[0] : void 0;
|
|
505
|
+
const sessionId = getArgValue(argv, "--session") || (argv[0] && !argv[0].startsWith("--") ? argv[0] : void 0);
|
|
497
506
|
const jsonMode = hasFlag(argv, "--json");
|
|
498
507
|
try {
|
|
499
508
|
const target = await resolveTargetSession(sessionId);
|
|
@@ -504,7 +513,7 @@ async function handleStatus(argv) {
|
|
|
504
513
|
}
|
|
505
514
|
if (!result.ok || !result.data) {
|
|
506
515
|
console.error(`Error: ${result.error}`);
|
|
507
|
-
|
|
516
|
+
process2.exitCode = 1;
|
|
508
517
|
return;
|
|
509
518
|
}
|
|
510
519
|
const s = result.data;
|
|
@@ -516,7 +525,7 @@ async function handleStatus(argv) {
|
|
|
516
525
|
console.log(` Started: ${s.startedAt}`);
|
|
517
526
|
} catch (err) {
|
|
518
527
|
console.error(err instanceof Error ? err.message : String(err));
|
|
519
|
-
|
|
528
|
+
process2.exitCode = 1;
|
|
520
529
|
}
|
|
521
530
|
}
|
|
522
531
|
async function handleStop(argv) {
|
|
@@ -529,11 +538,11 @@ async function handleStop(argv) {
|
|
|
529
538
|
console.log("All sessions stopped.");
|
|
530
539
|
} else {
|
|
531
540
|
console.error(`Error: ${result.error}`);
|
|
532
|
-
|
|
541
|
+
process2.exitCode = 1;
|
|
533
542
|
}
|
|
534
543
|
return;
|
|
535
544
|
}
|
|
536
|
-
const sessionId = argv[0] && !argv[0].startsWith("--") ? argv[0] : void 0;
|
|
545
|
+
const sessionId = getArgValue(argv, "--session") || (argv[0] && !argv[0].startsWith("--") ? argv[0] : void 0);
|
|
537
546
|
try {
|
|
538
547
|
const target = await resolveTargetSession(sessionId);
|
|
539
548
|
const result = await deleteSession(target.id);
|
|
@@ -543,11 +552,62 @@ async function handleStop(argv) {
|
|
|
543
552
|
console.log(`Session "${target.id}" stopped.`);
|
|
544
553
|
} else {
|
|
545
554
|
console.error(`Error: ${result.error}`);
|
|
546
|
-
|
|
555
|
+
process2.exitCode = 1;
|
|
547
556
|
}
|
|
548
557
|
} catch (err) {
|
|
549
558
|
console.error(err instanceof Error ? err.message : String(err));
|
|
550
|
-
|
|
559
|
+
process2.exitCode = 1;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function handleLoadCredential(argv) {
|
|
563
|
+
const jsonMode = hasFlag(argv, "--json");
|
|
564
|
+
const sessionId = getArgValue(argv, "--session") || (argv[0] && !argv[0].startsWith("--") ? argv[0] : void 0);
|
|
565
|
+
let target;
|
|
566
|
+
try {
|
|
567
|
+
target = await resolveTargetSession(sessionId);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
570
|
+
process2.exitCode = 1;
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
const credentialIdx = argv.indexOf("--credential");
|
|
574
|
+
const nextArg = credentialIdx !== -1 ? argv[credentialIdx + 1] : void 0;
|
|
575
|
+
const credentialArg = nextArg && !nextArg.startsWith("--") ? nextArg : void 0;
|
|
576
|
+
const storageStatePath = getArgValue(argv, "--storage-state");
|
|
577
|
+
let resolvedStorageStatePath;
|
|
578
|
+
let credentialName;
|
|
579
|
+
if (storageStatePath) {
|
|
580
|
+
resolvedStorageStatePath = storageStatePath;
|
|
581
|
+
credentialName = "(local storage state)";
|
|
582
|
+
} else {
|
|
583
|
+
const resolvedCred = await resolveCredential(argv, credentialArg);
|
|
584
|
+
if (!resolvedCred) {
|
|
585
|
+
process2.exitCode = 1;
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
resolvedStorageStatePath = resolvedCred.storageStatePath;
|
|
589
|
+
credentialName = resolvedCred.credentialName;
|
|
590
|
+
}
|
|
591
|
+
const result = await swapSessionContext(target.id, {
|
|
592
|
+
storageStatePath: resolvedStorageStatePath,
|
|
593
|
+
credentialName
|
|
594
|
+
});
|
|
595
|
+
if (jsonMode) {
|
|
596
|
+
console.log(JSON.stringify(result, null, 2));
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (!result.ok) {
|
|
600
|
+
console.error(`Error: ${result.error}`);
|
|
601
|
+
process2.exitCode = 1;
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const session = result.data;
|
|
605
|
+
console.log(`Credential loaded into session "${session.id}".`);
|
|
606
|
+
if (session.credentialName) {
|
|
607
|
+
console.log(` Credential: ${session.credentialName}`);
|
|
608
|
+
}
|
|
609
|
+
if (session.url) {
|
|
610
|
+
console.log(` URL: ${session.url}`);
|
|
551
611
|
}
|
|
552
612
|
}
|
|
553
613
|
async function handleToolCommand(toolName, argv) {
|
|
@@ -560,7 +620,7 @@ async function handleToolCommand(toolName, argv) {
|
|
|
560
620
|
printToolResult(result, jsonMode);
|
|
561
621
|
} catch (err) {
|
|
562
622
|
console.error(err instanceof Error ? err.message : String(err));
|
|
563
|
-
|
|
623
|
+
process2.exitCode = 1;
|
|
564
624
|
}
|
|
565
625
|
}
|
|
566
626
|
function buildToolArgs(toolName, argv) {
|
|
@@ -604,7 +664,7 @@ function buildToolArgs(toolName, argv) {
|
|
|
604
664
|
args.fields = JSON.parse(fieldsStr);
|
|
605
665
|
} catch {
|
|
606
666
|
console.error("Error: --fields must be valid JSON");
|
|
607
|
-
|
|
667
|
+
process2.exitCode = 1;
|
|
608
668
|
}
|
|
609
669
|
}
|
|
610
670
|
break;
|
|
@@ -675,6 +735,7 @@ function printHelp() {
|
|
|
675
735
|
" list List active sessions",
|
|
676
736
|
" status [<id>] Show session details",
|
|
677
737
|
" stop [<id>] Stop a session (or --all)",
|
|
738
|
+
" load-credential Load a credential into a running session",
|
|
678
739
|
"",
|
|
679
740
|
"Browser actions:",
|
|
680
741
|
" navigate --url <url> Navigate to URL",
|
|
@@ -702,6 +763,10 @@ function printHelp() {
|
|
|
702
763
|
" --url <url> Navigate to URL after launch",
|
|
703
764
|
" --name <label> Name the session",
|
|
704
765
|
"",
|
|
766
|
+
"Load-credential options:",
|
|
767
|
+
" --credential [<name|id>] Use a credential (interactive if no value)",
|
|
768
|
+
" --storage-state <path> Use a local storage state file",
|
|
769
|
+
"",
|
|
705
770
|
"Common options:",
|
|
706
771
|
" --session <id|name> Target a specific session (when multiple)",
|
|
707
772
|
" --json Output raw JSON",
|
|
@@ -733,6 +798,9 @@ async function runSession(argv) {
|
|
|
733
798
|
case "stop":
|
|
734
799
|
await handleStop(rest);
|
|
735
800
|
break;
|
|
801
|
+
case "load-credential":
|
|
802
|
+
await handleLoadCredential(rest);
|
|
803
|
+
break;
|
|
736
804
|
default: {
|
|
737
805
|
const toolName = TOOL_ALIASES[subcommand];
|
|
738
806
|
if (toolName) {
|
|
@@ -740,7 +808,7 @@ async function runSession(argv) {
|
|
|
740
808
|
} else {
|
|
741
809
|
console.error(`Unknown session subcommand: ${subcommand}`);
|
|
742
810
|
printHelp();
|
|
743
|
-
|
|
811
|
+
process2.exitCode = 1;
|
|
744
812
|
}
|
|
745
813
|
}
|
|
746
814
|
}
|
|
@@ -748,4 +816,4 @@ async function runSession(argv) {
|
|
|
748
816
|
export {
|
|
749
817
|
runSession
|
|
750
818
|
};
|
|
751
|
-
//# sourceMappingURL=session-
|
|
819
|
+
//# sourceMappingURL=session-UGNJXRUW.js.map
|