313 lines
9.2 KiB
JavaScript
313 lines
9.2 KiB
JavaScript
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 ?? "<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;
|
|
}
|
|
|
|
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("<hwlab-api-base-url>");
|
|
index += 1;
|
|
}
|
|
}
|
|
return redacted;
|
|
}
|
|
|
|
export function formatRuntime(runtime) {
|
|
return formatJson(runtime);
|
|
}
|