import type { Command } from "commander"; import type { QaProviderModeInput } from "./run-config.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; providerMode?: QaProviderModeInput; primaryModel?: string; alternateModel?: string; fastMode?: boolean; scenarioIds?: string[]; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaSuiteCommand(opts); } async function runQaCharacterEval(opts: { repoRoot?: string; outputDir?: string; model?: string[]; scenario?: string; fast?: boolean; thinking?: string; modelThinking?: string[]; judgeModel?: string[]; judgeTimeoutMs?: number; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaCharacterEvalCommand(opts); } async function runQaManualLane(opts: { repoRoot?: string; providerMode?: QaProviderModeInput; primaryModel?: string; alternateModel?: string; fastMode?: boolean; message: string; timeoutMs?: number; }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaManualLaneCommand(opts); } function collectString(value: string, previous: string[]) { const trimmed = value.trim(); return trimmed ? [...previous, trimmed] : previous; } 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 runQaMockOpenAi(opts: { host?: string; port?: number }) { const runtime = await loadQaLabCliRuntime(); await runtime.runQaMockOpenAiCommand(opts); } export function registerQaLabCli(program: Command) { const qa = program .command("qa") .description("Run private QA automation flows and launch the QA debugger"); 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( "--provider-mode ", "Provider mode: mock-openai or live-frontier (legacy live-openai still works)", "mock-openai", ) .option("--model ", "Primary provider/model ref") .option("--alt-model ", "Alternate provider/model ref") .option("--scenario ", "Run only the named QA scenario (repeatable)", collectString, []) .option("--fast", "Enable provider fast mode where supported", false) .action( async (opts: { repoRoot?: string; outputDir?: string; providerMode?: QaProviderModeInput; model?: string; altModel?: string; scenario?: string[]; fast?: boolean; }) => { await runQaSuite({ repoRoot: opts.repoRoot, outputDir: opts.outputDir, providerMode: opts.providerMode, primaryModel: opts.model, alternateModel: opts.altModel, fastMode: opts.fast, scenarioIds: opts.scenario, }); }, ); qa.command("character-eval") .description("Run the character QA scenario across live models and write a judged report") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--output-dir ", "Character eval artifact directory") .option( "--model ", "Provider/model ref to evaluate; options: thinking=, fast, no-fast, fast=", collectString, [], ) .option("--scenario ", "Character scenario id", "character-vibes-gollum") .option("--fast", "Enable provider fast mode for all candidate runs") .option( "--thinking ", "Candidate thinking default: off|minimal|low|medium|high|xhigh|adaptive", ) .option( "--model-thinking ", "Deprecated: candidate thinking override for one model ref (repeatable)", collectString, [], ) .option( "--judge-model ", "Judge provider/model ref; options: thinking=, fast, no-fast, fast= (repeatable)", collectString, [], ) .option("--judge-timeout-ms ", "Override judge wait timeout", (value: string) => Number(value), ) .action( async (opts: { repoRoot?: string; outputDir?: string; model?: string[]; scenario?: string; fast?: boolean; thinking?: string; modelThinking?: string[]; judgeModel?: string[]; judgeTimeoutMs?: number; }) => { await runQaCharacterEval(opts); }, ); qa.command("manual") .description("Run a one-off QA agent prompt against the selected provider/model lane") .requiredOption("--message ", "Prompt to send to the QA agent") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option( "--provider-mode ", "Provider mode: mock-openai or live-frontier (legacy live-openai still works)", "live-frontier", ) .option("--model ", "Primary provider/model ref (defaults by provider mode)") .option("--alt-model ", "Alternate provider/model ref") .option("--fast", "Enable provider fast mode where supported", false) .option("--timeout-ms ", "Override agent.wait timeout", (value: string) => Number(value)) .action( async (opts: { message: string; repoRoot?: string; providerMode?: QaProviderModeInput; model?: string; altModel?: string; fast?: boolean; timeoutMs?: number; }) => { await runQaManualLane({ repoRoot: opts.repoRoot, providerMode: opts.providerMode, primaryModel: opts.model, alternateModel: opts.altModel, fastMode: opts.fast, message: opts.message, timeoutMs: opts.timeoutMs, }); }, ); qa.command("ui") .description("Start the private QA debugger UI and local QA bus") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--host ", "Bind host", "127.0.0.1") .option("--port ", "Bind port", (value: string) => Number(value)) .option("--advertise-host ", "Optional public host to advertise in bootstrap payloads") .option("--advertise-port ", "Optional public port to advertise", (value: string) => Number(value), ) .option("--control-ui-url ", "Optional Control UI URL to embed beside the QA panel") .option("--control-ui-token ", "Optional Control UI token for embedded links") .option( "--control-ui-proxy-target ", "Optional upstream Control UI target for /control-ui proxying", ) .option("--ui-dist-dir ", "Optional QA Lab UI asset directory override") .option("--auto-kickoff-target ", "Kickoff default target (direct or channel)") .option("--embedded-gateway ", "Embedded gateway mode hint", "enabled") .option( "--send-kickoff-on-start", "Inject the repo-backed kickoff task when the UI starts", false, ) .action( async (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; }) => { await runQaUi(opts); }, ); qa.command("docker-scaffold") .description("Write a prebaked Docker scaffold for the QA dashboard + gateway lane") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .requiredOption("--output-dir ", "Output directory for docker-compose + state files") .option("--gateway-port ", "Gateway host port", (value: string) => Number(value)) .option("--qa-lab-port ", "QA lab host port", (value: string) => Number(value)) .option("--provider-base-url ", "Provider base URL for the QA gateway") .option("--image ", "Prebaked image name", "openclaw:qa-local-prebaked") .option("--use-prebuilt-image", "Use image: instead of build: in docker-compose", false) .option( "--bind-ui-dist", "Bind-mount extensions/qa-lab/web/dist into the qa-lab container for faster UI refresh", false, ) .action( async (opts: { repoRoot?: string; outputDir: string; gatewayPort?: number; qaLabPort?: number; providerBaseUrl?: string; image?: string; usePrebuiltImage?: boolean; bindUiDist?: boolean; }) => { await runQaDockerScaffold(opts); }, ); qa.command("docker-build-image") .description("Build the prebaked QA Docker image with qa-channel + qa-lab bundled") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--image ", "Image tag", "openclaw:qa-local-prebaked") .action(async (opts: { repoRoot?: string; image?: string }) => { await runQaDockerBuildImage(opts); }); qa.command("up") .description("Build the QA site, start the Docker-backed QA stack, and print the QA Lab URL") .option("--repo-root ", "Repository root to target when running from a neutral cwd") .option("--output-dir ", "Output directory for docker-compose + state files") .option("--gateway-port ", "Gateway host port", (value: string) => Number(value)) .option("--qa-lab-port ", "QA lab host port", (value: string) => Number(value)) .option("--provider-base-url ", "Provider base URL for the QA gateway") .option("--image ", "Image tag", "openclaw:qa-local-prebaked") .option("--use-prebuilt-image", "Use image: instead of build: in docker-compose", false) .option( "--bind-ui-dist", "Bind-mount extensions/qa-lab/web/dist into the qa-lab container for faster UI refresh", false, ) .option("--skip-ui-build", "Skip pnpm qa:lab:build before starting Docker", false) .action( async (opts: { repoRoot?: string; outputDir?: string; gatewayPort?: number; qaLabPort?: number; providerBaseUrl?: string; image?: string; usePrebuiltImage?: boolean; bindUiDist?: boolean; skipUiBuild?: boolean; }) => { await runQaDockerUp(opts); }, ); qa.command("mock-openai") .description("Run the local mock OpenAI Responses API server for QA") .option("--host ", "Bind host", "127.0.0.1") .option("--port ", "Bind port", (value: string) => Number(value)) .action(async (opts: { host?: string; port?: number }) => { await runQaMockOpenAi(opts); }); }