import type { Command } from "commander"; import { collectString } from "./cli-options.js"; import { listLiveTransportQaCliRegistrations } from "./live-transports/cli.js"; import { registerMantisCli } from "./mantis/cli.js"; import { DEFAULT_QA_LIVE_PROVIDER_MODE, formatQaProviderModeHelp, listQaStandaloneProviderCommands, } from "./providers/index.js"; import { QA_FRONTIER_PARITY_BASELINE_LABEL, QA_FRONTIER_PARITY_CANDIDATE_LABEL, } from "./providers/live-frontier/parity.js"; import type { QaProviderMode, QaProviderModeInput } from "./run-config.js"; import { hasQaScenarioPack } from "./scenario-catalog.js"; type QaLabCliRuntime = typeof import("./cli.runtime.js"); let qaLabCliRuntimePromise: Promise | null = null; async function loadQaLabCliRuntime(): Promise { qaLabCliRuntimePromise ??= import("./cli.runtime.js"); return await qaLabCliRuntimePromise; } async function runQaSelfCheck(opts: { repoRoot?: string; output?: string }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaLabSelfCheckCommand(opts); } async function runQaSuite(opts: { repoRoot?: string; outputDir?: string; transportId?: string; providerMode?: QaProviderModeInput; primaryModel?: string; alternateModel?: string; fastMode?: boolean; thinking?: string; allowFailures?: boolean; enabledPluginIds?: string[]; cliAuthMode?: string; parityPack?: string; scenarioIds?: string[]; concurrency?: number; runner?: string; image?: string; cpus?: number; memory?: string; disk?: string; preflight?: boolean; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaSuiteCommand(opts); } async function runQaParityReport(opts: { repoRoot?: string; candidateSummary: string; baselineSummary: string; candidateLabel?: string; baselineLabel?: string; outputDir?: string; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaParityReportCommand(opts); } async function runQaCoverageReport(opts: { repoRoot?: string; output?: string; json?: boolean }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaCoverageReportCommand(opts); } async function runQaCharacterEval(opts: { repoRoot?: string; outputDir?: string; model?: string[]; scenario?: string; fast?: boolean; thinking?: string; modelThinking?: string[]; judgeModel?: string[]; judgeTimeoutMs?: number; blindJudgeModels?: boolean; concurrency?: number; judgeConcurrency?: number; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaCharacterEvalCommand(opts); } async function runQaManualLane(opts: { repoRoot?: string; transportId?: string; providerMode?: QaProviderModeInput; primaryModel?: string; alternateModel?: string; fastMode?: boolean; message: string; timeoutMs?: number; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaManualLaneCommand(opts); } async function runQaCredentialsAdd(opts: { actorId?: string; endpointPrefix?: string; json?: boolean; kind: string; note?: string; payloadFile: string; repoRoot?: string; siteUrl?: string; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaCredentialsAddCommand(opts); } async function runQaCredentialsRemove(opts: { actorId?: string; credentialId: string; endpointPrefix?: string; json?: boolean; siteUrl?: string; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaCredentialsRemoveCommand(opts); } async function runQaCredentialsList(opts: { actorId?: string; endpointPrefix?: string; json?: boolean; kind?: string; limit?: number; showSecrets?: boolean; siteUrl?: string; status?: string; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaCredentialsListCommand(opts); } async function runQaCredentialsDoctor(opts: { actorId?: string; endpointPrefix?: string; json?: boolean; siteUrl?: string; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaCredentialsDoctorCommand(opts); } async function runQaUi(opts: { repoRoot?: string; host?: string; port?: number; advertiseHost?: string; advertisePort?: number; controlUiUrl?: string; controlUiToken?: string; controlUiProxyTarget?: string; uiDistDir?: string; autoKickoffTarget?: string; embeddedGateway?: string; sendKickoffOnStart?: boolean; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaLabUiCommand(opts); } async function runQaDockerScaffold(opts: { repoRoot?: string; outputDir: string; gatewayPort?: number; qaLabPort?: number; providerBaseUrl?: string; image?: string; usePrebuiltImage?: boolean; bindUiDist?: boolean; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaDockerScaffoldCommand(opts); } async function runQaDockerBuildImage(opts: { repoRoot?: string; image?: string }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaDockerBuildImageCommand(opts); } async function runQaDockerUp(opts: { repoRoot?: string; outputDir?: string; gatewayPort?: number; qaLabPort?: number; providerBaseUrl?: string; image?: string; usePrebuiltImage?: boolean; bindUiDist?: boolean; skipUiBuild?: boolean; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaDockerUpCommand(opts); } async function runQaProviderServer( providerMode: QaProviderMode, opts: { host?: string; port?: number }, ) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaProviderServerCommand(providerMode, opts); } export function isQaLabCliAvailable(): boolean { return hasQaScenarioPack(); } function assertNoQaSubcommandCollision(qa: Command, commandName: string) { if (qa.commands.some((command) => command.name() === commandName)) { throw new Error(`QA runner command "${commandName}" conflicts with an existing qa subcommand`); } } export function registerQaLabCli(program: Command) { const qa = program .command("qa") .description("Run private QA automation flows and launch the QA debugger"); registerMantisCli(qa); qa.command("run") .description("Run the bundled QA self-check and write a Markdown report") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--output ", "Report output path") .action(async (opts: { repoRoot?: string; output?: string }) => { await runQaSelfCheck(opts); }); qa.command("suite") .description("Run repo-backed QA scenarios against the QA gateway lane") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--output-dir ", "Suite artifact directory") .option("--runner ", "Execution runner: host or multipass", "host") .option("--transport ", "QA transport id", "qa-channel") .option("--provider-mode ", formatQaProviderModeHelp(), DEFAULT_QA_LIVE_PROVIDER_MODE) .option("--model ", "Primary provider/model ref") .option("--alt-model ", "Alternate provider/model ref") .option( "--cli-auth-mode ", "CLI backend auth mode for live Claude CLI runs: auto, api-key, or subscription", ) .option("--parity-pack ", 'Preset scenario pack; currently only "agentic" is supported') .option("--scenario ", "Run only the named QA scenario (repeatable)", collectString, []) .option( "--enable-plugin ", "Enable an extra bundled plugin in the QA gateway config (repeatable)", collectString, [], ) .option("--concurrency ", "Scenario worker concurrency", (value: string) => Number(value), ) .option("--preflight", "Run a single-scenario bootstrap preflight and stop", false) .option( "--allow-failures", "Write artifacts without setting a failing exit code when scenarios fail", false, ) .option("--fast", "Enable provider fast mode where supported", false) .option( "--thinking ", "Suite thinking default: off|minimal|low|medium|high|xhigh|adaptive|max", ) .option("--image ", "Multipass image alias") .option("--cpus ", "Multipass vCPU count", (value: string) => Number(value)) .option("--memory ", "Multipass memory size") .option("--disk ", "Multipass disk size") .action( async (opts: { repoRoot?: string; outputDir?: string; transport?: string; runner?: string; providerMode?: QaProviderModeInput; model?: string; altModel?: string; cliAuthMode?: string; parityPack?: string; scenario?: string[]; enablePlugin?: string[]; concurrency?: number; allowFailures?: boolean; fast?: boolean; thinking?: string; image?: string; cpus?: number; memory?: string; disk?: string; preflight?: boolean; }) => { await runQaSuite({ repoRoot: opts.repoRoot, outputDir: opts.outputDir, transportId: opts.transport, runner: opts.runner, providerMode: opts.providerMode, primaryModel: opts.model, alternateModel: opts.altModel, fastMode: opts.fast, thinking: opts.thinking, cliAuthMode: opts.cliAuthMode, parityPack: opts.parityPack, scenarioIds: opts.scenario, enabledPluginIds: opts.enablePlugin, concurrency: opts.concurrency, allowFailures: opts.allowFailures, image: opts.image, cpus: opts.cpus, memory: opts.memory, disk: opts.disk, preflight: opts.preflight, }); }, ); qa.command("parity-report") .description("Compare two QA suite summaries and write an agentic parity gate report") .requiredOption("--candidate-summary ", "Candidate qa-suite-summary.json path") .requiredOption("--baseline-summary ", "Baseline qa-suite-summary.json path") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option( "--candidate-label