Files
pikasTech-HWLAB/tools/hwlab-cli/lib/cli.mjs
T
Lyon 7a0648088c fix: wire distinct M3 simulator identities
Merge PR #159 after Code Queue rebase and validation. Runner verified M3 cardinality, hardware-loop smoke, patch-panel runtime smoke, m3 readonly contract, and git diff check. This is source/config blocker removal only; it does not claim DEV-LIVE M3 pass.
2026-05-23 00:09:29 +08:00

270 lines
7.5 KiB
JavaScript

import { DEV_ENDPOINT, loadMvpGateSummary } from "../../../internal/mvp-gate/summary.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 reports/dev-gate/dev-mvp-gate-report.json.",
"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",
"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();
let summary;
try {
summary = loadMvpGateSummary(repoRoot);
} catch (error) {
writeJson(stderr, makeError("GATE_SUMMARY_UNREADABLE", error.message));
return 1;
}
const [command = "help", subcommand, ...rest] = argv;
const { flags, options } = parseArgs(rest);
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 ?? "<missing>"}${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;
}
export function formatRuntime(runtime) {
return formatJson(runtime);
}