agent-qa 0.1.8 → 0.1.10

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.js CHANGED
@@ -248,7 +248,7 @@ __export(llm_utils_exports, {
248
248
  resolveModelAuth: () => resolveModelAuth,
249
249
  resolveNamedConfig: () => resolveNamedConfig
250
250
  });
251
- import { dirname as dirname3, resolve as resolvePath2 } from "path";
251
+ import { dirname as dirname4, resolve as resolvePath2 } from "path";
252
252
  function normalizeLLMConfig(raw) {
253
253
  return {
254
254
  name: raw.name,
@@ -266,7 +266,7 @@ async function loadAuthPluginsForRawConfig(raw, configPath = "agent-qa.config.ya
266
266
  if (!Array.isArray(declarations) || declarations.length === 0) return;
267
267
  const { loadLLMAuthPlugins: loadLLMAuthPlugins2 } = await import("@vostride/agent-qa-core");
268
268
  await loadLLMAuthPlugins2(declarations, {
269
- baseDir: dirname3(resolvePath2(configPath))
269
+ baseDir: dirname4(resolvePath2(configPath))
270
270
  });
271
271
  }
272
272
  async function resolveNamedConfig(configName, configPath = "agent-qa.config.yaml") {
@@ -337,7 +337,7 @@ var init_llm_utils = __esm({
337
337
  });
338
338
 
339
339
  // src/cli.ts
340
- import { Command as Command18 } from "commander";
340
+ import { Command as Command19 } from "commander";
341
341
 
342
342
  // src/index.ts
343
343
  var index_exports = {};
@@ -491,128 +491,19 @@ function createCacheCommand() {
491
491
  var VERSION = getAgentQaVersion();
492
492
 
493
493
  // src/commands/init.ts
494
- import { Command as Command3 } from "commander";
494
+ init_model_defaults();
495
+ import { Command as Command2 } from "commander";
495
496
  import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
496
- import { join as join3, resolve as resolve2 } from "path";
497
- import { execSync } from "child_process";
497
+ import { join as join2, resolve as resolve2 } from "path";
498
498
  import { stringify } from "yaml";
499
- import pc4 from "picocolors";
499
+ import pc3 from "picocolors";
500
500
  import select from "@inquirer/select";
501
501
  import input from "@inquirer/input";
502
502
  import checkbox from "@inquirer/checkbox";
503
-
504
- // src/commands/install-browsers.ts
505
- import { spawnSync } from "child_process";
506
- import { createRequire } from "module";
507
- import { dirname as dirname2, join as join2 } from "path";
508
- import { Command as Command2 } from "commander";
509
- import pc3 from "picocolors";
510
- function selectedBrowserCount(selection) {
511
- return [selection.chromium, selection.firefox, selection.webkit].filter(Boolean).length;
512
- }
513
- function validateBrowserInstallSelection(selection) {
514
- if (!selection.all && selectedBrowserCount(selection) === 0) {
515
- return "Select at least one browser flag: --all, --chromium, --firefox, or --webkit.";
516
- }
517
- if (selection.all && selectedBrowserCount(selection) > 0) {
518
- return "Cannot combine --all with --chromium, --firefox, or --webkit.";
519
- }
520
- return null;
521
- }
522
- function buildBrowserInstallArgs(selection) {
523
- const args = [];
524
- if (selection.withDeps) args.push("--with-deps");
525
- if (selection.force) args.push("--force");
526
- if (!selection.all) {
527
- if (selection.chromium) args.push("chromium");
528
- if (selection.firefox) args.push("firefox");
529
- if (selection.webkit) args.push("webkit");
530
- }
531
- return args;
532
- }
533
- function formatInstallBrowsersRetryCommand(selection) {
534
- const args = ["agent-qa", "install-browsers"];
535
- if (selection.all) {
536
- args.push("--all");
537
- } else {
538
- if (selection.chromium) args.push("--chromium");
539
- if (selection.firefox) args.push("--firefox");
540
- if (selection.webkit) args.push("--webkit");
541
- }
542
- if (selection.withDeps) args.push("--with-deps");
543
- if (selection.force) args.push("--force");
544
- return args.join(" ");
545
- }
546
- function resolvePlaywrightCoreCli() {
547
- const require2 = createRequire(import.meta.url);
548
- const webEntry = require2.resolve("@vostride/agent-qa-web");
549
- const webRequire = createRequire(webEntry);
550
- const packagePath = webRequire.resolve("playwright-core/package.json");
551
- return join2(dirname2(packagePath), "cli.js");
552
- }
553
- function normalizeSpawnStatus(result) {
554
- if (typeof result.status === "number") return result.status;
555
- return 1;
556
- }
557
- function runBrowserInstall(selection, deps = {}) {
558
- let cliPath;
559
- try {
560
- cliPath = (deps.resolveCli ?? resolvePlaywrightCoreCli)();
561
- } catch (error) {
562
- return { ok: false, status: 1, stage: "resolve", error };
563
- }
564
- const spawn = deps.spawn ?? spawnSync;
565
- const result = spawn(process.execPath, [cliPath, "install", ...buildBrowserInstallArgs(selection)], {
566
- stdio: "inherit"
567
- });
568
- if (result.error) {
569
- return { ok: false, status: normalizeSpawnStatus(result), stage: "spawn", error: result.error };
570
- }
571
- const status = normalizeSpawnStatus(result);
572
- return { ok: status === 0, status, stage: "installer" };
573
- }
574
- function printBrowserInstallFailure(selection, result) {
575
- if (result.stage === "resolve") {
576
- console.error(pc3.red("agent-qa could not find its bundled browser installer."));
577
- console.error(pc3.dim("Reinstall agent-qa dependencies, then retry:"));
578
- } else {
579
- console.error(pc3.red("Browser installation failed."));
580
- console.error(pc3.dim("Retry:"));
581
- }
582
- console.error(` ${pc3.cyan(formatInstallBrowsersRetryCommand(selection))}`);
583
- }
584
- function createInstallBrowsersCommand(deps = {}) {
585
- const cmd = new Command2("install-browsers").description("Install browser support for agent-qa").option("--all", "install all Playwright-managed browsers for agent-qa").option("--chromium", "install Chromium browser support").option("--firefox", "install Firefox browser support").option("--webkit", "install WebKit browser support").option("--with-deps", "also install required system dependencies").option("--force", "force reinstall even if browser support already exists").action((opts) => {
586
- const selection = {
587
- all: !!opts.all,
588
- chromium: !!opts.chromium,
589
- firefox: !!opts.firefox,
590
- webkit: !!opts.webkit,
591
- withDeps: !!opts.withDeps,
592
- force: !!opts.force
593
- };
594
- const validationError = validateBrowserInstallSelection(selection);
595
- if (validationError) {
596
- console.error(pc3.red(`Error: ${validationError}`));
597
- console.error(cmd.helpInformation());
598
- process.exitCode = 1;
599
- return;
600
- }
601
- console.log(pc3.blue("Installing agent-qa browser support..."));
602
- const result = runBrowserInstall(selection, deps);
603
- if (result.ok) {
604
- console.log(pc3.green("\u2713 agent-qa browser support installed"));
605
- return;
606
- }
607
- printBrowserInstallFailure(selection, result);
608
- process.exitCode = result.status || 1;
609
- });
610
- return cmd;
611
- }
612
-
613
- // src/commands/init.ts
614
- init_model_defaults();
615
- import { DEFAULT_AGENT_QA_CACHE_DIR as DEFAULT_AGENT_QA_CACHE_DIR2, DEFAULT_AGENT_QA_RUNTIME_DIR } from "@vostride/agent-qa-core";
503
+ import {
504
+ DEFAULT_AGENT_QA_CACHE_DIR as DEFAULT_AGENT_QA_CACHE_DIR2,
505
+ DEFAULT_AGENT_QA_RUNTIME_DIR
506
+ } from "@vostride/agent-qa-core";
616
507
  var INIT_CACHE_DIR = DEFAULT_AGENT_QA_CACHE_DIR2 || ".agent-qa/cache";
617
508
  var INIT_RUNTIME_DIR = DEFAULT_AGENT_QA_RUNTIME_DIR || ".agent-qa";
618
509
  var SUBSCRIPTION_AUTH_PACKAGE = "@vostride/agent-qa-subscription-auth";
@@ -676,9 +567,6 @@ function normalizePlatformSelection(platform) {
676
567
  return { capabilities: ["web", "mobile"], mobileRuntimes: ["ios"] };
677
568
  }
678
569
  }
679
- function hasMobile(platform) {
680
- return normalizePlatformSelection(platform).mobileRuntimes.length > 0;
681
- }
682
570
  function hasWeb(platform) {
683
571
  return normalizePlatformSelection(platform).capabilities.includes("web");
684
572
  }
@@ -851,7 +739,7 @@ function configUsesSubscriptionAuth(config) {
851
739
  return authPlugins.some((plugin) => objectRecord(plugin)?.package === SUBSCRIPTION_AUTH_PACKAGE);
852
740
  }
853
741
  function upsertSubscriptionAuthPackageDependency(dir) {
854
- const packageJsonPath = join3(dir, PACKAGE_JSON_FILE);
742
+ const packageJsonPath = join2(dir, PACKAGE_JSON_FILE);
855
743
  const created = !existsSync(packageJsonPath);
856
744
  const pkg = created ? { private: true } : JSON.parse(readFileSync(packageJsonPath, "utf-8"));
857
745
  if (!objectRecord(pkg)) {
@@ -1046,7 +934,7 @@ providers:
1046
934
  `;
1047
935
  }
1048
936
  function appendGitignore(dir) {
1049
- const gitignorePath = join3(dir, ".gitignore");
937
+ const gitignorePath = join2(dir, ".gitignore");
1050
938
  let existing = "";
1051
939
  if (existsSync(gitignorePath)) {
1052
940
  existing = readFileSync(gitignorePath, "utf-8");
@@ -1056,135 +944,38 @@ function appendGitignore(dir) {
1056
944
  const append = (existing.length > 0 && !existing.endsWith("\n") ? "\n" : "") + missing.join("\n") + "\n";
1057
945
  writeFileSync(gitignorePath, existing + append);
1058
946
  }
1059
- function installPlaywrightBrowsers(platform) {
1060
- if (!hasWeb(platform)) return;
1061
- const selection = { chromium: true };
1062
- console.log(pc4.blue("Installing agent-qa browser support..."));
1063
- const result = runBrowserInstall(selection);
1064
- if (result.ok) {
1065
- console.log(pc4.green("\u2713 agent-qa browser support installed"));
1066
- } else {
1067
- console.log(pc4.yellow(`\u26A0 Browser installation failed. Run \`${formatInstallBrowsersRetryCommand(selection)}\` to retry.`));
1068
- }
1069
- }
1070
- function collectDriverNames(value, names) {
1071
- if (!value) return;
1072
- if (typeof value === "string") {
1073
- names.add(value);
1074
- return;
1075
- }
1076
- if (Array.isArray(value)) {
1077
- for (const item of value) collectDriverNames(item, names);
1078
- return;
1079
- }
1080
- if (typeof value !== "object") return;
1081
- const record = value;
1082
- if (typeof record.name === "string") names.add(record.name);
1083
- if (typeof record.driverName === "string") names.add(record.driverName);
1084
- if (typeof record.automationName === "string") names.add(record.automationName);
1085
- for (const [key, child] of Object.entries(record)) {
1086
- if (key !== "drivers" && key !== "installed" && key !== "name" && key !== "driverName" && typeof child === "object" && child !== null) {
1087
- names.add(key);
1088
- }
1089
- collectDriverNames(child, names);
1090
- }
1091
- }
1092
- function parseInstalledAppiumDrivers(output) {
1093
- const names = /* @__PURE__ */ new Set();
1094
- const trimmed = output.trim();
1095
- if (!trimmed) return names;
1096
- try {
1097
- collectDriverNames(JSON.parse(trimmed), names);
1098
- const normalizedNames = /* @__PURE__ */ new Set();
1099
- for (const name of names) {
1100
- const normalized = name.toLowerCase();
1101
- normalizedNames.add(normalized);
1102
- if (normalized.includes("uiautomator2")) normalizedNames.add("uiautomator2");
1103
- if (normalized.includes("xcuitest")) normalizedNames.add("xcuitest");
1104
- }
1105
- return normalizedNames;
1106
- } catch {
1107
- for (const line of trimmed.split("\n")) {
1108
- const normalized = line.toLowerCase();
1109
- if (normalized.includes("uiautomator2")) names.add("uiautomator2");
1110
- if (normalized.includes("xcuitest")) names.add("xcuitest");
1111
- }
1112
- return names;
1113
- }
1114
- }
1115
- function getInstalledAppiumDrivers() {
1116
- try {
1117
- const output = execSync("appium driver list --installed --json", { stdio: "pipe", encoding: "utf-8" });
1118
- return parseInstalledAppiumDrivers(String(output));
1119
- } catch {
1120
- try {
1121
- const output = execSync("appium driver list --installed", { stdio: "pipe", encoding: "utf-8" });
1122
- return parseInstalledAppiumDrivers(String(output));
1123
- } catch {
1124
- return /* @__PURE__ */ new Set();
1125
- }
1126
- }
1127
- }
1128
- function appiumDriverLabel(driver) {
1129
- return driver === "uiautomator2" ? "UiAutomator2" : "XCUITest";
1130
- }
1131
- function isAlreadyInstalledDriverError(err, driver) {
1132
- const details = [
1133
- err instanceof Error ? err.message : String(err),
1134
- err?.stdout,
1135
- err?.stderr
1136
- ].map(String).join("\n").toLowerCase();
1137
- return details.includes(`driver named "${driver}" is already installed`) || details.includes(`${driver}" is already installed`) || details.includes(`${driver} is already installed`);
1138
- }
1139
- function ensureAppiumDriver(driver, installedDrivers) {
1140
- const label = appiumDriverLabel(driver);
1141
- if (installedDrivers.has(driver)) {
1142
- console.log(pc4.green(`\u2713 ${label} driver already installed`));
1143
- return;
1144
- }
1145
- try {
1146
- execSync(`appium driver install ${driver}`, { stdio: "inherit" });
1147
- console.log(pc4.green(`\u2713 ${label} driver installed`));
1148
- } catch (err) {
1149
- if (isAlreadyInstalledDriverError(err, driver)) {
1150
- console.log(pc4.green(`\u2713 ${label} driver already installed`));
1151
- return;
1152
- }
1153
- console.log(pc4.yellow(`\u26A0 ${label} driver installation failed. Run \`appium driver install ${driver}\` manually.`));
1154
- }
1155
- }
1156
- function installAppium(platform) {
1157
- if (!hasMobile(platform)) return;
1158
- const selection = normalizePlatformSelection(platform);
1159
- console.log(pc4.blue("Installing Appium..."));
1160
- let appiumInstalled = false;
1161
- try {
1162
- execSync("appium --version", { stdio: "pipe" });
1163
- appiumInstalled = true;
1164
- console.log(pc4.green("\u2713 Appium already installed"));
1165
- } catch {
1166
- try {
1167
- execSync("npm install -g appium", { stdio: "inherit" });
1168
- console.log(pc4.green("\u2713 Appium installed"));
1169
- appiumInstalled = true;
1170
- } catch {
1171
- console.log(pc4.yellow("\u26A0 Appium installation failed. Run `npm install -g appium` manually."));
1172
- }
1173
- }
1174
- if (!appiumInstalled) return;
1175
- const installedDrivers = getInstalledAppiumDrivers();
1176
- if (selection.mobileRuntimes.includes("android")) {
1177
- ensureAppiumDriver("uiautomator2", installedDrivers);
1178
- }
1179
- if (selection.mobileRuntimes.includes("ios")) {
1180
- ensureAppiumDriver("xcuitest", installedDrivers);
1181
- }
947
+ function browserSetupCommand(platform) {
948
+ return hasWeb(platform) ? "agent-qa install-browsers --chromium" : null;
949
+ }
950
+ function mobileDriverSetupCommand(platform) {
951
+ const runtimes = normalizePlatformSelection(platform).mobileRuntimes;
952
+ if (runtimes.length === 0) return null;
953
+ if (runtimes.includes("android") && runtimes.includes("ios")) return "agent-qa install-mobile-drivers --all";
954
+ if (runtimes.includes("android")) return "agent-qa install-mobile-drivers --android";
955
+ return "agent-qa install-mobile-drivers --ios";
956
+ }
957
+ function initNextSteps(platform) {
958
+ const steps = [];
959
+ const browserCommand = browserSetupCommand(platform);
960
+ const mobileCommand = mobileDriverSetupCommand(platform);
961
+ if (browserCommand) {
962
+ steps.push({ command: browserCommand, description: "Install browser support" });
963
+ }
964
+ if (mobileCommand) {
965
+ steps.push({ command: mobileCommand, description: "Install mobile driver support" });
966
+ }
967
+ steps.push(
968
+ { command: "agent-qa doctor", description: "Verify your environment" },
969
+ { command: "Edit tests/example-pass.yaml", description: "Write your first test" },
970
+ { command: "agent-qa run", description: "Run all tests" },
971
+ { command: "agent-qa dashboard", description: "View results in browser" }
972
+ );
973
+ return steps;
1182
974
  }
1183
975
  function createInitCommand() {
1184
- const cmd = new Command3("init").description("Initialize a new agent-qa project").option("--dir <path>", "target directory", process.cwd()).option("--platform <type>", "platform to configure (web, android, ios, web+android, web+ios)").option("--skip-install", "skip auto-installation of browsers/Appium").option("--force", "overwrite existing files").action(async (opts) => {
976
+ const cmd = new Command2("init").description("Initialize a new agent-qa project").option("--dir <path>", "target directory", process.cwd()).option("--platform <type>", "platform to configure (web, android, ios, web+android, web+ios)").option("--skip-install", "deprecated no-op; setup commands are shown after init").option("--force", "overwrite existing files").action(async (opts) => {
1185
977
  const dir = resolve2(opts.dir);
1186
978
  const force = opts.force ?? false;
1187
- const skipInstall = opts.skipInstall ?? false;
1188
979
  const isInteractive = !opts.platform;
1189
980
  let platform;
1190
981
  let provider = "anthropic-subscription";
@@ -1212,7 +1003,7 @@ function createInitCommand() {
1212
1003
  }) : [];
1213
1004
  platform = { capabilities, mobileRuntimes };
1214
1005
  const llmSetup = await checkbox({
1215
- message: `Subscription auth ${pc4.dim("(optional)")}`,
1006
+ message: `Subscription auth ${pc3.dim("(optional)")}`,
1216
1007
  choices: LLM_SETUP_CHOICES,
1217
1008
  required: true,
1218
1009
  validate: (value) => {
@@ -1247,12 +1038,12 @@ function createInitCommand() {
1247
1038
  provider = "anthropic-subscription";
1248
1039
  model = DEFAULT_ANTHROPIC_MODEL;
1249
1040
  }
1250
- const configPath = join3(dir, "agent-qa.config.yaml");
1041
+ const configPath = join2(dir, "agent-qa.config.yaml");
1251
1042
  if (existsSync(configPath) && !force) {
1252
- console.log(pc4.yellow(`Config file already exists at ${configPath}. Use --force to overwrite.`));
1043
+ console.log(pc3.yellow(`Config file already exists at ${configPath}. Use --force to overwrite.`));
1253
1044
  return;
1254
1045
  }
1255
- const testsDir = join3(dir, "tests");
1046
+ const testsDir = join2(dir, "tests");
1256
1047
  if (!existsSync(testsDir)) {
1257
1048
  mkdirSync(testsDir, { recursive: true });
1258
1049
  }
@@ -1261,119 +1052,111 @@ function createInitCommand() {
1261
1052
  const yamlStr = stringify(config);
1262
1053
  const configContent = addYamlComments(yamlStr);
1263
1054
  writeFileSync(configPath, configContent);
1264
- console.log(pc4.green(`\u2713 Created ${configPath}`));
1055
+ console.log(pc3.green(`\u2713 Created ${configPath}`));
1265
1056
  const subscriptionAuthDependency = usesSubscriptionAuth ? upsertSubscriptionAuthPackageDependency(dir) : null;
1266
1057
  if (subscriptionAuthDependency) {
1267
1058
  const action = subscriptionAuthDependency.created ? "Created" : "Updated";
1268
- console.log(pc4.green(`\u2713 ${action} ${subscriptionAuthDependency.path}`));
1059
+ console.log(pc3.green(`\u2713 ${action} ${subscriptionAuthDependency.path}`));
1269
1060
  }
1270
- const secretsPath = join3(dir, DEFAULT_SECRETS_FILE);
1061
+ const secretsPath = join2(dir, DEFAULT_SECRETS_FILE);
1271
1062
  if (force || !existsSync(secretsPath)) {
1272
1063
  writeFileSync(secretsPath, "");
1273
- console.log(pc4.green(`\u2713 Created ${secretsPath}`));
1064
+ console.log(pc3.green(`\u2713 Created ${secretsPath}`));
1274
1065
  }
1275
- const envPath = join3(dir, DEFAULT_ENV_FILE);
1066
+ const envPath = join2(dir, DEFAULT_ENV_FILE);
1276
1067
  if (force || !existsSync(envPath)) {
1277
1068
  writeFileSync(envPath, "");
1278
- console.log(pc4.green(`\u2713 Created ${envPath}`));
1069
+ console.log(pc3.green(`\u2713 Created ${envPath}`));
1279
1070
  }
1280
- const hooksPath = join3(dir, DEFAULT_HOOKS_FILE);
1071
+ const hooksPath = join2(dir, DEFAULT_HOOKS_FILE);
1281
1072
  if (force || !existsSync(hooksPath)) {
1282
1073
  writeFileSync(hooksPath, buildHooksFile(platform));
1283
- console.log(pc4.green(`\u2713 Created ${hooksPath}`));
1074
+ console.log(pc3.green(`\u2713 Created ${hooksPath}`));
1284
1075
  }
1285
- const agentRulesPath = join3(dir, DEFAULT_AGENT_RULES_FILE);
1076
+ const agentRulesPath = join2(dir, DEFAULT_AGENT_RULES_FILE);
1286
1077
  if (force || !existsSync(agentRulesPath)) {
1287
1078
  writeFileSync(agentRulesPath, "# agent-qa Rules\n");
1288
- console.log(pc4.green(`\u2713 Created ${agentRulesPath}`));
1079
+ console.log(pc3.green(`\u2713 Created ${agentRulesPath}`));
1289
1080
  }
1290
- const localConfigPath = join3(dir, DEFAULT_LOCAL_CONFIG_FILE);
1081
+ const localConfigPath = join2(dir, DEFAULT_LOCAL_CONFIG_FILE);
1291
1082
  if (force || !existsSync(localConfigPath)) {
1292
1083
  writeFileSync(localConfigPath, buildLocalConfigTemplate());
1293
- console.log(pc4.green(`\u2713 Created ${localConfigPath}`));
1084
+ console.log(pc3.green(`\u2713 Created ${localConfigPath}`));
1294
1085
  }
1295
- const examplePassPath = join3(testsDir, "example-pass.yaml");
1086
+ const examplePassPath = join2(testsDir, "example-pass.yaml");
1296
1087
  writeFileSync(examplePassPath, buildExampleTest(platform, "pass"));
1297
- console.log(pc4.green(`\u2713 Created ${examplePassPath}`));
1298
- const exampleFailPath = join3(testsDir, "example-fail.yaml");
1088
+ console.log(pc3.green(`\u2713 Created ${examplePassPath}`));
1089
+ const exampleFailPath = join2(testsDir, "example-fail.yaml");
1299
1090
  writeFileSync(exampleFailPath, buildExampleTest(platform, "fail"));
1300
- console.log(pc4.green(`\u2713 Created ${exampleFailPath}`));
1091
+ console.log(pc3.green(`\u2713 Created ${exampleFailPath}`));
1301
1092
  if (hasWeb(platform)) {
1302
- const suitesDir = join3(dir, "suites");
1093
+ const suitesDir = join2(dir, "suites");
1303
1094
  if (!existsSync(suitesDir)) {
1304
1095
  mkdirSync(suitesDir, { recursive: true });
1305
1096
  }
1306
- const scriptsDir = join3(dir, "scripts");
1097
+ const scriptsDir = join2(dir, "scripts");
1307
1098
  if (!existsSync(scriptsDir)) {
1308
1099
  mkdirSync(scriptsDir, { recursive: true });
1309
1100
  }
1310
- const automationTestsDir = join3(testsDir, "automation-exercise");
1101
+ const automationTestsDir = join2(testsDir, "automation-exercise");
1311
1102
  if (!existsSync(automationTestsDir)) {
1312
1103
  mkdirSync(automationTestsDir, { recursive: true });
1313
1104
  }
1314
1105
  for (const test of AUTOMATION_EXERCISE_TESTS) {
1315
- const testPath = join3(automationTestsDir, test.fileName);
1106
+ const testPath = join2(automationTestsDir, test.fileName);
1316
1107
  writeFileSync(testPath, buildAutomationExerciseTest(test));
1317
- console.log(pc4.green(`\u2713 Created ${testPath}`));
1108
+ console.log(pc3.green(`\u2713 Created ${testPath}`));
1318
1109
  }
1319
- const automationSuitePath = join3(suitesDir, "automation-exercise.suite.yaml");
1110
+ const automationSuitePath = join2(suitesDir, "automation-exercise.suite.yaml");
1320
1111
  writeFileSync(automationSuitePath, buildAutomationExerciseSuite());
1321
- console.log(pc4.green(`\u2713 Created ${automationSuitePath}`));
1322
- const hnScriptPath = join3(scriptsDir, "fetch-hn-top-story.mjs");
1112
+ console.log(pc3.green(`\u2713 Created ${automationSuitePath}`));
1113
+ const hnScriptPath = join2(scriptsDir, "fetch-hn-top-story.mjs");
1323
1114
  writeFileSync(hnScriptPath, buildHackerNewsHookScript());
1324
- console.log(pc4.green(`\u2713 Created ${hnScriptPath}`));
1325
- const hnTestPath = join3(testsDir, "hacker-news-top-story.yaml");
1115
+ console.log(pc3.green(`\u2713 Created ${hnScriptPath}`));
1116
+ const hnTestPath = join2(testsDir, "hacker-news-top-story.yaml");
1326
1117
  writeFileSync(hnTestPath, buildHackerNewsTopStoryTest());
1327
- console.log(pc4.green(`\u2713 Created ${hnTestPath}`));
1118
+ console.log(pc3.green(`\u2713 Created ${hnTestPath}`));
1328
1119
  }
1329
1120
  appendGitignore(dir);
1330
- console.log(pc4.green("\u2713 Updated .gitignore"));
1331
- if (!skipInstall) {
1332
- installPlaywrightBrowsers(platform);
1333
- installAppium(platform);
1334
- }
1121
+ console.log(pc3.green("\u2713 Updated .gitignore"));
1335
1122
  console.log("");
1336
- console.log(pc4.green(pc4.bold("\u2713 agent-qa project initialized!")));
1123
+ console.log(pc3.green(pc3.bold("\u2713 agent-qa project initialized!")));
1337
1124
  console.log("");
1338
1125
  console.log(" Created:");
1339
- console.log(` ${pc4.dim("\u2022")} agent-qa.config.yaml`);
1126
+ console.log(` ${pc3.dim("\u2022")} agent-qa.config.yaml`);
1340
1127
  if (subscriptionAuthDependency) {
1341
- console.log(` ${pc4.dim("\u2022")} ${PACKAGE_JSON_FILE} dependency for ${SUBSCRIPTION_AUTH_PACKAGE}@${subscriptionAuthDependency.range}`);
1342
- }
1343
- console.log(` ${pc4.dim("\u2022")} ${DEFAULT_HOOKS_FILE}`);
1344
- console.log(` ${pc4.dim("\u2022")} ${DEFAULT_AGENT_RULES_FILE}`);
1345
- console.log(` ${pc4.dim("\u2022")} ${DEFAULT_ENV_FILE}`);
1346
- console.log(` ${pc4.dim("\u2022")} ${DEFAULT_SECRETS_FILE}`);
1347
- console.log(` ${pc4.dim("\u2022")} ${DEFAULT_LOCAL_CONFIG_FILE}`);
1348
- console.log(` ${pc4.dim("\u2022")} tests/example-pass.yaml`);
1349
- console.log(` ${pc4.dim("\u2022")} tests/example-fail.yaml`);
1128
+ console.log(` ${pc3.dim("\u2022")} ${PACKAGE_JSON_FILE} dependency for ${SUBSCRIPTION_AUTH_PACKAGE}@${subscriptionAuthDependency.range}`);
1129
+ }
1130
+ console.log(` ${pc3.dim("\u2022")} ${DEFAULT_HOOKS_FILE}`);
1131
+ console.log(` ${pc3.dim("\u2022")} ${DEFAULT_AGENT_RULES_FILE}`);
1132
+ console.log(` ${pc3.dim("\u2022")} ${DEFAULT_ENV_FILE}`);
1133
+ console.log(` ${pc3.dim("\u2022")} ${DEFAULT_SECRETS_FILE}`);
1134
+ console.log(` ${pc3.dim("\u2022")} ${DEFAULT_LOCAL_CONFIG_FILE}`);
1135
+ console.log(` ${pc3.dim("\u2022")} tests/example-pass.yaml`);
1136
+ console.log(` ${pc3.dim("\u2022")} tests/example-fail.yaml`);
1350
1137
  if (hasWeb(platform)) {
1351
- console.log(` ${pc4.dim("\u2022")} tests/automation-exercise/*.yaml`);
1352
- console.log(` ${pc4.dim("\u2022")} tests/hacker-news-top-story.yaml`);
1353
- console.log(` ${pc4.dim("\u2022")} suites/automation-exercise.suite.yaml`);
1354
- console.log(` ${pc4.dim("\u2022")} scripts/fetch-hn-top-story.mjs`);
1138
+ console.log(` ${pc3.dim("\u2022")} tests/automation-exercise/*.yaml`);
1139
+ console.log(` ${pc3.dim("\u2022")} tests/hacker-news-top-story.yaml`);
1140
+ console.log(` ${pc3.dim("\u2022")} suites/automation-exercise.suite.yaml`);
1141
+ console.log(` ${pc3.dim("\u2022")} scripts/fetch-hn-top-story.mjs`);
1355
1142
  }
1356
- console.log(` ${pc4.dim("\u2022")} .gitignore entries`);
1143
+ console.log(` ${pc3.dim("\u2022")} .gitignore entries`);
1357
1144
  console.log("");
1358
1145
  console.log(" Next steps:");
1359
- console.log(` 1. ${pc4.cyan("agent-qa doctor")} Verify your environment`);
1360
- console.log(` 2. Edit ${pc4.cyan("tests/example-pass.yaml")} with your first test`);
1361
- console.log(` 3. ${pc4.cyan("agent-qa run")} Run all tests`);
1362
- console.log(` 4. ${pc4.cyan("agent-qa dashboard")} View results in browser`);
1363
- if (skipInstall && hasWeb(platform)) {
1364
- console.log(` ${pc4.dim("\u2022")} Install browser support with ${pc4.cyan("agent-qa install-browsers --chromium")}`);
1146
+ for (const [index, step] of initNextSteps(platform).entries()) {
1147
+ console.log(` ${index + 1}. ${pc3.cyan(step.command)} ${step.description}`);
1365
1148
  }
1366
1149
  const generatedLLMs = config.registry?.llms ?? [];
1367
1150
  const subscriptionLLMs = generatedLLMs.filter((llm) => isSubscriptionProvider(String(llm.provider)));
1368
1151
  if (subscriptionLLMs.length === 0) {
1369
- console.log(` ${pc4.dim("\u2022")} Save credentials with ${pc4.cyan("agent-qa auth set --config default --type api-key")}`);
1152
+ console.log(` ${pc3.dim("\u2022")} Save credentials with ${pc3.cyan("agent-qa auth set --config default --type api-key")}`);
1370
1153
  if (provider === "anthropic-compatible") {
1371
- console.log(` ${pc4.dim("\u2022")} Bearer tokens use ${pc4.cyan("agent-qa auth set --config default --type bearer-token")}`);
1154
+ console.log(` ${pc3.dim("\u2022")} Bearer tokens use ${pc3.cyan("agent-qa auth set --config default --type bearer-token")}`);
1372
1155
  }
1373
1156
  } else {
1374
- console.log(` ${pc4.dim("\u2022")} Fetch ${SUBSCRIPTION_AUTH_PACKAGE} with your package manager install command`);
1157
+ console.log(` ${pc3.dim("\u2022")} Fetch ${SUBSCRIPTION_AUTH_PACKAGE} with your package manager install command`);
1375
1158
  for (const llm of subscriptionLLMs) {
1376
- console.log(` ${pc4.dim("\u2022")} Authenticate ${pc4.cyan(String(llm.name))} from ${pc4.cyan("agent-qa dashboard")}`);
1159
+ console.log(` ${pc3.dim("\u2022")} Authenticate ${pc3.cyan(String(llm.name))} from ${pc3.cyan("agent-qa dashboard")}`);
1377
1160
  }
1378
1161
  }
1379
1162
  console.log("");
@@ -1381,16 +1164,369 @@ function createInitCommand() {
1381
1164
  return cmd;
1382
1165
  }
1383
1166
 
1167
+ // src/commands/install-browsers.ts
1168
+ import { spawnSync } from "child_process";
1169
+ import { createRequire } from "module";
1170
+ import { dirname as dirname2, join as join3 } from "path";
1171
+ import { Command as Command3 } from "commander";
1172
+ import pc4 from "picocolors";
1173
+ function selectedBrowserCount(selection) {
1174
+ return [selection.chromium, selection.firefox, selection.webkit].filter(Boolean).length;
1175
+ }
1176
+ function validateBrowserInstallSelection(selection) {
1177
+ if (!selection.all && selectedBrowserCount(selection) === 0) {
1178
+ return "Select at least one browser flag: --all, --chromium, --firefox, or --webkit.";
1179
+ }
1180
+ if (selection.all && selectedBrowserCount(selection) > 0) {
1181
+ return "Cannot combine --all with --chromium, --firefox, or --webkit.";
1182
+ }
1183
+ return null;
1184
+ }
1185
+ function buildBrowserInstallArgs(selection) {
1186
+ const args = [];
1187
+ if (selection.withDeps) args.push("--with-deps");
1188
+ if (selection.force) args.push("--force");
1189
+ if (!selection.all) {
1190
+ if (selection.chromium) args.push("chromium");
1191
+ if (selection.firefox) args.push("firefox");
1192
+ if (selection.webkit) args.push("webkit");
1193
+ }
1194
+ return args;
1195
+ }
1196
+ function formatInstallBrowsersRetryCommand(selection) {
1197
+ const args = ["agent-qa", "install-browsers"];
1198
+ if (selection.all) {
1199
+ args.push("--all");
1200
+ } else {
1201
+ if (selection.chromium) args.push("--chromium");
1202
+ if (selection.firefox) args.push("--firefox");
1203
+ if (selection.webkit) args.push("--webkit");
1204
+ }
1205
+ if (selection.withDeps) args.push("--with-deps");
1206
+ if (selection.force) args.push("--force");
1207
+ return args.join(" ");
1208
+ }
1209
+ function resolvePlaywrightCoreCli() {
1210
+ const require2 = createRequire(import.meta.url);
1211
+ const webEntry = require2.resolve("@vostride/agent-qa-web");
1212
+ const webRequire = createRequire(webEntry);
1213
+ const packagePath = webRequire.resolve("playwright-core/package.json");
1214
+ return join3(dirname2(packagePath), "cli.js");
1215
+ }
1216
+ function normalizeSpawnStatus(result) {
1217
+ if (typeof result.status === "number") return result.status;
1218
+ return 1;
1219
+ }
1220
+ function runBrowserInstall(selection, deps = {}) {
1221
+ let cliPath;
1222
+ try {
1223
+ cliPath = (deps.resolveCli ?? resolvePlaywrightCoreCli)();
1224
+ } catch (error) {
1225
+ return { ok: false, status: 1, stage: "resolve", error };
1226
+ }
1227
+ const spawn = deps.spawn ?? spawnSync;
1228
+ const result = spawn(process.execPath, [cliPath, "install", ...buildBrowserInstallArgs(selection)], {
1229
+ stdio: "inherit"
1230
+ });
1231
+ if (result.error) {
1232
+ return { ok: false, status: normalizeSpawnStatus(result), stage: "spawn", error: result.error };
1233
+ }
1234
+ const status = normalizeSpawnStatus(result);
1235
+ return { ok: status === 0, status, stage: "installer" };
1236
+ }
1237
+ function printBrowserInstallFailure(selection, result) {
1238
+ if (result.stage === "resolve") {
1239
+ console.error(pc4.red("agent-qa could not find its bundled browser installer."));
1240
+ console.error(pc4.dim("Reinstall agent-qa dependencies, then retry:"));
1241
+ } else {
1242
+ console.error(pc4.red("Browser installation failed."));
1243
+ console.error(pc4.dim("Retry:"));
1244
+ }
1245
+ console.error(` ${pc4.cyan(formatInstallBrowsersRetryCommand(selection))}`);
1246
+ }
1247
+ function createInstallBrowsersCommand(deps = {}) {
1248
+ const cmd = new Command3("install-browsers").description("Install browser support for agent-qa").option("--all", "install all Playwright-managed browsers for agent-qa").option("--chromium", "install Chromium browser support").option("--firefox", "install Firefox browser support").option("--webkit", "install WebKit browser support").option("--with-deps", "also install required system dependencies").option("--force", "force reinstall even if browser support already exists").action((opts) => {
1249
+ const selection = {
1250
+ all: !!opts.all,
1251
+ chromium: !!opts.chromium,
1252
+ firefox: !!opts.firefox,
1253
+ webkit: !!opts.webkit,
1254
+ withDeps: !!opts.withDeps,
1255
+ force: !!opts.force
1256
+ };
1257
+ const validationError = validateBrowserInstallSelection(selection);
1258
+ if (validationError) {
1259
+ console.error(pc4.red(`Error: ${validationError}`));
1260
+ console.error(cmd.helpInformation());
1261
+ process.exitCode = 1;
1262
+ return;
1263
+ }
1264
+ console.log(pc4.blue("Installing agent-qa browser support..."));
1265
+ const result = runBrowserInstall(selection, deps);
1266
+ if (result.ok) {
1267
+ console.log(pc4.green("\u2713 agent-qa browser support installed"));
1268
+ return;
1269
+ }
1270
+ printBrowserInstallFailure(selection, result);
1271
+ process.exitCode = result.status || 1;
1272
+ });
1273
+ return cmd;
1274
+ }
1275
+
1276
+ // src/commands/install-mobile-drivers.ts
1277
+ import { execFileSync } from "child_process";
1278
+ import { dirname as dirname3, resolve as resolve3 } from "path";
1279
+ import { Command as Command4 } from "commander";
1280
+ import pc5 from "picocolors";
1281
+ import {
1282
+ formatAppiumInstallGuidance,
1283
+ resolveAppiumExecutable
1284
+ } from "@vostride/agent-qa-core";
1285
+ var MOBILE_DRIVERS = [
1286
+ { target: "android", driver: "uiautomator2", label: "UiAutomator2" },
1287
+ { target: "ios", driver: "xcuitest", label: "XCUITest" }
1288
+ ];
1289
+ function selectedTargetCount(selection) {
1290
+ return [selection.android, selection.ios].filter(Boolean).length;
1291
+ }
1292
+ function selectedDrivers(selection) {
1293
+ if (selection.all) return MOBILE_DRIVERS;
1294
+ return MOBILE_DRIVERS.filter((driver) => selection[driver.target]);
1295
+ }
1296
+ function validateMobileDriverInstallSelection(selection) {
1297
+ if (!selection.all && selectedTargetCount(selection) === 0) {
1298
+ return "Select at least one mobile platform flag: --all, --android, or --ios.";
1299
+ }
1300
+ if (selection.all && selectedTargetCount(selection) > 0) {
1301
+ return "Cannot combine --all with --android or --ios.";
1302
+ }
1303
+ if (selection.unsafe && !selection.update) {
1304
+ return "Cannot use --unsafe without --update.";
1305
+ }
1306
+ return null;
1307
+ }
1308
+ function formatInstallMobileDriversRetryCommand(selection) {
1309
+ const args = ["agent-qa", "install-mobile-drivers"];
1310
+ if (selection.all) {
1311
+ args.push("--all");
1312
+ } else {
1313
+ if (selection.android) args.push("--android");
1314
+ if (selection.ios) args.push("--ios");
1315
+ }
1316
+ if (selection.update) args.push("--update");
1317
+ if (selection.unsafe) args.push("--unsafe");
1318
+ return args.join(" ");
1319
+ }
1320
+ function collectDriverNames(value, names) {
1321
+ if (!value) return;
1322
+ if (typeof value === "string") {
1323
+ names.add(value);
1324
+ return;
1325
+ }
1326
+ if (Array.isArray(value)) {
1327
+ for (const item of value) collectDriverNames(item, names);
1328
+ return;
1329
+ }
1330
+ if (typeof value !== "object") return;
1331
+ const record = value;
1332
+ if (typeof record.name === "string") names.add(record.name);
1333
+ if (typeof record.driverName === "string") names.add(record.driverName);
1334
+ if (typeof record.automationName === "string") names.add(record.automationName);
1335
+ for (const [key, child] of Object.entries(record)) {
1336
+ if (key !== "drivers" && key !== "installed" && key !== "name" && key !== "driverName" && typeof child === "object" && child !== null) {
1337
+ names.add(key);
1338
+ }
1339
+ collectDriverNames(child, names);
1340
+ }
1341
+ }
1342
+ function parseInstalledAppiumDrivers(output) {
1343
+ const names = /* @__PURE__ */ new Set();
1344
+ const trimmed = output.trim();
1345
+ if (!trimmed) return names;
1346
+ try {
1347
+ collectDriverNames(JSON.parse(trimmed), names);
1348
+ const normalizedNames = /* @__PURE__ */ new Set();
1349
+ for (const name of names) {
1350
+ const normalized = name.toLowerCase();
1351
+ normalizedNames.add(normalized);
1352
+ if (normalized.includes("uiautomator2")) normalizedNames.add("uiautomator2");
1353
+ if (normalized.includes("xcuitest")) normalizedNames.add("xcuitest");
1354
+ }
1355
+ return normalizedNames;
1356
+ } catch {
1357
+ for (const line of trimmed.split("\n")) {
1358
+ const normalized = line.toLowerCase();
1359
+ if (normalized.includes("uiautomator2")) names.add("uiautomator2");
1360
+ if (normalized.includes("xcuitest")) names.add("xcuitest");
1361
+ }
1362
+ return names;
1363
+ }
1364
+ }
1365
+ function errorStatus(error) {
1366
+ const status = error?.status;
1367
+ return typeof status === "number" ? status : 1;
1368
+ }
1369
+ function isMissingExecutableError(error) {
1370
+ return error?.code === "ENOENT";
1371
+ }
1372
+ function isAlreadyInstalledDriverError(error, driver) {
1373
+ const details = [
1374
+ error instanceof Error ? error.message : String(error),
1375
+ error?.stdout,
1376
+ error?.stderr
1377
+ ].map(String).join("\n").toLowerCase();
1378
+ return details.includes(`driver named "${driver}" is already installed`) || details.includes(`${driver}" is already installed`) || details.includes(`${driver} is already installed`);
1379
+ }
1380
+ function getInstalledAppiumDrivers(appiumCommand, execFile) {
1381
+ try {
1382
+ const output = execFile(appiumCommand, ["driver", "list", "--installed", "--json"], { stdio: "pipe", encoding: "utf-8" });
1383
+ return { ok: true, drivers: parseInstalledAppiumDrivers(String(output)) };
1384
+ } catch {
1385
+ try {
1386
+ const output = execFile(appiumCommand, ["driver", "list", "--installed"], { stdio: "pipe", encoding: "utf-8" });
1387
+ return { ok: true, drivers: parseInstalledAppiumDrivers(String(output)) };
1388
+ } catch (error) {
1389
+ return { ok: false, error, status: errorStatus(error) };
1390
+ }
1391
+ }
1392
+ }
1393
+ function driverOperationArgs(driver, selection, installed) {
1394
+ if (installed && !selection.update) {
1395
+ return { action: "reuse", args: null };
1396
+ }
1397
+ if (installed && selection.update) {
1398
+ const args = ["driver", "update", driver];
1399
+ if (selection.unsafe) args.push("--unsafe");
1400
+ return { action: "update", args };
1401
+ }
1402
+ return { action: "install", args: ["driver", "install", driver] };
1403
+ }
1404
+ function runMobileDriverInstall(selection, deps = {}) {
1405
+ const resolveAppium = deps.resolveAppium ?? resolveAppiumExecutable;
1406
+ const execFile = deps.execFile ?? execFileSync;
1407
+ const events = [];
1408
+ let appium;
1409
+ try {
1410
+ appium = resolveAppium({ cwd: selection.cwd ?? process.cwd() });
1411
+ } catch (error) {
1412
+ return { ok: false, status: 1, stage: "resolve", events, error };
1413
+ }
1414
+ try {
1415
+ execFile(appium.command, ["--version"], { stdio: "pipe" });
1416
+ } catch (error) {
1417
+ return { ok: false, status: errorStatus(error), stage: "check", appium, events, error };
1418
+ }
1419
+ const installedResult = getInstalledAppiumDrivers(appium.command, execFile);
1420
+ if (!installedResult.ok) {
1421
+ return { ok: false, status: installedResult.status, stage: "list", appium, events, error: installedResult.error };
1422
+ }
1423
+ let status = 0;
1424
+ for (const definition of selectedDrivers(selection)) {
1425
+ const installed = installedResult.drivers.has(definition.driver);
1426
+ const operation = driverOperationArgs(definition.driver, selection, installed);
1427
+ if (!operation.args) {
1428
+ events.push({ driver: definition.driver, label: definition.label, action: operation.action, ok: true });
1429
+ continue;
1430
+ }
1431
+ try {
1432
+ execFile(appium.command, operation.args, { stdio: "inherit" });
1433
+ events.push({ driver: definition.driver, label: definition.label, action: operation.action, ok: true });
1434
+ } catch (error) {
1435
+ if (operation.action === "install" && isAlreadyInstalledDriverError(error, definition.driver)) {
1436
+ events.push({ driver: definition.driver, label: definition.label, action: "reuse", ok: true });
1437
+ continue;
1438
+ }
1439
+ status = status || errorStatus(error);
1440
+ events.push({ driver: definition.driver, label: definition.label, action: operation.action, ok: false, error });
1441
+ }
1442
+ }
1443
+ return { ok: status === 0, status, stage: "driver", appium, events };
1444
+ }
1445
+ function resolveCommandCwd(command) {
1446
+ const configPath = command.parent?.opts().config ?? "agent-qa.config.yaml";
1447
+ return dirname3(resolve3(configPath));
1448
+ }
1449
+ function printMobileDriverInstallResult(selection, result) {
1450
+ if (result.stage === "check" && isMissingExecutableError(result.error)) {
1451
+ console.error(pc5.red("Appium not found."));
1452
+ console.error(pc5.dim(formatAppiumInstallGuidance()));
1453
+ return;
1454
+ }
1455
+ if (result.stage === "resolve") {
1456
+ console.error(pc5.red("agent-qa could not resolve an Appium executable."));
1457
+ console.error(pc5.dim(formatAppiumInstallGuidance()));
1458
+ return;
1459
+ }
1460
+ if (result.stage === "check") {
1461
+ console.error(pc5.red("Could not run Appium."));
1462
+ console.error(pc5.dim(formatAppiumInstallGuidance()));
1463
+ return;
1464
+ }
1465
+ if (result.stage === "list") {
1466
+ console.error(pc5.red("Could not inspect installed Appium drivers."));
1467
+ console.error(pc5.dim("Retry:"));
1468
+ console.error(` ${pc5.cyan(formatInstallMobileDriversRetryCommand(selection))}`);
1469
+ return;
1470
+ }
1471
+ for (const event of result.events) {
1472
+ if (event.ok && event.action === "reuse") {
1473
+ console.log(pc5.green(`\u2713 ${event.label} driver already installed`));
1474
+ } else if (event.ok && event.action === "install") {
1475
+ console.log(pc5.green(`\u2713 ${event.label} driver installed`));
1476
+ } else if (event.ok && event.action === "update") {
1477
+ console.log(pc5.green(`\u2713 ${event.label} driver updated`));
1478
+ } else {
1479
+ const action = event.action === "update" ? "update" : "installation";
1480
+ console.error(pc5.red(`\u2717 ${event.label} driver ${action} failed`));
1481
+ }
1482
+ }
1483
+ if (!result.ok) {
1484
+ console.error(pc5.dim("Retry:"));
1485
+ console.error(` ${pc5.cyan(formatInstallMobileDriversRetryCommand(selection))}`);
1486
+ return;
1487
+ }
1488
+ console.log(pc5.green("\u2713 agent-qa mobile driver support ready"));
1489
+ }
1490
+ function createInstallMobileDriversCommand(deps = {}) {
1491
+ const cmd = new Command4("install-mobile-drivers").description("Install Appium mobile drivers for agent-qa").option("--all", "install Android and iOS Appium drivers").option("--android", "install the UiAutomator2 Appium driver").option("--ios", "install the XCUITest Appium driver").option("--update", "update selected drivers when they are already installed").option("--unsafe", "allow Appium driver updates across major versions; requires --update").action((opts) => {
1492
+ const selection = {
1493
+ all: !!opts.all,
1494
+ android: !!opts.android,
1495
+ ios: !!opts.ios,
1496
+ update: !!opts.update,
1497
+ unsafe: !!opts.unsafe,
1498
+ cwd: resolveCommandCwd(cmd)
1499
+ };
1500
+ const validationError = validateMobileDriverInstallSelection(selection);
1501
+ if (validationError) {
1502
+ console.error(pc5.red(`Error: ${validationError}`));
1503
+ console.error(cmd.helpInformation());
1504
+ process.exitCode = 1;
1505
+ return;
1506
+ }
1507
+ console.log(pc5.blue("Installing agent-qa mobile driver support..."));
1508
+ const result = runMobileDriverInstall(selection, deps);
1509
+ printMobileDriverInstallResult(selection, result);
1510
+ if (!result.ok) {
1511
+ process.exitCode = result.status || 1;
1512
+ }
1513
+ });
1514
+ return cmd;
1515
+ }
1516
+
1384
1517
  // src/commands/doctor.ts
1385
1518
  init_config();
1386
1519
  init_llm_utils();
1387
1520
  init_model_defaults();
1388
- import { Command as Command4 } from "commander";
1389
- import { execSync as execSync2 } from "child_process";
1521
+ import { Command as Command5 } from "commander";
1522
+ import { execFileSync as execFileSync2, execSync } from "child_process";
1390
1523
  import { existsSync as existsSync2 } from "fs";
1391
- import pc5 from "picocolors";
1524
+ import { dirname as dirname5, resolve as resolve4 } from "path";
1525
+ import pc6 from "picocolors";
1392
1526
  import {
1393
1527
  discoverWorkspaceFiles,
1528
+ formatAppiumInstallGuidance as formatAppiumInstallGuidance2,
1529
+ resolveAppiumExecutable as resolveAppiumExecutable2,
1394
1530
  resolveWorkspacePaths
1395
1531
  } from "@vostride/agent-qa-core";
1396
1532
  function resolveDoctorWorkspace(config, configPath) {
@@ -1451,20 +1587,20 @@ function applyResolvedAuth(llm, auth) {
1451
1587
  function formatStatus(status) {
1452
1588
  switch (status) {
1453
1589
  case "pass":
1454
- return pc5.green("PASS");
1590
+ return pc6.green("PASS");
1455
1591
  case "warn":
1456
- return pc5.yellow("WARN");
1592
+ return pc6.yellow("WARN");
1457
1593
  case "fail":
1458
- return pc5.red("FAIL");
1594
+ return pc6.red("FAIL");
1459
1595
  case "skip":
1460
- return pc5.dim("SKIP");
1596
+ return pc6.dim("SKIP");
1461
1597
  }
1462
1598
  }
1463
1599
  function padDots(name, message) {
1464
1600
  const totalWidth = 60;
1465
1601
  const used = name.length + message.length;
1466
1602
  const dots = Math.max(2, totalWidth - used);
1467
- return pc5.dim(".".repeat(dots));
1603
+ return pc6.dim(".".repeat(dots));
1468
1604
  }
1469
1605
  function checkNodeVersion() {
1470
1606
  return {
@@ -1694,7 +1830,7 @@ function checkLLMConnection(config) {
1694
1830
  function checkPlaywright(config) {
1695
1831
  return {
1696
1832
  name: "Playwright",
1697
- fixInstructions: "Run `agent-qa install-browsers --chromium` or `agent-qa init`",
1833
+ fixInstructions: "Run `agent-qa install-browsers --chromium`",
1698
1834
  check: async () => {
1699
1835
  const browsers = config?.browsers;
1700
1836
  const hasBrowsers = browsers && browsers.length > 0;
@@ -1702,7 +1838,7 @@ function checkPlaywright(config) {
1702
1838
  return { status: "skip", message: "not configured" };
1703
1839
  }
1704
1840
  try {
1705
- execSync2("npx playwright --version", { stdio: "pipe" });
1841
+ execSync("npx playwright --version", { stdio: "pipe" });
1706
1842
  return { status: "pass", message: "installed" };
1707
1843
  } catch {
1708
1844
  return { status: "fail", message: "not installed" };
@@ -1710,16 +1846,28 @@ function checkPlaywright(config) {
1710
1846
  }
1711
1847
  };
1712
1848
  }
1713
- function checkAppium(config) {
1849
+ function appiumResolverCwd(configPath) {
1850
+ return dirname5(resolve4(configPath));
1851
+ }
1852
+ function mobileDriverInstallCommand(config) {
1853
+ const hasAndroid = hasAndroidPlatform(config);
1854
+ const hasIOS = hasIOSPlatform(config);
1855
+ if (hasAndroid && hasIOS) return "agent-qa install-mobile-drivers --all";
1856
+ if (hasAndroid) return "agent-qa install-mobile-drivers --android";
1857
+ if (hasIOS) return "agent-qa install-mobile-drivers --ios";
1858
+ return "agent-qa install-mobile-drivers --all";
1859
+ }
1860
+ function checkAppium(config, configPath) {
1714
1861
  return {
1715
1862
  name: "Appium",
1716
- fixInstructions: "Run `npm install -g appium`",
1863
+ fixInstructions: `${formatAppiumInstallGuidance2()} Then run \`${mobileDriverInstallCommand(config)}\`.`,
1717
1864
  check: async () => {
1718
1865
  if (!isMobilePlatform(config)) {
1719
1866
  return { status: "skip", message: "not configured" };
1720
1867
  }
1721
1868
  try {
1722
- const version = execSync2("appium --version", { stdio: "pipe" }).toString().trim();
1869
+ const appium = resolveAppiumExecutable2({ cwd: appiumResolverCwd(configPath) });
1870
+ const version = execFileSync2(appium.command, ["--version"], { stdio: "pipe" }).toString().trim();
1723
1871
  return { status: "pass", message: `v${version}` };
1724
1872
  } catch {
1725
1873
  return { status: "fail", message: "not installed" };
@@ -1727,16 +1875,17 @@ function checkAppium(config) {
1727
1875
  }
1728
1876
  };
1729
1877
  }
1730
- function checkAppiumDrivers(config) {
1878
+ function checkAppiumDrivers(config, configPath) {
1731
1879
  return {
1732
1880
  name: "Appium drivers",
1733
- fixInstructions: "Run `appium driver install uiautomator2` or `appium driver install xcuitest`",
1881
+ fixInstructions: `Run \`${mobileDriverInstallCommand(config)}\``,
1734
1882
  check: async () => {
1735
1883
  if (!isMobilePlatform(config)) {
1736
1884
  return { status: "skip", message: "not configured" };
1737
1885
  }
1738
1886
  try {
1739
- const output = execSync2("appium driver list --installed", { stdio: "pipe" }).toString();
1887
+ const appium = resolveAppiumExecutable2({ cwd: appiumResolverCwd(configPath) });
1888
+ const output = execFileSync2(appium.command, ["driver", "list", "--installed"], { stdio: "pipe" }).toString();
1740
1889
  const hasDrivers = output.includes("uiautomator2") || output.includes("xcuitest");
1741
1890
  if (hasDrivers) {
1742
1891
  return { status: "pass", message: "drivers installed" };
@@ -1780,7 +1929,7 @@ function checkXcode(config) {
1780
1929
  return { status: "fail", message: "iOS testing requires macOS" };
1781
1930
  }
1782
1931
  try {
1783
- execSync2("xcrun simctl list", { stdio: "pipe" });
1932
+ execSync("xcrun simctl list", { stdio: "pipe" });
1784
1933
  return { status: "pass", message: "simctl available" };
1785
1934
  } catch {
1786
1935
  return { status: "fail", message: "simctl not available" };
@@ -1872,7 +2021,7 @@ function getGlobalConfigPath(command) {
1872
2021
  return command.parent?.opts().config ?? "agent-qa.config.yaml";
1873
2022
  }
1874
2023
  function createDoctorCommand() {
1875
- const cmd = new Command4("doctor").description("Validate environment and dependencies").action(async function() {
2024
+ const cmd = new Command5("doctor").description("Validate environment and dependencies").action(async function() {
1876
2025
  const configPath = getGlobalConfigPath(this);
1877
2026
  let config = null;
1878
2027
  try {
@@ -1894,15 +2043,15 @@ function createDoctorCommand() {
1894
2043
  checkSubscriptionAuth(config),
1895
2044
  checkLLMConnection(config),
1896
2045
  checkPlaywright(config),
1897
- checkAppium(config),
1898
- checkAppiumDrivers(config),
2046
+ checkAppium(config, configPath),
2047
+ checkAppiumDrivers(config, configPath),
1899
2048
  checkAndroidSDK(config),
1900
2049
  checkXcode(config),
1901
2050
  checkTestDiscovery(config, configPath),
1902
2051
  checkValidation(config, configPath)
1903
2052
  ];
1904
2053
  console.log("");
1905
- console.log(pc5.bold(" agent-qa doctor"));
2054
+ console.log(pc6.bold(" agent-qa doctor"));
1906
2055
  console.log("");
1907
2056
  let hasFailure = false;
1908
2057
  const results = [];
@@ -1913,16 +2062,16 @@ function createDoctorCommand() {
1913
2062
  const status = formatStatus(result.status);
1914
2063
  console.log(` ${check.name} ${dots} ${result.message} ${status}`);
1915
2064
  if (result.status === "fail") {
1916
- console.log(pc5.dim(` Fix: ${check.fixInstructions}`));
2065
+ console.log(pc6.dim(` Fix: ${check.fixInstructions}`));
1917
2066
  hasFailure = true;
1918
2067
  }
1919
2068
  }
1920
2069
  console.log("");
1921
2070
  if (hasFailure) {
1922
- console.log(pc5.red(" Some checks failed. See fix instructions above."));
2071
+ console.log(pc6.red(" Some checks failed. See fix instructions above."));
1923
2072
  process.exitCode = 1;
1924
2073
  } else {
1925
- console.log(pc5.green(" All checks passed! Your environment is ready."));
2074
+ console.log(pc6.green(" All checks passed! Your environment is ready."));
1926
2075
  }
1927
2076
  console.log("");
1928
2077
  });
@@ -1933,8 +2082,8 @@ function createDoctorCommand() {
1933
2082
  init_config();
1934
2083
  import { randomUUID } from "crypto";
1935
2084
  import { resolve as resolvePath3 } from "path";
1936
- import { Command as Command5 } from "commander";
1937
- import pc6 from "picocolors";
2085
+ import { Command as Command6 } from "commander";
2086
+ import pc7 from "picocolors";
1938
2087
  import { ATTR_RUNNER, ATTR_TRIGGER, buildInternalRunAttributes, DEFAULT_AGENT_QA_ARTIFACTS_DIR, DEFAULT_AGENT_QA_CACHE_DIR as DEFAULT_AGENT_QA_CACHE_DIR3, DEFAULT_AGENT_QA_VIDEOS_DIR, formatRunAttributesBlock, generateRunId, mergeRunAttributes, MobileSetupError, parseRunAttrFlags, resolveMemoryRoot, resolveMobileRunConfig, validateTrustedRunAttributes } from "@vostride/agent-qa-core";
1939
2088
  import { discoverWorkspaceFiles as discoverWorkspaceFiles2, isWorkspacePathMatch, resolveWorkspaceFileTarget, resolveWorkspacePaths as resolveWorkspacePaths2 } from "@vostride/agent-qa-core";
1940
2089
 
@@ -2277,14 +2426,14 @@ async function captureSingleAnalyticsResult(reporter, result) {
2277
2426
  function formatParseErrors(errors) {
2278
2427
  for (const err of errors) {
2279
2428
  const location = `${err.file}:${err.line}:${err.column}`;
2280
- const severity = err.severity === "error" ? pc6.red("error") : pc6.yellow("warning");
2429
+ const severity = err.severity === "error" ? pc7.red("error") : pc7.yellow("warning");
2281
2430
  console.error(`${severity}: ${err.message}`);
2282
- console.error(` ${pc6.blue("-->")} ${location}`);
2431
+ console.error(` ${pc7.blue("-->")} ${location}`);
2283
2432
  if (err.source) {
2284
- console.error(` ${pc6.blue("|")} ${err.source}`);
2433
+ console.error(` ${pc7.blue("|")} ${err.source}`);
2285
2434
  }
2286
2435
  if (err.suggestion) {
2287
- console.error(` ${pc6.blue("=")} ${pc6.bold("help")}: ${err.suggestion}`);
2436
+ console.error(` ${pc7.blue("=")} ${pc7.bold("help")}: ${err.suggestion}`);
2288
2437
  }
2289
2438
  console.error();
2290
2439
  }
@@ -2300,14 +2449,14 @@ function printRunId(runId) {
2300
2449
  }
2301
2450
  function printMemoryStatus(log) {
2302
2451
  const parts = [];
2303
- if (log.added > 0) parts.push(pc6.green(`${log.added} added`));
2452
+ if (log.added > 0) parts.push(pc7.green(`${log.added} added`));
2304
2453
  if (log.confirmed > 0) parts.push(`${log.confirmed} confirmed`);
2305
- if (log.deprecated > 0) parts.push(pc6.yellow(`${log.deprecated} deprecated`));
2454
+ if (log.deprecated > 0) parts.push(pc7.yellow(`${log.deprecated} deprecated`));
2306
2455
  if (parts.length === 0) parts.push("no changes");
2307
2456
  const secs = log.curatorDuration >= 1e3 ? `${Math.round(log.curatorDuration / 1e3)}s` : `${log.curatorDuration}ms`;
2308
- console.log(` ${pc6.dim("Memory:")} ${parts.join(", ")} ${pc6.dim(`(${secs})`)}`);
2457
+ console.log(` ${pc7.dim("Memory:")} ${parts.join(", ")} ${pc7.dim(`(${secs})`)}`);
2309
2458
  for (const err of log.errors) {
2310
- console.log(` ${pc6.dim("Memory:")} ${pc6.yellow(`warning -- ${err}`)}`);
2459
+ console.log(` ${pc7.dim("Memory:")} ${pc7.yellow(`warning -- ${err}`)}`);
2311
2460
  }
2312
2461
  }
2313
2462
  function isGlobLikePattern(value) {
@@ -2379,21 +2528,21 @@ function buildHooksArtifact(hooks) {
2379
2528
  async function loadRuntimeSecrets(input3) {
2380
2529
  const configuredSecretsFile = input3.config.workspace?.secretsFile;
2381
2530
  if (typeof configuredSecretsFile !== "string" || configuredSecretsFile.trim().length === 0) {
2382
- console.error(pc6.red("Error: workspace.secretsFile is required. Run `agent-qa init` or set it in agent-qa.config.yaml."));
2531
+ console.error(pc7.red("Error: workspace.secretsFile is required. Run `agent-qa init` or set it in agent-qa.config.yaml."));
2383
2532
  process.exit(1);
2384
2533
  }
2385
2534
  const secretsPath = input3.secretsFilePath ?? resolvePath3(input3.configDir, configuredSecretsFile);
2386
2535
  if (!input3.existsSync(secretsPath)) {
2387
- console.error(pc6.red(`Error: Secrets file not found: ${secretsPath}`));
2388
- console.error(pc6.dim("Create the file before running tests. It may be empty, but it must exist."));
2536
+ console.error(pc7.red(`Error: Secrets file not found: ${secretsPath}`));
2537
+ console.error(pc7.dim("Create the file before running tests. It may be empty, but it must exist."));
2389
2538
  process.exit(1);
2390
2539
  }
2391
2540
  let secretsContent;
2392
2541
  try {
2393
2542
  secretsContent = input3.readFileSync(secretsPath, "utf-8");
2394
2543
  } catch {
2395
- console.error(pc6.red(`Error: Secrets file could not be read: ${secretsPath}`));
2396
- console.error(pc6.dim("Check the path and file permissions before running tests."));
2544
+ console.error(pc7.red(`Error: Secrets file could not be read: ${secretsPath}`));
2545
+ console.error(pc7.dim("Check the path and file permissions before running tests."));
2397
2546
  process.exit(1);
2398
2547
  }
2399
2548
  try {
@@ -2410,7 +2559,7 @@ async function loadRuntimeSecrets(input3) {
2410
2559
  };
2411
2560
  } catch (err) {
2412
2561
  const message = err instanceof Error ? err.message : String(err);
2413
- console.error(pc6.red(`Error: Secrets file is invalid: ${message}`));
2562
+ console.error(pc7.red(`Error: Secrets file is invalid: ${message}`));
2414
2563
  process.exit(1);
2415
2564
  }
2416
2565
  }
@@ -2573,7 +2722,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2573
2722
  const dashboardCfg = config.services?.dashboard;
2574
2723
  const reporterSelectionResult = resolveReporterSelection(opts, Boolean(dashboardCfg));
2575
2724
  if (reporterSelectionResult.error) {
2576
- console.error(pc6.red(`Error: ${reporterSelectionResult.error}`));
2725
+ console.error(pc7.red(`Error: ${reporterSelectionResult.error}`));
2577
2726
  process.exit(2);
2578
2727
  }
2579
2728
  const reporterSelection = reporterSelectionResult.selection;
@@ -2589,7 +2738,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2589
2738
  activeSuiteDbs.add(dashboardDb);
2590
2739
  return { dashboardDb, logStorage: dashboardDb };
2591
2740
  } catch (err) {
2592
- console.warn(pc6.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
2741
+ console.warn(pc7.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
2593
2742
  return {};
2594
2743
  }
2595
2744
  };
@@ -2631,7 +2780,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2631
2780
  const suiteMembers = [];
2632
2781
  for (const { test: relativePath, id: suiteEntryId } of suite.tests) {
2633
2782
  if (!isPathInsideDir(relativePath, configDir)) {
2634
- console.error(pc6.red(`Path traversal rejected: ${relativePath}`));
2783
+ console.error(pc7.red(`Path traversal rejected: ${relativePath}`));
2635
2784
  const runId = createSuiteRunId();
2636
2785
  if (dashboardDb) {
2637
2786
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2669,7 +2818,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2669
2818
  const fileContent = await readFile5(absolutePath, "utf-8");
2670
2819
  const parseResult = parseTestFile2(fileContent, absolutePath);
2671
2820
  if (parseResult.errors.length > 0) {
2672
- console.error(pc6.red(`Parse errors in ${relativePath}:`));
2821
+ console.error(pc7.red(`Parse errors in ${relativePath}:`));
2673
2822
  formatParseErrors(parseResult.errors);
2674
2823
  const runId = createSuiteRunId();
2675
2824
  if (dashboardDb) {
@@ -2711,7 +2860,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2711
2860
  process.exit(2);
2712
2861
  }
2713
2862
  if (parseResult.tests.length === 0) {
2714
- console.error(pc6.red(`No test definition found in ${relativePath}`));
2863
+ console.error(pc7.red(`No test definition found in ${relativePath}`));
2715
2864
  const runId = createSuiteRunId();
2716
2865
  if (dashboardDb) {
2717
2866
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2748,7 +2897,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2748
2897
  const testDef = parseResult.tests[0];
2749
2898
  if (testDef["test-id"] && testDef["test-id"] !== suiteEntryId) {
2750
2899
  const mismatchMsg = `ID mismatch in suite "${suite.name}": test file ${relativePath} has test-id "${testDef["test-id"]}" but suite entry specifies id "${suiteEntryId}"`;
2751
- console.error(pc6.red(mismatchMsg));
2900
+ console.error(pc7.red(mismatchMsg));
2752
2901
  const runId = createSuiteRunId();
2753
2902
  if (dashboardDb) {
2754
2903
  const now = (/* @__PURE__ */ new Date()).toISOString();
@@ -2949,11 +3098,11 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2949
3098
  if (reporterSelection.dashboard && dashboardDb) {
2950
3099
  try {
2951
3100
  const { DashboardReporter } = await import("@vostride/agent-qa-dashboard");
2952
- const { resolve: resolve7 } = await import("path");
3101
+ const { resolve: resolve9 } = await import("path");
2953
3102
  const dashArtifactsDir = config.services?.dashboard?.artifactsDir ?? RUNTIME_ARTIFACTS_DIR;
2954
3103
  reporters.push(new DashboardReporter({
2955
3104
  db: dashboardDb,
2956
- artifactsDir: resolve7(configDir, dashArtifactsDir),
3105
+ artifactsDir: resolve9(configDir, dashArtifactsDir),
2957
3106
  redactor: variableContext?.secretRedactor,
2958
3107
  onRunCreated: (runId) => {
2959
3108
  lastSuiteRunId = runId;
@@ -2968,9 +3117,9 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2968
3117
  let actionCache;
2969
3118
  if (opts.cache !== false) {
2970
3119
  const { FileActionCache } = await import("@vostride/agent-qa-core");
2971
- const { resolve: resolve7 } = await import("path");
3120
+ const { resolve: resolve9 } = await import("path");
2972
3121
  actionCache = new FileActionCache({
2973
- dir: resolve7(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR),
3122
+ dir: resolve9(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR),
2974
3123
  ttl: config.services?.cache?.ttl ?? "7d",
2975
3124
  logger: logger.createScopedLogger("cache")
2976
3125
  });
@@ -2992,7 +3141,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
2992
3141
  curatorLockTimeout: suiteMemoryConfig?.curatorLockTimeout
2993
3142
  });
2994
3143
  } catch (err) {
2995
- console.warn(` ${pc6.dim("Memory:")} init error -- ${err.message}`);
3144
+ console.warn(` ${pc7.dim("Memory:")} init error -- ${err.message}`);
2996
3145
  }
2997
3146
  if (suiteMemoryConfig?.circuitBreakerEnabled !== false) {
2998
3147
  const { CircuitBreaker } = await import("@vostride/agent-qa-core");
@@ -3131,19 +3280,19 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
3131
3280
  const batchOutcome = await flushSuiteParents();
3132
3281
  if (batchOutcome.cancelled || shutdownRequested) break;
3133
3282
  if (opts.bail && batchOutcome.failed) {
3134
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failed suite"));
3283
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failed suite"));
3135
3284
  break;
3136
3285
  }
3137
3286
  const outcome = await runOneSuiteFile(suiteFile, suite);
3138
3287
  if (outcome.cancelled || shutdownRequested) break;
3139
3288
  if (opts.bail && outcome.failed) {
3140
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failed suite"));
3289
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failed suite"));
3141
3290
  break;
3142
3291
  }
3143
3292
  }
3144
3293
  const finalBatchOutcome = await flushSuiteParents();
3145
3294
  if (opts.bail && finalBatchOutcome.failed) {
3146
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failed suite"));
3295
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failed suite"));
3147
3296
  }
3148
3297
  process.removeListener("SIGINT", handleShutdown);
3149
3298
  process.removeListener("SIGTERM", handleShutdown);
@@ -3151,12 +3300,12 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
3151
3300
  return suiteResults.some((r) => r.status === "failed" || r.status === "cancelled") || shutdownRequested;
3152
3301
  }
3153
3302
  function createRunCommand() {
3154
- const cmd = new Command5("run").description("Run test files").argument("[patterns...]", "glob patterns for test files").option("--browser <name>", "override browser (chromium, firefox, webkit)").option("--platform <name>", "filter by platform (web, android, ios)").option("--headless", "run in headless mode").option("--no-headless", "run in headed mode").option("--no-cache", "bypass action cache for this run").option("--no-memory", "disable runtime memory injection for this run").option("--bail", "stop on first test failure").option("--dry-run", "parse and list tests without executing").option("--list-tests", "list discovered test files without executing").option("--junit-output <path>", "path for JUnit XML output").option("--screenshot-dir <dir>", "directory for failure screenshots").option("--screenshot-mode <mode>", "when to capture: failure (default) or every-step").option("--reporter <names...>", "select reporters (default: console)").option("--record", "enable session video recording").option("--config-debug", "print resolved config with source attribution for each test").option("--test", "discover and run tests via testMatch patterns (default when no flags)").option("--suite", "discover and run suites via suiteMatch patterns").option("--all", "discover and run both tests (testMatch) and suites (suiteMatch)").option("--device <name>", "Override the mobile test/suite device for this run").option("--var <kv...>", "set variable KEY=VALUE (repeatable)").option("--run-attr <kv...>", "attach run attribute KEY=VALUE; repeatable").action(async (patterns, opts, command) => {
3303
+ const cmd = new Command6("run").description("Run test files").argument("[patterns...]", "glob patterns for test files").option("--browser <name>", "override browser (chromium, firefox, webkit)").option("--platform <name>", "filter by platform (web, android, ios)").option("--headless", "run in headless mode").option("--no-headless", "run in headed mode").option("--no-cache", "bypass action cache for this run").option("--no-memory", "disable runtime memory injection for this run").option("--bail", "stop on first test failure").option("--dry-run", "parse and list tests without executing").option("--list-tests", "list discovered test files without executing").option("--junit-output <path>", "path for JUnit XML output").option("--screenshot-dir <dir>", "directory for failure screenshots").option("--screenshot-mode <mode>", "when to capture: failure (default) or every-step").option("--reporter <names...>", "select reporters (default: console)").option("--record", "enable session video recording").option("--config-debug", "print resolved config with source attribution for each test").option("--test", "discover and run tests via testMatch patterns (default when no flags)").option("--suite", "discover and run suites via suiteMatch patterns").option("--all", "discover and run both tests (testMatch) and suites (suiteMatch)").option("--device <name>", "Override the mobile test/suite device for this run").option("--var <kv...>", "set variable KEY=VALUE (repeatable)").option("--run-attr <kv...>", "attach run attribute KEY=VALUE; repeatable").action(async (patterns, opts, command) => {
3155
3304
  let _crashDb;
3156
3305
  try {
3157
3306
  const modeFlags = [opts.test, opts.suite, opts.all].filter(Boolean).length;
3158
3307
  if (modeFlags > 1) {
3159
- console.error(pc6.red("Error: --test, --suite, and --all are mutually exclusive."));
3308
+ console.error(pc7.red("Error: --test, --suite, and --all are mutually exclusive."));
3160
3309
  process.exit(1);
3161
3310
  }
3162
3311
  const program2 = command.parent;
@@ -3197,7 +3346,7 @@ function createRunCommand() {
3197
3346
  console.warn(`Warning: duplicate --run-attr key "${duplicateKey}"; using last value`);
3198
3347
  }
3199
3348
  } catch (err) {
3200
- console.error(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3349
+ console.error(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3201
3350
  process.exit(2);
3202
3351
  }
3203
3352
  const inlineVars = {};
@@ -3213,29 +3362,29 @@ function createRunCommand() {
3213
3362
  let resolvedEnvFilePath = null;
3214
3363
  resolvedEnvFilePath = workspacePaths.envFile.absolutePath;
3215
3364
  if (!existsSync8(workspacePaths.envFile.absolutePath)) {
3216
- console.error(pc6.red(`Error: Env file not found: ${workspacePaths.envFile.absolutePath}`));
3365
+ console.error(pc7.red(`Error: Env file not found: ${workspacePaths.envFile.absolutePath}`));
3217
3366
  process.exit(1);
3218
3367
  }
3219
3368
  try {
3220
3369
  rawEnvFileContent = readFileSync5(workspacePaths.envFile.absolutePath, "utf-8");
3221
3370
  envFileVars = parseEnvFile(rawEnvFileContent);
3222
3371
  } catch {
3223
- console.error(pc6.red(`Error: Env file could not be read: ${workspacePaths.envFile.absolutePath}`));
3372
+ console.error(pc7.red(`Error: Env file could not be read: ${workspacePaths.envFile.absolutePath}`));
3224
3373
  process.exit(1);
3225
3374
  }
3226
3375
  let resolvedHooks;
3227
3376
  let sandboxOptions;
3228
3377
  const hooksFilePath = workspacePaths.hooksFile.absolutePath;
3229
3378
  if (!existsSync8(hooksFilePath)) {
3230
- console.error(pc6.red(`Error: Hooks file not found: ${hooksFilePath}`));
3379
+ console.error(pc7.red(`Error: Hooks file not found: ${hooksFilePath}`));
3231
3380
  process.exit(1);
3232
3381
  }
3233
3382
  try {
3234
3383
  const { parseHooksFile } = await import("@vostride/agent-qa-core");
3235
3384
  const { hooks, errors } = await parseHooksFile(hooksFilePath);
3236
3385
  if (errors.length > 0) {
3237
- console.error(pc6.red("Hooks configuration errors:"));
3238
- for (const err of errors) console.error(pc6.red(` - ${err}`));
3386
+ console.error(pc7.red("Hooks configuration errors:"));
3387
+ for (const err of errors) console.error(pc7.red(` - ${err}`));
3239
3388
  process.exit(1);
3240
3389
  }
3241
3390
  resolvedHooks = new Map(hooks.map((h) => [h.id, h]));
@@ -3244,8 +3393,8 @@ function createRunCommand() {
3244
3393
  secretRedactor: runtimeSecrets.secretRedactor
3245
3394
  };
3246
3395
  } catch (err) {
3247
- console.error(pc6.red(`Error: Hooks file could not be read: ${hooksFilePath}`));
3248
- if (err instanceof Error && err.message) console.error(pc6.dim(err.message));
3396
+ console.error(pc7.red(`Error: Hooks file could not be read: ${hooksFilePath}`));
3397
+ if (err instanceof Error && err.message) console.error(pc7.dim(err.message));
3249
3398
  process.exit(1);
3250
3399
  }
3251
3400
  let agentRulesContent;
@@ -3253,7 +3402,7 @@ function createRunCommand() {
3253
3402
  try {
3254
3403
  agentRulesContent = await readFileAsync(workspacePaths.agentRules.absolutePath, "utf-8");
3255
3404
  } catch {
3256
- console.error(pc6.red(`Error: Agent rules file not found: ${workspacePaths.agentRules.absolutePath}`));
3405
+ console.error(pc7.red(`Error: Agent rules file not found: ${workspacePaths.agentRules.absolutePath}`));
3257
3406
  process.exit(1);
3258
3407
  }
3259
3408
  const effectiveLogLevel = globalOpts.logLevel ?? (globalOpts.verbose ? "debug" : void 0) ?? (globalOpts.quiet ? "silent" : void 0) ?? config.services?.logging?.level ?? "warn";
@@ -3265,21 +3414,21 @@ function createRunCommand() {
3265
3414
  discoveredTestFiles = selected.tests;
3266
3415
  discoveredSuiteFiles = selected.suites;
3267
3416
  } catch (err) {
3268
- console.error(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3417
+ console.error(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3269
3418
  process.exit(1);
3270
3419
  }
3271
3420
  if (discoveredTestFiles.length === 0 && discoveredSuiteFiles.length === 0) {
3272
3421
  const patternKind = patterns.every((pattern) => classifyRunPattern(pattern) === "suite") ? "suite" : "test";
3273
- console.error(pc6.red(`Error: No ${patternKind} files found matching patterns:`));
3274
- for (const p of patterns) console.error(pc6.red(` ${p}`));
3422
+ console.error(pc7.red(`Error: No ${patternKind} files found matching patterns:`));
3423
+ for (const p of patterns) console.error(pc7.red(` ${p}`));
3275
3424
  process.exit(2);
3276
3425
  }
3277
3426
  } else if (opts.suite) {
3278
3427
  discoveredSuiteFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "suite" })).map((record) => record.absolutePath);
3279
3428
  if (discoveredSuiteFiles.length === 0) {
3280
- console.error(pc6.red("Error: No suite files found matching workspace.suiteMatch"));
3429
+ console.error(pc7.red("Error: No suite files found matching workspace.suiteMatch"));
3281
3430
  for (const p of workspacePaths.suiteMatch) {
3282
- console.error(pc6.red(` ${p}`));
3431
+ console.error(pc7.red(` ${p}`));
3283
3432
  }
3284
3433
  process.exit(2);
3285
3434
  }
@@ -3287,19 +3436,19 @@ function createRunCommand() {
3287
3436
  discoveredTestFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "test" })).map((record) => record.absolutePath);
3288
3437
  discoveredSuiteFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "suite" })).map((record) => record.absolutePath);
3289
3438
  if (discoveredTestFiles.length === 0 && discoveredSuiteFiles.length === 0) {
3290
- console.error(pc6.red("Error: No test or suite files found. Check workspace.testMatch and workspace.suiteMatch in agent-qa.config.yaml"));
3439
+ console.error(pc7.red("Error: No test or suite files found. Check workspace.testMatch and workspace.suiteMatch in agent-qa.config.yaml"));
3291
3440
  process.exit(2);
3292
3441
  }
3293
3442
  } else {
3294
3443
  if (!workspacePaths.testMatch || workspacePaths.testMatch.length === 0) {
3295
- console.error(pc6.red("Error: No testMatch patterns in config. Add workspace.testMatch to agent-qa.config.yaml."));
3444
+ console.error(pc7.red("Error: No testMatch patterns in config. Add workspace.testMatch to agent-qa.config.yaml."));
3296
3445
  process.exit(2);
3297
3446
  }
3298
3447
  discoveredTestFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "test" })).map((record) => record.absolutePath);
3299
3448
  if (discoveredTestFiles.length === 0) {
3300
- console.error(pc6.red("Error: No test files found matching workspace.testMatch"));
3449
+ console.error(pc7.red("Error: No test files found matching workspace.testMatch"));
3301
3450
  for (const p of workspacePaths.testMatch) {
3302
- console.error(pc6.red(` ${p}`));
3451
+ console.error(pc7.red(` ${p}`));
3303
3452
  }
3304
3453
  process.exit(2);
3305
3454
  }
@@ -3314,7 +3463,7 @@ function createRunCommand() {
3314
3463
  }
3315
3464
  const files = discoveredTestFiles;
3316
3465
  if (opts.listTests) {
3317
- console.log(pc6.bold(`
3466
+ console.log(pc7.bold(`
3318
3467
  Discovered ${files.length} test file(s):
3319
3468
  `));
3320
3469
  for (const file of files) {
@@ -3326,7 +3475,7 @@ Discovered ${files.length} test file(s):
3326
3475
  const dashboardCfg2 = config.services?.dashboard;
3327
3476
  const reporterSelectionResult = resolveReporterSelection(opts, Boolean(dashboardCfg2));
3328
3477
  if (reporterSelectionResult.error) {
3329
- console.error(pc6.red(`Error: ${reporterSelectionResult.error}`));
3478
+ console.error(pc7.red(`Error: ${reporterSelectionResult.error}`));
3330
3479
  process.exit(2);
3331
3480
  }
3332
3481
  const reporterSelection = reporterSelectionResult.selection;
@@ -3343,13 +3492,13 @@ Discovered ${files.length} test file(s):
3343
3492
  logStorage = dashboardDb;
3344
3493
  _crashDb = dashboardDb;
3345
3494
  } catch (err) {
3346
- console.warn(pc6.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3495
+ console.warn(pc7.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3347
3496
  }
3348
3497
  }
3349
3498
  const { parseAllTests, formatParseError } = await import("@vostride/agent-qa-core");
3350
3499
  const parseResult = await parseAllTests(files);
3351
3500
  if (parseResult.errors.length > 0) {
3352
- console.error(pc6.red(`Found ${parseResult.errors.length} parse error(s):
3501
+ console.error(pc7.red(`Found ${parseResult.errors.length} parse error(s):
3353
3502
  `));
3354
3503
  for (const err of parseResult.errors) {
3355
3504
  console.error(formatParseError(err));
@@ -3445,17 +3594,17 @@ Discovered ${files.length} test file(s):
3445
3594
  }
3446
3595
  const filtered = parseResult.tests;
3447
3596
  if (filtered.length === 0) {
3448
- console.log(pc6.yellow("No tests match filters"));
3597
+ console.log(pc7.yellow("No tests match filters"));
3449
3598
  process.exit(0);
3450
3599
  }
3451
3600
  if (opts.dryRun) {
3452
- console.log(pc6.bold(`
3601
+ console.log(pc7.bold(`
3453
3602
  Found ${filtered.length} test(s):
3454
3603
  `));
3455
3604
  for (const test of filtered) {
3456
- console.log(` ${pc6.green("\u25CF")} ${test.name}`);
3605
+ console.log(` ${pc7.green("\u25CF")} ${test.name}`);
3457
3606
  if (opts.configDebug) {
3458
- console.log(pc6.bold(`
3607
+ console.log(pc7.bold(`
3459
3608
  Config for: ${test.name}`));
3460
3609
  console.log(formatConfigDebug(config, test.use, flagOverrides));
3461
3610
  console.log();
@@ -3493,11 +3642,11 @@ Found ${filtered.length} test(s):
3493
3642
  if (reporterSelection.dashboard && dashboardDb) {
3494
3643
  try {
3495
3644
  const { DashboardReporter } = await import("@vostride/agent-qa-dashboard");
3496
- const { resolve: resolve7 } = await import("path");
3645
+ const { resolve: resolve9 } = await import("path");
3497
3646
  const dashArtifactsDir = config.services?.dashboard?.artifactsDir ?? RUNTIME_ARTIFACTS_DIR;
3498
3647
  reporters.push(new DashboardReporter({
3499
3648
  db: dashboardDb,
3500
- artifactsDir: resolve7(configDir, dashArtifactsDir),
3649
+ artifactsDir: resolve9(configDir, dashArtifactsDir),
3501
3650
  redactor: runtimeSecrets.secretRedactor,
3502
3651
  onRunCreated: (runId) => {
3503
3652
  currentRunId = runId;
@@ -3505,7 +3654,7 @@ Found ${filtered.length} test(s):
3505
3654
  }
3506
3655
  }));
3507
3656
  } catch (err) {
3508
- console.warn(pc6.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3657
+ console.warn(pc7.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3509
3658
  }
3510
3659
  }
3511
3660
  const analyticsReporter = createAnalyticsRunReporter({ config, surface: "cli" });
@@ -3519,9 +3668,9 @@ Found ${filtered.length} test(s):
3519
3668
  });
3520
3669
  if (opts.cache !== false) {
3521
3670
  const { FileActionCache } = await import("@vostride/agent-qa-core");
3522
- const { resolve: resolve7 } = await import("path");
3671
+ const { resolve: resolve9 } = await import("path");
3523
3672
  actionCache = new FileActionCache({
3524
- dir: resolve7(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR),
3673
+ dir: resolve9(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR),
3525
3674
  ttl: config.services?.cache?.ttl ?? "7d",
3526
3675
  logger: logger.createScopedLogger("cache")
3527
3676
  });
@@ -3599,7 +3748,7 @@ Found ${filtered.length} test(s):
3599
3748
  const testMergedConfig = { ...config, use: mergedUse };
3600
3749
  const effectiveCacheEnabled = opts.cache !== false && mergedUse.cache !== false;
3601
3750
  if (opts.configDebug) {
3602
- console.log(pc6.bold(`
3751
+ console.log(pc7.bold(`
3603
3752
  Config for: ${test.name}`));
3604
3753
  console.log(formatConfigDebug(config, test.use, testFlagOverrides));
3605
3754
  console.log();
@@ -3709,7 +3858,7 @@ Config for: ${test.name}`));
3709
3858
  });
3710
3859
  }
3711
3860
  if (effectiveLogLevel === "debug") {
3712
- console.log(pc6.dim(` Platform: ${testPlatform}${useFarm ? ` (farm: ${testResolvedDevice?.transport})` : ""}${testPlatform !== "web" ? " (adapter re-created)" : ""}`));
3861
+ console.log(pc7.dim(` Platform: ${testPlatform}${useFarm ? ` (farm: ${testResolvedDevice?.transport})` : ""}${testPlatform !== "web" ? " (adapter re-created)" : ""}`));
3713
3862
  }
3714
3863
  }
3715
3864
  const mergedForLLM = { registry: config.registry, use: testMergedConfig.use ?? config.use };
@@ -3720,7 +3869,7 @@ Config for: ${test.name}`));
3720
3869
  process.env.AGENT_QA_LLM_MODEL = plannerCfg.model;
3721
3870
  process.env.AGENT_QA_LLM_PROVIDER = plannerCfg.provider;
3722
3871
  if (effectiveLogLevel === "debug") {
3723
- console.log(pc6.dim(` Model: ${plannerCfg.model} (${plannerCfg.provider})`));
3872
+ console.log(pc7.dim(` Model: ${plannerCfg.model} (${plannerCfg.provider})`));
3724
3873
  }
3725
3874
  const plannerModel = await createModel(plannerModelConfig);
3726
3875
  const verifierModel = await createModel(verifierModelConfig);
@@ -3885,7 +4034,7 @@ Config for: ${test.name}`));
3885
4034
  const testId = testDef["test-id"] ?? testDef.name;
3886
4035
  memoryInitParams = { product, testId, memoryRoot };
3887
4036
  } catch (err) {
3888
- console.warn(` ${pc6.dim("Memory:")} init error -- ${err.message}`);
4037
+ console.warn(` ${pc7.dim("Memory:")} init error -- ${err.message}`);
3889
4038
  }
3890
4039
  }
3891
4040
  const result = await runTestWithRetry(testDef, {
@@ -3979,7 +4128,7 @@ Config for: ${test.name}`));
3979
4128
  try {
3980
4129
  const { shouldAblate, collectAllInjectedIds, deprecateOnFailure } = await import("@vostride/agent-qa-core");
3981
4130
  if (shouldAblate(result, memoryProvider)) {
3982
- console.log(` ${pc6.dim("Memory:")} ablation retry -- re-running without memory...`);
4131
+ console.log(` ${pc7.dim("Memory:")} ablation retry -- re-running without memory...`);
3983
4132
  const ablationAdapter = await createPlatformAdapter(testPlatform);
3984
4133
  const mergedBrowser = mergedUse.browser;
3985
4134
  const ablationPlatformConfig = buildPlatformConfig(testPlatform, testResolvedDevice, testTimeoutsForSetup, mergedBrowser ?? config.use?.browser, testMergedConfig.use?.logCapture ?? config.use?.logCapture, testFarmSession, testMobileResolved);
@@ -4024,7 +4173,7 @@ Config for: ${test.name}`));
4024
4173
  circuitBreaker.record({ withMemory: false, passed: ablationResult.status === "passed" });
4025
4174
  }
4026
4175
  if (ablationResult.status === "passed") {
4027
- console.log(` ${pc6.dim("Memory:")} ablation confirmed memory caused failure -- penalizing observations`);
4176
+ console.log(` ${pc7.dim("Memory:")} ablation confirmed memory caused failure -- penalizing observations`);
4028
4177
  const injectedMap = collectAllInjectedIds(result, memoryProvider);
4029
4178
  await deprecateOnFailure({
4030
4179
  testResult: result,
@@ -4035,14 +4184,14 @@ Config for: ${test.name}`));
4035
4184
  });
4036
4185
  ablationHandledDeprecation = true;
4037
4186
  } else {
4038
- console.log(` ${pc6.dim("Memory:")} ablation retry also failed -- memory not the cause`);
4187
+ console.log(` ${pc7.dim("Memory:")} ablation retry also failed -- memory not the cause`);
4039
4188
  }
4040
4189
  } finally {
4041
4190
  await ablationAdapter.cleanup();
4042
4191
  }
4043
4192
  }
4044
4193
  } catch (err) {
4045
- console.warn(` ${pc6.dim("Memory:")} ablation error -- ${err.message}`);
4194
+ console.warn(` ${pc7.dim("Memory:")} ablation error -- ${err.message}`);
4046
4195
  }
4047
4196
  }
4048
4197
  if (memoryProvider && memoryConfig?.curatorEnabled !== false && !ablationHandledDeprecation) {
@@ -4076,7 +4225,7 @@ Config for: ${test.name}`));
4076
4225
  }
4077
4226
  }
4078
4227
  } catch (err) {
4079
- console.warn(` ${pc6.dim("Memory:")} curator error -- ${err.message}`);
4228
+ console.warn(` ${pc7.dim("Memory:")} curator error -- ${err.message}`);
4080
4229
  }
4081
4230
  }
4082
4231
  results.push(result);
@@ -4087,13 +4236,13 @@ Config for: ${test.name}`));
4087
4236
  }
4088
4237
  if (circuitBreaker?.isTripped() && !circuitBreakerTripped) {
4089
4238
  circuitBreakerTripped = true;
4090
- console.log(` ${pc6.yellow("Memory: circuit breaker tripped -- disabling memory injection for remaining tests")}`);
4239
+ console.log(` ${pc7.yellow("Memory: circuit breaker tripped -- disabling memory injection for remaining tests")}`);
4091
4240
  }
4092
4241
  if (result.status === "failed" && actionCache && effectiveCacheEnabled && result.steps.length > 0) {
4093
4242
  const { hashStepInstruction: hashStepInstruction2 } = await import("@vostride/agent-qa-core");
4094
4243
  const { rm: rm3 } = await import("fs/promises");
4095
- const { resolve: resolve7, join: join8 } = await import("path");
4096
- const cacheDir = resolve7(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR);
4244
+ const { resolve: resolve9, join: join8 } = await import("path");
4245
+ const cacheDir = resolve9(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR);
4097
4246
  const cacheLog = logger.createScopedLogger("cache");
4098
4247
  let purged = 0;
4099
4248
  for (let i = 0; i < result.steps.length; i++) {
@@ -4172,13 +4321,13 @@ Config for: ${test.name}`));
4172
4321
  }
4173
4322
  const parallelBatchFailed = await flushParallelRuns();
4174
4323
  if (opts.bail && parallelBatchFailed) {
4175
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failure"));
4324
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failure"));
4176
4325
  bailTriggered = true;
4177
4326
  break;
4178
4327
  }
4179
4328
  const outcome = await runOneDirectTest(test);
4180
4329
  if (opts.bail && outcome.failed) {
4181
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failure"));
4330
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failure"));
4182
4331
  bailTriggered = true;
4183
4332
  break;
4184
4333
  }
@@ -4186,7 +4335,7 @@ Config for: ${test.name}`));
4186
4335
  if (!bailTriggered) {
4187
4336
  const parallelBatchFailed = await flushParallelRuns();
4188
4337
  if (opts.bail && parallelBatchFailed) {
4189
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failure"));
4338
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failure"));
4190
4339
  }
4191
4340
  }
4192
4341
  releaseAppium();
@@ -4205,7 +4354,7 @@ Config for: ${test.name}`));
4205
4354
  const errMsg = err instanceof Error ? err.message : String(err);
4206
4355
  const errStack = err instanceof Error ? err.stack ?? "" : "";
4207
4356
  const formattedError = formatFrameworkError(errMsg);
4208
- console.error(pc6.red(formattedError.displayMessage));
4357
+ console.error(pc7.red(formattedError.displayMessage));
4209
4358
  if (_crashDb) {
4210
4359
  const fullError = `${errMsg}
4211
4360
  ${errStack}`;
@@ -4259,10 +4408,10 @@ ${errStack}`;
4259
4408
  init_config();
4260
4409
  init_llm_utils();
4261
4410
  init_model_defaults();
4262
- import { Command as Command6 } from "commander";
4263
- import { resolve as resolve3, dirname as dirname4 } from "path";
4411
+ import { Command as Command7 } from "commander";
4412
+ import { resolve as resolve5, dirname as dirname6 } from "path";
4264
4413
  import { createRequire as createRequire2 } from "module";
4265
- import pc7 from "picocolors";
4414
+ import pc8 from "picocolors";
4266
4415
  import { flushAnalytics, resolveWorkspacePaths as resolveWorkspacePaths3 } from "@vostride/agent-qa-core";
4267
4416
  async function startDashboardBackedServices(opts, command, labels) {
4268
4417
  try {
@@ -4271,8 +4420,8 @@ async function startDashboardBackedServices(opts, command, labels) {
4271
4420
  const config = await resolveConfig({
4272
4421
  configPath: globalOpts.config
4273
4422
  });
4274
- const configFilePath = resolve3(globalOpts.config ?? "agent-qa.config.yaml");
4275
- const configDir = dirname4(configFilePath);
4423
+ const configFilePath = resolve5(globalOpts.config ?? "agent-qa.config.yaml");
4424
+ const configDir = dirname6(configFilePath);
4276
4425
  const workspacePaths = resolveWorkspacePaths3({
4277
4426
  config,
4278
4427
  configPath: configFilePath,
@@ -4312,17 +4461,17 @@ async function startDashboardBackedServices(opts, command, labels) {
4312
4461
  try {
4313
4462
  const require2 = createRequire2(import.meta.url);
4314
4463
  const pkgPath = require2.resolve("@vostride/agent-qa-dashboard-ui/package.json");
4315
- uiDir = resolve3(dirname4(pkgPath), "dist");
4464
+ uiDir = resolve5(dirname6(pkgPath), "dist");
4316
4465
  } catch {
4317
4466
  if (globalOpts.verbose) {
4318
- console.log(pc7.yellow("Warning: @vostride/agent-qa-dashboard-ui not found \u2014 API-only mode"));
4467
+ console.log(pc8.yellow("Warning: @vostride/agent-qa-dashboard-ui not found \u2014 API-only mode"));
4319
4468
  }
4320
4469
  }
4321
4470
  const { close } = await startServer({
4322
4471
  db,
4323
4472
  port,
4324
4473
  uiDir,
4325
- artifactsDir: resolve3(configDir, artifactsDir),
4474
+ artifactsDir: resolve5(configDir, artifactsDir),
4326
4475
  workspacePaths,
4327
4476
  configPath: configFilePath,
4328
4477
  mcp: mcpConfig,
@@ -4338,7 +4487,7 @@ async function startDashboardBackedServices(opts, command, labels) {
4338
4487
  const shutdown = async () => {
4339
4488
  if (shuttingDown) return;
4340
4489
  shuttingDown = true;
4341
- console.log(pc7.dim(`
4490
+ console.log(pc8.dim(`
4342
4491
  Shutting down ${labels.shutdown}...`));
4343
4492
  close();
4344
4493
  await flushAnalytics({ config }).catch(() => {
@@ -4348,12 +4497,12 @@ Shutting down ${labels.shutdown}...`));
4348
4497
  process.on("SIGINT", shutdown);
4349
4498
  process.on("SIGTERM", shutdown);
4350
4499
  } catch (err) {
4351
- console.error(pc7.red(`${labels.error} error: ${err instanceof Error ? err.message : String(err)}`));
4500
+ console.error(pc8.red(`${labels.error} error: ${err instanceof Error ? err.message : String(err)}`));
4352
4501
  process.exit(2);
4353
4502
  }
4354
4503
  }
4355
4504
  function createDashboardCommand() {
4356
- const cmd = new Command6("dashboard").description("Start the dashboard web server").option("--port <number>", "server port (default: 3470)").option("--db <path>", "database file path").option("--open", "open browser after start").action(async (opts, command) => {
4505
+ const cmd = new Command7("dashboard").description("Start the dashboard web server").option("--port <number>", "server port (default: 3470)").option("--db <path>", "database file path").option("--open", "open browser after start").action(async (opts, command) => {
4357
4506
  await startDashboardBackedServices(opts, command, {
4358
4507
  error: "Dashboard",
4359
4508
  shutdown: "dashboard"
@@ -4362,7 +4511,7 @@ function createDashboardCommand() {
4362
4511
  return cmd;
4363
4512
  }
4364
4513
  function createServeCommand() {
4365
- const cmd = new Command6("serve").description("Start configured local agent-qa services").action(async (_opts, command) => {
4514
+ const cmd = new Command7("serve").description("Start configured local agent-qa services").action(async (_opts, command) => {
4366
4515
  await startDashboardBackedServices({}, command, {
4367
4516
  error: "Serve",
4368
4517
  shutdown: "local services"
@@ -4373,11 +4522,11 @@ function createServeCommand() {
4373
4522
 
4374
4523
  // src/commands/mcp.ts
4375
4524
  init_config();
4376
- import { Command as Command7 } from "commander";
4377
- import pc8 from "picocolors";
4525
+ import { Command as Command8 } from "commander";
4526
+ import pc9 from "picocolors";
4378
4527
  import { flushAnalytics as flushAnalytics2 } from "@vostride/agent-qa-core";
4379
4528
  function createMcpCommand() {
4380
- const cmd = new Command7("mcp").description("Start MCP (Model Context Protocol) server over stdio").action(async (command) => {
4529
+ const cmd = new Command8("mcp").description("Start MCP (Model Context Protocol) server over stdio").action(async (command) => {
4381
4530
  try {
4382
4531
  const program2 = command.parent;
4383
4532
  const globalOpts = program2?.opts() ?? {};
@@ -4395,7 +4544,7 @@ function createMcpCommand() {
4395
4544
  });
4396
4545
  }
4397
4546
  } catch (err) {
4398
- console.error(pc8.red("Failed to start MCP server:"), err instanceof Error ? err.message : String(err));
4547
+ console.error(pc9.red("Failed to start MCP server:"), err instanceof Error ? err.message : String(err));
4399
4548
  process.exitCode = 1;
4400
4549
  }
4401
4550
  });
@@ -4404,8 +4553,8 @@ function createMcpCommand() {
4404
4553
 
4405
4554
  // src/commands/config.ts
4406
4555
  init_config();
4407
- import { Command as Command8 } from "commander";
4408
- import pc9 from "picocolors";
4556
+ import { Command as Command9 } from "commander";
4557
+ import pc10 from "picocolors";
4409
4558
  import { parseDocument, Document } from "yaml";
4410
4559
  import { readFile as readFile3, writeFile } from "fs/promises";
4411
4560
  import { createInterface as createInterface2 } from "readline";
@@ -4434,10 +4583,10 @@ async function readStdin() {
4434
4583
  }
4435
4584
  function plainPrompt(question) {
4436
4585
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
4437
- return new Promise((resolve7) => {
4586
+ return new Promise((resolve9) => {
4438
4587
  rl.question(question, (answer) => {
4439
4588
  rl.close();
4440
- resolve7(answer.trim());
4589
+ resolve9(answer.trim());
4441
4590
  });
4442
4591
  });
4443
4592
  }
@@ -4450,14 +4599,14 @@ async function getValue(key, providedValue) {
4450
4599
  }
4451
4600
  async function setAction(key, value) {
4452
4601
  if (isSensitiveKey(key)) {
4453
- console.error(pc9.red("Credential values are not written by config set."));
4602
+ console.error(pc10.red("Credential values are not written by config set."));
4454
4603
  console.error("Use agent-qa auth set --config <name> --type api-key|bearer-token for compatible or Gemini configs.");
4455
4604
  console.error("Use plugins.auth plus the dashboard auth flow for subscription configs.");
4456
4605
  return;
4457
4606
  }
4458
4607
  const resolved = await getValue(key, value);
4459
4608
  if (!resolved) {
4460
- console.error(pc9.red("No value provided."));
4609
+ console.error(pc10.red("No value provided."));
4461
4610
  return;
4462
4611
  }
4463
4612
  const configPath = "agent-qa.config.yaml";
@@ -4474,7 +4623,7 @@ async function setAction(key, value) {
4474
4623
  }
4475
4624
  doc.setIn(key.split("."), resolved);
4476
4625
  await writeFile(configPath, doc.toString());
4477
- console.log(pc9.green(`\u2713 ${key} = ${resolved} saved to agent-qa.config.yaml`));
4626
+ console.log(pc10.green(`\u2713 ${key} = ${resolved} saved to agent-qa.config.yaml`));
4478
4627
  }
4479
4628
  async function showAction() {
4480
4629
  const fileConfig = await loadConfigFile("agent-qa.config.yaml");
@@ -4482,12 +4631,12 @@ async function showAction() {
4482
4631
  const { mergeConfigs: mergeConfigs2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4483
4632
  const globalConfig = mergeConfigs2(fileConfig, envOverrides, {});
4484
4633
  const table = formatConfigDebug(globalConfig, {}, {});
4485
- console.log(pc9.bold("\nResolved Configuration:\n"));
4634
+ console.log(pc10.bold("\nResolved Configuration:\n"));
4486
4635
  console.log(table);
4487
4636
  console.log();
4488
4637
  }
4489
4638
  function createConfigCommand() {
4490
- const cmd = new Command8("config").description("Manage configuration");
4639
+ const cmd = new Command9("config").description("Manage configuration");
4491
4640
  cmd.command("set <key> [value]").description("Set a config value").action(setAction);
4492
4641
  cmd.command("show").description("Show resolved config with source attribution").action(showAction);
4493
4642
  return cmd;
@@ -4495,8 +4644,8 @@ function createConfigCommand() {
4495
4644
 
4496
4645
  // src/commands/queue.ts
4497
4646
  init_config();
4498
- import { Command as Command9 } from "commander";
4499
- import pc10 from "picocolors";
4647
+ import { Command as Command10 } from "commander";
4648
+ import pc11 from "picocolors";
4500
4649
  function timeAgo(dateStr) {
4501
4650
  const diff = Date.now() - new Date(dateStr).getTime();
4502
4651
  const seconds = Math.floor(diff / 1e3);
@@ -4519,7 +4668,7 @@ async function resolveServerUrl(opts, globalOpts) {
4519
4668
  }
4520
4669
  }
4521
4670
  function createQueueCommand() {
4522
- const cmd = new Command9("queue").description("Manage the test execution queue");
4671
+ const cmd = new Command10("queue").description("Manage the test execution queue");
4523
4672
  cmd.command("list").description("Show pending and running jobs in the queue").option("--json", "output raw JSON").option("--all", "include recently completed jobs").option("--server <url>", "dashboard server URL").action(async (opts, command) => {
4524
4673
  try {
4525
4674
  const program2 = command.parent.parent;
@@ -4530,13 +4679,13 @@ function createQueueCommand() {
4530
4679
  try {
4531
4680
  const response = await fetch(endpoint);
4532
4681
  if (!response.ok) {
4533
- console.error(pc10.red(`Server returned ${response.status}: ${response.statusText}`));
4682
+ console.error(pc11.red(`Server returned ${response.status}: ${response.statusText}`));
4534
4683
  process.exit(1);
4535
4684
  }
4536
4685
  data = await response.json();
4537
4686
  } catch {
4538
- console.error(pc10.red(`Could not connect to dashboard server at ${serverUrl}`));
4539
- console.error(pc10.dim("Make sure the dashboard is running: agent-qa dashboard"));
4687
+ console.error(pc11.red(`Could not connect to dashboard server at ${serverUrl}`));
4688
+ console.error(pc11.dim("Make sure the dashboard is running: agent-qa dashboard"));
4540
4689
  process.exit(1);
4541
4690
  }
4542
4691
  if (opts.json) {
@@ -4544,19 +4693,19 @@ function createQueueCommand() {
4544
4693
  return;
4545
4694
  }
4546
4695
  console.log();
4547
- console.log(pc10.bold("Queue Status"));
4548
- console.log(` Concurrency: ${pc10.blue(String(data.concurrency))} slots (${pc10.yellow(String(data.activeSlots))} active)`);
4696
+ console.log(pc11.bold("Queue Status"));
4697
+ console.log(` Concurrency: ${pc11.blue(String(data.concurrency))} slots (${pc11.yellow(String(data.activeSlots))} active)`);
4549
4698
  console.log();
4550
4699
  const pending = data.pending?.jobs ?? [];
4551
4700
  const running = data.running?.jobs ?? [];
4552
4701
  if (pending.length === 0 && running.length === 0) {
4553
- console.log(pc10.dim(" Queue is empty \u2014 no pending or running jobs"));
4702
+ console.log(pc11.dim(" Queue is empty \u2014 no pending or running jobs"));
4554
4703
  console.log();
4555
4704
  return;
4556
4705
  }
4557
4706
  if (pending.length > 0) {
4558
- console.log(pc10.bold(`Pending (${pending.length} jobs):`));
4559
- console.log(pc10.dim(" # ID Name Priority Source Queued"));
4707
+ console.log(pc11.bold(`Pending (${pending.length} jobs):`));
4708
+ console.log(pc11.dim(" # ID Name Priority Source Queued"));
4560
4709
  for (let i = 0; i < pending.length; i++) {
4561
4710
  const job = pending[i];
4562
4711
  const num = String(i + 1).padEnd(3);
@@ -4565,37 +4714,37 @@ function createQueueCommand() {
4565
4714
  const priority = String(job.priority ?? 0).padEnd(8);
4566
4715
  const source = (job.source ?? "").padEnd(10);
4567
4716
  const queued = job.createdAt ? timeAgo(job.createdAt) : "";
4568
- console.log(` ${num} ${pc10.dim(id)} ${name} ${priority} ${source} ${pc10.dim(queued)}`);
4717
+ console.log(` ${num} ${pc11.dim(id)} ${name} ${priority} ${source} ${pc11.dim(queued)}`);
4569
4718
  }
4570
4719
  console.log();
4571
4720
  }
4572
4721
  if (running.length > 0) {
4573
- console.log(pc10.bold(`Running (${running.length} jobs):`));
4574
- console.log(pc10.dim(" ID Name Source Started"));
4722
+ console.log(pc11.bold(`Running (${running.length} jobs):`));
4723
+ console.log(pc11.dim(" ID Name Source Started"));
4575
4724
  for (const job of running) {
4576
4725
  const id = (job.id ?? "").slice(0, 12).padEnd(12);
4577
4726
  const name = (job.name ?? "").slice(0, 18).padEnd(18);
4578
4727
  const source = (job.source ?? "").padEnd(10);
4579
4728
  const started = job.startedAt ? timeAgo(job.startedAt) : "";
4580
- console.log(` ${pc10.green(id)} ${name} ${source} ${pc10.dim(started)}`);
4729
+ console.log(` ${pc11.green(id)} ${name} ${source} ${pc11.dim(started)}`);
4581
4730
  }
4582
4731
  console.log();
4583
4732
  }
4584
4733
  if (opts.all && data.recent?.length > 0) {
4585
- console.log(pc10.bold(`Recent (${data.recent.length} jobs):`));
4586
- console.log(pc10.dim(" ID Name Status Duration"));
4734
+ console.log(pc11.bold(`Recent (${data.recent.length} jobs):`));
4735
+ console.log(pc11.dim(" ID Name Status Duration"));
4587
4736
  for (const job of data.recent) {
4588
4737
  const id = (job.id ?? "").slice(0, 12).padEnd(12);
4589
4738
  const name = (job.name ?? "").slice(0, 18).padEnd(18);
4590
4739
  const status = (job.status ?? "").padEnd(8);
4591
4740
  const duration = job.duration ? `${Math.round(job.duration / 1e3)}s` : "";
4592
- const statusColor = job.status === "passed" ? pc10.green : job.status === "failed" ? pc10.red : pc10.dim;
4593
- console.log(` ${pc10.dim(id)} ${name} ${statusColor(status)} ${pc10.dim(duration)}`);
4741
+ const statusColor = job.status === "passed" ? pc11.green : job.status === "failed" ? pc11.red : pc11.dim;
4742
+ console.log(` ${pc11.dim(id)} ${name} ${statusColor(status)} ${pc11.dim(duration)}`);
4594
4743
  }
4595
4744
  console.log();
4596
4745
  }
4597
4746
  } catch (err) {
4598
- console.error(pc10.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4747
+ console.error(pc11.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4599
4748
  process.exit(1);
4600
4749
  }
4601
4750
  });
@@ -4608,25 +4757,25 @@ function createQueueCommand() {
4608
4757
  try {
4609
4758
  response = await fetch(`${serverUrl}/api/runs/${runId}/cancel`, { method: "POST" });
4610
4759
  } catch {
4611
- console.error(pc10.red(`Could not connect to dashboard server at ${serverUrl}`));
4612
- console.error(pc10.dim("Make sure the dashboard is running: agent-qa dashboard"));
4760
+ console.error(pc11.red(`Could not connect to dashboard server at ${serverUrl}`));
4761
+ console.error(pc11.dim("Make sure the dashboard is running: agent-qa dashboard"));
4613
4762
  process.exit(1);
4614
4763
  }
4615
4764
  if (response.ok) {
4616
4765
  const data = await response.json();
4617
4766
  if (data.cancelled) {
4618
- console.log(pc10.green(`\u2713 Cancelled job ${runId}`));
4767
+ console.log(pc11.green(`\u2713 Cancelled job ${runId}`));
4619
4768
  } else {
4620
- console.log(pc10.red(`Failed to cancel: ${data.error ?? "Unknown error"}`));
4769
+ console.log(pc11.red(`Failed to cancel: ${data.error ?? "Unknown error"}`));
4621
4770
  }
4622
4771
  } else if (response.status === 404) {
4623
- console.log(pc10.red(`Job not found: ${runId}`));
4772
+ console.log(pc11.red(`Job not found: ${runId}`));
4624
4773
  } else {
4625
4774
  const data = await response.json().catch(() => ({}));
4626
- console.log(pc10.red(`Failed to cancel: ${data.error ?? response.statusText}`));
4775
+ console.log(pc11.red(`Failed to cancel: ${data.error ?? response.statusText}`));
4627
4776
  }
4628
4777
  } catch (err) {
4629
- console.error(pc10.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4778
+ console.error(pc11.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4630
4779
  process.exit(1);
4631
4780
  }
4632
4781
  });
@@ -4635,10 +4784,10 @@ function createQueueCommand() {
4635
4784
 
4636
4785
  // src/commands/validate.ts
4637
4786
  init_config();
4638
- import { Command as Command10 } from "commander";
4787
+ import { Command as Command11 } from "commander";
4639
4788
  import { validateProject, formatDiagnostics, resolveWorkspacePaths as resolveWorkspacePaths4 } from "@vostride/agent-qa-core";
4640
4789
  function createValidateCommand() {
4641
- const cmd = new Command10("validate").description("Validate config, test files, and suite references").argument("[files...]", "specific files to validate (default: auto-discover all)").action(async (files, command) => {
4790
+ const cmd = new Command11("validate").description("Validate config, test files, and suite references").argument("[files...]", "specific files to validate (default: auto-discover all)").action(async (files, command) => {
4642
4791
  const configPath = command.parent?.opts().config ?? "agent-qa.config.yaml";
4643
4792
  let workspace;
4644
4793
  try {
@@ -4661,9 +4810,9 @@ function createValidateCommand() {
4661
4810
  }
4662
4811
 
4663
4812
  // src/commands/auth.ts
4664
- import { Command as Command11 } from "commander";
4813
+ import { Command as Command12 } from "commander";
4665
4814
  import { exec } from "child_process";
4666
- import pc11 from "picocolors";
4815
+ import pc12 from "picocolors";
4667
4816
  import input2 from "@inquirer/input";
4668
4817
  import password from "@inquirer/password";
4669
4818
  var PROVIDER_LABELS = {
@@ -4698,10 +4847,10 @@ function getGlobalConfigPath2(command) {
4698
4847
  return command.parent?.parent?.opts().config ?? command.parent?.opts().config;
4699
4848
  }
4700
4849
  function printCredentialGuidance(configName, provider) {
4701
- console.log(pc11.red(`Config ${configName} uses ${providerLabel(provider)} credentials.`));
4702
- console.log(pc11.dim("Use agent-qa auth set --config <name> --type api-key"));
4850
+ console.log(pc12.red(`Config ${configName} uses ${providerLabel(provider)} credentials.`));
4851
+ console.log(pc12.dim("Use agent-qa auth set --config <name> --type api-key"));
4703
4852
  if (provider === "anthropic-compatible") {
4704
- console.log(pc11.dim("or --type bearer-token"));
4853
+ console.log(pc12.dim("or --type bearer-token"));
4705
4854
  }
4706
4855
  }
4707
4856
  function applyAuthToModelConfig(llmConfig, auth) {
@@ -4721,15 +4870,15 @@ function applyAuthToModelConfig(llmConfig, auth) {
4721
4870
  function statusForResolvedAuth(auth) {
4722
4871
  switch (auth.kind) {
4723
4872
  case "api-key":
4724
- return pc11.green("Saved API key");
4873
+ return pc12.green("Saved API key");
4725
4874
  case "bearer-token":
4726
- return pc11.green("Saved bearer token");
4875
+ return pc12.green("Saved bearer token");
4727
4876
  case "auth-fetch":
4728
- return Number(auth.expires ?? Date.now() + 1) <= Date.now() ? pc11.red("Expired") : pc11.green("OAuth connected");
4877
+ return Number(auth.expires ?? Date.now() + 1) <= Date.now() ? pc12.red("Expired") : pc12.green("OAuth connected");
4729
4878
  case "unauthenticated":
4730
- return pc11.dim("No credential");
4879
+ return pc12.dim("No credential");
4731
4880
  case "missing":
4732
- return pc11.red("Missing credential");
4881
+ return pc12.red("Missing credential");
4733
4882
  }
4734
4883
  }
4735
4884
  function extractProviderError(err) {
@@ -4760,16 +4909,16 @@ function openAuthorizationUrl(url) {
4760
4909
  });
4761
4910
  }
4762
4911
  function printMissingSubscriptionPluginGuidance(configName, provider) {
4763
- console.log(pc11.red(`Provider "${provider}" is configured for "${configName}", but no auth plugin is registered.`));
4764
- console.log(pc11.dim(`Config: ${configName}`));
4765
- console.log(pc11.dim(`Provider: ${provider}`));
4766
- console.log(pc11.dim("Add the subscription auth plugin to package.json and install with your package manager:"));
4767
- console.log(pc11.dim(' "devDependencies": { "@vostride/agent-qa-subscription-auth": "<agent-qa version>" }'));
4768
- console.log(pc11.dim("Declare it in agent-qa.config.yaml under plugins.auth:"));
4769
- console.log(pc11.dim(" plugins:"));
4770
- console.log(pc11.dim(" auth:"));
4771
- console.log(pc11.dim(" - package: @vostride/agent-qa-subscription-auth"));
4772
- console.log(pc11.dim("Then authenticate from agent-qa dashboard or rerun this command."));
4912
+ console.log(pc12.red(`Provider "${provider}" is configured for "${configName}", but no auth plugin is registered.`));
4913
+ console.log(pc12.dim(`Config: ${configName}`));
4914
+ console.log(pc12.dim(`Provider: ${provider}`));
4915
+ console.log(pc12.dim("Add the subscription auth plugin to package.json and install with your package manager:"));
4916
+ console.log(pc12.dim(' "devDependencies": { "@vostride/agent-qa-subscription-auth": "<agent-qa version>" }'));
4917
+ console.log(pc12.dim("Declare it in agent-qa.config.yaml under plugins.auth:"));
4918
+ console.log(pc12.dim(" plugins:"));
4919
+ console.log(pc12.dim(" auth:"));
4920
+ console.log(pc12.dim(" - package: @vostride/agent-qa-subscription-auth"));
4921
+ console.log(pc12.dim("Then authenticate from agent-qa dashboard or rerun this command."));
4773
4922
  }
4774
4923
  async function readTokensFromPlugin(provider, plugin, started) {
4775
4924
  if (started.waitForTokens) {
@@ -4777,19 +4926,19 @@ async function readTokensFromPlugin(provider, plugin, started) {
4777
4926
  }
4778
4927
  if (plugin.dashboardAuth.mode === "manual-code") {
4779
4928
  if (!plugin.exchangeCode) {
4780
- console.log(pc11.red(`Provider "${provider}" does not support CLI code exchange. Use agent-qa dashboard.`));
4929
+ console.log(pc12.red(`Provider "${provider}" does not support CLI code exchange. Use agent-qa dashboard.`));
4781
4930
  process.exitCode = 1;
4782
4931
  return null;
4783
4932
  }
4784
4933
  const code = (await input2({ message: "Authorization code:" })).trim();
4785
4934
  if (!code) {
4786
- console.log(pc11.red("Authorization code is required"));
4935
+ console.log(pc12.red("Authorization code is required"));
4787
4936
  process.exitCode = 1;
4788
4937
  return null;
4789
4938
  }
4790
4939
  return await plugin.exchangeCode({ code, sessionState: started.sessionState });
4791
4940
  }
4792
- console.log(pc11.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4941
+ console.log(pc12.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4793
4942
  process.exitCode = 1;
4794
4943
  return null;
4795
4944
  }
@@ -4802,14 +4951,14 @@ async function runSubscriptionPluginLogin(configName, provider) {
4802
4951
  return;
4803
4952
  }
4804
4953
  if (!plugin.startAuth) {
4805
- console.log(pc11.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4954
+ console.log(pc12.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4806
4955
  process.exitCode = 1;
4807
4956
  return;
4808
4957
  }
4809
4958
  let started;
4810
4959
  try {
4811
4960
  started = await plugin.startAuth({ configName });
4812
- console.log(`Open this URL to authenticate: ${pc11.cyan(started.authorizeUrl)}`);
4961
+ console.log(`Open this URL to authenticate: ${pc12.cyan(started.authorizeUrl)}`);
4813
4962
  openAuthorizationUrl(started.authorizeUrl);
4814
4963
  const tokens = await readTokensFromPlugin(provider, plugin, started);
4815
4964
  if (!tokens) return;
@@ -4818,18 +4967,18 @@ async function runSubscriptionPluginLogin(configName, provider) {
4818
4967
  provider: plugin.credentialProviderId,
4819
4968
  tokens
4820
4969
  });
4821
- console.log(pc11.green(`Authenticated ${configName} with ${plugin.label}`));
4970
+ console.log(pc12.green(`Authenticated ${configName} with ${plugin.label}`));
4822
4971
  } catch (err) {
4823
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
4972
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4824
4973
  process.exitCode = 1;
4825
4974
  } finally {
4826
4975
  started?.cleanup?.();
4827
4976
  }
4828
4977
  }
4829
4978
  function createAuthCommand() {
4830
- const cmd = new Command11("auth").description("Manage LLM config credentials");
4979
+ const cmd = new Command12("auth").description("Manage LLM config credentials");
4831
4980
  cmd.addCommand(
4832
- new Command11("login").description("Authenticate with a subscription provider").requiredOption("--config <name>", "named subscription LLM config to authenticate").action(async (opts, command) => {
4981
+ new Command12("login").description("Authenticate with a subscription provider").requiredOption("--config <name>", "named subscription LLM config to authenticate").action(async (opts, command) => {
4833
4982
  const { resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4834
4983
  const configPath = getGlobalConfigPath2(command);
4835
4984
  let provider;
@@ -4837,13 +4986,13 @@ function createAuthCommand() {
4837
4986
  try {
4838
4987
  const requestedName = normalizeConfigName(opts.config);
4839
4988
  if (!requestedName) {
4840
- console.log(pc11.red("configName is required"));
4989
+ console.log(pc12.red("configName is required"));
4841
4990
  return;
4842
4991
  }
4843
4992
  const resolved = await resolveNamedConfig2(requestedName, configPath);
4844
4993
  const resolvedName = normalizeConfigName(resolved.config.name);
4845
4994
  if (!resolvedName) {
4846
- console.log(pc11.red("Config name is invalid"));
4995
+ console.log(pc12.red("Config name is invalid"));
4847
4996
  return;
4848
4997
  }
4849
4998
  configName = resolvedName;
@@ -4854,17 +5003,17 @@ function createAuthCommand() {
4854
5003
  }
4855
5004
  provider = configProvider;
4856
5005
  } catch (err) {
4857
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
5006
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4858
5007
  return;
4859
5008
  }
4860
5009
  await runSubscriptionPluginLogin(configName, provider);
4861
5010
  })
4862
5011
  );
4863
5012
  cmd.addCommand(
4864
- new Command11("set").description("Save a credential for a named LLM config").requiredOption("--config <name>", "named LLM config to save a credential for").requiredOption("--type <type>", "credential type: api-key or bearer-token").argument("[secret]", "credential secret; prompted when omitted").action(async (secretArg, opts, command) => {
5013
+ new Command12("set").description("Save a credential for a named LLM config").requiredOption("--config <name>", "named LLM config to save a credential for").requiredOption("--type <type>", "credential type: api-key or bearer-token").argument("[secret]", "credential secret; prompted when omitted").action(async (secretArg, opts, command) => {
4865
5014
  const credentialType = opts.type.trim();
4866
5015
  if (credentialType !== "api-key" && credentialType !== "bearer-token") {
4867
- console.log(pc11.red("type must be api-key or bearer-token"));
5016
+ console.log(pc12.red("type must be api-key or bearer-token"));
4868
5017
  return;
4869
5018
  }
4870
5019
  const { resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
@@ -4873,44 +5022,44 @@ function createAuthCommand() {
4873
5022
  try {
4874
5023
  resolved = await resolveNamedConfig2(opts.config, configPath);
4875
5024
  } catch (err) {
4876
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
5025
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4877
5026
  return;
4878
5027
  }
4879
5028
  const configName = resolved.config.name ?? opts.config;
4880
5029
  const provider = resolved.config.provider;
4881
5030
  if (SUBSCRIPTION_PROVIDERS2.has(provider)) {
4882
- console.log(pc11.red("Subscription providers use OAuth login"));
5031
+ console.log(pc12.red("Subscription providers use OAuth login"));
4883
5032
  return;
4884
5033
  }
4885
5034
  if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
4886
- console.log(pc11.red("bearer-token credentials are only supported for anthropic-compatible configs"));
5035
+ console.log(pc12.red("bearer-token credentials are only supported for anthropic-compatible configs"));
4887
5036
  return;
4888
5037
  }
4889
5038
  if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
4890
- console.log(pc11.red("api-key credentials are not supported for this provider"));
5039
+ console.log(pc12.red("api-key credentials are not supported for this provider"));
4891
5040
  return;
4892
5041
  }
4893
5042
  const secret = (secretArg ?? await password({ message: "Secret:" })).trim();
4894
5043
  if (!secret) {
4895
- console.log(pc11.red("Secret is required"));
5044
+ console.log(pc12.red("Secret is required"));
4896
5045
  return;
4897
5046
  }
4898
5047
  const { writeAuth } = await import("@vostride/agent-qa-core");
4899
5048
  if (credentialType === "bearer-token") {
4900
5049
  await writeAuth(configName, { type: "bearer", provider: "anthropic-compatible", token: secret });
4901
- console.log(pc11.green(`Saved bearer token for ${configName}`));
5050
+ console.log(pc12.green(`Saved bearer token for ${configName}`));
4902
5051
  } else {
4903
5052
  await writeAuth(configName, { type: "api", provider, key: secret });
4904
- console.log(pc11.green(`Saved API key for ${configName}`));
5053
+ console.log(pc12.green(`Saved API key for ${configName}`));
4905
5054
  }
4906
5055
  })
4907
5056
  );
4908
5057
  cmd.addCommand(
4909
- new Command11("status").description("Show credential status for configured LLMs").action(async function() {
5058
+ new Command12("status").description("Show credential status for configured LLMs").action(async function() {
4910
5059
  const { loadAuthPluginsForRawConfig: loadAuthPluginsForRawConfig2, resolveModelAuth: resolveModelAuth2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4911
5060
  const { loadConfigFile: loadConfigFile2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4912
5061
  const configPath = getGlobalConfigPath2(this) ?? "agent-qa.config.yaml";
4913
- console.log(pc11.bold("Auth status:\n"));
5062
+ console.log(pc12.bold("Auth status:\n"));
4914
5063
  let llms;
4915
5064
  let defaultLLM;
4916
5065
  try {
@@ -4921,25 +5070,25 @@ function createAuthCommand() {
4921
5070
  } catch {
4922
5071
  }
4923
5072
  if (!llms?.length) {
4924
- console.log(pc11.dim(" No LLM configs found"));
5073
+ console.log(pc12.dim(" No LLM configs found"));
4925
5074
  return;
4926
5075
  }
4927
5076
  const maxNameLen = Math.max(...llms.map((c) => String(c.name ?? "").length), 4) + 12;
4928
5077
  for (const llm of llms) {
4929
5078
  const name = String(llm.name ?? "");
4930
5079
  const isDefault = name === defaultLLM;
4931
- const label = isDefault ? `${name} ${pc11.dim("(default)")}` : name;
5080
+ const label = isDefault ? `${name} ${pc12.dim("(default)")}` : name;
4932
5081
  const rawLabel = isDefault ? `${name} (default)` : name;
4933
5082
  const padLen = Math.max(0, maxNameLen - rawLabel.length);
4934
5083
  const dots = ".".repeat(padLen + 2);
4935
5084
  const provider = llm.provider;
4936
5085
  const status = statusForResolvedAuth(await resolveModelAuth2(name, llm));
4937
- console.log(` ${label} ${pc11.dim(`[${providerLabel(provider)}]`)} ${pc11.dim(dots)} ${status}`);
5086
+ console.log(` ${label} ${pc12.dim(`[${providerLabel(provider)}]`)} ${pc12.dim(dots)} ${status}`);
4938
5087
  }
4939
5088
  })
4940
5089
  );
4941
5090
  cmd.addCommand(
4942
- new Command11("logout").description("Remove stored credentials for a named config").option("--config <name>", "named LLM config to log out from").action(async (opts, command) => {
5091
+ new Command12("logout").description("Remove stored credentials for a named config").option("--config <name>", "named LLM config to log out from").action(async (opts, command) => {
4943
5092
  const { removeAuth } = await import("@vostride/agent-qa-core");
4944
5093
  const { resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4945
5094
  const configPath = getGlobalConfigPath2(command);
@@ -4951,16 +5100,16 @@ function createAuthCommand() {
4951
5100
  const resolved = await resolveNamedConfig2(void 0, configPath);
4952
5101
  storeKey = resolved.config.name;
4953
5102
  } catch {
4954
- console.log(pc11.red("Specify --config <name> or run with a config file"));
5103
+ console.log(pc12.red("Specify --config <name> or run with a config file"));
4955
5104
  return;
4956
5105
  }
4957
5106
  }
4958
5107
  await removeAuth(storeKey);
4959
- console.log(pc11.green(`Logged out from ${storeKey}`));
5108
+ console.log(pc12.green(`Logged out from ${storeKey}`));
4960
5109
  })
4961
5110
  );
4962
5111
  cmd.addCommand(
4963
- new Command11("test").description("Test LLM connection using named config credentials").option("--config <name>", "named LLM config to test").option("--provider <name>", "override config provider").option("--model <name>", "override config model").action(async (opts, command) => {
5112
+ new Command12("test").description("Test LLM connection using named config credentials").option("--config <name>", "named LLM config to test").option("--provider <name>", "override config provider").option("--model <name>", "override config model").action(async (opts, command) => {
4964
5113
  const { resolveModelAuth: resolveModelAuth2, resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4965
5114
  const configPath = getGlobalConfigPath2(command);
4966
5115
  let planner;
@@ -4971,19 +5120,19 @@ function createAuthCommand() {
4971
5120
  planner = modelFields;
4972
5121
  configName = name;
4973
5122
  } catch (err) {
4974
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
5123
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4975
5124
  return;
4976
5125
  }
4977
5126
  if (opts.provider) planner = { ...planner, provider: opts.provider };
4978
5127
  if (opts.model) planner = { ...planner, model: opts.model };
4979
- console.log(pc11.dim(`Testing config: ${configName}...`));
5128
+ console.log(pc12.dim(`Testing config: ${configName}...`));
4980
5129
  const auth = await resolveModelAuth2(configName, planner);
4981
5130
  if (auth.kind === "missing") {
4982
- console.log(pc11.red(auth.message));
5131
+ console.log(pc12.red(auth.message));
4983
5132
  return;
4984
5133
  }
4985
5134
  if (auth.kind === "unauthenticated") {
4986
- console.log(pc11.dim(auth.message));
5135
+ console.log(pc12.dim(auth.message));
4987
5136
  }
4988
5137
  const { createModel, getProviderOptions } = await import("@vostride/agent-qa-core");
4989
5138
  const { generateText } = await import("ai");
@@ -5001,24 +5150,24 @@ function createAuthCommand() {
5001
5150
  ...providerOpts ? { providerOptions: providerOpts } : {}
5002
5151
  });
5003
5152
  const elapsed = Date.now() - start;
5004
- console.log(pc11.green(`Connected to ${planner.provider}/${planner.model} (${elapsed}ms)`));
5153
+ console.log(pc12.green(`Connected to ${planner.provider}/${planner.model} (${elapsed}ms)`));
5005
5154
  } catch (err) {
5006
5155
  const { message, statusCode } = extractProviderError(err);
5007
5156
  const fullContext = `${message} ${statusCode ?? ""}`;
5008
5157
  if (/auth|unauthorized|401|invalid.*key|permission/i.test(fullContext)) {
5009
- console.log(pc11.red("Authentication failed. Check the saved credential for this config."));
5158
+ console.log(pc12.red("Authentication failed. Check the saved credential for this config."));
5010
5159
  } else if (/model.*not.*support|not found|404|does not exist/i.test(fullContext)) {
5011
- console.log(pc11.red("Model not found. Check the model name."));
5160
+ console.log(pc12.red("Model not found. Check the model name."));
5012
5161
  } else if (/rate|429|quota|limit/i.test(fullContext)) {
5013
- console.log(pc11.red("Rate limited. Try again later."));
5162
+ console.log(pc12.red("Rate limited. Try again later."));
5014
5163
  } else if (/ECONNREFUSED|ENOTFOUND|timeout|abort|fetch failed|network/i.test(fullContext)) {
5015
- console.log(pc11.red("Network error. Check the exact base URL and try again."));
5164
+ console.log(pc12.red("Network error. Check the exact base URL and try again."));
5016
5165
  } else if (/invalid_request|400|bad request/i.test(fullContext)) {
5017
- console.log(pc11.red("Invalid request. Check model and provider settings."));
5166
+ console.log(pc12.red("Invalid request. Check model and provider settings."));
5018
5167
  } else {
5019
- console.log(pc11.red(`Connection failed. ${message}`));
5168
+ console.log(pc12.red(`Connection failed. ${message}`));
5020
5169
  }
5021
- console.log(pc11.dim(message));
5170
+ console.log(pc12.dim(message));
5022
5171
  } finally {
5023
5172
  clearTimeout(timeout);
5024
5173
  }
@@ -5029,35 +5178,35 @@ function createAuthCommand() {
5029
5178
 
5030
5179
  // src/commands/devices.ts
5031
5180
  init_config();
5032
- import { Command as Command12 } from "commander";
5033
- import { execSync as execSync3 } from "child_process";
5181
+ import { Command as Command13 } from "commander";
5182
+ import { execSync as execSync2 } from "child_process";
5034
5183
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5035
5184
  import { join as join5 } from "path";
5036
5185
  import { stringify as stringify2 } from "yaml";
5037
- import pc12 from "picocolors";
5186
+ import pc13 from "picocolors";
5038
5187
  function formatStatus2(transport, localBinding) {
5039
5188
  if (transport !== "local") {
5040
- return pc12.green("Ready") + pc12.dim(" (no local binding required)");
5189
+ return pc13.green("Ready") + pc13.dim(" (no local binding required)");
5041
5190
  }
5042
5191
  if (!localBinding) {
5043
- return pc12.red("UNBOUND") + pc12.yellow(" - run `agent-qa devices init`");
5192
+ return pc13.red("UNBOUND") + pc13.yellow(" - run `agent-qa devices init`");
5044
5193
  }
5045
5194
  const keys = Object.entries(localBinding).slice(0, 2);
5046
5195
  const details = keys.map(([k, v]) => `${k}: ${v}`).join(", ");
5047
- return pc12.green("Bound") + pc12.dim(` (${details})`);
5196
+ return pc13.green("Bound") + pc13.dim(` (${details})`);
5048
5197
  }
5049
5198
  async function listDevices() {
5050
5199
  let config;
5051
5200
  try {
5052
5201
  config = await resolveConfig({});
5053
5202
  } catch (err) {
5054
- console.error(pc12.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
5203
+ console.error(pc13.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
5055
5204
  process.exitCode = 1;
5056
5205
  return;
5057
5206
  }
5058
5207
  const devices = config.registry?.devices;
5059
5208
  if (!devices || Object.keys(devices).length === 0) {
5060
- console.log(pc12.yellow("No devices configured in registry.devices"));
5209
+ console.log(pc13.yellow("No devices configured in registry.devices"));
5061
5210
  return;
5062
5211
  }
5063
5212
  const localBindings = loadLocalBindings();
@@ -5065,13 +5214,13 @@ async function listDevices() {
5065
5214
  const platWidth = 10;
5066
5215
  const transWidth = 14;
5067
5216
  console.log("");
5068
- console.log(pc12.bold(" Configured Devices"));
5217
+ console.log(pc13.bold(" Configured Devices"));
5069
5218
  console.log("");
5070
5219
  console.log(
5071
- ` ${pc12.dim("Name".padEnd(nameWidth))} ${pc12.dim("Platform".padEnd(platWidth))} ${pc12.dim("Transport".padEnd(transWidth))} ${pc12.dim("Status")}`
5220
+ ` ${pc13.dim("Name".padEnd(nameWidth))} ${pc13.dim("Platform".padEnd(platWidth))} ${pc13.dim("Transport".padEnd(transWidth))} ${pc13.dim("Status")}`
5072
5221
  );
5073
5222
  console.log(
5074
- ` ${pc12.dim("\u2500".repeat(nameWidth))} ${pc12.dim("\u2500".repeat(platWidth))} ${pc12.dim("\u2500".repeat(transWidth))} ${pc12.dim("\u2500".repeat(30))}`
5223
+ ` ${pc13.dim("\u2500".repeat(nameWidth))} ${pc13.dim("\u2500".repeat(platWidth))} ${pc13.dim("\u2500".repeat(transWidth))} ${pc13.dim("\u2500".repeat(30))}`
5075
5224
  );
5076
5225
  for (const [name, profile] of Object.entries(devices)) {
5077
5226
  const localBinding = localBindings?.devices?.[name] ?? null;
@@ -5084,7 +5233,7 @@ async function listDevices() {
5084
5233
  }
5085
5234
  function scanAdbDevices() {
5086
5235
  try {
5087
- const output = execSync3("adb devices -l", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5236
+ const output = execSync2("adb devices -l", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5088
5237
  const lines = output.split("\n").slice(1);
5089
5238
  const results = [];
5090
5239
  for (const line of lines) {
@@ -5103,7 +5252,7 @@ function scanAdbDevices() {
5103
5252
  }
5104
5253
  function scanSimctlDevices() {
5105
5254
  try {
5106
- const output = execSync3("xcrun simctl list devices --json", {
5255
+ const output = execSync2("xcrun simctl list devices --json", {
5107
5256
  encoding: "utf-8",
5108
5257
  stdio: ["pipe", "pipe", "pipe"]
5109
5258
  });
@@ -5135,28 +5284,28 @@ async function initDevices() {
5135
5284
  const dir = process.cwd();
5136
5285
  const localFilePath = join5(dir, "agent-qa.local.yaml");
5137
5286
  if (existsSync3(localFilePath)) {
5138
- console.log(pc12.yellow("agent-qa.local.yaml already exists. Skipping generation."));
5287
+ console.log(pc13.yellow("agent-qa.local.yaml already exists. Skipping generation."));
5139
5288
  return;
5140
5289
  }
5141
- console.log(pc12.bold("\n Scanning for connected devices...\n"));
5290
+ console.log(pc13.bold("\n Scanning for connected devices...\n"));
5142
5291
  const adbDevices = scanAdbDevices();
5143
5292
  const simctlDevices = scanSimctlDevices();
5144
5293
  if (adbDevices.length > 0) {
5145
- console.log(` ${pc12.green("Android:")} found ${adbDevices.length} device(s)`);
5294
+ console.log(` ${pc13.green("Android:")} found ${adbDevices.length} device(s)`);
5146
5295
  for (const d of adbDevices) {
5147
- console.log(` ${pc12.dim("\u2022")} ${d.model} (${d.serial})`);
5296
+ console.log(` ${pc13.dim("\u2022")} ${d.model} (${d.serial})`);
5148
5297
  }
5149
5298
  } else {
5150
- console.log(` ${pc12.dim("Android:")} no devices found (adb not available or no devices connected)`);
5299
+ console.log(` ${pc13.dim("Android:")} no devices found (adb not available or no devices connected)`);
5151
5300
  }
5152
5301
  if (simctlDevices.length > 0) {
5153
5302
  const booted = simctlDevices.filter((d) => d.state === "Booted");
5154
- console.log(` ${pc12.green("iOS:")} found ${simctlDevices.length} simulator(s) (${booted.length} booted)`);
5303
+ console.log(` ${pc13.green("iOS:")} found ${simctlDevices.length} simulator(s) (${booted.length} booted)`);
5155
5304
  for (const d of booted) {
5156
- console.log(` ${pc12.dim("\u2022")} ${d.name} (${d.udid})`);
5305
+ console.log(` ${pc13.dim("\u2022")} ${d.name} (${d.udid})`);
5157
5306
  }
5158
5307
  } else {
5159
- console.log(` ${pc12.dim("iOS:")} no simulators found (simctl not available or none installed)`);
5308
+ console.log(` ${pc13.dim("iOS:")} no simulators found (simctl not available or none installed)`);
5160
5309
  }
5161
5310
  let config;
5162
5311
  try {
@@ -5190,20 +5339,20 @@ async function initDevices() {
5190
5339
  writeFileSync2(localFilePath, yamlContent);
5191
5340
  appendGitignoreEntry(dir, "agent-qa.local.yaml");
5192
5341
  console.log("");
5193
- console.log(pc12.green(` Generated agent-qa.local.yaml with ${bindCount} device binding(s)`));
5194
- console.log(pc12.dim(" Added agent-qa.local.yaml to .gitignore"));
5342
+ console.log(pc13.green(` Generated agent-qa.local.yaml with ${bindCount} device binding(s)`));
5343
+ console.log(pc13.dim(" Added agent-qa.local.yaml to .gitignore"));
5195
5344
  console.log("");
5196
5345
  }
5197
5346
  function createDevicesCommand() {
5198
- const cmd = new Command12("devices").description("Manage device profiles and local bindings");
5347
+ const cmd = new Command13("devices").description("Manage device profiles and local bindings");
5199
5348
  cmd.command("list").description("Show configured devices with merged shared + local view").action(listDevices);
5200
5349
  cmd.command("init").description("Scan connected devices and generate agent-qa.local.yaml").action(initDevices);
5201
5350
  return cmd;
5202
5351
  }
5203
5352
 
5204
5353
  // src/commands/ids.ts
5205
- import { Command as Command13 } from "commander";
5206
- import pc13 from "picocolors";
5354
+ import { Command as Command14 } from "commander";
5355
+ import pc14 from "picocolors";
5207
5356
  import {
5208
5357
  ENTITY_ID_TYPES,
5209
5358
  generateCanonicalId,
@@ -5217,7 +5366,7 @@ function parseEntityType(value) {
5217
5366
  throw new Error(`Unknown ID type "${value}". Expected one of: ${ENTITY_ID_TYPES.join(", ")}`);
5218
5367
  }
5219
5368
  function createIdsCommand() {
5220
- const cmd = new Command13("ids").description("Generate and validate canonical agent-qa entity IDs");
5369
+ const cmd = new Command14("ids").description("Generate and validate canonical agent-qa entity IDs");
5221
5370
  cmd.command("generate").description("Generate a canonical agent-qa ID using id-agent").argument("<type>", `entity type: ${ENTITY_ID_TYPES.join("|")}`).option("--json", "print structured JSON").action((typeValue, opts) => {
5222
5371
  try {
5223
5372
  const type = parseEntityType(typeValue);
@@ -5229,7 +5378,7 @@ function createIdsCommand() {
5229
5378
  console.log(id);
5230
5379
  }
5231
5380
  } catch (err) {
5232
- console.error(pc13.red(err instanceof Error ? err.message : String(err)));
5381
+ console.error(pc14.red(err instanceof Error ? err.message : String(err)));
5233
5382
  process.exitCode = 1;
5234
5383
  }
5235
5384
  });
@@ -5248,15 +5397,15 @@ function createIdsCommand() {
5248
5397
  if (opts.json) {
5249
5398
  console.log(JSON.stringify(result, null, 2));
5250
5399
  } else if (valid) {
5251
- console.log(pc13.green(result.message));
5400
+ console.log(pc14.green(result.message));
5252
5401
  } else {
5253
- console.error(pc13.red(result.message));
5402
+ console.error(pc14.red(result.message));
5254
5403
  }
5255
5404
  if (!valid) {
5256
5405
  process.exitCode = 1;
5257
5406
  }
5258
5407
  } catch (err) {
5259
- console.error(pc13.red(err instanceof Error ? err.message : String(err)));
5408
+ console.error(pc14.red(err instanceof Error ? err.message : String(err)));
5260
5409
  process.exitCode = 1;
5261
5410
  }
5262
5411
  });
@@ -5264,18 +5413,18 @@ function createIdsCommand() {
5264
5413
  }
5265
5414
 
5266
5415
  // src/commands/create-test.ts
5267
- import { Command as Command14 } from "commander";
5416
+ import { Command as Command15 } from "commander";
5268
5417
  import { generateTestId } from "@vostride/agent-qa-ids";
5269
5418
  import { stringify as stringify3 } from "yaml";
5270
5419
  import { writeFile as writeFile2, mkdir } from "fs/promises";
5271
5420
  import { existsSync as existsSync4 } from "fs";
5272
- import { resolve as resolve4, dirname as dirname5 } from "path";
5273
- import pc14 from "picocolors";
5421
+ import { resolve as resolve6, dirname as dirname7 } from "path";
5422
+ import pc15 from "picocolors";
5274
5423
  function createCreateTestCommand() {
5275
- const cmd = new Command14("create-test").description("Scaffold a new test YAML file with auto-generated ID").argument("<path>", "output file path for the test YAML").action(async (path) => {
5276
- const resolved = resolve4(path);
5424
+ const cmd = new Command15("create-test").description("Scaffold a new test YAML file with auto-generated ID").argument("<path>", "output file path for the test YAML").action(async (path) => {
5425
+ const resolved = resolve6(path);
5277
5426
  if (existsSync4(resolved)) {
5278
- console.error(pc14.red("Error: File already exists: " + resolved));
5427
+ console.error(pc15.red("Error: File already exists: " + resolved));
5279
5428
  process.exitCode = 1;
5280
5429
  return;
5281
5430
  }
@@ -5287,27 +5436,27 @@ function createCreateTestCommand() {
5287
5436
  steps: ["Describe your first test step here"]
5288
5437
  };
5289
5438
  const yamlContent = stringify3(template);
5290
- await mkdir(dirname5(resolved), { recursive: true });
5439
+ await mkdir(dirname7(resolved), { recursive: true });
5291
5440
  await writeFile2(resolved, yamlContent, "utf-8");
5292
- console.log(pc14.green("Created " + resolved));
5293
- console.log(pc14.dim(" test-id: " + testId));
5441
+ console.log(pc15.green("Created " + resolved));
5442
+ console.log(pc15.dim(" test-id: " + testId));
5294
5443
  });
5295
5444
  return cmd;
5296
5445
  }
5297
5446
 
5298
5447
  // src/commands/create-suite.ts
5299
- import { Command as Command15 } from "commander";
5448
+ import { Command as Command16 } from "commander";
5300
5449
  import { generateSuiteId } from "@vostride/agent-qa-ids";
5301
5450
  import { stringify as stringify4 } from "yaml";
5302
5451
  import { writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
5303
5452
  import { existsSync as existsSync5 } from "fs";
5304
- import { resolve as resolve5, dirname as dirname6 } from "path";
5305
- import pc15 from "picocolors";
5453
+ import { resolve as resolve7, dirname as dirname8 } from "path";
5454
+ import pc16 from "picocolors";
5306
5455
  function createCreateSuiteCommand() {
5307
- const cmd = new Command15("create-suite").description("Scaffold a new suite YAML file with auto-generated ID").argument("<path>", "output file path for the suite YAML").action(async (path) => {
5308
- const resolved = resolve5(path);
5456
+ const cmd = new Command16("create-suite").description("Scaffold a new suite YAML file with auto-generated ID").argument("<path>", "output file path for the suite YAML").action(async (path) => {
5457
+ const resolved = resolve7(path);
5309
5458
  if (existsSync5(resolved)) {
5310
- console.error(pc15.red("Error: File already exists: " + resolved));
5459
+ console.error(pc16.red("Error: File already exists: " + resolved));
5311
5460
  process.exitCode = 1;
5312
5461
  return;
5313
5462
  }
@@ -5321,18 +5470,18 @@ function createCreateSuiteCommand() {
5321
5470
  "tests: []",
5322
5471
  "tests: []\n # - test: ./path/to/test.yaml\n # id: my-test-id"
5323
5472
  );
5324
- await mkdir2(dirname6(resolved), { recursive: true });
5473
+ await mkdir2(dirname8(resolved), { recursive: true });
5325
5474
  await writeFile3(resolved, yamlContent, "utf-8");
5326
- console.log(pc15.green("Created " + resolved));
5327
- console.log(pc15.dim(" suite-id: " + suiteId));
5475
+ console.log(pc16.green("Created " + resolved));
5476
+ console.log(pc16.dim(" suite-id: " + suiteId));
5328
5477
  });
5329
5478
  return cmd;
5330
5479
  }
5331
5480
 
5332
5481
  // src/commands/clean-memory.ts
5333
5482
  init_config();
5334
- import { Command as Command16 } from "commander";
5335
- import pc16 from "picocolors";
5483
+ import { Command as Command17 } from "commander";
5484
+ import pc17 from "picocolors";
5336
5485
  import { createInterface as createInterface3 } from "readline";
5337
5486
  import { readFile as readFile4, readdir as readdir2, rm as rm2, stat as stat2 } from "fs/promises";
5338
5487
  import { existsSync as existsSync6 } from "fs";
@@ -5389,7 +5538,7 @@ async function getOrphanInfo(memoryRoot, tier, scope) {
5389
5538
  };
5390
5539
  }
5391
5540
  function createCleanMemoryCommand() {
5392
- const cmd = new Command16("clean-memory").description("Remove orphaned memory observation directories").option("-y, --yes", "skip confirmation prompt").action(async (_opts, command) => {
5541
+ const cmd = new Command17("clean-memory").description("Remove orphaned memory observation directories").option("-y, --yes", "skip confirmation prompt").action(async (_opts, command) => {
5393
5542
  const opts = command.opts();
5394
5543
  const configPath = command.parent?.opts()?.config ?? "agent-qa.config.yaml";
5395
5544
  const config = await resolveConfig({ configPath });
@@ -5422,7 +5571,7 @@ function createCleanMemoryCommand() {
5422
5571
  }
5423
5572
  const memoryRoot = resolveMemoryRoot2(config, workspace.configDir);
5424
5573
  if (!existsSync6(memoryRoot)) {
5425
- console.log(pc16.dim("No memory directory found."));
5574
+ console.log(pc17.dim("No memory directory found."));
5426
5575
  return;
5427
5576
  }
5428
5577
  const TIERS = [
@@ -5452,17 +5601,17 @@ function createCleanMemoryCommand() {
5452
5601
  }
5453
5602
  }
5454
5603
  if (orphans.length === 0) {
5455
- console.log(pc16.dim("No orphaned memory directories found."));
5604
+ console.log(pc17.dim("No orphaned memory directories found."));
5456
5605
  return;
5457
5606
  }
5458
5607
  const maxPathLen = Math.max("Path".length, ...orphans.map((o) => o.path.length));
5459
5608
  console.log("");
5460
- console.log(pc16.bold("Orphaned memory directories:"));
5609
+ console.log(pc17.bold("Orphaned memory directories:"));
5461
5610
  console.log("");
5462
- console.log(" " + pc16.dim("Path".padEnd(maxPathLen + 2) + "Files".padStart(7) + " " + "Last Confirmed".padEnd(20) + "Size".padStart(10)));
5463
- console.log(" " + pc16.dim("-".repeat(maxPathLen + 2 + 7 + 2 + 20 + 10)));
5611
+ console.log(" " + pc17.dim("Path".padEnd(maxPathLen + 2) + "Files".padStart(7) + " " + "Last Confirmed".padEnd(20) + "Size".padStart(10)));
5612
+ console.log(" " + pc17.dim("-".repeat(maxPathLen + 2 + 7 + 2 + 20 + 10)));
5464
5613
  for (const o of orphans) {
5465
- const confirmed = o.lastConfirmed ?? pc16.dim("never");
5614
+ const confirmed = o.lastConfirmed ?? pc17.dim("never");
5466
5615
  console.log(" " + o.path.padEnd(maxPathLen + 2) + String(o.fileCount).padStart(7) + " " + String(confirmed).padEnd(20) + formatSize(o.size).padStart(10));
5467
5616
  }
5468
5617
  console.log("");
@@ -5480,18 +5629,18 @@ function createCleanMemoryCommand() {
5480
5629
  for (const orphan of orphans) {
5481
5630
  await rm2(join6(memoryRoot, orphan.tier, orphan.scope), { recursive: true, force: true });
5482
5631
  }
5483
- console.log(pc16.green(`Deleted ${orphans.length} orphaned director${orphans.length === 1 ? "y" : "ies"}.`));
5632
+ console.log(pc17.green(`Deleted ${orphans.length} orphaned director${orphans.length === 1 ? "y" : "ies"}.`));
5484
5633
  });
5485
5634
  return cmd;
5486
5635
  }
5487
5636
 
5488
5637
  // src/commands/skills.ts
5489
- import { Command as Command17 } from "commander";
5638
+ import { Command as Command18 } from "commander";
5490
5639
  import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
5491
- import { dirname as dirname7, join as join7, resolve as resolve6 } from "path";
5640
+ import { dirname as dirname9, join as join7, resolve as resolve8 } from "path";
5492
5641
  import { fileURLToPath } from "url";
5493
5642
  function findPackageRoot(startDir) {
5494
- let current = resolve6(startDir);
5643
+ let current = resolve8(startDir);
5495
5644
  while (true) {
5496
5645
  const packagePath = join7(current, "package.json");
5497
5646
  if (existsSync7(packagePath)) {
@@ -5503,9 +5652,9 @@ function findPackageRoot(startDir) {
5503
5652
  } catch {
5504
5653
  }
5505
5654
  }
5506
- const parent = dirname7(current);
5655
+ const parent = dirname9(current);
5507
5656
  if (parent === current) {
5508
- return resolve6(startDir);
5657
+ return resolve8(startDir);
5509
5658
  }
5510
5659
  current = parent;
5511
5660
  }
@@ -5521,13 +5670,13 @@ function hasSkillDirectories(dir) {
5521
5670
  return false;
5522
5671
  }
5523
5672
  }
5524
- function resolveAgentQaSkillsDirectory(startDir = dirname7(fileURLToPath(import.meta.url))) {
5673
+ function resolveAgentQaSkillsDirectory(startDir = dirname9(fileURLToPath(import.meta.url))) {
5525
5674
  const packageRoot = findPackageRoot(startDir);
5526
5675
  const packagedSkills = join7(packageRoot, "skills");
5527
5676
  if (hasSkillDirectories(packagedSkills)) {
5528
5677
  return { path: packagedSkills, source: "package" };
5529
5678
  }
5530
- const sourceSkills = resolve6(packageRoot, "../..", "skills");
5679
+ const sourceSkills = resolve8(packageRoot, "../..", "skills");
5531
5680
  if (hasSkillDirectories(sourceSkills)) {
5532
5681
  return { path: sourceSkills, source: "source" };
5533
5682
  }
@@ -5555,7 +5704,7 @@ function listAgentQaSkills(skillsDir) {
5555
5704
  function createSkillsCommand(options = {}) {
5556
5705
  const stdout = options.stdout ?? process.stdout;
5557
5706
  const stderr = options.stderr ?? process.stderr;
5558
- return new Command17("skills").description("List packaged agent-qa skills").option("--json", "print machine-readable output").action((opts) => {
5707
+ return new Command18("skills").description("List packaged agent-qa skills").option("--json", "print machine-readable output").action((opts) => {
5559
5708
  const resolved = resolveAgentQaSkillsDirectory(options.startDir);
5560
5709
  const skills = listAgentQaSkills(resolved.path);
5561
5710
  if (resolved.source === "missing" || skills.length === 0) {
@@ -5636,13 +5785,14 @@ function rejectRemovedCliSurface(input3 = {}) {
5636
5785
 
5637
5786
  // src/cli.ts
5638
5787
  rejectRemovedCliSurface();
5639
- var program = new Command18();
5788
+ var program = new Command19();
5640
5789
  program.enablePositionalOptions();
5641
5790
  program.name("agent-qa").description("AI-powered testing framework").version(VERSION);
5642
5791
  program.option("--config <path>", "config file path", "agent-qa.config.yaml").option("--log-level <level>", "log verbosity: silent|error|warn|info|debug").option("--verbose", "shorthand for --log-level debug").option("--quiet", "shorthand for --log-level silent");
5643
5792
  program.addCommand(createRunCommand());
5644
5793
  program.addCommand(createInitCommand());
5645
5794
  program.addCommand(createInstallBrowsersCommand());
5795
+ program.addCommand(createInstallMobileDriversCommand());
5646
5796
  program.addCommand(createDoctorCommand());
5647
5797
  program.addCommand(createDashboardCommand());
5648
5798
  program.addCommand(createServeCommand());