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