agent-qa 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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";
@@ -1195,8 +986,8 @@ function createInitCommand() {
1195
986
  const capabilities = await checkbox({
1196
987
  message: "What platform will you test?",
1197
988
  choices: [
1198
- { value: "web", name: "Web", description: "Test web apps with Playwright" },
1199
- { value: "mobile", name: "Mobile", description: "Test Android or iOS apps with Appium" }
989
+ { value: "web", name: "Web", description: "Test web apps with Playwright", checked: true },
990
+ { value: "mobile", name: "Mobile", description: "Test Android or iOS apps with Appium", checked: false }
1200
991
  ],
1201
992
  required: true,
1202
993
  validate: (value) => value.length > 0 || "Select at least one platform."
@@ -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");
@@ -3067,6 +3216,7 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
3067
3216
  resolvedHooks: hooksContext?.resolvedHooks,
3068
3217
  sandboxOptions: hooksContext?.sandboxOptions,
3069
3218
  logCapture: suiteLogCapture,
3219
+ accessibility: config.services?.accessibility,
3070
3220
  screenshotSize: plannerCfg.screenshotSize,
3071
3221
  effectiveResolution: plannerCfg.effectiveResolution,
3072
3222
  memoryProvider: suiteMemoryProvider,
@@ -3131,19 +3281,19 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
3131
3281
  const batchOutcome = await flushSuiteParents();
3132
3282
  if (batchOutcome.cancelled || shutdownRequested) break;
3133
3283
  if (opts.bail && batchOutcome.failed) {
3134
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failed suite"));
3284
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failed suite"));
3135
3285
  break;
3136
3286
  }
3137
3287
  const outcome = await runOneSuiteFile(suiteFile, suite);
3138
3288
  if (outcome.cancelled || shutdownRequested) break;
3139
3289
  if (opts.bail && outcome.failed) {
3140
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failed suite"));
3290
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failed suite"));
3141
3291
  break;
3142
3292
  }
3143
3293
  }
3144
3294
  const finalBatchOutcome = await flushSuiteParents();
3145
3295
  if (opts.bail && finalBatchOutcome.failed) {
3146
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failed suite"));
3296
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failed suite"));
3147
3297
  }
3148
3298
  process.removeListener("SIGINT", handleShutdown);
3149
3299
  process.removeListener("SIGTERM", handleShutdown);
@@ -3151,12 +3301,12 @@ async function executeSuites(suiteFiles, opts, config, globalOpts, configContent
3151
3301
  return suiteResults.some((r) => r.status === "failed" || r.status === "cancelled") || shutdownRequested;
3152
3302
  }
3153
3303
  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) => {
3304
+ 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
3305
  let _crashDb;
3156
3306
  try {
3157
3307
  const modeFlags = [opts.test, opts.suite, opts.all].filter(Boolean).length;
3158
3308
  if (modeFlags > 1) {
3159
- console.error(pc6.red("Error: --test, --suite, and --all are mutually exclusive."));
3309
+ console.error(pc7.red("Error: --test, --suite, and --all are mutually exclusive."));
3160
3310
  process.exit(1);
3161
3311
  }
3162
3312
  const program2 = command.parent;
@@ -3197,7 +3347,7 @@ function createRunCommand() {
3197
3347
  console.warn(`Warning: duplicate --run-attr key "${duplicateKey}"; using last value`);
3198
3348
  }
3199
3349
  } catch (err) {
3200
- console.error(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3350
+ console.error(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3201
3351
  process.exit(2);
3202
3352
  }
3203
3353
  const inlineVars = {};
@@ -3213,29 +3363,29 @@ function createRunCommand() {
3213
3363
  let resolvedEnvFilePath = null;
3214
3364
  resolvedEnvFilePath = workspacePaths.envFile.absolutePath;
3215
3365
  if (!existsSync8(workspacePaths.envFile.absolutePath)) {
3216
- console.error(pc6.red(`Error: Env file not found: ${workspacePaths.envFile.absolutePath}`));
3366
+ console.error(pc7.red(`Error: Env file not found: ${workspacePaths.envFile.absolutePath}`));
3217
3367
  process.exit(1);
3218
3368
  }
3219
3369
  try {
3220
3370
  rawEnvFileContent = readFileSync5(workspacePaths.envFile.absolutePath, "utf-8");
3221
3371
  envFileVars = parseEnvFile(rawEnvFileContent);
3222
3372
  } catch {
3223
- console.error(pc6.red(`Error: Env file could not be read: ${workspacePaths.envFile.absolutePath}`));
3373
+ console.error(pc7.red(`Error: Env file could not be read: ${workspacePaths.envFile.absolutePath}`));
3224
3374
  process.exit(1);
3225
3375
  }
3226
3376
  let resolvedHooks;
3227
3377
  let sandboxOptions;
3228
3378
  const hooksFilePath = workspacePaths.hooksFile.absolutePath;
3229
3379
  if (!existsSync8(hooksFilePath)) {
3230
- console.error(pc6.red(`Error: Hooks file not found: ${hooksFilePath}`));
3380
+ console.error(pc7.red(`Error: Hooks file not found: ${hooksFilePath}`));
3231
3381
  process.exit(1);
3232
3382
  }
3233
3383
  try {
3234
3384
  const { parseHooksFile } = await import("@vostride/agent-qa-core");
3235
3385
  const { hooks, errors } = await parseHooksFile(hooksFilePath);
3236
3386
  if (errors.length > 0) {
3237
- console.error(pc6.red("Hooks configuration errors:"));
3238
- for (const err of errors) console.error(pc6.red(` - ${err}`));
3387
+ console.error(pc7.red("Hooks configuration errors:"));
3388
+ for (const err of errors) console.error(pc7.red(` - ${err}`));
3239
3389
  process.exit(1);
3240
3390
  }
3241
3391
  resolvedHooks = new Map(hooks.map((h) => [h.id, h]));
@@ -3244,8 +3394,8 @@ function createRunCommand() {
3244
3394
  secretRedactor: runtimeSecrets.secretRedactor
3245
3395
  };
3246
3396
  } 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));
3397
+ console.error(pc7.red(`Error: Hooks file could not be read: ${hooksFilePath}`));
3398
+ if (err instanceof Error && err.message) console.error(pc7.dim(err.message));
3249
3399
  process.exit(1);
3250
3400
  }
3251
3401
  let agentRulesContent;
@@ -3253,7 +3403,7 @@ function createRunCommand() {
3253
3403
  try {
3254
3404
  agentRulesContent = await readFileAsync(workspacePaths.agentRules.absolutePath, "utf-8");
3255
3405
  } catch {
3256
- console.error(pc6.red(`Error: Agent rules file not found: ${workspacePaths.agentRules.absolutePath}`));
3406
+ console.error(pc7.red(`Error: Agent rules file not found: ${workspacePaths.agentRules.absolutePath}`));
3257
3407
  process.exit(1);
3258
3408
  }
3259
3409
  const effectiveLogLevel = globalOpts.logLevel ?? (globalOpts.verbose ? "debug" : void 0) ?? (globalOpts.quiet ? "silent" : void 0) ?? config.services?.logging?.level ?? "warn";
@@ -3265,21 +3415,21 @@ function createRunCommand() {
3265
3415
  discoveredTestFiles = selected.tests;
3266
3416
  discoveredSuiteFiles = selected.suites;
3267
3417
  } catch (err) {
3268
- console.error(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3418
+ console.error(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
3269
3419
  process.exit(1);
3270
3420
  }
3271
3421
  if (discoveredTestFiles.length === 0 && discoveredSuiteFiles.length === 0) {
3272
3422
  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}`));
3423
+ console.error(pc7.red(`Error: No ${patternKind} files found matching patterns:`));
3424
+ for (const p of patterns) console.error(pc7.red(` ${p}`));
3275
3425
  process.exit(2);
3276
3426
  }
3277
3427
  } else if (opts.suite) {
3278
3428
  discoveredSuiteFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "suite" })).map((record) => record.absolutePath);
3279
3429
  if (discoveredSuiteFiles.length === 0) {
3280
- console.error(pc6.red("Error: No suite files found matching workspace.suiteMatch"));
3430
+ console.error(pc7.red("Error: No suite files found matching workspace.suiteMatch"));
3281
3431
  for (const p of workspacePaths.suiteMatch) {
3282
- console.error(pc6.red(` ${p}`));
3432
+ console.error(pc7.red(` ${p}`));
3283
3433
  }
3284
3434
  process.exit(2);
3285
3435
  }
@@ -3287,19 +3437,19 @@ function createRunCommand() {
3287
3437
  discoveredTestFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "test" })).map((record) => record.absolutePath);
3288
3438
  discoveredSuiteFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "suite" })).map((record) => record.absolutePath);
3289
3439
  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"));
3440
+ console.error(pc7.red("Error: No test or suite files found. Check workspace.testMatch and workspace.suiteMatch in agent-qa.config.yaml"));
3291
3441
  process.exit(2);
3292
3442
  }
3293
3443
  } else {
3294
3444
  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."));
3445
+ console.error(pc7.red("Error: No testMatch patterns in config. Add workspace.testMatch to agent-qa.config.yaml."));
3296
3446
  process.exit(2);
3297
3447
  }
3298
3448
  discoveredTestFiles = (await discoverWorkspaceFiles2({ workspace: workspacePaths, kind: "test" })).map((record) => record.absolutePath);
3299
3449
  if (discoveredTestFiles.length === 0) {
3300
- console.error(pc6.red("Error: No test files found matching workspace.testMatch"));
3450
+ console.error(pc7.red("Error: No test files found matching workspace.testMatch"));
3301
3451
  for (const p of workspacePaths.testMatch) {
3302
- console.error(pc6.red(` ${p}`));
3452
+ console.error(pc7.red(` ${p}`));
3303
3453
  }
3304
3454
  process.exit(2);
3305
3455
  }
@@ -3314,7 +3464,7 @@ function createRunCommand() {
3314
3464
  }
3315
3465
  const files = discoveredTestFiles;
3316
3466
  if (opts.listTests) {
3317
- console.log(pc6.bold(`
3467
+ console.log(pc7.bold(`
3318
3468
  Discovered ${files.length} test file(s):
3319
3469
  `));
3320
3470
  for (const file of files) {
@@ -3326,7 +3476,7 @@ Discovered ${files.length} test file(s):
3326
3476
  const dashboardCfg2 = config.services?.dashboard;
3327
3477
  const reporterSelectionResult = resolveReporterSelection(opts, Boolean(dashboardCfg2));
3328
3478
  if (reporterSelectionResult.error) {
3329
- console.error(pc6.red(`Error: ${reporterSelectionResult.error}`));
3479
+ console.error(pc7.red(`Error: ${reporterSelectionResult.error}`));
3330
3480
  process.exit(2);
3331
3481
  }
3332
3482
  const reporterSelection = reporterSelectionResult.selection;
@@ -3343,13 +3493,13 @@ Discovered ${files.length} test file(s):
3343
3493
  logStorage = dashboardDb;
3344
3494
  _crashDb = dashboardDb;
3345
3495
  } catch (err) {
3346
- console.warn(pc6.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3496
+ console.warn(pc7.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3347
3497
  }
3348
3498
  }
3349
3499
  const { parseAllTests, formatParseError } = await import("@vostride/agent-qa-core");
3350
3500
  const parseResult = await parseAllTests(files);
3351
3501
  if (parseResult.errors.length > 0) {
3352
- console.error(pc6.red(`Found ${parseResult.errors.length} parse error(s):
3502
+ console.error(pc7.red(`Found ${parseResult.errors.length} parse error(s):
3353
3503
  `));
3354
3504
  for (const err of parseResult.errors) {
3355
3505
  console.error(formatParseError(err));
@@ -3445,17 +3595,17 @@ Discovered ${files.length} test file(s):
3445
3595
  }
3446
3596
  const filtered = parseResult.tests;
3447
3597
  if (filtered.length === 0) {
3448
- console.log(pc6.yellow("No tests match filters"));
3598
+ console.log(pc7.yellow("No tests match filters"));
3449
3599
  process.exit(0);
3450
3600
  }
3451
3601
  if (opts.dryRun) {
3452
- console.log(pc6.bold(`
3602
+ console.log(pc7.bold(`
3453
3603
  Found ${filtered.length} test(s):
3454
3604
  `));
3455
3605
  for (const test of filtered) {
3456
- console.log(` ${pc6.green("\u25CF")} ${test.name}`);
3606
+ console.log(` ${pc7.green("\u25CF")} ${test.name}`);
3457
3607
  if (opts.configDebug) {
3458
- console.log(pc6.bold(`
3608
+ console.log(pc7.bold(`
3459
3609
  Config for: ${test.name}`));
3460
3610
  console.log(formatConfigDebug(config, test.use, flagOverrides));
3461
3611
  console.log();
@@ -3493,11 +3643,11 @@ Found ${filtered.length} test(s):
3493
3643
  if (reporterSelection.dashboard && dashboardDb) {
3494
3644
  try {
3495
3645
  const { DashboardReporter } = await import("@vostride/agent-qa-dashboard");
3496
- const { resolve: resolve7 } = await import("path");
3646
+ const { resolve: resolve9 } = await import("path");
3497
3647
  const dashArtifactsDir = config.services?.dashboard?.artifactsDir ?? RUNTIME_ARTIFACTS_DIR;
3498
3648
  reporters.push(new DashboardReporter({
3499
3649
  db: dashboardDb,
3500
- artifactsDir: resolve7(configDir, dashArtifactsDir),
3650
+ artifactsDir: resolve9(configDir, dashArtifactsDir),
3501
3651
  redactor: runtimeSecrets.secretRedactor,
3502
3652
  onRunCreated: (runId) => {
3503
3653
  currentRunId = runId;
@@ -3505,7 +3655,7 @@ Found ${filtered.length} test(s):
3505
3655
  }
3506
3656
  }));
3507
3657
  } catch (err) {
3508
- console.warn(pc6.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3658
+ console.warn(pc7.yellow(`Warning: Could not load dashboard reporter: ${err instanceof Error ? err.message : String(err)}`));
3509
3659
  }
3510
3660
  }
3511
3661
  const analyticsReporter = createAnalyticsRunReporter({ config, surface: "cli" });
@@ -3519,9 +3669,9 @@ Found ${filtered.length} test(s):
3519
3669
  });
3520
3670
  if (opts.cache !== false) {
3521
3671
  const { FileActionCache } = await import("@vostride/agent-qa-core");
3522
- const { resolve: resolve7 } = await import("path");
3672
+ const { resolve: resolve9 } = await import("path");
3523
3673
  actionCache = new FileActionCache({
3524
- dir: resolve7(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR),
3674
+ dir: resolve9(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR),
3525
3675
  ttl: config.services?.cache?.ttl ?? "7d",
3526
3676
  logger: logger.createScopedLogger("cache")
3527
3677
  });
@@ -3599,7 +3749,7 @@ Found ${filtered.length} test(s):
3599
3749
  const testMergedConfig = { ...config, use: mergedUse };
3600
3750
  const effectiveCacheEnabled = opts.cache !== false && mergedUse.cache !== false;
3601
3751
  if (opts.configDebug) {
3602
- console.log(pc6.bold(`
3752
+ console.log(pc7.bold(`
3603
3753
  Config for: ${test.name}`));
3604
3754
  console.log(formatConfigDebug(config, test.use, testFlagOverrides));
3605
3755
  console.log();
@@ -3709,7 +3859,7 @@ Config for: ${test.name}`));
3709
3859
  });
3710
3860
  }
3711
3861
  if (effectiveLogLevel === "debug") {
3712
- console.log(pc6.dim(` Platform: ${testPlatform}${useFarm ? ` (farm: ${testResolvedDevice?.transport})` : ""}${testPlatform !== "web" ? " (adapter re-created)" : ""}`));
3862
+ console.log(pc7.dim(` Platform: ${testPlatform}${useFarm ? ` (farm: ${testResolvedDevice?.transport})` : ""}${testPlatform !== "web" ? " (adapter re-created)" : ""}`));
3713
3863
  }
3714
3864
  }
3715
3865
  const mergedForLLM = { registry: config.registry, use: testMergedConfig.use ?? config.use };
@@ -3720,7 +3870,7 @@ Config for: ${test.name}`));
3720
3870
  process.env.AGENT_QA_LLM_MODEL = plannerCfg.model;
3721
3871
  process.env.AGENT_QA_LLM_PROVIDER = plannerCfg.provider;
3722
3872
  if (effectiveLogLevel === "debug") {
3723
- console.log(pc6.dim(` Model: ${plannerCfg.model} (${plannerCfg.provider})`));
3873
+ console.log(pc7.dim(` Model: ${plannerCfg.model} (${plannerCfg.provider})`));
3724
3874
  }
3725
3875
  const plannerModel = await createModel(plannerModelConfig);
3726
3876
  const verifierModel = await createModel(verifierModelConfig);
@@ -3885,7 +4035,7 @@ Config for: ${test.name}`));
3885
4035
  const testId = testDef["test-id"] ?? testDef.name;
3886
4036
  memoryInitParams = { product, testId, memoryRoot };
3887
4037
  } catch (err) {
3888
- console.warn(` ${pc6.dim("Memory:")} init error -- ${err.message}`);
4038
+ console.warn(` ${pc7.dim("Memory:")} init error -- ${err.message}`);
3889
4039
  }
3890
4040
  }
3891
4041
  const result = await runTestWithRetry(testDef, {
@@ -3915,6 +4065,7 @@ Config for: ${test.name}`));
3915
4065
  inlineHookDefs: resolvedHooks,
3916
4066
  inlineHookSandboxOptions: sandboxOptions,
3917
4067
  logCapture: testLogCapture,
4068
+ accessibility: config.services?.accessibility,
3918
4069
  screenshotSize: plannerCfg.screenshotSize,
3919
4070
  effectiveResolution: plannerCfg.effectiveResolution,
3920
4071
  contextWindow: plannerCfg.contextWindow,
@@ -3979,7 +4130,7 @@ Config for: ${test.name}`));
3979
4130
  try {
3980
4131
  const { shouldAblate, collectAllInjectedIds, deprecateOnFailure } = await import("@vostride/agent-qa-core");
3981
4132
  if (shouldAblate(result, memoryProvider)) {
3982
- console.log(` ${pc6.dim("Memory:")} ablation retry -- re-running without memory...`);
4133
+ console.log(` ${pc7.dim("Memory:")} ablation retry -- re-running without memory...`);
3983
4134
  const ablationAdapter = await createPlatformAdapter(testPlatform);
3984
4135
  const mergedBrowser = mergedUse.browser;
3985
4136
  const ablationPlatformConfig = buildPlatformConfig(testPlatform, testResolvedDevice, testTimeoutsForSetup, mergedBrowser ?? config.use?.browser, testMergedConfig.use?.logCapture ?? config.use?.logCapture, testFarmSession, testMobileResolved);
@@ -4016,6 +4167,7 @@ Config for: ${test.name}`));
4016
4167
  inlineHookDefs: resolvedHooks,
4017
4168
  inlineHookSandboxOptions: sandboxOptions,
4018
4169
  logCapture: testLogCapture,
4170
+ accessibility: config.services?.accessibility,
4019
4171
  screenshotSize: plannerCfg.screenshotSize,
4020
4172
  effectiveResolution: plannerCfg.effectiveResolution,
4021
4173
  contextWindow: plannerCfg.contextWindow
@@ -4024,7 +4176,7 @@ Config for: ${test.name}`));
4024
4176
  circuitBreaker.record({ withMemory: false, passed: ablationResult.status === "passed" });
4025
4177
  }
4026
4178
  if (ablationResult.status === "passed") {
4027
- console.log(` ${pc6.dim("Memory:")} ablation confirmed memory caused failure -- penalizing observations`);
4179
+ console.log(` ${pc7.dim("Memory:")} ablation confirmed memory caused failure -- penalizing observations`);
4028
4180
  const injectedMap = collectAllInjectedIds(result, memoryProvider);
4029
4181
  await deprecateOnFailure({
4030
4182
  testResult: result,
@@ -4035,14 +4187,14 @@ Config for: ${test.name}`));
4035
4187
  });
4036
4188
  ablationHandledDeprecation = true;
4037
4189
  } else {
4038
- console.log(` ${pc6.dim("Memory:")} ablation retry also failed -- memory not the cause`);
4190
+ console.log(` ${pc7.dim("Memory:")} ablation retry also failed -- memory not the cause`);
4039
4191
  }
4040
4192
  } finally {
4041
4193
  await ablationAdapter.cleanup();
4042
4194
  }
4043
4195
  }
