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.cjs +829 -684
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +754 -601
- package/dist/cli.js.map +1 -1
- package/package.json +9 -9
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
494
|
+
init_model_defaults();
|
|
495
|
+
import { Command as Command2 } from "commander";
|
|
495
496
|
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "fs";
|
|
496
|
-
import { join as
|
|
497
|
-
import { execSync } from "child_process";
|
|
497
|
+
import { join as join2, resolve as resolve2 } from "path";
|
|
498
498
|
import { stringify } from "yaml";
|
|
499
|
-
import
|
|
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
|
-
|
|
505
|
-
|
|
506
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
const
|
|
1064
|
-
if (
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
}
|
|
1076
|
-
if (
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
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
|
|
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 ${
|
|
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 =
|
|
1041
|
+
const configPath = join2(dir, "agent-qa.config.yaml");
|
|
1251
1042
|
if (existsSync(configPath) && !force) {
|
|
1252
|
-
console.log(
|
|
1043
|
+
console.log(pc3.yellow(`Config file already exists at ${configPath}. Use --force to overwrite.`));
|
|
1253
1044
|
return;
|
|
1254
1045
|
}
|
|
1255
|
-
const testsDir =
|
|
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(
|
|
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(
|
|
1059
|
+
console.log(pc3.green(`\u2713 ${action} ${subscriptionAuthDependency.path}`));
|
|
1269
1060
|
}
|
|
1270
|
-
const secretsPath =
|
|
1061
|
+
const secretsPath = join2(dir, DEFAULT_SECRETS_FILE);
|
|
1271
1062
|
if (force || !existsSync(secretsPath)) {
|
|
1272
1063
|
writeFileSync(secretsPath, "");
|
|
1273
|
-
console.log(
|
|
1064
|
+
console.log(pc3.green(`\u2713 Created ${secretsPath}`));
|
|
1274
1065
|
}
|
|
1275
|
-
const envPath =
|
|
1066
|
+
const envPath = join2(dir, DEFAULT_ENV_FILE);
|
|
1276
1067
|
if (force || !existsSync(envPath)) {
|
|
1277
1068
|
writeFileSync(envPath, "");
|
|
1278
|
-
console.log(
|
|
1069
|
+
console.log(pc3.green(`\u2713 Created ${envPath}`));
|
|
1279
1070
|
}
|
|
1280
|
-
const hooksPath =
|
|
1071
|
+
const hooksPath = join2(dir, DEFAULT_HOOKS_FILE);
|
|
1281
1072
|
if (force || !existsSync(hooksPath)) {
|
|
1282
1073
|
writeFileSync(hooksPath, buildHooksFile(platform));
|
|
1283
|
-
console.log(
|
|
1074
|
+
console.log(pc3.green(`\u2713 Created ${hooksPath}`));
|
|
1284
1075
|
}
|
|
1285
|
-
const agentRulesPath =
|
|
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(
|
|
1079
|
+
console.log(pc3.green(`\u2713 Created ${agentRulesPath}`));
|
|
1289
1080
|
}
|
|
1290
|
-
const localConfigPath =
|
|
1081
|
+
const localConfigPath = join2(dir, DEFAULT_LOCAL_CONFIG_FILE);
|
|
1291
1082
|
if (force || !existsSync(localConfigPath)) {
|
|
1292
1083
|
writeFileSync(localConfigPath, buildLocalConfigTemplate());
|
|
1293
|
-
console.log(
|
|
1084
|
+
console.log(pc3.green(`\u2713 Created ${localConfigPath}`));
|
|
1294
1085
|
}
|
|
1295
|
-
const examplePassPath =
|
|
1086
|
+
const examplePassPath = join2(testsDir, "example-pass.yaml");
|
|
1296
1087
|
writeFileSync(examplePassPath, buildExampleTest(platform, "pass"));
|
|
1297
|
-
console.log(
|
|
1298
|
-
const exampleFailPath =
|
|
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(
|
|
1091
|
+
console.log(pc3.green(`\u2713 Created ${exampleFailPath}`));
|
|
1301
1092
|
if (hasWeb(platform)) {
|
|
1302
|
-
const suitesDir =
|
|
1093
|
+
const suitesDir = join2(dir, "suites");
|
|
1303
1094
|
if (!existsSync(suitesDir)) {
|
|
1304
1095
|
mkdirSync(suitesDir, { recursive: true });
|
|
1305
1096
|
}
|
|
1306
|
-
const scriptsDir =
|
|
1097
|
+
const scriptsDir = join2(dir, "scripts");
|
|
1307
1098
|
if (!existsSync(scriptsDir)) {
|
|
1308
1099
|
mkdirSync(scriptsDir, { recursive: true });
|
|
1309
1100
|
}
|
|
1310
|
-
const automationTestsDir =
|
|
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 =
|
|
1106
|
+
const testPath = join2(automationTestsDir, test.fileName);
|
|
1316
1107
|
writeFileSync(testPath, buildAutomationExerciseTest(test));
|
|
1317
|
-
console.log(
|
|
1108
|
+
console.log(pc3.green(`\u2713 Created ${testPath}`));
|
|
1318
1109
|
}
|
|
1319
|
-
const automationSuitePath =
|
|
1110
|
+
const automationSuitePath = join2(suitesDir, "automation-exercise.suite.yaml");
|
|
1320
1111
|
writeFileSync(automationSuitePath, buildAutomationExerciseSuite());
|
|
1321
|
-
console.log(
|
|
1322
|
-
const hnScriptPath =
|
|
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(
|
|
1325
|
-
const hnTestPath =
|
|
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(
|
|
1118
|
+
console.log(pc3.green(`\u2713 Created ${hnTestPath}`));
|
|
1328
1119
|
}
|
|
1329
1120
|
appendGitignore(dir);
|
|
1330
|
-
console.log(
|
|
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(
|
|
1123
|
+
console.log(pc3.green(pc3.bold("\u2713 agent-qa project initialized!")));
|
|
1337
1124
|
console.log("");
|
|
1338
1125
|
console.log(" Created:");
|
|
1339
|
-
console.log(` ${
|
|
1126
|
+
console.log(` ${pc3.dim("\u2022")} agent-qa.config.yaml`);
|
|
1340
1127
|
if (subscriptionAuthDependency) {
|
|
1341
|
-
console.log(` ${
|
|
1342
|
-
}
|
|
1343
|
-
console.log(` ${
|
|
1344
|
-
console.log(` ${
|
|
1345
|
-
console.log(` ${
|
|
1346
|
-
console.log(` ${
|
|
1347
|
-
console.log(` ${
|
|
1348
|
-
console.log(` ${
|
|
1349
|
-
console.log(` ${
|
|
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(` ${
|
|
1352
|
-
console.log(` ${
|
|
1353
|
-
console.log(` ${
|
|
1354
|
-
console.log(` ${
|
|
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(` ${
|
|
1143
|
+
console.log(` ${pc3.dim("\u2022")} .gitignore entries`);
|
|
1357
1144
|
console.log("");
|
|
1358
1145
|
console.log(" Next steps:");
|
|
1359
|
-
|
|
1360
|
-
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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
|
|
1389
|
-
import {
|
|
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
|
|
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
|
|
1590
|
+
return pc6.green("PASS");
|
|
1455
1591
|
case "warn":
|
|
1456
|
-
return
|
|
1592
|
+
return pc6.yellow("WARN");
|
|
1457
1593
|
case "fail":
|
|
1458
|
-
return
|
|
1594
|
+
return pc6.red("FAIL");
|
|
1459
1595
|
case "skip":
|
|
1460
|
-
return
|
|
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
|
|
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`
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
2071
|
+
console.log(pc6.red(" Some checks failed. See fix instructions above."));
|
|
1923
2072
|
process.exitCode = 1;
|
|
1924
2073
|
} else {
|
|
1925
|
-
console.log(
|
|
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
|
|
1937
|
-
import
|
|
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" ?
|
|
2429
|
+
const severity = err.severity === "error" ? pc7.red("error") : pc7.yellow("warning");
|
|
2281
2430
|
console.error(`${severity}: ${err.message}`);
|
|
2282
|
-
console.error(` ${
|
|
2431
|
+
console.error(` ${pc7.blue("-->")} ${location}`);
|
|
2283
2432
|
if (err.source) {
|
|
2284
|
-
console.error(` ${
|
|
2433
|
+
console.error(` ${pc7.blue("|")} ${err.source}`);
|
|
2285
2434
|
}
|
|
2286
2435
|
if (err.suggestion) {
|
|
2287
|
-
console.error(` ${
|
|
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(
|
|
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(
|
|
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(` ${
|
|
2457
|
+
console.log(` ${pc7.dim("Memory:")} ${parts.join(", ")} ${pc7.dim(`(${secs})`)}`);
|
|
2309
2458
|
for (const err of log.errors) {
|
|
2310
|
-
console.log(` ${
|
|
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(
|
|
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(
|
|
2388
|
-
console.error(
|
|
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(
|
|
2396
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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:
|
|
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:
|
|
3120
|
+
const { resolve: resolve9 } = await import("path");
|
|
2972
3121
|
actionCache = new FileActionCache({
|
|
2973
|
-
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(` ${
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3238
|
-
for (const err of errors) console.error(
|
|
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(
|
|
3248
|
-
if (err instanceof Error && err.message) console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
3274
|
-
for (const p of patterns) console.error(
|
|
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(
|
|
3430
|
+
console.error(pc7.red("Error: No suite files found matching workspace.suiteMatch"));
|
|
3281
3431
|
for (const p of workspacePaths.suiteMatch) {
|
|
3282
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
3450
|
+
console.error(pc7.red("Error: No test files found matching workspace.testMatch"));
|
|
3301
3451
|
for (const p of workspacePaths.testMatch) {
|
|
3302
|
-
console.error(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
3598
|
+
console.log(pc7.yellow("No tests match filters"));
|
|
3449
3599
|
process.exit(0);
|
|
3450
3600
|
}
|
|
3451
3601
|
if (opts.dryRun) {
|
|
3452
|
-
console.log(
|
|
3602
|
+
console.log(pc7.bold(`
|
|
3453
3603
|
Found ${filtered.length} test(s):
|
|
3454
3604
|
`));
|
|
3455
3605
|
for (const test of filtered) {
|
|
3456
|
-
console.log(` ${
|
|
3606
|
+
console.log(` ${pc7.green("\u25CF")} ${test.name}`);
|
|
3457
3607
|
if (opts.configDebug) {
|
|
3458
|
-
console.log(
|
|
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:
|
|
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:
|
|
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(
|
|
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:
|
|
3672
|
+
const { resolve: resolve9 } = await import("path");
|
|
3523
3673
|
actionCache = new FileActionCache({
|
|
3524
|
-
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(
|
|
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(
|
|
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(
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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(` ${
|
|
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:
|
|
4096
|
-
const cacheDir =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
4263
|
-
import { resolve as
|
|
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
|
|
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 =
|
|
4275
|
-
const configDir =
|
|
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 =
|
|
4467
|
+
uiDir = resolve5(dirname6(pkgPath), "dist");
|
|
4316
4468
|
} catch {
|
|
4317
4469
|
if (globalOpts.verbose) {
|
|
4318
|
-
console.log(
|
|
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:
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
4377
|
-
import
|
|
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
|
|
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(
|
|
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
|
|
4408
|
-
import
|
|
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((
|
|
4589
|
+
return new Promise((resolve9) => {
|
|
4438
4590
|
rl.question(question, (answer) => {
|
|
4439
4591
|
rl.close();
|
|
4440
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
4499
|
-
import
|
|
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
|
|
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(
|
|
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(
|
|
4539
|
-
console.error(
|
|
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(
|
|
4548
|
-
console.log(` Concurrency: ${
|
|
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(
|
|
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(
|
|
4559
|
-
console.log(
|
|
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} ${
|
|
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(
|
|
4574
|
-
console.log(
|
|
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(` ${
|
|
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(
|
|
4586
|
-
console.log(
|
|
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" ?
|
|
4593
|
-
console.log(` ${
|
|
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(
|
|
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(
|
|
4612
|
-
console.error(
|
|
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(
|
|
4770
|
+
console.log(pc11.green(`\u2713 Cancelled job ${runId}`));
|
|
4619
4771
|
} else {
|
|
4620
|
-
console.log(
|
|
4772
|
+
console.log(pc11.red(`Failed to cancel: ${data.error ?? "Unknown error"}`));
|
|
4621
4773
|
}
|
|
4622
4774
|
} else if (response.status === 404) {
|
|
4623
|
-
console.log(
|
|
4775
|
+
console.log(pc11.red(`Job not found: ${runId}`));
|
|
4624
4776
|
} else {
|
|
4625
4777
|
const data = await response.json().catch(() => ({}));
|
|
4626
|
-
console.log(
|
|
4778
|
+
console.log(pc11.red(`Failed to cancel: ${data.error ?? response.statusText}`));
|
|
4627
4779
|
}
|
|
4628
4780
|
} catch (err) {
|
|
4629
|
-
console.error(
|
|
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
|
|
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
|
|
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
|
|
4816
|
+
import { Command as Command12 } from "commander";
|
|
4665
4817
|
import { exec } from "child_process";
|
|
4666
|
-
import
|
|
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(
|
|
4702
|
-
console.log(
|
|
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(
|
|
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
|
|
4876
|
+
return pc12.green("Saved API key");
|
|
4725
4877
|
case "bearer-token":
|
|
4726
|
-
return
|
|
4878
|
+
return pc12.green("Saved bearer token");
|
|
4727
4879
|
case "auth-fetch":
|
|
4728
|
-
return Number(auth.expires ?? Date.now() + 1) <= Date.now() ?
|
|
4880
|
+
return Number(auth.expires ?? Date.now() + 1) <= Date.now() ? pc12.red("Expired") : pc12.green("OAuth connected");
|
|
4729
4881
|
case "unauthenticated":
|
|
4730
|
-
return
|
|
4882
|
+
return pc12.dim("No credential");
|
|
4731
4883
|
case "missing":
|
|
4732
|
-
return
|
|
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(
|
|
4764
|
-
console.log(
|
|
4765
|
-
console.log(
|
|
4766
|
-
console.log(
|
|
4767
|
-
console.log(
|
|
4768
|
-
console.log(
|
|
4769
|
-
console.log(
|
|
4770
|
-
console.log(
|
|
4771
|
-
console.log(
|
|
4772
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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: ${
|
|
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(
|
|
4973
|
+
console.log(pc12.green(`Authenticated ${configName} with ${plugin.label}`));
|
|
4822
4974
|
} catch (err) {
|
|
4823
|
-
console.log(
|
|
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
|
|
4982
|
+
const cmd = new Command12("auth").description("Manage LLM config credentials");
|
|
4831
4983
|
cmd.addCommand(
|
|
4832
|
-
new
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
5056
|
+
console.log(pc12.green(`Saved API key for ${configName}`));
|
|
4905
5057
|
}
|
|
4906
5058
|
})
|
|
4907
5059
|
);
|
|
4908
5060
|
cmd.addCommand(
|
|
4909
|
-
new
|
|
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(
|
|
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(
|
|
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} ${
|
|
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} ${
|
|
5089
|
+
console.log(` ${label} ${pc12.dim(`[${providerLabel(provider)}]`)} ${pc12.dim(dots)} ${status}`);
|
|
4938
5090
|
}
|
|
4939
5091
|
})
|
|
4940
5092
|
);
|
|
4941
5093
|
cmd.addCommand(
|
|
4942
|
-
new
|
|
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(
|
|
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(
|
|
5111
|
+
console.log(pc12.green(`Logged out from ${storeKey}`));
|
|
4960
5112
|
})
|
|
4961
5113
|
);
|
|
4962
5114
|
cmd.addCommand(
|
|
4963
|
-
new
|
|
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(
|
|
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(
|
|
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(
|
|
5134
|
+
console.log(pc12.red(auth.message));
|
|
4983
5135
|
return;
|
|
4984
5136
|
}
|
|
4985
5137
|
if (auth.kind === "unauthenticated") {
|
|
4986
|
-
console.log(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
5169
|
+
console.log(pc12.red("Invalid request. Check model and provider settings."));
|
|
5018
5170
|
} else {
|
|
5019
|
-
console.log(
|
|
5171
|
+
console.log(pc12.red(`Connection failed. ${message}`));
|
|
5020
5172
|
}
|
|
5021
|
-
console.log(
|
|
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
|
|
5033
|
-
import { execSync as
|
|
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
|
|
5189
|
+
import pc13 from "picocolors";
|
|
5038
5190
|
function formatStatus2(transport, localBinding) {
|
|
5039
5191
|
if (transport !== "local") {
|
|
5040
|
-
return
|
|
5192
|
+
return pc13.green("Ready") + pc13.dim(" (no local binding required)");
|
|
5041
5193
|
}
|
|
5042
5194
|
if (!localBinding) {
|
|
5043
|
-
return
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
5220
|
+
console.log(pc13.bold(" Configured Devices"));
|
|
5069
5221
|
console.log("");
|
|
5070
5222
|
console.log(
|
|
5071
|
-
` ${
|
|
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
|
-
` ${
|
|
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 =
|
|
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 =
|
|
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(
|
|
5290
|
+
console.log(pc13.yellow("agent-qa.local.yaml already exists. Skipping generation."));
|
|
5139
5291
|
return;
|
|
5140
5292
|
}
|
|
5141
|
-
console.log(
|
|
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(` ${
|
|
5297
|
+
console.log(` ${pc13.green("Android:")} found ${adbDevices.length} device(s)`);
|
|
5146
5298
|
for (const d of adbDevices) {
|
|
5147
|
-
console.log(` ${
|
|
5299
|
+
console.log(` ${pc13.dim("\u2022")} ${d.model} (${d.serial})`);
|
|
5148
5300
|
}
|
|
5149
5301
|
} else {
|
|
5150
|
-
console.log(` ${
|
|
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(` ${
|
|
5306
|
+
console.log(` ${pc13.green("iOS:")} found ${simctlDevices.length} simulator(s) (${booted.length} booted)`);
|
|
5155
5307
|
for (const d of booted) {
|
|
5156
|
-
console.log(` ${
|
|
5308
|
+
console.log(` ${pc13.dim("\u2022")} ${d.name} (${d.udid})`);
|
|
5157
5309
|
}
|
|
5158
5310
|
} else {
|
|
5159
|
-
console.log(` ${
|
|
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(
|
|
5194
|
-
console.log(
|
|
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
|
|
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
|
|
5206
|
-
import
|
|
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
|
|
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(
|
|
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(
|
|
5403
|
+
console.log(pc14.green(result.message));
|
|
5252
5404
|
} else {
|
|
5253
|
-
console.error(
|
|
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(
|
|
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
|
|
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
|
|
5273
|
-
import
|
|
5424
|
+
import { resolve as resolve6, dirname as dirname7 } from "path";
|
|
5425
|
+
import pc15 from "picocolors";
|
|
5274
5426
|
function createCreateTestCommand() {
|
|
5275
|
-
const cmd = new
|
|
5276
|
-
const resolved =
|
|
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(
|
|
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(
|
|
5442
|
+
await mkdir(dirname7(resolved), { recursive: true });
|
|
5291
5443
|
await writeFile2(resolved, yamlContent, "utf-8");
|
|
5292
|
-
console.log(
|
|
5293
|
-
console.log(
|
|
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
|
|
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
|
|
5305
|
-
import
|
|
5456
|
+
import { resolve as resolve7, dirname as dirname8 } from "path";
|
|
5457
|
+
import pc16 from "picocolors";
|
|
5306
5458
|
function createCreateSuiteCommand() {
|
|
5307
|
-
const cmd = new
|
|
5308
|
-
const resolved =
|
|
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(
|
|
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(
|
|
5476
|
+
await mkdir2(dirname8(resolved), { recursive: true });
|
|
5325
5477
|
await writeFile3(resolved, yamlContent, "utf-8");
|
|
5326
|
-
console.log(
|
|
5327
|
-
console.log(
|
|
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
|
|
5335
|
-
import
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
5612
|
+
console.log(pc17.bold("Orphaned memory directories:"));
|
|
5461
5613
|
console.log("");
|
|
5462
|
-
console.log(" " +
|
|
5463
|
-
console.log(" " +
|
|
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 ??
|
|
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(
|
|
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
|
|
5641
|
+
import { Command as Command18 } from "commander";
|
|
5490
5642
|
import { existsSync as existsSync7, readdirSync, readFileSync as readFileSync4, statSync } from "fs";
|
|
5491
|
-
import { dirname as
|
|
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 =
|
|
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 =
|
|
5658
|
+
const parent = dirname9(current);
|
|
5507
5659
|
if (parent === current) {
|
|
5508
|
-
return
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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());
|