@hasna/testers 0.0.12 → 0.0.13

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
@@ -4,26 +4,62 @@ var __create = Object.create;
4
4
  var __getProtoOf = Object.getPrototypeOf;
5
5
  var __defProp = Object.defineProperty;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ function __accessProp(key) {
10
+ return this[key];
11
+ }
12
+ var __toESMCache_node;
13
+ var __toESMCache_esm;
8
14
  var __toESM = (mod, isNodeMode, target) => {
15
+ var canCache = mod != null && typeof mod === "object";
16
+ if (canCache) {
17
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
18
+ var cached = cache.get(mod);
19
+ if (cached)
20
+ return cached;
21
+ }
9
22
  target = mod != null ? __create(__getProtoOf(mod)) : {};
10
23
  const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
24
  for (let key of __getOwnPropNames(mod))
12
25
  if (!__hasOwnProp.call(to, key))
13
26
  __defProp(to, key, {
14
- get: () => mod[key],
27
+ get: __accessProp.bind(mod, key),
15
28
  enumerable: true
16
29
  });
30
+ if (canCache)
31
+ cache.set(mod, to);
17
32
  return to;
18
33
  };
34
+ var __toCommonJS = (from) => {
35
+ var entry = (__moduleCache ??= new WeakMap).get(from), desc;
36
+ if (entry)
37
+ return entry;
38
+ entry = __defProp({}, "__esModule", { value: true });
39
+ if (from && typeof from === "object" || typeof from === "function") {
40
+ for (var key of __getOwnPropNames(from))
41
+ if (!__hasOwnProp.call(entry, key))
42
+ __defProp(entry, key, {
43
+ get: __accessProp.bind(from, key),
44
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
45
+ });
46
+ }
47
+ __moduleCache.set(from, entry);
48
+ return entry;
49
+ };
50
+ var __moduleCache;
19
51
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
52
+ var __returnValue = (v) => v;
53
+ function __exportSetter(name, newValue) {
54
+ this[name] = __returnValue.bind(null, newValue);
55
+ }
20
56
  var __export = (target, all) => {
21
57
  for (var name in all)
22
58
  __defProp(target, name, {
23
59
  get: all[name],
24
60
  enumerable: true,
25
61
  configurable: true,
26
- set: (newValue) => all[name] = () => newValue
62
+ set: __exportSetter.bind(all, name)
27
63
  });
28
64
  };
29
65
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -2074,6 +2110,17 @@ function projectFromRow(row) {
2074
2110
  updatedAt: row.updated_at
2075
2111
  };
2076
2112
  }
