import { DEV_ENDPOINT, loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs"; import { runM3IoSkillCommand } from "../../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs"; function formatJson(value) { return JSON.stringify(value, null, 2); } function writeLine(stream, line = "") { stream.write(`${line}\n`); } function writeJson(stream, value) { writeLine(stream, formatJson(value)); } function parseArgs(args) { const flags = new Set(); const options = new Map(); const rest = []; for (let index = 0; index < args.length; index += 1) { const arg = args[index]; if (arg.startsWith("--")) { const next = args[index + 1]; if (next && !next.startsWith("--")) { options.set(arg, next); index += 1; } else { flags.add(arg); } } else { rest.push(arg); } } return { flags, options, rest }; } function makeError(code, message, details = {}) { return { ok: false, code, message, ...details }; } function mvpGateOverview(summary) { return { ok: true, command: "hwlab-cli health", environment: summary.environment, endpoint: summary.endpoint, gateStatus: summary.gateStatus, devOnly: summary.devOnly, prodDisabled: summary.prodDisabled, reportCommitId: summary.reportCommitId, artifactCount: summary.artifactCount, healthCount: summary.healthCount, milestoneStatus: Object.fromEntries(summary.milestones.map((item) => [item.id, item.status])), blocked: summary.blocked, blockers: summary.blockers }; } function mvpProjectList(summary) { return { ok: true, command: "hwlab-cli project list", environment: summary.environment, endpoint: summary.endpoint, projects: [ { projectId: summary.m5DryRunTopology.projectId, name: "HWLAB M5 MVP E2E Dry Run", status: summary.m5DryRunTopology.projectStatus, environment: summary.environment, source: summary.generatedFrom.m5Plan } ], topology: { gatewaySessions: summary.m5DryRunTopology.gateways.map((gateway) => gateway.gatewaySessionId), boxResources: summary.m5DryRunTopology.boxResources.map((resource) => resource.resourceId), patchPanelStatusId: summary.m5DryRunTopology.patchPanel.patchPanelStatusId } }; } function mvpDryRunPlan(summary, command) { return { ok: true, command, mode: "dry-run", dryRunOnly: true, environment: summary.environment, endpoint: summary.endpoint, gateStatus: summary.gateStatus, blocked: summary.blocked, generatedFrom: summary.generatedFrom, safety: summary.safety, milestones: summary.milestones, contractStatus: { artifacts: { status: "fixture", count: summary.artifactCount, commitIds: [...new Set(summary.artifacts.map((artifact) => artifact.commitId))], services: summary.artifacts.map((artifact) => artifact.serviceId) }, health: { status: "fixture", count: summary.healthCount, components: summary.health.map((health) => ({ component: health.component, serviceId: health.serviceId, status: health.status, dryRun: health.dryRun })) }, topology: { projectId: summary.m5DryRunTopology.projectId, gatewaySessions: summary.gatewaySessionCount, boxResources: summary.boxResourceCount, patchPanelStatusId: summary.m5DryRunTopology.patchPanel.patchPanelStatusId, activeConnectionCount: summary.m5DryRunTopology.patchPanel.activeConnectionCount }, agent: summary.agent, evidence: { operations: summary.operationCount, traceEvents: summary.traceCount, auditEvents: summary.auditCount, records: summary.evidenceRecords, cleanupRecords: summary.cleanupCount } }, steps: summary.steps.map((step) => ({ id: step.id, order: step.order, title: step.title, kind: step.kind, requires: step.requires })), blockers: summary.blockers, realDevGate: summary.realDevGate, statement: "dry-run only: no DEV/PROD changes were made" }; } function mvpLiveBlocked(summary, command) { return { ok: false, code: "BLOCKED", command, mode: "live-dev", environment: summary.environment, endpoint: summary.endpoint, gateStatus: summary.gateStatus, blocked: true, blockers: summary.blockers, requiredBeforeRetry: [ "Clear all open blockers in the owning GitHub issues and PR comments.", "Record live HWLAB DEV health for /health and /live on the frozen DEV endpoint.", "Keep the run DEV-only; do not use UniDesk runtime or PROD substitutes." ], generatedFrom: summary.generatedFrom }; } function writeHelp(stdout) { writeJson(stdout, { ok: true, command: "hwlab-cli", commands: [ "health", "project list", "m3 status --api-base-url URL", "m3 io --action do.write --value true --approved --api-base-url URL", "m3 io --action di.read --api-base-url URL", "G14 CI/CD is managed by native Tekton + GitOps; hwlab-cli cicd is removed", "test e2e --env dev --mvp", "test e2e --env dev --mvp --dry-run", "test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production" ], defaults: { testE2eMvp: "dry-run plan", output: "json", devEndpoint: DEV_ENDPOINT } }); } export async function runCli(argv, io) { const stdout = io.stdout ?? process.stdout; const stderr = io.stderr ?? process.stderr; const repoRoot = io.cwd ?? process.cwd(); const env = io.env ?? process.env; const [command = "help", subcommand, ...rest] = argv; const { flags, options } = parseArgs(rest); if (command === "cicd") { writeJson(stderr, makeError("legacy-cicd-removed", "hwlab-cli cicd was removed from the active G14 release path. Use G14 k3s Tekton/GitOps via tran G14:k3s and scripts/g14-gitops-render.mjs.", { subcommand: subcommand ?? null, replacement: { observePipelineRuns: "tran G14:k3s kubectl get pipelineruns -n hwlab-ci", observeArgo: "tran G14:k3s kubectl -n argocd get application hwlab-g14-dev hwlab-g14-prod", renderSource: "node scripts/g14-gitops-render.mjs --check" } })); return 2; } if (command === "m3" && ["io", "status"].includes(subcommand)) { const result = await runM3IoSkillCommand(["m3", subcommand, ...rest], { env, now: io.now, requestJson: io.requestJson }); writeJson(result.ok ? stdout : stderr, { ...result, command: `hwlab-cli ${["m3", subcommand, ...redactM3CliArgs(rest)].join(" ")}` }); return result.ok ? 0 : 2; } let summary; try { summary = loadMvpGateSummary(repoRoot); } catch (error) { writeJson(stderr, makeError("GATE_SUMMARY_UNREADABLE", error.message)); return 1; } if (command === "health") { writeJson(stdout, mvpGateOverview(summary)); return 0; } if (command === "project" && subcommand === "list") { writeJson(stdout, mvpProjectList(summary)); return 0; } if (command === "test" && subcommand === "e2e") { const env = options.get("--env") ?? null; const isMvp = flags.has("--mvp"); const isLive = flags.has("--live"); const isDryRun = flags.has("--dry-run") || !isLive; const commandLine = `hwlab-cli test e2e --env ${env ?? ""}${isMvp ? " --mvp" : ""}${ isLive ? " --live" : isDryRun && flags.has("--dry-run") ? " --dry-run" : "" }`; if (env !== "dev") { writeJson(stderr, makeError("UNSUPPORTED_ENV", "Only --env dev is supported for the MVP gate.", { env })); return 1; } if (!isMvp) { writeJson(stderr, makeError("MISSING_MVP_FLAG", "Use --mvp for the HWLAB MVP E2E gate.")); return 1; } if (isLive) { if (!flags.has("--confirm-dev") || !flags.has("--confirmed-non-production")) { writeJson( stderr, makeError("LIVE_CONFIRMATION_REQUIRED", "Live DEV requires --confirm-dev --confirmed-non-production.", { requiredFlags: ["--confirm-dev", "--confirmed-non-production"] }) ); return 1; } if (summary.blocked) { writeJson(stderr, mvpLiveBlocked(summary, commandLine)); return 2; } writeJson( stderr, makeError("LIVE_DEV_NOT_IMPLEMENTED", "The MVP CLI does not execute live DEV e2e from this runner.") ); return 2; } writeJson(stdout, mvpDryRunPlan(summary, commandLine)); return 0; } if (command === "project" && !subcommand) { writeJson(stderr, makeError("USAGE", "usage: hwlab-cli project list")); return 1; } if (command === "test" && !subcommand) { writeJson(stderr, makeError("USAGE", "usage: hwlab-cli test e2e --env dev --mvp")); return 1; } writeHelp(stdout); return 0; } function redactM3CliArgs(args = []) { const redacted = []; for (let index = 0; index < args.length; index += 1) { redacted.push(args[index]); if (args[index] === "--api-base-url" && index + 1 < args.length) { redacted.push(""); index += 1; } } return redacted; } export function formatRuntime(runtime) { return formatJson(runtime); }