Files
pikasTech-HWLAB/tools/hwlab-cli/lib/cli.mjs
T
2026-05-26 05:31:57 +08:00

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);
}