2113
+ function agentFromRow(row) {
2114
+ return {
2115
+ id: row.id,
2116
+ name: row.name,
2117
+ description: row.description,
2118
+ role: row.role,
2119
+ metadata: row.metadata ? JSON.parse(row.metadata) : null,
2120
+ createdAt: row.created_at,
2121
+ lastSeenAt: row.last_seen_at
2122
+ };
2123
+ }
2077
2124
  function scenarioFromRow(row) {
2078
2125
  return {
2079
2126
  id: row.id,
@@ -2820,6 +2867,112 @@ var init_runs = __esm(() => {
2820
2867
  init_database();
2821
2868
  });
2822
2869
 
2870
+ // src/db/results.ts
2871
+ function createResult(input) {
2872
+ const db2 = getDatabase();
2873
+ const id = uuid();
2874
+ const timestamp = now();
2875
+ db2.query(`
2876
+ INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at)
2877
+ VALUES (?, ?, ?, 'skipped', NULL, NULL, 0, ?, 0, ?, 0, 0, '{}', ?)
2878
+ `).run(id, input.runId, input.scenarioId, input.stepsTotal, input.model, timestamp);
2879
+ return getResult(id);
2880
+ }
2881
+ function getResult(id) {
2882
+ const db2 = getDatabase();
2883
+ let row = db2.query("SELECT * FROM results WHERE id = ?").get(id);
2884
+ if (row)
2885
+ return resultFromRow(row);
2886
+ const fullId = resolvePartialId("results", id);
2887
+ if (fullId) {
2888
+ row = db2.query("SELECT * FROM results WHERE id = ?").get(fullId);
2889
+ if (row)
2890
+ return resultFromRow(row);
2891
+ }
2892
+ return null;
2893
+ }
2894
+ function listResults(runId) {
2895
+ const db2 = getDatabase();
2896
+ const rows = db2.query("SELECT * FROM results WHERE run_id = ? ORDER BY created_at ASC").all(runId);
2897
+ return rows.map(resultFromRow);
2898
+ }
2899
+ function updateResult(id, updates) {
2900
+ const db2 = getDatabase();
2901
+ const existing = getResult(id);
2902
+ if (!existing) {
2903
+ throw new Error(`Result not found: ${id}`);
2904
+ }
2905
+ const sets = [];
2906
+ const params = [];
2907
+ if (updates.status !== undefined) {
2908
+ sets.push("status = ?");
2909
+ params.push(updates.status);
2910
+ }
2911
+ if (updates.reasoning !== undefined) {
2912
+ sets.push("reasoning = ?");
2913
+ params.push(updates.reasoning);
2914
+ }
2915
+ if (updates.error !== undefined) {
2916
+ sets.push("error = ?");
2917
+ params.push(updates.error);
2918
+ }
2919
+ if (updates.stepsCompleted !== undefined) {
2920
+ sets.push("steps_completed = ?");
2921
+ params.push(updates.stepsCompleted);
2922
+ }
2923
+ if (updates.durationMs !== undefined) {
2924
+ sets.push("duration_ms = ?");
2925
+ params.push(updates.durationMs);
2926
+ }
2927
+ if (updates.tokensUsed !== undefined) {
2928
+ sets.push("tokens_used = ?");
2929
+ params.push(updates.tokensUsed);
2930
+ }
2931
+ if (updates.costCents !== undefined) {
2932
+ sets.push("cost_cents = ?");
2933
+ params.push(updates.costCents);
2934
+ }
2935
+ if (sets.length === 0) {
2936
+ return existing;
2937
+ }
2938
+ params.push(existing.id);
2939
+ db2.query(`UPDATE results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
2940
+ return getResult(existing.id);
2941
+ }
2942
+ function getResultsByRun(runId) {
2943
+ return listResults(runId);
2944
+ }
2945
+ var init_results = __esm(() => {
2946
+ init_types();
2947
+ init_database();
2948
+ });
2949
+
2950
+ // src/db/screenshots.ts
2951
+ function createScreenshot(input) {
2952
+ const db2 = getDatabase();
2953
+ const id = uuid();
2954
+ const timestamp = now();
2955
+ db2.query(`
2956
+ INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp, description, page_url, thumbnail_path)
2957
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
2958
+ `).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp, input.description ?? null, input.pageUrl ?? null, input.thumbnailPath ?? null);
2959
+ return getScreenshot(id);
2960
+ }
2961
+ function getScreenshot(id) {
2962
+ const db2 = getDatabase();
2963
+ const row = db2.query("SELECT * FROM screenshots WHERE id = ?").get(id);
2964
+ return row ? screenshotFromRow(row) : null;
2965
+ }
2966
+ function listScreenshots(resultId) {
2967
+ const db2 = getDatabase();
2968
+ const rows = db2.query("SELECT * FROM screenshots WHERE result_id = ? ORDER BY step_number ASC").all(resultId);
2969
+ return rows.map(screenshotFromRow);
2970
+ }
2971
+ var init_screenshots = __esm(() => {
2972
+ init_types();
2973
+ init_database();
2974
+ });
2975
+
2823
2976
  // src/lib/browser-lightpanda.ts
2824
2977
  var exports_browser_lightpanda = {};
2825
2978
  __export(exports_browser_lightpanda, {
@@ -2982,694 +3135,69 @@ var init_browser_lightpanda = __esm(() => {
2982
3135
  init_types();
2983
3136
  });
2984
3137
 
2985
- // src/db/flows.ts
2986
- var exports_flows = {};
2987
- __export(exports_flows, {
2988
- topologicalSort: () => topologicalSort,
2989
- removeDependency: () => removeDependency,
2990
- listFlows: () => listFlows,
2991
- getTransitiveDependencies: () => getTransitiveDependencies,
2992
- getFlow: () => getFlow,
2993
- getDependents: () => getDependents,
2994
- getDependencies: () => getDependencies,
2995
- deleteFlow: () => deleteFlow,
2996
- createFlow: () => createFlow,
2997
- addDependency: () => addDependency
2998
- });
2999
- function addDependency(scenarioId, dependsOn) {
3000
- const db2 = getDatabase();
3001
- const visited = new Set;
3002
- const queue = [dependsOn];
3003
- while (queue.length > 0) {
3004
- const current = queue.shift();
3005
- if (current === scenarioId) {
3006
- throw new DependencyCycleError(scenarioId, dependsOn);
3007
- }
3008
- if (visited.has(current))
3009
- continue;
3010
- visited.add(current);
3011
- const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
3012
- for (const dep of deps) {
3013
- if (!visited.has(dep.depends_on)) {
3014
- queue.push(dep.depends_on);
3015
- }
3138
+ // src/lib/browser.ts
3139
+ import { chromium as chromium2 } from "playwright";
3140
+ import { execSync } from "child_process";
3141
+ async function launchBrowser(options) {
3142
+ const engine = options?.engine ?? process.env["TESTERS_BROWSER_ENGINE"] ?? "playwright";
3143
+ if (engine === "lightpanda") {
3144
+ const { launchLightpanda: launchLightpanda2, isLightpandaAvailable: isLightpandaAvailable2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3145
+ if (!isLightpandaAvailable2()) {
3146
+ throw new BrowserError("Lightpanda not installed. Run: testers install-browser --engine lightpanda");
3016
3147
  }
3148
+ return launchLightpanda2({ viewport: options?.viewport });
3149
+ }
3150
+ const headless = options?.headless ?? true;
3151
+ const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
3152
+ try {
3153
+ const browser = await chromium2.launch({
3154
+ headless,
3155
+ args: [
3156
+ `--window-size=${viewport.width},${viewport.height}`
3157
+ ]
3158
+ });
3159
+ return browser;
3160
+ } catch (error) {
3161
+ const message = error instanceof Error ? error.message : String(error);
3162
+ throw new BrowserError(`Failed to launch browser: ${message}`);
3017
3163
  }
3018
- db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
3019
- }
3020
- function removeDependency(scenarioId, dependsOn) {
3021
- const db2 = getDatabase();
3022
- const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
3023
- return result.changes > 0;
3024
- }
3025
- function getDependencies(scenarioId) {
3026
- const db2 = getDatabase();
3027
- const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
3028
- return rows.map((r) => r.depends_on);
3029
- }
3030
- function getDependents(scenarioId) {
3031
- const db2 = getDatabase();
3032
- const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
3033
- return rows.map((r) => r.scenario_id);
3034
3164
  }
3035
- function getTransitiveDependencies(scenarioId) {
3036
- const db2 = getDatabase();
3037
- const visited = new Set;
3038
- const queue = [scenarioId];
3039
- while (queue.length > 0) {
3040
- const current = queue.shift();
3041
- const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
3042
- for (const dep of deps) {
3043
- if (!visited.has(dep.depends_on)) {
3044
- visited.add(dep.depends_on);
3045
- queue.push(dep.depends_on);
3046
- }
3047
- }
3165
+ async function getPage(browser, options) {
3166
+ const engine = options?.engine ?? "playwright";
3167
+ if (engine === "lightpanda") {
3168
+ const { getLightpandaPage: getLightpandaPage2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3169
+ return getLightpandaPage2(browser, options);
3170
+ }
3171
+ const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
3172
+ try {
3173
+ const context = await browser.newContext({
3174
+ viewport,
3175
+ userAgent: options?.userAgent,
3176
+ locale: options?.locale
3177
+ });
3178
+ const page = await context.newPage();
3179
+ return page;
3180
+ } catch (error) {
3181
+ const message = error instanceof Error ? error.message : String(error);
3182
+ throw new BrowserError(`Failed to create page: ${message}`);
3048
3183
  }
3049
- return Array.from(visited);
3050
3184
  }
3051
- function topologicalSort(scenarioIds) {
3052
- const db2 = getDatabase();
3053
- const idSet = new Set(scenarioIds);
3054
- const inDegree = new Map;
3055
- const dependents = new Map;
3056
- for (const id of scenarioIds) {
3057
- inDegree.set(id, 0);
3058
- dependents.set(id, []);
3185
+ async function closeBrowser(browser, engine) {
3186
+ if (engine === "lightpanda") {
3187
+ const { closeLightpanda: closeLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3188
+ return closeLightpanda2(browser);
3059
3189
  }
3060
- for (const id of scenarioIds) {
3061
- const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
3062
- for (const dep of deps) {
3063
- if (idSet.has(dep.depends_on)) {
3064
- inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
3065
- dependents.get(dep.depends_on).push(id);
3066
- }
3067
- }
3190
+ try {
3191
+ await browser.close();
3192
+ } catch (error) {
3193
+ const message = error instanceof Error ? error.message : String(error);
3194
+ throw new BrowserError(`Failed to close browser: ${message}`);
3068
3195
  }
3069
- const queue = [];
3070
- for (const [id, deg] of inDegree) {
3071
- if (deg === 0)
3072
- queue.push(id);
3073
- }
3074
- const sorted = [];
3075
- while (queue.length > 0) {
3076
- const current = queue.shift();
3077
- sorted.push(current);
3078
- for (const dep of dependents.get(current) ?? []) {
3079
- const newDeg = (inDegree.get(dep) ?? 1) - 1;
3080
- inDegree.set(dep, newDeg);
3081
- if (newDeg === 0)
3082
- queue.push(dep);
3083
- }
3084
- }
3085
- if (sorted.length !== scenarioIds.length) {
3086
- throw new DependencyCycleError("multiple", "multiple");
3087
- }
3088
- return sorted;
3089
- }
3090
- function createFlow(input) {
3091
- const db2 = getDatabase();
3092
- const id = uuid();
3093
- const timestamp = now();
3094
- db2.query(`
3095
- INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
3096
- VALUES (?, ?, ?, ?, ?, ?, ?)
3097
- `).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
3098
- return getFlow(id);
3099
- }
3100
- function getFlow(id) {
3101
- const db2 = getDatabase();
3102
- let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
3103
- if (row)
3104
- return flowFromRow(row);
3105
- const fullId = resolvePartialId("flows", id);
3106
- if (fullId) {
3107
- row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
3108
- if (row)
3109
- return flowFromRow(row);
3110
- }
3111
- return null;
3112
- }
3113
- function listFlows(projectId) {
3114
- const db2 = getDatabase();
3115
- if (projectId) {
3116
- const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
3117
- return rows2.map(flowFromRow);
3118
- }
3119
- const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
3120
- return rows.map(flowFromRow);
3121
- }
3122
- function deleteFlow(id) {
3123
- const db2 = getDatabase();
3124
- const flow = getFlow(id);
3125
- if (!flow)
3126
- return false;
3127
- const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
3128
- return result.changes > 0;
3129
- }
3130
- var init_flows = __esm(() => {
3131
- init_database();
3132
- init_database();
3133
- init_types();
3134
- });
3135
-
3136
- // src/lib/openapi-import.ts
3137
- var exports_openapi_import = {};
3138
- __export(exports_openapi_import, {
3139
- parseOpenAPISpec: () => parseOpenAPISpec,
3140
- importFromOpenAPI: () => importFromOpenAPI
3141
- });
3142
- import { readFileSync as readFileSync5 } from "fs";
3143
- function parseSpec(content) {
3144
- try {
3145
- return JSON.parse(content);
3146
- } catch {
3147
- 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`");
3148
- }
3149
- }
3150
- function methodPriority(method) {
3151
- switch (method.toUpperCase()) {
3152
- case "GET":
3153
- return "medium";
3154
- case "POST":
3155
- return "high";
3156
- case "PUT":
3157
- return "high";
3158
- case "DELETE":
3159
- return "critical";
3160
- case "PATCH":
3161
- return "medium";
3162
- default:
3163
- return "low";
3164
- }
3165
- }
3166
- function parseOpenAPISpec(filePathOrUrl) {
3167
- let content;
3168
- if (filePathOrUrl.startsWith("http")) {
3169
- throw new Error("URL fetching not supported yet. Download the spec file first.");
3170
- }
3171
- content = readFileSync5(filePathOrUrl, "utf-8");
3172
- const spec = parseSpec(content);
3173
- const isOpenAPI3 = !!spec.openapi;
3174
- const isSwagger2 = !!spec.swagger;
3175
- if (!isOpenAPI3 && !isSwagger2) {
3176
- throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
3177
- }
3178
- const scenarios = [];
3179
- const paths = spec.paths ?? {};
3180
- for (const [path, methods] of Object.entries(paths)) {
3181
- for (const [method, operation] of Object.entries(methods)) {
3182
- if (["get", "post", "put", "delete", "patch"].indexOf(method.toLowerCase()) === -1)
3183
- continue;
3184
- const op = operation;
3185
- const name = op.summary ?? op.operationId ?? `${method.toUpperCase()} ${path}`;
3186
- const tags = op.tags ?? [];
3187
- const requiresAuth = !!(op.security?.length ?? spec.security?.length);
3188
- const steps = [];
3189
- steps.push(`Navigate to the API endpoint: ${method.toUpperCase()} ${path}`);
3190
- if (op.parameters?.length) {
3191
- const required = op.parameters.filter((p) => p.required);
3192
- if (required.length > 0) {
3193
- steps.push(`Fill required parameters: ${required.map((p) => p.name).join(", ")}`);
3194
- }
3195
- }
3196
- if (["post", "put", "patch"].includes(method.toLowerCase())) {
3197
- steps.push("Fill the request body with valid test data");
3198
- }
3199
- steps.push("Submit the request");
3200
- const responses = op.responses ?? {};
3201
- const successCodes = Object.keys(responses).filter((c) => c.startsWith("2"));
3202
- if (successCodes.length > 0) {
3203
- steps.push(`Verify response status is ${successCodes.join(" or ")}`);
3204
- } else {
3205
- steps.push("Verify the response is successful");
3206
- }
3207
- const description = [
3208
- op.description ?? `Test the ${method.toUpperCase()} ${path} endpoint.`,
3209
- requiresAuth ? "This endpoint requires authentication." : ""
3210
- ].filter(Boolean).join(" ");
3211
- scenarios.push({
3212
- name,
3213
- description,
3214
- steps,
3215
- tags: [...tags, "api", method.toLowerCase()],
3216
- priority: methodPriority(method),
3217
- targetPath: path,
3218
- requiresAuth
3219
- });
3220
- }
3221
- }
3222
- return scenarios;
3223
- }
3224
- function importFromOpenAPI(filePathOrUrl, projectId) {
3225
- const inputs = parseOpenAPISpec(filePathOrUrl);
3226
- const scenarios = inputs.map((input) => createScenario({ ...input, projectId }));
3227
- return { imported: scenarios.length, scenarios };
3228
- }
3229
- var init_openapi_import = __esm(() => {
3230
- init_scenarios();
3231
- });
3232
-
3233
- // src/lib/recorder.ts
3234
- var exports_recorder = {};
3235
- __export(exports_recorder, {
3236
- recordSession: () => recordSession,
3237
- recordAndSave: () => recordAndSave,
3238
- actionsToScenarioInput: () => actionsToScenarioInput
3239
- });
3240
- import { chromium as chromium3 } from "playwright";
3241
- async function recordSession(url, options) {
3242
- const browser = await chromium3.launch({ headless: false });
3243
- const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
3244
- const page = await context.newPage();
3245
- const actions = [];
3246
- const startTime = Date.now();
3247
- const timeout = options?.timeout ?? 300000;
3248
- page.on("framenavigated", (frame) => {
3249
- if (frame === page.mainFrame()) {
3250
- actions.push({ type: "navigate", url: frame.url(), timestamp: Date.now() - startTime });
3251
- }
3252
- });
3253
- await page.addInitScript(() => {
3254
- document.addEventListener("click", (e) => {
3255
- const target = e.target;
3256
- const selector = buildSelector(target);
3257
- window.postMessage({ __testers_action: "click", selector }, "*");
3258
- }, true);
3259
- document.addEventListener("input", (e) => {
3260
- const target = e.target;
3261
- const selector = buildSelector(target);
3262
- window.postMessage({ __testers_action: "fill", selector, value: target.value }, "*");
3263
- }, true);
3264
- document.addEventListener("change", (e) => {
3265
- const target = e.target;
3266
- if (target.tagName === "SELECT") {
3267
- const selector = buildSelector(target);
3268
- window.postMessage({ __testers_action: "select", selector, value: target.value }, "*");
3269
- }
3270
- }, true);
3271
- document.addEventListener("keydown", (e) => {
3272
- if (["Enter", "Tab", "Escape"].includes(e.key)) {
3273
- window.postMessage({ __testers_action: "press", key: e.key }, "*");
3274
- }
3275
- }, true);
3276
- function buildSelector(el) {
3277
- if (el.id)
3278
- return `#${el.id}`;
3279
- if (el.getAttribute("data-testid"))
3280
- return `[data-testid="${el.getAttribute("data-testid")}"]`;
3281
- if (el.getAttribute("name"))
3282
- return `${el.tagName.toLowerCase()}[name="${el.getAttribute("name")}"]`;
3283
- if (el.getAttribute("aria-label"))
3284
- return `[aria-label="${el.getAttribute("aria-label")}"]`;
3285
- if (el.className && typeof el.className === "string") {
3286
- const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
3287
- if (classes)
3288
- return `${el.tagName.toLowerCase()}.${classes}`;
3289
- }
3290
- const text = el.textContent?.trim().slice(0, 30);
3291
- if (text)
3292
- return `text="${text}"`;
3293
- return el.tagName.toLowerCase();
3294
- }
3295
- });
3296
- const pollInterval = setInterval(async () => {
3297
- try {
3298
- const newActions = await page.evaluate(() => {
3299
- const collected = window.__testers_collected ?? [];
3300
- window.__testers_collected = [];
3301
- return collected;
3302
- });
3303
- for (const a of newActions) {
3304
- actions.push({
3305
- type: a["type"],
3306
- selector: a["selector"],
3307
- value: a["value"],
3308
- key: a["key"],
3309
- timestamp: Date.now() - startTime
3310
- });
3311
- }
3312
- } catch {}
3313
- }, 500);
3314
- await page.exposeFunction("__testersRecord", (action) => {
3315
- actions.push({ ...action, timestamp: Date.now() - startTime });
3316
- });
3317
- await page.addInitScript(() => {
3318
- window.addEventListener("message", (e) => {
3319
- if (e.data?.__testers_action) {
3320
- const { __testers_action, ...rest } = e.data;
3321
- window.__testersRecord({ type: __testers_action, ...rest });
3322
- }
3323
- });
3324
- });
3325
- await page.goto(url);
3326
- actions.push({ type: "navigate", url, timestamp: 0 });
3327
- console.log(`
3328
- Recording started. Interact with the browser.`);
3329
- console.log(` Close the browser window or wait ${timeout / 1000}s to stop.
3330
- `);
3331
- await Promise.race([
3332
- page.waitForEvent("close").catch(() => {}),
3333
- context.waitForEvent("close").catch(() => {}),
3334
- new Promise((resolve) => setTimeout(resolve, timeout))
3335
- ]);
3336
- clearInterval(pollInterval);
3337
- try {
3338
- await browser.close();
3339
- } catch {}
3340
- return {
3341
- actions,
3342
- url,
3343
- duration: Date.now() - startTime
3344
- };
3345
- }
3346
- function actionsToScenarioInput(recording, name, projectId) {
3347
- const steps = [];
3348
- const seenFills = new Map;
3349
- for (const action of recording.actions) {
3350
- switch (action.type) {
3351
- case "navigate":
3352
- if (action.url)
3353
- steps.push(`Navigate to ${action.url}`);
3354
- break;
3355
- case "click":
3356
- if (action.selector)
3357
- steps.push(`Click ${action.selector}`);
3358
- break;
3359
- case "fill":
3360
- if (action.selector && action.value) {
3361
- seenFills.set(action.selector, action.value);
3362
- }
3363
- break;
3364
- case "select":
3365
- if (action.selector && action.value)
3366
- steps.push(`Select "${action.value}" in ${action.selector}`);
3367
- break;
3368
- case "press":
3369
- if (action.key)
3370
- steps.push(`Press ${action.key}`);
3371
- break;
3372
- }
3373
- }
3374
- for (const [selector, value] of seenFills) {
3375
- steps.push(`Fill ${selector} with "${value}"`);
3376
- }
3377
- return {
3378
- name,
3379
- description: `Recorded session on ${recording.url} (${(recording.duration / 1000).toFixed(0)}s, ${recording.actions.length} actions)`,
3380
- steps,
3381
- tags: ["recorded"],
3382
- projectId
3383
- };
3384
- }
3385
- async function recordAndSave(url, name, projectId) {
3386
- const recording = await recordSession(url);
3387
- const input = actionsToScenarioInput(recording, name, projectId);
3388
- const scenario = createScenario(input);
3389
- return { recording, scenario };
3390
- }
3391
- var init_recorder = __esm(() => {
3392
- init_scenarios();
3393
- });
3394
-
3395
- // node_modules/commander/esm.mjs
3396
- var import__ = __toESM(require_commander(), 1);
3397
- var {
3398
- program,
3399
- createCommand,
3400
- createArgument,
3401
- createOption,
3402
- CommanderError,
3403
- InvalidArgumentError,
3404
- InvalidOptionArgumentError,
3405
- Command,
3406
- Argument,
3407
- Option,
3408
- Help
3409
- } = import__.default;
3410
-
3411
- // src/cli/index.tsx
3412
- import chalk5 from "chalk";
3413
- // package.json
3414
- var package_default = {
3415
- name: "@hasna/testers",
3416
- version: "0.0.12",
3417
- description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
3418
- type: "module",
3419
- main: "dist/index.js",
3420
- types: "dist/index.d.ts",
3421
- bin: {
3422
- testers: "dist/cli/index.js",
3423
- "testers-mcp": "dist/mcp/index.js",
3424
- "testers-serve": "dist/server/index.js"
3425
- },
3426
- exports: {
3427
- ".": {
3428
- types: "./dist/index.d.ts",
3429
- import: "./dist/index.js"
3430
- }
3431
- },
3432
- files: [
3433
- "dist/",
3434
- "dashboard/dist/",
3435
- "LICENSE",
3436
- "README.md"
3437
- ],
3438
- scripts: {
3439
- build: "bun run build:dashboard && bun run build:cli && bun run build:mcp && bun run build:server && bun run build:lib && bun run build:types",
3440
- "build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
3441
- "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
3442
- "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright",
3443
- "build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk",
3444
- "build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck",
3445
- "build:dashboard": "cd dashboard && bun run build",
3446
- typecheck: "tsc --noEmit",
3447
- test: "bun test",
3448
- "dev:cli": "bun run src/cli/index.tsx",
3449
- "dev:mcp": "bun run src/mcp/index.ts",
3450
- "dev:serve": "bun run src/server/index.ts",
3451
- prepublishOnly: "bun run build"
3452
- },
3453
- dependencies: {
3454
- "@anthropic-ai/sdk": "^0.52.0",
3455
- "@modelcontextprotocol/sdk": "^1.12.1",
3456
- chalk: "^5.4.1",
3457
- commander: "^13.1.0",
3458
- ink: "^5.2.0",
3459
- playwright: "^1.50.0",
3460
- react: "^18.3.1",
3461
- zod: "^3.24.2"
3462
- },
3463
- devDependencies: {
3464
- "@types/bun": "latest",
3465
- "@types/react": "^18.3.18",
3466
- typescript: "^5.7.3"
3467
- },
3468
- engines: {
3469
- bun: ">=1.0.0"
3470
- },
3471
- publishConfig: {
3472
- access: "public",
3473
- registry: "https://registry.npmjs.org/"
3474
- },
3475
- repository: {
3476
- type: "git",
3477
- url: "https://github.com/hasna/open-testers.git"
3478
- },
3479
- license: "MIT",
3480
- keywords: [
3481
- "testing",
3482
- "qa",
3483
- "ai",
3484
- "playwright",
3485
- "browser",
3486
- "screenshot",
3487
- "automation",
3488
- "cli",
3489
- "mcp"
3490
- ]
3491
- };
3492
-
3493
- // src/cli/index.tsx
3494
- init_scenarios();
3495
- init_runs();
3496
- import { render, Box, Text, useInput, useApp } from "ink";
3497
- import React, { useState } from "react";
3498
- import { readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
3499
- import { createInterface } from "readline";
3500
- import { join as join6, resolve } from "path";
3501
-
3502
- // src/db/results.ts
3503
- init_types();
3504
- init_database();
3505
- function createResult(input) {
3506
- const db2 = getDatabase();
3507
- const id = uuid();
3508
- const timestamp = now();
3509
- db2.query(`
3510
- INSERT INTO results (id, run_id, scenario_id, status, reasoning, error, steps_completed, steps_total, duration_ms, model, tokens_used, cost_cents, metadata, created_at)
3511
- VALUES (?, ?, ?, 'skipped', NULL, NULL, 0, ?, 0, ?, 0, 0, '{}', ?)
3512
- `).run(id, input.runId, input.scenarioId, input.stepsTotal, input.model, timestamp);
3513
- return getResult(id);
3514
- }
3515
- function getResult(id) {
3516
- const db2 = getDatabase();
3517
- let row = db2.query("SELECT * FROM results WHERE id = ?").get(id);
3518
- if (row)
3519
- return resultFromRow(row);
3520
- const fullId = resolvePartialId("results", id);
3521
- if (fullId) {
3522
- row = db2.query("SELECT * FROM results WHERE id = ?").get(fullId);
3523
- if (row)
3524
- return resultFromRow(row);
3525
- }
3526
- return null;
3527
- }
3528
- function listResults(runId) {
3529
- const db2 = getDatabase();
3530
- const rows = db2.query("SELECT * FROM results WHERE run_id = ? ORDER BY created_at ASC").all(runId);
3531
- return rows.map(resultFromRow);
3532
- }
3533
- function updateResult(id, updates) {
3534
- const db2 = getDatabase();
3535
- const existing = getResult(id);
3536
- if (!existing) {
3537
- throw new Error(`Result not found: ${id}`);
3538
- }
3539
- const sets = [];
3540
- const params = [];
3541
- if (updates.status !== undefined) {
3542
- sets.push("status = ?");
3543
- params.push(updates.status);
3544
- }
3545
- if (updates.reasoning !== undefined) {
3546
- sets.push("reasoning = ?");
3547
- params.push(updates.reasoning);
3548
- }
3549
- if (updates.error !== undefined) {
3550
- sets.push("error = ?");
3551
- params.push(updates.error);
3552
- }
3553
- if (updates.stepsCompleted !== undefined) {
3554
- sets.push("steps_completed = ?");
3555
- params.push(updates.stepsCompleted);
3556
- }
3557
- if (updates.durationMs !== undefined) {
3558
- sets.push("duration_ms = ?");
3559
- params.push(updates.durationMs);
3560
- }
3561
- if (updates.tokensUsed !== undefined) {
3562
- sets.push("tokens_used = ?");
3563
- params.push(updates.tokensUsed);
3564
- }
3565
- if (updates.costCents !== undefined) {
3566
- sets.push("cost_cents = ?");
3567
- params.push(updates.costCents);
3568
- }
3569
- if (sets.length === 0) {
3570
- return existing;
3571
- }
3572
- params.push(existing.id);
3573
- db2.query(`UPDATE results SET ${sets.join(", ")} WHERE id = ?`).run(...params);
3574
- return getResult(existing.id);
3575
- }
3576
- function getResultsByRun(runId) {
3577
- return listResults(runId);
3578
- }
3579
-
3580
- // src/db/screenshots.ts
3581
- init_types();
3582
- init_database();
3583
- function createScreenshot(input) {
3584
- const db2 = getDatabase();
3585
- const id = uuid();
3586
- const timestamp = now();
3587
- db2.query(`
3588
- INSERT INTO screenshots (id, result_id, step_number, action, file_path, width, height, timestamp, description, page_url, thumbnail_path)
3589
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
3590
- `).run(id, input.resultId, input.stepNumber, input.action, input.filePath, input.width, input.height, timestamp, input.description ?? null, input.pageUrl ?? null, input.thumbnailPath ?? null);
3591
- return getScreenshot(id);
3592
- }
3593
- function getScreenshot(id) {
3594
- const db2 = getDatabase();
3595
- const row = db2.query("SELECT * FROM screenshots WHERE id = ?").get(id);
3596
- return row ? screenshotFromRow(row) : null;
3597
- }
3598
- function listScreenshots(resultId) {
3599
- const db2 = getDatabase();
3600
- const rows = db2.query("SELECT * FROM screenshots WHERE result_id = ? ORDER BY step_number ASC").all(resultId);
3601
- return rows.map(screenshotFromRow);
3602
- }
3603
-
3604
- // src/lib/runner.ts
3605
- init_runs();
3606
- init_scenarios();
3607
-
3608
- // src/lib/browser.ts
3609
- init_types();
3610
- import { chromium as chromium2 } from "playwright";
3611
- import { execSync } from "child_process";
3612
- var DEFAULT_VIEWPORT = { width: 1280, height: 720 };
3613
- async function launchBrowser(options) {
3614
- const engine = options?.engine ?? process.env["TESTERS_BROWSER_ENGINE"] ?? "playwright";
3615
- if (engine === "lightpanda") {
3616
- const { launchLightpanda: launchLightpanda2, isLightpandaAvailable: isLightpandaAvailable2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3617
- if (!isLightpandaAvailable2()) {
3618
- throw new BrowserError("Lightpanda not installed. Run: testers install-browser --engine lightpanda");
3619
- }
3620
- return launchLightpanda2({ viewport: options?.viewport });
3621
- }
3622
- const headless = options?.headless ?? true;
3623
- const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
3624
- try {
3625
- const browser = await chromium2.launch({
3626
- headless,
3627
- args: [
3628
- `--window-size=${viewport.width},${viewport.height}`
3629
- ]
3630
- });
3631
- return browser;
3632
- } catch (error) {
3633
- const message = error instanceof Error ? error.message : String(error);
3634
- throw new BrowserError(`Failed to launch browser: ${message}`);
3635
- }
3636
- }
3637
- async function getPage(browser, options) {
3638
- const engine = options?.engine ?? "playwright";
3639
- if (engine === "lightpanda") {
3640
- const { getLightpandaPage: getLightpandaPage2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3641
- return getLightpandaPage2(browser, options);
3642
- }
3643
- const viewport = options?.viewport ?? DEFAULT_VIEWPORT;
3644
- try {
3645
- const context = await browser.newContext({
3646
- viewport,
3647
- userAgent: options?.userAgent,
3648
- locale: options?.locale
3649
- });
3650
- const page = await context.newPage();
3651
- return page;
3652
- } catch (error) {
3653
- const message = error instanceof Error ? error.message : String(error);
3654
- throw new BrowserError(`Failed to create page: ${message}`);
3655
- }
3656
- }
3657
- async function closeBrowser(browser, engine) {
3658
- if (engine === "lightpanda") {
3659
- const { closeLightpanda: closeLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3660
- return closeLightpanda2(browser);
3661
- }
3662
- try {
3663
- await browser.close();
3664
- } catch (error) {
3665
- const message = error instanceof Error ? error.message : String(error);
3666
- throw new BrowserError(`Failed to close browser: ${message}`);
3667
- }
3668
- }
3669
- async function installBrowser(engine) {
3670
- if (engine === "lightpanda") {
3671
- const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3672
- return installLightpanda2();
3196
+ }
3197
+ async function installBrowser(engine) {
3198
+ if (engine === "lightpanda") {
3199
+ const { installLightpanda: installLightpanda2 } = await Promise.resolve().then(() => (init_browser_lightpanda(), exports_browser_lightpanda));
3200
+ return installLightpanda2();
3673
3201
  }
3674
3202
  try {
3675
3203
  execSync("bunx playwright install chromium", {
@@ -3680,6 +3208,11 @@ async function installBrowser(engine) {
3680
3208
  throw new BrowserError(`Failed to install browser: ${message}`);
3681
3209
  }
3682
3210
  }
3211
+ var DEFAULT_VIEWPORT;
3212
+ var init_browser = __esm(() => {
3213
+ init_types();
3214
+ DEFAULT_VIEWPORT = { width: 1280, height: 720 };
3215
+ });
3683
3216
 
3684
3217
  // src/lib/screenshotter.ts
3685
3218
  import { mkdirSync as mkdirSync2, existsSync as existsSync2, writeFileSync } from "fs";
@@ -3736,7 +3269,6 @@ async function generateThumbnail(page, screenshotDir, filename) {
3736
3269
  return null;
3737
3270
  }
3738
3271
  }
3739
- var DEFAULT_BASE_DIR = join2(homedir2(), ".testers", "screenshots");
3740
3272
 
3741
3273
  class Screenshotter {
3742
3274
  baseDir;
@@ -3857,320 +3389,19 @@ class Screenshotter {
3857
3389
  };
3858
3390
  }
3859
3391
  }
3392
+ var DEFAULT_BASE_DIR;
3393
+ var init_screenshotter = __esm(() => {
3394
+ DEFAULT_BASE_DIR = join2(homedir2(), ".testers", "screenshots");
3395
+ });
3860
3396
 
3861
- // src/lib/ai-client.ts
3862
- init_types();
3863
- import Anthropic from "@anthropic-ai/sdk";
3864
- function resolveModel(nameOrPreset) {
3865
- if (nameOrPreset in MODEL_MAP) {
3866
- return MODEL_MAP[nameOrPreset];
3867
- }
3868
- return nameOrPreset;
3869
- }
3870
- var BROWSER_TOOLS = [
3871
- {
3872
- name: "navigate",
3873
- description: "Navigate the browser to a specific URL.",
3874
- input_schema: {
3875
- type: "object",
3876
- properties: {
3877
- url: { type: "string", description: "The URL to navigate to." }
3878
- },
3879
- required: ["url"]
3880
- }
3881
- },
3882
- {
3883
- name: "click",
3884
- description: "Click on an element matching the given CSS selector.",
3885
- input_schema: {
3886
- type: "object",
3887
- properties: {
3888
- selector: {
3889
- type: "string",
3890
- description: "CSS selector of the element to click."
3891
- }
3892
- },
3893
- required: ["selector"]
3894
- }
3895
- },
3896
- {
3897
- name: "fill",
3898
- description: "Fill an input field with the given value.",
3899
- input_schema: {
3900
- type: "object",
3901
- properties: {
3902
- selector: {
3903
- type: "string",
3904
- description: "CSS selector of the input field."
3905
- },
3906
- value: {
3907
- type: "string",
3908
- description: "The value to fill into the input."
3909
- }
3910
- },
3911
- required: ["selector", "value"]
3912
- }
3913
- },
3914
- {
3915
- name: "select_option",
3916
- description: "Select an option from a dropdown/select element.",
3917
- input_schema: {
3918
- type: "object",
3919
- properties: {
3920
- selector: {
3921
- type: "string",
3922
- description: "CSS selector of the select element."
3923
- },
3924
- value: {
3925
- type: "string",
3926
- description: "The value of the option to select."
3927
- }
3928
- },
3929
- required: ["selector", "value"]
3930
- }
3931
- },
3932
- {
3933
- name: "screenshot",
3934
- description: "Take a screenshot of the current page state.",
3935
- input_schema: {
3936
- type: "object",
3937
- properties: {},
3938
- required: []
3939
- }
3940
- },
3941
- {
3942
- name: "get_text",
3943
- description: "Get the text content of an element matching the selector.",
3944
- input_schema: {
3945
- type: "object",
3946
- properties: {
3947
- selector: {
3948
- type: "string",
3949
- description: "CSS selector of the element."
3950
- }
3951
- },
3952
- required: ["selector"]
3953
- }
3954
- },
3955
- {
3956
- name: "get_url",
3957
- description: "Get the current page URL.",
3958
- input_schema: {
3959
- type: "object",
3960
- properties: {},
3961
- required: []
3962
- }
3963
- },
3964
- {
3965
- name: "wait_for",
3966
- description: "Wait for an element matching the selector to appear on the page.",
3967
- input_schema: {
3968
- type: "object",
3969
- properties: {
3970
- selector: {
3971
- type: "string",
3972
- description: "CSS selector to wait for."
3973
- },
3974
- timeout: {
3975
- type: "number",
3976
- description: "Maximum time to wait in milliseconds (default: 10000)."
3977
- }
3978
- },
3979
- required: ["selector"]
3980
- }
3981
- },
3982
- {
3983
- name: "go_back",
3984
- description: "Navigate back to the previous page.",
3985
- input_schema: {
3986
- type: "object",
3987
- properties: {},
3988
- required: []
3989
- }
3990
- },
3991
- {
3992
- name: "press_key",
3993
- description: "Press a keyboard key (e.g., Enter, Tab, Escape, ArrowDown).",
3994
- input_schema: {
3995
- type: "object",
3996
- properties: {
3997
- key: {
3998
- type: "string",
3999
- description: "The key to press (e.g., 'Enter', 'Tab', 'Escape')."
4000
- }
4001
- },
4002
- required: ["key"]
4003
- }
4004
- },
4005
- {
4006
- name: "assert_visible",
4007
- description: "Assert that an element matching the selector is visible on the page. Returns 'true' or 'false'.",
4008
- input_schema: {
4009
- type: "object",
4010
- properties: {
4011
- selector: {
4012
- type: "string",
4013
- description: "CSS selector of the element to check."
4014
- }
4015
- },
4016
- required: ["selector"]
4017
- }
4018
- },
4019
- {
4020
- name: "assert_text",
4021
- description: "Assert that the given text is visible somewhere on the page. Returns 'true' or 'false'.",
4022
- input_schema: {
4023
- type: "object",
4024
- properties: {
4025
- text: {
4026
- type: "string",
4027
- description: "The text to search for on the page."
4028
- }
4029
- },
4030
- required: ["text"]
4031
- }
4032
- },
4033
- {
4034
- name: "scroll",
4035
- description: "Scroll the page up or down by a given amount of pixels.",
4036
- input_schema: {
4037
- type: "object",
4038
- properties: {
4039
- direction: {
4040
- type: "string",
4041
- enum: ["up", "down"],
4042
- description: "Direction to scroll."
4043
- },
4044
- amount: {
4045
- type: "number",
4046
- description: "Number of pixels to scroll (default: 500)."
4047
- }
4048
- },
4049
- required: ["direction"]
4050
- }
4051
- },
4052
- {
4053
- name: "get_page_html",
4054
- description: "Get simplified HTML of the page body content, truncated to 8000 characters.",
4055
- input_schema: {
4056
- type: "object",
4057
- properties: {},
4058
- required: []
4059
- }
4060
- },
4061
- {
4062
- name: "get_elements",
4063
- description: "List elements matching a CSS selector with their text, tag name, and key attributes (max 20 results).",
4064
- input_schema: {
4065
- type: "object",
4066
- properties: {
4067
- selector: {
4068
- type: "string",
4069
- description: "CSS selector to match elements."
4070
- }
4071
- },
4072
- required: ["selector"]
4073
- }
4074
- },
4075
- {
4076
- name: "wait_for_navigation",
4077
- description: "Wait for page navigation/load to complete (network idle).",
4078
- input_schema: {
4079
- type: "object",
4080
- properties: {
4081
- timeout: {
4082
- type: "number",
4083
- description: "Maximum time to wait in milliseconds (default: 10000)."
4084
- }
4085
- },
4086
- required: []
4087
- }
4088
- },
4089
- {
4090
- name: "get_page_title",
4091
- description: "Get the document title of the current page.",
4092
- input_schema: {
4093
- type: "object",
4094
- properties: {},
4095
- required: []
4096
- }
4097
- },
4098
- {
4099
- name: "count_elements",
4100
- description: "Count the number of elements matching a CSS selector.",
4101
- input_schema: {
4102
- type: "object",
4103
- properties: {
4104
- selector: {
4105
- type: "string",
4106
- description: "CSS selector to count matching elements."
4107
- }
4108
- },
4109
- required: ["selector"]
4110
- }
4111
- },
4112
- {
4113
- name: "hover",
4114
- description: "Hover over an element matching the given CSS selector.",
4115
- input_schema: {
4116
- type: "object",
4117
- properties: {
4118
- selector: {
4119
- type: "string",
4120
- description: "CSS selector of the element to hover over."
4121
- }
4122
- },
4123
- required: ["selector"]
4124
- }
4125
- },
4126
- {
4127
- name: "check",
4128
- description: "Check a checkbox matching the given CSS selector.",
4129
- input_schema: {
4130
- type: "object",
4131
- properties: {
4132
- selector: {
4133
- type: "string",
4134
- description: "CSS selector of the checkbox to check."
4135
- }
4136
- },
4137
- required: ["selector"]
4138
- }
4139
- },
4140
- {
4141
- name: "uncheck",
4142
- description: "Uncheck a checkbox matching the given CSS selector.",
4143
- input_schema: {
4144
- type: "object",
4145
- properties: {
4146
- selector: {
4147
- type: "string",
4148
- description: "CSS selector of the checkbox to uncheck."
4149
- }
4150
- },
4151
- required: ["selector"]
4152
- }
4153
- },
4154
- {
4155
- name: "report_result",
4156
- description: "Report the final test result. Call this when you have completed testing the scenario. This MUST be the last tool you call.",
4157
- input_schema: {
4158
- type: "object",
4159
- properties: {
4160
- status: {
4161
- type: "string",
4162
- enum: ["passed", "failed"],
4163
- description: "Whether the test scenario passed or failed."
4164
- },
4165
- reasoning: {
4166
- type: "string",
4167
- description: "Detailed explanation of why the test passed or failed, including any issues found."
4168
- }
4169
- },
4170
- required: ["status", "reasoning"]
4171
- }
3397
+ // src/lib/ai-client.ts
3398
+ import Anthropic from "@anthropic-ai/sdk";
3399
+ function resolveModel(nameOrPreset) {
3400
+ if (nameOrPreset in MODEL_MAP) {
3401
+ return MODEL_MAP[nameOrPreset];
4172
3402
  }
4173
- ];
3403
+ return nameOrPreset;
3404
+ }
4174
3405
  async function executeTool(page, screenshotter, toolName, toolInput, context) {
4175
3406
  try {
4176
3407
  switch (toolName) {
@@ -4549,14 +3780,319 @@ function createClient(apiKey) {
4549
3780
  }
4550
3781
  return new Anthropic({ apiKey: key });
4551
3782
  }
3783
+ var BROWSER_TOOLS;
3784
+ var init_ai_client = __esm(() => {
3785
+ init_types();
3786
+ BROWSER_TOOLS = [
3787
+ {
3788
+ name: "navigate",
3789
+ description: "Navigate the browser to a specific URL.",
3790
+ input_schema: {
3791
+ type: "object",
3792
+ properties: {
3793
+ url: { type: "string", description: "The URL to navigate to." }
3794
+ },
3795
+ required: ["url"]
3796
+ }
3797
+ },
3798
+ {
3799
+ name: "click",
3800
+ description: "Click on an element matching the given CSS selector.",
3801
+ input_schema: {
3802
+ type: "object",
3803
+ properties: {
3804
+ selector: {
3805
+ type: "string",
3806
+ description: "CSS selector of the element to click."
3807
+ }
3808
+ },
3809
+ required: ["selector"]
3810
+ }
3811
+ },
3812
+ {
3813
+ name: "fill",
3814
+ description: "Fill an input field with the given value.",
3815
+ input_schema: {
3816
+ type: "object",
3817
+ properties: {
3818
+ selector: {
3819
+ type: "string",
3820
+ description: "CSS selector of the input field."
3821
+ },
3822
+ value: {
3823
+ type: "string",
3824
+ description: "The value to fill into the input."
3825
+ }
3826
+ },
3827
+ required: ["selector", "value"]
3828
+ }
3829
+ },
3830
+ {
3831
+ name: "select_option",
3832
+ description: "Select an option from a dropdown/select element.",
3833
+ input_schema: {
3834
+ type: "object",
3835
+ properties: {
3836
+ selector: {
3837
+ type: "string",
3838
+ description: "CSS selector of the select element."
3839
+ },
3840
+ value: {
3841
+ type: "string",
3842
+ description: "The value of the option to select."
3843
+ }
3844
+ },
3845
+ required: ["selector", "value"]
3846
+ }
3847
+ },
3848
+ {
3849
+ name: "screenshot",
3850
+ description: "Take a screenshot of the current page state.",
3851
+ input_schema: {
3852
+ type: "object",
3853
+ properties: {},
3854
+ required: []
3855
+ }
3856
+ },
3857
+ {
3858
+ name: "get_text",
3859
+ description: "Get the text content of an element matching the selector.",
3860
+ input_schema: {
3861
+ type: "object",
3862
+ properties: {
3863
+ selector: {
3864
+ type: "string",
3865
+ description: "CSS selector of the element."
3866
+ }
3867
+ },
3868
+ required: ["selector"]
3869
+ }
3870
+ },
3871
+ {
3872
+ name: "get_url",
3873
+ description: "Get the current page URL.",
3874
+ input_schema: {
3875
+ type: "object",
3876
+ properties: {},
3877
+ required: []
3878
+ }
3879
+ },
3880
+ {
3881
+ name: "wait_for",
3882
+ description: "Wait for an element matching the selector to appear on the page.",
3883
+ input_schema: {
3884
+ type: "object",
3885
+ properties: {
3886
+ selector: {
3887
+ type: "string",
3888
+ description: "CSS selector to wait for."
3889
+ },
3890
+ timeout: {
3891
+ type: "number",
3892
+ description: "Maximum time to wait in milliseconds (default: 10000)."
3893
+ }
3894
+ },
3895
+ required: ["selector"]
3896
+ }
3897
+ },
3898
+ {
3899
+ name: "go_back",
3900
+ description: "Navigate back to the previous page.",
3901
+ input_schema: {
3902
+ type: "object",
3903
+ properties: {},
3904
+ required: []
3905
+ }
3906
+ },
3907
+ {
3908
+ name: "press_key",
3909
+ description: "Press a keyboard key (e.g., Enter, Tab, Escape, ArrowDown).",
3910
+ input_schema: {
3911
+ type: "object",
3912
+ properties: {
3913
+ key: {
3914
+ type: "string",
3915
+ description: "The key to press (e.g., 'Enter', 'Tab', 'Escape')."
3916
+ }
3917
+ },
3918
+ required: ["key"]
3919
+ }
3920
+ },
3921
+ {
3922
+ name: "assert_visible",
3923
+ description: "Assert that an element matching the selector is visible on the page. Returns 'true' or 'false'.",
3924
+ input_schema: {
3925
+ type: "object",
3926
+ properties: {
3927
+ selector: {
3928
+ type: "string",
3929
+ description: "CSS selector of the element to check."
3930
+ }
3931
+ },
3932
+ required: ["selector"]
3933
+ }
3934
+ },
3935
+ {
3936
+ name: "assert_text",
3937
+ description: "Assert that the given text is visible somewhere on the page. Returns 'true' or 'false'.",
3938
+ input_schema: {
3939
+ type: "object",
3940
+ properties: {
3941
+ text: {
3942
+ type: "string",
3943
+ description: "The text to search for on the page."
3944
+ }
3945
+ },
3946
+ required: ["text"]
3947
+ }
3948
+ },
3949
+ {
3950
+ name: "scroll",
3951
+ description: "Scroll the page up or down by a given amount of pixels.",
3952
+ input_schema: {
3953
+ type: "object",
3954
+ properties: {
3955
+ direction: {
3956
+ type: "string",
3957
+ enum: ["up", "down"],
3958
+ description: "Direction to scroll."
3959
+ },
3960
+ amount: {
3961
+ type: "number",
3962
+ description: "Number of pixels to scroll (default: 500)."
3963
+ }
3964
+ },
3965
+ required: ["direction"]
3966
+ }
3967
+ },
3968
+ {
3969
+ name: "get_page_html",
3970
+ description: "Get simplified HTML of the page body content, truncated to 8000 characters.",
3971
+ input_schema: {
3972
+ type: "object",
3973
+ properties: {},
3974
+ required: []
3975
+ }
3976
+ },
3977
+ {
3978
+ name: "get_elements",
3979
+ description: "List elements matching a CSS selector with their text, tag name, and key attributes (max 20 results).",
3980
+ input_schema: {
3981
+ type: "object",
3982
+ properties: {
3983
+ selector: {
3984
+ type: "string",
3985
+ description: "CSS selector to match elements."
3986
+ }
3987
+ },
3988
+ required: ["selector"]
3989
+ }
3990
+ },
3991
+ {
3992
+ name: "wait_for_navigation",
3993
+ description: "Wait for page navigation/load to complete (network idle).",
3994
+ input_schema: {
3995
+ type: "object",
3996
+ properties: {
3997
+ timeout: {
3998
+ type: "number",
3999
+ description: "Maximum time to wait in milliseconds (default: 10000)."
4000
+ }
4001
+ },
4002
+ required: []
4003
+ }
4004
+ },
4005
+ {
4006
+ name: "get_page_title",
4007
+ description: "Get the document title of the current page.",
4008
+ input_schema: {
4009
+ type: "object",
4010
+ properties: {},
4011
+ required: []
4012
+ }
4013
+ },
4014
+ {
4015
+ name: "count_elements",
4016
+ description: "Count the number of elements matching a CSS selector.",
4017
+ input_schema: {
4018
+ type: "object",
4019
+ properties: {
4020
+ selector: {
4021
+ type: "string",
4022
+ description: "CSS selector to count matching elements."
4023
+ }
4024
+ },
4025
+ required: ["selector"]
4026
+ }
4027
+ },
4028
+ {
4029
+ name: "hover",
4030
+ description: "Hover over an element matching the given CSS selector.",
4031
+ input_schema: {
4032
+ type: "object",
4033
+ properties: {
4034
+ selector: {
4035
+ type: "string",
4036
+ description: "CSS selector of the element to hover over."
4037
+ }
4038
+ },
4039
+ required: ["selector"]
4040
+ }
4041
+ },
4042
+ {
4043
+ name: "check",
4044
+ description: "Check a checkbox matching the given CSS selector.",
4045
+ input_schema: {
4046
+ type: "object",
4047
+ properties: {
4048
+ selector: {
4049
+ type: "string",
4050
+ description: "CSS selector of the checkbox to check."
4051
+ }
4052
+ },
4053
+ required: ["selector"]
4054
+ }
4055
+ },
4056
+ {
4057
+ name: "uncheck",
4058
+ description: "Uncheck a checkbox matching the given CSS selector.",
4059
+ input_schema: {
4060
+ type: "object",
4061
+ properties: {
4062
+ selector: {
4063
+ type: "string",
4064
+ description: "CSS selector of the checkbox to uncheck."
4065
+ }
4066
+ },
4067
+ required: ["selector"]
4068
+ }
4069
+ },
4070
+ {
4071
+ name: "report_result",
4072
+ description: "Report the final test result. Call this when you have completed testing the scenario. This MUST be the last tool you call.",
4073
+ input_schema: {
4074
+ type: "object",
4075
+ properties: {
4076
+ status: {
4077
+ type: "string",
4078
+ enum: ["passed", "failed"],
4079
+ description: "Whether the test scenario passed or failed."
4080
+ },
4081
+ reasoning: {
4082
+ type: "string",
4083
+ description: "Detailed explanation of why the test passed or failed, including any issues found."
4084
+ }
4085
+ },
4086
+ required: ["status", "reasoning"]
4087
+ }
4088
+ }
4089
+ ];
4090
+ });
4552
4091
 
4553
4092
  // src/lib/config.ts
4554
- init_types();
4555
4093
  import { homedir as homedir3 } from "os";
4556
4094
  import { join as join3 } from "path";
4557
4095
  import { readFileSync, existsSync as existsSync3 } from "fs";
4558
- var CONFIG_DIR = join3(homedir3(), ".testers");
4559
- var CONFIG_PATH = join3(CONFIG_DIR, "config.json");
4560
4096
  function getDefaultConfig() {
4561
4097
  return {
4562
4098
  defaultModel: "claude-haiku-4-5-20251001",
@@ -4605,9 +4141,14 @@ function loadConfig() {
4605
4141
  }
4606
4142
  return config;
4607
4143
  }
4144
+ var CONFIG_DIR, CONFIG_PATH;
4145
+ var init_config = __esm(() => {
4146
+ init_types();
4147
+ CONFIG_DIR = join3(homedir3(), ".testers");
4148
+ CONFIG_PATH = join3(CONFIG_DIR, "config.json");
4149
+ });
4608
4150
 
4609
4151
  // src/lib/webhooks.ts
4610
- init_database();
4611
4152
  function fromRow(row) {
4612
4153
  return {
4613
4154
  id: row.id,
@@ -4664,80 +4205,440 @@ Schedule: ${payload.schedule.name}` : "")
4664
4205
  ]
4665
4206
  };
4666
4207
  }
4667
- async function dispatchWebhooks(event, run, schedule) {
4668
- const webhooks = listWebhooks(run.projectId ?? undefined);
4669
- const payload = {
4670
- event,
4671
- run: {
4672
- id: run.id,
4673
- url: run.url,
4674
- status: run.status,
4675
- passed: run.passed,
4676
- failed: run.failed,
4677
- total: run.total
4678
- },
4679
- schedule,
4680
- timestamp: new Date().toISOString()
4681
- };
4682
- for (const webhook of webhooks) {
4683
- if (!webhook.events.includes(event) && !webhook.events.includes("*"))
4208
+ async function dispatchWebhooks(event, run, schedule) {
4209
+ const webhooks = listWebhooks(run.projectId ?? undefined);
4210
+ const payload = {
4211
+ event,
4212
+ run: {
4213
+ id: run.id,
4214
+ url: run.url,
4215
+ status: run.status,
4216
+ passed: run.passed,
4217
+ failed: run.failed,
4218
+ total: run.total
4219
+ },
4220
+ schedule,
4221
+ timestamp: new Date().toISOString()
4222
+ };
4223
+ for (const webhook of webhooks) {
4224
+ if (!webhook.events.includes(event) && !webhook.events.includes("*"))
4225
+ continue;
4226
+ const isSlack = webhook.url.includes("hooks.slack.com");
4227
+ const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
4228
+ const headers = {
4229
+ "Content-Type": "application/json"
4230
+ };
4231
+ if (webhook.secret) {
4232
+ headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
4233
+ }
4234
+ try {
4235
+ const response = await fetch(webhook.url, {
4236
+ method: "POST",
4237
+ headers,
4238
+ body
4239
+ });
4240
+ if (!response.ok) {
4241
+ await new Promise((r) => setTimeout(r, 5000));
4242
+ await fetch(webhook.url, { method: "POST", headers, body });
4243
+ }
4244
+ } catch {}
4245
+ }
4246
+ }
4247
+ var init_webhooks = __esm(() => {
4248
+ init_database();
4249
+ });
4250
+
4251
+ // src/lib/logs-integration.ts
4252
+ async function pushFailedRunToLogs(run, failedResults, scenarios) {
4253
+ const logsUrl = process.env.LOGS_URL;
4254
+ if (!logsUrl)
4255
+ return;
4256
+ const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
4257
+ const entries = failedResults.map((result) => {
4258
+ const scenario = scenarioMap.get(result.scenarioId);
4259
+ return {
4260
+ level: "error",
4261
+ source: "sdk",
4262
+ service: "testers",
4263
+ message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
4264
+ metadata: {
4265
+ run_id: run.id,
4266
+ scenario_id: result.scenarioId,
4267
+ scenario_name: scenario?.name,
4268
+ url: run.url,
4269
+ status: result.status,
4270
+ duration_ms: result.durationMs
4271
+ }
4272
+ };
4273
+ });
4274
+ try {
4275
+ await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
4276
+ method: "POST",
4277
+ headers: { "Content-Type": "application/json" },
4278
+ body: JSON.stringify(entries)
4279
+ });
4280
+ } catch {}
4281
+ }
4282
+
4283
+ // src/lib/todos-connector.ts
4284
+ import { Database as Database2 } from "bun:sqlite";
4285
+ import { existsSync as existsSync4 } from "fs";
4286
+ import { join as join4 } from "path";
4287
+ import { homedir as homedir4 } from "os";
4288
+ function resolveTodosDbPath() {
4289
+ const envPath = process.env["TODOS_DB_PATH"];
4290
+ if (envPath)
4291
+ return envPath;
4292
+ return join4(homedir4(), ".todos", "todos.db");
4293
+ }
4294
+ function connectToTodos() {
4295
+ const dbPath = resolveTodosDbPath();
4296
+ if (!existsSync4(dbPath)) {
4297
+ throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
4298
+ }
4299
+ const db2 = new Database2(dbPath, { readonly: true });
4300
+ db2.exec("PRAGMA foreign_keys = ON");
4301
+ return db2;
4302
+ }
4303
+ function pullTasks(options = {}) {
4304
+ const db2 = connectToTodos();
4305
+ try {
4306
+ let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
4307
+ const params = [];
4308
+ if (options.status) {
4309
+ query += " AND status = ?";
4310
+ params.push(options.status);
4311
+ } else {
4312
+ query += " AND status IN ('pending', 'in_progress')";
4313
+ }
4314
+ if (options.priority) {
4315
+ query += " AND priority = ?";
4316
+ params.push(options.priority);
4317
+ }
4318
+ if (options.projectName) {
4319
+ const project = db2.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
4320
+ if (project) {
4321
+ query += " AND project_id = ?";
4322
+ params.push(project.id);
4323
+ }
4324
+ }
4325
+ query += " ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END";
4326
+ const tasks = db2.query(query).all(...params);
4327
+ if (options.tags && options.tags.length > 0) {
4328
+ return tasks.filter((task) => {
4329
+ const taskTags = JSON.parse(task.tags || "[]");
4330
+ return options.tags.some((tag) => taskTags.includes(tag));
4331
+ });
4332
+ }
4333
+ return tasks;
4334
+ } finally {
4335
+ db2.close();
4336
+ }
4337
+ }
4338
+ function taskToScenarioInput(task, projectId) {
4339
+ const tags = JSON.parse(task.tags || "[]");
4340
+ const priority = ["low", "medium", "high", "critical"].includes(task.priority) ? task.priority : "medium";
4341
+ const steps = [];
4342
+ if (task.description) {
4343
+ const lines = task.description.split(`
4344
+ `);
4345
+ for (const line of lines) {
4346
+ const match = line.match(/^\s*\d+[\.\)]\s*(.+)/);
4347
+ if (match?.[1]) {
4348
+ steps.push(match[1].trim());
4349
+ }
4350
+ }
4351
+ }
4352
+ return {
4353
+ name: task.title.replace(/^(OPE\d+-\d+|[A-Z]+-\d+):\s*/, ""),
4354
+ description: task.description || task.title,
4355
+ steps,
4356
+ tags,
4357
+ priority,
4358
+ projectId,
4359
+ metadata: { todosTaskId: task.id, todosShortId: task.short_id }
4360
+ };
4361
+ }
4362
+ function importFromTodos(options = {}) {
4363
+ const tasks = pullTasks({
4364
+ projectName: options.projectName,
4365
+ tags: options.tags ?? ["qa", "test", "testing"],
4366
+ priority: options.priority
4367
+ });
4368
+ const existing = listScenarios({ projectId: options.projectId });
4369
+ const existingTodoIds = new Set(existing.filter((s) => s.metadata?.todosTaskId).map((s) => s.metadata.todosTaskId));
4370
+ let imported = 0;
4371
+ let skipped = 0;
4372
+ for (const task of tasks) {
4373
+ if (existingTodoIds.has(task.id)) {
4374
+ skipped++;
4684
4375
  continue;
4685
- const isSlack = webhook.url.includes("hooks.slack.com");
4686
- const body = isSlack ? JSON.stringify(formatSlackPayload(payload)) : JSON.stringify(payload);
4687
- const headers = {
4688
- "Content-Type": "application/json"
4689
- };
4690
- if (webhook.secret) {
4691
- headers["X-Testers-Signature"] = signPayload(body, webhook.secret);
4692
4376
  }
4693
- try {
4694
- const response = await fetch(webhook.url, {
4695
- method: "POST",
4696
- headers,
4697
- body
4698
- });
4699
- if (!response.ok) {
4700
- await new Promise((r) => setTimeout(r, 5000));
4701
- await fetch(webhook.url, { method: "POST", headers, body });
4702
- }
4703
- } catch {}
4377
+ const input = taskToScenarioInput(task, options.projectId);
4378
+ createScenario(input);
4379
+ imported++;
4704
4380
  }
4381
+ return { imported, skipped };
4705
4382
  }
4383
+ var init_todos_connector = __esm(() => {
4384
+ init_scenarios();
4385
+ init_types();
4386
+ });
4706
4387
 
4707
- // src/lib/logs-integration.ts
4708
- async function pushFailedRunToLogs(run, failedResults, scenarios) {
4709
- const logsUrl = process.env.LOGS_URL;
4710
- if (!logsUrl)
4711
- return;
4388
+ // src/lib/failure-pipeline.ts
4389
+ async function createFailureTasks(run, failedResults, scenarios) {
4390
+ if (failedResults.length === 0)
4391
+ return { created: 0, skipped: 0 };
4392
+ const projectId = process.env["TESTERS_TODOS_PROJECT_ID"];
4393
+ if (!projectId)
4394
+ return { created: 0, skipped: 0 };
4395
+ let db2 = null;
4396
+ try {
4397
+ db2 = connectToTodos();
4398
+ } catch {
4399
+ return { created: 0, skipped: 0 };
4400
+ }
4712
4401
  const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
4713
- const entries = failedResults.map((result) => {
4714
- const scenario = scenarioMap.get(result.scenarioId);
4715
- return {
4716
- level: "error",
4717
- source: "sdk",
4718
- service: "testers",
4719
- message: `[testers] Scenario failed: ${scenario?.name ?? result.scenarioId}${result.error ? ` \u2014 ${result.error}` : ""}`,
4720
- metadata: {
4721
- run_id: run.id,
4722
- scenario_id: result.scenarioId,
4723
- scenario_name: scenario?.name,
4724
- url: run.url,
4725
- status: result.status,
4726
- duration_ms: result.durationMs
4402
+ let created = 0;
4403
+ let skipped = 0;
4404
+ try {
4405
+ for (const result of failedResults) {
4406
+ const scenario = scenarioMap.get(result.scenarioId);
4407
+ const title = `BUG: [testers] ${scenario?.name ?? result.scenarioId} failed`;
4408
+ const existing = db2.query("SELECT id FROM tasks WHERE title = ? AND status NOT IN ('completed', 'cancelled') LIMIT 1").get(title);
4409
+ if (existing) {
4410
+ skipped++;
4411
+ continue;
4727
4412
  }
4728
- };
4413
+ const id = crypto.randomUUID();
4414
+ const now2 = new Date().toISOString();
4415
+ const description = [
4416
+ `Test failure detected by open-testers.`,
4417
+ ``,
4418
+ `**Run:** ${run.id}`,
4419
+ `**URL:** ${run.url}`,
4420
+ `**Scenario:** ${scenario?.name ?? result.scenarioId}`,
4421
+ `**Status:** ${result.status}`,
4422
+ result.error ? `**Error:** ${result.error}` : null,
4423
+ result.reasoning ? `**Reasoning:** ${result.reasoning.slice(0, 500)}` : null,
4424
+ `**Duration:** ${result.durationMs ? `${(result.durationMs / 1000).toFixed(1)}s` : "N/A"}`,
4425
+ `**Tokens:** ${result.tokensUsed ?? 0}`
4426
+ ].filter(Boolean).join(`
4427
+ `);
4428
+ try {
4429
+ db2.query(`
4430
+ INSERT INTO tasks (id, short_id, title, description, status, priority, tags, project_id, version, created_at, updated_at)
4431
+ VALUES (?, ?, ?, ?, 'pending', 'high', ?, ?, 1, ?, ?)
4432
+ `).run(id, `BUG-${id.slice(0, 6)}`, title, description, JSON.stringify(["bug", "testers", "auto-created"]), projectId, now2, now2);
4433
+ created++;
4434
+ } catch {
4435
+ skipped++;
4436
+ }
4437
+ }
4438
+ } finally {
4439
+ db2.close();
4440
+ }
4441
+ return { created, skipped };
4442
+ }
4443
+ async function notifyFailureToConversations(run, failedResults, scenarios) {
4444
+ const baseUrl = process.env["TESTERS_CONVERSATIONS_URL"];
4445
+ const space = process.env["TESTERS_CONVERSATIONS_SPACE"];
4446
+ if (!baseUrl || !space)
4447
+ return;
4448
+ const scenarioMap = new Map(scenarios.map((s) => [s.id, s]));
4449
+ const total = run.total;
4450
+ const failedCount = failedResults.length;
4451
+ const passedCount = run.passed;
4452
+ const failureLines = failedResults.slice(0, 5).map((r) => {
4453
+ const name = scenarioMap.get(r.scenarioId)?.name ?? r.scenarioId;
4454
+ const err = r.error ? ` \u2014 ${r.error.slice(0, 120)}` : "";
4455
+ return ` \u274C ${name}${err}`;
4729
4456
  });
4457
+ const extra = failedResults.length > 5 ? ` \u2026 and ${failedResults.length - 5} more` : "";
4458
+ const message = [
4459
+ `\uD83D\uDEA8 **Testers run failed** \u2014 ${failedCount}/${total} scenarios failed`,
4460
+ ``,
4461
+ `**URL:** ${run.url}`,
4462
+ `**Run ID:** \`${run.id}\``,
4463
+ `**Pass rate:** ${passedCount}/${total}`,
4464
+ ``,
4465
+ `**Failures:**`,
4466
+ ...failureLines,
4467
+ extra
4468
+ ].filter((l) => l !== "").join(`
4469
+ `);
4730
4470
  try {
4731
- await fetch(`${logsUrl.replace(/\/$/, "")}/api/logs`, {
4471
+ await fetch(`${baseUrl.replace(/\/$/, "")}/api/spaces/${encodeURIComponent(space)}/messages`, {
4732
4472
  method: "POST",
4733
4473
  headers: { "Content-Type": "application/json" },
4734
- body: JSON.stringify(entries)
4474
+ body: JSON.stringify({ content: message, from: "testers" })
4735
4475
  });
4736
4476
  } catch {}
4737
4477
  }
4478
+ var init_failure_pipeline = __esm(() => {
4479
+ init_todos_connector();
4480
+ });
4481
+
4482
+ // src/db/flows.ts
4483
+ var exports_flows = {};
4484
+ __export(exports_flows, {
4485
+ topologicalSort: () => topologicalSort,
4486
+ removeDependency: () => removeDependency,
4487
+ listFlows: () => listFlows,
4488
+ getTransitiveDependencies: () => getTransitiveDependencies,
4489
+ getFlow: () => getFlow,
4490
+ getDependents: () => getDependents,
4491
+ getDependencies: () => getDependencies,
4492
+ deleteFlow: () => deleteFlow,
4493
+ createFlow: () => createFlow,
4494
+ addDependency: () => addDependency
4495
+ });
4496
+ function addDependency(scenarioId, dependsOn) {
4497
+ const db2 = getDatabase();
4498
+ const visited = new Set;
4499
+ const queue = [dependsOn];
4500
+ while (queue.length > 0) {
4501
+ const current = queue.shift();
4502
+ if (current === scenarioId) {
4503
+ throw new DependencyCycleError(scenarioId, dependsOn);
4504
+ }
4505
+ if (visited.has(current))
4506
+ continue;
4507
+ visited.add(current);
4508
+ const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
4509
+ for (const dep of deps) {
4510
+ if (!visited.has(dep.depends_on)) {
4511
+ queue.push(dep.depends_on);
4512
+ }
4513
+ }
4514
+ }
4515
+ db2.query("INSERT OR IGNORE INTO scenario_dependencies (scenario_id, depends_on) VALUES (?, ?)").run(scenarioId, dependsOn);
4516
+ }
4517
+ function removeDependency(scenarioId, dependsOn) {
4518
+ const db2 = getDatabase();
4519
+ const result = db2.query("DELETE FROM scenario_dependencies WHERE scenario_id = ? AND depends_on = ?").run(scenarioId, dependsOn);
4520
+ return result.changes > 0;
4521
+ }
4522
+ function getDependencies(scenarioId) {
4523
+ const db2 = getDatabase();
4524
+ const rows = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(scenarioId);
4525
+ return rows.map((r) => r.depends_on);
4526
+ }
4527
+ function getDependents(scenarioId) {
4528
+ const db2 = getDatabase();
4529
+ const rows = db2.query("SELECT scenario_id FROM scenario_dependencies WHERE depends_on = ?").all(scenarioId);
4530
+ return rows.map((r) => r.scenario_id);
4531
+ }
4532
+ function getTransitiveDependencies(scenarioId) {
4533
+ const db2 = getDatabase();
4534
+ const visited = new Set;
4535
+ const queue = [scenarioId];
4536
+ while (queue.length > 0) {
4537
+ const current = queue.shift();
4538
+ const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(current);
4539
+ for (const dep of deps) {
4540
+ if (!visited.has(dep.depends_on)) {
4541
+ visited.add(dep.depends_on);
4542
+ queue.push(dep.depends_on);
4543
+ }
4544
+ }
4545
+ }
4546
+ return Array.from(visited);
4547
+ }
4548
+ function topologicalSort(scenarioIds) {
4549
+ const db2 = getDatabase();
4550
+ const idSet = new Set(scenarioIds);
4551
+ const inDegree = new Map;
4552
+ const dependents = new Map;
4553
+ for (const id of scenarioIds) {
4554
+ inDegree.set(id, 0);
4555
+ dependents.set(id, []);
4556
+ }
4557
+ for (const id of scenarioIds) {
4558
+ const deps = db2.query("SELECT depends_on FROM scenario_dependencies WHERE scenario_id = ?").all(id);
4559
+ for (const dep of deps) {
4560
+ if (idSet.has(dep.depends_on)) {
4561
+ inDegree.set(id, (inDegree.get(id) ?? 0) + 1);
4562
+ dependents.get(dep.depends_on).push(id);
4563
+ }
4564
+ }
4565
+ }
4566
+ const queue = [];
4567
+ for (const [id, deg] of inDegree) {
4568
+ if (deg === 0)
4569
+ queue.push(id);
4570
+ }
4571
+ const sorted = [];
4572
+ while (queue.length > 0) {
4573
+ const current = queue.shift();
4574
+ sorted.push(current);
4575
+ for (const dep of dependents.get(current) ?? []) {
4576
+ const newDeg = (inDegree.get(dep) ?? 1) - 1;
4577
+ inDegree.set(dep, newDeg);
4578
+ if (newDeg === 0)
4579
+ queue.push(dep);
4580
+ }
4581
+ }
4582
+ if (sorted.length !== scenarioIds.length) {
4583
+ throw new DependencyCycleError("multiple", "multiple");
4584
+ }
4585
+ return sorted;
4586
+ }
4587
+ function createFlow(input) {
4588
+ const db2 = getDatabase();
4589
+ const id = uuid();
4590
+ const timestamp = now();
4591
+ db2.query(`
4592
+ INSERT INTO flows (id, project_id, name, description, scenario_ids, created_at, updated_at)
4593
+ VALUES (?, ?, ?, ?, ?, ?, ?)
4594
+ `).run(id, input.projectId ?? null, input.name, input.description ?? null, JSON.stringify(input.scenarioIds), timestamp, timestamp);
4595
+ return getFlow(id);
4596
+ }
4597
+ function getFlow(id) {
4598
+ const db2 = getDatabase();
4599
+ let row = db2.query("SELECT * FROM flows WHERE id = ?").get(id);
4600
+ if (row)
4601
+ return flowFromRow(row);
4602
+ const fullId = resolvePartialId("flows", id);
4603
+ if (fullId) {
4604
+ row = db2.query("SELECT * FROM flows WHERE id = ?").get(fullId);
4605
+ if (row)
4606
+ return flowFromRow(row);
4607
+ }
4608
+ return null;
4609
+ }
4610
+ function listFlows(projectId) {
4611
+ const db2 = getDatabase();
4612
+ if (projectId) {
4613
+ const rows2 = db2.query("SELECT * FROM flows WHERE project_id = ? ORDER BY created_at DESC").all(projectId);
4614
+ return rows2.map(flowFromRow);
4615
+ }
4616
+ const rows = db2.query("SELECT * FROM flows ORDER BY created_at DESC").all();
4617
+ return rows.map(flowFromRow);
4618
+ }
4619
+ function deleteFlow(id) {
4620
+ const db2 = getDatabase();
4621
+ const flow = getFlow(id);
4622
+ if (!flow)
4623
+ return false;
4624
+ const result = db2.query("DELETE FROM flows WHERE id = ?").run(flow.id);
4625
+ return result.changes > 0;
4626
+ }
4627
+ var init_flows = __esm(() => {
4628
+ init_database();
4629
+ init_database();
4630
+ init_types();
4631
+ });
4738
4632
 
4739
4633
  // src/lib/runner.ts
4740
- var eventHandler = null;
4634
+ var exports_runner = {};
4635
+ __export(exports_runner, {
4636
+ startRunAsync: () => startRunAsync,
4637
+ runSingleScenario: () => runSingleScenario,
4638
+ runByFilter: () => runByFilter,
4639
+ runBatch: () => runBatch,
4640
+ onRunEvent: () => onRunEvent
4641
+ });
4741
4642
  function onRunEvent(handler) {
4742
4643
  eventHandler = handler;
4743
4644
  }
@@ -4972,6 +4873,8 @@ async function runBatch(scenarios, options) {
4972
4873
  if (finalRun.status === "failed") {
4973
4874
  const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
4974
4875
  pushFailedRunToLogs(finalRun, failedResults, scenarios).catch(() => {});
4876
+ createFailureTasks(finalRun, failedResults, scenarios).catch(() => {});
4877
+ notifyFailureToConversations(finalRun, failedResults, scenarios).catch(() => {});
4975
4878
  }
4976
4879
  return { run: finalRun, results };
4977
4880
  }
@@ -5084,11 +4987,34 @@ function estimateCost(model, tokens) {
5084
4987
  const costPer1M = costs[model] ?? 0.5;
5085
4988
  return tokens / 1e6 * costPer1M * 100;
5086
4989
  }
4990
+ var eventHandler = null;
4991
+ var init_runner = __esm(() => {
4992
+ init_runs();
4993
+ init_results();
4994
+ init_screenshots();
4995
+ init_scenarios();
4996
+ init_browser();
4997
+ init_screenshotter();
4998
+ init_ai_client();
4999
+ init_config();
5000
+ init_webhooks();
5001
+ init_failure_pipeline();
5002
+ });
5087
5003
 
5088
5004
  // src/lib/reporter.ts
5005
+ var exports_reporter = {};
5006
+ __export(exports_reporter, {
5007
+ getScenarioRunStats: () => getScenarioRunStats,
5008
+ getExitCode: () => getExitCode,
5009
+ formatTerminal: () => formatTerminal,
5010
+ formatSummary: () => formatSummary,
5011
+ formatScenarioList: () => formatScenarioList,
5012
+ formatRunList: () => formatRunList,
5013
+ formatResultDetail: () => formatResultDetail,
5014
+ formatJSON: () => formatJSON,
5015
+ formatActionableSummary: () => formatActionableSummary
5016
+ });
5089
5017
  import chalk from "chalk";
5090
- init_scenarios();
5091
- init_database();
5092
5018
  function useEmoji() {
5093
5019
  return !process.env["NO_COLOR"] && process.argv.indexOf("--no-color") === -1;
5094
5020
  }
@@ -5150,6 +5076,13 @@ function formatTerminal(run, results, options) {
5150
5076
  return lines.join(`
5151
5077
  `);
5152
5078
  }
5079
+ function formatSummary(run) {
5080
+ const duration = run.finishedAt ? `${((new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()) / 1000).toFixed(1)}s` : "running";
5081
+ const passedStr = chalk.green(`${run.passed} passed`);
5082
+ const failedStr = run.failed > 0 ? chalk.red(` ${run.failed} failed`) : "";
5083
+ const totalStr = chalk.dim(` (${run.total} total)`);
5084
+ return ` ${passedStr}${failedStr}${totalStr} in ${duration}`;
5085
+ }
5153
5086
  function formatActionableSummary(run, results) {
5154
5087
  const emoji = useEmoji();
5155
5088
  const passedCount = results.filter((r) => r.status === "passed").length;
@@ -5222,182 +5155,707 @@ function formatJSON(run, results) {
5222
5155
  durationMs: run.finishedAt ? new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime() : null
5223
5156
  }
5224
5157
  };
5225
- return JSON.stringify(output, null, 2);
5226
- }
5227
- function getExitCode(run) {
5228
- if (run.status === "passed")
5229
- return 0;
5230
- if (run.status === "failed")
5231
- return 1;
5232
- return 2;
5158
+ return JSON.stringify(output, null, 2);
5159
+ }
5160
+ function getExitCode(run) {
5161
+ if (run.status === "passed")
5162
+ return 0;
5163
+ if (run.status === "failed")
5164
+ return 1;
5165
+ return 2;
5166
+ }
5167
+ function formatRunList(runs) {
5168
+ const lines = [];
5169
+ lines.push("");
5170
+ lines.push(chalk.bold(" Recent Runs"));
5171
+ lines.push("");
5172
+ if (runs.length === 0) {
5173
+ lines.push(chalk.dim(" No runs found."));
5174
+ lines.push("");
5175
+ return lines.join(`
5176
+ `);
5177
+ }
5178
+ for (const run of runs) {
5179
+ const statusIcon = run.status === "passed" ? chalk.green("PASS") : run.status === "failed" ? chalk.red("FAIL") : run.status === "running" ? chalk.blue("RUN ") : chalk.dim(run.status.toUpperCase().padEnd(4));
5180
+ const date = new Date(run.startedAt).toLocaleString();
5181
+ const id = run.id.slice(0, 8);
5182
+ lines.push(` ${statusIcon} ${chalk.dim(id)} ${run.url} ${chalk.dim(`${run.passed}/${run.total}`)} ${chalk.dim(date)}`);
5183
+ }
5184
+ lines.push("");
5185
+ return lines.join(`
5186
+ `);
5187
+ }
5188
+ function getScenarioRunStats(scenarioId) {
5189
+ const db2 = getDatabase();
5190
+ const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
5191
+ const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
5192
+ return {
5193
+ lastStatus: lastRow ? lastRow.status : null,
5194
+ passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
5195
+ };
5196
+ }
5197
+ function formatScenarioList(scenarios) {
5198
+ const lines = [];
5199
+ lines.push("");
5200
+ lines.push(chalk.bold(" Scenarios"));
5201
+ lines.push("");
5202
+ if (scenarios.length === 0) {
5203
+ lines.push(chalk.dim(" No scenarios found. Use 'testers add' to create one."));
5204
+ lines.push("");
5205
+ return lines.join(`
5206
+ `);
5207
+ }
5208
+ for (const s of scenarios) {
5209
+ const priorityColor = s.priority === "critical" ? chalk.red : s.priority === "high" ? chalk.yellow : s.priority === "medium" ? chalk.blue : chalk.dim;
5210
+ const tags = s.tags.length > 0 ? chalk.dim(` [${s.tags.join(", ")}]`) : "";
5211
+ let lastStatusIcon = chalk.dim("\u2014");
5212
+ let passRateStr = chalk.dim("\u2014");
5213
+ if (s.id) {
5214
+ const stats = getScenarioRunStats(s.id);
5215
+ if (stats.lastStatus === "passed")
5216
+ lastStatusIcon = chalk.green("\u2713");
5217
+ else if (stats.lastStatus === "failed")
5218
+ lastStatusIcon = chalk.red("\u2717");
5219
+ else if (stats.lastStatus === "error")
5220
+ lastStatusIcon = chalk.yellow("!");
5221
+ else if (stats.lastStatus === "skipped")
5222
+ lastStatusIcon = chalk.dim("~");
5223
+ passRateStr = stats.passRate === "\u2014" ? chalk.dim("\u2014") : chalk.dim(stats.passRate);
5224
+ }
5225
+ lines.push(` ${chalk.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags} ${lastStatusIcon} ${passRateStr}`);
5226
+ }
5227
+ lines.push("");
5228
+ return lines.join(`
5229
+ `);
5230
+ }
5231
+ function formatResultDetail(result, screenshots) {
5232
+ const lines = [];
5233
+ const scenario = getScenario(result.scenarioId);
5234
+ lines.push("");
5235
+ lines.push(chalk.bold(` Result ${result.id.slice(0, 8)}`));
5236
+ if (scenario) {
5237
+ lines.push(` Scenario: ${scenario.shortId} \u2014 ${scenario.name}`);
5238
+ }
5239
+ lines.push(` Status: ${result.status === "passed" ? chalk.green("PASSED") : chalk.red(result.status.toUpperCase())}`);
5240
+ lines.push(` Model: ${result.model}`);
5241
+ lines.push(` Duration: ${(result.durationMs / 1000).toFixed(1)}s`);
5242
+ lines.push(` Steps: ${result.stepsCompleted}/${result.stepsTotal}`);
5243
+ lines.push(` Tokens: ${result.tokensUsed} (~$${(result.costCents / 100).toFixed(4)})`);
5244
+ if (result.reasoning) {
5245
+ lines.push("");
5246
+ lines.push(chalk.bold(" Reasoning:"));
5247
+ lines.push(` ${result.reasoning}`);
5248
+ }
5249
+ if (result.error) {
5250
+ lines.push("");
5251
+ lines.push(chalk.red.bold(" Error:"));
5252
+ lines.push(chalk.red(` ${result.error}`));
5253
+ }
5254
+ if (screenshots.length > 0) {
5255
+ lines.push("");
5256
+ lines.push(chalk.bold(` Screenshots (${screenshots.length}):`));
5257
+ for (const ss of screenshots) {
5258
+ lines.push(` ${chalk.dim(`${String(ss.stepNumber).padStart(3, "0")}`)} ${ss.action} \u2014 ${chalk.dim(ss.filePath)}`);
5259
+ }
5260
+ }
5261
+ lines.push("");
5262
+ return lines.join(`
5263
+ `);
5264
+ }
5265
+ var init_reporter = __esm(() => {
5266
+ init_screenshots();
5267
+ init_scenarios();
5268
+ init_database();
5269
+ });
5270
+
5271
+ // src/lib/openapi-import.ts
5272
+ var exports_openapi_import = {};
5273
+ __export(exports_openapi_import, {
5274
+ parseOpenAPISpec: () => parseOpenAPISpec,
5275
+ importFromOpenAPI: () => importFromOpenAPI
5276
+ });
5277
+ import { readFileSync as readFileSync5 } from "fs";
5278
+ function parseSpec(content) {
5279
+ try {
5280
+ return JSON.parse(content);
5281
+ } catch {
5282
+ 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`");
5283
+ }
5284
+ }
5285
+ function methodPriority(method) {
5286
+ switch (method.toUpperCase()) {
5287
+ case "GET":
5288
+ return "medium";
5289
+ case "POST":
5290
+ return "high";
5291
+ case "PUT":
5292
+ return "high";
5293
+ case "DELETE":
5294
+ return "critical";
5295
+ case "PATCH":
5296
+ return "medium";
5297
+ default:
5298
+ return "low";
5299
+ }
5300
+ }
5301
+ function parseOpenAPISpec(filePathOrUrl) {
5302
+ let content;
5303
+ if (filePathOrUrl.startsWith("http")) {
5304
+ throw new Error("URL fetching not supported yet. Download the spec file first.");
5305
+ }
5306
+ content = readFileSync5(filePathOrUrl, "utf-8");
5307
+ const spec = parseSpec(content);
5308
+ const isOpenAPI3 = !!spec.openapi;
5309
+ const isSwagger2 = !!spec.swagger;
5310
+ if (!isOpenAPI3 && !isSwagger2) {
5311
+ throw new Error("Not a valid OpenAPI 3.x or Swagger 2.0 spec");
5312
+ }
5313
+ const scenarios = [];
5314
+ const paths = spec.paths ?? {};
5315
+ for (const [path, methods] of Object.entries(paths)) {
5316
+ for (const [method, operation] of Object.entries(methods)) {
5317
+ if (["get", "post", "put", "delete", "patch"].indexOf(method.toLowerCase()) === -1)
5318
+ continue;
5319
+ const op = operation;
5320
+ const name = op.summary ?? op.operationId ?? `${method.toUpperCase()} ${path}`;
5321
+ const tags = op.tags ?? [];
5322
+ const requiresAuth = !!(op.security?.length ?? spec.security?.length);
5323
+ const steps = [];
5324
+ steps.push(`Navigate to the API endpoint: ${method.toUpperCase()} ${path}`);
5325
+ if (op.parameters?.length) {
5326
+ const required = op.parameters.filter((p) => p.required);
5327
+ if (required.length > 0) {
5328
+ steps.push(`Fill required parameters: ${required.map((p) => p.name).join(", ")}`);
5329
+ }
5330
+ }
5331
+ if (["post", "put", "patch"].includes(method.toLowerCase())) {
5332
+ steps.push("Fill the request body with valid test data");
5333
+ }
5334
+ steps.push("Submit the request");
5335
+ const responses = op.responses ?? {};
5336
+ const successCodes = Object.keys(responses).filter((c) => c.startsWith("2"));
5337
+ if (successCodes.length > 0) {
5338
+ steps.push(`Verify response status is ${successCodes.join(" or ")}`);
5339
+ } else {
5340
+ steps.push("Verify the response is successful");
5341
+ }
5342
+ const description = [
5343
+ op.description ?? `Test the ${method.toUpperCase()} ${path} endpoint.`,
5344
+ requiresAuth ? "This endpoint requires authentication." : ""
5345
+ ].filter(Boolean).join(" ");
5346
+ scenarios.push({
5347
+ name,
5348
+ description,
5349
+ steps,
5350
+ tags: [...tags, "api", method.toLowerCase()],
5351
+ priority: methodPriority(method),
5352
+ targetPath: path,
5353
+ requiresAuth
5354
+ });
5355
+ }
5356
+ }
5357
+ return scenarios;
5358
+ }
5359
+ function importFromOpenAPI(filePathOrUrl, projectId) {
5360
+ const inputs = parseOpenAPISpec(filePathOrUrl);
5361
+ const scenarios = inputs.map((input) => createScenario({ ...input, projectId }));
5362
+ return { imported: scenarios.length, scenarios };
5363
+ }
5364
+ var init_openapi_import = __esm(() => {
5365
+ init_scenarios();
5366
+ });
5367
+
5368
+ // src/lib/recorder.ts
5369
+ var exports_recorder = {};
5370
+ __export(exports_recorder, {
5371
+ recordSession: () => recordSession,
5372
+ recordAndSave: () => recordAndSave,
5373
+ actionsToScenarioInput: () => actionsToScenarioInput
5374
+ });
5375
+ import { chromium as chromium3 } from "playwright";
5376
+ async function recordSession(url, options) {
5377
+ const browser = await chromium3.launch({ headless: false });
5378
+ const context = await browser.newContext({ viewport: { width: 1280, height: 720 } });
5379
+ const page = await context.newPage();
5380
+ const actions = [];
5381
+ const startTime = Date.now();
5382
+ const timeout = options?.timeout ?? 300000;
5383
+ page.on("framenavigated", (frame) => {
5384
+ if (frame === page.mainFrame()) {
5385
+ actions.push({ type: "navigate", url: frame.url(), timestamp: Date.now() - startTime });
5386
+ }
5387
+ });
5388
+ await page.addInitScript(() => {
5389
+ document.addEventListener("click", (e) => {
5390
+ const target = e.target;
5391
+ const selector = buildSelector(target);
5392
+ window.postMessage({ __testers_action: "click", selector }, "*");
5393
+ }, true);
5394
+ document.addEventListener("input", (e) => {
5395
+ const target = e.target;
5396
+ const selector = buildSelector(target);
5397
+ window.postMessage({ __testers_action: "fill", selector, value: target.value }, "*");
5398
+ }, true);
5399
+ document.addEventListener("change", (e) => {
5400
+ const target = e.target;
5401
+ if (target.tagName === "SELECT") {
5402
+ const selector = buildSelector(target);
5403
+ window.postMessage({ __testers_action: "select", selector, value: target.value }, "*");
5404
+ }
5405
+ }, true);
5406
+ document.addEventListener("keydown", (e) => {
5407
+ if (["Enter", "Tab", "Escape"].includes(e.key)) {
5408
+ window.postMessage({ __testers_action: "press", key: e.key }, "*");
5409
+ }
5410
+ }, true);
5411
+ function buildSelector(el) {
5412
+ if (el.id)
5413
+ return `#${el.id}`;
5414
+ if (el.getAttribute("data-testid"))
5415
+ return `[data-testid="${el.getAttribute("data-testid")}"]`;
5416
+ if (el.getAttribute("name"))
5417
+ return `${el.tagName.toLowerCase()}[name="${el.getAttribute("name")}"]`;
5418
+ if (el.getAttribute("aria-label"))
5419
+ return `[aria-label="${el.getAttribute("aria-label")}"]`;
5420
+ if (el.className && typeof el.className === "string") {
5421
+ const classes = el.className.trim().split(/\s+/).slice(0, 2).join(".");
5422
+ if (classes)
5423
+ return `${el.tagName.toLowerCase()}.${classes}`;
5424
+ }
5425
+ const text = el.textContent?.trim().slice(0, 30);
5426
+ if (text)
5427
+ return `text="${text}"`;
5428
+ return el.tagName.toLowerCase();
5429
+ }
5430
+ });
5431
+ const pollInterval = setInterval(async () => {
5432
+ try {
5433
+ const newActions = await page.evaluate(() => {
5434
+ const collected = window.__testers_collected ?? [];
5435
+ window.__testers_collected = [];
5436
+ return collected;
5437
+ });
5438
+ for (const a of newActions) {
5439
+ actions.push({
5440
+ type: a["type"],
5441
+ selector: a["selector"],
5442
+ value: a["value"],
5443
+ key: a["key"],
5444
+ timestamp: Date.now() - startTime
5445
+ });
5446
+ }
5447
+ } catch {}
5448
+ }, 500);
5449
+ await page.exposeFunction("__testersRecord", (action) => {
5450
+ actions.push({ ...action, timestamp: Date.now() - startTime });
5451
+ });
5452
+ await page.addInitScript(() => {
5453
+ window.addEventListener("message", (e) => {
5454
+ if (e.data?.__testers_action) {
5455
+ const { __testers_action, ...rest } = e.data;
5456
+ window.__testersRecord({ type: __testers_action, ...rest });
5457
+ }
5458
+ });
5459
+ });
5460
+ await page.goto(url);
5461
+ actions.push({ type: "navigate", url, timestamp: 0 });
5462
+ console.log(`
5463
+ Recording started. Interact with the browser.`);
5464
+ console.log(` Close the browser window or wait ${timeout / 1000}s to stop.
5465
+ `);
5466
+ await Promise.race([
5467
+ page.waitForEvent("close").catch(() => {}),
5468
+ context.waitForEvent("close").catch(() => {}),
5469
+ new Promise((resolve) => setTimeout(resolve, timeout))
5470
+ ]);
5471
+ clearInterval(pollInterval);
5472
+ try {
5473
+ await browser.close();
5474
+ } catch {}
5475
+ return {
5476
+ actions,
5477
+ url,
5478
+ duration: Date.now() - startTime
5479
+ };
5233
5480
  }
5234
- function formatRunList(runs) {
5235
- const lines = [];
5236
- lines.push("");
5237
- lines.push(chalk.bold(" Recent Runs"));
5238
- lines.push("");
5239
- if (runs.length === 0) {
5240
- lines.push(chalk.dim(" No runs found."));
5241
- lines.push("");
5242
- return lines.join(`
5243
- `);
5481
+ function actionsToScenarioInput(recording, name, projectId) {
5482
+ const steps = [];
5483
+ const seenFills = new Map;
5484
+ for (const action of recording.actions) {
5485
+ switch (action.type) {
5486
+ case "navigate":
5487
+ if (action.url)
5488
+ steps.push(`Navigate to ${action.url}`);
5489
+ break;
5490
+ case "click":
5491
+ if (action.selector)
5492
+ steps.push(`Click ${action.selector}`);
5493
+ break;
5494
+ case "fill":
5495
+ if (action.selector && action.value) {
5496
+ seenFills.set(action.selector, action.value);
5497
+ }
5498
+ break;
5499
+ case "select":
5500
+ if (action.selector && action.value)
5501
+ steps.push(`Select "${action.value}" in ${action.selector}`);
5502
+ break;
5503
+ case "press":
5504
+ if (action.key)
5505
+ steps.push(`Press ${action.key}`);
5506
+ break;
5507
+ }
5244
5508
  }
5245
- for (const run of runs) {
5246
- const statusIcon = run.status === "passed" ? chalk.green("PASS") : run.status === "failed" ? chalk.red("FAIL") : run.status === "running" ? chalk.blue("RUN ") : chalk.dim(run.status.toUpperCase().padEnd(4));
5247
- const date = new Date(run.startedAt).toLocaleString();
5248
- const id = run.id.slice(0, 8);
5249
- lines.push(` ${statusIcon} ${chalk.dim(id)} ${run.url} ${chalk.dim(`${run.passed}/${run.total}`)} ${chalk.dim(date)}`);
5509
+ for (const [selector, value] of seenFills) {
5510
+ steps.push(`Fill ${selector} with "${value}"`);
5250
5511
  }
5251
- lines.push("");
5252
- return lines.join(`
5253
- `);
5254
- }
5255
- function getScenarioRunStats(scenarioId) {
5256
- const db2 = getDatabase();
5257
- const lastRow = db2.query("SELECT status FROM results WHERE scenario_id = ? ORDER BY created_at DESC LIMIT 1").get(scenarioId);
5258
- const statsRow = db2.query("SELECT COUNT(*) as total, SUM(CASE WHEN status = 'passed' THEN 1 ELSE 0 END) as passed FROM results WHERE scenario_id = ?").get(scenarioId);
5259
5512
  return {
5260
- lastStatus: lastRow ? lastRow.status : null,
5261
- passRate: statsRow && statsRow.total > 0 ? `${statsRow.passed}/${statsRow.total}` : "\u2014"
5513
+ name,
5514
+ description: `Recorded session on ${recording.url} (${(recording.duration / 1000).toFixed(0)}s, ${recording.actions.length} actions)`,
5515
+ steps,
5516
+ tags: ["recorded"],
5517
+ projectId
5262
5518
  };
5263
5519
  }
5264
- function formatScenarioList(scenarios) {
5265
- const lines = [];
5266
- lines.push("");
5267
- lines.push(chalk.bold(" Scenarios"));
5268
- lines.push("");
5269
- if (scenarios.length === 0) {
5270
- lines.push(chalk.dim(" No scenarios found. Use 'testers add' to create one."));
5271
- lines.push("");
5272
- return lines.join(`
5273
- `);
5274
- }
5275
- for (const s of scenarios) {
5276
- const priorityColor = s.priority === "critical" ? chalk.red : s.priority === "high" ? chalk.yellow : s.priority === "medium" ? chalk.blue : chalk.dim;
5277
- const tags = s.tags.length > 0 ? chalk.dim(` [${s.tags.join(", ")}]`) : "";
5278
- let lastStatusIcon = chalk.dim("\u2014");
5279
- let passRateStr = chalk.dim("\u2014");
5280
- if (s.id) {
5281
- const stats = getScenarioRunStats(s.id);
5282
- if (stats.lastStatus === "passed")
5283
- lastStatusIcon = chalk.green("\u2713");
5284
- else if (stats.lastStatus === "failed")
5285
- lastStatusIcon = chalk.red("\u2717");
5286
- else if (stats.lastStatus === "error")
5287
- lastStatusIcon = chalk.yellow("!");
5288
- else if (stats.lastStatus === "skipped")
5289
- lastStatusIcon = chalk.dim("~");
5290
- passRateStr = stats.passRate === "\u2014" ? chalk.dim("\u2014") : chalk.dim(stats.passRate);
5291
- }
5292
- lines.push(` ${chalk.cyan(s.shortId)} ${s.name} ${priorityColor(s.priority)}${tags} ${lastStatusIcon} ${passRateStr}`);
5293
- }
5294
- lines.push("");
5295
- return lines.join(`
5296
- `);
5520
+ async function recordAndSave(url, name, projectId) {
5521
+ const recording = await recordSession(url);
5522
+ const input = actionsToScenarioInput(recording, name, projectId);
5523
+ const scenario = createScenario(input);
5524
+ return { recording, scenario };
5297
5525
  }
5526
+ var init_recorder = __esm(() => {
5527
+ init_scenarios();
5528
+ });
5298
5529
 
5299
- // src/lib/todos-connector.ts
5300
- init_scenarios();
5301
- init_types();
5302
- import { Database as Database2 } from "bun:sqlite";
5303
- import { existsSync as existsSync4 } from "fs";
5304
- import { join as join4 } from "path";
5305
- import { homedir as homedir4 } from "os";
5306
- function resolveTodosDbPath() {
5307
- const envPath = process.env["TODOS_DB_PATH"];
5308
- if (envPath)
5309
- return envPath;
5310
- return join4(homedir4(), ".todos", "todos.db");
5311
- }
5312
- function connectToTodos() {
5313
- const dbPath = resolveTodosDbPath();
5314
- if (!existsSync4(dbPath)) {
5315
- throw new TodosConnectionError(`Todos database not found at ${dbPath}. Install @hasna/todos or set TODOS_DB_PATH.`);
5316
- }
5317
- const db2 = new Database2(dbPath, { readonly: true });
5318
- db2.exec("PRAGMA foreign_keys = ON");
5319
- return db2;
5320
- }
5321
- function pullTasks(options = {}) {
5322
- const db2 = connectToTodos();
5323
- try {
5324
- let query = "SELECT id, short_id, title, description, status, priority, tags, project_id FROM tasks WHERE 1=1";
5325
- const params = [];
5326
- if (options.status) {
5327
- query += " AND status = ?";
5328
- params.push(options.status);
5329
- } else {
5330
- query += " AND status IN ('pending', 'in_progress')";
5530
+ // src/lib/affected.ts
5531
+ var exports_affected = {};
5532
+ __export(exports_affected, {
5533
+ matchFilesToScenarios: () => matchFilesToScenarios
5534
+ });
5535
+ function globToRegex(glob) {
5536
+ const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\x00DS\x00").replace(/\*/g, "[^/]*").replace(/\x00DS\x00/g, ".*");
5537
+ return new RegExp(`^${escaped}$`, "i");
5538
+ }
5539
+ function matchFilesToScenarios(filePaths, scenarios, mappings = []) {
5540
+ if (filePaths.length === 0)
5541
+ return scenarios;
5542
+ const compiledMappings = mappings.map((m) => ({
5543
+ regex: globToRegex(m.glob),
5544
+ tags: m.tags
5545
+ }));
5546
+ const normPaths = filePaths.map((p) => p.replace(/\\/g, "/").toLowerCase());
5547
+ const matchedIds = new Set;
5548
+ for (const scenario of scenarios) {
5549
+ let matched = false;
5550
+ if (!matched) {
5551
+ for (const { regex, tags } of compiledMappings) {
5552
+ if (normPaths.some((fp) => regex.test(fp)) && tags.some((tag) => scenario.tags.includes(tag))) {
5553
+ matched = true;
5554
+ break;
5555
+ }
5556
+ }
5331
5557
  }
5332
- if (options.priority) {
5333
- query += " AND priority = ?";
5334
- params.push(options.priority);
5558
+ if (!matched && scenario.targetPath) {
5559
+ const segments = scenario.targetPath.replace(/^\//, "").split("/").filter((s) => s.length > 2);
5560
+ if (segments.some((seg) => normPaths.some((fp) => fp.includes(seg.toLowerCase())))) {
5561
+ matched = true;
5562
+ }
5335
5563
  }
5336
- if (options.projectName) {
5337
- const project = db2.query("SELECT id FROM projects WHERE name = ?").get(options.projectName);
5338
- if (project) {
5339
- query += " AND project_id = ?";
5340
- params.push(project.id);
5564
+ if (!matched) {
5565
+ for (const tag of scenario.tags) {
5566
+ if (tag.length > 2 && normPaths.some((fp) => fp.includes(tag.toLowerCase()))) {
5567
+ matched = true;
5568
+ break;
5569
+ }
5341
5570
  }
5342
5571
  }
5343
- query += " ORDER BY CASE priority WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2 WHEN 'low' THEN 3 END";
5344
- const tasks = db2.query(query).all(...params);
5345
- if (options.tags && options.tags.length > 0) {
5346
- return tasks.filter((task) => {
5347
- const taskTags = JSON.parse(task.tags || "[]");
5348
- return options.tags.some((tag) => taskTags.includes(tag));
5349
- });
5572
+ if (!matched) {
5573
+ const nameWords = scenario.name.toLowerCase().split(/[\s\-_/]+/).filter((w) => w.length > 3);
5574
+ if (nameWords.some((word) => normPaths.some((fp) => fp.includes(word)))) {
5575
+ matched = true;
5576
+ }
5350
5577
  }
5351
- return tasks;
5352
- } finally {
5353
- db2.close();
5578
+ if (matched)
5579
+ matchedIds.add(scenario.id);
5354
5580
  }
5581
+ return scenarios.filter((s) => matchedIds.has(s.id));
5355
5582
  }
5356
- function taskToScenarioInput(task, projectId) {
5357
- const tags = JSON.parse(task.tags || "[]");
5358
- const priority = ["low", "medium", "high", "critical"].includes(task.priority) ? task.priority : "medium";
5359
- const steps = [];
5360
- if (task.description) {
5361
- const lines = task.description.split(`
5362
- `);
5363
- for (const line of lines) {
5364
- const match = line.match(/^\s*\d+[\.\)]\s*(.+)/);
5365
- if (match?.[1]) {
5366
- steps.push(match[1].trim());
5583
+
5584
+ // src/lib/git-watch.ts
5585
+ var exports_git_watch = {};
5586
+ __export(exports_git_watch, {
5587
+ startGitWatcher: () => startGitWatcher
5588
+ });
5589
+ import { execSync as execSync2 } from "child_process";
5590
+ import chalk5 from "chalk";
5591
+ function getLatestCommitHash(dir) {
5592
+ try {
5593
+ return execSync2("git rev-parse HEAD", { cwd: dir, stdio: ["pipe", "pipe", "pipe"] }).toString().trim();
5594
+ } catch {
5595
+ return null;
5596
+ }
5597
+ }
5598
+ function getChangedFiles(fromHash, toHash, dir) {
5599
+ try {
5600
+ const out = execSync2(`git diff --name-only ${fromHash} ${toHash}`, {
5601
+ cwd: dir,
5602
+ stdio: ["pipe", "pipe", "pipe"]
5603
+ }).toString();
5604
+ return out.split(`
5605
+ `).filter(Boolean);
5606
+ } catch {
5607
+ return [];
5608
+ }
5609
+ }
5610
+ async function startGitWatcher(options) {
5611
+ const {
5612
+ url,
5613
+ dir = process.cwd(),
5614
+ pollIntervalMs = 1e4,
5615
+ mappings = [],
5616
+ projectId,
5617
+ tags,
5618
+ ...runOpts
5619
+ } = options;
5620
+ let lastHash = getLatestCommitHash(dir);
5621
+ if (!lastHash) {
5622
+ console.error(chalk5.red(" [git-watch] Not a git repository or git not available."));
5623
+ process.exit(1);
5624
+ }
5625
+ console.log("");
5626
+ console.log(chalk5.bold(" Testers Git Watch"));
5627
+ console.log(chalk5.dim(` Directory: ${dir}`));
5628
+ console.log(chalk5.dim(` Target URL: ${url}`));
5629
+ console.log(chalk5.dim(` Poll every: ${pollIntervalMs / 1000}s`));
5630
+ console.log(chalk5.dim(` Last commit: ${lastHash.slice(0, 8)}`));
5631
+ console.log("");
5632
+ console.log(chalk5.dim(" Watching for new commits... (Ctrl+C to stop)"));
5633
+ console.log("");
5634
+ const cleanup = () => {
5635
+ console.log("");
5636
+ console.log(chalk5.dim(" Git watch stopped."));
5637
+ process.exit(0);
5638
+ };
5639
+ process.on("SIGINT", cleanup);
5640
+ process.on("SIGTERM", cleanup);
5641
+ while (true) {
5642
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
5643
+ const currentHash = getLatestCommitHash(dir);
5644
+ if (!currentHash || currentHash === lastHash)
5645
+ continue;
5646
+ const changedFiles = getChangedFiles(lastHash, currentHash, dir);
5647
+ lastHash = currentHash;
5648
+ console.log(chalk5.yellow(` [commit] ${currentHash.slice(0, 8)} \u2014 ${changedFiles.length} file(s) changed`));
5649
+ for (const f of changedFiles.slice(0, 10)) {
5650
+ console.log(chalk5.dim(` ${f}`));
5651
+ }
5652
+ if (changedFiles.length > 10) {
5653
+ console.log(chalk5.dim(` \u2026 and ${changedFiles.length - 10} more`));
5654
+ }
5655
+ const allScenarios = listScenarios({ projectId, tags });
5656
+ const matched = matchFilesToScenarios(changedFiles, allScenarios, mappings);
5657
+ if (matched.length === 0) {
5658
+ console.log(chalk5.dim(" No matching scenarios \u2014 skipping run."));
5659
+ console.log("");
5660
+ continue;
5661
+ }
5662
+ console.log(chalk5.blue(` [running] ${matched.length} affected scenario(s) against ${url}...`));
5663
+ console.log("");
5664
+ try {
5665
+ const { run, results } = await runBatch(matched, { url, projectId, ...runOpts });
5666
+ console.log(formatTerminal(run, results));
5667
+ if (run.status === "failed") {
5668
+ const failedResults = results.filter((r) => r.status === "failed" || r.status === "error");
5669
+ notifyFailureToConversations(run, failedResults, matched).catch(() => {});
5367
5670
  }
5671
+ } catch (error) {
5672
+ console.error(chalk5.red(` Error: ${error instanceof Error ? error.message : String(error)}`));
5368
5673
  }
5674
+ console.log(chalk5.dim(" Watching for new commits..."));
5675
+ console.log("");
5369
5676
  }
5370
- return {
5371
- name: task.title.replace(/^(OPE\d+-\d+|[A-Z]+-\d+):\s*/, ""),
5372
- description: task.description || task.title,
5373
- steps,
5374
- tags,
5375
- priority,
5376
- projectId,
5377
- metadata: { todosTaskId: task.id, todosShortId: task.short_id }
5378
- };
5379
5677
  }
5380
- function importFromTodos(options = {}) {
5381
- const tasks = pullTasks({
5382
- projectName: options.projectName,
5383
- tags: options.tags ?? ["qa", "test", "testing"],
5384
- priority: options.priority
5385
- });
5386
- const existing = listScenarios({ projectId: options.projectId });
5387
- const existingTodoIds = new Set(existing.filter((s) => s.metadata?.todosTaskId).map((s) => s.metadata.todosTaskId));
5388
- let imported = 0;
5389
- let skipped = 0;
5390
- for (const task of tasks) {
5391
- if (existingTodoIds.has(task.id)) {
5392
- skipped++;
5393
- continue;
5394
- }
5395
- const input = taskToScenarioInput(task, options.projectId);
5396
- createScenario(input);
5397
- imported++;
5678
+ var init_git_watch = __esm(() => {
5679
+ init_scenarios();
5680
+ init_runner();
5681
+ init_reporter();
5682
+ init_failure_pipeline();
5683
+ });
5684
+
5685
+ // src/db/agents.ts
5686
+ var exports_agents = {};
5687
+ __export(exports_agents, {
5688
+ setAgentFocus: () => setAgentFocus,
5689
+ registerAgent: () => registerAgent,
5690
+ listAgents: () => listAgents,
5691
+ heartbeatAgent: () => heartbeatAgent,
5692
+ getAgentByName: () => getAgentByName,
5693
+ getAgent: () => getAgent
5694
+ });
5695
+ function registerAgent(input) {
5696
+ const db2 = getDatabase();
5697
+ const existing = db2.query("SELECT * FROM agents WHERE name = ?").get(input.name);
5698
+ if (existing) {
5699
+ db2.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), existing.id);
5700
+ return getAgent(existing.id);
5398
5701
  }
5399
- return { imported, skipped };
5702
+ const id = uuid();
5703
+ const timestamp = now();
5704
+ db2.query(`
5705
+ INSERT INTO agents (id, name, description, role, metadata, created_at, last_seen_at)
5706
+ VALUES (?, ?, ?, ?, '{}', ?, ?)
5707
+ `).run(id, input.name, input.description ?? null, input.role ?? null, timestamp, timestamp);
5708
+ return getAgent(id);
5709
+ }
5710
+ function getAgent(id) {
5711
+ const db2 = getDatabase();
5712
+ const row = db2.query("SELECT * FROM agents WHERE id = ?").get(id);
5713
+ return row ? agentFromRow(row) : null;
5714
+ }
5715
+ function getAgentByName(name) {
5716
+ const db2 = getDatabase();
5717
+ const row = db2.query("SELECT * FROM agents WHERE name = ?").get(name);
5718
+ return row ? agentFromRow(row) : null;
5719
+ }
5720
+ function listAgents() {
5721
+ const db2 = getDatabase();
5722
+ const rows = db2.query("SELECT * FROM agents ORDER BY created_at DESC").all();
5723
+ return rows.map(agentFromRow);
5724
+ }
5725
+ function heartbeatAgent(id) {
5726
+ const db2 = getDatabase();
5727
+ const affected = db2.query("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(now(), id);
5728
+ if (affected.changes === 0)
5729
+ return null;
5730
+ return getAgent(id);
5731
+ }
5732
+ function setAgentFocus(id, scenarioId) {
5733
+ const db2 = getDatabase();
5734
+ const agent = getAgent(id);
5735
+ if (!agent)
5736
+ return null;
5737
+ const metadata = { ...agent.metadata ?? {}, focus: scenarioId };
5738
+ db2.query("UPDATE agents SET metadata = ?, last_seen_at = ? WHERE id = ?").run(JSON.stringify(metadata), now(), id);
5739
+ return getAgent(id);
5400
5740
  }
5741
+ var init_agents = __esm(() => {
5742
+ init_types();
5743
+ init_database();
5744
+ });
5745
+
5746
+ // node_modules/commander/esm.mjs
5747
+ var import__ = __toESM(require_commander(), 1);
5748
+ var {
5749
+ program,
5750
+ createCommand,
5751
+ createArgument,
5752
+ createOption,
5753
+ CommanderError,
5754
+ InvalidArgumentError,
5755
+ InvalidOptionArgumentError,
5756
+ Command,
5757
+ Argument,
5758
+ Option,
5759
+ Help
5760
+ } = import__.default;
5761
+
5762
+ // src/cli/index.tsx
5763
+ import chalk6 from "chalk";
5764
+ // package.json
5765
+ var package_default = {
5766
+ name: "@hasna/testers",
5767
+ version: "0.0.13",
5768
+ description: "AI-powered QA testing CLI \u2014 spawns cheap AI agents to test web apps with headless browsers",
5769
+ type: "module",
5770
+ main: "dist/index.js",
5771
+ types: "dist/index.d.ts",
5772
+ bin: {
5773
+ testers: "dist/cli/index.js",
5774
+ "testers-mcp": "dist/mcp/index.js",
5775
+ "testers-serve": "dist/server/index.js"
5776
+ },
5777
+ exports: {
5778
+ ".": {
5779
+ types: "./dist/index.d.ts",
5780
+ import: "./dist/index.js"
5781
+ }
5782
+ },
5783
+ files: [
5784
+ "dist/",
5785
+ "dashboard/dist/",
5786
+ "LICENSE",
5787
+ "README.md"
5788
+ ],
5789
+ scripts: {
5790
+ build: "bun run build:dashboard && bun run build:cli && bun run build:mcp && bun run build:server && bun run build:lib && bun run build:types",
5791
+ "build:cli": "bun build src/cli/index.tsx --outdir dist/cli --target bun --external ink --external react --external chalk --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
5792
+ "build:mcp": "bun build src/mcp/index.ts --outdir dist/mcp --target bun --external @modelcontextprotocol/sdk --external @anthropic-ai/sdk --external playwright",
5793
+ "build:server": "bun build src/server/index.ts --outdir dist/server --target bun --external @anthropic-ai/sdk --external playwright",
5794
+ "build:lib": "bun build src/index.ts --outdir dist --target bun --external playwright --external @anthropic-ai/sdk --external @modelcontextprotocol/sdk",
5795
+ "build:types": "NODE_OPTIONS='--max-old-space-size=8192' tsc --emitDeclarationOnly --outDir dist --skipLibCheck",
5796
+ "build:dashboard": "cd dashboard && bun run build",
5797
+ typecheck: "tsc --noEmit",
5798
+ test: "bun test",
5799
+ "dev:cli": "bun run src/cli/index.tsx",
5800
+ "dev:mcp": "bun run src/mcp/index.ts",
5801
+ "dev:serve": "bun run src/server/index.ts",
5802
+ prepublishOnly: "bun run build"
5803
+ },
5804
+ dependencies: {
5805
+ "@anthropic-ai/sdk": "^0.52.0",
5806
+ "@modelcontextprotocol/sdk": "^1.12.1",
5807
+ chalk: "^5.4.1",
5808
+ commander: "^13.1.0",
5809
+ ink: "^5.2.0",
5810
+ playwright: "^1.50.0",
5811
+ react: "^18.3.1",
5812
+ zod: "^3.24.2"
5813
+ },
5814
+ devDependencies: {
5815
+ "@types/bun": "latest",
5816
+ "@types/react": "^18.3.18",
5817
+ typescript: "^5.7.3"
5818
+ },
5819
+ engines: {
5820
+ bun: ">=1.0.0"
5821
+ },
5822
+ publishConfig: {
5823
+ access: "public",
5824
+ registry: "https://registry.npmjs.org/"
5825
+ },
5826
+ repository: {
5827
+ type: "git",
5828
+ url: "https://github.com/hasna/open-testers.git"
5829
+ },
5830
+ license: "MIT",
5831
+ keywords: [
5832
+ "testing",
5833
+ "qa",
5834
+ "ai",
5835
+ "playwright",
5836
+ "browser",
5837
+ "screenshot",
5838
+ "automation",
5839
+ "cli",
5840
+ "mcp"
5841
+ ]
5842
+ };
5843
+
5844
+ // src/cli/index.tsx
5845
+ init_scenarios();
5846
+ init_runs();
5847
+ init_results();
5848
+ init_screenshots();
5849
+ init_runner();
5850
+ init_reporter();
5851
+ init_config();
5852
+ init_todos_connector();
5853
+ init_browser();
5854
+ import { render, Box, Text, useInput, useApp } from "ink";
5855
+ import React, { useState } from "react";
5856
+ import { readFileSync as readFileSync6, readdirSync, writeFileSync as writeFileSync3 } from "fs";
5857
+ import { createInterface } from "readline";
5858
+ import { join as join6, resolve } from "path";
5401
5859
 
5402
5860
  // src/lib/init.ts
5403
5861
  init_scenarios();
@@ -5679,7 +6137,10 @@ function initProject(options) {
5679
6137
 
5680
6138
  // src/lib/smoke.ts
5681
6139
  init_scenarios();
6140
+ init_runner();
5682
6141
  init_runs();
6142
+ init_config();
6143
+ init_ai_client();
5683
6144
  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:
5684
6145
 
5685
6146
  1. Start at the given URL and take a screenshot
@@ -5899,8 +6360,9 @@ function formatSmokeReport(result) {
5899
6360
 
5900
6361
  // src/lib/diff.ts
5901
6362
  init_runs();
5902
- import chalk2 from "chalk";
6363
+ init_results();
5903
6364
  init_scenarios();
6365
+ import chalk2 from "chalk";
5904
6366
  function diffRuns(runId1, runId2) {
5905
6367
  const run1 = getRun(runId1);
5906
6368
  if (!run1) {
@@ -6047,11 +6509,13 @@ function formatDiffJSON(diff) {
6047
6509
  }
6048
6510
 
6049
6511
  // src/lib/visual-diff.ts
6050
- import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
6051
- import chalk3 from "chalk";
6512
+ init_screenshots();
6513
+ init_results();
6052
6514
  init_runs();
6053
6515
  init_scenarios();
6054
6516
  init_database();
6517
+ import { readFileSync as readFileSync3, existsSync as existsSync6 } from "fs";
6518
+ import chalk3 from "chalk";
6055
6519
  var DEFAULT_THRESHOLD = 0.1;
6056
6520
  function setBaseline(runId) {
6057
6521
  const run = getRun(runId);
@@ -6178,8 +6642,10 @@ function formatVisualDiffTerminal(results, threshold = DEFAULT_THRESHOLD) {
6178
6642
 
6179
6643
  // src/lib/report.ts
6180
6644
  init_runs();
6181
- import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
6645
+ init_results();
6646
+ init_screenshots();
6182
6647
  init_scenarios();
6648
+ import { readFileSync as readFileSync4, existsSync as existsSync7 } from "fs";
6183
6649
  function imageToBase64(filePath) {
6184
6650
  if (!filePath || !existsSync7(filePath))
6185
6651
  return "";
@@ -6378,6 +6844,7 @@ function generateLatestReport() {
6378
6844
 
6379
6845
  // src/lib/costs.ts
6380
6846
  init_database();
6847
+ init_config();
6381
6848
  import chalk4 from "chalk";
6382
6849
  function getDateFilter(period) {
6383
6850
  switch (period) {
@@ -7177,10 +7644,10 @@ async function runInteractiveAdd(projectId) {
7177
7644
  priority: result.priority,
7178
7645
  projectId
7179
7646
  });
7180
- log(chalk5.green(`
7181
- Created scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
7647
+ log(chalk6.green(`
7648
+ Created scenario ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
7182
7649
  } else {
7183
- log(chalk5.dim(`
7650
+ log(chalk6.dim(`
7184
7651
  Cancelled.`));
7185
7652
  }
7186
7653
  }
@@ -7239,19 +7706,19 @@ program2.command("add [name]").alias("create").description("Create a new test sc
7239
7706
  return;
7240
7707
  }
7241
7708
  if (!name) {
7242
- logError(chalk5.red("Error: scenario name is required"));
7709
+ logError(chalk6.red("Error: scenario name is required"));
7243
7710
  process.exit(1);
7244
7711
  }
7245
7712
  if (opts.template) {
7246
7713
  const template = getTemplate(opts.template);
7247
7714
  if (!template) {
7248
- logError(chalk5.red(`Unknown template: ${opts.template}. Available: ${listTemplateNames().join(", ")}`));
7715
+ logError(chalk6.red(`Unknown template: ${opts.template}. Available: ${listTemplateNames().join(", ")}`));
7249
7716
  process.exit(1);
7250
7717
  }
7251
7718
  const projectId2 = resolveProject(opts.project);
7252
7719
  for (const input of template) {
7253
7720
  const s = createScenario({ ...input, projectId: projectId2 });
7254
- log(chalk5.green(` Created ${s.shortId}: ${s.name}`));
7721
+ log(chalk6.green(` Created ${s.shortId}: ${s.name}`));
7255
7722
  }
7256
7723
  return;
7257
7724
  }
@@ -7270,9 +7737,9 @@ program2.command("add [name]").alias("create").description("Create a new test sc
7270
7737
  assertions: assertions.length > 0 ? assertions : undefined,
7271
7738
  projectId
7272
7739
  });
7273
- log(chalk5.green(`Created scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
7740
+ log(chalk6.green(`Created scenario ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
7274
7741
  } catch (error) {
7275
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7742
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7276
7743
  process.exit(1);
7277
7744
  }
7278
7745
  });
@@ -7294,7 +7761,7 @@ program2.command("list").alias("ls").description("List test scenarios").option("
7294
7761
  log(formatScenarioList(scenarios));
7295
7762
  }
7296
7763
  } catch (error) {
7297
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7764
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7298
7765
  process.exit(1);
7299
7766
  }
7300
7767
  });
@@ -7302,7 +7769,7 @@ program2.command("show <id>").description("Show scenario details").option("--jso
7302
7769
  try {
7303
7770
  const scenario = getScenario(id) ?? getScenarioByShortId(id);
7304
7771
  if (!scenario) {
7305
- logError(chalk5.red(`Scenario not found: ${id}`));
7772
+ logError(chalk6.red(`Scenario not found: ${id}`));
7306
7773
  process.exit(1);
7307
7774
  }
7308
7775
  if (opts.json) {
@@ -7310,29 +7777,29 @@ program2.command("show <id>").description("Show scenario details").option("--jso
7310
7777
  return;
7311
7778
  }
7312
7779
  log("");
7313
- log(chalk5.bold(` Scenario ${scenario.shortId}`));
7780
+ log(chalk6.bold(` Scenario ${scenario.shortId}`));
7314
7781
  log(` Name: ${scenario.name}`);
7315
- log(` ID: ${chalk5.dim(scenario.id)}`);
7782
+ log(` ID: ${chalk6.dim(scenario.id)}`);
7316
7783
  log(` Description: ${scenario.description}`);
7317
7784
  log(` Priority: ${scenario.priority}`);
7318
- log(` Model: ${scenario.model ?? chalk5.dim("default")}`);
7319
- log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") : chalk5.dim("none")}`);
7320
- log(` Path: ${scenario.targetPath ?? chalk5.dim("none")}`);
7785
+ log(` Model: ${scenario.model ?? chalk6.dim("default")}`);
7786
+ log(` Tags: ${scenario.tags.length > 0 ? scenario.tags.join(", ") : chalk6.dim("none")}`);
7787
+ log(` Path: ${scenario.targetPath ?? chalk6.dim("none")}`);
7321
7788
  log(` Auth: ${scenario.requiresAuth ? "yes" : "no"}`);
7322
- log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` : chalk5.dim("default")}`);
7789
+ log(` Timeout: ${scenario.timeoutMs ? `${scenario.timeoutMs}ms` : chalk6.dim("default")}`);
7323
7790
  log(` Version: ${scenario.version}`);
7324
7791
  log(` Created: ${scenario.createdAt}`);
7325
7792
  log(` Updated: ${scenario.updatedAt}`);
7326
7793
  if (scenario.steps.length > 0) {
7327
7794
  log("");
7328
- log(chalk5.bold(" Steps:"));
7795
+ log(chalk6.bold(" Steps:"));
7329
7796
  for (let i = 0;i < scenario.steps.length; i++) {
7330
7797
  log(` ${i + 1}. ${scenario.steps[i]}`);
7331
7798
  }
7332
7799
  }
7333
7800
  log("");
7334
7801
  } catch (error) {
7335
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7802
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7336
7803
  process.exit(1);
7337
7804
  }
7338
7805
  });
@@ -7352,7 +7819,7 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
7352
7819
  try {
7353
7820
  const scenario = getScenario(id) ?? getScenarioByShortId(id);
7354
7821
  if (!scenario) {
7355
- logError(chalk5.red(`Scenario not found: ${id}`));
7822
+ logError(chalk6.red(`Scenario not found: ${id}`));
7356
7823
  process.exit(1);
7357
7824
  }
7358
7825
  let newTags;
@@ -7374,12 +7841,12 @@ program2.command("update <id>").description("Update a scenario").option("-n, --n
7374
7841
  priority: opts.priority,
7375
7842
  model: opts.model
7376
7843
  }, scenario.version);
7377
- log(chalk5.green(`Updated scenario ${chalk5.bold(updated.shortId)}: ${updated.name}`));
7844
+ log(chalk6.green(`Updated scenario ${chalk6.bold(updated.shortId)}: ${updated.name}`));
7378
7845
  if (newTags !== undefined) {
7379
- log(chalk5.dim(` Tags: [${updated.tags.join(", ")}]`));
7846
+ log(chalk6.dim(` Tags: [${updated.tags.join(", ")}]`));
7380
7847
  }
7381
7848
  } catch (error) {
7382
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7849
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7383
7850
  process.exit(1);
7384
7851
  }
7385
7852
  });
@@ -7387,11 +7854,50 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
7387
7854
  try {
7388
7855
  const scenario = getScenario(id) ?? getScenarioByShortId(id);
7389
7856
  if (!scenario) {
7390
- logError(chalk5.red(`Scenario not found: ${id}`));
7857
+ logError(chalk6.red(`Scenario not found: ${id}`));
7858
+ process.exit(1);
7859
+ }
7860
+ if (!opts.yes) {
7861
+ process.stdout.write(chalk6.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
7862
+ const answer = await new Promise((resolve2) => {
7863
+ let buf = "";
7864
+ process.stdin.setRawMode?.(true);
7865
+ process.stdin.resume();
7866
+ process.stdin.once("data", (chunk) => {
7867
+ buf = chunk.toString().trim().toLowerCase();
7868
+ process.stdin.setRawMode?.(false);
7869
+ process.stdin.pause();
7870
+ process.stdout.write(`
7871
+ `);
7872
+ resolve2(buf);
7873
+ });
7874
+ });
7875
+ if (answer !== "y" && answer !== "yes") {
7876
+ log(chalk6.dim("Cancelled."));
7877
+ return;
7878
+ }
7879
+ }
7880
+ const deleted = deleteScenario(scenario.id);
7881
+ if (deleted) {
7882
+ log(chalk6.green(`Deleted scenario ${scenario.shortId}: ${scenario.name}`));
7883
+ } else {
7884
+ logError(chalk6.red(`Failed to delete scenario: ${id}`));
7885
+ process.exit(1);
7886
+ }
7887
+ } catch (error) {
7888
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7889
+ process.exit(1);
7890
+ }
7891
+ });
7892
+ program2.command("remove <id>").alias("uninstall").description("Remove a scenario (alias for delete)").option("-y, --yes", "Skip confirmation prompt", false).action(async (id, opts) => {
7893
+ try {
7894
+ const scenario = getScenario(id) ?? getScenarioByShortId(id);
7895
+ if (!scenario) {
7896
+ logError(chalk6.red(`Scenario not found: ${id}`));
7391
7897
  process.exit(1);
7392
7898
  }
7393
7899
  if (!opts.yes) {
7394
- process.stdout.write(chalk5.yellow(`Delete scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
7900
+ process.stdout.write(chalk6.yellow(`Remove scenario ${scenario.shortId} "${scenario.name}"? [y/N] `));
7395
7901
  const answer = await new Promise((resolve2) => {
7396
7902
  let buf = "";
7397
7903
  process.stdin.setRawMode?.(true);
@@ -7406,33 +7912,33 @@ program2.command("delete <id>").description("Delete a scenario").option("-y, --y
7406
7912
  });
7407
7913
  });
7408
7914
  if (answer !== "y" && answer !== "yes") {
7409
- log(chalk5.dim("Cancelled."));
7915
+ log(chalk6.dim("Cancelled."));
7410
7916
  return;
7411
7917
  }
7412
7918
  }
7413
7919
  const deleted = deleteScenario(scenario.id);
7414
7920
  if (deleted) {
7415
- log(chalk5.green(`Deleted scenario ${scenario.shortId}: ${scenario.name}`));
7921
+ log(chalk6.green(`Removed scenario ${scenario.shortId}: ${scenario.name}`));
7416
7922
  } else {
7417
- logError(chalk5.red(`Failed to delete scenario: ${id}`));
7923
+ logError(chalk6.red(`Failed to remove scenario: ${id}`));
7418
7924
  process.exit(1);
7419
7925
  }
7420
7926
  } catch (error) {
7421
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7927
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7422
7928
  process.exit(1);
7423
7929
  }
7424
7930
  });
7425
7931
  program2.command("run [url] [description]").alias("test").description("Run test scenarios against a URL").option("-t, --tag <tag>", "Filter by tag (repeatable)", (val, acc) => {
7426
7932
  acc.push(val);
7427
7933
  return acc;
7428
- }, []).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("--browser <engine>", "Browser engine: playwright or lightpanda", "playwright").option("--env <name>", "Use a named environment for the URL").option("--dry-run", "Print what would run without launching browser", false).option("--retry <n>", "Retry failed scenarios up to n times", "0").option("--verbose", "Show per-step timing and full tool results", false).option("--watch-results", "When used with --background, poll and display live results table until run completes", false).option("--failed-only", "Only show failed/error scenarios in output (passed count shown as summary)", false).action(async (urlArg, description, opts) => {
7934
+ }, []).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("--browser <engine>", "Browser engine: playwright or lightpanda", "playwright").option("--env <name>", "Use a named environment for the URL").option("--dry-run", "Print what would run without launching browser", false).option("--retry <n>", "Retry failed scenarios up to n times", "0").option("--verbose", "Show per-step timing and full tool results", false).option("--watch-results", "When used with --background, poll and display live results table until run completes", false).option("--failed-only", "Only show failed/error scenarios in output (passed count shown as summary)", false).option("--smoke", "Run only smoke-tagged scenarios (fast validation suite, <2 min)", false).action(async (urlArg, description, opts) => {
7429
7935
  try {
7430
7936
  const projectId = resolveProject(opts.project);
7431
7937
  let url = urlArg;
7432
7938
  if (!url && opts.env) {
7433
7939
  const env = getEnvironment(opts.env);
7434
7940
  if (!env) {
7435
- logError(chalk5.red(`Environment not found: ${opts.env}`));
7941
+ logError(chalk6.red(`Environment not found: ${opts.env}`));
7436
7942
  process.exit(1);
7437
7943
  }
7438
7944
  url = env.url;
@@ -7441,29 +7947,33 @@ program2.command("run [url] [description]").alias("test").description("Run test
7441
7947
  const defaultEnv = getDefaultEnvironment();
7442
7948
  if (defaultEnv) {
7443
7949
  url = defaultEnv.url;
7444
- log(chalk5.dim(`Using default environment: ${defaultEnv.name} (${defaultEnv.url})`));
7950
+ log(chalk6.dim(`Using default environment: ${defaultEnv.name} (${defaultEnv.url})`));
7445
7951
  }
7446
7952
  }
7447
7953
  if (!url) {
7448
- logError(chalk5.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
7954
+ logError(chalk6.red("No URL provided. Pass a URL argument, use --env <name>, or set a default environment with 'testers env use <name>'."));
7449
7955
  process.exit(1);
7450
7956
  }
7451
7957
  if (!opts.dryRun && !opts.background) {
7452
7958
  const budgetResult = checkBudget(0);
7453
7959
  if (budgetResult.warning) {
7454
- log(chalk5.yellow(` \u26A0\uFE0F Budget warning: ${budgetResult.warning}`));
7960
+ log(chalk6.yellow(` \u26A0\uFE0F Budget warning: ${budgetResult.warning}`));
7455
7961
  if (!budgetResult.allowed) {
7456
7962
  if (!opts.yes) {
7457
- log(chalk5.yellow(" Use --yes to run anyway, or check your budget config."));
7963
+ log(chalk6.yellow(" Use --yes to run anyway, or check your budget config."));
7458
7964
  process.exit(1);
7459
7965
  }
7460
- log(chalk5.yellow(" --yes passed, proceeding despite budget limit."));
7966
+ log(chalk6.yellow(" --yes passed, proceeding despite budget limit."));
7461
7967
  }
7462
7968
  }
7463
7969
  }
7970
+ if (opts.smoke && !opts.tag.includes("smoke")) {
7971
+ opts.tag.push("smoke");
7972
+ log(chalk6.dim(" Running smoke suite (scenarios tagged 'smoke')..."));
7973
+ }
7464
7974
  if (opts.fromTodos) {
7465
7975
  const result = importFromTodos({ projectId });
7466
- log(chalk5.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
7976
+ log(chalk6.blue(`Imported ${result.imported} scenarios from todos (${result.skipped} skipped)`));
7467
7977
  }
7468
7978
  if (opts.dryRun) {
7469
7979
  const dryScenarios = listScenarios({
@@ -7477,10 +7987,10 @@ program2.command("run [url] [description]").alias("test").description("Run test
7477
7987
  return true;
7478
7988
  });
7479
7989
  log("");
7480
- log(chalk5.bold(" Dry Run \u2014 scenarios that would execute:"));
7990
+ log(chalk6.bold(" Dry Run \u2014 scenarios that would execute:"));
7481
7991
  log("");
7482
7992
  if (dryScenarios.length === 0) {
7483
- log(chalk5.yellow(" No matching scenarios found."));
7993
+ log(chalk6.yellow(" No matching scenarios found."));
7484
7994
  } else {
7485
7995
  for (const s of dryScenarios) {
7486
7996
  const assertionErrors = [];
@@ -7496,19 +8006,19 @@ program2.command("run [url] [description]").alias("test").description("Run test
7496
8006
  const presets = listAuthPresets();
7497
8007
  authOk = presets.some((p) => p.name === s.authPreset);
7498
8008
  }
7499
- const statusIcon = assertionErrors.length === 0 && authOk ? chalk5.green("\u2713") : chalk5.red("\u2717");
7500
- log(` ${statusIcon} ${chalk5.cyan(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
8009
+ const statusIcon = assertionErrors.length === 0 && authOk ? chalk6.green("\u2713") : chalk6.red("\u2717");
8010
+ log(` ${statusIcon} ${chalk6.cyan(s.shortId)} ${s.name} ${chalk6.dim(`[${s.tags.join(", ")}]`)}`);
7501
8011
  if (assertionErrors.length > 0) {
7502
- log(chalk5.red(` Invalid assertions: ${assertionErrors.join(", ")}`));
8012
+ log(chalk6.red(` Invalid assertions: ${assertionErrors.join(", ")}`));
7503
8013
  }
7504
8014
  if (!authOk) {
7505
- log(chalk5.red(` Auth preset not found: ${s.authPreset}`));
8015
+ log(chalk6.red(` Auth preset not found: ${s.authPreset}`));
7506
8016
  }
7507
8017
  }
7508
8018
  }
7509
8019
  log("");
7510
- log(chalk5.dim(` URL: ${url}`));
7511
- log(chalk5.dim(` Total: ${dryScenarios.length} scenarios`));
8020
+ log(chalk6.dim(` URL: ${url}`));
8021
+ log(chalk6.dim(` Total: ${dryScenarios.length} scenarios`));
7512
8022
  log("");
7513
8023
  process.exit(0);
7514
8024
  }
@@ -7528,11 +8038,11 @@ program2.command("run [url] [description]").alias("test").description("Run test
7528
8038
  projectId,
7529
8039
  engine: opts.browser
7530
8040
  });
7531
- log(chalk5.green(`Run started in background: ${chalk5.bold(runId.slice(0, 8))}`));
7532
- log(chalk5.dim(` Scenarios: ${scenarioCount}`));
7533
- log(chalk5.dim(` URL: ${url}`));
8041
+ log(chalk6.green(`Run started in background: ${chalk6.bold(runId.slice(0, 8))}`));
8042
+ log(chalk6.dim(` Scenarios: ${scenarioCount}`));
8043
+ log(chalk6.dim(` URL: ${url}`));
7534
8044
  if (opts.watchResults) {
7535
- log(chalk5.dim(` Watching results (polling every 3s)...`));
8045
+ log(chalk6.dim(` Watching results (polling every 3s)...`));
7536
8046
  log("");
7537
8047
  const POLL_INTERVAL = 3000;
7538
8048
  const DONE_STATUSES = new Set(["passed", "failed", "cancelled"]);
@@ -7541,14 +8051,14 @@ program2.command("run [url] [description]").alias("test").description("Run test
7541
8051
  if (!run2)
7542
8052
  return;
7543
8053
  const results2 = getResultsByRun(runId);
7544
- const statusIcon = run2.status === "passed" ? chalk5.green("PASS") : run2.status === "failed" ? chalk5.red("FAIL") : chalk5.blue("RUN ");
8054
+ const statusIcon = run2.status === "passed" ? chalk6.green("PASS") : run2.status === "failed" ? chalk6.red("FAIL") : chalk6.blue("RUN ");
7545
8055
  process.stdout.write(`\r ${statusIcon} ${run2.passed} passed ${run2.failed} failed ${run2.total - run2.passed - run2.failed} running (${results2.length}/${run2.total})
7546
8056
  `);
7547
8057
  for (const r of results2) {
7548
8058
  const scenario = getScenario(r.scenarioId);
7549
8059
  const name = scenario ? scenario.name : r.scenarioId.slice(0, 8);
7550
- const icon = r.status === "passed" ? chalk5.green("\u2713") : r.status === "failed" ? chalk5.red("\u2717") : r.status === "error" ? chalk5.yellow("!") : chalk5.blue("\u2026");
7551
- const dur = r.durationMs > 0 ? chalk5.dim(` ${(r.durationMs / 1000).toFixed(1)}s`) : "";
8060
+ const icon = r.status === "passed" ? chalk6.green("\u2713") : r.status === "failed" ? chalk6.red("\u2717") : r.status === "error" ? chalk6.yellow("!") : chalk6.blue("\u2026");
8061
+ const dur = r.durationMs > 0 ? chalk6.dim(` ${(r.durationMs / 1000).toFixed(1)}s`) : "";
7552
8062
  process.stdout.write(` ${icon} ${name}${dur}
7553
8063
  `);
7554
8064
  }
@@ -7573,7 +8083,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
7573
8083
  }
7574
8084
  process.exit(finalRun ? getExitCode(finalRun) : 0);
7575
8085
  }
7576
- log(chalk5.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
8086
+ log(chalk6.dim(` Check progress: testers results ${runId.slice(0, 8)}`));
7577
8087
  process.exit(0);
7578
8088
  }
7579
8089
  if (!opts.json && !opts.output) {
@@ -7582,51 +8092,51 @@ program2.command("run [url] [description]").alias("test").description("Run test
7582
8092
  switch (event.type) {
7583
8093
  case "scenario:start":
7584
8094
  if (event.retryAttempt) {
7585
- log(chalk5.yellow(` [retry] Retrying scenario ${event.scenarioName ?? event.scenarioId} (attempt ${event.retryAttempt}/${event.maxRetries})...`));
8095
+ log(chalk6.yellow(` [retry] Retrying scenario ${event.scenarioName ?? event.scenarioId} (attempt ${event.retryAttempt}/${event.maxRetries})...`));
7586
8096
  } else {
7587
- log(chalk5.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
8097
+ log(chalk6.blue(` [start] ${event.scenarioName ?? event.scenarioId}`));
7588
8098
  }
7589
8099
  break;
7590
8100
  case "scenario:timeout_warning": {
7591
8101
  const elapsedS = ((event.elapsedMs ?? 0) / 1000).toFixed(0);
7592
8102
  const totalS = ((event.timeoutMs ?? 0) / 1000).toFixed(0);
7593
- log(chalk5.yellow(` \u26A0\uFE0F Scenario '${event.scenarioName}' at 80% timeout (${elapsedS}s/${totalS}s) \u2014 still running`));
8103
+ log(chalk6.yellow(` \u26A0\uFE0F Scenario '${event.scenarioName}' at 80% timeout (${elapsedS}s/${totalS}s) \u2014 still running`));
7594
8104
  break;
7595
8105
  }
7596
8106
  case "step:thinking":
7597
8107
  if (event.thinking) {
7598
8108
  const preview = event.thinking.length > 120 ? event.thinking.slice(0, 120) + "..." : event.thinking;
7599
- log(chalk5.dim(` [think] ${preview}`));
8109
+ log(chalk6.dim(` [think] ${preview}`));
7600
8110
  }
7601
8111
  break;
7602
8112
  case "step:tool_call":
7603
- log(chalk5.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
8113
+ log(chalk6.cyan(` [step ${event.stepNumber}] ${event.toolName}${event.toolInput ? ` ${formatToolInput(event.toolInput)}` : ""}`));
7604
8114
  break;
7605
8115
  case "step:tool_result":
7606
8116
  if (event.toolName === "report_result") {
7607
- log(chalk5.bold(` [result] ${event.toolResult}`));
8117
+ log(chalk6.bold(` [result] ${event.toolResult}`));
7608
8118
  } else {
7609
- const durationStr = verbose && event.stepDurationMs !== undefined ? chalk5.dim(`[${(event.stepDurationMs / 1000).toFixed(1)}s] `) : "";
8119
+ const durationStr = verbose && event.stepDurationMs !== undefined ? chalk6.dim(`[${(event.stepDurationMs / 1000).toFixed(1)}s] `) : "";
7610
8120
  const resultPreview = (event.toolResult ?? "").length > 100 ? (event.toolResult ?? "").slice(0, 100) + "..." : event.toolResult ?? "";
7611
- log(chalk5.dim(` [done] ${durationStr}${resultPreview}`));
8121
+ log(chalk6.dim(` [done] ${durationStr}${resultPreview}`));
7612
8122
  }
7613
8123
  break;
7614
8124
  case "screenshot:captured":
7615
- log(chalk5.dim(` [screenshot] ${event.screenshotPath}`));
8125
+ log(chalk6.dim(` [screenshot] ${event.screenshotPath}`));
7616
8126
  break;
7617
8127
  case "scenario:pass":
7618
- log(chalk5.green(` [PASS] ${event.scenarioName}`));
8128
+ log(chalk6.green(` [PASS] ${event.scenarioName}`));
7619
8129
  break;
7620
8130
  case "scenario:fail":
7621
- log(chalk5.red(` [FAIL] ${event.scenarioName}`));
8131
+ log(chalk6.red(` [FAIL] ${event.scenarioName}`));
7622
8132
  break;
7623
8133
  case "scenario:error":
7624
- log(chalk5.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
8134
+ log(chalk6.yellow(` [ERR] ${event.scenarioName}: ${event.error}`));
7625
8135
  break;
7626
8136
  }
7627
8137
  });
7628
8138
  log("");
7629
- log(chalk5.bold(` Running tests against ${url}`));
8139
+ log(chalk6.bold(` Running tests against ${url}`));
7630
8140
  log("");
7631
8141
  }
7632
8142
  if (description) {
@@ -7651,7 +8161,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
7651
8161
  const jsonOutput = formatJSON(run2, results2);
7652
8162
  if (opts.output) {
7653
8163
  writeFileSync3(opts.output, jsonOutput, "utf-8");
7654
- log(chalk5.green(`Results written to ${opts.output}`));
8164
+ log(chalk6.green(`Results written to ${opts.output}`));
7655
8165
  }
7656
8166
  if (opts.json) {
7657
8167
  log(jsonOutput);
@@ -7664,7 +8174,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
7664
8174
  const noFilters = !opts.scenario && opts.tag.length === 0 && !opts.priority;
7665
8175
  if (noFilters && !opts.json && !opts.output) {
7666
8176
  const allScenarios = listScenarios({ projectId });
7667
- log(chalk5.bold(` Running all ${allScenarios.length} scenarios...`));
8177
+ log(chalk6.bold(` Running all ${allScenarios.length} scenarios...`));
7668
8178
  log("");
7669
8179
  }
7670
8180
  const { run, results } = await runByFilter({
@@ -7684,7 +8194,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
7684
8194
  const jsonOutput = formatJSON(run, results);
7685
8195
  if (opts.output) {
7686
8196
  writeFileSync3(opts.output, jsonOutput, "utf-8");
7687
- log(chalk5.green(`Results written to ${opts.output}`));
8197
+ log(chalk6.green(`Results written to ${opts.output}`));
7688
8198
  }
7689
8199
  if (opts.json) {
7690
8200
  log(jsonOutput);
@@ -7694,7 +8204,7 @@ program2.command("run [url] [description]").alias("test").description("Run test
7694
8204
  }
7695
8205
  process.exit(getExitCode(run));
7696
8206
  } catch (error) {
7697
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8207
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7698
8208
  process.exit(1);
7699
8209
  }
7700
8210
  });
@@ -7713,7 +8223,7 @@ program2.command("runs").description("List past test runs").option("--status <st
7713
8223
  log(formatRunList(runs));
7714
8224
  }
7715
8225
  } catch (error) {
7716
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8226
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7717
8227
  process.exit(1);
7718
8228
  }
7719
8229
  });
@@ -7721,7 +8231,7 @@ program2.command("results <run-id>").description("Show results for a test run").
7721
8231
  try {
7722
8232
  const run = getRun(runId);
7723
8233
  if (!run) {
7724
- logError(chalk5.red(`Run not found: ${runId}`));
8234
+ logError(chalk6.red(`Run not found: ${runId}`));
7725
8235
  process.exit(1);
7726
8236
  }
7727
8237
  const results = getResultsByRun(run.id);
@@ -7731,7 +8241,7 @@ program2.command("results <run-id>").description("Show results for a test run").
7731
8241
  log(formatTerminal(run, results));
7732
8242
  }
7733
8243
  } catch (error) {
7734
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8244
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7735
8245
  process.exit(1);
7736
8246
  }
7737
8247
  });
@@ -7742,23 +8252,23 @@ program2.command("screenshots <id>").description("List screenshots for a run or
7742
8252
  const results = getResultsByRun(run.id);
7743
8253
  let total = 0;
7744
8254
  log("");
7745
- log(chalk5.bold(` Screenshots for run ${run.id.slice(0, 8)}`));
8255
+ log(chalk6.bold(` Screenshots for run ${run.id.slice(0, 8)}`));
7746
8256
  log("");
7747
8257
  for (const result of results) {
7748
8258
  const screenshots2 = listScreenshots(result.id);
7749
8259
  if (screenshots2.length > 0) {
7750
8260
  const scenario = getScenario(result.scenarioId);
7751
8261
  const label = scenario ? `${scenario.shortId}: ${scenario.name}` : result.scenarioId.slice(0, 8);
7752
- log(chalk5.bold(` ${label}`));
8262
+ log(chalk6.bold(` ${label}`));
7753
8263
  for (const ss of screenshots2) {
7754
- log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
8264
+ log(` ${chalk6.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk6.dim(ss.filePath)}`);
7755
8265
  total++;
7756
8266
  }
7757
8267
  log("");
7758
8268
  }
7759
8269
  }
7760
8270
  if (total === 0) {
7761
- log(chalk5.dim(" No screenshots found."));
8271
+ log(chalk6.dim(" No screenshots found."));
7762
8272
  log("");
7763
8273
  }
7764
8274
  return;
@@ -7766,18 +8276,18 @@ program2.command("screenshots <id>").description("List screenshots for a run or
7766
8276
  const screenshots = listScreenshots(id);
7767
8277
  if (screenshots.length > 0) {
7768
8278
  log("");
7769
- log(chalk5.bold(` Screenshots for result ${id.slice(0, 8)}`));
8279
+ log(chalk6.bold(` Screenshots for result ${id.slice(0, 8)}`));
7770
8280
  log("");
7771
8281
  for (const ss of screenshots) {
7772
- log(` ${chalk5.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk5.dim(ss.filePath)}`);
8282
+ log(` ${chalk6.dim(String(ss.stepNumber).padStart(3, "0"))} ${ss.action} \u2014 ${chalk6.dim(ss.filePath)}`);
7773
8283
  }
7774
8284
  log("");
7775
8285
  return;
7776
8286
  }
7777
- logError(chalk5.red(`No screenshots found for: ${id}`));
8287
+ logError(chalk6.red(`No screenshots found for: ${id}`));
7778
8288
  process.exit(1);
7779
8289
  } catch (error) {
7780
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8290
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7781
8291
  process.exit(1);
7782
8292
  }
7783
8293
  });
@@ -7786,7 +8296,7 @@ program2.command("import <dir>").description("Import markdown test files as scen
7786
8296
  const absDir = resolve(dir);
7787
8297
  const files = readdirSync(absDir).filter((f) => f.endsWith(".md"));
7788
8298
  if (files.length === 0) {
7789
- log(chalk5.dim("No .md files found in directory."));
8299
+ log(chalk6.dim("No .md files found in directory."));
7790
8300
  return;
7791
8301
  }
7792
8302
  let imported = 0;
@@ -7816,13 +8326,13 @@ program2.command("import <dir>").description("Import markdown test files as scen
7816
8326
  description: descriptionLines.join(" ") || name,
7817
8327
  steps
7818
8328
  });
7819
- log(chalk5.green(` Imported ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
8329
+ log(chalk6.green(` Imported ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
7820
8330
  imported++;
7821
8331
  }
7822
8332
  log("");
7823
- log(chalk5.green(`Imported ${imported} scenario(s) from ${absDir}`));
8333
+ log(chalk6.green(`Imported ${imported} scenario(s) from ${absDir}`));
7824
8334
  } catch (error) {
7825
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8335
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7826
8336
  process.exit(1);
7827
8337
  }
7828
8338
  });
@@ -7830,7 +8340,7 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
7830
8340
  try {
7831
8341
  const fmt = (format ?? "json").toLowerCase();
7832
8342
  if (fmt !== "json" && fmt !== "markdown") {
7833
- logError(chalk5.red(`Unknown format: ${fmt}. Supported: json, markdown`));
8343
+ logError(chalk6.red(`Unknown format: ${fmt}. Supported: json, markdown`));
7834
8344
  process.exit(1);
7835
8345
  }
7836
8346
  const projectId = resolveProject(opts.project);
@@ -7840,14 +8350,14 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
7840
8350
  projectId
7841
8351
  });
7842
8352
  if (scenarios.length === 0) {
7843
- log(chalk5.dim("No scenarios found to export."));
8353
+ log(chalk6.dim("No scenarios found to export."));
7844
8354
  return;
7845
8355
  }
7846
8356
  if (fmt === "json") {
7847
8357
  const outputPath = opts.output ?? "testers-export.json";
7848
8358
  const data = JSON.stringify(scenarios, null, 2);
7849
8359
  writeFileSync3(outputPath, data, "utf-8");
7850
- log(chalk5.green(`Exported ${scenarios.length} scenario(s) to ${resolve(outputPath)}`));
8360
+ log(chalk6.green(`Exported ${scenarios.length} scenario(s) to ${resolve(outputPath)}`));
7851
8361
  return;
7852
8362
  }
7853
8363
  const outputDir = opts.output ?? ".";
@@ -7882,12 +8392,12 @@ program2.command("export [format]").description("Export scenarios as JSON (defau
7882
8392
  const filePath = join6(outputDir, `${s.shortId}-${safeFilename}.md`);
7883
8393
  writeFileSync3(filePath, lines.join(`
7884
8394
  `), "utf-8");
7885
- log(chalk5.dim(` ${s.shortId}: ${s.name} \u2192 ${filePath}`));
8395
+ log(chalk6.dim(` ${s.shortId}: ${s.name} \u2192 ${filePath}`));
7886
8396
  }
7887
- log(chalk5.green(`
8397
+ log(chalk6.green(`
7888
8398
  Exported ${scenarios.length} scenario(s) as markdown to ${resolve(outputDir)}`));
7889
8399
  } catch (error) {
7890
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8400
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7891
8401
  process.exit(1);
7892
8402
  }
7893
8403
  });
@@ -7896,7 +8406,7 @@ program2.command("config").description("Show current configuration").action(() =
7896
8406
  const config = loadConfig();
7897
8407
  log(JSON.stringify(config, null, 2));
7898
8408
  } catch (error) {
7899
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8409
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7900
8410
  process.exit(1);
7901
8411
  }
7902
8412
  });
@@ -7906,32 +8416,32 @@ program2.command("status").description("Show database and auth status").action((
7906
8416
  const hasApiKey = !!config.anthropicApiKey || !!process.env["ANTHROPIC_API_KEY"];
7907
8417
  const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
7908
8418
  log("");
7909
- log(chalk5.bold(" Open Testers Status"));
8419
+ log(chalk6.bold(" Open Testers Status"));
7910
8420
  log("");
7911
- log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk5.green("set") : chalk5.red("not set")}`);
8421
+ log(` ANTHROPIC_API_KEY: ${hasApiKey ? chalk6.green("set") : chalk6.red("not set")}`);
7912
8422
  log(` Database: ${dbPath}`);
7913
8423
  log(` Default model: ${config.defaultModel}`);
7914
8424
  log(` Screenshots dir: ${config.screenshots.dir}`);
7915
8425
  log("");
7916
8426
  } catch (error) {
7917
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8427
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7918
8428
  process.exit(1);
7919
8429
  }
7920
8430
  });
7921
8431
  program2.command("install-browser").description("Install browser engine").option("--engine <engine>", "Engine to install: playwright, lightpanda, or all", "playwright").action(async (opts) => {
7922
8432
  try {
7923
8433
  if (opts.engine === "all" || opts.engine === "playwright") {
7924
- log(chalk5.blue("Installing Playwright Chromium..."));
8434
+ log(chalk6.blue("Installing Playwright Chromium..."));
7925
8435
  await installBrowser("playwright");
7926
- log(chalk5.green("Playwright Chromium installed."));
8436
+ log(chalk6.green("Playwright Chromium installed."));
7927
8437
  }
7928
8438
  if (opts.engine === "all" || opts.engine === "lightpanda") {
7929
- log(chalk5.blue("Installing Lightpanda..."));
8439
+ log(chalk6.blue("Installing Lightpanda..."));
7930
8440
  await installBrowser("lightpanda");
7931
- log(chalk5.green("Lightpanda installed."));
8441
+ log(chalk6.green("Lightpanda installed."));
7932
8442
  }
7933
8443
  } catch (error) {
7934
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8444
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7935
8445
  process.exit(1);
7936
8446
  }
7937
8447
  });
@@ -7943,9 +8453,9 @@ projectCmd.command("create <name>").description("Create a new project").option("
7943
8453
  path: opts.path,
7944
8454
  description: opts.description
7945
8455
  });
7946
- log(chalk5.green(`Created project ${chalk5.bold(project.name)} (${project.id})`));
8456
+ log(chalk6.green(`Created project ${chalk6.bold(project.name)} (${project.id})`));
7947
8457
  } catch (error) {
7948
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8458
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7949
8459
  process.exit(1);
7950
8460
  }
7951
8461
  });
@@ -7953,20 +8463,20 @@ projectCmd.command("list").description("List all projects").action(() => {
7953
8463
  try {
7954
8464
  const projects = listProjects();
7955
8465
  if (projects.length === 0) {
7956
- log(chalk5.dim("No projects found."));
8466
+ log(chalk6.dim("No projects found."));
7957
8467
  return;
7958
8468
  }
7959
8469
  log("");
7960
- log(chalk5.bold(" Projects"));
8470
+ log(chalk6.bold(" Projects"));
7961
8471
  log("");
7962
8472
  log(` ${"ID".padEnd(38)} ${"Name".padEnd(24)} ${"Path".padEnd(30)} Created`);
7963
8473
  log(` ${"\u2500".repeat(38)} ${"\u2500".repeat(24)} ${"\u2500".repeat(30)} ${"\u2500".repeat(20)}`);
7964
8474
  for (const p of projects) {
7965
- log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk5.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
8475
+ log(` ${p.id.padEnd(38)} ${p.name.padEnd(24)} ${(p.path ?? chalk6.dim("\u2014")).toString().padEnd(30)} ${p.createdAt}`);
7966
8476
  }
7967
8477
  log("");
7968
8478
  } catch (error) {
7969
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8479
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7970
8480
  process.exit(1);
7971
8481
  }
7972
8482
  });
@@ -7974,19 +8484,19 @@ projectCmd.command("show <id>").description("Show project details").action((id)
7974
8484
  try {
7975
8485
  const project = getProject(id);
7976
8486
  if (!project) {
7977
- logError(chalk5.red(`Project not found: ${id}`));
8487
+ logError(chalk6.red(`Project not found: ${id}`));
7978
8488
  process.exit(1);
7979
8489
  }
7980
8490
  log("");
7981
- log(chalk5.bold(` Project: ${project.name}`));
8491
+ log(chalk6.bold(` Project: ${project.name}`));
7982
8492
  log(` ID: ${project.id}`);
7983
- log(` Path: ${project.path ?? chalk5.dim("none")}`);
7984
- log(` Description: ${project.description ?? chalk5.dim("none")}`);
8493
+ log(` Path: ${project.path ?? chalk6.dim("none")}`);
8494
+ log(` Description: ${project.description ?? chalk6.dim("none")}`);
7985
8495
  log(` Created: ${project.createdAt}`);
7986
8496
  log(` Updated: ${project.updatedAt}`);
7987
8497
  log("");
7988
8498
  } catch (error) {
7989
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8499
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
7990
8500
  process.exit(1);
7991
8501
  }
7992
8502
  });
@@ -8004,9 +8514,9 @@ projectCmd.command("use <name>").description("Set active project (find or create
8004
8514
  }
8005
8515
  config.activeProject = project.id;
8006
8516
  writeFileSync3(CONFIG_PATH2, JSON.stringify(config, null, 2), "utf-8");
8007
- log(chalk5.green(`Active project set to ${chalk5.bold(project.name)} (${project.id})`));
8517
+ log(chalk6.green(`Active project set to ${chalk6.bold(project.name)} (${project.id})`));
8008
8518
  } catch (error) {
8009
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8519
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8010
8520
  process.exit(1);
8011
8521
  }
8012
8522
  });
@@ -8031,12 +8541,12 @@ scheduleCmd.command("create <name>").description("Create a new schedule").requir
8031
8541
  timeoutMs: opts.timeout ? parseInt(opts.timeout, 10) : undefined,
8032
8542
  projectId
8033
8543
  });
8034
- log(chalk5.green(`Created schedule ${chalk5.bold(schedule.name)} (${schedule.id})`));
8544
+ log(chalk6.green(`Created schedule ${chalk6.bold(schedule.name)} (${schedule.id})`));
8035
8545
  if (schedule.nextRunAt) {
8036
- log(chalk5.dim(` Next run at: ${schedule.nextRunAt}`));
8546
+ log(chalk6.dim(` Next run at: ${schedule.nextRunAt}`));
8037
8547
  }
8038
8548
  } catch (error) {
8039
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8549
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8040
8550
  process.exit(1);
8041
8551
  }
8042
8552
  });
@@ -8052,23 +8562,23 @@ scheduleCmd.command("list").description("List schedules").option("--project <id>
8052
8562
  return;
8053
8563
  }
8054
8564
  if (schedules.length === 0) {
8055
- log(chalk5.dim("No schedules found."));
8565
+ log(chalk6.dim("No schedules found."));
8056
8566
  return;
8057
8567
  }
8058
8568
  log("");
8059
- log(chalk5.bold(" Schedules"));
8569
+ log(chalk6.bold(" Schedules"));
8060
8570
  log("");
8061
8571
  log(` ${"Name".padEnd(20)} ${"Cron".padEnd(18)} ${"URL".padEnd(30)} ${"Enabled".padEnd(9)} ${"Next Run".padEnd(22)} Last Run`);
8062
8572
  log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(18)} ${"\u2500".repeat(30)} ${"\u2500".repeat(9)} ${"\u2500".repeat(22)} ${"\u2500".repeat(22)}`);
8063
8573
  for (const s of schedules) {
8064
- const enabled = s.enabled ? chalk5.green("yes") : chalk5.red("no");
8065
- const nextRun = s.nextRunAt ?? chalk5.dim("\u2014");
8066
- const lastRun = s.lastRunAt ?? chalk5.dim("\u2014");
8574
+ const enabled = s.enabled ? chalk6.green("yes") : chalk6.red("no");
8575
+ const nextRun = s.nextRunAt ?? chalk6.dim("\u2014");
8576
+ const lastRun = s.lastRunAt ?? chalk6.dim("\u2014");
8067
8577
  log(` ${s.name.padEnd(20)} ${s.cronExpression.padEnd(18)} ${s.url.padEnd(30)} ${enabled.toString().padEnd(9)} ${nextRun.toString().padEnd(22)} ${lastRun}`);
8068
8578
  }
8069
8579
  log("");
8070
8580
  } catch (error) {
8071
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8581
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8072
8582
  process.exit(1);
8073
8583
  }
8074
8584
  });
@@ -8076,47 +8586,47 @@ scheduleCmd.command("show <id>").description("Show schedule details").action((id
8076
8586
  try {
8077
8587
  const schedule = getSchedule(id);
8078
8588
  if (!schedule) {
8079
- logError(chalk5.red(`Schedule not found: ${id}`));
8589
+ logError(chalk6.red(`Schedule not found: ${id}`));
8080
8590
  process.exit(1);
8081
8591
  }
8082
8592
  log("");
8083
- log(chalk5.bold(` Schedule: ${schedule.name}`));
8593
+ log(chalk6.bold(` Schedule: ${schedule.name}`));
8084
8594
  log(` ID: ${schedule.id}`);
8085
8595
  log(` Cron: ${schedule.cronExpression}`);
8086
8596
  log(` URL: ${schedule.url}`);
8087
- log(` Enabled: ${schedule.enabled ? chalk5.green("yes") : chalk5.red("no")}`);
8088
- log(` Model: ${schedule.model ?? chalk5.dim("default")}`);
8597
+ log(` Enabled: ${schedule.enabled ? chalk6.green("yes") : chalk6.red("no")}`);
8598
+ log(` Model: ${schedule.model ?? chalk6.dim("default")}`);
8089
8599
  log(` Headed: ${schedule.headed ? "yes" : "no"}`);
8090
8600
  log(` Parallel: ${schedule.parallel}`);
8091
- log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk5.dim("default")}`);
8092
- log(` Project: ${schedule.projectId ?? chalk5.dim("none")}`);
8601
+ log(` Timeout: ${schedule.timeoutMs ? `${schedule.timeoutMs}ms` : chalk6.dim("default")}`);
8602
+ log(` Project: ${schedule.projectId ?? chalk6.dim("none")}`);
8093
8603
  log(` Filter: ${JSON.stringify(schedule.scenarioFilter)}`);
8094
- log(` Next run: ${schedule.nextRunAt ?? chalk5.dim("not scheduled")}`);
8095
- log(` Last run: ${schedule.lastRunAt ?? chalk5.dim("never")}`);
8096
- log(` Last run ID: ${schedule.lastRunId ?? chalk5.dim("none")}`);
8604
+ log(` Next run: ${schedule.nextRunAt ?? chalk6.dim("not scheduled")}`);
8605
+ log(` Last run: ${schedule.lastRunAt ?? chalk6.dim("never")}`);
8606
+ log(` Last run ID: ${schedule.lastRunId ?? chalk6.dim("none")}`);
8097
8607
  log(` Created: ${schedule.createdAt}`);
8098
8608
  log(` Updated: ${schedule.updatedAt}`);
8099
8609
  log("");
8100
8610
  } catch (error) {
8101
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8611
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8102
8612
  process.exit(1);
8103
8613
  }
8104
8614
  });
8105
8615
  scheduleCmd.command("enable <id>").description("Enable a schedule").action((id) => {
8106
8616
  try {
8107
8617
  const schedule = updateSchedule(id, { enabled: true });
8108
- log(chalk5.green(`Enabled schedule ${chalk5.bold(schedule.name)}`));
8618
+ log(chalk6.green(`Enabled schedule ${chalk6.bold(schedule.name)}`));
8109
8619
  } catch (error) {
8110
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8620
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8111
8621
  process.exit(1);
8112
8622
  }
8113
8623
  });
8114
8624
  scheduleCmd.command("disable <id>").description("Disable a schedule").action((id) => {
8115
8625
  try {
8116
8626
  const schedule = updateSchedule(id, { enabled: false });
8117
- log(chalk5.green(`Disabled schedule ${chalk5.bold(schedule.name)}`));
8627
+ log(chalk6.green(`Disabled schedule ${chalk6.bold(schedule.name)}`));
8118
8628
  } catch (error) {
8119
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8629
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8120
8630
  process.exit(1);
8121
8631
  }
8122
8632
  });
@@ -8124,13 +8634,13 @@ scheduleCmd.command("delete <id>").description("Delete a schedule").action((id)
8124
8634
  try {
8125
8635
  const deleted = deleteSchedule(id);
8126
8636
  if (deleted) {
8127
- log(chalk5.green(`Deleted schedule: ${id}`));
8637
+ log(chalk6.green(`Deleted schedule: ${id}`));
8128
8638
  } else {
8129
- logError(chalk5.red(`Schedule not found: ${id}`));
8639
+ logError(chalk6.red(`Schedule not found: ${id}`));
8130
8640
  process.exit(1);
8131
8641
  }
8132
8642
  } catch (error) {
8133
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8643
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8134
8644
  process.exit(1);
8135
8645
  }
8136
8646
  });
@@ -8138,11 +8648,11 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
8138
8648
  try {
8139
8649
  const schedule = getSchedule(id);
8140
8650
  if (!schedule) {
8141
- logError(chalk5.red(`Schedule not found: ${id}`));
8651
+ logError(chalk6.red(`Schedule not found: ${id}`));
8142
8652
  process.exit(1);
8143
8653
  return;
8144
8654
  }
8145
- log(chalk5.blue(`Running schedule ${chalk5.bold(schedule.name)} against ${schedule.url}...`));
8655
+ log(chalk6.blue(`Running schedule ${chalk6.bold(schedule.name)} against ${schedule.url}...`));
8146
8656
  const { run, results } = await runByFilter({
8147
8657
  url: schedule.url,
8148
8658
  tags: schedule.scenarioFilter.tags,
@@ -8161,15 +8671,15 @@ scheduleCmd.command("run <id>").description("Manually trigger a schedule").optio
8161
8671
  }
8162
8672
  process.exit(getExitCode(run));
8163
8673
  } catch (error) {
8164
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8674
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8165
8675
  process.exit(1);
8166
8676
  }
8167
8677
  });
8168
8678
  program2.command("daemon").description("Start the scheduler daemon").option("--interval <seconds>", "Check interval in seconds", "60").action(async (opts) => {
8169
8679
  try {
8170
8680
  const intervalMs = parseInt(opts.interval, 10) * 1000;
8171
- log(chalk5.blue("Scheduler daemon started. Press Ctrl+C to stop."));
8172
- log(chalk5.dim(` Check interval: ${opts.interval}s`));
8681
+ log(chalk6.blue("Scheduler daemon started. Press Ctrl+C to stop."));
8682
+ log(chalk6.dim(` Check interval: ${opts.interval}s`));
8173
8683
  let running = true;
8174
8684
  const checkAndRun = async () => {
8175
8685
  while (running) {
@@ -8178,7 +8688,7 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
8178
8688
  const now2 = new Date().toISOString();
8179
8689
  for (const schedule of schedules) {
8180
8690
  if (schedule.nextRunAt && schedule.nextRunAt <= now2) {
8181
- log(chalk5.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
8691
+ log(chalk6.blue(`[${new Date().toISOString()}] Triggering schedule: ${schedule.name}`));
8182
8692
  try {
8183
8693
  const { run } = await runByFilter({
8184
8694
  url: schedule.url,
@@ -8191,35 +8701,35 @@ program2.command("daemon").description("Start the scheduler daemon").option("--i
8191
8701
  timeout: schedule.timeoutMs ?? undefined,
8192
8702
  projectId: schedule.projectId ?? undefined
8193
8703
  });
8194
- const statusColor = run.status === "passed" ? chalk5.green : chalk5.red;
8704
+ const statusColor = run.status === "passed" ? chalk6.green : chalk6.red;
8195
8705
  log(` ${statusColor(run.status)} \u2014 ${run.passed}/${run.total} passed`);
8196
8706
  updateSchedule(schedule.id, {});
8197
8707
  } catch (err) {
8198
- logError(chalk5.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
8708
+ logError(chalk6.red(` Error running schedule ${schedule.name}: ${err instanceof Error ? err.message : String(err)}`));
8199
8709
  }
8200
8710
  }
8201
8711
  }
8202
8712
  } catch (err) {
8203
- logError(chalk5.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
8713
+ logError(chalk6.red(`Daemon error: ${err instanceof Error ? err.message : String(err)}`));
8204
8714
  }
8205
8715
  await new Promise((resolve2) => setTimeout(resolve2, intervalMs));
8206
8716
  }
8207
8717
  };
8208
8718
  process.on("SIGINT", () => {
8209
- log(chalk5.yellow(`
8719
+ log(chalk6.yellow(`
8210
8720
  Shutting down scheduler daemon...`));
8211
8721
  running = false;
8212
8722
  process.exit(0);
8213
8723
  });
8214
8724
  process.on("SIGTERM", () => {
8215
- log(chalk5.yellow(`
8725
+ log(chalk6.yellow(`
8216
8726
  Shutting down scheduler daemon...`));
8217
8727
  running = false;
8218
8728
  process.exit(0);
8219
8729
  });
8220
8730
  await checkAndRun();
8221
8731
  } catch (error) {
8222
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8732
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8223
8733
  process.exit(1);
8224
8734
  }
8225
8735
  });
@@ -8231,21 +8741,21 @@ program2.command("init").description("Initialize a new testing project").option(
8231
8741
  path: opts.path
8232
8742
  });
8233
8743
  log("");
8234
- log(chalk5.bold(" Project initialized!"));
8744
+ log(chalk6.bold(" Project initialized!"));
8235
8745
  log("");
8236
8746
  if (framework) {
8237
- log(` Framework: ${chalk5.cyan(framework.name)}`);
8747
+ log(` Framework: ${chalk6.cyan(framework.name)}`);
8238
8748
  if (framework.features.length > 0) {
8239
- log(` Features: ${chalk5.dim(framework.features.join(", "))}`);
8749
+ log(` Features: ${chalk6.dim(framework.features.join(", "))}`);
8240
8750
  }
8241
8751
  } else {
8242
- log(` Framework: ${chalk5.dim("not detected")}`);
8752
+ log(` Framework: ${chalk6.dim("not detected")}`);
8243
8753
  }
8244
- log(` Project: ${chalk5.green(project.name)} ${chalk5.dim(`(${project.id})`)}`);
8245
- log(` Scenarios: ${chalk5.green(String(scenarios.length))} starter scenarios created`);
8754
+ log(` Project: ${chalk6.green(project.name)} ${chalk6.dim(`(${project.id})`)}`);
8755
+ log(` Scenarios: ${chalk6.green(String(scenarios.length))} starter scenarios created`);
8246
8756
  log("");
8247
8757
  for (const s of scenarios) {
8248
- log(` ${chalk5.dim(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
8758
+ log(` ${chalk6.dim(s.shortId)} ${s.name} ${chalk6.dim(`[${s.tags.join(", ")}]`)}`);
8249
8759
  }
8250
8760
  if (opts.ci === "github") {
8251
8761
  const workflowDir = join6(process.cwd(), ".github", "workflows");
@@ -8254,9 +8764,9 @@ program2.command("init").description("Initialize a new testing project").option(
8254
8764
  }
8255
8765
  const workflowPath = join6(workflowDir, "testers.yml");
8256
8766
  writeFileSync3(workflowPath, generateGitHubActionsWorkflow(), "utf-8");
8257
- log(` CI: ${chalk5.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
8767
+ log(` CI: ${chalk6.green("GitHub Actions workflow written to .github/workflows/testers.yml")}`);
8258
8768
  } else if (opts.ci) {
8259
- log(chalk5.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
8769
+ log(chalk6.yellow(` Unknown CI provider: ${opts.ci}. Supported: github`));
8260
8770
  }
8261
8771
  log("");
8262
8772
  const rl = createInterface({ input: process.stdin, output: process.stdout });
@@ -8269,7 +8779,7 @@ program2.command("init").description("Initialize a new testing project").option(
8269
8779
  const resolvedEnvName = envName.trim() || "staging";
8270
8780
  const resolvedEnvUrl = envUrl.trim() || url;
8271
8781
  createEnvironment({ name: resolvedEnvName, url: resolvedEnvUrl, projectId: project.id, isDefault: true });
8272
- log(chalk5.green(` \u2713 Environment '${resolvedEnvName}' created (${resolvedEnvUrl})`));
8782
+ log(chalk6.green(` \u2713 Environment '${resolvedEnvName}' created (${resolvedEnvUrl})`));
8273
8783
  log("");
8274
8784
  }
8275
8785
  const scenarioAnswer = await ask(" Would you like to create your first test scenario? [y/N] ");
@@ -8286,19 +8796,19 @@ program2.command("init").description("Initialize a new testing project").option(
8286
8796
  tags: ["smoke"],
8287
8797
  priority: "high"
8288
8798
  });
8289
- log(chalk5.green(` \u2713 Scenario '${newScenario.name}' created ${chalk5.dim(`(${newScenario.shortId})`)}`));
8799
+ log(chalk6.green(` \u2713 Scenario '${newScenario.name}' created ${chalk6.dim(`(${newScenario.shortId})`)}`));
8290
8800
  log("");
8291
8801
  }
8292
8802
  } finally {
8293
8803
  rl.close();
8294
8804
  }
8295
- log(chalk5.bold(" Next steps:"));
8805
+ log(chalk6.bold(" Next steps:"));
8296
8806
  log(` 1. Start your dev server`);
8297
- log(` 2. Run ${chalk5.cyan("testers run <url>")} to execute tests`);
8298
- log(` 3. Add more scenarios with ${chalk5.cyan("testers add <name>")}`);
8807
+ log(` 2. Run ${chalk6.cyan("testers run <url>")} to execute tests`);
8808
+ log(` 3. Add more scenarios with ${chalk6.cyan("testers add <name>")}`);
8299
8809
  log("");
8300
8810
  } catch (error) {
8301
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8811
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8302
8812
  process.exit(1);
8303
8813
  }
8304
8814
  });
@@ -8306,16 +8816,16 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
8306
8816
  try {
8307
8817
  const originalRun = getRun(runId);
8308
8818
  if (!originalRun) {
8309
- logError(chalk5.red(`Run not found: ${runId}`));
8819
+ logError(chalk6.red(`Run not found: ${runId}`));
8310
8820
  process.exit(1);
8311
8821
  }
8312
8822
  const originalResults = getResultsByRun(originalRun.id);
8313
8823
  const scenarioIds = originalResults.map((r) => r.scenarioId);
8314
8824
  if (scenarioIds.length === 0) {
8315
- log(chalk5.dim("No scenarios to replay."));
8825
+ log(chalk6.dim("No scenarios to replay."));
8316
8826
  return;
8317
8827
  }
8318
- log(chalk5.blue(`Replaying ${scenarioIds.length} scenarios from run ${originalRun.id.slice(0, 8)}...`));
8828
+ log(chalk6.blue(`Replaying ${scenarioIds.length} scenarios from run ${originalRun.id.slice(0, 8)}...`));
8319
8829
  const { run, results } = await runByFilter({
8320
8830
  url: opts.url ?? originalRun.url,
8321
8831
  scenarioIds,
@@ -8330,7 +8840,7 @@ program2.command("replay <run-id>").description("Re-run all scenarios from a pre
8330
8840
  }
8331
8841
  process.exit(getExitCode(run));
8332
8842
  } catch (error) {
8333
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8843
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8334
8844
  process.exit(1);
8335
8845
  }
8336
8846
  });
@@ -8338,16 +8848,16 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
8338
8848
  try {
8339
8849
  const originalRun = getRun(runId);
8340
8850
  if (!originalRun) {
8341
- logError(chalk5.red(`Run not found: ${runId}`));
8851
+ logError(chalk6.red(`Run not found: ${runId}`));
8342
8852
  process.exit(1);
8343
8853
  }
8344
8854
  const originalResults = getResultsByRun(originalRun.id);
8345
8855
  const failedScenarioIds = originalResults.filter((r) => r.status === "failed" || r.status === "error").map((r) => r.scenarioId);
8346
8856
  if (failedScenarioIds.length === 0) {
8347
- log(chalk5.green("No failed scenarios to retry. All passed!"));
8857
+ log(chalk6.green("No failed scenarios to retry. All passed!"));
8348
8858
  return;
8349
8859
  }
8350
- log(chalk5.blue(`Retrying ${failedScenarioIds.length} failed scenarios from run ${originalRun.id.slice(0, 8)}...`));
8860
+ log(chalk6.blue(`Retrying ${failedScenarioIds.length} failed scenarios from run ${originalRun.id.slice(0, 8)}...`));
8351
8861
  const { run, results } = await runByFilter({
8352
8862
  url: opts.url ?? originalRun.url,
8353
8863
  scenarioIds: failedScenarioIds,
@@ -8357,13 +8867,13 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
8357
8867
  });
8358
8868
  if (!opts.json) {
8359
8869
  log("");
8360
- log(chalk5.bold(" Comparison with original run:"));
8870
+ log(chalk6.bold(" Comparison with original run:"));
8361
8871
  for (const result of results) {
8362
8872
  const original = originalResults.find((r) => r.scenarioId === result.scenarioId);
8363
8873
  if (original) {
8364
8874
  const changed = original.status !== result.status;
8365
- const arrow = changed ? chalk5.yellow(`${original.status} \u2192 ${result.status}`) : chalk5.dim(`${result.status} (unchanged)`);
8366
- const icon = result.status === "passed" ? chalk5.green("\u2713") : chalk5.red("\u2717");
8875
+ const arrow = changed ? chalk6.yellow(`${original.status} \u2192 ${result.status}`) : chalk6.dim(`${result.status} (unchanged)`);
8876
+ const icon = result.status === "passed" ? chalk6.green("\u2713") : chalk6.red("\u2717");
8367
8877
  log(` ${icon} ${result.scenarioId.slice(0, 8)}: ${arrow}`);
8368
8878
  }
8369
8879
  }
@@ -8376,14 +8886,14 @@ program2.command("retry <run-id>").description("Re-run only failed scenarios fro
8376
8886
  }
8377
8887
  process.exit(getExitCode(run));
8378
8888
  } catch (error) {
8379
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8889
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8380
8890
  process.exit(1);
8381
8891
  }
8382
8892
  });
8383
8893
  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) => {
8384
8894
  try {
8385
8895
  const projectId = resolveProject(opts.project);
8386
- log(chalk5.blue(`Running smoke test against ${chalk5.bold(url)}...`));
8896
+ log(chalk6.blue(`Running smoke test against ${chalk6.bold(url)}...`));
8387
8897
  log("");
8388
8898
  const smokeResult = await runSmoke({
8389
8899
  url,
@@ -8405,7 +8915,7 @@ program2.command("smoke <url>").description("Run autonomous smoke test").option(
8405
8915
  const hasCritical = smokeResult.issuesFound.some((i) => i.severity === "critical" || i.severity === "high");
8406
8916
  process.exit(hasCritical ? 1 : 0);
8407
8917
  } catch (error) {
8408
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8918
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8409
8919
  process.exit(1);
8410
8920
  }
8411
8921
  });
@@ -8429,7 +8939,7 @@ program2.command("diff <run1> <run2>").description("Compare two test runs").opti
8429
8939
  const hasVisualRegressions = visualResults.some((r) => r.isRegression);
8430
8940
  process.exit(diff.regressions.length > 0 || hasVisualRegressions ? 1 : 0);
8431
8941
  } catch (error) {
8432
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8942
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8433
8943
  process.exit(1);
8434
8944
  }
8435
8945
  });
@@ -8443,13 +8953,13 @@ program2.command("report [run-id]").description("Generate HTML test report").opt
8443
8953
  }
8444
8954
  writeFileSync3(opts.output, html, "utf-8");
8445
8955
  const absPath = resolve(opts.output);
8446
- log(chalk5.green(`Report generated: ${absPath}`));
8956
+ log(chalk6.green(`Report generated: ${absPath}`));
8447
8957
  if (opts.open) {
8448
8958
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
8449
8959
  Bun.spawn([openCmd, absPath]);
8450
8960
  }
8451
8961
  } catch (error) {
8452
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8962
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8453
8963
  process.exit(1);
8454
8964
  }
8455
8965
  });
@@ -8462,9 +8972,9 @@ authCmd.command("add <name>").description("Create an auth preset").requiredOptio
8462
8972
  password: opts.password,
8463
8973
  loginPath: opts.loginPath
8464
8974
  });
