7a0648088c
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.
270 lines
7.5 KiB
JavaScript
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);
|
|
}
|