4044
4196
  } catch (err) {
4045
- console.warn(` ${pc6.dim("Memory:")} ablation error -- ${err.message}`);
4197
+ console.warn(` ${pc7.dim("Memory:")} ablation error -- ${err.message}`);
4046
4198
  }
4047
4199
  }
4048
4200
  if (memoryProvider && memoryConfig?.curatorEnabled !== false && !ablationHandledDeprecation) {
@@ -4076,7 +4228,7 @@ Config for: ${test.name}`));
4076
4228
  }
4077
4229
  }
4078
4230
  } catch (err) {
4079
- console.warn(` ${pc6.dim("Memory:")} curator error -- ${err.message}`);
4231
+ console.warn(` ${pc7.dim("Memory:")} curator error -- ${err.message}`);
4080
4232
  }
4081
4233
  }
4082
4234
  results.push(result);
@@ -4087,13 +4239,13 @@ Config for: ${test.name}`));
4087
4239
  }
4088
4240
  if (circuitBreaker?.isTripped() && !circuitBreakerTripped) {
4089
4241
  circuitBreakerTripped = true;
4090
- console.log(` ${pc6.yellow("Memory: circuit breaker tripped -- disabling memory injection for remaining tests")}`);
4242
+ console.log(` ${pc7.yellow("Memory: circuit breaker tripped -- disabling memory injection for remaining tests")}`);
4091
4243
  }