8465
- log(chalk5.green(`Created auth preset ${chalk5.bold(preset.name)} (${preset.email})`));
8975
+ log(chalk6.green(`Created auth preset ${chalk6.bold(preset.name)} (${preset.email})`));
8466
8976
  } catch (error) {
8467
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8977
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8468
8978
  process.exit(1);
8469
8979
  }
8470
8980
  });
@@ -8472,11 +8982,11 @@ authCmd.command("list").description("List auth presets").action(() => {
8472
8982
  try {
8473
8983
  const presets = listAuthPresets();
8474
8984
  if (presets.length === 0) {
8475
- log(chalk5.dim("No auth presets found."));
8985
+ log(chalk6.dim("No auth presets found."));
8476
8986
  return;
8477
8987
  }
8478
8988
  log("");
8479
- log(chalk5.bold(" Auth Presets"));
8989
+ log(chalk6.bold(" Auth Presets"));
8480
8990
  log("");
8481
8991
  log(` ${"Name".padEnd(20)} ${"Email".padEnd(30)} ${"Login Path".padEnd(15)} Created`);
8482
8992
  log(` ${"\u2500".repeat(20)} ${"\u2500".repeat(30)} ${"\u2500".repeat(15)} ${"\u2500".repeat(22)}`);
@@ -8485,7 +8995,7 @@ authCmd.command("list").description("List auth presets").action(() => {
8485
8995
  }
8486
8996
  log("");
8487
8997
  } catch (error) {
8488
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8998
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8489
8999
  process.exit(1);
8490
9000
  }
8491
9001
  });
@@ -8493,13 +9003,13 @@ authCmd.command("delete <name>").description("Delete an auth preset").action((na
8493
9003
  try {
8494
9004
  const deleted = deleteAuthPreset(name);
8495
9005
  if (deleted) {
8496
- log(chalk5.green(`Deleted auth preset: ${name}`));
9006
+ log(chalk6.green(`Deleted auth preset: ${name}`));
8497
9007
  } else {
8498
- logError(chalk5.red(`Auth preset not found: ${name}`));
9008
+ logError(chalk6.red(`Auth preset not found: ${name}`));
8499
9009
  process.exit(1);
8500
9010
  }
8501
9011
  } catch (error) {
8502
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9012
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8503
9013
  process.exit(1);
8504
9014
  }
8505
9015
  });
@@ -8525,7 +9035,7 @@ program2.command("costs").description("Show cost tracking and budget status").op
8525
9035
  log(formatCostsTerminal(summary));
8526
9036
  }
