@canaryai/cli 0.1.6 → 0.1.11

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 (37) hide show
  1. package/README.md +52 -1
  2. package/dist/bin.js +1 -6
  3. package/dist/bin.js.map +1 -1
  4. package/dist/{chunk-7OCVIDC7.js → chunk-DGUM43GV.js} +1 -2
  5. package/dist/{chunk-55MFLJD7.js → chunk-G2X3H7AM.js} +1 -3
  6. package/dist/{chunk-55MFLJD7.js.map → chunk-G2X3H7AM.js.map} +1 -1
  7. package/dist/chunk-HJ2JWIJ7.js +91 -0
  8. package/dist/chunk-HJ2JWIJ7.js.map +1 -0
  9. package/dist/{chunk-7AP5KRVU.js → chunk-ROTCL5WO.js} +1 -3
  10. package/dist/{chunk-7AP5KRVU.js.map → chunk-ROTCL5WO.js.map} +1 -1
  11. package/dist/{chunk-Z6I3ZXZL.js → chunk-VYBCH4ZP.js} +2 -3
  12. package/dist/{chunk-Z6I3ZXZL.js.map → chunk-VYBCH4ZP.js.map} +1 -1
  13. package/dist/feature-flag-PN5IFFQR.js +226 -0
  14. package/dist/feature-flag-PN5IFFQR.js.map +1 -0
  15. package/dist/index.js +1207 -8
  16. package/dist/index.js.map +1 -1
  17. package/dist/knobs-DAG7HD2F.js +286 -0
  18. package/dist/knobs-DAG7HD2F.js.map +1 -0
  19. package/dist/{local-browser-5LJ7UPOH.js → local-browser-VOBIUIGT.js} +4 -5
  20. package/dist/{local-browser-5LJ7UPOH.js.map → local-browser-VOBIUIGT.js.map} +1 -1
  21. package/dist/{mcp-P2B24MTM.js → mcp-I6FCGDDR.js} +5 -6
  22. package/dist/{mcp-P2B24MTM.js.map → mcp-I6FCGDDR.js.map} +1 -1
  23. package/dist/psql-A3BADRQN.js +124 -0
  24. package/dist/psql-A3BADRQN.js.map +1 -0
  25. package/dist/redis-N2DSDDQU.js +130 -0
  26. package/dist/redis-N2DSDDQU.js.map +1 -0
  27. package/dist/runner/preload.js +2 -3
  28. package/dist/runner/preload.js.map +1 -1
  29. package/dist/test.js +2 -3
  30. package/dist/test.js.map +1 -1
  31. package/package.json +3 -4
  32. package/dist/bin.d.ts +0 -2
  33. package/dist/chunk-UBYYNMML.js +0 -21
  34. package/dist/chunk-UBYYNMML.js.map +0 -1
  35. package/dist/chunk-YA43CE6P.js +0 -781
  36. package/dist/chunk-YA43CE6P.js.map +0 -1
  37. /package/dist/{chunk-7OCVIDC7.js.map → chunk-DGUM43GV.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,12 +1,1211 @@
1
- #!/usr/bin/env node
2
1
  import {
3
- canary,
4
- main,
5
- run
6
- } from "./chunk-YA43CE6P.js";
7
- import "./chunk-Z6I3ZXZL.js";
8
- import "./chunk-UBYYNMML.js";
9
- import "./chunk-7OCVIDC7.js";
2
+ connectTunnel,
3
+ createLocalRun,
4
+ createTunnel,
5
+ runLocalTest,
6
+ runTunnel
7
+ } from "./chunk-VYBCH4ZP.js";
8
+ import {
9
+ ENV_URLS,
10
+ readStoredApiUrl,
11
+ readStoredAuth,
12
+ readStoredToken,
13
+ saveAuth
14
+ } from "./chunk-HJ2JWIJ7.js";
15
+ import {
16
+ __require
17
+ } from "./chunk-DGUM43GV.js";
18
+
19
+ // src/index.ts
20
+ import { spawnSync as spawnSync2 } from "child_process";
21
+ import { createRequire as createRequire2 } from "module";
22
+ import process7 from "process";
23
+ import path4 from "path";
24
+ import { fileURLToPath as fileURLToPath2, pathToFileURL as pathToFileURL2 } from "url";
25
+
26
+ // src/runner/common.ts
27
+ import { spawnSync } from "child_process";
28
+ import fs from "fs";
29
+ import path from "path";
30
+ import { createRequire } from "module";
31
+ import { pathToFileURL } from "url";
32
+ function makeRequire() {
33
+ try {
34
+ return createRequire(import.meta.url);
35
+ } catch {
36
+ try {
37
+ return createRequire(process.cwd());
38
+ } catch {
39
+ return typeof __require !== "undefined" ? __require : createRequire(".");
40
+ }
41
+ }
42
+ }
43
+ function resolveRunner(preloadPath2) {
44
+ const { bin, version } = pickNodeBinary();
45
+ const supportsImport = typeof version === "number" && version >= 18;
46
+ if (supportsImport && preloadPath2 && fs.existsSync(preloadPath2)) {
47
+ return { runnerBin: bin, preloadFlag: `--import=${pathToFileURL(preloadPath2).href}` };
48
+ }
49
+ if (preloadPath2) {
50
+ console.warn("[canary] Warning: no preload module found; instrumentation may be disabled.");
51
+ }
52
+ return { runnerBin: bin };
53
+ }
54
+ function pickNodeBinary() {
55
+ const candidates = collectNodeCandidates();
56
+ let best;
57
+ let fallback;
58
+ for (const bin of candidates) {
59
+ const version = getNodeMajor(bin);
60
+ if (!version) continue;
61
+ const current = { bin, version };
62
+ if (version >= 18 && !fallback) {
63
+ fallback = current;
64
+ }
65
+ if (!best || version > (best.version ?? 0)) {
66
+ best = current;
67
+ }
68
+ }
69
+ if (fallback) return fallback;
70
+ if (best) return best;
71
+ return { bin: candidates[0] ?? "node" };
72
+ }
73
+ function collectNodeCandidates() {
74
+ const seen = /* @__PURE__ */ new Set();
75
+ const push = (value) => {
76
+ if (!value) return;
77
+ if (seen.has(value)) return;
78
+ seen.add(value);
79
+ };
80
+ const isBun = path.basename(process.execPath).includes("bun");
81
+ push(process.env.CANARY_NODE_BIN);
82
+ push(isBun ? void 0 : process.execPath);
83
+ push("node");
84
+ try {
85
+ const which = spawnSync("which", ["-a", "node"], { encoding: "utf-8" });
86
+ which.stdout?.toString().split("\n").map((line) => line.trim()).forEach((line) => push(line));
87
+ } catch {
88
+ }
89
+ const nvmDir = process.env.NVM_DIR || (process.env.HOME ? path.join(process.env.HOME, ".nvm") : void 0);
90
+ if (nvmDir) {
91
+ const versionsDir = path.join(nvmDir, "versions", "node");
92
+ if (fs.existsSync(versionsDir)) {
93
+ try {
94
+ const versions = fs.readdirSync(versionsDir);
95
+ versions.sort((a, b) => a > b ? -1 : 1).forEach((v) => push(path.join(versionsDir, v, "bin", "node")));
96
+ } catch {
97
+ }
98
+ }
99
+ }
100
+ return Array.from(seen);
101
+ }
102
+ function getNodeMajor(bin) {
103
+ try {
104
+ const result = spawnSync(bin, ["-v"], { encoding: "utf-8" });
105
+ const output = (result.stdout || result.stderr || "").toString().trim();
106
+ const match = output.match(/^v(\d+)/);
107
+ if (match) return Number(match[1]);
108
+ } catch {
109
+ }
110
+ return void 0;
111
+ }
112
+
113
+ // src/run.ts
114
+ import { spawn } from "child_process";
115
+ import fs2 from "fs";
116
+ import os from "os";
117
+ import path2 from "path";
118
+ import { fileURLToPath } from "url";
119
+ async function run(request = {}) {
120
+ const cwd = request.projectRoot ?? process.cwd();
121
+ const stdio = request.stdio ?? "inherit";
122
+ const requireFn2 = makeRequire();
123
+ const playwrightCli = requireFn2.resolve("@playwright/test/cli");
124
+ const baseDir2 = path2.dirname(fileURLToPath(import.meta.url));
125
+ const preloadPath2 = path2.join(baseDir2, "runner", "preload.js");
126
+ const { runnerBin, preloadFlag } = resolveRunner(preloadPath2);
127
+ const { jsonReportPath, eventLogPath, artifactsDir } = prepareArtifactsDir(cwd);
128
+ const reporter = buildReporterArgs(request.reporter, jsonReportPath);
129
+ const args = buildArgs({
130
+ testDir: request.testDir,
131
+ configFile: request.configFile,
132
+ cliArgs: request.cliArgs,
133
+ reporter
134
+ });
135
+ const nodeOptions = process.env.NODE_OPTIONS && preloadFlag ? `${process.env.NODE_OPTIONS} ${preloadFlag}` : preloadFlag ?? process.env.NODE_OPTIONS;
136
+ const env = buildEnv({
137
+ base: process.env,
138
+ overrides: request.env,
139
+ healing: request.healing,
140
+ eventLogPath,
141
+ nodeOptions
142
+ });
143
+ const runResult = await spawnPlaywright({
144
+ bin: request.nodeBin ?? runnerBin,
145
+ args: [playwrightCli, ...args],
146
+ cwd,
147
+ env,
148
+ stdio,
149
+ timeoutMs: request.timeoutMs
150
+ });
151
+ const summary = summarize(jsonReportPath, eventLogPath, runResult.durationMs);
152
+ return {
153
+ ok: runResult.exitCode === 0,
154
+ exitCode: runResult.exitCode,
155
+ summary,
156
+ artifactsDir,
157
+ rawOutput: runResult.output,
158
+ error: runResult.error
159
+ };
160
+ }
161
+ function buildArgs(opts) {
162
+ const args = ["test"];
163
+ if (opts.testDir) {
164
+ const dirs = Array.isArray(opts.testDir) ? opts.testDir : [opts.testDir];
165
+ args.push(...dirs);
166
+ }
167
+ if (opts.configFile) {
168
+ args.push("--config", opts.configFile);
169
+ }
170
+ args.push("--reporter", opts.reporter);
171
+ if (opts.cliArgs?.length) {
172
+ args.push(...opts.cliArgs);
173
+ }
174
+ return args;
175
+ }
176
+ function buildReporterArgs(requested, jsonReportPath) {
177
+ if (requested === "json") return `json=${jsonReportPath}`;
178
+ if (requested && requested !== "default") return requested;
179
+ return `list,json=${jsonReportPath}`;
180
+ }
181
+ function prepareArtifactsDir(cwd) {
182
+ const dir = fs2.mkdtempSync(path2.join(os.tmpdir(), "canary-run-"));
183
+ const jsonReportPath = path2.join(dir, "report.json");
184
+ const eventLogPath = path2.join(dir, "events-worker-0.jsonl");
185
+ const artifactsDir = path2.join(cwd, "test-results", "ai-healer");
186
+ return { jsonReportPath, eventLogPath, artifactsDir: dir };
187
+ }
188
+ function buildEnv(params) {
189
+ const healing = params.healing ?? {};
190
+ const env = {
191
+ ...params.base,
192
+ CANARY_ENABLED: params.base.CANARY_ENABLED ?? "1",
193
+ CANARY_RUNNER: "canary",
194
+ CANARY_EVENT_LOG: params.eventLogPath,
195
+ ...params.nodeOptions ? { NODE_OPTIONS: params.nodeOptions } : {},
196
+ ...healing.apiKey ? { AI_API_KEY: healing.apiKey } : {},
197
+ ...healing.provider ? { AI_PROVIDER: healing.provider } : {},
198
+ ...healing.model ? { AI_MODEL: healing.model } : {},
199
+ ...healing.timeoutMs ? { AI_TIMEOUT_MS: String(healing.timeoutMs) } : {},
200
+ ...healing.maxActions ? { CANARY_MAX_ACTIONS: String(healing.maxActions) } : {},
201
+ ...healing.vision ? { CANARY_VISION: "1" } : {},
202
+ ...healing.dryRun ? { CANARY_DRY_RUN: "1" } : {},
203
+ ...healing.warnOnly ? { CANARY_WARN_ONLY: "1" } : {},
204
+ ...healing.debug ? { CANARY_DEBUG: "1" } : {},
205
+ ...healing.readOnly ? { CANARY_READ_ONLY: "1" } : {},
206
+ ...healing.allowEvaluate === false ? { CANARY_ALLOW_EVALUATE: "0" } : {},
207
+ ...healing.allowRunCode ? { CANARY_ALLOW_RUN_CODE: "1" } : {},
208
+ ...healing.maxPayloadBytes ? { CANARY_MAX_PAYLOAD_BYTES: String(healing.maxPayloadBytes) } : {},
209
+ ...params.overrides
210
+ };
211
+ return env;
212
+ }
213
+ async function spawnPlaywright(opts) {
214
+ return new Promise((resolve) => {
215
+ const started = Date.now();
216
+ const child = spawn(opts.bin, opts.args, {
217
+ cwd: opts.cwd,
218
+ env: opts.env,
219
+ stdio: opts.stdio === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"]
220
+ });
221
+ let timer;
222
+ let output = "";
223
+ let error;
224
+ if (opts.stdio === "pipe") {
225
+ child.stdout?.on("data", (chunk) => {
226
+ output += chunk.toString();
227
+ });
228
+ child.stderr?.on("data", (chunk) => {
229
+ output += chunk.toString();
230
+ });
231
+ }
232
+ if (opts.timeoutMs && opts.timeoutMs > 0) {
233
+ timer = setTimeout(() => {
234
+ error = new Error(`canary.run timed out after ${opts.timeoutMs}ms`);
235
+ child.kill("SIGKILL");
236
+ }, opts.timeoutMs);
237
+ }
238
+ child.on("close", (code) => {
239
+ if (timer) clearTimeout(timer);
240
+ resolve({ exitCode: code ?? 1, output: output || void 0, durationMs: Date.now() - started, error });
241
+ });
242
+ });
243
+ }
244
+ function summarize(jsonReportPath, eventLogPath, durationMs) {
245
+ const base = {
246
+ total: 0,
247
+ passed: 0,
248
+ failed: 0,
249
+ flaky: 0,
250
+ skipped: 0,
251
+ durationMs
252
+ };
253
+ const jsonReport = readJsonReport(jsonReportPath);
254
+ if (jsonReport) {
255
+ const counts = countTests(jsonReport);
256
+ base.total = counts.total;
257
+ base.passed = counts.passed;
258
+ base.failed = counts.failed;
259
+ base.flaky = counts.flaky;
260
+ base.skipped = counts.skipped;
261
+ base.durationMs = jsonReport.duration ?? durationMs;
262
+ }
263
+ const healed = countHealed(eventLogPath);
264
+ if (healed) {
265
+ return { ...base, healed };
266
+ }
267
+ return base;
268
+ }
269
+ function readJsonReport(reportPath) {
270
+ try {
271
+ if (fs2.existsSync(reportPath)) {
272
+ const raw = fs2.readFileSync(reportPath, "utf-8");
273
+ return JSON.parse(raw);
274
+ }
275
+ } catch {
276
+ }
277
+ return void 0;
278
+ }
279
+ function countTests(report) {
280
+ let total = 0;
281
+ let passed = 0;
282
+ let failed = 0;
283
+ let flaky = 0;
284
+ let skipped = 0;
285
+ const visitSuite = (suite) => {
286
+ if (!suite) return;
287
+ suite.tests?.forEach((test) => {
288
+ total += 1;
289
+ const statuses = test.results.map((r) => r.status);
290
+ const hasFailed = statuses.includes("failed") || statuses.includes("interrupted");
291
+ const hasPassed = statuses.includes("passed");
292
+ const hasTimedOut = statuses.includes("timedOut");
293
+ const allSkipped = statuses.every((s) => s === "skipped");
294
+ if (allSkipped) {
295
+ skipped += 1;
296
+ } else if ((hasFailed || hasTimedOut) && hasPassed) {
297
+ flaky += 1;
298
+ } else if (hasFailed || hasTimedOut) {
299
+ failed += 1;
300
+ } else if (hasPassed && statuses.length > 1) {
301
+ flaky += 1;
302
+ } else if (hasPassed) {
303
+ passed += 1;
304
+ }
305
+ });
306
+ suite.suites?.forEach(visitSuite);
307
+ };
308
+ report.suites?.forEach(visitSuite);
309
+ return { total, passed, failed, flaky, skipped };
310
+ }
311
+ function countHealed(eventLogPath) {
312
+ try {
313
+ if (!fs2.existsSync(eventLogPath)) return void 0;
314
+ const raw = fs2.readFileSync(eventLogPath, "utf-8").trim();
315
+ if (!raw) return void 0;
316
+ const lines = raw.split("\n");
317
+ let healed = 0;
318
+ for (const line of lines) {
319
+ try {
320
+ const event = JSON.parse(line);
321
+ if (event?.healed === true) healed += 1;
322
+ } catch {
323
+ }
324
+ }
325
+ return healed;
326
+ } catch {
327
+ return void 0;
328
+ }
329
+ }
330
+
331
+ // src/login.ts
332
+ import process2 from "process";
333
+ import readline from "readline";
334
+ import { spawn as spawn2 } from "child_process";
335
+ function envToProfile(env) {
336
+ if (env === "prod" || env === "production") return "production";
337
+ if (env === "dev") return "dev";
338
+ if (env === "local") return "local";
339
+ return env;
340
+ }
341
+ function getArgValue(argv, key) {
342
+ const index = argv.indexOf(key);
343
+ if (index === -1) return void 0;
344
+ return argv[index + 1];
345
+ }
346
+ function shouldOpenBrowser(argv) {
347
+ return !argv.includes("--no-open");
348
+ }
349
+ function openUrl(url) {
350
+ const platform = process2.platform;
351
+ if (platform === "darwin") {
352
+ spawn2("open", [url], { stdio: "ignore" });
353
+ return;
354
+ }
355
+ if (platform === "win32") {
356
+ spawn2("cmd", ["/c", "start", "", url], { stdio: "ignore" });
357
+ return;
358
+ }
359
+ spawn2("xdg-open", [url], { stdio: "ignore" });
360
+ }
361
+ function promptChoice(question) {
362
+ const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
363
+ return new Promise((resolve) => {
364
+ rl.question(question, (answer) => {
365
+ rl.close();
366
+ resolve(answer.trim());
367
+ });
368
+ });
369
+ }
370
+ async function fetchOrgs(apiUrl, token) {
371
+ try {
372
+ const res = await fetch(`${apiUrl}/cli-login/orgs`, {
373
+ headers: { Authorization: `Bearer ${token}` }
374
+ });
375
+ if (!res.ok) return null;
376
+ return await res.json();
377
+ } catch {
378
+ return null;
379
+ }
380
+ }
381
+ async function switchOrg(apiUrl, token, orgId) {
382
+ const res = await fetch(`${apiUrl}/cli-login/switch-org`, {
383
+ method: "POST",
384
+ headers: {
385
+ Authorization: `Bearer ${token}`,
386
+ "Content-Type": "application/json"
387
+ },
388
+ body: JSON.stringify({ orgId })
389
+ });
390
+ return await res.json();
391
+ }
392
+ async function runLogin(argv) {
393
+ const env = getArgValue(argv, "--env");
394
+ const envUrls = env ? ENV_URLS[env] : void 0;
395
+ if (env && !envUrls) {
396
+ console.error(`Unknown environment: ${env}`);
397
+ console.error("Valid environments: prod, dev, local");
398
+ process2.exit(1);
399
+ }
400
+ const apiUrl = getArgValue(argv, "--api-url") ?? envUrls?.api ?? process2.env.CANARY_API_URL ?? "https://api.trycanary.ai";
401
+ const appUrl = getArgValue(argv, "--app-url") ?? envUrls?.app ?? process2.env.CANARY_APP_URL ?? "https://app.trycanary.ai";
402
+ const orgFlag = getArgValue(argv, "--org");
403
+ const startRes = await fetch(`${apiUrl}/cli-login/start`, {
404
+ method: "POST",
405
+ headers: { "content-type": "application/json" },
406
+ body: JSON.stringify({ appUrl })
407
+ });
408
+ const startJson = await startRes.json();
409
+ if (!startRes.ok || !startJson.ok || !startJson.deviceCode || !startJson.userCode) {
410
+ console.error("Login start failed", startJson.error ?? startRes.statusText);
411
+ process2.exit(1);
412
+ }
413
+ console.log("Login required.");
414
+ console.log(`User code: ${startJson.userCode}`);
415
+ if (startJson.verificationUrl) {
416
+ console.log(`Open: ${startJson.verificationUrl}`);
417
+ if (shouldOpenBrowser(argv)) {
418
+ try {
419
+ openUrl(startJson.verificationUrl);
420
+ } catch {
421
+ console.log("Unable to open browser automatically. Please open the URL manually.");
422
+ }
423
+ }
424
+ }
425
+ const intervalMs = (startJson.intervalSeconds ?? 3) * 1e3;
426
+ const expiresAt = startJson.expiresAt ? new Date(startJson.expiresAt).getTime() : null;
427
+ let token;
428
+ let initialOrgId;
429
+ while (true) {
430
+ if (expiresAt && Date.now() > expiresAt) {
431
+ console.error("Login code expired.");
432
+ process2.exit(1);
433
+ }
434
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
435
+ const pollRes = await fetch(`${apiUrl}/cli-login/poll`, {
436
+ method: "POST",
437
+ headers: { "content-type": "application/json" },
438
+ body: JSON.stringify({ deviceCode: startJson.deviceCode })
439
+ });
440
+ const pollJson = await pollRes.json();
441
+ if (!pollRes.ok || !pollJson.ok) {
442
+ console.error("Login poll failed", pollJson.error ?? pollRes.statusText);
443
+ process2.exit(1);
444
+ }
445
+ if (pollJson.status === "approved" && pollJson.accessToken) {
446
+ token = pollJson.accessToken;
447
+ initialOrgId = pollJson.orgId;
448
+ break;
449
+ }
450
+ if (pollJson.status === "rejected") {
451
+ console.error("Login rejected.");
452
+ process2.exit(1);
453
+ }
454
+ if (pollJson.status === "expired") {
455
+ console.error("Login expired.");
456
+ process2.exit(1);
457
+ }
458
+ }
459
+ const orgsData = await fetchOrgs(apiUrl, token);
460
+ const orgs = orgsData?.organizations ?? [];
461
+ let finalToken = token;
462
+ let finalOrgId = initialOrgId;
463
+ let finalOrgName;
464
+ if (orgs.length <= 1) {
465
+ finalOrgName = orgs[0]?.name;
466
+ } else {
467
+ let selectedOrg;
468
+ if (orgFlag) {
469
+ selectedOrg = orgs.find(
470
+ (o) => o.name.toLowerCase() === orgFlag.toLowerCase() || o.id === orgFlag
471
+ );
472
+ if (!selectedOrg) {
473
+ console.error(`Organization "${orgFlag}" not found. Available orgs:`);
474
+ for (const o of orgs) {
475
+ console.error(` - ${o.name} (${o.id})`);
476
+ }
477
+ process2.exit(1);
478
+ }
479
+ } else if (process2.stdin.isTTY) {
480
+ console.log("\nYou belong to multiple organizations. Select one:");
481
+ for (let i = 0; i < orgs.length; i++) {
482
+ const marker = orgs[i].id === initialOrgId ? " (current)" : "";
483
+ console.log(` ${i + 1}. ${orgs[i].name}${marker}`);
484
+ }
485
+ const answer = await promptChoice(`
486
+ Choice [1-${orgs.length}]: `);
487
+ const idx = parseInt(answer, 10) - 1;
488
+ if (isNaN(idx) || idx < 0 || idx >= orgs.length) {
489
+ console.error("Invalid selection.");
490
+ process2.exit(1);
491
+ }
492
+ selectedOrg = orgs[idx];
493
+ } else {
494
+ const defaultOrg = orgs.find((o) => o.id === initialOrgId);
495
+ console.log(
496
+ `Warning: Multiple organizations available but running non-interactively. Using "${defaultOrg?.name ?? initialOrgId}".`
497
+ );
498
+ console.log("Tip: Use --org <name> to select a specific organization.");
499
+ selectedOrg = defaultOrg;
500
+ }
501
+ if (selectedOrg && selectedOrg.id !== initialOrgId) {
502
+ const switchRes = await switchOrg(apiUrl, token, selectedOrg.id);
503
+ if (!switchRes.ok || !switchRes.accessToken) {
504
+ console.error("Failed to switch organization:", switchRes.error ?? "Unknown error");
505
+ process2.exit(1);
506
+ }
507
+ finalToken = switchRes.accessToken;
508
+ finalOrgId = switchRes.orgId;
509
+ finalOrgName = switchRes.orgName;
510
+ } else if (selectedOrg) {
511
+ finalOrgName = selectedOrg.name;
512
+ }
513
+ }
514
+ const profileName = env ? envToProfile(env) : void 0;
515
+ const filePath = await saveAuth({ token: finalToken, apiUrl, orgId: finalOrgId, orgName: finalOrgName }, profileName);
516
+ const displayName = finalOrgName ? ` to ${finalOrgName}` : "";
517
+ const profileLabel = profileName ? ` (profile: ${profileName})` : "";
518
+ console.log(`Login successful${displayName}${profileLabel}. Token saved to ${filePath}`);
519
+ console.log("Set CANARY_API_TOKEN to use the CLI without re-login.");
520
+ }
521
+
522
+ // src/orgs.ts
523
+ import process3 from "process";
524
+ async function runOrgs(argv) {
525
+ const token = process3.env.CANARY_API_TOKEN ?? await readStoredToken();
526
+ if (!token) {
527
+ console.error("Not logged in. Run: canary login");
528
+ process3.exit(1);
529
+ }
530
+ const auth = await readStoredAuth();
531
+ const apiUrl = process3.env.CANARY_API_URL ?? auth?.apiUrl ?? "https://api.trycanary.ai";
532
+ const res = await fetch(`${apiUrl}/cli-login/orgs`, {
533
+ headers: { Authorization: `Bearer ${token}` }
534
+ });
535
+ if (!res.ok) {
536
+ console.error("Failed to fetch organizations. You may need to re-login: canary login");
537
+ process3.exit(1);
538
+ }
539
+ const data = await res.json();
540
+ if (!data.ok || !data.organizations) {
541
+ console.error("Failed to fetch organizations:", data.error ?? "Unknown error");
542
+ process3.exit(1);
543
+ }
544
+ const currentOrgId = auth?.orgId ?? data.currentOrgId;
545
+ console.log("Organizations:\n");
546
+ for (const org of data.organizations) {
547
+ const marker = org.id === currentOrgId ? " *" : "";
548
+ console.log(` ${org.name} (${org.role})${marker}`);
549
+ }
550
+ console.log("\n* = current organization");
551
+ console.log("To switch: canary login --org <name>");
552
+ }
553
+
554
+ // src/run-local.ts
555
+ import process4 from "process";
556
+ function getArgValue2(argv, key) {
557
+ const index = argv.indexOf(key);
558
+ if (index === -1) return void 0;
559
+ return argv[index + 1];
560
+ }
561
+ async function runLocalSession(argv) {
562
+ const apiUrl = getArgValue2(argv, "--api-url") ?? process4.env.CANARY_API_URL ?? "https://api.trycanary.ai";
563
+ const token = getArgValue2(argv, "--token") ?? process4.env.CANARY_API_TOKEN ?? await readStoredToken();
564
+ if (!token) {
565
+ console.error("Missing token. Run `canary login` first or set CANARY_API_TOKEN.");
566
+ process4.exit(1);
567
+ }
568
+ const portRaw = getArgValue2(argv, "--port") ?? process4.env.CANARY_LOCAL_PORT;
569
+ const tunnelUrl = getArgValue2(argv, "--tunnel-url");
570
+ const title = getArgValue2(argv, "--title");
571
+ const featureSpec = getArgValue2(argv, "--feature");
572
+ const startUrl = getArgValue2(argv, "--start-url");
573
+ if (!tunnelUrl && !portRaw) {
574
+ console.error("Missing --port or --tunnel-url");
575
+ process4.exit(1);
576
+ }
577
+ let publicUrl = tunnelUrl;
578
+ let ws = null;
579
+ if (!publicUrl && portRaw) {
580
+ const port = Number(portRaw);
581
+ if (Number.isNaN(port) || port <= 0) {
582
+ console.error("Invalid --port value");
583
+ process4.exit(1);
584
+ }
585
+ const tunnel = await createTunnel({ apiUrl, token, port });
586
+ publicUrl = tunnel.publicUrl;
587
+ ws = connectTunnel({
588
+ apiUrl,
589
+ tunnelId: tunnel.tunnelId,
590
+ token: tunnel.token,
591
+ port,
592
+ onReady: () => {
593
+ console.log(`Tunnel connected: ${publicUrl ?? tunnel.tunnelId}`);
594
+ }
595
+ });
596
+ }
597
+ if (!publicUrl) {
598
+ console.error("Failed to resolve tunnel URL");
599
+ process4.exit(1);
600
+ }
601
+ const run2 = await createLocalRun({
602
+ apiUrl,
603
+ token,
604
+ title,
605
+ featureSpec,
606
+ startUrl,
607
+ tunnelUrl: publicUrl
608
+ });
609
+ console.log(`Local test queued: ${run2.runId}`);
610
+ if (run2.watchUrl) {
611
+ console.log(`Watch: ${run2.watchUrl}`);
612
+ }
613
+ if (ws) {
614
+ console.log("Tunnel active. Press Ctrl+C to stop.");
615
+ process4.on("SIGINT", () => {
616
+ ws?.close();
617
+ process4.exit(0);
618
+ });
619
+ await new Promise(() => void 0);
620
+ }
621
+ }
622
+
623
+ // src/remote-test.ts
624
+ import process5 from "process";
625
+ import { createParser } from "eventsource-parser";
626
+ function getArgValue3(argv, key) {
627
+ const index = argv.indexOf(key);
628
+ if (index === -1 || index >= argv.length - 1) return void 0;
629
+ return argv[index + 1];
630
+ }
631
+ function hasFlag(argv, ...flags) {
632
+ return flags.some((flag) => argv.includes(flag));
633
+ }
634
+ function formatFailedTests(failedTests, appUrl) {
635
+ if (failedTests.length === 0 || !appUrl) return null;
636
+ const lines = ["", "Failed tests:"];
637
+ for (const t of failedTests) {
638
+ lines.push(` \u2717 ${t.name}`);
639
+ lines.push(` ${appUrl}/runs/tests/${t.testRunId}`);
640
+ }
641
+ return lines.join("\n");
642
+ }
643
+ async function runRemoteTest(argv) {
644
+ const apiUrl = getArgValue3(argv, "--api-url") ?? process5.env.CANARY_API_URL ?? "https://api.trycanary.ai";
645
+ const token = getArgValue3(argv, "--token") ?? process5.env.CANARY_API_TOKEN ?? await readStoredToken();
646
+ const tag = getArgValue3(argv, "--tag");
647
+ const namePattern = getArgValue3(argv, "--name-pattern");
648
+ const verbose = hasFlag(argv, "--verbose", "-v");
649
+ if (!token) {
650
+ console.error("Error: No API token found.");
651
+ console.error("");
652
+ console.error("Set CANARY_API_TOKEN environment variable or run:");
653
+ console.error(" canary login");
654
+ console.error("");
655
+ console.error("Or create an API key in Settings > API Keys and pass it:");
656
+ console.error(" canary test --remote --token cnry_...");
657
+ process5.exit(1);
658
+ }
659
+ console.log("Starting remote workflow tests...");
660
+ if (tag) console.log(` Filtering by tag: ${tag}`);
661
+ if (namePattern) console.log(` Filtering by name pattern: ${namePattern}`);
662
+ console.log("");
663
+ const queryParams = new URLSearchParams();
664
+ if (tag) queryParams.set("tag", tag);
665
+ if (namePattern) queryParams.set("namePattern", namePattern);
666
+ const triggerUrl = `${apiUrl}/workflows/test-runs${queryParams.toString() ? `?${queryParams}` : ""}`;
667
+ let triggerRes;
668
+ try {
669
+ triggerRes = await fetch(triggerUrl, {
670
+ method: "POST",
671
+ headers: {
672
+ Authorization: `Bearer ${token}`,
673
+ "Content-Type": "application/json"
674
+ }
675
+ });
676
+ } catch (err) {
677
+ console.error(`Failed to connect to API: ${err}`);
678
+ process5.exit(1);
679
+ }
680
+ if (!triggerRes.ok) {
681
+ const errorText = await triggerRes.text();
682
+ console.error(`Failed to start tests: ${triggerRes.status}`);
683
+ console.error(errorText);
684
+ process5.exit(1);
685
+ }
686
+ const triggerData = await triggerRes.json();
687
+ if (!triggerData.ok || !triggerData.suiteId) {
688
+ console.error(`Failed to start tests: ${triggerData.error ?? "Unknown error"}`);
689
+ process5.exit(1);
690
+ }
691
+ const { suiteId, jobId, appUrl } = triggerData;
692
+ if (verbose) {
693
+ console.log(`Suite ID: ${suiteId}`);
694
+ console.log(`Job ID: ${jobId}`);
695
+ console.log("");
696
+ }
697
+ const streamUrl = `${apiUrl}/workflows/test-runs/stream?suiteId=${suiteId}`;
698
+ let streamRes;
699
+ try {
700
+ streamRes = await fetch(streamUrl, {
701
+ headers: {
702
+ Authorization: `Bearer ${token}`,
703
+ Accept: "text/event-stream"
704
+ }
705
+ });
706
+ } catch (err) {
707
+ console.error(`Failed to connect to event stream: ${err}`);
708
+ process5.exit(1);
709
+ }
710
+ if (!streamRes.ok || !streamRes.body) {
711
+ console.error(`Failed to connect to event stream: ${streamRes.status}`);
712
+ process5.exit(1);
713
+ }
714
+ let exitCode = 0;
715
+ let hasCompleted = false;
716
+ const workflowNames = /* @__PURE__ */ new Map();
717
+ const failedTests = [];
718
+ let totalWorkflows = 0;
719
+ let completedWorkflows = 0;
720
+ let failedWorkflows = 0;
721
+ let successfulWorkflows = 0;
722
+ const parser = createParser({
723
+ onEvent: (event) => {
724
+ if (!event.data) return;
725
+ try {
726
+ const data = JSON.parse(event.data);
727
+ if (verbose) {
728
+ console.log(`[${event.event}] ${JSON.stringify(data)}`);
729
+ }
730
+ if (event.event === "workflow-test") {
731
+ const testEvent = data;
732
+ const { status, workflowId, message, errorMessage } = testEvent;
733
+ const name = workflowNames.get(workflowId) || message?.replace(/^Flow "(.+)" .*$/, "$1") || workflowId;
734
+ if (message?.startsWith('Flow "')) {
735
+ const match = message.match(/^Flow "(.+?)" /);
736
+ if (match) workflowNames.set(workflowId, match[1]);
737
+ }
738
+ if (!verbose) {
739
+ if (status === "success") {
740
+ console.log(` \u2713 ${name}`);
741
+ } else if (status === "failed") {
742
+ console.log(` \u2717 ${name}`);
743
+ if (errorMessage) {
744
+ console.log(` Error: ${errorMessage.slice(0, 200)}`);
745
+ }
746
+ failedTests.push({ name, testRunId: testEvent.testRunId });
747
+ exitCode = 1;
748
+ } else if (status === "running") {
749
+ } else if (status === "waiting") {
750
+ console.log(` \u23F3 ${name} (waiting for scheduled time)`);
751
+ }
752
+ }
753
+ }
754
+ if (event.event === "workflow-test-suite") {
755
+ const suiteEvent = data;
756
+ totalWorkflows = suiteEvent.totalWorkflows;
757
+ completedWorkflows = suiteEvent.completedWorkflows;
758
+ failedWorkflows = suiteEvent.failedWorkflows;
759
+ successfulWorkflows = suiteEvent.successfulWorkflows;
760
+ if (suiteEvent.status === "completed") {
761
+ hasCompleted = true;
762
+ }
763
+ }
764
+ if (event.event === "error") {
765
+ const errorData = data;
766
+ console.error(`Stream error: ${errorData.error ?? "Unknown error"}`);
767
+ exitCode = 1;
768
+ }
769
+ } catch {
770
+ }
771
+ }
772
+ });
773
+ const reader = streamRes.body.getReader();
774
+ const decoder = new TextDecoder();
775
+ try {
776
+ while (!hasCompleted) {
777
+ const { done, value } = await reader.read();
778
+ if (done) break;
779
+ parser.feed(decoder.decode(value, { stream: true }));
780
+ }
781
+ } finally {
782
+ reader.releaseLock();
783
+ }
784
+ console.log("");
785
+ console.log("\u2500".repeat(50));
786
+ if (totalWorkflows === 0) {
787
+ console.log("No workflows found matching the filter criteria.");
788
+ process5.exit(0);
789
+ }
790
+ const passRate = totalWorkflows > 0 ? Math.round(successfulWorkflows / totalWorkflows * 100) : 0;
791
+ if (failedWorkflows > 0) {
792
+ console.log(`FAILED: ${failedWorkflows} of ${totalWorkflows} workflows failed (${passRate}% pass rate)`);
793
+ exitCode = 1;
794
+ } else {
795
+ console.log(`PASSED: ${successfulWorkflows} of ${totalWorkflows} workflows passed`);
796
+ }
797
+ const waitingWorkflows = totalWorkflows - completedWorkflows;
798
+ if (waitingWorkflows > 0) {
799
+ console.log(`Note: ${waitingWorkflows} workflow(s) are still waiting (scheduled for later)`);
800
+ }
801
+ const failedSection = formatFailedTests(failedTests, appUrl);
802
+ if (failedSection) {
803
+ console.log(failedSection);
804
+ }
805
+ process5.exit(exitCode);
806
+ }
807
+
808
+ // src/debug-session.ts
809
+ import fs3 from "fs/promises";
810
+ import os2 from "os";
811
+ import path3 from "path";
812
+ import process6 from "process";
813
+ var ENV_URLS2 = {
814
+ prod: {
815
+ api: "https://api.trycanary.ai",
816
+ app: "https://app.trycanary.ai"
817
+ },
818
+ production: {
819
+ api: "https://api.trycanary.ai",
820
+ app: "https://app.trycanary.ai"
821
+ },
822
+ dev: {
823
+ api: "https://api.dev.trycanary.ai",
824
+ app: "https://app.dev.trycanary.ai"
825
+ },
826
+ local: {
827
+ api: "http://localhost:3000",
828
+ app: "http://localhost:5173"
829
+ }
830
+ };
831
+ function getArgValue4(argv, key) {
832
+ const index = argv.indexOf(key);
833
+ if (index === -1 || index >= argv.length - 1) return void 0;
834
+ return argv[index + 1];
835
+ }
836
+ function hasFlag2(argv, ...flags) {
837
+ return flags.some((flag) => argv.includes(flag));
838
+ }
839
+ async function writeDebugSession(loginUrl, expiresAt, apiUrl) {
840
+ const dir = path3.join(os2.homedir(), ".config", "canary-cli");
841
+ const filePath = path3.join(dir, "debug-session.json");
842
+ await fs3.mkdir(dir, { recursive: true, mode: 448 });
843
+ await fs3.writeFile(
844
+ filePath,
845
+ JSON.stringify({ loginUrl, expiresAt, apiUrl, createdAt: (/* @__PURE__ */ new Date()).toISOString() }, null, 2),
846
+ { encoding: "utf8", mode: 384 }
847
+ );
848
+ return filePath;
849
+ }
850
+ async function runDebugSession(argv) {
851
+ const env = getArgValue4(argv, "--env");
852
+ const envUrls = env ? ENV_URLS2[env] : void 0;
853
+ if (env && !envUrls) {
854
+ console.error(`Unknown environment: ${env}`);
855
+ console.error("Valid environments: prod, dev, local");
856
+ process6.exit(1);
857
+ }
858
+ const storedApiUrl = await readStoredApiUrl();
859
+ const apiUrl = getArgValue4(argv, "--api-url") ?? envUrls?.api ?? process6.env.CANARY_API_URL ?? storedApiUrl ?? "https://api.trycanary.ai";
860
+ const token = getArgValue4(argv, "--token") ?? process6.env.CANARY_API_TOKEN ?? await readStoredToken();
861
+ const jsonOutput = hasFlag2(argv, "--json");
862
+ if (!token) {
863
+ console.error("Error: No API token found.");
864
+ console.error("Run: canary login [--env <env>]");
865
+ process6.exit(1);
866
+ }
867
+ try {
868
+ const res = await fetch(`${apiUrl}/auth/debug-session/create`, {
869
+ method: "POST",
870
+ headers: {
871
+ Authorization: `Bearer ${token}`,
872
+ "Content-Type": "application/json"
873
+ }
874
+ });
875
+ if (!res.ok) {
876
+ const text = await res.text();
877
+ if (res.status === 401) {
878
+ console.error("Error: Unauthorized. Your session may have expired.");
879
+ console.error("Run: canary login");
880
+ process6.exit(1);
881
+ }
882
+ if (res.status === 403) {
883
+ console.error("Error: Forbidden. Debug session creation requires superadmin access.");
884
+ process6.exit(1);
885
+ }
886
+ if (res.status === 404) {
887
+ console.error(
888
+ "Error: Endpoint not found. The debug-session feature may not be deployed to this environment."
889
+ );
890
+ process6.exit(1);
891
+ }
892
+ try {
893
+ const errorJson = JSON.parse(text);
894
+ console.error(`Error: ${errorJson.message ?? errorJson.error ?? text}`);
895
+ } catch {
896
+ console.error(`Error (${res.status}): ${text || res.statusText}`);
897
+ }
898
+ process6.exit(1);
899
+ }
900
+ const json = await res.json();
901
+ if (!json.ok || !json.loginUrl) {
902
+ console.error(`Error: ${json.message ?? json.error ?? "Failed to create debug session"}`);
903
+ process6.exit(1);
904
+ }
905
+ const filePath = await writeDebugSession(json.loginUrl, json.expiresAt ?? "", apiUrl);
906
+ if (jsonOutput) {
907
+ console.log(
908
+ JSON.stringify(
909
+ {
910
+ loginUrl: json.loginUrl,
911
+ expiresAt: json.expiresAt,
912
+ sessionFile: filePath
913
+ },
914
+ null,
915
+ 2
916
+ )
917
+ );
918
+ } else {
919
+ console.log("Debug session created successfully.");
920
+ console.log("");
921
+ console.log(`Login URL: ${json.loginUrl}`);
922
+ console.log(`Expires: ${json.expiresAt}`);
923
+ console.log(`Session saved to: ${filePath}`);
924
+ console.log("");
925
+ console.log("Use this URL with Playwright to authenticate as the debug agent.");
926
+ console.log("The token is single-use and expires in 5 minutes.");
927
+ }
928
+ } catch (err) {
929
+ console.error(`Failed to create debug session: ${err}`);
930
+ process6.exit(1);
931
+ }
932
+ }
933
+
934
+ // src/jwt.ts
935
+ function decodeJwtPayload(token) {
936
+ try {
937
+ const parts = token.split(".");
938
+ if (parts.length !== 3) return null;
939
+ const base64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
940
+ const json = Buffer.from(base64, "base64").toString("utf8");
941
+ const payload = JSON.parse(json);
942
+ if (typeof payload !== "object" || payload === null || Array.isArray(payload)) {
943
+ return null;
944
+ }
945
+ return payload;
946
+ } catch {
947
+ return null;
948
+ }
949
+ }
950
+ function isSuperadminToken(token) {
951
+ if (!token) return false;
952
+ const payload = decodeJwtPayload(token);
953
+ return payload?.is_superadmin === true;
954
+ }
955
+
956
+ // src/index.ts
957
+ var require2 = createRequire2(import.meta.url);
958
+ var pkg = require2("../package.json");
959
+ var loadMcp = () => import("./mcp-I6FCGDDR.js").then((m) => m.runMcp);
960
+ var loadLocalBrowser = () => import("./local-browser-VOBIUIGT.js").then((m) => m.runLocalBrowser);
961
+ var canary = { run };
962
+ var baseDir = typeof __dirname !== "undefined" ? __dirname : path4.dirname(fileURLToPath2(import.meta.url));
963
+ var preloadPath = path4.join(baseDir, "runner", "preload.js");
964
+ var requireFn = makeRequire();
965
+ function runPlaywrightTests(args) {
966
+ const playwrightCli = requireFn.resolve("@playwright/test/cli");
967
+ const { runnerBin, preloadFlag } = resolveRunner(preloadPath);
968
+ const nodeOptions = process7.env.NODE_OPTIONS && preloadFlag ? `${process7.env.NODE_OPTIONS} ${preloadFlag}` : preloadFlag ?? process7.env.NODE_OPTIONS;
969
+ const env = {
970
+ ...process7.env,
971
+ CANARY_ENABLED: process7.env.CANARY_ENABLED ?? "1",
972
+ CANARY_RUNNER: "canary",
973
+ ...nodeOptions ? { NODE_OPTIONS: nodeOptions } : {}
974
+ };
975
+ const result = spawnSync2(runnerBin, [playwrightCli, "test", ...args], {
976
+ env,
977
+ stdio: "inherit",
978
+ cwd: process7.cwd()
979
+ });
980
+ if (result.error) {
981
+ console.error("canary failed to launch Playwright:", result.error);
982
+ process7.exit(1);
983
+ }
984
+ process7.exit(result.status ?? 1);
985
+ }
986
+ function printVersion() {
987
+ console.log(`canary v${pkg.version}`);
988
+ }
989
+ function printHelp({ isSuperadmin }) {
990
+ const lines = [
991
+ `canary v${pkg.version}: Local and remote testing CLI`,
992
+ "",
993
+ "Usage:",
994
+ " canary test [playwright options] Run local Playwright tests",
995
+ " canary test --remote [options] Run remote workflow tests",
996
+ " canary local-run --tunnel-url <url> [options]",
997
+ " canary tunnel --port <localPort> [options]",
998
+ " canary run --port <localPort> [options]",
999
+ " canary mcp",
1000
+ " canary browser [--mode playwright|cdp] [--cdp-url <url>] [--no-headless]",
1001
+ " canary login [--org <name>] [--app-url https://app.trycanary.ai] [--no-open]",
1002
+ " canary orgs List organizations"
1003
+ ];
1004
+ if (isSuperadmin) {
1005
+ lines.push(
1006
+ " canary debug-session [--env dev|local] [--json] Create browser debug session",
1007
+ " canary psql <query> [--json] Execute read-only SQL",
1008
+ " canary redis <command> [--json] Execute read-only Redis commands",
1009
+ " canary feature-flag <sub-command> Manage feature flags",
1010
+ " canary knobs <sub-command> Manage knobs (global config)"
1011
+ );
1012
+ }
1013
+ lines.push(
1014
+ " canary version Show version",
1015
+ " canary help",
1016
+ "",
1017
+ "Remote test options:",
1018
+ " --token <key> API key (or set CANARY_API_TOKEN)",
1019
+ " --api-url <url> API URL (default: https://api.trycanary.ai)",
1020
+ " --tag <tag> Filter workflows by tag",
1021
+ " --name-pattern <pat> Filter workflows by name pattern",
1022
+ " --verbose, -v Show all events",
1023
+ "",
1024
+ "Browser options:",
1025
+ " --mode <playwright|cdp> Browser mode (default: playwright)",
1026
+ " --cdp-url <url> CDP endpoint for existing Chrome",
1027
+ " --no-headless Run browser with visible UI",
1028
+ " --storage-state <path> Path to storage state JSON",
1029
+ " --instructions <text> Instructions for the cloud agent",
1030
+ "",
1031
+ "Login options:",
1032
+ " --org <name> Select organization by name or ID (for multi-org users)",
1033
+ "",
1034
+ "Login environments:",
1035
+ " Production: canary login",
1036
+ " Dev: canary login --app-url https://app.dev.trycanary.ai --api-url https://api.dev.trycanary.ai",
1037
+ " Local: canary login --app-url http://localhost:5173 --api-url http://localhost:3000",
1038
+ "",
1039
+ " Or set CANARY_API_URL env var for non-production environments:",
1040
+ " export CANARY_API_URL=http://localhost:3000"
1041
+ );
1042
+ if (isSuperadmin) {
1043
+ lines.push(
1044
+ "",
1045
+ "PSQL options:",
1046
+ " --json Output results as JSON",
1047
+ " --query <sql> SQL query (alternative to positional)",
1048
+ "",
1049
+ "Redis options:",
1050
+ " --json Output results as JSON",
1051
+ "",
1052
+ "Feature flag sub-commands:",
1053
+ " list List all flags",
1054
+ " create <name> [--description <text>] Create a flag",
1055
+ " delete <name> Delete a flag and its gates",
1056
+ " enable <name> --org <orgId> Enable for an org",
1057
+ " disable <name> --org <orgId> Disable for an org",
1058
+ "",
1059
+ "Knobs sub-commands:",
1060
+ " list List all knobs",
1061
+ " get <key> Get a knob value",
1062
+ " set <key> <value> --type <type> Set a knob value",
1063
+ " delete <key> Delete a knob",
1064
+ " toggle <key> Toggle a boolean knob"
1065
+ );
1066
+ }
1067
+ lines.push(
1068
+ "",
1069
+ "Flags:",
1070
+ " -h, --help Show help",
1071
+ " -V, --version Show version"
1072
+ );
1073
+ console.log(lines.join("\n"));
1074
+ }
1075
+ function printTestHelp() {
1076
+ console.log(
1077
+ [
1078
+ `canary v${pkg.version}: Test command`,
1079
+ "",
1080
+ "Usage:",
1081
+ " canary test [playwright options] Run local Playwright tests",
1082
+ " canary test --remote [options] Run remote workflow tests",
1083
+ "",
1084
+ "Local Playwright options (passed through to Playwright):",
1085
+ " --grep <pattern> Only run tests matching pattern",
1086
+ " --headed Run in headed browser mode",
1087
+ " --workers <n> Number of parallel workers",
1088
+ " --project <name> Run specific project",
1089
+ " --reporter <reporter> Use a specific reporter",
1090
+ " --retries <n> Number of retries for failed tests",
1091
+ " --timeout <ms> Test timeout in milliseconds",
1092
+ "",
1093
+ "Remote test options:",
1094
+ " --remote Run tests remotely (required)",
1095
+ " --token <key> API key (or set CANARY_API_TOKEN)",
1096
+ " --api-url <url> API URL (default: https://api.trycanary.ai)",
1097
+ " --tag <tag> Filter workflows by tag",
1098
+ " --name-pattern <pat> Filter workflows by name pattern",
1099
+ " --verbose, -v Show all events",
1100
+ "",
1101
+ "Examples:",
1102
+ " canary test Run all local tests",
1103
+ ' canary test --grep "login" Run tests matching "login"',
1104
+ " canary test --headed --workers 1 Debug with visible browser",
1105
+ " canary test --remote --tag smoke Run remote smoke tests"
1106
+ ].join("\n")
1107
+ );
1108
+ }
1109
+ var COMMANDS_WITH_HELP = /* @__PURE__ */ new Set(["test"]);
1110
+ async function resolveToken() {
1111
+ return process7.env.CANARY_API_TOKEN ?? await readStoredToken();
1112
+ }
1113
+ async function main(argv) {
1114
+ if (argv.includes("--version") || argv.includes("-V")) {
1115
+ printVersion();
1116
+ return;
1117
+ }
1118
+ const [command, ...rest] = argv;
1119
+ const hasHelpFlag = argv.includes("--help") || argv.includes("-h");
1120
+ if (hasHelpFlag && (!command || !COMMANDS_WITH_HELP.has(command))) {
1121
+ const token2 = await resolveToken();
1122
+ printHelp({ isSuperadmin: isSuperadminToken(token2) });
1123
+ return;
1124
+ }
1125
+ if (!command || command === "help") {
1126
+ const token2 = await resolveToken();
1127
+ printHelp({ isSuperadmin: isSuperadminToken(token2) });
1128
+ return;
1129
+ }
1130
+ if (command === "version") {
1131
+ printVersion();
1132
+ return;
1133
+ }
1134
+ if (command === "test") {
1135
+ if (rest.includes("--help") || rest.includes("-h")) {
1136
+ printTestHelp();
1137
+ return;
1138
+ }
1139
+ if (rest.includes("--remote")) {
1140
+ const remoteArgs = rest.filter((arg) => arg !== "--remote");
1141
+ await runRemoteTest(remoteArgs);
1142
+ return;
1143
+ }
1144
+ runPlaywrightTests(rest);
1145
+ return;
1146
+ }
1147
+ if (command === "local-run") {
1148
+ await runLocalTest(rest);
1149
+ return;
1150
+ }
1151
+ if (command === "run") {
1152
+ await runLocalSession(rest);
1153
+ return;
1154
+ }
1155
+ if (command === "mcp") {
1156
+ const runMcp = await loadMcp();
1157
+ await runMcp(rest);
1158
+ return;
1159
+ }
1160
+ if (command === "tunnel") {
1161
+ await runTunnel(rest);
1162
+ return;
1163
+ }
1164
+ if (command === "login") {
1165
+ await runLogin(rest);
1166
+ return;
1167
+ }
1168
+ if (command === "orgs") {
1169
+ await runOrgs(rest);
1170
+ return;
1171
+ }
1172
+ if (command === "browser") {
1173
+ const runLocalBrowser = await loadLocalBrowser();
1174
+ await runLocalBrowser(rest);
1175
+ return;
1176
+ }
1177
+ if (command === "debug-session") {
1178
+ await runDebugSession(rest);
1179
+ return;
1180
+ }
1181
+ if (command === "psql") {
1182
+ const { runPsql } = await import("./psql-A3BADRQN.js");
1183
+ await runPsql(rest);
1184
+ return;
1185
+ }
1186
+ if (command === "redis") {
1187
+ const { runRedis } = await import("./redis-N2DSDDQU.js");
1188
+ await runRedis(rest);
1189
+ return;
1190
+ }
1191
+ if (command === "feature-flag") {
1192
+ const { runFeatureFlag } = await import("./feature-flag-PN5IFFQR.js");
1193
+ await runFeatureFlag(rest);
1194
+ return;
1195
+ }
1196
+ if (command === "knobs") {
1197
+ const { runKnobs } = await import("./knobs-DAG7HD2F.js");
1198
+ await runKnobs(rest);
1199
+ return;
1200
+ }
1201
+ console.log(`Unknown command "${command}".`);
1202
+ const token = await resolveToken();
1203
+ printHelp({ isSuperadmin: isSuperadminToken(token) });
1204
+ process7.exit(1);
1205
+ }
1206
+ if (import.meta.url === pathToFileURL2(process7.argv[1]).href) {
1207
+ void main(process7.argv.slice(2));
1208
+ }
10
1209
  export {
11
1210
  canary,
12
1211
  main,