4092
4244
  if (result.status === "failed" && actionCache && effectiveCacheEnabled && result.steps.length > 0) {
4093
4245
  const { hashStepInstruction: hashStepInstruction2 } = await import("@vostride/agent-qa-core");
4094
4246
  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);
4247
+ const { resolve: resolve9, join: join8 } = await import("path");
4248
+ const cacheDir = resolve9(configDir, config.services?.cache?.dir ?? RUNTIME_CACHE_DIR);
4097
4249
  const cacheLog = logger.createScopedLogger("cache");
4098
4250
  let purged = 0;
4099
4251
  for (let i = 0; i < result.steps.length; i++) {
@@ -4172,13 +4324,13 @@ Config for: ${test.name}`));
4172
4324
  }
4173
4325
  const parallelBatchFailed = await flushParallelRuns();
4174
4326
  if (opts.bail && parallelBatchFailed) {
4175
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failure"));
4327
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failure"));
4176
4328
  bailTriggered = true;
4177
4329
  break;
4178
4330
  }
4179
4331
  const outcome = await runOneDirectTest(test);
4180
4332
  if (opts.bail && outcome.failed) {
4181
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failure"));
4333
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failure"));
4182
4334
  bailTriggered = true;
4183
4335
  break;
4184
4336
  }
@@ -4186,7 +4338,7 @@ Config for: ${test.name}`));
4186
4338
  if (!bailTriggered) {
4187
4339
  const parallelBatchFailed = await flushParallelRuns();
4188
4340
  if (opts.bail && parallelBatchFailed) {
4189
- console.log(pc6.yellow("\nBailing out \u2014 stopping after first failure"));
4341
+ console.log(pc7.yellow("\nBailing out \u2014 stopping after first failure"));
4190
4342
  }
4191
4343
  }
4192
4344
  releaseAppium();
@@ -4205,7 +4357,7 @@ Config for: ${test.name}`));
4205
4357
  const errMsg = err instanceof Error ? err.message : String(err);
4206
4358
  const errStack = err instanceof Error ? err.stack ?? "" : "";
4207
4359
  const formattedError = formatFrameworkError(errMsg);
4208
- console.error(pc6.red(formattedError.displayMessage));
4360
+ console.error(pc7.red(formattedError.displayMessage));
4209
4361
  if (_crashDb) {
4210
4362
  const fullError = `${errMsg}
4211
4363
  ${errStack}`;
@@ -4259,10 +4411,10 @@ ${errStack}`;
4259
4411
  init_config();
4260
4412
  init_llm_utils();
4261
4413
  init_model_defaults();
4262
- import { Command as Command6 } from "commander";
4263
- import { resolve as resolve3, dirname as dirname4 } from "path";
4414
+ import { Command as Command7 } from "commander";
4415
+ import { resolve as resolve5, dirname as dirname6 } from "path";
4264
4416
  import { createRequire as createRequire2 } from "module";
4265
- import pc7 from "picocolors";
4417
+ import pc8 from "picocolors";
4266
4418
  import { flushAnalytics, resolveWorkspacePaths as resolveWorkspacePaths3 } from "@vostride/agent-qa-core";