8527
9037
  } catch (error) {
8528
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9038
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8529
9039
  process.exit(1);
8530
9040
  }
8531
9041
  });
@@ -8533,18 +9043,18 @@ program2.command("chain <scenario-id>").description("Add a dependency to a scena
8533
9043
  try {
8534
9044
  const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
8535
9045
  if (!scenario) {
8536
- logError(chalk5.red(`Scenario not found: ${scenarioId}`));
9046
+ logError(chalk6.red(`Scenario not found: ${scenarioId}`));
8537
9047
  process.exit(1);
8538
9048
  }
8539
9049
  const dep = getScenario(opts.dependsOn) ?? getScenarioByShortId(opts.dependsOn);
8540
9050
  if (!dep) {
8541
- logError(chalk5.red(`Dependency scenario not found: ${opts.dependsOn}`));
9051
+ logError(chalk6.red(`Dependency scenario not found: ${opts.dependsOn}`));
8542
9052
  process.exit(1);
8543
9053
  }
8544
9054
  addDependency(scenario.id, dep.id);
8545
- log(chalk5.green(`${scenario.shortId} now depends on ${dep.shortId}`));
9055
+ log(chalk6.green(`${scenario.shortId} now depends on ${dep.shortId}`));
8546
9056
  } catch (error) {
8547
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9057
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8548
9058
  process.exit(1);
8549
9059
  }
8550
9060
  });
