@hasna/testers 0.0.5 → 0.0.7

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/cli/index.js CHANGED
@@ -2090,6 +2090,7 @@ function scenarioFromRow(row) {
2090
2090
  requiresAuth: row.requires_auth === 1,
2091
2091
  authConfig: row.auth_config ? JSON.parse(row.auth_config) : null,
2092
2092
  metadata: row.metadata ? JSON.parse(row.metadata) : null,
2093
+ assertions: JSON.parse(row.assertions || "[]"),
2093
2094
  version: row.version,
2094
2095
  createdAt: row.created_at,
2095
2096
  updatedAt: row.updated_at
@@ -2109,7 +2110,8 @@ function runFromRow(row) {
2109
2110
  failed: row.failed,
2110
2111
  startedAt: row.started_at,
2111
2112
  finishedAt: row.finished_at,
2112
- metadata: row.metadata ? JSON.parse(row.metadata) : null
2113
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
2114
+ isBaseline: row.is_baseline === 1
2113
2115
  };
2114
2116
  }
2115
2117
  function resultFromRow(row) {
@@ -2165,7 +2167,18 @@ function scheduleFromRow(row) {
2165
2167
  updatedAt: row.updated_at
2166
2168
  };
2167
2169
  }
2168
- var MODEL_MAP, VersionConflictError, BrowserError, AIClientError, TodosConnectionError, ScheduleNotFoundError;
2170
+ function flowFromRow(row) {
2171
+ return {
2172
+ id: row.id,
2173
+ projectId: row.project_id,
2174
+ name: row.name,
2175
+ description: row.description,
2176
+ scenarioIds: JSON.parse(row.scenario_ids),
2177
+ createdAt: row.created_at,
2178
+ updatedAt: row.updated_at
2179
+ };
2180
+ }
2181
+ var MODEL_MAP, VersionConflictError, BrowserError, AIClientError, TodosConnectionError, ScheduleNotFoundError, DependencyCycleError;
2169
2182
  var init_types = __esm(() => {
2170
2183
  MODEL_MAP = {
2171
2184
  quick: "claude-haiku-4-5-20251001",
@@ -2202,6 +2215,12 @@ var init_types = __esm(() => {
2202
2215
  this.name = "ScheduleNotFoundError";
2203
2216
  }
2204
2217
  };
2218
+ DependencyCycleError = class DependencyCycleError extends Error {
2219
+ constructor(scenarioId, dependsOn) {
2220
+ super(`Adding dependency ${dependsOn} to ${scenarioId} would create a cycle`);
2221
+ this.name = "DependencyCycleError";
2222
+ }
2223
+ };
2205
2224
  });
2206
2225
 
2207
2226
  // src/db/database.ts
@@ -2425,166 +2444,51 @@ var init_database = __esm(() => {
2425
2444
  created_at TEXT NOT NULL DEFAULT (datetime('now'))
2426
2445
  );
2427
2446
  CREATE INDEX IF NOT EXISTS idx_webhooks_active ON webhooks(active);
2447
+ `,
2448
+ `
2449
+ CREATE TABLE IF NOT EXISTS scenario_dependencies (
2450
+ scenario_id TEXT NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
2451
+ depends_on TEXT NOT NULL REFERENCES scenarios(id) ON DELETE CASCADE,
2452
+ PRIMARY KEY (scenario_id, depends_on),
2453
+ CHECK (scenario_id != depends_on)
2454
+ );
2455
+
2456
+ CREATE TABLE IF NOT EXISTS flows (
2457
+ id TEXT PRIMARY KEY,
2458
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
2459
+ name TEXT NOT NULL,
2460
+ description TEXT,
2461
+ scenario_ids TEXT NOT NULL DEFAULT '[]',
2462
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
2463
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
2464
+ );
2465
+
2466
+ CREATE INDEX IF NOT EXISTS idx_deps_scenario ON scenario_dependencies(scenario_id);
2467
+ CREATE INDEX IF NOT EXISTS idx_deps_depends ON scenario_dependencies(depends_on);
2468
+ CREATE INDEX IF NOT EXISTS idx_flows_project ON flows(project_id);
2469
+ `,
2470
+ `
2471
+ ALTER TABLE scenarios ADD COLUMN assertions TEXT DEFAULT '[]';
2472
+ `,
2473
+ `
2474
+ CREATE TABLE IF NOT EXISTS environments (
2475
+ id TEXT PRIMARY KEY,
2476
+ name TEXT NOT NULL UNIQUE,
2477
+ url TEXT NOT NULL,
2478
+ auth_preset_name TEXT,
2479
+ project_id TEXT REFERENCES projects(id) ON DELETE CASCADE,
2480
+ is_default INTEGER NOT NULL DEFAULT 0,
2481
+ metadata TEXT DEFAULT '{}',
2482
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
2483
+ );
2484
+ `,
2485
+ `
2486
+ ALTER TABLE runs ADD COLUMN is_baseline INTEGER NOT NULL DEFAULT 0;
2428
2487
  `
2429
2488
  ];
2430
2489
  });
2431
2490
 
2432
- // src/db/runs.ts
2433
- var exports_runs = {};
2434
- __export(exports_runs, {
2435
- updateRun: () => updateRun,
2436
- listRuns: () => listRuns,
2437
- getRun: () => getRun,
2438
- deleteRun: () => deleteRun,
2439
- createRun: () => createRun
2440
- });
2441
- function createRun(input) {
2442
- const db2 = getDatabase();
2443
- const id = uuid();
2444
- const timestamp = now();
2445
- db2.query(`
2446
- INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata)
2447
- VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?)
2448
- `).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp, input.model ? JSON.stringify({}) : null);
2449
- return getRun(id);
2450
- }
2451
- function getRun(id) {
2452
- const db2 = getDatabase();
2453
- let row = db2.query("SELECT * FROM runs WHERE id = ?").get(id);
2454
- if (row)
2455
- return runFromRow(row);
2456
- const fullId = resolvePartialId("runs", id);
2457
- if (fullId) {
2458
- row = db2.query("SELECT * FROM runs WHERE id = ?").get(fullId);
2459
- if (row)
2460
- return runFromRow(row);
2461
- }
2462
- return null;
2463
- }
2464
- function listRuns(filter) {
2465
- const db2 = getDatabase();
2466
- const conditions = [];
2467
- const params = [];
2468
- if (filter?.projectId) {
2469
- conditions.push("project_id = ?");
2470
- params.push(filter.projectId);
2471
- }
2472
- if (filter?.status) {
2473
- conditions.push("status = ?");
2474
- params.push(filter.status);
2475
- }
2476
- let sql = "SELECT * FROM runs";
2477
- if (conditions.length > 0) {
2478
- sql += " WHERE " + conditions.join(" AND ");
2479
- }
2480
- sql += " ORDER BY started_at DESC";
2481
- if (filter?.limit) {
2482
- sql += " LIMIT ?";
2483
- params.push(filter.limit);
2484
- }
2485
- if (filter?.offset) {
2486
- sql += " OFFSET ?";
2487
- params.push(filter.offset);
2488
- }
2489
- const rows = db2.query(sql).all(...params);
2490
- return rows.map(runFromRow);
2491
- }
2492
- function updateRun(id, updates) {
2493
- const db2 = getDatabase();
2494
- const existing = getRun(id);
2495
- if (!existing) {
2496
- throw new Error(`Run not found: ${id}`);
2497
- }
2498
- const sets = [];
2499
- const params = [];
2500
- if (updates.status !== undefined) {
2501
- sets.push("status = ?");
2502
- params.push(updates.status);
2503
- }
2504
- if (updates.url !== undefined) {
2505
- sets.push("url = ?");
2506
- params.push(updates.url);
2507
- }
2508
- if (updates.model !== undefined) {
2509
- sets.push("model = ?");
2510
- params.push(updates.model);
2511
- }
2512
- if (updates.headed !== undefined) {
2513
- sets.push("headed = ?");
2514
- params.push(updates.headed);
2515
- }
2516
- if (updates.parallel !== undefined) {
2517
- sets.push("parallel = ?");
2518
- params.push(updates.parallel);
2519
- }
2520
- if (updates.total !== undefined) {
2521
- sets.push("total = ?");
2522
- params.push(updates.total);
2523
- }
2524
- if (updates.passed !== undefined) {
2525
- sets.push("passed = ?");
2526
- params.push(updates.passed);
2527
- }
2528
- if (updates.failed !== undefined) {
2529
- sets.push("failed = ?");
2530
- params.push(updates.failed);
2531
- }
2532
- if (updates.started_at !== undefined) {
2533
- sets.push("started_at = ?");
2534
- params.push(updates.started_at);
2535
- }
2536
- if (updates.finished_at !== undefined) {
2537
- sets.push("finished_at = ?");
2538
- params.push(updates.finished_at);
2539
- }
2540
- if (updates.metadata !== undefined) {
2541
- sets.push("metadata = ?");
2542
- params.push(updates.metadata);
2543
- }
2544
- if (sets.length === 0) {
2545
- return existing;
2546
- }
2547
- params.push(existing.id);
2548
- db2.query(`UPDATE runs SET ${sets.join(", ")} WHERE id = ?`).run(...params);
2549
- return getRun(existing.id);
2550
- }
2551
- function deleteRun(id) {
2552
- const db2 = getDatabase();
2553
- const run = getRun(id);
2554
- if (!run)
2555
- return false;
2556
- const result = db2.query("DELETE FROM runs WHERE id = ?").run(run.id);
2557
- return result.changes > 0;
2558
- }
2559
- var init_runs = __esm(() => {
2560
- init_types();
2561
- init_database();
2562
- });
2563
-
2564
- // node_modules/commander/esm.mjs
2565
- var import__ = __toESM(require_commander(), 1);
2566
- var {
2567
- program,
2568
- createCommand,
2569
- createArgument,
2570
- createOption,
2571
- CommanderError,
2572
- InvalidArgumentError,
2573
- InvalidOptionArgumentError,
2574
- Command,
2575
- Argument,
2576
- Option,
2577
- Help
2578
- } = import__.default;
2579
-
2580
- // src/cli/index.tsx
2581
- import chalk4 from "chalk";
2582
- import { readFileSync as readFileSync4, readdirSync, writeFileSync as writeFileSync3 } from "fs";
2583
- import { join as join6, resolve } from "path";
2584
-
2585
2491
  // src/db/scenarios.ts
2586
- init_types();
2587
- init_database();
2588
2492
  function nextShortId(projectId) {
2589
2493
  const db2 = getDatabase();
2590
2494
  if (projectId) {
@@ -2603,9 +2507,9 @@ function createScenario(input) {
2603
2507
  const short_id = nextShortId(input.projectId);
2604
2508
  const timestamp = now();
2605
2509
  db2.query(`
2606
- INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, version, created_at, updated_at)
2607
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
2608
- `).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, timestamp, timestamp);
2510
+ INSERT INTO scenarios (id, short_id, project_id, name, description, steps, tags, priority, model, timeout_ms, target_path, requires_auth, auth_config, metadata, assertions, version, created_at, updated_at)
2511
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?, ?)
2512
+ `).run(id, short_id, input.projectId ?? null, input.name, input.description, JSON.stringify(input.steps ?? []), JSON.stringify(input.tags ?? []), input.priority ?? "medium", input.model ?? null, input.timeoutMs ?? null, input.targetPath ?? null, input.requiresAuth ? 1 : 0, input.authConfig ? JSON.stringify(input.authConfig) : null, input.metadata ? JSON.stringify(input.metadata) : null, JSON.stringify(input.assertions ?? []), timestamp, timestamp);
2609
2513
  return getScenario(id);
2610
2514
  }