4267
4419
  async function startDashboardBackedServices(opts, command, labels) {
4268
4420
  try {
@@ -4271,8 +4423,8 @@ async function startDashboardBackedServices(opts, command, labels) {
4271
4423
  const config = await resolveConfig({
4272
4424
  configPath: globalOpts.config
4273
4425
  });
4274
- const configFilePath = resolve3(globalOpts.config ?? "agent-qa.config.yaml");
4275
- const configDir = dirname4(configFilePath);
4426
+ const configFilePath = resolve5(globalOpts.config ?? "agent-qa.config.yaml");
4427
+ const configDir = dirname6(configFilePath);
4276
4428
  const workspacePaths = resolveWorkspacePaths3({
4277
4429
  config,
4278
4430
  configPath: configFilePath,
@@ -4312,17 +4464,17 @@ async function startDashboardBackedServices(opts, command, labels) {
4312
4464
  try {
4313
4465
  const require2 = createRequire2(import.meta.url);
4314
4466
  const pkgPath = require2.resolve("@vostride/agent-qa-dashboard-ui/package.json");
4315
- uiDir = resolve3(dirname4(pkgPath), "dist");
4467
+ uiDir = resolve5(dirname6(pkgPath), "dist");
4316
4468
  } catch {
4317
4469
  if (globalOpts.verbose) {
4318
- console.log(pc7.yellow("Warning: @vostride/agent-qa-dashboard-ui not found \u2014 API-only mode"));
4470
+ console.log(pc8.yellow("Warning: @vostride/agent-qa-dashboard-ui not found \u2014 API-only mode"));
4319
4471
  }
4320
4472
  }
4321
4473
  const { close } = await startServer({
4322
4474
  db,
4323
4475
  port,
4324
4476
  uiDir,
4325
- artifactsDir: resolve3(configDir, artifactsDir),
4477
+ artifactsDir: resolve5(configDir, artifactsDir),
4326
4478
  workspacePaths,
4327
4479
  configPath: configFilePath,
4328
4480
  mcp: mcpConfig,
@@ -4338,7 +4490,7 @@ async function startDashboardBackedServices(opts, command, labels) {
4338
4490
  const shutdown = async () => {
4339
4491
  if (shuttingDown) return;
4340
4492
  shuttingDown = true;
4341
- console.log(pc7.dim(`
4493
+ console.log(pc8.dim(`
4342
4494
  Shutting down ${labels.shutdown}...`));
4343
4495
  close();
4344
4496
  await flushAnalytics({ config }).catch(() => {
@@ -4348,12 +4500,12 @@ Shutting down ${labels.shutdown}...`));
4348
4500
  process.on("SIGINT", shutdown);
4349
4501
  process.on("SIGTERM", shutdown);
4350
4502
  } catch (err) {
4351
- console.error(pc7.red(`${labels.error} error: ${err instanceof Error ? err.message : String(err)}`));
4503
+ console.error(pc8.red(`${labels.error} error: ${err instanceof Error ? err.message : String(err)}`));
4352
4504
  process.exit(2);
4353
4505
  }
4354
4506
  }
4355
4507
  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) => {
4508
+ 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
4509
  await startDashboardBackedServices(opts, command, {
4358
4510
  error: "Dashboard",
4359
4511
  shutdown: "dashboard"
@@ -4362,7 +4514,7 @@ function createDashboardCommand() {
4362
4514
  return cmd;
4363
4515
  }
4364
4516
  function createServeCommand() {
4365
- const cmd = new Command6("serve").description("Start configured local agent-qa services").action(async (_opts, command) => {
4517
+ const cmd = new Command7("serve").description("Start configured local agent-qa services").action(async (_opts, command) => {
4366
4518
  await startDashboardBackedServices({}, command, {
4367
4519
  error: "Serve",
4368
4520
  shutdown: "local services"
@@ -4373,11 +4525,11 @@ function createServeCommand() {
4373
4525
 
4374
4526
  // src/commands/mcp.ts
4375
4527
  init_config();
4376
- import { Command as Command7 } from "commander";
4377
- import pc8 from "picocolors";
4528
+ import { Command as Command8 } from "commander";
4529
+ import pc9 from "picocolors";
4378
4530
  import { flushAnalytics as flushAnalytics2 } from "@vostride/agent-qa-core";
4379
4531
  function createMcpCommand() {
4380
- const cmd = new Command7("mcp").description("Start MCP (Model Context Protocol) server over stdio").action(async (command) => {
4532
+ const cmd = new Command8("mcp").description("Start MCP (Model Context Protocol) server over stdio").action(async (command) => {
4381
4533
  try {
4382
4534
  const program2 = command.parent;
4383
4535
  const globalOpts = program2?.opts() ?? {};
@@ -4395,7 +4547,7 @@ function createMcpCommand() {
4395
4547
  });
4396
4548
  }
4397
4549
  } catch (err) {
4398
- console.error(pc8.red("Failed to start MCP server:"), err instanceof Error ? err.message : String(err));
4550
+ console.error(pc9.red("Failed to start MCP server:"), err instanceof Error ? err.message : String(err));
4399
4551
  process.exitCode = 1;
4400
4552
  }
4401
4553
  });
@@ -4404,8 +4556,8 @@ function createMcpCommand() {
4404
4556
 
4405
4557
  // src/commands/config.ts
4406
4558
  init_config();
4407
- import { Command as Command8 } from "commander";
4408
- import pc9 from "picocolors";
4559
+ import { Command as Command9 } from "commander";
4560
+ import pc10 from "picocolors";
4409
4561
  import { parseDocument, Document } from "yaml";
4410
4562
  import { readFile as readFile3, writeFile } from "fs/promises";
4411
4563
  import { createInterface as createInterface2 } from "readline";
@@ -4434,10 +4586,10 @@ async function readStdin() {
4434
4586
  }
4435
4587
  function plainPrompt(question) {
4436
4588
  const rl = createInterface2({ input: process.stdin, output: process.stdout });
4437
- return new Promise((resolve7) => {
4589
+ return new Promise((resolve9) => {
4438
4590
  rl.question(question, (answer) => {
4439
4591
  rl.close();
4440
- resolve7(answer.trim());
4592
+ resolve9(answer.trim());
4441
4593
  });
4442
4594
  });
4443
4595
  }
@@ -4450,14 +4602,14 @@ async function getValue(key, providedValue) {
4450
4602
  }
4451
4603
  async function setAction(key, value) {
4452
4604
  if (isSensitiveKey(key)) {
4453
- console.error(pc9.red("Credential values are not written by config set."));
4605
+ console.error(pc10.red("Credential values are not written by config set."));
4454
4606
  console.error("Use agent-qa auth set --config <name> --type api-key|bearer-token for compatible or Gemini configs.");
4455
4607
  console.error("Use plugins.auth plus the dashboard auth flow for subscription configs.");
4456
4608
  return;
4457
4609
  }
4458
4610
  const resolved = await getValue(key, value);
4459
4611
  if (!resolved) {
4460
- console.error(pc9.red("No value provided."));
4612
+ console.error(pc10.red("No value provided."));
4461
4613
  return;
4462
4614
  }
4463
4615
  const configPath = "agent-qa.config.yaml";
@@ -4474,7 +4626,7 @@ async function setAction(key, value) {
4474
4626
  }
4475
4627
  doc.setIn(key.split("."), resolved);
4476
4628
  await writeFile(configPath, doc.toString());
4477
- console.log(pc9.green(`\u2713 ${key} = ${resolved} saved to agent-qa.config.yaml`));
4629
+ console.log(pc10.green(`\u2713 ${key} = ${resolved} saved to agent-qa.config.yaml`));
4478
4630
  }
4479
4631
  async function showAction() {
4480
4632
  const fileConfig = await loadConfigFile("agent-qa.config.yaml");
@@ -4482,12 +4634,12 @@ async function showAction() {
4482
4634
  const { mergeConfigs: mergeConfigs2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4483
4635
  const globalConfig = mergeConfigs2(fileConfig, envOverrides, {});
4484
4636
  const table = formatConfigDebug(globalConfig, {}, {});
4485
- console.log(pc9.bold("\nResolved Configuration:\n"));
4637
+ console.log(pc10.bold("\nResolved Configuration:\n"));
4486
4638
  console.log(table);
4487
4639
  console.log();
4488
4640
  }
4489
4641
  function createConfigCommand() {
4490
- const cmd = new Command8("config").description("Manage configuration");
4642
+ const cmd = new Command9("config").description("Manage configuration");
4491
4643
  cmd.command("set <key> [value]").description("Set a config value").action(setAction);
4492
4644
  cmd.command("show").description("Show resolved config with source attribution").action(showAction);
4493
4645
  return cmd;
@@ -4495,8 +4647,8 @@ function createConfigCommand() {
4495
4647
 
4496
4648
  // src/commands/queue.ts
4497
4649
  init_config();
4498
- import { Command as Command9 } from "commander";
4499
- import pc10 from "picocolors";
4650
+ import { Command as Command10 } from "commander";
4651
+ import pc11 from "picocolors";
4500
4652
  function timeAgo(dateStr) {
4501
4653
  const diff = Date.now() - new Date(dateStr).getTime();
4502
4654
  const seconds = Math.floor(diff / 1e3);
@@ -4519,7 +4671,7 @@ async function resolveServerUrl(opts, globalOpts) {
4519
4671
  }
4520
4672
  }
4521
4673
  function createQueueCommand() {
4522
- const cmd = new Command9("queue").description("Manage the test execution queue");
4674
+ const cmd = new Command10("queue").description("Manage the test execution queue");
4523
4675
  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
4676
  try {
4525
4677
  const program2 = command.parent.parent;
@@ -4530,13 +4682,13 @@ function createQueueCommand() {
4530
4682
  try {
4531
4683
  const response = await fetch(endpoint);
4532
4684
  if (!response.ok) {
4533
- console.error(pc10.red(`Server returned ${response.status}: ${response.statusText}`));
4685
+ console.error(pc11.red(`Server returned ${response.status}: ${response.statusText}`));
4534
4686
  process.exit(1);
4535
4687
  }
4536
4688
  data = await response.json();
4537
4689
  } 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"));
4690
+ console.error(pc11.red(`Could not connect to dashboard server at ${serverUrl}`));
4691
+ console.error(pc11.dim("Make sure the dashboard is running: agent-qa dashboard"));
4540
4692
  process.exit(1);
4541
4693
  }
4542
4694
  if (opts.json) {
@@ -4544,19 +4696,19 @@ function createQueueCommand() {
4544
4696
  return;
4545
4697
  }
4546
4698
  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)`);
4699
+ console.log(pc11.bold("Queue Status"));
4700
+ console.log(` Concurrency: ${pc11.blue(String(data.concurrency))} slots (${pc11.yellow(String(data.activeSlots))} active)`);
4549
4701
  console.log();
4550
4702
  const pending = data.pending?.jobs ?? [];
4551
4703
  const running = data.running?.jobs ?? [];
4552
4704
  if (pending.length === 0 && running.length === 0) {
4553
- console.log(pc10.dim(" Queue is empty \u2014 no pending or running jobs"));
4705
+ console.log(pc11.dim(" Queue is empty \u2014 no pending or running jobs"));
4554
4706
  console.log();
4555
4707
  return;
4556
4708
  }
4557
4709
  if (pending.length > 0) {
4558
- console.log(pc10.bold(`Pending (${pending.length} jobs):`));
4559
- console.log(pc10.dim(" # ID Name Priority Source Queued"));
4710
+ console.log(pc11.bold(`Pending (${pending.length} jobs):`));
4711
+ console.log(pc11.dim(" # ID Name Priority Source Queued"));
4560
4712
  for (let i = 0; i < pending.length; i++) {
4561
4713
  const job = pending[i];
4562
4714
  const num = String(i + 1).padEnd(3);
@@ -4565,37 +4717,37 @@ function createQueueCommand() {
4565
4717
  const priority = String(job.priority ?? 0).padEnd(8);
4566
4718
  const source = (job.source ?? "").padEnd(10);
4567
4719
  const queued = job.createdAt ? timeAgo(job.createdAt) : "";
4568
- console.log(` ${num} ${pc10.dim(id)} ${name} ${priority} ${source} ${pc10.dim(queued)}`);
4720
+ console.log(` ${num} ${pc11.dim(id)} ${name} ${priority} ${source} ${pc11.dim(queued)}`);
4569
4721
  }
4570
4722
  console.log();
4571
4723
  }
4572
4724
  if (running.length > 0) {
4573
- console.log(pc10.bold(`Running (${running.length} jobs):`));
4574
- console.log(pc10.dim(" ID Name Source Started"));
4725
+ console.log(pc11.bold(`Running (${running.length} jobs):`));
4726
+ console.log(pc11.dim(" ID Name Source Started"));
4575
4727
  for (const job of running) {
4576
4728
  const id = (job.id ?? "").slice(0, 12).padEnd(12);
4577
4729
  const name = (job.name ?? "").slice(0, 18).padEnd(18);
4578
4730
  const source = (job.source ?? "").padEnd(10);
4579
4731
  const started = job.startedAt ? timeAgo(job.startedAt) : "";
4580
- console.log(` ${pc10.green(id)} ${name} ${source} ${pc10.dim(started)}`);
4732
+ console.log(` ${pc11.green(id)} ${name} ${source} ${pc11.dim(started)}`);
4581
4733
  }
4582
4734
  console.log();
4583
4735
  }
4584
4736
  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"));
4737
+ console.log(pc11.bold(`Recent (${data.recent.length} jobs):`));
4738
+ console.log(pc11.dim(" ID Name Status Duration"));
4587
4739
  for (const job of data.recent) {
4588
4740
  const id = (job.id ?? "").slice(0, 12).padEnd(12);
4589
4741
  const name = (job.name ?? "").slice(0, 18).padEnd(18);
4590
4742
  const status = (job.status ?? "").padEnd(8);
4591
4743
  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)}`);
4744
+ const statusColor = job.status === "passed" ? pc11.green : job.status === "failed" ? pc11.red : pc11.dim;
4745
+ console.log(` ${pc11.dim(id)} ${name} ${statusColor(status)} ${pc11.dim(duration)}`);
4594
4746
  }
4595
4747
  console.log();
4596
4748
  }
4597
4749
  } catch (err) {
4598
- console.error(pc10.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4750
+ console.error(pc11.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4599
4751
  process.exit(1);
4600
4752
  }
4601
4753
  });
@@ -4608,25 +4760,25 @@ function createQueueCommand() {
4608
4760
  try {
4609
4761
  response = await fetch(`${serverUrl}/api/runs/${runId}/cancel`, { method: "POST" });
4610
4762
  } 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"));
4763
+ console.error(pc11.red(`Could not connect to dashboard server at ${serverUrl}`));
4764
+ console.error(pc11.dim("Make sure the dashboard is running: agent-qa dashboard"));
4613
4765
  process.exit(1);
4614
4766
  }
4615
4767
  if (response.ok) {
4616
4768
  const data = await response.json();
4617
4769
  if (data.cancelled) {
4618
- console.log(pc10.green(`\u2713 Cancelled job ${runId}`));
4770
+ console.log(pc11.green(`\u2713 Cancelled job ${runId}`));
4619
4771
  } else {
4620
- console.log(pc10.red(`Failed to cancel: ${data.error ?? "Unknown error"}`));
4772
+ console.log(pc11.red(`Failed to cancel: ${data.error ?? "Unknown error"}`));
4621
4773
  }
4622
4774
  } else if (response.status === 404) {
4623
- console.log(pc10.red(`Job not found: ${runId}`));
4775
+ console.log(pc11.red(`Job not found: ${runId}`));
4624
4776
  } else {
4625
4777
  const data = await response.json().catch(() => ({}));
4626
- console.log(pc10.red(`Failed to cancel: ${data.error ?? response.statusText}`));
4778
+ console.log(pc11.red(`Failed to cancel: ${data.error ?? response.statusText}`));
4627
4779
  }
4628
4780
  } catch (err) {
4629
- console.error(pc10.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4781
+ console.error(pc11.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
4630
4782
  process.exit(1);
4631
4783
  }
4632
4784
  });
@@ -4635,10 +4787,10 @@ function createQueueCommand() {
4635
4787
 
4636
4788
  // src/commands/validate.ts
4637
4789
  init_config();
4638
- import { Command as Command10 } from "commander";
4790
+ import { Command as Command11 } from "commander";
4639
4791
  import { validateProject, formatDiagnostics, resolveWorkspacePaths as resolveWorkspacePaths4 } from "@vostride/agent-qa-core";
4640
4792
  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) => {
4793
+ 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
4794
  const configPath = command.parent?.opts().config ?? "agent-qa.config.yaml";
4643
4795
  let workspace;
4644
4796
  try {
@@ -4661,9 +4813,9 @@ function createValidateCommand() {
4661
4813
  }
4662
4814
 
4663
4815
  // src/commands/auth.ts
4664
- import { Command as Command11 } from "commander";
4816
+ import { Command as Command12 } from "commander";
4665
4817
  import { exec } from "child_process";
4666
- import pc11 from "picocolors";
4818
+ import pc12 from "picocolors";
4667
4819
  import input2 from "@inquirer/input";
4668
4820
  import password from "@inquirer/password";
4669
4821
  var PROVIDER_LABELS = {
@@ -4698,10 +4850,10 @@ function getGlobalConfigPath2(command) {
4698
4850
  return command.parent?.parent?.opts().config ?? command.parent?.opts().config;
4699
4851
  }
4700
4852
  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"));
4853
+ console.log(pc12.red(`Config ${configName} uses ${providerLabel(provider)} credentials.`));
4854
+ console.log(pc12.dim("Use agent-qa auth set --config <name> --type api-key"));
4703
4855
  if (provider === "anthropic-compatible") {
4704
- console.log(pc11.dim("or --type bearer-token"));
4856
+ console.log(pc12.dim("or --type bearer-token"));
4705
4857
  }
4706
4858
  }
4707
4859
  function applyAuthToModelConfig(llmConfig, auth) {
@@ -4721,15 +4873,15 @@ function applyAuthToModelConfig(llmConfig, auth) {
4721
4873
  function statusForResolvedAuth(auth) {
4722
4874
  switch (auth.kind) {
4723
4875
  case "api-key":
4724
- return pc11.green("Saved API key");
4876
+ return pc12.green("Saved API key");
4725
4877
  case "bearer-token":
4726
- return pc11.green("Saved bearer token");
4878
+ return pc12.green("Saved bearer token");
4727
4879
  case "auth-fetch":
4728
- return Number(auth.expires ?? Date.now() + 1) <= Date.now() ? pc11.red("Expired") : pc11.green("OAuth connected");
4880
+ return Number(auth.expires ?? Date.now() + 1) <= Date.now() ? pc12.red("Expired") : pc12.green("OAuth connected");
4729
4881
  case "unauthenticated":
4730
- return pc11.dim("No credential");
4882
+ return pc12.dim("No credential");
4731
4883
  case "missing":
4732
- return pc11.red("Missing credential");
4884
+ return pc12.red("Missing credential");
4733
4885
  }
4734
4886
  }
4735
4887
  function extractProviderError(err) {
@@ -4760,16 +4912,16 @@ function openAuthorizationUrl(url) {
4760
4912
  });
4761
4913
  }
4762
4914
  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."));
4915
+ console.log(pc12.red(`Provider "${provider}" is configured for "${configName}", but no auth plugin is registered.`));
4916
+ console.log(pc12.dim(`Config: ${configName}`));
4917
+ console.log(pc12.dim(`Provider: ${provider}`));
4918
+ console.log(pc12.dim("Add the subscription auth plugin to package.json and install with your package manager:"));
4919
+ console.log(pc12.dim(' "devDependencies": { "@vostride/agent-qa-subscription-auth": "<agent-qa version>" }'));
4920
+ console.log(pc12.dim("Declare it in agent-qa.config.yaml under plugins.auth:"));
4921
+ console.log(pc12.dim(" plugins:"));
4922
+ console.log(pc12.dim(" auth:"));
4923
+ console.log(pc12.dim(" - package: @vostride/agent-qa-subscription-auth"));
4924
+ console.log(pc12.dim("Then authenticate from agent-qa dashboard or rerun this command."));
4773
4925
  }
4774
4926
  async function readTokensFromPlugin(provider, plugin, started) {
4775
4927
  if (started.waitForTokens) {
@@ -4777,19 +4929,19 @@ async function readTokensFromPlugin(provider, plugin, started) {
4777
4929
  }
4778
4930
  if (plugin.dashboardAuth.mode === "manual-code") {
4779
4931
  if (!plugin.exchangeCode) {
4780
- console.log(pc11.red(`Provider "${provider}" does not support CLI code exchange. Use agent-qa dashboard.`));
4932
+ console.log(pc12.red(`Provider "${provider}" does not support CLI code exchange. Use agent-qa dashboard.`));
4781
4933
  process.exitCode = 1;
4782
4934
  return null;
4783
4935
  }
4784
4936
  const code = (await input2({ message: "Authorization code:" })).trim();
4785
4937
  if (!code) {
4786
- console.log(pc11.red("Authorization code is required"));
4938
+ console.log(pc12.red("Authorization code is required"));
4787
4939
  process.exitCode = 1;
4788
4940
  return null;
4789
4941
  }
4790
4942
  return await plugin.exchangeCode({ code, sessionState: started.sessionState });
4791
4943
  }
4792
- console.log(pc11.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4944
+ console.log(pc12.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4793
4945
  process.exitCode = 1;
4794
4946
  return null;
4795
4947
  }
@@ -4802,14 +4954,14 @@ async function runSubscriptionPluginLogin(configName, provider) {
4802
4954
  return;
4803
4955
  }
4804
4956
  if (!plugin.startAuth) {
4805
- console.log(pc11.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4957
+ console.log(pc12.red(`Provider "${provider}" does not support CLI auth login. Use agent-qa dashboard.`));
4806
4958
  process.exitCode = 1;
4807
4959
  return;
4808
4960
  }
4809
4961
  let started;
4810
4962
  try {
4811
4963
  started = await plugin.startAuth({ configName });
4812
- console.log(`Open this URL to authenticate: ${pc11.cyan(started.authorizeUrl)}`);
4964
+ console.log(`Open this URL to authenticate: ${pc12.cyan(started.authorizeUrl)}`);
4813
4965
  openAuthorizationUrl(started.authorizeUrl);
4814
4966
  const tokens = await readTokensFromPlugin(provider, plugin, started);
4815
4967
  if (!tokens) return;
@@ -4818,18 +4970,18 @@ async function runSubscriptionPluginLogin(configName, provider) {
4818
4970
  provider: plugin.credentialProviderId,
4819
4971
  tokens
4820
4972
  });
4821
- console.log(pc11.green(`Authenticated ${configName} with ${plugin.label}`));
4973
+ console.log(pc12.green(`Authenticated ${configName} with ${plugin.label}`));
4822
4974
  } catch (err) {
4823
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
4975
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4824
4976
  process.exitCode = 1;
4825
4977
  } finally {
4826
4978
  started?.cleanup?.();
4827
4979
  }
4828
4980
  }
4829
4981
  function createAuthCommand() {
4830
- const cmd = new Command11("auth").description("Manage LLM config credentials");
4982
+ const cmd = new Command12("auth").description("Manage LLM config credentials");
4831
4983
  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) => {
4984
+ new Command12("login").description("Authenticate with a subscription provider").requiredOption("--config <name>", "named subscription LLM config to authenticate").action(async (opts, command) => {
4833
4985
  const { resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4834
4986
  const configPath = getGlobalConfigPath2(command);
4835
4987
  let provider;
@@ -4837,13 +4989,13 @@ function createAuthCommand() {
4837
4989
  try {
4838
4990
  const requestedName = normalizeConfigName(opts.config);
4839
4991
  if (!requestedName) {
4840
- console.log(pc11.red("configName is required"));
4992
+ console.log(pc12.red("configName is required"));
4841
4993
  return;
4842
4994
  }
4843
4995
  const resolved = await resolveNamedConfig2(requestedName, configPath);
4844
4996
  const resolvedName = normalizeConfigName(resolved.config.name);
4845
4997
  if (!resolvedName) {
4846
- console.log(pc11.red("Config name is invalid"));
4998
+ console.log(pc12.red("Config name is invalid"));
4847
4999
  return;
4848
5000
  }
4849
5001
  configName = resolvedName;
@@ -4854,17 +5006,17 @@ function createAuthCommand() {
4854
5006
  }
4855
5007
  provider = configProvider;
4856
5008
  } catch (err) {
4857
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
5009
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4858
5010
  return;
4859
5011
  }
4860
5012
  await runSubscriptionPluginLogin(configName, provider);
4861
5013
  })
4862
5014
  );
4863
5015
  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) => {
5016
+ 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
5017
  const credentialType = opts.type.trim();
4866
5018
  if (credentialType !== "api-key" && credentialType !== "bearer-token") {
4867
- console.log(pc11.red("type must be api-key or bearer-token"));
5019
+ console.log(pc12.red("type must be api-key or bearer-token"));
4868
5020
  return;
4869
5021
  }
4870
5022
  const { resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
@@ -4873,44 +5025,44 @@ function createAuthCommand() {
4873
5025
  try {
4874
5026
  resolved = await resolveNamedConfig2(opts.config, configPath);
4875
5027
  } catch (err) {
4876
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
5028
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4877
5029
  return;
4878
5030
  }
4879
5031
  const configName = resolved.config.name ?? opts.config;
4880
5032
  const provider = resolved.config.provider;
4881
5033
  if (SUBSCRIPTION_PROVIDERS2.has(provider)) {
4882
- console.log(pc11.red("Subscription providers use OAuth login"));
5034
+ console.log(pc12.red("Subscription providers use OAuth login"));
4883
5035
  return;
4884
5036
  }
4885
5037
  if (credentialType === "bearer-token" && provider !== "anthropic-compatible") {
4886
- console.log(pc11.red("bearer-token credentials are only supported for anthropic-compatible configs"));
5038
+ console.log(pc12.red("bearer-token credentials are only supported for anthropic-compatible configs"));
4887
5039
  return;
4888
5040
  }
4889
5041
  if (credentialType === "api-key" && !API_KEY_CREDENTIAL_PROVIDERS.has(provider)) {
4890
- console.log(pc11.red("api-key credentials are not supported for this provider"));
5042
+ console.log(pc12.red("api-key credentials are not supported for this provider"));
4891
5043
  return;
4892
5044
  }
4893
5045
  const secret = (secretArg ?? await password({ message: "Secret:" })).trim();
4894
5046
  if (!secret) {
4895
- console.log(pc11.red("Secret is required"));
5047
+ console.log(pc12.red("Secret is required"));
4896
5048
  return;
4897
5049
  }
4898
5050
  const { writeAuth } = await import("@vostride/agent-qa-core");
4899
5051
  if (credentialType === "bearer-token") {
4900
5052
  await writeAuth(configName, { type: "bearer", provider: "anthropic-compatible", token: secret });
4901
- console.log(pc11.green(`Saved bearer token for ${configName}`));
5053
+ console.log(pc12.green(`Saved bearer token for ${configName}`));
4902
5054
  } else {
4903
5055
  await writeAuth(configName, { type: "api", provider, key: secret });
4904
- console.log(pc11.green(`Saved API key for ${configName}`));
5056
+ console.log(pc12.green(`Saved API key for ${configName}`));
4905
5057
  }
4906
5058
  })
4907
5059
  );
4908
5060
  cmd.addCommand(
4909
- new Command11("status").description("Show credential status for configured LLMs").action(async function() {
5061
+ new Command12("status").description("Show credential status for configured LLMs").action(async function() {
4910
5062
  const { loadAuthPluginsForRawConfig: loadAuthPluginsForRawConfig2, resolveModelAuth: resolveModelAuth2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4911
5063
  const { loadConfigFile: loadConfigFile2 } = await Promise.resolve().then(() => (init_config(), config_exports));
4912
5064
  const configPath = getGlobalConfigPath2(this) ?? "agent-qa.config.yaml";
4913
- console.log(pc11.bold("Auth status:\n"));
5065
+ console.log(pc12.bold("Auth status:\n"));
4914
5066
  let llms;
4915
5067
  let defaultLLM;
4916
5068
  try {
@@ -4921,25 +5073,25 @@ function createAuthCommand() {
4921
5073
  } catch {
4922
5074
  }
4923
5075
  if (!llms?.length) {
4924
- console.log(pc11.dim(" No LLM configs found"));
5076
+ console.log(pc12.dim(" No LLM configs found"));
4925
5077
  return;
4926
5078
  }
4927
5079
  const maxNameLen = Math.max(...llms.map((c) => String(c.name ?? "").length), 4) + 12;
4928
5080
  for (const llm of llms) {
4929
5081
  const name = String(llm.name ?? "");
4930
5082
  const isDefault = name === defaultLLM;
4931
- const label = isDefault ? `${name} ${pc11.dim("(default)")}` : name;
5083
+ const label = isDefault ? `${name} ${pc12.dim("(default)")}` : name;
4932
5084
  const rawLabel = isDefault ? `${name} (default)` : name;
4933
5085
  const padLen = Math.max(0, maxNameLen - rawLabel.length);
4934
5086
  const dots = ".".repeat(padLen + 2);
4935
5087
  const provider = llm.provider;
4936
5088
  const status = statusForResolvedAuth(await resolveModelAuth2(name, llm));
4937
- console.log(` ${label} ${pc11.dim(`[${providerLabel(provider)}]`)} ${pc11.dim(dots)} ${status}`);
5089
+ console.log(` ${label} ${pc12.dim(`[${providerLabel(provider)}]`)} ${pc12.dim(dots)} ${status}`);
4938
5090
  }
4939
5091
  })
4940
5092
  );
4941
5093
  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) => {
5094
+ 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
5095
  const { removeAuth } = await import("@vostride/agent-qa-core");
4944
5096
  const { resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4945
5097
  const configPath = getGlobalConfigPath2(command);
@@ -4951,16 +5103,16 @@ function createAuthCommand() {
4951
5103
  const resolved = await resolveNamedConfig2(void 0, configPath);
4952
5104
  storeKey = resolved.config.name;
4953
5105
  } catch {
4954
- console.log(pc11.red("Specify --config <name> or run with a config file"));
5106
+ console.log(pc12.red("Specify --config <name> or run with a config file"));
4955
5107
  return;
4956
5108
  }
4957
5109
  }
4958
5110
  await removeAuth(storeKey);
4959
- console.log(pc11.green(`Logged out from ${storeKey}`));
5111
+ console.log(pc12.green(`Logged out from ${storeKey}`));
4960
5112
  })
4961
5113
  );
4962
5114
  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) => {
5115
+ 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
5116
  const { resolveModelAuth: resolveModelAuth2, resolveNamedConfig: resolveNamedConfig2 } = await Promise.resolve().then(() => (init_llm_utils(), llm_utils_exports));
4965
5117
  const configPath = getGlobalConfigPath2(command);
4966
5118
  let planner;
@@ -4971,19 +5123,19 @@ function createAuthCommand() {
4971
5123
  planner = modelFields;
4972
5124
  configName = name;
4973
5125
  } catch (err) {
4974
- console.log(pc11.red(err instanceof Error ? err.message : String(err)));
5126
+ console.log(pc12.red(err instanceof Error ? err.message : String(err)));
4975
5127
  return;
4976
5128
  }
4977
5129
  if (opts.provider) planner = { ...planner, provider: opts.provider };
4978
5130
  if (opts.model) planner = { ...planner, model: opts.model };
4979
- console.log(pc11.dim(`Testing config: ${configName}...`));
5131
+ console.log(pc12.dim(`Testing config: ${configName}...`));
4980
5132
  const auth = await resolveModelAuth2(configName, planner);
4981
5133
  if (auth.kind === "missing") {
4982
- console.log(pc11.red(auth.message));
5134
+ console.log(pc12.red(auth.message));
4983
5135
  return;
4984
5136
  }
4985
5137
  if (auth.kind === "unauthenticated") {
4986
- console.log(pc11.dim(auth.message));
5138
+ console.log(pc12.dim(auth.message));
4987
5139
  }
4988
5140
  const { createModel, getProviderOptions } = await import("@vostride/agent-qa-core");
4989
5141
  const { generateText } = await import("ai");
@@ -5001,24 +5153,24 @@ function createAuthCommand() {
5001
5153
  ...providerOpts ? { providerOptions: providerOpts } : {}
5002
5154
  });
5003
5155
  const elapsed = Date.now() - start;
5004
- console.log(pc11.green(`Connected to ${planner.provider}/${planner.model} (${elapsed}ms)`));
5156
+ console.log(pc12.green(`Connected to ${planner.provider}/${planner.model} (${elapsed}ms)`));
5005
5157
  } catch (err) {
5006
5158
  const { message, statusCode } = extractProviderError(err);
5007
5159
  const fullContext = `${message} ${statusCode ?? ""}`;
5008
5160
  if (/auth|unauthorized|401|invalid.*key|permission/i.test(fullContext)) {
5009
- console.log(pc11.red("Authentication failed. Check the saved credential for this config."));
5161
+ console.log(pc12.red("Authentication failed. Check the saved credential for this config."));
5010
5162
  } 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."));
5163
+ console.log(pc12.red("Model not found. Check the model name."));
5012
5164
  } else if (/rate|429|quota|limit/i.test(fullContext)) {
5013
- console.log(pc11.red("Rate limited. Try again later."));
5165
+ console.log(pc12.red("Rate limited. Try again later."));
5014
5166
  } 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."));
5167
+ console.log(pc12.red("Network error. Check the exact base URL and try again."));
5016
5168
  } else if (/invalid_request|400|bad request/i.test(fullContext)) {
5017
- console.log(pc11.red("Invalid request. Check model and provider settings."));
5169
+ console.log(pc12.red("Invalid request. Check model and provider settings."));
5018
5170
  } else {
5019
- console.log(pc11.red(`Connection failed. ${message}`));
5171
+ console.log(pc12.red(`Connection failed. ${message}`));
5020
5172
  }
5021
- console.log(pc11.dim(message));
5173
+ console.log(pc12.dim(message));
5022
5174
  } finally {
5023
5175
  clearTimeout(timeout);
5024
5176
  }
@@ -5029,35 +5181,35 @@ function createAuthCommand() {
5029
5181
 
5030
5182
  // src/commands/devices.ts
5031
5183
  init_config();
5032
- import { Command as Command12 } from "commander";
5033
- import { execSync as execSync3 } from "child_process";
5184
+ import { Command as Command13 } from "commander";
5185
+ import { execSync as execSync2 } from "child_process";
5034
5186
  import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
5035
5187
  import { join as join5 } from "path";
5036
5188
  import { stringify as stringify2 } from "yaml";
5037
- import pc12 from "picocolors";
5189
+ import pc13 from "picocolors";
5038
5190
  function formatStatus2(transport, localBinding) {
5039
5191
  if (transport !== "local") {
5040
- return pc12.green("Ready") + pc12.dim(" (no local binding required)");
5192
+ return pc13.green("Ready") + pc13.dim(" (no local binding required)");
5041
5193
  }
5042
5194
  if (!localBinding) {
5043
- return pc12.red("UNBOUND") + pc12.yellow(" - run `agent-qa devices init`");
5195
+ return pc13.red("UNBOUND") + pc13.yellow(" - run `agent-qa devices init`");
5044
5196
  }
5045
5197
  const keys = Object.entries(localBinding).slice(0, 2);
5046
5198
  const details = keys.map(([k, v]) => `${k}: ${v}`).join(", ");
5047
- return pc12.green("Bound") + pc12.dim(` (${details})`);
5199
+ return pc13.green("Bound") + pc13.dim(` (${details})`);
5048
5200
  }
5049
5201
  async function listDevices() {
5050
5202
  let config;
5051
5203
  try {
5052
5204
  config = await resolveConfig({});
5053
5205
  } catch (err) {
5054
- console.error(pc12.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
5206
+ console.error(pc13.red(`Config error: ${err instanceof Error ? err.message : String(err)}`));
5055
5207
  process.exitCode = 1;
5056
5208
  return;
5057
5209
  }
5058
5210
  const devices = config.registry?.devices;
5059
5211
  if (!devices || Object.keys(devices).length === 0) {
5060
- console.log(pc12.yellow("No devices configured in registry.devices"));
5212
+ console.log(pc13.yellow("No devices configured in registry.devices"));
5061
5213
  return;
5062
5214
  }
5063
5215
  const localBindings = loadLocalBindings();
@@ -5065,13 +5217,13 @@ async function listDevices() {
5065
5217
  const platWidth = 10;
5066
5218
  const transWidth = 14;
5067
5219
  console.log("");
5068
- console.log(pc12.bold(" Configured Devices"));
5220
+ console.log(pc13.bold(" Configured Devices"));
5069
5221
  console.log("");
5070
5222
  console.log(
5071
- ` ${pc12.dim("Name".padEnd(nameWidth))} ${pc12.dim("Platform".padEnd(platWidth))} ${pc12.dim("Transport".padEnd(transWidth))} ${pc12.dim("Status")}`
5223
+ ` ${pc13.dim("Name".padEnd(nameWidth))} ${pc13.dim("Platform".padEnd(platWidth))} ${pc13.dim("Transport".padEnd(transWidth))} ${pc13.dim("Status")}`
5072
5224
  );
5073
5225
  console.log(
5074
- ` ${pc12.dim("\u2500".repeat(nameWidth))} ${pc12.dim("\u2500".repeat(platWidth))} ${pc12.dim("\u2500".repeat(transWidth))} ${pc12.dim("\u2500".repeat(30))}`
5226
+ ` ${pc13.dim("\u2500".repeat(nameWidth))} ${pc13.dim("\u2500".repeat(platWidth))} ${pc13.dim("\u2500".repeat(transWidth))} ${pc13.dim("\u2500".repeat(30))}`
5075
5227
  );
5076
5228
  for (const [name, profile] of Object.entries(devices)) {
5077
5229
  const localBinding = localBindings?.devices?.[name] ?? null;
@@ -5084,7 +5236,7 @@ async function listDevices() {
5084
5236
  }
5085
5237
  function scanAdbDevices() {
5086
5238
  try {
5087
- const output = execSync3("adb devices -l", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5239
+ const output = execSync2("adb devices -l", { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
5088
5240
  const lines = output.split("\n").slice(1);
5089
5241
  const results = [];
5090
5242
  for (const line of lines) {
@@ -5103,7 +5255,7 @@ function scanAdbDevices() {
5103
5255
  }
5104
5256
  function scanSimctlDevices() {
5105
5257
  try {
5106
- const output = execSync3("xcrun simctl list devices --json", {
5258
+ const output = execSync2("xcrun simctl list devices --json", {
5107
5259
  encoding: "utf-8",
5108
5260
  stdio: ["pipe", "pipe", "pipe"]
5109
5261
  });
@@ -5135,28 +5287,28 @@ async function initDevices() {
5135
5287
  const dir = process.cwd();
5136
5288
  const localFilePath = join5(dir, "agent-qa.local.yaml");
5137
5289
  if (existsSync3(localFilePath)) {
5138
- console.log(pc12.yellow("agent-qa.local.yaml already exists. Skipping generation."));
5290
+ console.log(pc13.yellow("agent-qa.local.yaml already exists. Skipping generation."));
5139
5291
  return;
5140
5292
  }
5141
- console.log(pc12.bold("\n Scanning for connected devices...\n"));
5293
+ console.log(pc13.bold("\n Scanning for connected devices...\n"));
5142
5294
  const adbDevices = scanAdbDevices();
5143
5295
  const simctlDevices = scanSimctlDevices();
5144
5296
  if (adbDevices.length > 0) {
5145
- console.log(` ${pc12.green("Android:")} found ${adbDevices.length} device(s)`);
5297
+ console.log(` ${pc13.green("Android:")} found ${adbDevices.length} device(s)`);
5146
5298
  for (const d of adbDevices) {
5147
- console.log(` ${pc12.dim("\u2022")} ${d.model} (${d.serial})`);
5299
+ console.log(` ${pc13.dim("\u2022")} ${d.model} (${d.serial})`);
5148
5300
  }
5149
5301
  } else {
5150
- console.log(` ${pc12.dim("Android:")} no devices found (adb not available or no devices connected)`);
5302
+ console.log(` ${pc13.dim("Android:")} no devices found (adb not available or no devices connected)`);
5151
5303
  }
5152
5304
  if (simctlDevices.length > 0) {
5153
5305
  const booted = simctlDevices.filter((d) => d.state === "Booted");
5154
- console.log(` ${pc12.green("iOS:")} found ${simctlDevices.length} simulator(s) (${booted.length} booted)`);
5306
+ console.log(` ${pc13.green("iOS:")} found ${simctlDevices.length} simulator(s) (${booted.length} booted)`);
5155
5307
  for (const d of booted) {
5156
- console.log(` ${pc12.dim("\u2022")} ${d.name} (${d.udid})`);
5308
+ console.log(` ${pc13.dim("\u2022")} ${d.name} (${d.udid})`);
5157
5309
  }
5158
5310
  } else {
5159
- console.log(` ${pc12.dim("iOS:")} no simulators found (simctl not available or none installed)`);
5311
+ console.log(` ${pc13.dim("iOS:")} no simulators found (simctl not available or none installed)`);
5160
5312
  }
5161
5313
  let config;
5162
5314
  try {
@@ -5190,20 +5342,20 @@ async function initDevices() {
5190
5342
  writeFileSync2(localFilePath, yamlContent);
5191
5343
  appendGitignoreEntry(dir, "agent-qa.local.yaml");
5192
5344
  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"));
5345
+ console.log(pc13.green(` Generated agent-qa.local.yaml with ${bindCount} device binding(s)`));
5346
+ console.log(pc13.dim(" Added agent-qa.local.yaml to .gitignore"));
5195
5347
  console.log("");
5196
5348
  }
5197
5349
  function createDevicesCommand() {
5198
- const cmd = new Command12("devices").description("Manage device profiles and local bindings");
5350
+ const cmd = new Command13("devices").description("Manage device profiles and local bindings");
5199
5351
  cmd.command("list").description("Show configured devices with merged shared + local view").action(listDevices);
5200
5352
  cmd.command("init").description("Scan connected devices and generate agent-qa.local.yaml").action(initDevices);
5201
5353
  return cmd;
5202
5354
  }
5203
5355
 
5204
5356
  // src/commands/ids.ts
5205
- import { Command as Command13 } from "commander";
5206
- import pc13 from "picocolors";
5357
+ import { Command as Command14 } from "commander";
5358
+ import pc14 from "picocolors";
5207
5359
  import {
5208
5360
  ENTITY_ID_TYPES,
5209
5361
  generateCanonicalId,
@@ -5217,7 +5369,7 @@ function parseEntityType(value) {
5217
5369
  throw new Error(`Unknown ID type "${value}". Expected one of: ${ENTITY_ID_TYPES.join(", ")}`);
5218
5370
  }
5219
5371
  function createIdsCommand() {
5220
- const cmd = new Command13("ids").description("Generate and validate canonical agent-qa entity IDs");
5372
+ const cmd = new Command14("ids").description("Generate and validate canonical agent-qa entity IDs");
5221
5373
  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
5374
  try {
5223
5375
  const type = parseEntityType(typeValue);
@@ -5229,7 +5381,7 @@ function createIdsCommand() {
5229
5381
  console.log(id);
5230
5382
  }
5231
5383
  } catch (err) {
5232
- console.error(pc13.red(err instanceof Error ? err.message : String(err)));
5384
+ console.error(pc14.red(err instanceof Error ? err.message : String(err)));
5233
5385
  process.exitCode = 1;
5234
5386
  }
5235
5387
  });
@@ -5248,15 +5400,15 @@ function createIdsCommand() {
5248
5400
  if (opts.json) {
5249
5401
  console.log(JSON.stringify(result, null, 2));
5250
5402
  } else if (valid) {
5251
- console.log(pc13.green(result.message));
5403
+ console.log(pc14.green(result.message));
5252
5404
  } else {
5253
- console.error(pc13.red(result.message));
5405
+ console.error(pc14.red(result.message));
5254
5406
  }
5255
5407
  if (!valid) {
5256
5408
  process.exitCode = 1;
5257
5409
  }
5258
5410
  } catch (err) {
5259
- console.error(pc13.red(err instanceof Error ? err.message : String(err)));
5411
+ console.error(pc14.red(err instanceof Error ? err.message : String(err)));
5260
5412
  process.exitCode = 1;
5261
5413
  }
5262
5414
  });
@@ -5264,18 +5416,18 @@ function createIdsCommand() {
5264
5416
  }
5265
5417
 
5266
5418
  // src/commands/create-test.ts
5267
- import { Command as Command14 } from "commander";
5419
+ import { Command as Command15 } from "commander";
5268
5420
  import { generateTestId } from "@vostride/agent-qa-ids";
5269
5421
  import { stringify as stringify3 } from "yaml";
5270
5422
  import { writeFile as writeFile2, mkdir } from "fs/promises";
5271
5423
  import { existsSync as existsSync4 } from "fs";
5272
- import { resolve as resolve4, dirname as dirname5 } from "path";
5273
- import pc14 from "picocolors";
5424
+ import { resolve as resolve6, dirname as dirname7 } from "path";
5425
+ import pc15 from "picocolors";
5274
5426
  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);
5427
+ 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) => {
5428
+ const resolved = resolve6(path);
5277
5429
  if (existsSync4(resolved)) {
5278
- console.error(pc14.red("Error: File already exists: " + resolved));
5430
+ console.error(pc15.red("Error: File already exists: " + resolved));
5279
5431
  process.exitCode = 1;
5280
5432
  return;
5281
5433
  }
@@ -5287,27 +5439,27 @@ function createCreateTestCommand() {
5287
5439
  steps: ["Describe your first test step here"]
5288
5440
  };
5289
5441
  const yamlContent = stringify3(template);
5290
- await mkdir(dirname5(resolved), { recursive: true });
5442
+ await mkdir(dirname7(resolved), { recursive: true });
5291
5443
  await writeFile2(resolved, yamlContent, "utf-8");
5292
- console.log(pc14.green("Created " + resolved));
5293
- console.log(pc14.dim(" test-id: " + testId));
5444
+ console.log(pc15.green("Created " + resolved));
5445
+ console.log(pc15.dim(" test-id: " + testId));
5294
5446
  });
5295
5447
  return cmd;
5296
5448
  }
5297
5449
 
5298
5450
  // src/commands/create-suite.ts
5299
- import { Command as Command15 } from "commander";
5451
+ import { Command as Command16 } from "commander";
5300
5452
  import { generateSuiteId } from "@vostride/agent-qa-ids";
5301
5453
  import { stringify as stringify4 } from "yaml";
5302
5454
  import { writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
5303
5455
  import { existsSync as existsSync5 } from "fs";
5304
- import { resolve as resolve5, dirname as dirname6 } from "path";
5305
- import pc15 from "picocolors";
5456
+ import { resolve as resolve7, dirname as dirname8 } from "path";
5457
+ import pc16 from "picocolors";
5306
5458
  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);
5459
+ 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) => {
5460
+ const resolved = resolve7(path);
5309
5461
  if (existsSync5(resolved)) {
5310
- console.error(pc15.red("Error: File already exists: " + resolved));
5462
+ console.error(pc16.red("Error: File already exists: " + resolved));
5311
5463
  process.exitCode = 1;
5312
5464
  return;
5313
5465
  }
@@ -5321,18 +5473,18 @@ function createCreateSuiteCommand() {
5321
5473
  "tests: []",
5322
5474
  "tests: []\n # - test: ./path/to/test.yaml\n # id: my-test-id"
5323
5475
  );
5324
- await mkdir2(dirname6(resolved), { recursive: true });
5476
+ await mkdir2(dirname8(resolved), { recursive: true });
5325
5477
  await writeFile3(resolved, yamlContent, "utf-8");
5326
- console.log(pc15.green("Created " + resolved));
5327
- console.log(pc15.dim(" suite-id: " + suiteId));
5478
+ console.log(pc16.green("Created " + resolved));
5479
+ console.log(pc16.dim(" suite-id: " + suiteId));
5328
5480
  });
5329
5481
  return cmd;
5330
5482
  }
5331
5483
 
5332
5484
  // src/commands/clean-memory.ts
5333
5485
  init_config();
5334
- import { Command as Command16 } from "commander";
5335
- import pc16 from "picocolors";
5486
+ import { Command as Command17 } from "commander";
5487
+ import pc17 from "picocolors";
5336
5488
  import { createInterface as createInterface3 } from "readline";
5337
5489
  import { readFile as readFile4, readdir as readdir2, rm as rm2, stat as stat2 } from "fs/promises";
5338
5490
  import { existsSync as existsSync6 } from "fs";
@@ -5389,7 +5541,7 @@ async function getOrphanInfo(memoryRoot, tier, scope) {
5389
5541
  };
5390
5542
  }
5391
5543
  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) => {
5544
+ const cmd = new Command17("clean-memory").description("Remove orphaned memory observation directories").option("-y, --yes", "skip confirmation prompt").action(async (_opts, command) => {
5393
5545
  const opts = command.opts();
5394
5546
  const configPath = command.parent?.opts()?.config ?? "agent-qa.config.yaml";
5395
5547
  const config = await resolveConfig({ configPath });
@@ -5422,7 +5574,7 @@ function createCleanMemoryCommand() {
5422
5574
  }
5423
5575
  const memoryRoot = resolveMemoryRoot2(config, workspace.configDir);
5424
5576
  if (!existsSync6(memoryRoot)) {
5425
- console.log(pc16.dim("No memory directory found."));
5577
+ console.log(pc17.dim("No memory directory found."));
5426
5578
  return;
5427
5579
  }
5428
5580
  const TIERS = [
@@ -5452,17 +5604,17 @@ function createCleanMemoryCommand() {
5452
5604
  }
5453
5605
  }
5454
5606
  if (orphans.length === 0) {
5455
- console.log(pc16.dim("No orphaned memory directories found."));
5607
+ console.log(pc17.dim("No orphaned memory directories found."));
5456
5608
  return;
5457
5609
  }
5458
5610
  const maxPathLen = Math.max("Path".length, ...orphans.map((o) => o.path.length));
5459
5611
  console.log("");
5460
- console.log(pc16.bold("Orphaned memory directories:"));
5612
+ console.log(pc17.bold("Orphaned memory directories:"));
5461
5613
  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)));
5614
+ console.log(" " + pc17.dim("Path".padEnd(maxPathLen + 2) + "Files".padStart(7) + " " + "Last Confirmed".padEnd(20) + "Size".padStart(10)));
5615
+ console.log(" " + pc17.dim("-".repeat(maxPathLen + 2 + 7 + 2 + 20 + 10)));
5464
5616
  for (const o of orphans) {
5465
- const confirmed = o.lastConfirmed ?? pc16.dim("never");
5617
+ const confirmed = o.lastConfirmed ?? pc17.dim("never");
5466
5618
  console.log(" " + o.path.padEnd(maxPathLen + 2) + String(o.fileCount).padStart(7) + " " + String(confirmed).padEnd(20) + formatSize(o.size).padStart(10));
5467
5619
  }
5468
5620
  console.log("");
@@ -5480,18 +5632,18 @@ function createCleanMemoryCommand() {
5480
5632
  for (const orphan of orphans) {
5481
5633
  await rm2(join6(memoryRoot, orphan.tier, orphan.scope), { recursive: true, force: true });
5482
5634
  }
5483
- console.log(pc16.green(`Deleted ${orphans.length} orphaned director${orphans.length === 1 ? "y" : "ies"}.`));
5635
+ console.log(pc17.green(`Deleted ${orphans.length} orphaned director${orphans.length === 1 ? "y" : "ies"}.`));
5484
5636
  });
5485
5637
  return cmd;
5486
5638
  }
5487
5639
 
5488
5640
  // src/commands/skills.ts
5489
- import { Command as Command17 } from "commander";
5641
+ import { Command as Command18 } from "commander";
5490
5642
  import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
5491
- import { dirname as dirname7, join as join7, resolve as resolve6 } from "path";
5643
+ import { dirname as dirname9, join as join7, resolve as resolve8 } from "path";
5492
5644
  import { fileURLToPath } from "url";
5493
5645
  function findPackageRoot(startDir) {
5494
- let current = resolve6(startDir);
5646
+ let current = resolve8(startDir);
5495
5647
  while (true) {
5496
5648
  const packagePath = join7(current, "package.json");
5497
5649
  if (existsSync7(packagePath)) {
@@ -5503,9 +5655,9 @@ function findPackageRoot(startDir) {
5503
5655
  } catch {
5504
5656
  }
5505
5657
  }
5506
- const parent = dirname7(current);
5658
+ const parent = dirname9(current);
5507
5659
  if (parent === current) {
5508
- return resolve6(startDir);
5660
+ return resolve8(startDir);
5509
5661
  }
5510
5662
  current = parent;
5511
5663
  }
@@ -5521,13 +5673,13 @@ function hasSkillDirectories(dir) {
5521
5673
  return false;
5522
5674
  }
5523
5675
  }
5524
- function resolveAgentQaSkillsDirectory(startDir = dirname7(fileURLToPath(import.meta.url))) {
5676
+ function resolveAgentQaSkillsDirectory(startDir = dirname9(fileURLToPath(import.meta.url))) {
5525
5677
  const packageRoot = findPackageRoot(startDir);
5526
5678
  const packagedSkills = join7(packageRoot, "skills");
5527
5679
  if (hasSkillDirectories(packagedSkills)) {
5528
5680
  return { path: packagedSkills, source: "package" };
5529
5681
  }
5530
- const sourceSkills = resolve6(packageRoot, "../..", "skills");
5682
+ const sourceSkills = resolve8(packageRoot, "../..", "skills");
5531
5683
  if (hasSkillDirectories(sourceSkills)) {
5532
5684
  return { path: sourceSkills, source: "source" };
5533
5685
  }
@@ -5555,7 +5707,7 @@ function listAgentQaSkills(skillsDir) {
5555
5707
  function createSkillsCommand(options = {}) {
5556
5708
  const stdout = options.stdout ?? process.stdout;
5557
5709
  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) => {
5710
+ return new Command18("skills").description("List packaged agent-qa skills").option("--json", "print machine-readable output").action((opts) => {
5559
5711
  const resolved = resolveAgentQaSkillsDirectory(options.startDir);
5560
5712
  const skills = listAgentQaSkills(resolved.path);
5561
5713
  if (resolved.source === "missing" || skills.length === 0) {
@@ -5636,13 +5788,14 @@ function rejectRemovedCliSurface(input3 = {}) {
5636
5788
 
5637
5789
  // src/cli.ts
5638
5790
  rejectRemovedCliSurface();
5639
- var program = new Command18();
5791
+ var program = new Command19();
5640
5792
  program.enablePositionalOptions();
5641
5793
  program.name("agent-qa").description("AI-powered testing framework").version(VERSION);
5642
5794
  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
5795
  program.addCommand(createRunCommand());
5644
5796
  program.addCommand(createInitCommand());
5645
5797
  program.addCommand(createInstallBrowsersCommand());
5798
+ program.addCommand(createInstallMobileDriversCommand());
5646
5799
  program.addCommand(createDoctorCommand());
5647
5800
  program.addCommand(createDashboardCommand());
5648
5801
  program.addCommand(createServeCommand());