@@ -8552,18 +9062,18 @@ program2.command("unchain <scenario-id>").description("Remove a dependency from
8552
9062
  try {
8553
9063
  const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
8554
9064
  if (!scenario) {
8555
- logError(chalk5.red(`Scenario not found: ${scenarioId}`));
9065
+ logError(chalk6.red(`Scenario not found: ${scenarioId}`));
8556
9066
  process.exit(1);
8557
9067
  }
8558
9068
  const dep = getScenario(opts.from) ?? getScenarioByShortId(opts.from);
8559
9069
  if (!dep) {
8560
- logError(chalk5.red(`Dependency not found: ${opts.from}`));
9070
+ logError(chalk6.red(`Dependency not found: ${opts.from}`));
8561
9071
  process.exit(1);
8562
9072
  }
8563
9073
  removeDependency(scenario.id, dep.id);
8564
- log(chalk5.green(`Removed dependency: ${scenario.shortId} no longer depends on ${dep.shortId}`));
9074
+ log(chalk6.green(`Removed dependency: ${scenario.shortId} no longer depends on ${dep.shortId}`));
8565
9075
  } catch (error) {
8566
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9076
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8567
9077
  process.exit(1);
8568
9078
  }
8569
9079
  });
@@ -8571,26 +9081,26 @@ program2.command("deps <scenario-id>").description("Show dependencies for a scen
8571
9081
  try {
8572
9082
  const scenario = getScenario(scenarioId) ?? getScenarioByShortId(scenarioId);
8573
9083
  if (!scenario) {
8574
- logError(chalk5.red(`Scenario not found: ${scenarioId}`));
9084
+ logError(chalk6.red(`Scenario not found: ${scenarioId}`));
8575
9085
  process.exit(1);
8576
9086
  }
8577
9087
  const deps = getDependencies(scenario.id);
8578
9088
  const dependents = getDependents(scenario.id);
8579
9089
  log("");
8580
- log(chalk5.bold(` Dependencies for ${scenario.shortId}: ${scenario.name}`));
9090
+ log(chalk6.bold(` Dependencies for ${scenario.shortId}: ${scenario.name}`));
8581
9091
  log("");
8582
9092
  if (deps.length > 0) {
8583
- log(chalk5.dim(" Depends on:"));
9093
+ log(chalk6.dim(" Depends on:"));
8584
9094
  for (const depId of deps) {
8585
9095
  const s = getScenario(depId);
8586
9096
  log(` \u2192 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
8587
9097
  }
8588
9098
  } else {
8589
- log(chalk5.dim(" No dependencies"));
9099
+ log(chalk6.dim(" No dependencies"));
8590
9100
  }
8591
9101
  if (dependents.length > 0) {
8592
9102
  log("");
8593
- log(chalk5.dim(" Required by:"));
9103
+ log(chalk6.dim(" Required by:"));
8594
9104
  for (const depId of dependents) {
8595
9105
  const s = getScenario(depId);
8596
9106
  log(` \u2190 ${s ? `${s.shortId}: ${s.name}` : depId.slice(0, 8)}`);
@@ -8598,7 +9108,7 @@ program2.command("deps <scenario-id>").description("Show dependencies for a scen
8598
9108
  }
8599
9109
  log("");
8600
9110
  } catch (error) {
8601
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9111
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8602
9112
  process.exit(1);
8603
9113
  }
8604
9114
  });
@@ -8608,7 +9118,7 @@ flowCmd.command("create <name>").description("Create a flow from scenario IDs").
8608
9118
  const ids = opts.chain.split(",").map((id) => {
8609
9119
  const s = getScenario(id.trim()) ?? getScenarioByShortId(id.trim());
8610
9120
  if (!s) {
8611
- logError(chalk5.red(`Scenario not found: ${id.trim()}`));
9121
+ logError(chalk6.red(`Scenario not found: ${id.trim()}`));
8612
9122
  process.exit(1);
8613
9123
  }
8614
9124
  return s.id;
@@ -8619,37 +9129,37 @@ flowCmd.command("create <name>").description("Create a flow from scenario IDs").
8619
9129
  } catch {}
8620
9130
  }
8621
9131
  const flow = createFlow({ name, scenarioIds: ids, projectId: resolveProject(opts.project) });
8622
- log(chalk5.green(`Flow created: ${flow.id.slice(0, 8)} \u2014 ${flow.name} (${ids.length} scenarios)`));
9132
+ log(chalk6.green(`Flow created: ${flow.id.slice(0, 8)} \u2014 ${flow.name} (${ids.length} scenarios)`));
8623
9133
  } catch (error) {
8624
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9134
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8625
9135
  process.exit(1);
8626
9136
  }
8627
9137
  });
8628
9138
  flowCmd.command("list").description("List all flows").option("--project <id>", "Project ID").action((opts) => {
8629
9139
  const flows = listFlows(resolveProject(opts.project) ?? undefined);
8630
9140
  if (flows.length === 0) {
8631
- log(chalk5.dim(`
9141
+ log(chalk6.dim(`
8632
9142
  No flows found.
8633
9143
  `));
8634
9144
  return;
8635
9145
  }
8636
9146
  log("");
8637
- log(chalk5.bold(" Flows"));
9147
+ log(chalk6.bold(" Flows"));
8638
9148
  log("");
8639
9149
  for (const f of flows) {
8640
- log(` ${chalk5.dim(f.id.slice(0, 8))} ${f.name} ${chalk5.dim(`(${f.scenarioIds.length} scenarios)`)}`);
9150
+ log(` ${chalk6.dim(f.id.slice(0, 8))} ${f.name} ${chalk6.dim(`(${f.scenarioIds.length} scenarios)`)}`);
8641
9151
  }
8642
9152
  log("");
8643
9153
  });
8644
9154
  flowCmd.command("show <id>").description("Show flow details").action((id) => {
8645
9155
  const flow = getFlow(id);
8646
9156
  if (!flow) {
8647
- logError(chalk5.red(`Flow not found: ${id}`));
9157
+ logError(chalk6.red(`Flow not found: ${id}`));
8648
9158
  process.exit(1);
8649
9159
  }
8650
9160
  log("");
8651
- log(chalk5.bold(` Flow: ${flow.name}`));
8652
- log(` ID: ${chalk5.dim(flow.id)}`);
9161
+ log(chalk6.bold(` Flow: ${flow.name}`));
9162
+ log(` ID: ${chalk6.dim(flow.id)}`);
8653
9163
  log(` Scenarios (in order):`);
8654
9164
  for (let i = 0;i < flow.scenarioIds.length; i++) {
8655
9165
  const s = getScenario(flow.scenarioIds[i]);
@@ -8659,9 +9169,9 @@ flowCmd.command("show <id>").description("Show flow details").action((id) => {
8659
9169
  });
8660
9170
  flowCmd.command("delete <id>").description("Delete a flow").action((id) => {
8661
9171
  if (deleteFlow(id))
8662
- log(chalk5.green("Flow deleted."));
9172
+ log(chalk6.green("Flow deleted."));
8663
9173
  else {
8664
- logError(chalk5.red("Flow not found."));
9174
+ logError(chalk6.red("Flow not found."));
8665
9175
  process.exit(1);
8666
9176
  }
8667
9177
  });
@@ -8669,14 +9179,14 @@ flowCmd.command("run <id>").description("Run a flow (scenarios in dependency ord
8669
9179
  try {
8670
9180
  const flow = getFlow(id);
8671
9181
  if (!flow) {
8672
- logError(chalk5.red(`Flow not found: ${id}`));
9182
+ logError(chalk6.red(`Flow not found: ${id}`));
8673
9183
  process.exit(1);
8674
9184
  }
8675
9185
  if (!opts.url) {
8676
- logError(chalk5.red("--url is required for flow run"));
9186
+ logError(chalk6.red("--url is required for flow run"));
8677
9187
  process.exit(1);
8678
9188
  }
8679
- log(chalk5.blue(`Running flow: ${flow.name} (${flow.scenarioIds.length} scenarios)`));
9189
+ log(chalk6.blue(`Running flow: ${flow.name} (${flow.scenarioIds.length} scenarios)`));
8680
9190
  const { run, results } = await runByFilter({
8681
9191
  url: opts.url,
8682
9192
  scenarioIds: flow.scenarioIds,
@@ -8690,7 +9200,7 @@ flowCmd.command("run <id>").description("Run a flow (scenarios in dependency ord
8690
9200
  log(formatTerminal(run, results));
8691
9201
  process.exit(getExitCode(run));
8692
9202
  } catch (error) {
8693
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9203
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8694
9204
  process.exit(1);
8695
9205
  }
8696
9206
  });
@@ -8704,9 +9214,9 @@ envCmd.command("add <name>").description("Add a named environment").requiredOpti
8704
9214
  projectId: opts.project,
8705
9215
  isDefault: opts.default
8706
9216
  });
8707
- log(chalk5.green(`Environment added: ${env.name} \u2192 ${env.url}${env.isDefault ? " (default)" : ""}`));
9217
+ log(chalk6.green(`Environment added: ${env.name} \u2192 ${env.url}${env.isDefault ? " (default)" : ""}`));
8708
9218
  } catch (error) {
8709
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9219
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8710
9220
  process.exit(1);
8711
9221
  }
8712
9222
  });
@@ -8714,16 +9224,16 @@ envCmd.command("list").description("List all environments").option("--project <i
8714
9224
  try {
8715
9225
  const envs = listEnvironments(opts.project);
8716
9226
  if (envs.length === 0) {
8717
- log(chalk5.dim("No environments configured. Add one with: testers env add <name> --url <url>"));
9227
+ log(chalk6.dim("No environments configured. Add one with: testers env add <name> --url <url>"));
8718
9228
  return;
8719
9229
  }
8720
9230
  for (const env of envs) {
8721
- const marker = env.isDefault ? chalk5.green(" \u2605 default") : "";
8722
- const auth = env.authPresetName ? chalk5.dim(` (auth: ${env.authPresetName})`) : "";
8723
- log(` ${chalk5.bold(env.name)} ${env.url}${auth}${marker}`);
9231
+ const marker = env.isDefault ? chalk6.green(" \u2605 default") : "";
9232
+ const auth = env.authPresetName ? chalk6.dim(` (auth: ${env.authPresetName})`) : "";
9233
+ log(` ${chalk6.bold(env.name)} ${env.url}${auth}${marker}`);
8724
9234
  }
8725
9235
  } catch (error) {
8726
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9236
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8727
9237
  process.exit(1);
8728
9238
  }
8729
9239
  });
@@ -8731,9 +9241,9 @@ envCmd.command("use <name>").description("Set an environment as the default").ac
8731
9241
  try {
8732
9242
  setDefaultEnvironment(name);
8733
9243
  const env = getEnvironment(name);
8734
- log(chalk5.green(`Default environment set: ${env.name} \u2192 ${env.url}`));
9244
+ log(chalk6.green(`Default environment set: ${env.name} \u2192 ${env.url}`));
8735
9245
  } catch (error) {
8736
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9246
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8737
9247
  process.exit(1);
8738
9248
  }
8739
9249
  });