2611
2515
  function getScenario(id) {
@@ -2723,6 +2627,10 @@ function updateScenario(id, input, version) {
2723
2627
  sets.push("metadata = ?");
2724
2628
  params.push(JSON.stringify(input.metadata));
2725
2629
  }
2630
+ if (input.assertions !== undefined) {
2631
+ sets.push("assertions = ?");
2632
+ params.push(JSON.stringify(input.assertions));
2633
+ }
2726
2634
  if (sets.length === 0) {
2727
2635
  return existing;
2728
2636
  }
@@ -2746,9 +2654,579 @@ function deleteScenario(id) {
2746
2654
  const result = db2.query("DELETE FROM scenarios WHERE id = ?").run(scenario.id);
2747
2655
  return result.changes > 0;
2748
2656
  }
2657
+ var init_scenarios = __esm(() => {
2658
+ init_types();
2659
+ init_database();
2660
+ });
2661
+
2662
+ // src/db/runs.ts
2663
+ var exports_runs = {};
2664
+ __export(exports_runs, {
2665
+ updateRun: () => updateRun,
2666
+ listRuns: () => listRuns,
2667
+ getRun: () => getRun,
2668
+ deleteRun: () => deleteRun,
2669
+ createRun: () => createRun
2670
+ });
2671
+ function createRun(input) {
2672
+ const db2 = getDatabase();
2673
+ const id = uuid();
2674
+ const timestamp = now();
2675
+ db2.query(`
2676
+ INSERT INTO runs (id, project_id, status, url, model, headed, parallel, total, passed, failed, started_at, finished_at, metadata)
2677
+ VALUES (?, ?, 'pending', ?, ?, ?, ?, 0, 0, 0, ?, NULL, ?)
2678
+ `).run(id, input.projectId ?? null, input.url, input.model, input.headed ? 1 : 0, input.parallel ?? 1, timestamp, input.model ? JSON.stringify({}) : null);
2679
+ return getRun(id);
2680
+ }
2681
+ function getRun(id) {
2682
+ const db2 = getDatabase();
2683
+ let row = db2.query("SELECT * FROM runs WHERE id = ?").get(id);
2684
+ if (row)
2685
+ return runFromRow(row);
2686
+ const fullId = resolvePartialId("runs", id);
2687
+ if (fullId) {
2688
+ row = db2.query("SELECT * FROM runs WHERE id = ?").get(fullId);
2689
+ if (row)
2690
+ return runFromRow(row);
2691
+ }
2692
+ return null;
2693
+ }
2694
+ function listRuns(filter) {
2695
+ const db2 = getDatabase();
2696
+ const conditions = [];
2697
+ const params = [];
2698
+ if (filter?.projectId) {
2699
+ conditions.push("project_id = ?");
2700
+ params.push(filter.projectId);
2701
+ }
2702
+ if (filter?.status) {
2703
+ conditions.push("status = ?");
2704
+ params.push(filter.status);
2705
+ }
2706
+ let sql = "SELECT * FROM runs";
2707
+ if (conditions.length > 0) {
2708
+ sql += " WHERE " + conditions.join(" AND ");
2709
+ }
2710
+ sql += " ORDER BY started_at DESC";
2711
+ if (filter?.limit) {
2712
+ sql += " LIMIT ?";
2713
+ params.push(filter.limit);
2714
+ }
2715
+ if (filter?.offset) {
2716
+ sql += " OFFSET ?";
2717
+ params.push(filter.offset);
2718
+ }
2719
+ const rows = db2.query(sql).all(...params);
2720
+ return rows.map(runFromRow);
2721
+ }
2722
+ function updateRun(id, updates) {
2723
+ const db2 = getDatabase();
2724
+ const existing = getRun(id);
2725
+ if (!existing) {
2726
+ throw new Error(`Run not found: ${id}`);
2727
+ }
2728
+ const sets = [];
2729
+ const params = [];
2730
+ if (updates.status !== undefined) {
2731
+ sets.push("status = ?");
2732
+ params.push(updates.status);
2733
+ }
2734
+ if (updates.url !== undefined) {
2735
+ sets.push("url = ?");
2736
+ params.push(updates.url);
2737
+ }
2738
+ if (updates.model !== undefined) {
2739
+ sets.push("model = ?");
2740
+ params.push(updates.model);
2741
+ }
2742
+ if (updates.headed !== undefined) {
2743
+ sets.push("headed = ?");
2744
+ params.push(updates.headed);
2745
+ }
2746
+ if (updates.parallel !== undefined) {
2747
+ sets.push("parallel = ?");
2748
+ params.push(updates.parallel);
2749
+ }
2750
+ if (updates.total !== undefined) {
2751
+ sets.push("total = ?");
2752
+ params.push(updates.total);
2753
+ }
2754
+ if (updates.passed !== undefined) {
2755
+ sets.push("passed = ?");
2756
+ params.push(updates.passed);
2757
+ }
2758
+ if (updates.failed !== undefined) {
2759
+ sets.push("failed = ?");
2760
+ params.push(updates.failed);
2761
+ }
2762
+ if (updates.started_at !== undefined) {
2763
+ sets.push("started_at = ?");
2764
+ params.push(updates.started_at);
2765
+ }
2766
+ if (updates.finished_at !== undefined) {
2767
+ sets.push("finished_at = ?");
2768
+ params.push(updates.finished_at);
2769
+ }
2770
+ if (updates.metadata !== undefined) {
2771
+ sets.push("metadata = ?");
2772
+ params.push(updates.metadata);
2773
+ }
2774
+ if (updates.is_baseline !== undefined) {
2775
+ sets.push("is_baseline = ?");
2776
+ params.push(updates.is_baseline);
2777
+ }
2778
+ if (sets.length === 0) {
2779
+ return existing;
2780
+ }
2781
+ params.push(existing.id);
2782
+ db2.query(`UPDATE runs SET ${sets.join(", ")} WHERE id = ?`).run(...params);
2783
+ return getRun(existing.id);
2784
+ }
2785
+ function deleteRun(id) {
2786
+ const db2 = getDatabase();
2787
+ const run = getRun(id);
2788
+ if (!run)
2789
+ return false;
2790
+ const result = db2.query("DELETE FROM runs WHERE id = ?").run(run.id);
2791
+ return result.changes > 0;
2792
+ }
2793
+ var init_runs = __esm(() => {
2794
+ init_types();
2795
+ init_database();
2796
+ });
2797
+
2798
+ // src/db/flows.ts
2799
+ var exports_flows = {};
2800
+ __export(exports_flows, {
2801
+ topologicalSort: () => topologicalSort,
2802
+ removeDependency: () => removeDependency,
2803
+ listFlows: () => listFlows,
2804
+ getTransitiveDependencies: () => getTransitiveDependencies,
2805
+ getFlow: () => getFlow,
2806
+ getDependents: () => getDependents,
2807
+ getDependencies: () => getDependencies,
2808
+ deleteFlow: () => deleteFlow,
2809
+ createFlow: () => createFlow,
2810
+ addDependency: () => addDependency
2811
+ });
2812
+ function addDependency(scenarioId, dependsOn) {
2813
+ const db2 = getDatabase();
2814
+ const visited = new Set;
2815
+ const queue = [dependsOn];
2816
+ while (queue.length > 0) {
2817
+ const current = queue.shift();
2818
+ if (current === scenarioId) {
2819
+ throw new DependencyCycleError(scenarioId, dependsOn);
2820
+ }
2821
+ if (visited.has(current))
2822
+ continue;
2823
+ visited.add(current);
2824
+ const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
2825
+ for (const dep of deps) {
2826
+ if (!visited.has(dep.depends_on)) {
2827
+ queue.push(dep.depends_on);
2828
+ }
2829
+ }
2830
+ }
2831
+ db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
2832
+ }
2833
+ function removeDependency(scenarioId, dependsOn) {
2834
+ const db2 = getDatabase();
2835
+ const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
2836
+ return result.changes > 0;
2837
+ }
2838
+ function getDependencies(scenarioId) {
2839
+ const db2 = getDatabase();
2840
+ const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
2841
+ return rows.map((r) => r.depends_on);
2842
+ }
2843
+ function getDependents(scenarioId) {
2844
+ const db2 = getDatabase();
2845
+ const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
2846
+ return rows.map((r) => r.scenario_id);
2847
+ }
2848
+ function getTransitiveDependencies(scenarioId) {
2849
+ const db2 = getDatabase();
2850
+ const visited = new Set;
2851
+ const queue = [scenarioId];
2852
+ while (queue.length > 0) {
2853
+ const current = queue.shift();
2854
+ const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
2855
+ for (const dep of deps) {
2856
+ if (!visited.has(dep.depends_on)) {
2857
+ visited.add(dep.depends_on);
2858
+ queue.push(dep.depends_on);
2859
+ }
2860
+ }
2861
+ }
2862
+ return Array.from(visited);
2863
+ }
2864
+ function topologicalSort(scenarioIds) {
2865
+ const db2 = getDatabase();
2866
+ const idSet = new Set(scenarioIds);
2867
+ const inDegree = new Map;
2868
+ const dependents = new Map;
2869
+ for (const id of scenarioIds) {
2870
+ inDegree.set(id, 0);
2871
+ dependents.set(id, []);
2872
+ }
2873
+ for (const id of scenarioIds) {
2874
+ const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
2875
+ for (const dep of deps) {
2876
+ if (idSet.has(dep.depends_on)) {
2877
+ inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
2878
+ dependents.get(dep.depends_on).push(id);
2879
+ }
2880
+ }
2881
+ }
2882
+ const queue = [];
2883
+ for (const [id, deg] of inDegree) {
2884
+ if (deg === 0)
2885
+ queue.push(id);
2886
+ }
2887
+ const sorted = [];
2888
+ while (queue.length > 0) {
2889
+ const current = queue.shift();
2890
+ sorted.push(current);
2891
+ for (const dep of dependents.get(current) ?? []) {
2892
+ const newDeg = (inDegree.get(dep) ?? 1) - 1;
2893
+ inDegree.set(dep, newDeg);
2894
+ if (newDeg === 0)
2895
+ queue.push(dep);
2896
+ }
2897
+ }
2898
+ if (sorted.length !== scenarioIds.length) {
2899
+ throw new DependencyCycleError("multiple", "multiple");
2900
+ }
2901
+ return sorted;
2902
+ }
2903
+ function createFlow(input) {
2904
+ const db2 = getDatabase();
2905
+ const id = uuid();
2906
+ const timestamp = now();
2907
+ db2.query(`
2908
+ INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
2909
+ VALUES (?, ?, ?, ?, ?, ?, ?)
2910
+ `).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
2911
+ return getFlow(id);
2912
+ }
2913
+ function getFlow(id) {
2914
+ const db2 = getDatabase();
2915
+ let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
2916
+ if (row)
2917
+ return flowFromRow(row);
2918
+ const fullId = resolvePartialId("flows", id);
2919
+ if (fullId) {
2920
+ row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
2921
+ if (row)
2922
+ return flowFromRow(row);
2923
+ }
2924
+ return null;
2925
+ }
2926
+ function listFlows(projectId) {
2927
+ const db2 = getDatabase();
2928
+ if (projectId) {
2929
+ const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
2930
+ return rows2.map(flowFromRow);
2931
+ }
2932
+ const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
2933
+ return rows.map(flowFromRow);
2934
+ }
2935
+ function deleteFlow(id) {
2936
+ const db2 = getDatabase();
2937
+ const flow = getFlow(id);
2938
+ if (!flow)
2939
+ return false;
2940
+ const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
2941
+ return result.changes > 0;
2942
+ }
2943
+ var init_flows = __esm(() => {
2944
+ init_database();
2945
+ init_database();
2946
+ init_types();
2947
+ });
2948
+
2949
+ // src/lib/openapi-import.ts
2950
+ var exports_openapi_import = {};
2951
+ __export(exports_openapi_import, {
2952
+ parseOpenAPISpec: () => parseOpenAPISpec,
2953
+ importFromOpenAPI: () => importFromOpenAPI
2954
+ });
2955
+ import { readFileSync as readFileSync5 } from "fs";
2956
+ function parseSpec(content) {
2957
+ try {
2958
+ return JSON.parse(content);
2959
+ } catch {
2960
+ throw new Error("Only JSON specs are supported. Convert YAML to JSON first: `cat spec.yaml | python -c 'import sys,yaml,json; json.dump(yaml.safe_load(sys.stdin),sys.stdout)' > spec.json`");
2961
+ }
2962
+ }
2963
+ function methodPriority(method) {
2964
+ switch (method.toUpperCase()) {
2965
+ case "GET":
2966
+ return "medium";
2967
+ case "POST":
2968
+ return "high";
2969
+ case "PUT":
2970
+ return "high";
2971
+ case "DELETE":
2972
+ return "critical";
2973
+ case "PATCH":
2974
+ return "medium";
2975
+ default:
2976
+ return "low";
2977
+ }
2978
+ }
2979
+ function parseOpenAPISpec(filePathOrUrl) {
2980
+ let content;
2981
+ if (filePathOrUrl.startsWith("http")) {
2982
+ throw new Error("URL fetching not supported yet. Download the spec file first.");
2983
+ }
2984
+ content = readFileSync5(filePathOrUrl, "utf-8");
2985
+ const spec = parseSpec(content);
2986
+ const isOpenAPI3 = !!spec.openapi;
2987
+ const isSwagger2 = !!spec.swagger;
2988
+ if (!isOpenAPI3 && !isSwagger2) {
2989
+ throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
2990
+ }
2991
+ const scenarios = [];
2992
+ const paths = spec.paths ?? {};
2993
+ for (const [path, methods] of Object.entries(paths)) {
2994
+ for (const [method, operation] of Object.entries(methods)) {
2995
+ if (["get", "post", "put", "delete", "patch"].indexOf(method.toLowerCase()) === -1)
2996
+ continue;
2997
+ const op = operation;
2998
+ const name = op.summary ?? op.operationId ?? `${method.toUpperCase()} ${path}`;
2999
+ const tags = op.tags ?? [];
3000
+ const requiresAuth = !!(op.security?.length ?? spec.security?.length);
3001
+ const steps = [];
3002
+ steps.push(`Navigate to the API endpoint: ${method.toUpperCase()} ${path}`);
3003
+ if (op.parameters?.length) {
3004
+ const required = op.parameters.filter((p) => p.required);
3005
+ if (required.length > 0) {
3006
+ steps.push(`Fill required parameters: ${required.map((p) => p.name).join(", ")}`);
3007
+ }
3008
+ }
3009
+ if (["post", "put", "patch"].includes(method.toLowerCase())) {
3010
+ steps.push("Fill the request body with valid test data");
3011
+ }
3012
+ steps.push("Submit the request");
3013
+ const responses = op.responses ?? {};
3014
+ const successCodes = Object.keys(responses).filter((c) => c.startsWith("2"));
3015
+ if (successCodes.length > 0) {
3016
+ steps.push(`Verify response status is ${successCodes.join(" or ")}`);
3017
+ } else {
3018
+ steps.push("Verify the response is successful");
3019
+ }
3020
+ const description = [
3021
+ op.description ?? `Test the ${method.toUpperCase()} ${path} endpoint.`,
3022
+ requiresAuth ? "This endpoint requires authentication." : ""
3023
+ ].filter(Boolean).join(" ");
3024
+ scenarios.push({
3025
+ name,
3026
+ description,
3027
+ steps,
3028
+ tags: [...tags, "api", method.toLowerCase()],
3029
+ priority: methodPriority(method),
3030
+ targetPath: path,
3031
+ requiresAuth
3032
+ });
3033
+ }
3034
+ }
3035
+ return scenarios;
3036
+ }
3037
+ function importFromOpenAPI(filePathOrUrl, projectId) {
3038
+ const inputs = parseOpenAPISpec(filePathOrUrl);
3039
+ const scenarios = inputs.map((input) => createScenario({ ...input, projectId }));
3040
+ return { imported: scenarios.length, scenarios };
3041
+ }
3042
+ var init_openapi_import = __esm(() => {
3043
+ init_scenarios();
3044
+ });
3045
+
3046
+ // src/lib/recorder.ts
3047
+ var exports_recorder = {};
3048
+ __export(exports_recorder, {
3049
+ recordSession: () => recordSession,
3050
+ recordAndSave: () => recordAndSave,
3051
+ actionsToScenarioInput: () => actionsToScenarioInput
3052
+ });
3053
+ import { chromium as chromium2 } from "playwright";
3054
+ async function recordSession(url, options) {
3055
+ const browser = await chromium2.launch({ headless: false });
3056
+ const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
3057
+ const page = await context.newPage();
3058
+ const actions = [];
3059
+ const startTime = Date.now();
3060
+ const timeout = options?.timeout ?? 300000;
3061
+ page.on("framenavigated", (frame) => {
3062
+ if (frame === page.mainFrame()) {
3063
+ actions.push({ type: "navigate", url: frame.url(), timestamp: Date.now() - startTime });
3064
+ }
3065
+ });
3066
+ await page.addInitScript(() => {
3067
+ document.addEventListener("click", (e) => {
3068
+ const target = e.target;
3069
+ const selector = buildSelector(target);
3070
+ window.postMessage({ __testers_action: "click", selector }, "*");
3071
+ }, true);
3072
+ document.addEventListener("input", (e) => {
3073
+ const target = e.target;
3074
+ const selector = buildSelector(target);
3075
+ window.postMessage({ __testers_action: "fill", selector, value: target.value }, "*");
3076
+ }, true);
3077
+ document.addEventListener("change", (e) => {
3078
+ const target = e.target;
3079
+ if (target.tagName === "SELECT") {
3080
+ const selector = buildSelector(target);
3081
+ window.postMessage({ __testers_action: "select", selector, value: target.value }, "*");
3082
+ }
3083
+ }, true);
3084
+ document.addEventListener("keydown", (e) => {
3085
+ if (["Enter", "Tab", "Escape"].includes(e.key)) {
3086
+ window.postMessage({ __testers_action: "press", key: e.key }, "*");
3087
+ }
3088
+ }, true);
3089
+ function buildSelector(el) {
3090
+ if (el.id)
3091
+ return `#${el.id}`;
3092
+ if (el.getAttribute("data-testid"))
3093
+ return `[data-testid="${el.getAttribute("data-testid")}"]`;
3094
+ if (el.getAttribute("name"))
3095
+ return `${el.tagName.toLowerCase()}[name="${el.getAttribute("name")}"]`;
3096
+ if (el.getAttribute("aria-label"))
3097
+ return `[aria-label="${el.getAttribute("aria-label")}"]`;
3098
+ if (el.className && typeof el.className === "string") {
3099
+ const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
3100
+ if (classes)
3101
+ return `${el.tagName.toLowerCase()}.${classes}`;
3102
+ }
3103
+ const text = el.textContent?.trim().slice(0, 30);
3104
+ if (text)
3105
+ return `text="${text}"`;
3106
+ return el.tagName.toLowerCase();
3107
+ }
3108
+ });
3109
+ const pollInterval = setInterval(async () => {
3110
+ try {
3111
+ const newActions = await page.evaluate(() => {
3112
+ const collected = window.__testers_collected ?? [];
3113
+ window.__testers_collected = [];
3114
+ return collected;
3115
+ });
3116
+ for (const a of newActions) {
3117
+ actions.push({
3118
+ type: a["type"],
3119
+ selector: a["selector"],
3120
+ value: a["value"],
3121
+ key: a["key"],
3122
+ timestamp: Date.now() - startTime
3123
+ });
3124
+ }
3125
+ } catch {}
3126
+ }, 500);
3127
+ await page.exposeFunction("__testersRecord", (action) => {
3128
+ actions.push({ ...action, timestamp: Date.now() - startTime });
3129
+ });
3130
+ await page.addInitScript(() => {
3131
+ window.addEventListener("message", (e) => {
3132
+ if (e.data?.__testers_action) {
3133
+ const { __testers_action, ...rest } = e.data;
3134
+ window.__testersRecord({ type: __testers_action, ...rest });
3135
+ }
3136
+ });
3137
+ });
3138
+ await page.goto(url);
3139
+ actions.push({ type: "navigate", url, timestamp: 0 });
3140
+ console.log(`
3141
+ Recording started. Interact with the browser.`);
3142
+ console.log(` Close the browser window or wait ${timeout / 1000}s to stop.
3143
+ `);
3144
+ await Promise.race([
3145
+ page.waitForEvent("close").catch(() => {}),
3146
+ context.waitForEvent("close").catch(() => {}),
3147
+ new Promise((resolve) => setTimeout(resolve, timeout))
3148
+ ]);
3149
+ clearInterval(pollInterval);
3150
+ try {
3151
+ await browser.close();
3152
+ } catch {}
3153
+ return {
3154
+ actions,
3155
+ url,
3156
+ duration: Date.now() - startTime
3157
+ };
3158
+ }
3159
+ function actionsToScenarioInput(recording, name, projectId) {
3160
+ const steps = [];
3161
+ const seenFills = new Map;
3162
+ for (const action of recording.actions) {
3163
+ switch (action.type) {
3164
+ case "navigate":
3165
+ if (action.url)
3166
+ steps.push(`Navigate to ${action.url}`);
3167
+ break;
3168
+ case "click":
3169
+ if (action.selector)
3170
+ steps.push(`Click ${action.selector}`);
3171
+ break;
3172
+ case "fill":
3173
+ if (action.selector && action.value) {
3174
+ seenFills.set(action.selector, action.value);
3175
+ }
3176
+ break;
3177
+ case "select":
3178
+ if (action.selector && action.value)
3179
+ steps.push(`Select "${action.value}" in ${action.selector}`);
3180
+ break;
3181
+ case "press":
3182
+ if (action.key)
3183
+ steps.push(`Press ${action.key}`);
3184
+ break;
3185
+ }
3186
+ }
3187
+ for (const [selector, value] of seenFills) {
3188
+ steps.push(`Fill ${selector} with "${value}"`);
3189
+ }
3190
+ return {
3191
+ name,
3192
+ description: `Recorded session on ${recording.url} (${(recording.duration / 1000).toFixed(0)}s, ${recording.actions.length} actions)`,
3193
+ steps,
3194
+ tags: ["recorded"],
3195
+ projectId
3196
+ };
3197
+ }
3198
+ async function recordAndSave(url, name, projectId) {
3199
+ const recording = await recordSession(url);
3200
+ const input = actionsToScenarioInput(recording, name, projectId);
3201
+ const scenario = createScenario(input);
3202
+ return { recording, scenario };
3203
+ }
3204
+ var init_recorder = __esm(() => {
3205
+ init_scenarios();
3206
+ });
3207
+
3208
+ // node_modules/commander/esm.mjs
3209
+ var import__ = __toESM(require_commander(), 1);
3210
+ var {
3211
+ program,
3212
+ createCommand,
3213
+ createArgument,
3214
+ createOption,
3215
+ CommanderError,
3216
+ InvalidArgumentError,
3217
+ InvalidOptionArgumentError,
3218
+ Command,
3219
+ Argument,
3220
+ Option,
3221
+ Help
3222
+ } = import__.default;
2749
3223
 
2750
3224
  // src/cli/index.tsx
3225
+ init_scenarios();
2751
3226
  init_runs();
3227
+ import chalk5 from "chalk";
3228
+ import { readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
3229
+ import { join as join6, resolve } from "path";
2752
3230
 
2753
3231
  // src/db/results.ts
2754
3232
  init_types();
@@ -2854,6 +3332,7 @@ function listScreenshots(resultId) {
2854
3332
 
2855
3333
  // src/lib/runner.ts
2856
3334
  init_runs();
3335
+ init_scenarios();
2857
3336
 
2858
3337
  // src/lib/browser.ts
2859
3338
  init_types();
@@ -3832,7 +4311,105 @@ function loadConfig() {
3832
4311
  if (envApiKey) {
3833
4312
  config.anthropicApiKey = envApiKey;
3834
4313
  }
3835
- return config;
4314
+ return config;
4315
+ }
4316
+
4317
+ // src/lib/webhooks.ts
4318
+ init_database();
4319
+ function fromRow(row) {
4320
+ return {
4321
+ id: row.id,
4322
+ url: row.url,
4323
+ events: JSON.parse(row.events),
4324
+ projectId: row.project_id,
4325
+ secret: row.secret,
4326
+ active: row.active === 1,
4327
+ createdAt: row.created_at
4328
+ };
4329
+ }
4330
+ function listWebhooks(projectId) {
4331
+ const db2 = getDatabase();
4332
+ let query = "SELECT * FROM webhooks WHERE active = 1";
4333
+ const params = [];
4334
+ if (projectId) {
4335
+ query += " AND (project_id = ? OR project_id IS NULL)";
4336
+ params.push(projectId);
4337
+ }
4338
+ query += " ORDER BY created_at DESC";
4339
+ const rows = db2.query(query).all(...params);
4340
+ return rows.map(fromRow);
4341
+ }
4342
+ function signPayload(body, secret) {
4343
+ const encoder = new TextEncoder;
4344
+ const key = encoder.encode(secret);
4345
+ const data = encoder.encode(body);
4346
+ let hash = 0;
4347
+ for (let i = 0;i < data.length; i++) {
4348
+ hash = (hash << 5) - hash + data[i] + (key[i % key.length] ?? 0) | 0;
4349
+ }
4350
+ return `sha256=${Math.abs(hash).toString(16).padStart(16, "0")}`;
4351
+ }
4352
+ function formatSlackPayload(payload) {
4353
+ const status = payload.run.status === "passed" ? ":white_check_mark:" : ":x:";
4354
+ const color = payload.run.status === "passed" ? "#22c55e" : "#ef4444";
4355
+ return {
4356
+ attachments: [
4357
+ {
4358
+ color,
4359
+ blocks: [
4360
+ {
4361
+ type: "section",
4362
+ text: {
4363
+ type: "mrkdwn",
4364
+ text: `${status} *Test Run ${payload.run.status.toUpperCase()}*
4365
+ ` + `URL: ${payload.run.url}
4366
+ ` + `Results: ${payload.run.passed}/${payload.run.total} passed` + (payload.run.failed > 0 ? ` (${payload.run.failed} failed)` : "") + (payload.schedule ? `
4367
+ Schedule: ${payload.schedule.name}` : "")
4368
+ }
4369
+ }
4370
+ ]
4371
+ }
4372
+ ]
4373
+ };
4374
+ }
4375
+ async function dispatchWebhooks(event, run, schedule) {
4376
+ const webhooks = listWebhooks(run.projectId ?? undefined);
4377
+ const payload = {
4378
+ event,
4379
+ run: {
4380
+ id: run.id,
4381
+ url: run.url,
4382
+ status: run.status,
4383
+ passed: run.passed,
4384
+ failed: run.failed,
4385
+ total: run.total
4386
+ },
4387
+ schedule,
4388
+ timestamp: new Date().toISOString()
4389
+ };
4390
+ for (const webhook of webhooks) {
4391
+ if (!webhook.events.includes(event) && !webhook.events.includes("*"))
4392
+ continue;
4393
+ const isSlack = webhook.url.includes("hooks.slack.com");
4394
+ const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
4395
+ const headers = {
4396
+ "Content-Type": "application/json"
4397
+ };
4398
+ if (webhook.secret) {
4399
+ headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
4400
+ }
4401
+ try {
4402
+ const response = await fetch(webhook.url, {
4403
+ method: "POST",
4404
+ headers,
4405
+ body
4406
+ });
4407
+ if (!response.ok) {
4408
+ await new Promise((r) => setTimeout(r, 5000));
4409
+ await fetch(webhook.url, { method: "POST", headers, body });
4410
+ }
4411
+ } catch {}
4412
+ }
3836
4413
  }
3837
4414
 
3838
4415
  // src/lib/runner.ts
@@ -3844,6 +4421,20 @@ function emit(event) {
3844
4421
  if (eventHandler)
3845
4422
  eventHandler(event);
3846
4423
  }
4424
+ function withTimeout(promise, ms, label) {
4425
+ return new Promise((resolve, reject) => {
4426
+ const timer = setTimeout(() => {
4427
+ reject(new Error(`Scenario timeout after ${ms}ms: ${label}`));
4428
+ }, ms);
4429
+ promise.then((val) => {
4430
+ clearTimeout(timer);
4431
+ resolve(val);
4432
+ }, (err) => {
4433
+ clearTimeout(timer);
4434
+ reject(err);
4435
+ });
4436
+ });
4437
+ }
3847
4438
  async function runSingleScenario(scenario, runId, options) {
3848
4439
  const config = loadConfig();
3849
4440
  const model = resolveModel(options.model ?? scenario.model ?? config.defaultModel);
@@ -3866,8 +4457,9 @@ async function runSingleScenario(scenario, runId, options) {
3866
4457
  viewport: config.browser.viewport
3867
4458
  });
3868
4459
  const targetUrl = scenario.targetPath ? `${options.url.replace(/\/$/, "")}${scenario.targetPath}` : options.url;
3869
- await page.goto(targetUrl, { timeout: options.timeout ?? config.browser.timeout });
3870
- const agentResult = await runAgentLoop({
4460
+ const scenarioTimeout = scenario.timeoutMs ?? options.timeout ?? config.browser.timeout ?? 60000;
4461
+ await page.goto(targetUrl, { timeout: Math.min(scenarioTimeout, 30000) });
4462
+ const agentResult = await withTimeout(runAgentLoop({
3871
4463
  client,
3872
4464
  page,
3873
4465
  scenario,
@@ -3888,7 +4480,7 @@ async function runSingleScenario(scenario, runId, options) {
3888
4480
  stepNumber: stepEvent.stepNumber
3889
4481
  });
3890
4482
  }
3891
- });
4483
+ }), scenarioTimeout, scenario.name);
3892
4484
  for (const ss of agentResult.screenshots) {
3893
4485
  createScreenshot({
3894
4486
  resultId: result.id,
@@ -3940,24 +4532,70 @@ async function runBatch(scenarios, options) {
3940
4532
  projectId: options.projectId
3941
4533
  });
3942
4534
  updateRun(run.id, { status: "running", total: scenarios.length });
4535
+ let sortedScenarios = scenarios;
4536
+ try {
4537
+ const { topologicalSort: topologicalSort2 } = await Promise.resolve().then(() => (init_flows(), exports_flows));
4538
+ const scenarioIds = scenarios.map((s) => s.id);
4539
+ const sortedIds = topologicalSort2(scenarioIds);
4540
+ const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
4541
+ sortedScenarios = sortedIds.map((id) => scenarioMap.get(id)).filter((s) => s !== undefined);
4542
+ for (const s of scenarios) {
4543
+ if (!sortedIds.includes(s.id))
4544
+ sortedScenarios.push(s);
4545
+ }
4546
+ } catch {}
3943
4547
  const results = [];
4548
+ const failedScenarioIds = new Set;
4549
+ const canRun = async (scenario) => {
4550
+ try {
4551
+ const { getDependencies: getDependencies2 } = await Promise.resolve().then(() => (init_flows(), exports_flows));
4552
+ const deps = getDependencies2(scenario.id);
4553
+ for (const depId of deps) {
4554
+ if (failedScenarioIds.has(depId))
4555
+ return false;
4556
+ }
4557
+ } catch {}
4558
+ return true;
4559
+ };
3944
4560
  if (parallel <= 1) {
3945
- for (const scenario of scenarios) {
4561
+ for (const scenario of sortedScenarios) {
4562
+ if (!await canRun(scenario)) {
4563
+ const result2 = createResult({ runId: run.id, scenarioId: scenario.id, model, stepsTotal: 0 });
4564
+ const skipped = updateResult(result2.id, { status: "skipped", error: "Skipped: dependency failed" });
4565
+ results.push(skipped);
4566
+ failedScenarioIds.add(scenario.id);
4567
+ emit({ type: "scenario:error", scenarioId: scenario.id, scenarioName: scenario.name, error: "Dependency failed \u2014 skipped", runId: run.id });
4568
+ continue;
4569
+ }
3946
4570
  const result = await runSingleScenario(scenario, run.id, options);
3947
4571
  results.push(result);
4572
+ if (result.status === "failed" || result.status === "error") {
4573
+ failedScenarioIds.add(scenario.id);
4574
+ }
3948
4575
  }
3949
4576
  } else {
3950
- const queue = [...scenarios];
4577
+ const queue = [...sortedScenarios];
3951
4578
  const running = [];
3952
4579
  const processNext = async () => {
3953
4580
  const scenario = queue.shift();
3954
4581
  if (!scenario)
3955
4582
  return;
4583
+ if (!await canRun(scenario)) {
4584
+ const result2 = createResult({ runId: run.id, scenarioId: scenario.id, model, stepsTotal: 0 });
4585
+ const skipped = updateResult(result2.id, { status: "skipped", error: "Skipped: dependency failed" });
4586
+ results.push(skipped);
4587
+ failedScenarioIds.add(scenario.id);
4588
+ await processNext();
4589
+ return;
4590
+ }
3956
4591
  const result = await runSingleScenario(scenario, run.id, options);
3957
4592
  results.push(result);
4593
+ if (result.status === "failed" || result.status === "error") {
4594
+ failedScenarioIds.add(scenario.id);
4595
+ }
3958
4596
  await processNext();
3959
4597
  };
3960
- const workers = Math.min(parallel, scenarios.length);
4598
+ const workers = Math.min(parallel, sortedScenarios.length);
3961
4599
  for (let i = 0;i < workers; i++) {
3962
4600
  running.push(processNext());
3963
4601
  }
@@ -3974,6 +4612,8 @@ async function runBatch(scenarios, options) {
3974
4612
  finished_at: new Date().toISOString()
3975
4613
  });
3976
4614
  emit({ type: "run:complete", runId: run.id });
4615
+ const eventType = finalRun.status === "failed" ? "failed" : "completed";
4616
+ dispatchWebhooks(eventType, finalRun).catch(() => {});
3977
4617
  return { run: finalRun, results };
3978
4618
  }
3979
4619
  async function runByFilter(options) {
@@ -4059,6 +4699,9 @@ function startRunAsync(options) {
4059
4699
  finished_at: new Date().toISOString()
4060
4700
  });
4061
4701
  emit({ type: "run:complete", runId: run.id });
4702
+ const asyncRun = getRun(run.id);
4703
+ if (asyncRun)
4704
+ dispatchWebhooks(asyncRun.status === "failed" ? "failed" : "completed", asyncRun).catch(() => {});
4062
4705
  } catch (error) {
4063
4706
  const errorMsg = error instanceof Error ? error.message : String(error);
4064
4707
  updateRun(run.id, {
@@ -4066,6 +4709,9 @@ function startRunAsync(options) {
4066
4709
  finished_at: new Date().toISOString()
4067
4710
  });
4068
4711
  emit({ type: "run:complete", runId: run.id, error: errorMsg });
4712
+ const failedRun = getRun(run.id);
4713
+ if (failedRun)
4714
+ dispatchWebhooks("failed", failedRun).catch(() => {});
4069
4715
  }
4070
4716
  })();
4071
4717
  return { runId: run.id, scenarioCount: scenarios.length };
@@ -4082,6 +4728,7 @@ function estimateCost(model, tokens) {
4082
4728
 
4083
4729
  // src/lib/reporter.ts
4084
4730
  import chalk from "chalk";
4731
+ init_scenarios();
4085
4732
  function formatTerminal(run, results) {
4086
4733
  const lines = [];
4087
4734
  lines.push("");
@@ -4235,11 +4882,12 @@ function formatScenarioList(scenarios) {
4235
4882
  }
4236
4883
 
4237
4884
  // src/lib/todos-connector.ts
4885
+ init_scenarios();
4886
+ init_types();
4238
4887
  import { Database as Database2 } from "bun:sqlite";
4239
4888
  import { existsSync as existsSync4 } from "fs";
4240
4889
  import { join as join4 } from "path";
4241
4890
  import { homedir as homedir4 } from "os";
4242
- init_types();
4243
4891
  function resolveTodosDbPath() {
4244
4892
  const envPath = process.env["TODOS_DB_PATH"];
4245
4893
  if (envPath)
@@ -4337,6 +4985,7 @@ function importFromTodos(options = {}) {
4337
4985
  }
4338
4986
 
4339
4987
  // src/lib/init.ts
4988
+ init_scenarios();
4340
4989
  import { existsSync as existsSync5, readFileSync as readFileSync2, writeFileSync as writeFileSync2, mkdirSync as mkdirSync3 } from "fs";
4341
4990
  import { join as join5, basename } from "path";
4342
4991
  import { homedir as homedir5 } from "os";
@@ -4494,6 +5143,7 @@ function initProject(options) {
4494
5143
  }
4495
5144
 
4496
5145
  // src/lib/smoke.ts
5146
+ init_scenarios();
4497
5147
  init_runs();
4498
5148
  var SMOKE_DESCRIPTION = `You are performing an autonomous smoke test of this web application. Your job is to explore as many pages as possible and find issues. Follow these instructions:
4499
5149
 
@@ -4715,6 +5365,7 @@ function formatSmokeReport(result) {
4715
5365
  // src/lib/diff.ts
4716
5366
  init_runs();
4717
5367
  import chalk2 from "chalk";
5368
+ init_scenarios();
4718
5369
  function diffRuns(runId1, runId2) {
4719
5370
  const run1 = getRun(runId1);
4720
5371
  if (!run1) {
@@ -4860,14 +5511,145 @@ function formatDiffJSON(diff) {
4860
5511
  return JSON.stringify(diff, null, 2);
4861
5512
  }
4862
5513
 
5514
+ // src/lib/visual-diff.ts
5515
+ import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
5516
+ import chalk3 from "chalk";
5517
+ init_runs();
5518
+ init_scenarios();
5519
+ init_database();
5520
+ var DEFAULT_THRESHOLD = 0.1;
5521
+ function setBaseline(runId) {
5522
+ const run = getRun(runId);
5523
+ if (!run) {
5524
+ throw new Error(`Run not found: ${runId}`);
5525
+ }
5526
+ const db2 = getDatabase();
5527
+ if (run.projectId) {
5528
+ db2.query("UPDATE runs SET is_baseline = 0 WHERE project_id = ? AND is_baseline = 1").run(run.projectId);
5529
+ } else {
5530
+ db2.query("UPDATE runs SET is_baseline = 0 WHERE project_id IS NULL AND is_baseline = 1").run();
5531
+ }
5532
+ updateRun(run.id, { is_baseline: 1 });
5533
+ }
5534
+ function compareImages(image1Path, image2Path) {
5535
+ if (!existsSync6(image1Path)) {
5536
+ throw new Error(`Baseline image not found: ${image1Path}`);
5537
+ }
5538
+ if (!existsSync6(image2Path)) {
5539
+ throw new Error(`Current image not found: ${image2Path}`);
5540
+ }
5541
+ const buf1 = readFileSync3(image1Path);
5542
+ const buf2 = readFileSync3(image2Path);
5543
+ if (buf1.equals(buf2)) {
5544
+ const estimatedPixels = Math.max(1, Math.floor(buf1.length / 4));
5545
+ return { diffPercent: 0, diffPixels: 0, totalPixels: estimatedPixels };
5546
+ }
5547
+ if (buf1.length !== buf2.length) {
5548
+ const maxLen = Math.max(buf1.length, buf2.length);
5549
+ const estimatedPixels = Math.max(1, Math.floor(maxLen / 4));
5550
+ return { diffPercent: 100, diffPixels: estimatedPixels, totalPixels: estimatedPixels };
5551
+ }
5552
+ let diffBytes = 0;
5553
+ for (let i = 0;i < buf1.length; i++) {
5554
+ if (buf1[i] !== buf2[i]) {
5555
+ diffBytes++;
5556
+ }
5557
+ }
5558
+ const totalPixels = Math.max(1, Math.floor(buf1.length / 4));
5559
+ const diffPixels = Math.max(1, Math.floor(diffBytes / 4));
5560
+ const diffPercent = parseFloat((diffBytes / buf1.length * 100).toFixed(4));
5561
+ return { diffPercent, diffPixels, totalPixels };
5562
+ }
5563
+ function compareRunScreenshots(runId, baselineRunId, threshold = DEFAULT_THRESHOLD) {
5564
+ const run = getRun(runId);
5565
+ if (!run)
5566
+ throw new Error(`Run not found: ${runId}`);
5567
+ const baselineRun = getRun(baselineRunId);
5568
+ if (!baselineRun)
5569
+ throw new Error(`Baseline run not found: ${baselineRunId}`);
5570
+ const currentResults = getResultsByRun(run.id);
5571
+ const baselineResults = getResultsByRun(baselineRun.id);
5572
+ const baselineMap = new Map;
5573
+ for (const result of baselineResults) {
5574
+ const screenshots = listScreenshots(result.id);
5575
+ for (const ss of screenshots) {
5576
+ const key = `${result.scenarioId}:${ss.stepNumber}`;
5577
+ baselineMap.set(key, { path: ss.filePath, action: ss.action });
5578
+ }
5579
+ }
5580
+ const results = [];
5581
+ for (const result of currentResults) {
5582
+ const screenshots = listScreenshots(result.id);
5583
+ for (const ss of screenshots) {
5584
+ const key = `${result.scenarioId}:${ss.stepNumber}`;
5585
+ const baseline = baselineMap.get(key);
5586
+ if (!baseline)
5587
+ continue;
5588
+ if (!existsSync6(baseline.path) || !existsSync6(ss.filePath))
5589
+ continue;
5590
+ try {
5591
+ const comparison = compareImages(baseline.path, ss.filePath);
5592
+ results.push({
5593
+ scenarioId: result.scenarioId,
5594
+ stepNumber: ss.stepNumber,
5595
+ action: ss.action,
5596
+ baselinePath: baseline.path,
5597
+ currentPath: ss.filePath,
5598
+ diffPercent: comparison.diffPercent,
5599
+ isRegression: comparison.diffPercent > threshold
5600
+ });
5601
+ } catch {}
5602
+ }
5603
+ }
5604
+ return results;
5605
+ }
5606
+ function formatVisualDiffTerminal(results, threshold = DEFAULT_THRESHOLD) {
5607
+ if (results.length === 0) {
5608
+ return chalk3.dim(`
5609
+ No screenshot comparisons found.
5610
+ `);
5611
+ }
5612
+ const lines = [];
5613
+ lines.push("");
5614
+ lines.push(chalk3.bold(" Visual Regression Summary"));
5615
+ lines.push("");
5616
+ const regressions = results.filter((r) => r.diffPercent >= threshold);
5617
+ const passed = results.filter((r) => r.diffPercent < threshold);
5618
+ if (regressions.length > 0) {
5619
+ lines.push(chalk3.red.bold(` Regressions (${regressions.length}):`));
5620
+ for (const r of regressions) {
5621
+ const scenario = getScenario(r.scenarioId);
5622
+ const label = scenario ? `${scenario.shortId}: ${scenario.name}` : r.scenarioId.slice(0, 8);
5623
+ const pct = chalk3.red(`${r.diffPercent.toFixed(2)}%`);
5624
+ lines.push(` ${chalk3.red("!")} ${label} step ${r.stepNumber} (${r.action}) \u2014 ${pct} diff`);
5625
+ }
5626
+ lines.push("");
5627
+ }
5628
+ if (passed.length > 0) {
5629
+ lines.push(chalk3.green.bold(` Passed (${passed.length}):`));
5630
+ for (const r of passed) {
5631
+ const scenario = getScenario(r.scenarioId);
5632
+ const label = scenario ? `${scenario.shortId}: ${scenario.name}` : r.scenarioId.slice(0, 8);
5633
+ const pct = chalk3.green(`${r.diffPercent.toFixed(2)}%`);
5634
+ lines.push(` ${chalk3.green("\u2713")} ${label} step ${r.stepNumber} (${r.action}) \u2014 ${pct} diff`);
5635
+ }
5636
+ lines.push("");
5637
+ }
5638
+ lines.push(chalk3.bold(` Visual Summary: ${regressions.length} regressions, ${passed.length} passed (threshold: ${threshold}%)`));
5639
+ lines.push("");
5640
+ return lines.join(`
5641
+ `);
5642
+ }
5643
+
4863
5644
  // src/lib/report.ts
4864
5645
  init_runs();
4865
- import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
5646
+ import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
5647
+ init_scenarios();
4866
5648
  function imageToBase64(filePath) {
4867
- if (!filePath || !existsSync6(filePath))
5649
+ if (!filePath || !existsSync7(filePath))
4868
5650
  return "";
4869
5651
  try {
4870
- const buffer = readFileSync3(filePath);
5652
+ const buffer = readFileSync4(filePath);
4871
5653
  const base64 = buffer.toString("base64");
4872
5654
  return `data:image/png;base64,${base64}`;
4873
5655
  } catch {
@@ -5061,7 +5843,7 @@ function generateLatestReport() {
5061
5843
 
5062
5844
  // src/lib/costs.ts
5063
5845
  init_database();
5064
- import chalk3 from "chalk";
5846
+ import chalk4 from "chalk";
5065
5847
  function getDateFilter(period) {
5066
5848
  switch (period) {
5067
5849
  case "day":
@@ -5166,15 +5948,15 @@ function formatTokens(tokens) {
5166
5948
  function formatCostsTerminal(summary) {
5167
5949
  const lines = [];
5168
5950
  lines.push("");
5169
- lines.push(chalk3.bold(` Cost Summary (${summary.period})`));
5951
+ lines.push(chalk4.bold(` Cost Summary (${summary.period})`));
5170
5952
  lines.push("");
5171
- lines.push(` Total: ${chalk3.yellow(formatDollars(summary.totalCostCents))} (${formatTokens(summary.totalTokens)} tokens across ${summary.runCount} runs)`);
5172
- lines.push(` Avg/run: ${chalk3.yellow(formatDollars(summary.avgCostPerRun))}`);
5173
- lines.push(` Est/month: ${chalk3.yellow(formatDollars(summary.estimatedMonthlyCents))}`);
5953
+ lines.push(` Total: ${chalk4.yellow(formatDollars(summary.totalCostCents))} (${formatTokens(summary.totalTokens)} tokens across ${summary.runCount} runs)`);
5954
+ lines.push(` Avg/run: ${chalk4.yellow(formatDollars(summary.avgCostPerRun))}`);
5955
+ lines.push(` Est/month: ${chalk4.yellow(formatDollars(summary.estimatedMonthlyCents))}`);
5174
5956
  const modelEntries = Object.entries(summary.byModel);
5175
5957
  if (modelEntries.length > 0) {
5176
5958
  lines.push("");
5177
- lines.push(chalk3.bold(" By Model"));
5959
+ lines.push(chalk4.bold(" By Model"));
5178
5960
  lines.push(` ${"Model".padEnd(40)} ${"Cost".padEnd(12)} ${"Tokens".padEnd(12)} Runs`);
5179
5961
  lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)}`);
5180
5962
  for (const [model, data] of modelEntries) {
@@ -5183,7 +5965,7 @@ function formatCostsTerminal(summary) {
5183
5965
  }
5184
5966
  if (summary.byScenario.length > 0) {
5185
5967
  lines.push("");
5186
- lines.push(chalk3.bold(" Top Scenarios by Cost"));
5968
+ lines.push(chalk4.bold(" Top Scenarios by Cost"));
5187
5969
  lines.push(` ${"Scenario".padEnd(40)} ${"Cost".padEnd(12)} ${"Tokens".padEnd(12)} Runs`);
5188
5970
  lines.push(` ${"\u2500".repeat(40)} ${"\u2500".repeat(12)} ${"\u2500".repeat(12)} ${"\u2500".repeat(6)}`);
5189
5971
  for (const s of summary.byScenario) {
@@ -5352,7 +6134,7 @@ function listTemplateNames() {
5352
6134
 
5353
6135
  // src/db/auth-presets.ts
5354
6136
  init_database();
5355
- function fromRow(row) {
6137
+ function fromRow2(row) {
5356
6138
  return {
5357
6139
  id: row.id,
5358
6140
  name: row.name,
@@ -5376,12 +6158,12 @@ function createAuthPreset(input) {
5376
6158
  function getAuthPreset(name) {
5377
6159
  const db2 = getDatabase();
5378
6160
  const row = db2.query("SELECT * FROM auth_presets WHERE name = ?").get(name);
5379
- return row ? fromRow(row) : null;
6161
+ return row ? fromRow2(row) : null;
5380
6162
  }
5381
6163
  function listAuthPresets() {
5382
6164
  const db2 = getDatabase();
5383
6165
  const rows = db2.query("SELECT * FROM auth_presets ORDER BY created_at DESC").all();
5384
- return rows.map(fromRow);
6166
+ return rows.map(fromRow2);
5385
6167
  }
5386
6168
  function deleteAuthPreset(name) {
5387
6169
  const db2 = getDatabase();
@@ -5390,7 +6172,156 @@ function deleteAuthPreset(name) {
5390
6172
  }
5391
6173
 
5392
6174
  // src/cli/index.tsx
5393
- import { existsSync as existsSync7, mkdirSync as mkdirSync4 } from "fs";
6175
+ init_flows();
6176
+
6177
+ // src/db/environments.ts
6178
+ init_database();
6179
+ function fromRow3(row) {
6180
+ return {
6181
+ id: row.id,
6182
+ name: row.name,
6183
+ url: row.url,
6184
+ authPresetName: row.auth_preset_name,
6185
+ projectId: row.project_id,
6186
+ isDefault: row.is_default === 1,
6187
+ createdAt: row.created_at
6188
+ };
6189
+ }
6190
+ function createEnvironment(input) {
6191
+ const db2 = getDatabase();
6192
+ const id = uuid();
6193
+ const timestamp = now();
6194
+ db2.query(`
6195
+ INSERT INTO environments (id, name, url, auth_preset_name, project_id, is_default, metadata, created_at)
6196
+ VALUES (?, ?, ?, ?, ?, ?, '{}', ?)
6197
+ `).run(id, input.name, input.url, input.authPresetName ?? null, input.projectId ?? null, input.isDefault ? 1 : 0, timestamp);
6198
+ return getEnvironment(input.name);
6199
+ }
6200
+ function getEnvironment(name) {
6201
+ const db2 = getDatabase();
6202
+ const row = db2.query("SELECT * FROM environments WHERE name = ?").get(name);
6203
+ return row ? fromRow3(row) : null;
6204
+ }
6205
+ function listEnvironments(projectId) {
6206
+ const db2 = getDatabase();
6207
+ if (projectId) {
6208
+ const rows2 = db2.query("SELECT * FROM environments WHERE project_id = ? ORDER BY is_default DESC, created_at DESC").all(projectId);
6209
+ return rows2.map(fromRow3);
6210
+ }
6211
+ const rows = db2.query("SELECT * FROM environments ORDER BY is_default DESC, created_at DESC").all();
6212
+ return rows.map(fromRow3);
6213
+ }
6214
+ function deleteEnvironment(name) {
6215
+ const db2 = getDatabase();
6216
+ const result = db2.query("DELETE FROM environments WHERE name = ?").run(name);
6217
+ return result.changes > 0;
6218
+ }
6219
+ function setDefaultEnvironment(name) {
6220
+ const db2 = getDatabase();
6221
+ db2.exec("UPDATE environments SET is_default = 0");
6222
+ const result = db2.query("UPDATE environments SET is_default = 1 WHERE name = ?").run(name);
6223
+ if (result.changes === 0) {
6224
+ throw new Error(`Environment not found: ${name}`);
6225
+ }
6226
+ }
6227
+ function getDefaultEnvironment() {
6228
+ const db2 = getDatabase();
6229
+ const row = db2.query("SELECT * FROM environments WHERE is_default = 1 LIMIT 1").get();
6230
+ return row ? fromRow3(row) : null;
6231
+ }
6232
+
6233
+ // src/lib/ci.ts
6234
+ function generateGitHubActionsWorkflow() {
6235
+ return `name: AI QA Tests
6236
+ on:
6237
+ pull_request:
6238
+ push:
6239
+ branches: [main]
6240
+
6241
+ jobs:
6242
+ test:
6243
+ runs-on: ubuntu-latest
6244
+ steps:
6245
+ - uses: actions/checkout@v4
6246
+ - uses: oven-sh/setup-bun@v2
6247
+ - run: bun install -g @hasna/testers
6248
+ - run: testers install-browser
6249
+ - run: testers run \${{ env.TEST_URL }} --json --output results.json
6250
+ env:
6251
+ ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
6252
+ TEST_URL: \${{ vars.TEST_URL || 'http://localhost:3000' }}
6253
+ - run: testers report --latest --output report.html
6254
+ - uses: actions/upload-artifact@v4
6255
+ if: always()
6256
+ with:
6257
+ name: test-report
6258
+ path: |
6259
+ report.html
6260
+ results.json
6261
+ `;
6262
+ }
6263
+
6264
+ // src/lib/assertions.ts
6265
+ function parseAssertionString(str) {
6266
+ const trimmed = str.trim();
6267
+ if (trimmed === "no-console-errors") {
6268
+ return { type: "no_console_errors", description: "No console errors" };
6269
+ }
6270
+ if (trimmed.startsWith("url:contains:")) {
6271
+ const expected = trimmed.slice("url:contains:".length);
6272
+ return { type: "url_contains", expected, description: `URL contains "${expected}"` };
6273
+ }
6274
+ if (trimmed.startsWith("title:contains:")) {
6275
+ const expected = trimmed.slice("title:contains:".length);
6276
+ return { type: "title_contains", expected, description: `Title contains "${expected}"` };
6277
+ }
6278
+ if (trimmed.startsWith("count:")) {
6279
+ const rest = trimmed.slice("count:".length);
6280
+ const eqIdx = rest.indexOf(" eq:");
6281
+ if (eqIdx === -1) {
6282
+ throw new Error(`Invalid count assertion format: ${str}. Expected "count:<selector> eq:<number>"`);
6283
+ }
6284
+ const selector = rest.slice(0, eqIdx);
6285
+ const expected = parseInt(rest.slice(eqIdx + " eq:".length), 10);
6286
+ return { type: "element_count", selector, expected, description: `${selector} count equals ${expected}` };
6287
+ }
6288
+ if (trimmed.startsWith("text:")) {
6289
+ const rest = trimmed.slice("text:".length);
6290
+ const containsIdx = rest.indexOf(" contains:");
6291
+ const equalsIdx = rest.indexOf(" equals:");
6292
+ if (containsIdx !== -1) {
6293
+ const selector = rest.slice(0, containsIdx);
6294
+ const expected = rest.slice(containsIdx + " contains:".length);
6295
+ return { type: "text_contains", selector, expected, description: `${selector} text contains "${expected}"` };
6296
+ }
6297
+ if (equalsIdx !== -1) {
6298
+ const selector = rest.slice(0, equalsIdx);
6299
+ const expected = rest.slice(equalsIdx + " equals:".length);
6300
+ return { type: "text_equals", selector, expected, description: `${selector} text equals "${expected}"` };
6301
+ }
6302
+ throw new Error(`Invalid text assertion format: ${str}. Expected "text:<selector> contains:<text>" or "text:<selector> equals:<text>"`);
6303
+ }
6304
+ if (trimmed.startsWith("selector:")) {
6305
+ const rest = trimmed.slice("selector:".length);
6306
+ const lastSpace = rest.lastIndexOf(" ");
6307
+ if (lastSpace === -1) {
6308
+ throw new Error(`Invalid selector assertion format: ${str}. Expected "selector:<selector> visible" or "selector:<selector> not-visible"`);
6309
+ }
6310
+ const selector = rest.slice(0, lastSpace);
6311
+ const action = rest.slice(lastSpace + 1);
6312
+ if (action === "visible") {
6313
+ return { type: "visible", selector, description: `${selector} is visible` };
6314
+ }
6315
+ if (action === "not-visible") {
6316
+ return { type: "not_visible", selector, description: `${selector} is not visible` };
6317
+ }
6318
+ throw new Error(`Unknown selector action: "${action}". Expected "visible" or "not-visible"`);
6319
+ }
6320
+ throw new Error(`Cannot parse assertion: "${str}". See --help for assertion formats.`);
6321
+ }
6322
+
6323
+ // src/cli/index.tsx
6324
+ import { existsSync as existsSync8, mkdirSync as mkdirSync4 } from "fs";
5394
6325
  function formatToolInput(input) {
5395
6326
  const parts = [];
5396
6327
  for (const [key, value] of Object.entries(input)) {
@@ -5406,8 +6337,8 @@ var CONFIG_DIR2 = join6(process.env["HOME"] ?? "~", ".testers");
5406
6337
  var CONFIG_PATH2 = join6(CONFIG_DIR2, "config.json");
5407
6338
  function getActiveProject() {
5408
6339
  try {
5409
- if (existsSync7(CONFIG_PATH2)) {
5410
- const raw = JSON.parse(readFileSync4(CONFIG_PATH2, "utf-8"));
6340
+ if (existsSync8(CONFIG_PATH2)) {
6341
+ const raw = JSON.parse(readFileSync6(CONFIG_PATH2, "utf-8"));
5411
6342
  return raw.activeProject ?? undefined;
5412
6343
  }
5413
6344
  } catch {}
@@ -5422,21 +6353,25 @@ program2.command("add <name>").description("Create a new test scenario").option(
5422
6353
  }, []).option("-t, --tag <tag>", "Tag (repeatable)", (val, acc) => {
5423
6354
  acc.push(val);
5424
6355
  return acc;
5425
- }, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").action((name, opts) => {
6356
+ }, []).option("-p, --priority <level>", "Priority level", "medium").option("-m, --model <model>", "AI model to use").option("--path <path>", "Target path on the URL").option("--auth", "Requires authentication", false).option("--timeout <ms>", "Timeout in milliseconds").option("--project <id>", "Project ID").option("--template <name>", "Seed scenarios from a template (auth, crud, forms, nav, a11y)").option("--assert <assertion>", "Structured assertion (repeatable). Formats: selector:<sel> visible, text:<sel> contains:<text>, no-console-errors, url:contains:<path>, title:contains:<text>, count:<sel> eq:<n>", (val, acc) => {
6357
+ acc.push(val);
6358
+ return acc;
6359
+ }, []).action((name, opts) => {
5426
6360
  try {
5427
6361
  if (opts.template) {
5428
6362
  const template = getTemplate(opts.template);
5429
6363
  if (!template) {
5430
- console.error(chalk4.red(`Unknown template: ${opts.template}. Available: ${listTemplateNames().join(", ")}`));
6364
+ console.error(chalk5.red(`Unknown template: ${opts.template}. Available: ${listTemplateNames().join(", ")}`));
5431
6365
  process.exit(1);
5432
6366
  }
5433
6367
  const projectId2 = resolveProject(opts.project);
5434
6368
  for (const input of template) {
5435
6369
  const s = createScenario({ ...input, projectId: projectId2 });
5436
- console.log(chalk4.green(` Created ${s.shortId}: ${s.name}`));
6370
+ console.log(chalk5.green(` Created ${s.shortId}: ${s.name}`));
5437
6371
  }
5438
6372
  return;
5439
6373
  }
6374
+ const assertions = opts.assert.map(parseAssertionString);
5440
6375
  const projectId = resolveProject(opts.project);
5441
6376
  const scenario = createScenario({
5442
6377
  name,
@@ -5448,11 +6383,12 @@ program2.command("add <name>").description("Create a new test scenario").option(
5448
6383
  targetPath: opts.path,
5449
6384
  requiresAuth: opts.auth,
5450
6385
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
6386
+ assertions: assertions.length > 0 ? assertions : undefined,
5451
6387
  projectId
5452
6388
  });
5453
- console.log(chalk4.green(`Created scenario ${chalk4.bold(scenario.shortId)}: ${scenario.name}`));
6389
+ console.log(chalk5.green(`Created scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
5454
6390
  } catch (error) {
5455
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6391
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5456
6392
  process.exit(1);
5457
6393
  }
5458
6394
  });
@@ -5466,7 +6402,7 @@ program2.command("list").description("List test scenarios").option("-t, --tag <t
5466
6402
  });
5467
6403
  console.log(formatScenarioList(scenarios));
5468
6404
  } catch (error) {
5469
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6405
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5470
6406
  process.exit(1);
5471
6407
  }
5472
6408
  });
@@ -5474,33 +6410,33 @@ program2.command("show <id>").description("Show scenario details").action((id) =
5474
6410
  try {
5475
6411
  const scenario = getScenario(id) ?? getScenarioByShortId(id);
5476
6412
  if (!scenario) {
5477
- console.error(chalk4.red(`Scenario not found: ${id}`));
6413
+ console.error(chalk5.red(`Scenario not found: ${id}`));
5478
6414
  process.exit(1);
5479
6415
  }
5480
6416
  console.log("");
5481
- console.log(chalk4.bold(` Scenario ${scenario.shortId}`));
6417
+ console.log(chalk5.bold(` Scenario ${scenario.shortId}`));
5482
6418
  console.log(` Name: ${scenario.name}`);
5483
- console.log(` ID: ${chalk4.dim(scenario.id)}`);
6419
+ console.log(` ID: ${chalk5.dim(scenario.id)}`);
5484
6420
  console.log(` Description: ${scenario.description}`);
5485
6421
  console.log(` Priority: ${scenario.priority}`);
5486
- console.log(` Model: ${scenario.model ?? chalk4.dim("default")}`);
5487
- console.log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") : chalk4.dim("none")}`);
5488
- console.log(` Path: ${scenario.targetPath ?? chalk4.dim("none")}`);
6422
+ console.log(` Model: ${scenario.model ?? chalk5.dim("default")}`);
6423
+ console.log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") : chalk5.dim("none")}`);
6424
+ console.log(` Path: ${scenario.targetPath ?? chalk5.dim("none")}`);
5489
6425
  console.log(` Auth: ${scenario.requiresAuth ? "yes" : "no"}`);
5490
- console.log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` : chalk4.dim("default")}`);
6426
+ console.log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` : chalk5.dim("default")}`);
5491
6427
  console.log(` Version: ${scenario.version}`);
5492
6428
  console.log(` Created: ${scenario.createdAt}`);
5493
6429
  console.log(` Updated: ${scenario.updatedAt}`);
5494
6430
  if (scenario.steps.length > 0) {
5495
6431
  console.log("");
5496
- console.log(chalk4.bold(" Steps:"));
6432
+ console.log(chalk5.bold(" Steps:"));
5497
6433
  for (let i = 0;i < scenario.steps.length; i++) {
5498
6434
  console.log(` ${i + 1}. ${scenario.steps[i]}`);
5499
6435
  }
5500
6436
  }
5501
6437
  console.log("");
5502
6438
  } catch (error) {
5503
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6439
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5504
6440
  process.exit(1);
5505
6441
  }
5506
6442
  });
@@ -5514,7 +6450,7 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
5514
6450
  try {
5515
6451
  const scenario = getScenario(id) ?? getScenarioByShortId(id);
5516
6452
  if (!scenario) {
5517
- console.error(chalk4.red(`Scenario not found: ${id}`));
6453
+ console.error(chalk5.red(`Scenario not found: ${id}`));
5518
6454
  process.exit(1);
5519
6455
  }
5520
6456
  const updated = updateScenario(scenario.id, {
@@ -5525,9 +6461,9 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
5525
6461
  priority: opts.priority,
5526
6462
  model: opts.model
5527
6463
  }, scenario.version);
5528
- console.log(chalk4.green(`Updated scenario ${chalk4.bold(updated.shortId)}: ${updated.name}`));
6464
+ console.log(chalk5.green(`Updated scenario ${chalk5.bold(updated.shortId)}: ${updated.name}`));
5529
6465
  } catch (error) {
5530
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6466
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5531
6467
  process.exit(1);
5532
6468
  }
5533
6469
  });
@@ -5535,30 +6471,50 @@ program2.command("delete <id>").description("Delete a scenario").action((id) =>
5535
6471
  try {
5536
6472
  const scenario = getScenario(id) ?? getScenarioByShortId(id);
5537
6473
  if (!scenario) {
5538
- console.error(chalk4.red(`Scenario not found: ${id}`));
6474
+ console.error(chalk5.red(`Scenario not found: ${id}`));
5539
6475
  process.exit(1);
5540
6476
  }
5541
6477
  const deleted = deleteScenario(scenario.id);
5542
6478
  if (deleted) {
5543
- console.log(chalk4.green(`Deleted scenario ${scenario.shortId}: ${scenario.name}`));
6479
+ console.log(chalk5.green(`Deleted scenario ${scenario.shortId}: ${scenario.name}`));
5544
6480
  } else {
5545
- console.error(chalk4.red(`Failed to delete scenario: ${id}`));
6481
+ console.error(chalk5.red(`Failed to delete scenario: ${id}`));
5546
6482
  process.exit(1);
5547
6483
  }
5548
6484
  } catch (error) {
5549
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6485
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5550
6486
  process.exit(1);
5551
6487
  }
5552
6488
  });
5553
- program2.command("run <url> [description]").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
6489
+ program2.command("run [url] [description]").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
5554
6490
  acc.push(val);
5555
6491
  return acc;
5556
- }, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).action(async (url, description, opts) => {
6492
+ }, []).option("-s, --scenario <id>", "Run specific scenario ID").option("-p, --priority <level>", "Filter by priority").option("--headed", "Run browser in headed mode", false).option("-m, --model <model>", "AI model to use").option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).option("-o, --output <filepath>", "Write JSON results to file").option("--timeout <ms>", "Timeout in milliseconds").option("--from-todos", "Import scenarios from todos before running", false).option("--project <id>", "Project ID").option("-b, --background", "Start run in background and return immediately", false).option("--env <name>", "Use a named environment for the URL").action(async (urlArg, description, opts) => {
5557
6493
  try {
5558
6494
  const projectId = resolveProject(opts.project);
6495
+ let url = urlArg;
6496
+ if (!url && opts.env) {
6497
+ const env = getEnvironment(opts.env);
6498
+ if (!env) {
6499
+ console.error(chalk5.red(`Environment not found: ${opts.env}`));
6500
+ process.exit(1);
6501
+ }
6502
+ url = env.url;
6503
+ }
6504
+ if (!url) {
6505
+ const defaultEnv = getDefaultEnvironment();
6506
+ if (defaultEnv) {
6507
+ url = defaultEnv.url;
6508
+ console.log(chalk5.dim(`Using default environment: ${defaultEnv.name} (${defaultEnv.url})`));
6509
+ }
6510
+ }
6511
+ if (!url) {
6512
+ console.error(chalk5.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
6513
+ process.exit(1);
6514
+ }
5559
6515
  if (opts.fromTodos) {
5560
6516
  const result = importFromTodos({ projectId });
5561
- console.log(chalk4.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
6517
+ console.log(chalk5.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
5562
6518
  }
5563
6519
  if (opts.background) {
5564
6520
  if (description) {
@@ -5575,51 +6531,51 @@ program2.command("run <url> [description]").description("Run test scenarios agai
5575
6531
  timeout: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
5576
6532
  projectId
5577
6533
  });
5578
- console.log(chalk4.green(`Run started in background: ${chalk4.bold(runId.slice(0, 8))}`));
5579
- console.log(chalk4.dim(` Scenarios: ${scenarioCount}`));
5580
- console.log(chalk4.dim(` URL: ${url}`));
5581
- console.log(chalk4.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
6534
+ console.log(chalk5.green(`Run started in background: ${chalk5.bold(runId.slice(0, 8))}`));
6535
+ console.log(chalk5.dim(` Scenarios: ${scenarioCount}`));
6536
+ console.log(chalk5.dim(` URL: ${url}`));
6537
+ console.log(chalk5.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
5582
6538
  process.exit(0);
5583
6539
  }
5584
6540
  if (!opts.json && !opts.output) {
5585
6541
  onRunEvent((event) => {
5586
6542
  switch (event.type) {
5587
6543
  case "scenario:start":
5588
- console.log(chalk4.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
6544
+ console.log(chalk5.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
5589
6545
  break;
5590
6546
  case "step:thinking":
5591
6547
  if (event.thinking) {
5592
6548
  const preview = event.thinking.length > 120 ? event.thinking.slice(0, 120) + "..." : event.thinking;
5593
- console.log(chalk4.dim(` [think] ${preview}`));
6549
+ console.log(chalk5.dim(` [think] ${preview}`));
5594
6550
  }
5595
6551
  break;
5596
6552
  case "step:tool_call":
5597
- console.log(chalk4.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
6553
+ console.log(chalk5.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
5598
6554
  break;
5599
6555
  case "step:tool_result":
5600
6556
  if (event.toolName === "report_result") {
5601
- console.log(chalk4.bold(` [result] ${event.toolResult}`));
6557
+ console.log(chalk5.bold(` [result] ${event.toolResult}`));
5602
6558
  } else {
5603
6559
  const resultPreview = (event.toolResult ?? "").length > 100 ? (event.toolResult ?? "").slice(0, 100) + "..." : event.toolResult ?? "";
5604
- console.log(chalk4.dim(` [done] ${resultPreview}`));
6560
+ console.log(chalk5.dim(` [done] ${resultPreview}`));
5605
6561
  }
5606
6562
  break;
5607
6563
  case "screenshot:captured":
5608
- console.log(chalk4.dim(` [screenshot] ${event.screenshotPath}`));
6564
+ console.log(chalk5.dim(` [screenshot] ${event.screenshotPath}`));
5609
6565
  break;
5610
6566
  case "scenario:pass":
5611
- console.log(chalk4.green(` [PASS] ${event.scenarioName}`));
6567
+ console.log(chalk5.green(` [PASS] ${event.scenarioName}`));
5612
6568
  break;
5613
6569
  case "scenario:fail":
5614
- console.log(chalk4.red(` [FAIL] ${event.scenarioName}`));
6570
+ console.log(chalk5.red(` [FAIL] ${event.scenarioName}`));
5615
6571
  break;
5616
6572
  case "scenario:error":
5617
- console.log(chalk4.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
6573
+ console.log(chalk5.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
5618
6574
  break;
5619
6575
  }
5620
6576
  });
5621
6577
  console.log("");
5622
- console.log(chalk4.bold(` Running tests against ${url}`));
6578
+ console.log(chalk5.bold(` Running tests against ${url}`));
5623
6579
  console.log("");
5624
6580
  }
5625
6581
  if (description) {
@@ -5642,7 +6598,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
5642
6598
  const jsonOutput = formatJSON(run2, results2);
5643
6599
  if (opts.output) {
5644
6600
  writeFileSync3(opts.output, jsonOutput, "utf-8");
5645
- console.log(chalk4.green(`Results written to ${opts.output}`));
6601
+ console.log(chalk5.green(`Results written to ${opts.output}`));
5646
6602
  }
5647
6603
  if (opts.json) {
5648
6604
  console.log(jsonOutput);
@@ -5667,7 +6623,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
5667
6623
  const jsonOutput = formatJSON(run, results);
5668
6624
  if (opts.output) {
5669
6625
  writeFileSync3(opts.output, jsonOutput, "utf-8");
5670
- console.log(chalk4.green(`Results written to ${opts.output}`));
6626
+ console.log(chalk5.green(`Results written to ${opts.output}`));
5671
6627
  }
5672
6628
  if (opts.json) {
5673
6629
  console.log(jsonOutput);
@@ -5677,7 +6633,7 @@ program2.command("run <url> [description]").description("Run test scenarios agai
5677
6633
  }
5678
6634
  process.exit(getExitCode(run));
5679
6635
  } catch (error) {
5680
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6636
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5681
6637
  process.exit(1);
5682
6638
  }
5683
6639
  });
@@ -5689,7 +6645,7 @@ program2.command("runs").description("List past test runs").option("--status <st
5689
6645
  });
5690
6646
  console.log(formatRunList(runs));
5691
6647
  } catch (error) {
5692
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6648
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5693
6649
  process.exit(1);
5694
6650
  }
5695
6651
  });
@@ -5697,13 +6653,13 @@ program2.command("results <run-id>").description("Show results for a test run").
5697
6653
  try {
5698
6654
  const run = getRun(runId);
5699
6655
  if (!run) {
5700
- console.error(chalk4.red(`Run not found: ${runId}`));
6656
+ console.error(chalk5.red(`Run not found: ${runId}`));
5701
6657
  process.exit(1);
5702
6658
  }
5703
6659
  const results = getResultsByRun(run.id);
5704
6660
  console.log(formatTerminal(run, results));
5705
6661
  } catch (error) {
5706
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6662
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5707
6663
  process.exit(1);
5708
6664
  }
5709
6665
  });
@@ -5714,23 +6670,23 @@ program2.command("screenshots <id>").description("List screenshots for a run or
5714
6670
  const results = getResultsByRun(run.id);
5715
6671
  let total = 0;
5716
6672
  console.log("");
5717
- console.log(chalk4.bold(` Screenshots for run ${run.id.slice(0, 8)}`));
6673
+ console.log(chalk5.bold(` Screenshots for run ${run.id.slice(0, 8)}`));
5718
6674
  console.log("");
5719
6675
  for (const result of results) {
5720
6676
  const screenshots2 = listScreenshots(result.id);
5721
6677
  if (screenshots2.length > 0) {
5722
6678
  const scenario = getScenario(result.scenarioId);
5723
6679
  const label = scenario ? `${scenario.shortId}: ${scenario.name}` : result.scenarioId.slice(0, 8);
5724
- console.log(chalk4.bold(` ${label}`));
6680
+ console.log(chalk5.bold(` ${label}`));
5725
6681
  for (const ss of screenshots2) {
5726
- console.log(` ${chalk4.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk4.dim(ss.filePath)}`);
6682
+ console.log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
5727
6683
  total++;
5728
6684
  }
5729
6685
  console.log("");
5730
6686
  }
5731
6687
  }
5732
6688
  if (total === 0) {
5733
- console.log(chalk4.dim(" No screenshots found."));
6689
+ console.log(chalk5.dim(" No screenshots found."));
5734
6690
  console.log("");
5735
6691
  }
5736
6692
  return;
@@ -5738,18 +6694,18 @@ program2.command("screenshots <id>").description("List screenshots for a run or
5738
6694
  const screenshots = listScreenshots(id);
5739
6695
  if (screenshots.length > 0) {
5740
6696
  console.log("");
5741
- console.log(chalk4.bold(` Screenshots for result ${id.slice(0, 8)}`));
6697
+ console.log(chalk5.bold(` Screenshots for result ${id.slice(0, 8)}`));
5742
6698
  console.log("");
5743
6699
  for (const ss of screenshots) {
5744
- console.log(` ${chalk4.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk4.dim(ss.filePath)}`);
6700
+ console.log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
5745
6701
  }
5746
6702
  console.log("");
5747
6703
  return;
5748
6704
  }
5749
- console.error(chalk4.red(`No screenshots found for: ${id}`));
6705
+ console.error(chalk5.red(`No screenshots found for: ${id}`));
5750
6706
  process.exit(1);
5751
6707
  } catch (error) {
5752
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6708
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5753
6709
  process.exit(1);
5754
6710
  }
5755
6711
  });
@@ -5758,12 +6714,12 @@ program2.command("import <dir>").description("Import markdown test files as scen
5758
6714
  const absDir = resolve(dir);
5759
6715
  const files = readdirSync(absDir).filter((f) => f.endsWith(".md"));
5760
6716
  if (files.length === 0) {
5761
- console.log(chalk4.dim("No .md files found in directory."));
6717
+ console.log(chalk5.dim("No .md files found in directory."));
5762
6718
  return;
5763
6719
  }
5764
6720
  let imported = 0;
5765
6721
  for (const file of files) {
5766
- const content = readFileSync4(join6(absDir, file), "utf-8");
6722
+ const content = readFileSync6(join6(absDir, file), "utf-8");
5767
6723
  const lines = content.split(`
5768
6724
  `);
5769
6725
  let name = file.replace(/\.md$/, "");
@@ -5788,13 +6744,13 @@ program2.command("import <dir>").description("Import markdown test files as scen
5788
6744
  description: descriptionLines.join(" ") || name,
5789
6745
  steps
5790
6746
  });
5791
- console.log(chalk4.green(` Imported ${chalk4.bold(scenario.shortId)}: ${scenario.name}`));
6747
+ console.log(chalk5.green(` Imported ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
5792
6748
  imported++;
5793
6749
  }
5794
6750
  console.log("");
5795
- console.log(chalk4.green(`Imported ${imported} scenario(s) from ${absDir}`));
6751
+ console.log(chalk5.green(`Imported ${imported} scenario(s) from ${absDir}`));
5796
6752
  } catch (error) {
5797
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6753
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5798
6754
  process.exit(1);
5799
6755
  }
5800
6756
  });
@@ -5803,7 +6759,7 @@ program2.command("config").description("Show current configuration").action(() =
5803
6759
  const config = loadConfig();
5804
6760
  console.log(JSON.stringify(config, null, 2));
5805
6761
  } catch (error) {
5806
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6762
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5807
6763
  process.exit(1);
5808
6764
  }
5809
6765
  });
@@ -5813,25 +6769,25 @@ program2.command("status").description("Show database and auth status").action((
5813
6769
  const hasApiKey = !!config.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
5814
6770
  const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
5815
6771
  console.log("");
5816
- console.log(chalk4.bold(" Open Testers Status"));
6772
+ console.log(chalk5.bold(" Open Testers Status"));
5817
6773
  console.log("");
5818
- console.log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk4.green("set") : chalk4.red("not set")}`);
6774
+ console.log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk5.green("set") : chalk5.red("not set")}`);
5819
6775
  console.log(` Database: ${dbPath}`);
5820
6776
  console.log(` Default model: ${config.defaultModel}`);
5821
6777
  console.log(` Screenshots dir: ${config.screenshots.dir}`);
5822
6778
  console.log("");
5823
6779
  } catch (error) {
5824
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6780
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5825
6781
  process.exit(1);
5826
6782
  }
5827
6783
  });
5828
6784
  program2.command("install-browser").description("Install Playwright Chromium browser").action(async () => {
5829
6785
  try {
5830
- console.log(chalk4.blue("Installing Playwright Chromium..."));
6786
+ console.log(chalk5.blue("Installing Playwright Chromium..."));
5831
6787
  await installBrowser();
5832
- console.log(chalk4.green("Browser installed successfully."));
6788
+ console.log(chalk5.green("Browser installed successfully."));
5833
6789
  } catch (error) {
5834
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6790
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5835
6791
  process.exit(1);
5836
6792
  }
5837
6793
  });
@@ -5843,9 +6799,9 @@ projectCmd.command("create <name>").description("Create a new project").option("
5843
6799
  path: opts.path,
5844
6800
  description: opts.description
5845
6801
  });
5846
- console.log(chalk4.green(`Created project ${chalk4.bold(project.name)} (${project.id})`));
6802
+ console.log(chalk5.green(`Created project ${chalk5.bold(project.name)} (${project.id})`));
5847
6803
  } catch (error) {
5848
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6804
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5849
6805
  process.exit(1);
5850
6806
  }
5851
6807
  });
@@ -5853,20 +6809,20 @@ projectCmd.command("list").description("List all projects").action(() => {
5853
6809
  try {
5854
6810
  const projects = listProjects();
5855
6811
  if (projects.length === 0) {
5856
- console.log(chalk4.dim("No projects found."));
6812
+ console.log(chalk5.dim("No projects found."));
5857
6813
  return;
5858
6814
  }
5859
6815
  console.log("");
5860
- console.log(chalk4.bold(" Projects"));
6816
+ console.log(chalk5.bold(" Projects"));
5861
6817
  console.log("");
5862
6818
  console.log(` ${"ID".padEnd(38)} ${"Name".padEnd(24)} ${"Path".padEnd(30)} Created`);
5863
6819
  console.log(` ${"\u2500".repeat(38)} ${"\u2500".repeat(24)} ${"\u2500".repeat(30)} ${"\u2500".repeat(20)}`);
5864
6820
  for (const p of projects) {
5865
- console.log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk4.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
6821
+ console.log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk5.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
5866
6822
  }
5867
6823
  console.log("");
5868
6824
  } catch (error) {
5869
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6825
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5870
6826
  process.exit(1);
5871
6827
  }
5872
6828
  });
@@ -5874,39 +6830,39 @@ projectCmd.command("show <id>").description("Show project details").action((id)
5874
6830
  try {
5875
6831
  const project = getProject(id);
5876
6832
  if (!project) {
5877
- console.error(chalk4.red(`Project not found: ${id}`));
6833
+ console.error(chalk5.red(`Project not found: ${id}`));
5878
6834
  process.exit(1);
5879
6835
  }
5880
6836
  console.log("");
5881
- console.log(chalk4.bold(` Project: ${project.name}`));
6837
+ console.log(chalk5.bold(` Project: ${project.name}`));
5882
6838
  console.log(` ID: ${project.id}`);
5883
- console.log(` Path: ${project.path ?? chalk4.dim("none")}`);
5884
- console.log(` Description: ${project.description ?? chalk4.dim("none")}`);
6839
+ console.log(` Path: ${project.path ?? chalk5.dim("none")}`);
6840
+ console.log(` Description: ${project.description ?? chalk5.dim("none")}`);
5885
6841
  console.log(` Created: ${project.createdAt}`);
5886
6842
  console.log(` Updated: ${project.updatedAt}`);
5887
6843
  console.log("");
5888
6844
  } catch (error) {
5889
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6845
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5890
6846
  process.exit(1);
5891
6847
  }
5892
6848
  });
5893
6849
  projectCmd.command("use <name>").description("Set active project (find or create)").action((name) => {
5894
6850
  try {
5895
6851
  const project = ensureProject(name, process.cwd());
5896
- if (!existsSync7(CONFIG_DIR2)) {
6852
+ if (!existsSync8(CONFIG_DIR2)) {
5897
6853
  mkdirSync4(CONFIG_DIR2, { recursive: true });
5898
6854
  }
5899
6855
  let config = {};
5900
- if (existsSync7(CONFIG_PATH2)) {
6856
+ if (existsSync8(CONFIG_PATH2)) {
5901
6857
  try {
5902
- config = JSON.parse(readFileSync4(CONFIG_PATH2, "utf-8"));
6858
+ config = JSON.parse(readFileSync6(CONFIG_PATH2, "utf-8"));
5903
6859
  } catch {}
5904
6860
  }
5905
6861
  config.activeProject = project.id;
5906
6862
  writeFileSync3(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
5907
- console.log(chalk4.green(`Active project set to ${chalk4.bold(project.name)} (${project.id})`));
6863
+ console.log(chalk5.green(`Active project set to ${chalk5.bold(project.name)} (${project.id})`));
5908
6864
  } catch (error) {
5909
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6865
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5910
6866
  process.exit(1);
5911
6867
  }
5912
6868
  });
@@ -5931,12 +6887,12 @@ scheduleCmd.command("create <name>").description("Create a new schedule").requir
5931
6887
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
5932
6888
  projectId
5933
6889
  });
5934
- console.log(chalk4.green(`Created schedule ${chalk4.bold(schedule.name)} (${schedule.id})`));
6890
+ console.log(chalk5.green(`Created schedule ${chalk5.bold(schedule.name)} (${schedule.id})`));
5935
6891
  if (schedule.nextRunAt) {
5936
- console.log(chalk4.dim(` Next run at: ${schedule.nextRunAt}`));
6892
+ console.log(chalk5.dim(` Next run at: ${schedule.nextRunAt}`));
5937
6893
  }
5938
6894
  } catch (error) {
5939
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6895
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5940
6896
  process.exit(1);
5941
6897
  }
5942
6898
  });
@@ -5948,23 +6904,23 @@ scheduleCmd.command("list").description("List schedules").option("--project <id>
5948
6904
  enabled: opts.enabled ? true : undefined
5949
6905
  });
5950
6906
  if (schedules.length === 0) {
5951
- console.log(chalk4.dim("No schedules found."));
6907
+ console.log(chalk5.dim("No schedules found."));
5952
6908
  return;
5953
6909
  }
5954
6910
  console.log("");
5955
- console.log(chalk4.bold(" Schedules"));
6911
+ console.log(chalk5.bold(" Schedules"));
5956
6912
  console.log("");
5957
6913
  console.log(` ${"Name".padEnd(20)} ${"Cron".padEnd(18)} ${"URL".padEnd(30)} ${"Enabled".padEnd(9)} ${"Next Run".padEnd(22)} Last Run`);
5958
6914
  console.log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(30)} ${"\u2500".repeat(9)} ${"\u2500".repeat(22)} ${"\u2500".repeat(22)}`);
5959
6915
  for (const s of schedules) {
5960
- const enabled = s.enabled ? chalk4.green("yes") : chalk4.red("no");
5961
- const nextRun = s.nextRunAt ?? chalk4.dim("\u2014");
5962
- const lastRun = s.lastRunAt ?? chalk4.dim("\u2014");
6916
+ const enabled = s.enabled ? chalk5.green("yes") : chalk5.red("no");
6917
+ const nextRun = s.nextRunAt ?? chalk5.dim("\u2014");
6918
+ const lastRun = s.lastRunAt ?? chalk5.dim("\u2014");
5963
6919
  console.log(` ${s.name.padEnd(20)} ${s.cronExpression.padEnd(18)} ${s.url.padEnd(30)} ${enabled.toString().padEnd(9)} ${nextRun.toString().padEnd(22)} ${lastRun}`);
5964
6920
  }
5965
6921
  console.log("");
5966
6922
  } catch (error) {
5967
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6923
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5968
6924
  process.exit(1);
5969
6925
  }
5970
6926
  });
@@ -5972,47 +6928,47 @@ scheduleCmd.command("show <id>").description("Show schedule details").action((id
5972
6928
  try {
5973
6929
  const schedule = getSchedule(id);
5974
6930
  if (!schedule) {
5975
- console.error(chalk4.red(`Schedule not found: ${id}`));
6931
+ console.error(chalk5.red(`Schedule not found: ${id}`));
5976
6932
  process.exit(1);
5977
6933
  }
5978
6934
  console.log("");
5979
- console.log(chalk4.bold(` Schedule: ${schedule.name}`));
6935
+ console.log(chalk5.bold(` Schedule: ${schedule.name}`));
5980
6936
  console.log(` ID: ${schedule.id}`);
5981
6937
  console.log(` Cron: ${schedule.cronExpression}`);
5982
6938
  console.log(` URL: ${schedule.url}`);
5983
- console.log(` Enabled: ${schedule.enabled ? chalk4.green("yes") : chalk4.red("no")}`);
5984
- console.log(` Model: ${schedule.model ?? chalk4.dim("default")}`);
6939
+ console.log(` Enabled: ${schedule.enabled ? chalk5.green("yes") : chalk5.red("no")}`);
6940
+ console.log(` Model: ${schedule.model ?? chalk5.dim("default")}`);
5985
6941
  console.log(` Headed: ${schedule.headed ? "yes" : "no"}`);
5986
6942
  console.log(` Parallel: ${schedule.parallel}`);
5987
- console.log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk4.dim("default")}`);
5988
- console.log(` Project: ${schedule.projectId ?? chalk4.dim("none")}`);
6943
+ console.log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk5.dim("default")}`);
6944
+ console.log(` Project: ${schedule.projectId ?? chalk5.dim("none")}`);
5989
6945
  console.log(` Filter: ${JSON.stringify(schedule.scenarioFilter)}`);
5990
- console.log(` Next run: ${schedule.nextRunAt ?? chalk4.dim("not scheduled")}`);
5991
- console.log(` Last run: ${schedule.lastRunAt ?? chalk4.dim("never")}`);
5992
- console.log(` Last run ID: ${schedule.lastRunId ?? chalk4.dim("none")}`);
6946
+ console.log(` Next run: ${schedule.nextRunAt ?? chalk5.dim("not scheduled")}`);
6947
+ console.log(` Last run: ${schedule.lastRunAt ?? chalk5.dim("never")}`);
6948
+ console.log(` Last run ID: ${schedule.lastRunId ?? chalk5.dim("none")}`);
5993
6949
  console.log(` Created: ${schedule.createdAt}`);
5994
6950
  console.log(` Updated: ${schedule.updatedAt}`);
5995
6951
  console.log("");
5996
6952
  } catch (error) {
5997
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6953
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
5998
6954
  process.exit(1);
5999
6955
  }
6000
6956
  });
6001
6957
  scheduleCmd.command("enable <id>").description("Enable a schedule").action((id) => {
6002
6958
  try {
6003
6959
  const schedule = updateSchedule(id, { enabled: true });
6004
- console.log(chalk4.green(`Enabled schedule ${chalk4.bold(schedule.name)}`));
6960
+ console.log(chalk5.green(`Enabled schedule ${chalk5.bold(schedule.name)}`));
6005
6961
  } catch (error) {
6006
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6962
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6007
6963
  process.exit(1);
6008
6964
  }
6009
6965
  });
6010
6966
  scheduleCmd.command("disable <id>").description("Disable a schedule").action((id) => {
6011
6967
  try {
6012
6968
  const schedule = updateSchedule(id, { enabled: false });
6013
- console.log(chalk4.green(`Disabled schedule ${chalk4.bold(schedule.name)}`));
6969
+ console.log(chalk5.green(`Disabled schedule ${chalk5.bold(schedule.name)}`));
6014
6970
  } catch (error) {
6015
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6971
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6016
6972
  process.exit(1);
6017
6973
  }
6018
6974
  });
@@ -6020,13 +6976,13 @@ scheduleCmd.command("delete <id>").description("Delete a schedule").action((id)
6020
6976
  try {
6021
6977
  const deleted = deleteSchedule(id);
6022
6978
  if (deleted) {
6023
- console.log(chalk4.green(`Deleted schedule: ${id}`));
6979
+ console.log(chalk5.green(`Deleted schedule: ${id}`));
6024
6980
  } else {
6025
- console.error(chalk4.red(`Schedule not found: ${id}`));
6981
+ console.error(chalk5.red(`Schedule not found: ${id}`));
6026
6982
  process.exit(1);
6027
6983
  }
6028
6984
  } catch (error) {
6029
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6985
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6030
6986
  process.exit(1);
6031
6987
  }
6032
6988
  });
@@ -6034,11 +6990,11 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
6034
6990
  try {
6035
6991
  const schedule = getSchedule(id);
6036
6992
  if (!schedule) {
6037
- console.error(chalk4.red(`Schedule not found: ${id}`));
6993
+ console.error(chalk5.red(`Schedule not found: ${id}`));
6038
6994
  process.exit(1);
6039
6995
  return;
6040
6996
  }
6041
- console.log(chalk4.blue(`Running schedule ${chalk4.bold(schedule.name)} against ${schedule.url}...`));
6997
+ console.log(chalk5.blue(`Running schedule ${chalk5.bold(schedule.name)} against ${schedule.url}...`));
6042
6998
  const { run, results } = await runByFilter({
6043
6999
  url: schedule.url,
6044
7000
  tags: schedule.scenarioFilter.tags,
@@ -6057,15 +7013,15 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
6057
7013
  }
6058
7014
  process.exit(getExitCode(run));
6059
7015
  } catch (error) {
6060
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7016
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6061
7017
  process.exit(1);
6062
7018
  }
6063
7019
  });
6064
7020
  program2.command("daemon").description("Start the scheduler daemon").option("--interval <seconds>", "Check interval in seconds", "60").action(async (opts) => {
6065
7021
  try {
6066
7022
  const intervalMs = parseInt(opts.interval, 10) * 1000;
6067
- console.log(chalk4.blue("Scheduler daemon started. Press Ctrl+C to stop."));
6068
- console.log(chalk4.dim(` Check interval: ${opts.interval}s`));
7023
+ console.log(chalk5.blue("Scheduler daemon started. Press Ctrl+C to stop."));
7024
+ console.log(chalk5.dim(` Check interval: ${opts.interval}s`));
6069
7025
  let running = true;
6070
7026
  const checkAndRun = async () => {
6071
7027
  while (running) {
@@ -6074,7 +7030,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
6074
7030
  const now2 = new Date().toISOString();
6075
7031
  for (const schedule of schedules) {
6076
7032
  if (schedule.nextRunAt && schedule.nextRunAt <= now2) {
6077
- console.log(chalk4.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
7033
+ console.log(chalk5.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
6078
7034
  try {
6079
7035
  const { run } = await runByFilter({
6080
7036
  url: schedule.url,
@@ -6087,39 +7043,39 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
6087
7043
  timeout: schedule.timeoutMs ?? undefined,
6088
7044
  projectId: schedule.projectId ?? undefined
6089
7045
  });
6090
- const statusColor = run.status === "passed" ? chalk4.green : chalk4.red;
7046
+ const statusColor = run.status === "passed" ? chalk5.green : chalk5.red;
6091
7047
  console.log(` ${statusColor(run.status)} \u2014 ${run.passed}/${run.total} passed`);
6092
7048
  updateSchedule(schedule.id, {});
6093
7049
  } catch (err) {
6094
- console.error(chalk4.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
7050
+ console.error(chalk5.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
6095
7051
  }
6096
7052
  }
6097
7053
  }
6098
7054
  } catch (err) {
6099
- console.error(chalk4.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
7055
+ console.error(chalk5.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
6100
7056
  }
6101
7057
  await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
6102
7058
  }
6103
7059
  };
6104
7060
  process.on("SIGINT", () => {
6105
- console.log(chalk4.yellow(`
7061
+ console.log(chalk5.yellow(`
6106
7062
  Shutting down scheduler daemon...`));
6107
7063
  running = false;
6108
7064
  process.exit(0);
6109
7065
  });
6110
7066
  process.on("SIGTERM", () => {
6111
- console.log(chalk4.yellow(`
7067
+ console.log(chalk5.yellow(`
6112
7068
  Shutting down scheduler daemon...`));
6113
7069
  running = false;
6114
7070
  process.exit(0);
6115
7071
  });
6116
7072
  await checkAndRun();
6117
7073
  } catch (error) {
6118
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7074
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6119
7075
  process.exit(1);
6120
7076
  }
6121
7077
  });
6122
- program2.command("init").description("Initialize a new testing project").option("-n, --name <name>", "Project name").option("-u, --url <url>", "Base URL").option("-p, --path <path>", "Project path").action((opts) => {
7078
+ program2.command("init").description("Initialize a new testing project").option("-n, --name <name>", "Project name").option("-u, --url <url>", "Base URL").option("-p, --path <path>", "Project path").option("--ci <provider>", "Generate CI workflow (github)").action((opts) => {
6123
7079
  try {
6124
7080
  const { project, scenarios, framework } = initProject({
6125
7081
  name: opts.name,
@@ -6127,30 +7083,41 @@ program2.command("init").description("Initialize a new testing project").option(
6127
7083
  path: opts.path
6128
7084
  });
6129
7085
  console.log("");
6130
- console.log(chalk4.bold(" Project initialized!"));
7086
+ console.log(chalk5.bold(" Project initialized!"));
6131
7087
  console.log("");
6132
7088
  if (framework) {
6133
- console.log(` Framework: ${chalk4.cyan(framework.name)}`);
7089
+ console.log(` Framework: ${chalk5.cyan(framework.name)}`);
6134
7090
  if (framework.features.length > 0) {
6135
- console.log(` Features: ${chalk4.dim(framework.features.join(", "))}`);
7091
+ console.log(` Features: ${chalk5.dim(framework.features.join(", "))}`);
6136
7092
  }
6137
7093
  } else {
6138
- console.log(` Framework: ${chalk4.dim("not detected")}`);
7094
+ console.log(` Framework: ${chalk5.dim("not detected")}`);
6139
7095
  }
6140
- console.log(` Project: ${chalk4.green(project.name)} ${chalk4.dim(`(${project.id})`)}`);
6141
- console.log(` Scenarios: ${chalk4.green(String(scenarios.length))} starter scenarios created`);
7096
+ console.log(` Project: ${chalk5.green(project.name)} ${chalk5.dim(`(${project.id})`)}`);
7097
+ console.log(` Scenarios: ${chalk5.green(String(scenarios.length))} starter scenarios created`);
6142
7098
  console.log("");
6143
7099
  for (const s of scenarios) {
6144
- console.log(` ${chalk4.dim(s.shortId)} ${s.name} ${chalk4.dim(`[${s.tags.join(", ")}]`)}`);
7100
+ console.log(` ${chalk5.dim(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
7101
+ }
7102
+ if (opts.ci === "github") {
7103
+ const workflowDir = join6(process.cwd(), ".github", "workflows");
7104
+ if (!existsSync8(workflowDir)) {
7105
+ mkdirSync4(workflowDir, { recursive: true });
7106
+ }
7107
+ const workflowPath = join6(workflowDir, "testers.yml");
7108
+ writeFileSync3(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
7109
+ console.log(` CI: ${chalk5.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
7110
+ } else if (opts.ci) {
7111
+ console.log(chalk5.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
6145
7112
  }
6146
7113
  console.log("");
6147
- console.log(chalk4.bold(" Next steps:"));
7114
+ console.log(chalk5.bold(" Next steps:"));
6148
7115
  console.log(` 1. Start your dev server`);
6149
- console.log(` 2. Run ${chalk4.cyan("testers run <url>")} to execute tests`);
6150
- console.log(` 3. Add more scenarios with ${chalk4.cyan("testers add <name>")}`);
7116
+ console.log(` 2. Run ${chalk5.cyan("testers run <url>")} to execute tests`);
7117
+ console.log(` 3. Add more scenarios with ${chalk5.cyan("testers add <name>")}`);
6151
7118
  console.log("");
6152
7119
  } catch (error) {
6153
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7120
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6154
7121
  process.exit(1);
6155
7122
  }
6156
7123
  });
@@ -6158,16 +7125,16 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
6158
7125
  try {
6159
7126
  const originalRun = getRun(runId);
6160
7127
  if (!originalRun) {
6161
- console.error(chalk4.red(`Run not found: ${runId}`));
7128
+ console.error(chalk5.red(`Run not found: ${runId}`));
6162
7129
  process.exit(1);
6163
7130
  }
6164
7131
  const originalResults = getResultsByRun(originalRun.id);
6165
7132
  const scenarioIds = originalResults.map((r) => r.scenarioId);
6166
7133
  if (scenarioIds.length === 0) {
6167
- console.log(chalk4.dim("No scenarios to replay."));
7134
+ console.log(chalk5.dim("No scenarios to replay."));
6168
7135
  return;
6169
7136
  }
6170
- console.log(chalk4.blue(`Replaying ${scenarioIds.length} scenarios from run ${originalRun.id.slice(0, 8)}...`));
7137
+ console.log(chalk5.blue(`Replaying ${scenarioIds.length} scenarios from run ${originalRun.id.slice(0, 8)}...`));
6171
7138
  const { run, results } = await runByFilter({
6172
7139
  url: opts.url ?? originalRun.url,
6173
7140
  scenarioIds,
@@ -6182,7 +7149,7 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
6182
7149
  }
6183
7150
  process.exit(getExitCode(run));
6184
7151
  } catch (error) {
6185
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7152
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6186
7153
  process.exit(1);
6187
7154
  }
6188
7155
  });
@@ -6190,16 +7157,16 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
6190
7157
  try {
6191
7158
  const originalRun = getRun(runId);
6192
7159
  if (!originalRun) {
6193
- console.error(chalk4.red(`Run not found: ${runId}`));
7160
+ console.error(chalk5.red(`Run not found: ${runId}`));
6194
7161
  process.exit(1);
6195
7162
  }
6196
7163
  const originalResults = getResultsByRun(originalRun.id);
6197
7164
  const failedScenarioIds = originalResults.filter((r) => r.status === "failed" || r.status === "error").map((r) => r.scenarioId);
6198
7165
  if (failedScenarioIds.length === 0) {
6199
- console.log(chalk4.green("No failed scenarios to retry. All passed!"));
7166
+ console.log(chalk5.green("No failed scenarios to retry. All passed!"));
6200
7167
  return;
6201
7168
  }
6202
- console.log(chalk4.blue(`Retrying ${failedScenarioIds.length} failed scenarios from run ${originalRun.id.slice(0, 8)}...`));
7169
+ console.log(chalk5.blue(`Retrying ${failedScenarioIds.length} failed scenarios from run ${originalRun.id.slice(0, 8)}...`));
6203
7170
  const { run, results } = await runByFilter({
6204
7171
  url: opts.url ?? originalRun.url,
6205
7172
  scenarioIds: failedScenarioIds,
@@ -6209,13 +7176,13 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
6209
7176
  });
6210
7177
  if (!opts.json) {
6211
7178
  console.log("");
6212
- console.log(chalk4.bold(" Comparison with original run:"));
7179
+ console.log(chalk5.bold(" Comparison with original run:"));
6213
7180
  for (const result of results) {
6214
7181
  const original = originalResults.find((r) => r.scenarioId === result.scenarioId);
6215
7182
  if (original) {
6216
7183
  const changed = original.status !== result.status;
6217
- const arrow = changed ? chalk4.yellow(`${original.status} \u2192 ${result.status}`) : chalk4.dim(`${result.status} (unchanged)`);
6218
- const icon = result.status === "passed" ? chalk4.green("\u2713") : chalk4.red("\u2717");
7184
+ const arrow = changed ? chalk5.yellow(`${original.status} \u2192 ${result.status}`) : chalk5.dim(`${result.status} (unchanged)`);
7185
+ const icon = result.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
6219
7186
  console.log(` ${icon} ${result.scenarioId.slice(0, 8)}: ${arrow}`);
6220
7187
  }
6221
7188
  }
@@ -6228,14 +7195,14 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
6228
7195
  }
6229
7196
  process.exit(getExitCode(run));
6230
7197
  } catch (error) {
6231
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7198
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6232
7199
  process.exit(1);
6233
7200
  }
6234
7201
  });
6235
7202
  program2.command("smoke <url>").description("Run autonomous smoke test").option("-m, --model <model>", "AI model").option("--headed", "Watch browser", false).option("--timeout <ms>", "Timeout in milliseconds").option("--json", "JSON output", false).option("--project <id>", "Project ID").action(async (url, opts) => {
6236
7203
  try {
6237
7204
  const projectId = resolveProject(opts.project);
6238
- console.log(chalk4.blue(`Running smoke test against ${chalk4.bold(url)}...`));
7205
+ console.log(chalk5.blue(`Running smoke test against ${chalk5.bold(url)}...`));
6239
7206
  console.log("");
6240
7207
  const smokeResult = await runSmoke({
6241
7208
  url,
@@ -6257,11 +7224,11 @@ program2.command("smoke <url>").description("Run autonomous smoke test").option(
6257
7224
  const hasCritical = smokeResult.issuesFound.some((i) => i.severity === "critical" || i.severity === "high");
6258
7225
  process.exit(hasCritical ? 1 : 0);
6259
7226
  } catch (error) {
6260
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7227
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6261
7228
  process.exit(1);
6262
7229
  }
6263
7230
  });
6264
- program2.command("diff <run1> <run2>").description("Compare two test runs").option("--json", "JSON output", false).action((run1, run2, opts) => {
7231
+ program2.command("diff <run1> <run2>").description("Compare two test runs").option("--json", "JSON output", false).option("--threshold <percent>", "Visual diff threshold percentage", "0.1").action((run1, run2, opts) => {
6265
7232
  try {
6266
7233
  const diff = diffRuns(run1, run2);
6267
7234
  if (opts.json) {
@@ -6269,9 +7236,19 @@ program2.command("diff <run1> <run2>").description("Compare two test runs").opti
6269
7236
  } else {
6270
7237
  console.log(formatDiffTerminal(diff));
6271
7238
  }
6272
- process.exit(diff.regressions.length > 0 ? 1 : 0);
7239
+ const threshold = parseFloat(opts.threshold);
7240
+ const visualResults = compareRunScreenshots(run2, run1, threshold);
7241
+ if (visualResults.length > 0) {
7242
+ if (opts.json) {
7243
+ console.log(JSON.stringify({ visualDiff: visualResults }, null, 2));
7244
+ } else {
7245
+ console.log(formatVisualDiffTerminal(visualResults, threshold));
7246
+ }
7247
+ }
7248
+ const hasVisualRegressions = visualResults.some((r) => r.isRegression);
7249
+ process.exit(diff.regressions.length > 0 || hasVisualRegressions ? 1 : 0);
6273
7250
  } catch (error) {
6274
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7251
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6275
7252
  process.exit(1);
6276
7253
  }
6277
7254
  });
@@ -6284,9 +7261,9 @@ program2.command("report [run-id]").description("Generate HTML test report").opt
6284
7261
  html = generateHtmlReport(runId);
6285
7262
  }
6286
7263
  writeFileSync3(opts.output, html, "utf-8");
6287
- console.log(chalk4.green(`Report generated: ${opts.output}`));
7264
+ console.log(chalk5.green(`Report generated: ${opts.output}`));
6288
7265
  } catch (error) {
6289
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7266
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6290
7267
  process.exit(1);
6291
7268
  }
6292
7269
  });
@@ -6299,9 +7276,9 @@ authCmd.command("add <name>").description("Create an auth preset").requiredOptio
6299
7276
  password: opts.password,
6300
7277
  loginPath: opts.loginPath
6301
7278
  });
6302
- console.log(chalk4.green(`Created auth preset ${chalk4.bold(preset.name)} (${preset.email})`));
7279
+ console.log(chalk5.green(`Created auth preset ${chalk5.bold(preset.name)} (${preset.email})`));
6303
7280
  } catch (error) {
6304
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7281
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6305
7282
  process.exit(1);
6306
7283
  }
6307
7284
  });
@@ -6309,11 +7286,11 @@ authCmd.command("list").description("List auth presets").action(() => {
6309
7286
  try {
6310
7287
  const presets = listAuthPresets();
6311
7288
  if (presets.length === 0) {
6312
- console.log(chalk4.dim("No auth presets found."));
7289
+ console.log(chalk5.dim("No auth presets found."));
6313
7290
  return;
6314
7291
  }
6315
7292
  console.log("");
6316
- console.log(chalk4.bold(" Auth Presets"));
7293
+ console.log(chalk5.bold(" Auth Presets"));
6317
7294
  console.log("");
6318
7295
  console.log(` ${"Name".padEnd(20)} ${"Email".padEnd(30)} ${"Login Path".padEnd(15)} Created`);
6319
7296
  console.log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(30)} ${"\u2500".repeat(15)} ${"\u2500".repeat(22)}`);
@@ -6322,7 +7299,7 @@ authCmd.command("list").description("List auth presets").action(() => {
6322
7299
  }
6323
7300
  console.log("");
6324
7301
  } catch (error) {
6325
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7302
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6326
7303
  process.exit(1);
6327
7304
  }
6328
7305
  });
@@ -6330,13 +7307,13 @@ authCmd.command("delete <name>").description("Delete an auth preset").action((na
6330
7307
  try {
6331
7308
  const deleted = deleteAuthPreset(name);
6332
7309
  if (deleted) {
6333
- console.log(chalk4.green(`Deleted auth preset: ${name}`));
7310
+ console.log(chalk5.green(`Deleted auth preset: ${name}`));
6334
7311
  } else {
6335
- console.error(chalk4.red(`Auth preset not found: ${name}`));
7312
+ console.error(chalk5.red(`Auth preset not found: ${name}`));
6336
7313
  process.exit(1);
6337
7314
  }
6338
7315
  } catch (error) {
6339
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7316
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6340
7317
  process.exit(1);
6341
7318
  }
6342
7319
  });
@@ -6349,7 +7326,268 @@ program2.command("costs").description("Show cost tracking and budget status").op
6349
7326
  console.log(formatCostsTerminal(summary));
6350
7327
  }
6351
7328
  } catch (error) {
6352
- console.error(chalk4.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7329
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7330
+ process.exit(1);
7331
+ }
7332
+ });
7333
+ program2.command("chain <scenario-id>").description("Add a dependency to a scenario").requiredOption("--depends-on <id>", "Scenario ID that must run first").action((scenarioId, opts) => {
7334
+ try {
7335
+ const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
7336
+ if (!scenario) {
7337
+ console.error(chalk5.red(`Scenario not found: ${scenarioId}`));
7338
+ process.exit(1);
7339
+ }
7340
+ const dep = getScenario(opts.dependsOn) ?? getScenarioByShortId(opts.dependsOn);
7341
+ if (!dep) {
7342
+ console.error(chalk5.red(`Dependency scenario not found: ${opts.dependsOn}`));
7343
+ process.exit(1);
7344
+ }
7345
+ addDependency(scenario.id, dep.id);
7346
+ console.log(chalk5.green(`${scenario.shortId} now depends on ${dep.shortId}`));
7347
+ } catch (error) {
7348
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7349
+ process.exit(1);
7350
+ }
7351
+ });
7352
+ program2.command("unchain <scenario-id>").description("Remove a dependency from a scenario").requiredOption("--from <id>", "Dependency to remove").action((scenarioId, opts) => {
7353
+ try {
7354
+ const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
7355
+ if (!scenario) {
7356
+ console.error(chalk5.red(`Scenario not found: ${scenarioId}`));
7357
+ process.exit(1);
7358
+ }
7359
+ const dep = getScenario(opts.from) ?? getScenarioByShortId(opts.from);
7360
+ if (!dep) {
7361
+ console.error(chalk5.red(`Dependency not found: ${opts.from}`));
7362
+ process.exit(1);
7363
+ }
7364
+ removeDependency(scenario.id, dep.id);
7365
+ console.log(chalk5.green(`Removed dependency: ${scenario.shortId} no longer depends on ${dep.shortId}`));
7366
+ } catch (error) {
7367
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7368
+ process.exit(1);
7369
+ }
7370
+ });
7371
+ program2.command("deps <scenario-id>").description("Show dependencies for a scenario").action((scenarioId) => {
7372
+ try {
7373
+ const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
7374
+ if (!scenario) {
7375
+ console.error(chalk5.red(`Scenario not found: ${scenarioId}`));
7376
+ process.exit(1);
7377
+ }
7378
+ const deps = getDependencies(scenario.id);
7379
+ const dependents = getDependents(scenario.id);
7380
+ console.log("");
7381
+ console.log(chalk5.bold(` Dependencies for ${scenario.shortId}: ${scenario.name}`));
7382
+ console.log("");
7383
+ if (deps.length > 0) {
7384
+ console.log(chalk5.dim(" Depends on:"));
7385
+ for (const depId of deps) {
7386
+ const s = getScenario(depId);
7387
+ console.log(` \u2192 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
7388
+ }
7389
+ } else {
7390
+ console.log(chalk5.dim(" No dependencies"));
7391
+ }
7392
+ if (dependents.length > 0) {
7393
+ console.log("");
7394
+ console.log(chalk5.dim(" Required by:"));
7395
+ for (const depId of dependents) {
7396
+ const s = getScenario(depId);
7397
+ console.log(` \u2190 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
7398
+ }
7399
+ }
7400
+ console.log("");
7401
+ } catch (error) {
7402
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7403
+ process.exit(1);
7404
+ }
7405
+ });
7406
+ var flowCmd = program2.command("flow").description("Manage test flows (ordered scenario chains)");
7407
+ flowCmd.command("create <name>").description("Create a flow from scenario IDs").requiredOption("--chain <ids>", "Comma-separated scenario IDs in order").option("--project <id>", "Project ID").action((name, opts) => {
7408
+ try {
7409
+ const ids = opts.chain.split(",").map((id) => {
7410
+ const s = getScenario(id.trim()) ?? getScenarioByShortId(id.trim());
7411
+ if (!s) {
7412
+ console.error(chalk5.red(`Scenario not found: ${id.trim()}`));
7413
+ process.exit(1);
7414
+ }
7415
+ return s.id;
7416
+ });
7417
+ for (let i = 1;i < ids.length; i++) {
7418
+ try {
7419
+ addDependency(ids[i], ids[i - 1]);
7420
+ } catch {}
7421
+ }
7422
+ const flow = createFlow({ name, scenarioIds: ids, projectId: resolveProject(opts.project) });
7423
+ console.log(chalk5.green(`Flow created: ${flow.id.slice(0, 8)} \u2014 ${flow.name} (${ids.length} scenarios)`));
7424
+ } catch (error) {
7425
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7426
+ process.exit(1);
7427
+ }
7428
+ });
7429
+ flowCmd.command("list").description("List all flows").option("--project <id>", "Project ID").action((opts) => {
7430
+ const flows = listFlows(resolveProject(opts.project) ?? undefined);
7431
+ if (flows.length === 0) {
7432
+ console.log(chalk5.dim(`
7433
+ No flows found.
7434
+ `));
7435
+ return;
7436
+ }
7437
+ console.log("");
7438
+ console.log(chalk5.bold(" Flows"));
7439
+ console.log("");
7440
+ for (const f of flows) {
7441
+ console.log(` ${chalk5.dim(f.id.slice(0, 8))} ${f.name} ${chalk5.dim(`(${f.scenarioIds.length} scenarios)`)}`);
7442
+ }
7443
+ console.log("");
7444
+ });
7445
+ flowCmd.command("show <id>").description("Show flow details").action((id) => {
7446
+ const flow = getFlow(id);
7447
+ if (!flow) {
7448
+ console.error(chalk5.red(`Flow not found: ${id}`));
7449
+ process.exit(1);
7450
+ }
7451
+ console.log("");
7452
+ console.log(chalk5.bold(` Flow: ${flow.name}`));
7453
+ console.log(` ID: ${chalk5.dim(flow.id)}`);
7454
+ console.log(` Scenarios (in order):`);
7455
+ for (let i = 0;i < flow.scenarioIds.length; i++) {
7456
+ const s = getScenario(flow.scenarioIds[i]);
7457
+ console.log(` ${i + 1}. ${s ? `${s.shortId}: ${s.name}` : flow.scenarioIds[i].slice(0, 8)}`);
7458
+ }
7459
+ console.log("");
7460
+ });
7461
+ flowCmd.command("delete <id>").description("Delete a flow").action((id) => {
7462
+ if (deleteFlow(id))
7463
+ console.log(chalk5.green("Flow deleted."));
7464
+ else {
7465
+ console.error(chalk5.red("Flow not found."));
7466
+ process.exit(1);
7467
+ }
7468
+ });
7469
+ flowCmd.command("run <id>").description("Run a flow (scenarios in dependency order)").option("-u, --url <url>", "Target URL (required)").option("-m, --model <model>", "AI model").option("--headed", "Run headed", false).option("--json", "JSON output", false).action(async (id, opts) => {
7470
+ try {
7471
+ const flow = getFlow(id);
7472
+ if (!flow) {
7473
+ console.error(chalk5.red(`Flow not found: ${id}`));
7474
+ process.exit(1);
7475
+ }
7476
+ if (!opts.url) {
7477
+ console.error(chalk5.red("--url is required for flow run"));
7478
+ process.exit(1);
7479
+ }
7480
+ console.log(chalk5.blue(`Running flow: ${flow.name} (${flow.scenarioIds.length} scenarios)`));
7481
+ const { run, results } = await runByFilter({
7482
+ url: opts.url,
7483
+ scenarioIds: flow.scenarioIds,
7484
+ model: opts.model,
7485
+ headed: opts.headed,
7486
+ parallel: 1
7487
+ });
7488
+ if (opts.json)
7489
+ console.log(formatJSON(run, results));
7490
+ else
7491
+ console.log(formatTerminal(run, results));
7492
+ process.exit(getExitCode(run));
7493
+ } catch (error) {
7494
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7495
+ process.exit(1);
7496
+ }
7497
+ });
7498
+ var envCmd = program2.command("env").description("Manage environments");
7499
+ envCmd.command("add <name>").description("Add a named environment").requiredOption("--url <url>", "Environment URL").option("--auth <preset>", "Auth preset name").option("--project <id>", "Project ID").option("--default", "Set as default environment", false).action((name, opts) => {
7500
+ try {
7501
+ const env = createEnvironment({
7502
+ name,
7503
+ url: opts.url,
7504
+ authPresetName: opts.auth,
7505
+ projectId: opts.project,
7506
+ isDefault: opts.default
7507
+ });
7508
+ console.log(chalk5.green(`Environment added: ${env.name} \u2192 ${env.url}${env.isDefault ? " (default)" : ""}`));
7509
+ } catch (error) {
7510
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7511
+ process.exit(1);
7512
+ }
7513
+ });
7514
+ envCmd.command("list").description("List all environments").option("--project <id>", "Filter by project ID").action((opts) => {
7515
+ try {
7516
+ const envs = listEnvironments(opts.project);
7517
+ if (envs.length === 0) {
7518
+ console.log(chalk5.dim("No environments configured. Add one with: testers env add <name> --url <url>"));
7519
+ return;
7520
+ }
7521
+ for (const env of envs) {
7522
+ const marker = env.isDefault ? chalk5.green(" \u2605 default") : "";
7523
+ const auth = env.authPresetName ? chalk5.dim(` (auth: ${env.authPresetName})`) : "";
7524
+ console.log(` ${chalk5.bold(env.name)} ${env.url}${auth}${marker}`);
7525
+ }
7526
+ } catch (error) {
7527
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7528
+ process.exit(1);
7529
+ }
7530
+ });
7531
+ envCmd.command("use <name>").description("Set an environment as the default").action((name) => {
7532
+ try {
7533
+ setDefaultEnvironment(name);
7534
+ const env = getEnvironment(name);
7535
+ console.log(chalk5.green(`Default environment set: ${env.name} \u2192 ${env.url}`));
7536
+ } catch (error) {
7537
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7538
+ process.exit(1);
7539
+ }
7540
+ });
7541
+ envCmd.command("delete <name>").description("Delete an environment").action((name) => {
7542
+ try {
7543
+ const deleted = deleteEnvironment(name);
7544
+ if (deleted) {
7545
+ console.log(chalk5.green(`Environment deleted: ${name}`));
7546
+ } else {
7547
+ console.error(chalk5.red(`Environment not found: ${name}`));
7548
+ process.exit(1);
7549
+ }
7550
+ } catch (error) {
7551
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7552
+ process.exit(1);
7553
+ }
7554
+ });
7555
+ program2.command("baseline <run-id>").description("Set a run as the visual baseline").action((runId) => {
7556
+ try {
7557
+ setBaseline(runId);
7558
+ const run = getRun(runId);
7559
+ console.log(chalk5.green(`Baseline set: ${chalk5.bold(runId.slice(0, 8))}${run ? ` (${run.status}, ${run.total} scenarios)` : ""}`));
7560
+ } catch (error) {
7561
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7562
+ process.exit(1);
7563
+ }
7564
+ });
7565
+ program2.command("import-api <spec>").description("Import test scenarios from an OpenAPI/Swagger spec file").option("--project <id>", "Project ID").action(async (spec, opts) => {
7566
+ try {
7567
+ const { importFromOpenAPI: importFromOpenAPI2 } = await Promise.resolve().then(() => (init_openapi_import(), exports_openapi_import));
7568
+ const { imported, scenarios } = importFromOpenAPI2(spec, resolveProject(opts.project) ?? undefined);
7569
+ console.log(chalk5.green(`
7570
+ Imported ${imported} scenarios from API spec:`));
7571
+ for (const s of scenarios) {
7572
+ console.log(` ${chalk5.cyan(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
7573
+ }
7574
+ console.log("");
7575
+ } catch (error) {
7576
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7577
+ process.exit(1);
7578
+ }
7579
+ });
7580
+ program2.command("record <url>").description("Record a browser session and generate a test scenario").option("-n, --name <name>", "Scenario name", "Recorded session").option("--project <id>", "Project ID").action(async (url, opts) => {
7581
+ try {
7582
+ const { recordAndSave: recordAndSave2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
7583
+ console.log(chalk5.blue("Opening browser for recording..."));
7584
+ const { recording, scenario } = await recordAndSave2(url, opts.name, resolveProject(opts.project) ?? undefined);
7585
+ console.log("");
7586
+ console.log(chalk5.green(`Recording saved as scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
7587
+ console.log(chalk5.dim(` ${recording.actions.length} actions recorded in ${(recording.duration / 1000).toFixed(0)}s`));
7588
+ console.log(chalk5.dim(` ${scenario.steps.length} steps generated`));
7589
+ } catch (error) {
7590
+ console.error(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
6353
7591
  process.exit(1);
6354
7592
  }
6355
7593
  });