@@ -8741,13 +9251,13 @@ envCmd.command("delete <name>").description("Delete an environment").action((nam
8741
9251
  try {
8742
9252
  const deleted = deleteEnvironment(name);
8743
9253
  if (deleted) {
8744
- log(chalk5.green(`Environment deleted: ${name}`));
9254
+ log(chalk6.green(`Environment deleted: ${name}`));
8745
9255
  } else {
8746
- logError(chalk5.red(`Environment not found: ${name}`));
9256
+ logError(chalk6.red(`Environment not found: ${name}`));
8747
9257
  process.exit(1);
8748
9258
  }
8749
9259
  } catch (error) {
8750
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9260
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8751
9261
  process.exit(1);
8752
9262
  }
8753
9263
  });
@@ -8755,9 +9265,9 @@ program2.command("baseline <run-id>").description("Set a run as the visual basel
8755
9265
  try {
8756
9266
  setBaseline(runId);
8757
9267
  const run = getRun(runId);
8758
- log(chalk5.green(`Baseline set: ${chalk5.bold(runId.slice(0, 8))}${run ? ` (${run.status}, ${run.total} scenarios)` : ""}`));
9268
+ log(chalk6.green(`Baseline set: ${chalk6.bold(runId.slice(0, 8))}${run ? ` (${run.status}, ${run.total} scenarios)` : ""}`));
8759
9269
  } catch (error) {
8760
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9270
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8761
9271
  process.exit(1);
8762
9272
  }
8763
9273
  });
@@ -8765,28 +9275,159 @@ program2.command("import-api <spec>").description("Import test scenarios from an
8765
9275
  try {
8766
9276
  const { importFromOpenAPI: importFromOpenAPI2 } = await Promise.resolve().then(() => (init_openapi_import(), exports_openapi_import));
8767
9277
  const { imported, scenarios } = importFromOpenAPI2(spec, resolveProject(opts.project) ?? undefined);
8768
- log(chalk5.green(`
9278
+ log(chalk6.green(`
8769
9279
  Imported ${imported} scenarios from API spec:`));
8770
9280
  for (const s of scenarios) {
8771
- log(` ${chalk5.cyan(s.shortId)} ${s.name} ${chalk5.dim(`[${s.tags.join(", ")}]`)}`);
9281
+ log(` ${chalk6.cyan(s.shortId)} ${s.name} ${chalk6.dim(`[${s.tags.join(", ")}]`)}`);
8772
9282
  }
8773
9283
  log("");
8774
9284
  } catch (error) {
8775
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9285
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8776
9286
  process.exit(1);
8777
9287
  }
8778
9288
  });
8779
9289
  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) => {
8780
9290
  try {
8781
9291
  const { recordAndSave: recordAndSave2 } = await Promise.resolve().then(() => (init_recorder(), exports_recorder));
8782
- log(chalk5.blue("Opening browser for recording..."));
9292
+ log(chalk6.blue("Opening browser for recording..."));
8783
9293
  const { recording, scenario } = await recordAndSave2(url, opts.name, resolveProject(opts.project) ?? undefined);
8784
9294
  log("");
8785
- log(chalk5.green(`Recording saved as scenario ${chalk5.bold(scenario.shortId)}: ${scenario.name}`));
8786
- log(chalk5.dim(` ${recording.actions.length} actions recorded in ${(recording.duration / 1000).toFixed(0)}s`));
8787
- log(chalk5.dim(` ${scenario.steps.length} steps generated`));
9295
+ log(chalk6.green(`Recording saved as scenario ${chalk6.bold(scenario.shortId)}: ${scenario.name}`));
9296
+ log(chalk6.dim(` ${recording.actions.length} actions recorded in ${(recording.duration / 1000).toFixed(0)}s`));
9297
+ log(chalk6.dim(` ${scenario.steps.length} steps generated`));
9298
+ } catch (error) {
9299
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9300
+ process.exit(1);
9301
+ }
9302
+ });
9303
+ program2.command("run-affected <url>").description("Run only scenarios relevant to changed files (diff-aware testing)").option("-f, --file <path>", "Changed file path (repeatable)", (v, acc) => {
9304
+ acc.push(v);
9305
+ return acc;
9306
+ }, []).option("--map <glob:tags>", "Glob\u2192tag mapping, e.g. 'src/chat*:chat,messaging' (repeatable)", (v, acc) => {
9307
+ acc.push(v);
9308
+ return acc;
9309
+ }, []).option("--project <id>", "Project ID").option("-m, --model <model>", "AI model to use").option("--headed", "Run browser in headed mode", false).option("--parallel <n>", "Number of parallel browsers", "1").option("--json", "Output results as JSON", false).action(async (url, opts) => {
9310
+ try {
9311
+ const { matchFilesToScenarios: matchFilesToScenarios2 } = await Promise.resolve().then(() => exports_affected);
9312
+ const { runBatch: runBatch2 } = await Promise.resolve().then(() => (init_runner(), exports_runner));
9313
+ const projectId = resolveProject(opts.project);
9314
+ const mappings = opts.map.map((m) => {
9315
+ const sep = m.lastIndexOf(":");
9316
+ if (sep < 1)
9317
+ return null;
9318
+ return { glob: m.slice(0, sep), tags: m.slice(sep + 1).split(",").map((t) => t.trim()) };
9319
+ }).filter(Boolean);
9320
+ const allScenarios = listScenarios({ projectId });
9321
+ const matched = matchFilesToScenarios2(opts.file, allScenarios, mappings);
9322
+ if (matched.length === 0) {
9323
+ log(chalk6.yellow(" No scenarios matched the provided file paths."));
9324
+ log(chalk6.dim(" Tip: use --map 'src/chat*:chat' to add explicit mappings."));
9325
+ process.exit(0);
9326
+ }
9327
+ log(chalk6.blue(` Running ${matched.length} affected scenario(s) against ${url}...`));
9328
+ log(chalk6.dim(` Files: ${opts.file.slice(0, 5).join(", ")}${opts.file.length > 5 ? "\u2026" : ""}`));
9329
+ log("");
9330
+ const { run, results } = await runBatch2(matched, {
9331
+ url,
9332
+ model: opts.model,
9333
+ headed: opts.headed,
9334
+ parallel: parseInt(opts.parallel, 10),
9335
+ projectId
9336
+ });
9337
+ if (opts.json) {
9338
+ log(JSON.stringify({ run, results }, null, 2));
9339
+ } else {
9340
+ const { formatTerminal: formatTerminal2 } = await Promise.resolve().then(() => (init_reporter(), exports_reporter));
9341
+ log(formatTerminal2(run, results));
9342
+ }
9343
+ const { getExitCode: getExitCode2 } = await Promise.resolve().then(() => (init_reporter(), exports_reporter));
9344
+ process.exit(getExitCode2(run));
9345
+ } catch (error) {
9346
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9347
+ process.exit(1);
9348
+ }
9349
+ });
9350
+ program2.command("git-watch <url>").description("Watch for git commits and auto-run affected scenarios").option("--dir <path>", "Git repository directory to watch", process.cwd()).option("--poll <ms>", "Poll interval in milliseconds", "10000").option("--map <glob:tags>", "Glob\u2192tag mapping (repeatable)", (v, acc) => {
9351
+ acc.push(v);
9352
+ return acc;
9353
+ }, []).option("--project <id>", "Project ID").option("-m, --model <model>", "AI model to use").option("--headed", "Run browser in headed mode", false).option("--parallel <n>", "Number of parallel browsers", "1").action(async (url, opts) => {
9354
+ try {
9355
+ const { startGitWatcher: startGitWatcher2 } = await Promise.resolve().then(() => (init_git_watch(), exports_git_watch));
9356
+ const mappings = opts.map.map((m) => {
9357
+ const sep = m.lastIndexOf(":");
9358
+ if (sep < 1)
9359
+ return null;
9360
+ return { glob: m.slice(0, sep), tags: m.slice(sep + 1).split(",").map((t) => t.trim()) };
9361
+ }).filter(Boolean);
9362
+ await startGitWatcher2({
9363
+ url,
9364
+ dir: opts.dir,
9365
+ pollIntervalMs: parseInt(opts.poll, 10),
9366
+ mappings,
9367
+ projectId: resolveProject(opts.project),
9368
+ model: opts.model,
9369
+ headed: opts.headed,
9370
+ parallel: parseInt(opts.parallel, 10)
9371
+ });
9372
+ } catch (error) {
9373
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9374
+ process.exit(1);
9375
+ }
9376
+ });
9377
+ var agentCmd = program2.command("agent").description("Manage registered agents");
9378
+ agentCmd.command("register <name>").description("Register an agent (idempotent)").option("-d, --description <text>", "Agent description").option("-r, --role <role>", "Agent role").action((name, opts) => {
9379
+ try {
9380
+ const { registerAgent: registerAgent2 } = (init_agents(), __toCommonJS(exports_agents));
9381
+ const agent = registerAgent2({ name, description: opts.description, role: opts.role });
9382
+ log(chalk6.green(`Registered agent: ${agent.name} (${agent.id.slice(0, 8)})`));
9383
+ } catch (error) {
9384
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9385
+ process.exit(1);
9386
+ }
9387
+ });
9388
+ agentCmd.command("heartbeat <id>").description("Update agent last_seen_at timestamp").action((id) => {
9389
+ try {
9390
+ const { heartbeatAgent: heartbeatAgent2 } = (init_agents(), __toCommonJS(exports_agents));
9391
+ const agent = heartbeatAgent2(id);
9392
+ if (!agent) {
9393
+ logError(chalk6.red(`Agent not found: ${id}`));
9394
+ process.exit(1);
9395
+ }
9396
+ log(chalk6.green(`Heartbeat sent for ${agent.name} \u2014 ${agent.lastSeenAt}`));
9397
+ } catch (error) {
9398
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9399
+ process.exit(1);
9400
+ }
9401
+ });
9402
+ agentCmd.command("focus <agent-id> [scenario-id]").description("Set (or clear) an agent's current focus scenario").action((agentId, scenarioId) => {
9403
+ try {
9404
+ const { setAgentFocus: setAgentFocus2 } = (init_agents(), __toCommonJS(exports_agents));
9405
+ const agent = setAgentFocus2(agentId, scenarioId ?? null);
9406
+ if (!agent) {
9407
+ logError(chalk6.red(`Agent not found: ${agentId}`));
9408
+ process.exit(1);
9409
+ }
9410
+ const focus = agent.metadata?.focus ?? null;
9411
+ log(focus ? chalk6.green(`Agent ${agent.name} focus set to: ${focus}`) : chalk6.dim(`Agent ${agent.name} focus cleared`));
9412
+ } catch (error) {
9413
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9414
+ process.exit(1);
9415
+ }
9416
+ });
9417
+ agentCmd.command("list").description("List all registered agents").action(() => {
9418
+ try {
9419
+ const { listAgents: listAgents2 } = (init_agents(), __toCommonJS(exports_agents));
9420
+ const agents = listAgents2();
9421
+ if (agents.length === 0) {
9422
+ log(chalk6.dim("No agents registered."));
9423
+ return;
9424
+ }
9425
+ for (const a of agents) {
9426
+ const focus = a.metadata?.focus;
9427
+ log(` ${chalk6.cyan(a.id.slice(0, 8))} ${chalk6.bold(a.name)}${a.role ? chalk6.dim(` [${a.role}]`) : ""}${focus ? chalk6.yellow(` \u2192 ${focus}`) : ""} ${chalk6.dim(a.lastSeenAt)}`);
9428
+ }
8788
9429
  } catch (error) {
8789
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9430
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8790
9431
  process.exit(1);
8791
9432
  }
8792
9433
  });
@@ -8794,9 +9435,9 @@ program2.command("doctor").description("Check system setup and configuration").a
8794
9435
  let allPassed = true;
8795
9436
  const hasApiKey = Boolean(process.env["ANTHROPIC_API_KEY"]);
8796
9437
  if (hasApiKey) {
8797
- log(chalk5.green("\u2713") + " ANTHROPIC_API_KEY is set");
9438
+ log(chalk6.green("\u2713") + " ANTHROPIC_API_KEY is set");
8798
9439
  } else {
8799
- log(chalk5.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
9440
+ log(chalk6.red("\u2717") + " ANTHROPIC_API_KEY is not set (required for AI-powered tests)");
8800
9441
  allPassed = false;
8801
9442
  }
8802
9443
  const dbPath = join6(process.env["HOME"] ?? "~", ".testers", "testers.db");
@@ -8804,9 +9445,9 @@ program2.command("doctor").description("Check system setup and configuration").a
8804
9445
  const { Database: Database3 } = await import("bun:sqlite");
8805
9446
  const db2 = new Database3(dbPath, { create: true });
8806
9447
  db2.close();
8807
- log(chalk5.green("\u2713") + ` Database accessible: ${dbPath}`);
9448
+ log(chalk6.green("\u2713") + ` Database accessible: ${dbPath}`);
8808
9449
  } catch (err) {
8809
- log(chalk5.red("\u2717") + ` Database not accessible at ${dbPath}: ${err instanceof Error ? err.message : String(err)}`);
9450
+ log(chalk6.red("\u2717") + ` Database not accessible at ${dbPath}: ${err instanceof Error ? err.message : String(err)}`);
8810
9451
  allPassed = false;
8811
9452
  }
8812
9453
  try {
@@ -8814,13 +9455,13 @@ program2.command("doctor").description("Check system setup and configuration").a
8814
9455
  const execPath = chromium4.executablePath();
8815
9456
  const { existsSync: fsExists } = await import("fs");
8816
9457
  if (fsExists(execPath)) {
8817
- log(chalk5.green("\u2713") + " Playwright chromium is installed");
9458
+ log(chalk6.green("\u2713") + " Playwright chromium is installed");
8818
9459
  } else {
8819
- log(chalk5.red("\u2717") + ` Playwright chromium executable not found at ${execPath}. Run: testers install`);
9460
+ log(chalk6.red("\u2717") + ` Playwright chromium executable not found at ${execPath}. Run: testers install`);
8820
9461
  allPassed = false;
8821
9462
  }
8822
9463
  } catch {
8823
- log(chalk5.red("\u2717") + " Playwright is not installed. Run: testers install");
9464
+ log(chalk6.red("\u2717") + " Playwright is not installed. Run: testers install");
8824
9465
  allPassed = false;
8825
9466
  }
8826
9467
  if (!allPassed) {
@@ -8840,7 +9481,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
8840
9481
  stdout: "inherit",
8841
9482
  stderr: "inherit"
8842
9483
  });
8843
- log(chalk5.green(`Open Testers dashboard starting at ${url}`));
9484
+ log(chalk6.green(`Open Testers dashboard starting at ${url}`));
8844
9485
  if (opts.open !== false) {
8845
9486
  await new Promise((r) => setTimeout(r, 1500));
8846
9487
  const openCmd = process.platform === "darwin" ? "open" : "xdg-open";
@@ -8848,7 +9489,7 @@ program2.command("serve").description("Start the Open Testers web dashboard").op
8848
9489
  }
8849
9490
  await proc.exited;
8850
9491
  } catch (error) {
8851
- logError(chalk5.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
9492
+ logError(chalk6.red(`Error: ${error instanceof Error ? error.message : String(error)}`));
8852
9493
  process.exit(1);
8853
9494
  }
8854
9495
  });