Files
pikasTech-HWLAB/tools/src/hwpod-harness-lib.ts
T
2026-07-16 16:08:05 +02:00

1341 lines
67 KiB
TypeScript

import { randomUUID } from "node:crypto";
import http from "node:http";
import https from "node:https";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import path from "node:path";
import { HWPOD_NODE_OPS, HWPOD_NODE_OPS_CONTRACT_VERSION } from "./hwpod-node-ops-contract.ts";
import { resolveRuntimeEndpoint, runtimeEndpointVisibility } from "./runtime-endpoint-resolver.ts";
export const DEFAULT_HWPOD_SPEC_PATH = ".hwlab/hwpod-spec.yaml";
const COMPILER_NAME = "hwpod-compiler-cli";
const CTL_NAME = "hwpod-ctl";
const CLI_NAME = "hwpod-cli";
const DEFAULT_TIMEOUT_MS = 30000;
const BOOLEAN_OPTIONS = new Set(["all", "allowMultiple", "dryRun", "finalNewline", "force", "full", "help", "h", "ignoreCase", "json", "noAuth", "sessionOnly", "wait"]);
const DEFAULT_KEIL_COMMAND_TIMEOUT_MS = 30000;
type EnvLike = Record<string, string | undefined>;
type ParsedArgs = Record<string, unknown> & { _: string[] };
type FetchLike = typeof fetch;
export async function mainHwpodCompilerCli(argv = process.argv.slice(2), options: any = {}) {
const result = await runHwpodCompilerCli(argv, options);
console.log(JSON.stringify(result.payload, null, 2));
process.exitCode = result.exitCode;
}
export async function mainHwpodCtl(argv = process.argv.slice(2), options: any = {}) {
const result = await runHwpodCtl(argv, options);
console.log(JSON.stringify(result.payload, null, 2));
process.exitCode = result.exitCode;
}
export async function mainHwpodCli(argv = process.argv.slice(2), options: any = {}) {
const stdinText = options.stdinText ?? await readCliStdinForCommand(argv);
const result = await runHwpodCli(argv, { ...options, stdinText });
console.log(JSON.stringify(result.payload, null, 2));
process.exitCode = result.exitCode;
}
async function readCliStdinForCommand(argv: string[]): Promise<string | undefined> {
const parsed = parseOptions(argv);
const command = parsed._[0] || "help";
const subcommand = parsed._[1] || "";
if (command === "workspace" && ["apply-patch", "write"].includes(subcommand) && parsed.patch === undefined && parsed.patchText === undefined && parsed.patchContent === undefined && parsed.patchBase64 === undefined && parsed.content === undefined && parsed.contentText === undefined && parsed.contentBase64 === undefined && !process.stdin.isTTY) {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
return Buffer.concat(chunks).toString("utf8");
}
if (command === "uart" && subcommand === "write" && parsed.data === undefined && parsed._[3] === undefined && !process.stdin.isTTY) {
const chunks: Buffer[] = [];
for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
return Buffer.concat(chunks).toString("utf8");
}
return undefined;
}
export async function runHwpodCompilerCli(argv: string[], options: { env?: EnvLike; fetchImpl?: FetchLike; now?: () => string } = {}) {
const env = options.env ?? process.env;
const now = options.now ?? (() => new Date().toISOString());
try {
const parsed = parseOptions(argv);
const command = parsed._[0] || "help";
if (["help", "--help", "-h"].includes(command)) return result(0, compilerHelp(), now);
if (command !== "compile") throw cliError("unsupported_compiler_command", `unsupported hwpod-compiler-cli command: ${command}`);
const intent = requiredText(parsed.intent ?? parsed._[1], "intent");
const args = parseJsonObject(parsed.argsJson ?? parsed.args ?? "{}", "args");
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl: options.fetchImpl });
const plan = compileHwpodNodeOpsPlan({ document: resolved.document, specPath: resolved.specPath, specAuthority: resolved.authority, intent, args, now });
return result(0, ok("hwpod-compiler.compile", { specPath: resolved.specPath, hwpodId: resolved.hwpodId, specAuthority: resolved.authority, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, plan }), now);
} catch (error) {
return result(1, failure(COMPILER_NAME, error), now);
}
}
export async function runHwpodCtl(argv: string[], options: { env?: EnvLike; fetchImpl?: FetchLike; now?: () => string } = {}) {
const env = options.env ?? process.env;
const now = options.now ?? (() => new Date().toISOString());
try {
const parsed = parseOptions(argv);
const group = parsed._[0] || "help";
if (["help", "--help", "-h"].includes(group)) return result(0, ctlHelp(), now);
if (group === "bind") {
return result(0, await bindSpec(parsed, now), now);
}
if (group !== "spec") throw cliError("unsupported_ctl_command", `unsupported hwpod-ctl command: ${group}`);
const subcommand = parsed._[1] || "validate";
if (subcommand === "init") return result(0, await initSpec(parsed, now), now);
if (subcommand === "validate") return result(0, await validateSpec(parsed, env, options.fetchImpl), now);
if (subcommand === "show") return result(0, await showSpec(parsed, env, options.fetchImpl), now);
if (subcommand === "set") return result(0, await setSpec(parsed, now), now);
if (subcommand === "bind") return result(0, await bindSpec(parsed, now), now);
throw cliError("unsupported_spec_command", `unsupported hwpod-ctl spec command: ${subcommand}`);
} catch (error) {
return result(1, failure(CTL_NAME, error), now);
}
}
export async function runHwpodCli(argv: string[], options: { env?: EnvLike; fetchImpl?: FetchLike; stdinText?: string; now?: () => string } = {}) {
const env = options.env ?? process.env;
const now = options.now ?? (() => new Date().toISOString());
try {
const parsed = parseOptions(argv);
const command = parsed._[0] || "help";
if (["help", "--help", "-h"].includes(command)) return result(0, cliHelp(), now);
if (parsed.help === true || parsed.h === true) return result(0, hwpodCliCommandHelp(command, parsed), now);
if (command === "closeout") return result(0, closeout(parsed), now);
const { intent, args } = commandToIntent(parsed, options.stdinText);
const compiled = await compilePlanWithCompilerCli({ parsed, env, fetchImpl: options.fetchImpl, intent, args });
const plan = compiled.plan;
if (parsed.dryRun === true) {
return result(0, ok("hwpod-cli.plan", { specPath: compiled.specPath, hwpodId: compiled.hwpodId, specAuthority: compiled.specAuthority, intent: plan.intent, contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION, compilerInvocation: compiled.compilerInvocation, plan, dryRun: true }), now);
}
const response = await submitHwpodNodeOpsPlan({ parsed, env, fetchImpl: options.fetchImpl, plan });
const exitCode = response.body?.ok === false || response.status >= 400 ? 1 : 0;
const diagnostic = hwpodNodeOpsCliDiagnostic(response);
const payload = ok("hwpod-cli.invoke", {
specPath: compiled.specPath,
hwpodId: compiled.hwpodId,
specAuthority: compiled.specAuthority,
intent: plan.intent,
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
compilerInvocation: compiled.compilerInvocation,
route: response.route,
runtimeEndpoint: response.runtimeEndpoint,
body: response.body,
httpStatus: response.status,
...(response.otelTraceId ? { otelTraceId: response.otelTraceId } : {}),
...(diagnostic ? { diagnostic } : {})
}, response.body?.status ?? (exitCode === 0 ? "succeeded" : "failed"));
if (exitCode !== 0) payload.ok = false;
return result(exitCode, payload, now);
} catch (error) {
return result(1, failure(CLI_NAME, error), now);
}
}
export async function readHwpodSpec(specPath = DEFAULT_HWPOD_SPEC_PATH) {
const text = await readFile(specPath, "utf8");
return normalizeHwpodSpec(parseSimpleYaml(text, specPath), specPath);
}
export function compileHwpodNodeOpsPlan({ document, specPath = DEFAULT_HWPOD_SPEC_PATH, specAuthority = "code-agent-workspace", intent, args = {}, now = () => new Date().toISOString() }: any) {
const normalizedIntent = normalizeIntent(intent);
const nodeId = document.spec.nodeBinding.nodeId;
const hwpodId = document.metadata.name || document.metadata.uid;
const ops = opsForIntent(normalizedIntent, args, document).map((operation: any, index: number) => ({
opId: operation.opId ?? `op_${String(index + 1).padStart(2, "0")}_${operation.op.replace(/[^a-z0-9]+/giu, "_")}`,
...operation
}));
return {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
planId: `hwpod_plan_${randomUUID()}`,
hwpodId,
nodeId,
intent: normalizedIntent,
source: {
compiler: COMPILER_NAME,
specPath,
specAuthority
},
resourceHints: {
workspacePath: document.spec.workspace.path,
targetDevice: document.spec.targetDevice,
debugProbe: document.spec.debugProbe,
ioProbe: document.spec.ioProbe
},
ops,
createdAt: now()
};
}
function compilerHelp() {
return ok("hwpod-compiler.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
usage: [
"bun tools/hwpod-compiler-cli.ts compile --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --intent workspace.ls --args '{\"path\":\".\"}'",
"bun tools/hwpod-compiler-cli.ts compile --spec .hwlab/hwpod-spec.yaml --intent workspace.ls --args '{\"path\":\".\"}'",
"bun tools/hwpod-compiler-cli.ts compile --intent inspect"
],
role: "Translate a runtime-resolved hwpodId plus high-level intent into hwpod-node-ops. Local --spec is an explicit debug/import path."
});
}
function ctlHelp() {
return ok("hwpod-ctl.help", {
contractVersion: "hwpod-ctl-v1",
usage: [
"bun tools/hwpod-ctl.ts spec validate --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"bun tools/hwpod-ctl.ts spec init --spec .hwlab/hwpod-spec.yaml --node local-node",
"bun tools/hwpod-ctl.ts spec validate --spec .hwlab/hwpod-spec.yaml",
"bun tools/hwpod-ctl.ts spec set spec.workspace.path /workspace/firmware",
"bun tools/hwpod-ctl.ts bind --node pc-host-1"
],
defaultSpecPath: DEFAULT_HWPOD_SPEC_PATH
});
}
function cliHelp() {
return ok("hwpod-cli.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
usage: [
"bun tools/hwpod-cli.ts inspect --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
"bun tools/hwpod-cli.ts workspace ls . --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
"bun tools/hwpod-cli.ts workspace cat projects/01_baseline/User/main.c --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"bun tools/hwpod-cli.ts workspace read projects/01_baseline/User/main.c --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"bun tools/hwpod-cli.ts workspace rg arm_2d_init projects/01_baseline/Middlewares/Arm-2D --context 3 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"cat patch.txt | bun tools/hwpod-cli.ts workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --reason \"edit subject workspace through hwpod-node\"",
"bun tools/hwpod-cli.ts workspace replace --path projects/01_baseline/User/main.c --find \"old text\" --replace \"new text\" --expected-sha <sha>",
"bun tools/hwpod-cli.ts workspace insert-after --path projects/01_baseline/User/main.c --anchor \"while (1)\" --line \" /* marker */\"",
"bun tools/hwpod-cli.ts build --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
"bun tools/hwpod-cli.ts download --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
"bun tools/hwpod-cli.ts job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
"bun tools/hwpod-cli.ts uart read --port uart1 --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run",
"bun tools/hwpod-cli.ts --api-base-url http://74.48.78.17:19667 inspect"
],
workspace: {
cat: "Read a subject workspace file through hwpod-node; path is relative to spec.workspace.path. `read` is a CLI alias for `cat`.",
applyPatch: "Apply a Codex/UniDesk apply_patch v2 envelope through hwpod-node. Patch content may come from stdin, --patch/--patchText/--patch-content, or --patchBase64. Raw unified diff is rejected with a format hint.",
write: "Write a whole file with optional --expected-sha, --line-ending preserve|lf|crlf and dry-run diff output.",
replace: "Replace exact text in a subject file with SHA/diff diagnostics; pass --all for global replacement.",
insertAfter: "Insert one or more lines after an exact anchor without shell quoting."
},
debug: {
build: "Keil build starts an async job by default; pass --wait only for bounded manual debugging.",
download: "Keil download compiles to a flash job through cmd.run, so build+program stays inside the Keil job.",
jobStatus: "Query a Keil async job with `hwpod job status <jobId>`; this compiles to cmd.run rather than a new node op."
},
uart: {
read: "Use the configured serial-monitor CLI through cmd.run. The hwpod-node contract stays at cmd.run plus file ops."
},
applyPatchExample: [
"*** Begin Patch",
"*** Update File: projects/01_baseline/User/main.c",
"@@",
" existing line",
"+new line",
"*** End Patch"
].join("\n"),
route: "POST /v1/hwpod-node-ops"
});
}
function hwpodCliCommandHelp(command: string, parsed: ParsedArgs) {
if (command === "workspace") return hwpodWorkspaceHelp(text(parsed._[1]));
if (command === "job") return hwpodJobHelp(text(parsed._[1]));
if (command === "build") return hwpodBuildHelp();
if (command === "download") return hwpodDownloadHelp();
if (command === "uart") return hwpodUartHelp(text(parsed._[1]));
if (command === "cmd") return hwpodCmdHelp();
return cliHelp();
}
function hwpodWorkspaceHelp(subcommand = "") {
return ok("hwpod-cli.workspace.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
command: "workspace",
subcommand: subcommand || null,
usage: [
"hwpod workspace ls [path] --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"hwpod workspace cat <path> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"hwpod workspace read <path> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"hwpod workspace rg <pattern> [path] --context 3 --max-matches 20 --hwpod-id d601-f103-v2 --workspace-path <run-worktree>",
"hwpod workspace write --path <path> --content <text> --expected-sha <sha>",
"hwpod workspace replace --path <path> --find <text> --replace <text> --expected-sha <sha>",
"hwpod workspace insert-after --path <path> --anchor <text> --line <text> --expected-sha <sha>",
"cat patch.txt | hwpod workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --reason <reason>",
"hwpod workspace apply-patch --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --patch-content '<apply_patch v2 envelope>'"
],
aliases: {
read: "workspace.cat",
grep: "workspace.rg",
search: "workspace.rg",
file: "--path",
remotePath: "--path",
patchContent: "--patch"
},
applyPatchEngine: "codex-apply-patch-v2-compatible",
applyPatchBoundary: "workspace apply-patch accepts a Codex/UniDesk v2 envelope. Put target paths in *** Update/Add/Delete File headers; --remote-path is only a path alias for file-oriented workspace commands, not a substitute for envelope headers.",
boundary: "Workspace file operations are the standard editing path; do not use shell quoting for subject text edits unless debugging cmd.run itself."
});
}
function hwpodJobHelp(subcommand = "") {
return ok("hwpod-cli.job.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
command: "job",
subcommand: subcommand || null,
usage: ["hwpod job status <jobId> --hwpod-id d601-f103-v2 --workspace-path <run-worktree>"],
boundary: "Job status compiles to cmd.run against the configured Keil CLI; it is not a separate hwpod-node op."
});
}
function hwpodBuildHelp() {
return ok("hwpod-cli.build.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
usage: ["hwpod build --hwpod-id d601-f103-v2 --workspace-path <run-worktree>", "hwpod build --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run"],
boundary: "Build is a short HWPOD CLI invocation that normally starts an async Keil job through cmd.run."
});
}
function hwpodDownloadHelp() {
return ok("hwpod-cli.download.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
usage: ["hwpod download --hwpod-id d601-f103-v2 --workspace-path <run-worktree>", "hwpod download --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --dry-run"],
boundary: "Download compiles to a Keil flash cmd.run plan; do not add a download-specific hwpod-node op."
});
}
function hwpodUartHelp(subcommand = "") {
return ok("hwpod-cli.uart.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
command: "uart",
subcommand: subcommand || null,
usage: ["hwpod uart read --hwpod-id d601-f103-v2 --workspace-path <run-worktree> --port uart1"],
boundary: "UART read is compiled to cmd.run against the configured serial-monitor CLI."
});
}
function hwpodCmdHelp() {
return ok("hwpod-cli.cmd.help", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
usage: ["hwpod cmd <command> [...argv] --hwpod-id d601-f103-v2 --workspace-path <run-worktree>"],
boundary: "cmd.run is the full-capability host execution op; add compiler/CLI combinations before considering new hwpod-node ops."
});
}
async function initSpec(parsed: ParsedArgs, now: () => string) {
const specPath = specPathFrom(parsed);
const document = defaultSpecDocument(parsed, now);
await mkdir(path.dirname(specPath), { recursive: true });
if (parsed.force !== true) {
try {
await readFile(specPath, "utf8");
throw cliError("hwpod_spec_exists", `hwpod spec already exists: ${specPath}`, { specPath, next: "pass --force to replace it" });
} catch (error) {
if (error?.code !== "ENOENT") throw error;
}
}
await writeFile(specPath, stringifySimpleYaml(document), "utf8");
return ok("hwpod-ctl.spec.init", { specPath, document: normalizeHwpodSpec(document, specPath) });
}
async function validateSpec(parsed: ParsedArgs, env: EnvLike = process.env, fetchImpl?: FetchLike) {
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl });
const document = resolved.document;
return ok("hwpod-ctl.spec.validate", { specPath: resolved.specPath, hwpodId: resolved.hwpodId, specAuthority: resolved.authority, document, fourElements: ["targetDevice", "workspace", "debugProbe", "ioProbe"], nodeId: document.spec.nodeBinding.nodeId });
}
async function showSpec(parsed: ParsedArgs, env: EnvLike = process.env, fetchImpl?: FetchLike) {
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl });
return ok("hwpod-ctl.spec.show", { specPath: resolved.specPath, hwpodId: resolved.hwpodId, specAuthority: resolved.authority, document: resolved.document });
}
async function setSpec(parsed: ParsedArgs, now: () => string) {
const specPath = specPathFrom(parsed);
const dotPath = requiredText(parsed._[2] ?? parsed.path, "path");
const value = scalarFromSetValue(requiredText(parsed._[3] ?? parsed.value, "value"));
const document = parseSimpleYaml(await readFile(specPath, "utf8"), specPath);
setDotPath(document, dotPath, value);
document.metadata = objectValue(document.metadata);
document.metadata.updatedAt = now();
const normalized = normalizeHwpodSpec(document, specPath);
await writeFile(specPath, stringifySimpleYaml(document), "utf8");
return ok("hwpod-ctl.spec.set", { specPath, path: dotPath, value, document: normalized });
}
async function bindSpec(parsed: ParsedArgs, now: () => string) {
const nodeId = requiredText(parsed.node ?? parsed.nodeId ?? parsed._[1] ?? parsed._[2], "node");
const specPath = specPathFrom(parsed);
const document = parseSimpleYaml(await readFile(specPath, "utf8"), specPath);
setDotPath(document, "spec.nodeBinding.nodeId", nodeId);
document.metadata = objectValue(document.metadata);
document.metadata.updatedAt = now();
const normalized = normalizeHwpodSpec(document, specPath);
await writeFile(specPath, stringifySimpleYaml(document), "utf8");
return ok("hwpod-ctl.bind", { specPath, nodeId, document: normalized });
}
function closeout(parsed: ParsedArgs) {
return ok("hwpod-cli.closeout", {
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
specPath: specPathFrom(parsed),
note: "Closeout is a reporting step; use hwpod-cli operations and hwlab-cli runtime evidence before closing the issue."
});
}
async function compilePlanWithCompilerCli({ parsed, env, fetchImpl, intent, args }: { parsed: ParsedArgs; env: EnvLike; fetchImpl?: FetchLike; intent: string; args: Record<string, unknown> }) {
const resolved = await resolveHwpodDocument({ parsed, env, fetchImpl });
const plan = compileHwpodNodeOpsPlan({ document: resolved.document, specPath: resolved.specPath, specAuthority: resolved.authority, intent, args });
return {
plan,
specPath: resolved.specPath,
hwpodId: resolved.hwpodId,
specAuthority: resolved.authority,
compilerInvocation: clean({
compiler: COMPILER_NAME,
mode: "in-process",
entrypoint: "tools/src/hwpod-harness-lib.ts",
argv: ["compile", resolved.specPath.startsWith("hwpod://") ? "--hwpod-id" : "--spec", resolved.specPath.startsWith("hwpod://") ? resolved.hwpodId : resolved.specPath, "--intent", intent, "--args", "<json>"],
exitCode: 0,
action: "hwpod-compiler.compile",
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
planId: plan.planId,
source: plan.source
})
};
}
async function resolveHwpodDocument({ parsed, env, fetchImpl }: { parsed: ParsedArgs; env: EnvLike; fetchImpl?: FetchLike }) {
const explicitSpecPath = text(parsed.spec ?? parsed.specPath);
if (explicitSpecPath) {
const document = applyHwpodRuntimeOverrides(await readHwpodSpec(explicitSpecPath), parsed, env, explicitSpecPath);
return { document, specPath: explicitSpecPath, hwpodId: document.metadata.name || document.metadata.uid, authority: "code-agent-workspace" };
}
const hwpodId = requiredText(parsed.hwpodId ?? parsed.hwpod ?? env.HWPOD_ID ?? env.HWLAB_HWPOD_ID, "hwpodId");
const spec = await fetchHwpodSpecById({ hwpodId, parsed, env, fetchImpl });
const document = applyHwpodRuntimeOverrides(spec.document, parsed, env, `hwpod://${hwpodId}`);
return {
document,
specPath: `hwpod://${hwpodId}`,
hwpodId: document.metadata.name || document.metadata.uid || hwpodId,
authority: text(spec.authority) || text(spec.source?.kind) || "runtime-hwpod-registry"
};
}
async function fetchHwpodSpecById({ hwpodId, parsed, env, fetchImpl }: { hwpodId: string; parsed: ParsedArgs; env: EnvLike; fetchImpl?: FetchLike }) {
const endpoint = resolveRuntimeEndpoint({ kind: "api", parsed, env });
const route = { method: "GET", path: `/v1/hwpod/specs?hwpodId=${encodeURIComponent(hwpodId)}` };
const url = `${endpoint.baseUrl}${route.path}`;
const headers = authHeaders(parsed, env);
const response = fetchImpl
? await fetchImpl(url, { method: route.method, headers })
: await requestJsonNative(url, { method: route.method, headers, timeoutMs: numberValue(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS });
const body = await response.json().catch(() => null);
if (!body || body.ok === false) throw cliError("hwpod_registry_request_failed", "failed to query HWPOD registry", { hwpodId, httpStatus: response.status, body: compactObject(body), runtimeEndpoint: runtimeEndpointVisibility(endpoint) });
const specs = Array.isArray(body.specs) ? body.specs : [];
const match = specs.find((item: any) => text(item.hwpodId) === hwpodId || text(item.name) === hwpodId || text(item.uid) === hwpodId);
if (!match) throw cliError("hwpod_id_not_found", `HWPOD id not found in runtime registry: ${hwpodId}`, { hwpodId, count: specs.length, runtimeEndpoint: runtimeEndpointVisibility(endpoint) });
const document = objectValue(match.document);
if (!isPlainObject(document)) throw cliError("hwpod_registry_document_missing", `HWPOD registry item has no document: ${hwpodId}`, { hwpodId, item: compactObject(match) });
return { ...match, document };
}
function applyHwpodRuntimeOverrides(document: any, parsed: ParsedArgs, env: EnvLike, sourcePath: string) {
const copy = JSON.parse(JSON.stringify(document));
const workspacePath = text(parsed.workspacePath ?? parsed.subjectWorktreePath ?? env.HWPOD_WORKSPACE_PATH ?? env.HWLAB_HWPOD_WORKSPACE_PATH);
if (workspacePath) {
copy.spec = objectValue(copy.spec);
copy.spec.workspace = { ...objectValue(copy.spec.workspace), path: workspacePath };
}
return normalizeHwpodSpec(copy, sourcePath);
}
function commandToIntent(parsed: ParsedArgs, stdinText?: string) {
const command = parsed._[0];
if (command === "inspect") return { intent: "inspect", args: {} };
if (command === "node") {
const subcommand = parsed._[1] || "health";
return { intent: `node.${subcommand}`, args: {} };
}
if (command === "workspace") {
const subcommand = parsed._[1] || "ls";
if (subcommand === "ls") return { intent: "workspace.ls", args: { path: text(parsed.path ?? parsed._[2] ?? ".") } };
if (subcommand === "cat" || subcommand === "read") return { intent: "workspace.cat", args: { path: requiredWorkspacePath(parsed, 2) } };
if (subcommand === "rg" || subcommand === "grep" || subcommand === "search") return { intent: "workspace.rg", args: workspaceSearchArgs(parsed) };
if (subcommand === "apply-patch") return { intent: "workspace.apply-patch", args: { patch: patchText(parsed.patch ?? parsed.patchText ?? parsed.patchContent ?? stdinText), patchBase64: text(parsed.patchBase64), reason: text(parsed.reason) } };
if (subcommand === "write") return { intent: "workspace.write", args: clean({ path: requiredWorkspacePath(parsed, 2), content: patchText(parsed.content ?? parsed.contentText ?? stdinText), contentBase64: text(parsed.contentBase64), expectedSha: text(parsed.expectedSha), lineEnding: text(parsed.lineEnding), finalNewline: parsed.finalNewline === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) };
if (subcommand === "replace") return { intent: "workspace.replace", args: cleanTextEditArgs({ path: requiredWorkspacePath(parsed, 2), find: requiredRawString(parsed.find ?? parsed._[3], "find"), replace: rawString(parsed.replace ?? parsed.replacement ?? parsed._[4]), expectedSha: text(parsed.expectedSha), all: parsed.all === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) };
if (subcommand === "insert-after") return { intent: "workspace.insert-after", args: cleanTextEditArgs({ path: requiredWorkspacePath(parsed, 2), anchor: requiredRawString(parsed.anchor ?? parsed.marker ?? parsed._[3], "anchor"), line: requiredRawString(parsed.line ?? parsed.content ?? parsed.insert ?? parsed.text ?? parsed._[4], "line"), expectedSha: text(parsed.expectedSha), allowMultiple: parsed.allowMultiple === true, dryRun: parsed.dryRun === true, reason: text(parsed.reason) }) };
throw cliError("unsupported_workspace_command", `unsupported workspace command: ${subcommand}`);
}
if (command === "build") return { intent: "debug.build", args: clean({ target: text(parsed.target), command: text(parsed.command ?? parsed.commandLine), wait: parsed.wait === true, timeoutMs: numberValue(parsed.timeoutMs), reason: text(parsed.reason) }) };
if (command === "download") return { intent: "debug.download", args: clean({ artifact: text(parsed.artifact ?? parsed._[1]), target: text(parsed.target), command: text(parsed.command ?? parsed.commandLine), wait: parsed.wait === true, timeoutMs: numberValue(parsed.timeoutMs), reason: text(parsed.reason) }) };
if (command === "job") {
const subcommand = parsed._[1] || "status";
if (subcommand === "status") return { intent: "debug.job-status", args: clean({ jobId: requiredText(parsed.jobId ?? parsed._[2], "jobId"), timeoutMs: numberValue(parsed.timeoutMs), reason: text(parsed.reason) }) };
throw cliError("unsupported_job_command", `unsupported job command: ${subcommand}`);
}
if (command === "reset") return { intent: "debug.reset", args: clean({ mode: text(parsed.mode ?? parsed._[1]), command: text(parsed.command ?? parsed.commandLine), reason: text(parsed.reason) }) };
if (command === "uart") {
const subcommand = parsed._[1] || "read";
if (subcommand === "read") return { intent: "io.uart.read", args: clean({ port: text(parsed.port ?? parsed._[2] ?? "uart1"), maxBytes: numberValue(parsed.maxBytes), limit: numberValue(parsed.limit), since: text(parsed.since), sessionOnly: parsed.sessionOnly === true, timeoutMs: numberValue(parsed.timeoutMs), serialMonitorDir: text(parsed.serialMonitorDir), serialMonitorCommand: text(parsed.serialMonitorCommand), reason: text(parsed.reason) }) };
if (subcommand === "write") return { intent: "io.uart.write", args: clean({ port: text(parsed.port ?? parsed._[2] ?? "uart1"), data: requiredText(parsed.data ?? parsed._[3] ?? stdinText, "data") }) };
throw cliError("unsupported_uart_command", `unsupported uart command: ${subcommand}`);
}
if (command === "jsonrpc") {
const method = requiredText(parsed.method ?? parsed._[2] ?? parsed._[1], "method");
return { intent: "io.uart.jsonrpc", args: clean({ port: text(parsed.port ?? "uart1"), method, params: parseJsonObject(parsed.paramsJson ?? parsed.params ?? "{}", "params") }) };
}
if (command === "cmd") return { intent: "cmd.run", args: { command: requiredText(parsed._[1] ?? parsed.command, "command"), argv: parsed._.slice(2) } };
throw cliError("unsupported_hwpod_command", `unsupported hwpod-cli command: ${command}`);
}
function workspaceSearchArgs(parsed: ParsedArgs) {
return clean({
pattern: requiredText(parsed.pattern ?? parsed._[2], "pattern"),
path: text(parsed.path ?? parsed._[3] ?? "."),
ignoreCase: parsed.ignoreCase === true,
beforeContext: numberValue(parsed.beforeContext),
afterContext: numberValue(parsed.afterContext),
context: numberValue(parsed.context),
maxMatches: numberValue(parsed.maxMatches),
maxFiles: numberValue(parsed.maxFiles),
maxBytesPerFile: numberValue(parsed.maxBytesPerFile),
maxLineBytes: numberValue(parsed.maxLineBytes)
});
}
function opsForIntent(intent: string, args: any, document: any) {
const common = commonOpArgs(document);
if (intent === "inspect") return [
{ op: "node.health", args: common },
{ op: "node.inventory", args: common }
];
if (intent === "node.health" || intent === "node.version" || intent === "node.inventory") return [{ op: intent, args: common }];
if (intent === "workspace.ls") return [{ op: "workspace.ls", args: { ...common, path: text(args.path) || "." } }];
if (intent === "workspace.cat") return [{ op: "workspace.cat", args: { ...common, path: requiredText(args.path, "path") } }];
if (intent === "workspace.rg") return [{ op: "workspace.rg", args: clean({ ...common, pattern: requiredText(args.pattern, "pattern"), path: text(args.path) || ".", ignoreCase: args.ignoreCase === true, beforeContext: numberValue(args.beforeContext), afterContext: numberValue(args.afterContext), context: numberValue(args.context), maxMatches: numberValue(args.maxMatches), maxFiles: numberValue(args.maxFiles), maxBytesPerFile: numberValue(args.maxBytesPerFile), maxLineBytes: numberValue(args.maxLineBytes) }) }];
if (intent === "workspace.apply-patch") return [{ op: "workspace.apply-patch", args: clean({ ...common, patch: patchText(args.patch), patchBase64: text(args.patchBase64), reason: text(args.reason) }) }];
if (intent === "workspace.write") return [{ op: "workspace.write", args: clean({ ...common, path: requiredText(args.path, "path"), content: patchText(args.content), contentBase64: text(args.contentBase64), expectedSha: text(args.expectedSha), lineEnding: text(args.lineEnding), finalNewline: args.finalNewline === true, dryRun: args.dryRun === true, reason: text(args.reason) }) }];
if (intent === "workspace.replace") return [{ op: "workspace.replace", args: cleanTextEditArgs({ ...common, path: requiredText(args.path, "path"), find: requiredRawString(args.find, "find"), replace: rawString(args.replace), expectedSha: text(args.expectedSha), all: args.all === true, dryRun: args.dryRun === true, reason: text(args.reason) }) }];
if (intent === "workspace.insert-after") return [{ op: "workspace.insert-after", args: cleanTextEditArgs({ ...common, path: requiredText(args.path, "path"), anchor: requiredRawString(args.anchor, "anchor"), line: requiredRawString(args.line, "line"), expectedSha: text(args.expectedSha), allowMultiple: args.allowMultiple === true, dryRun: args.dryRun === true, reason: text(args.reason) }) }];
if (intent === "debug.build") return debugBuildOps(common, args, document);
if (intent === "debug.download") return debugDownloadOps(common, args, document);
if (intent === "debug.job-status") return debugJobStatusOps(common, args, document);
if (intent === "debug.reset") return [{ op: "debug.reset", args: clean({ ...common, mode: text(args.mode), command: text(args.command) || text(document.spec.debugProbe.resetCommand), reason: text(args.reason) }) }];
if (intent === "io.uart.read") return uartReadOps(common, args, document);
if (intent === "io.uart.write") return [{ op: "io.uart.write", args: clean({ ...common, port: text(args.port) || "uart1", data: requiredText(args.data, "data") }) }];
if (intent === "io.uart.jsonrpc") return [{ op: "io.uart.jsonrpc", args: clean({ ...common, port: text(args.port) || "uart1", method: requiredText(args.method, "method"), params: objectValue(args.params) }) }];
if (intent === "cmd.run") return [{ op: "cmd.run", args: clean({ ...common, command: requiredText(args.command, "command"), argv: Array.isArray(args.argv) ? args.argv : [] }) }];
throw cliError("unsupported_hwpod_intent", `unsupported hwpod intent: ${intent}`, { supportedIntents: Array.from(HWPOD_NODE_OPS).concat(["inspect"]) });
}
function commonOpArgs(document: any) {
return {
hwpodId: document.metadata.name || document.metadata.uid,
workspacePath: document.spec.workspace.path,
targetDevice: document.spec.targetDevice,
debugProbe: document.spec.debugProbe,
ioProbe: document.spec.ioProbe
};
}
function debugBuildOps(common: any, args: any, document: any) {
const explicitCommand = text(args.command) || text(document.spec.workspace.buildCommand);
const generated = explicitCommand ? null : keilCommandForIntent("debug.build", args, document);
const opArgs = clean({
...common,
target: text(args.target) || generated?.target,
commandBinding: generated?.binding,
timeoutMs: numberValue(args.timeoutMs) ?? generated?.timeoutMs,
reason: text(args.reason)
});
if (generated?.commandRun) {
return [{ op: "debug.build", args: clean({ ...opArgs, commandRun: generated.commandRun, command: generated.command }) }];
}
return [{ op: "debug.build", args: clean({ ...opArgs, command: explicitCommand || generated?.command }) }];
}
function debugDownloadArgs(common: any, args: any, document: any) {
const explicitCommand = text(args.command) || text(document.spec.debugProbe.downloadCommand);
const generated = explicitCommand ? null : keilCommandForIntent("debug.download", args, document);
return clean({
...common,
artifact: text(args.artifact),
target: text(args.target) || generated?.target,
command: explicitCommand || generated?.command,
commandRuns: generated?.commandRuns,
commandBinding: generated?.binding,
timeoutMs: numberValue(args.timeoutMs) ?? generated?.timeoutMs,
reason: text(args.reason)
});
}
function debugDownloadOps(common: any, args: any, document: any) {
const explicitCommand = text(args.command) || text(document.spec.debugProbe.downloadCommand);
const generated = explicitCommand ? null : keilCommandForIntent("debug.download", args, document);
const opArgs = clean({
...common,
artifact: text(args.artifact),
target: text(args.target) || generated?.target,
commandBinding: generated?.binding,
timeoutMs: numberValue(args.timeoutMs) ?? generated?.timeoutMs,
reason: text(args.reason)
});
if (generated?.commandRuns?.length) {
return generated.commandRuns.map((entry: any) => ({
op: "cmd.run",
args: clean({
...opArgs,
commandBinding: entry.commandBinding ?? opArgs.commandBinding,
command: entry.commandRun.command,
argv: entry.commandRun.argv,
commandLine: entry.command,
step: entry.step
})
}));
}
return [{ op: "debug.download", args: debugDownloadArgs(common, args, document) }];
}
function debugJobStatusOps(common: any, args: any, document: any) {
const generated = keilJobStatusCommand(args, document);
return [{
op: "cmd.run",
args: clean({
...common,
workspacePath: dirnameForCommandPath(generated.keilCliPath),
command: generated.commandRun.command,
argv: generated.commandRun.argv,
commandLine: generated.command,
step: "keil-job-status",
commandBinding: generated.binding,
timeoutMs: numberValue(args.timeoutMs) ?? generated.timeoutMs,
reason: text(args.reason)
})
}];
}
function uartReadOps(common: any, args: any, document: any) {
const generated = serialMonitorReadCommand(args, document);
return [{
op: "io.uart.read",
args: clean({
...common,
port: generated.physicalPort,
baudRate: generated.baudRate,
maxBytes: numberValue(args.maxBytes),
limit: numberValue(args.limit),
since: text(args.since),
sessionOnly: args.sessionOnly === true,
serialMonitorDir: generated.serialMonitorDir,
serialMonitorCommand: generated.commandBase,
commandBinding: generated.binding,
timeoutMs: numberValue(args.timeoutMs) ?? generated.timeoutMs,
reason: text(args.reason)
})
}];
}
function keilCommandForIntent(intent: "debug.build" | "debug.download", args: any, document: any) {
const workspace = objectValue(document.spec.workspace);
const debugProbe = objectValue(document.spec.debugProbe);
const toolchain = text(workspace.toolchain).toLowerCase();
if (!toolchain || !["keil", "keil-mdk", "mdk", "uv4"].includes(toolchain)) return null;
const project = hwpodKeilProject(args, document);
const target = hwpodKeilTarget(args, document);
const keilCliPath = text(args.keilCliPath) || text(workspace.keilCliPath) || text(debugProbe.keilCliPath) || text(objectValue(document.spec.tooling).keilCliPath) || "keil-cli.py";
const pythonCommand = text(args.pythonCommand) || text(workspace.pythonCommand) || text(debugProbe.pythonCommand) || text(objectValue(document.spec.tooling).pythonCommand) || "py -3";
const timeoutMs = numberValue(args.timeoutMs) ?? numberValue(workspace.keilCommandTimeoutMs) ?? numberValue(debugProbe.keilCommandTimeoutMs) ?? DEFAULT_KEIL_COMMAND_TIMEOUT_MS;
const commonTokens = [...splitCommandWords(pythonCommand), keilCliPath];
if (intent === "debug.build") {
const commandTokens = [...commonTokens, "build", "-p", project, ...targetOption(target), ...(args.wait === true ? ["--wait"] : [])];
return {
target,
command: shellCommand(commandTokens),
commandRun: commandRun(commandTokens),
timeoutMs,
binding: clean({ kind: "keil-mdk", source: "hwpod-compiler.keil-mdk", action: "build", project, target, keilCliPath, pythonCommand, wait: args.wait === true })
};
}
const probeUid = text(args.probeUid ?? args.probe) || text(debugProbe.probeUid ?? debugProbe.uid);
const programmer = text(args.programmer) || text(debugProbe.programmer) || (text(debugProbe.type).toLowerCase() === "daplink" ? "daplink" : "daplink");
const programBackend = text(args.programBackend ?? args.backend) || text(debugProbe.programBackend) || (text(debugProbe.adapter).toLowerCase() === "keil" ? "keil" : "keil");
const probeName = text(args.probeName) || text(debugProbe.probeName) || text(debugProbe.name);
const autoBindUvoptx = debugProbe.autoBindUvoptx === true || debugProbe.autoBindProbe === true;
const sidecarDiagnostics = keilSidecarDiagnosticRun(project);
const bindTokens = autoBindUvoptx && probeUid
? [...commonTokens, "project", "probe-binding", "set", "-p", project, ...projectTargetOption(target), "--probe-uid", probeUid, ...probeNameOption(probeName)]
: [];
const flashTokens = [
...commonTokens,
"flash",
"-p",
project,
"-m",
programmer,
"--program-backend",
programBackend,
...probeOption(probeUid),
...targetOption(target),
...(args.wait === true ? ["--wait"] : [])
];
const commandRuns = [
sidecarDiagnostics,
...(bindTokens.length > 0 ? [{ step: "keil-probe-binding", command: shellCommand(bindTokens), commandRun: commandRun(bindTokens) }] : []),
{ step: "keil-flash", command: shellCommand(flashTokens), commandRun: commandRun(flashTokens) }
];
return {
target,
command: commandRuns.map((entry) => entry.command).join(" && "),
commandRuns,
timeoutMs,
binding: clean({ kind: "keil-mdk", source: "hwpod-compiler.keil-mdk", action: "download", keilCommand: "flash", project, target, keilCliPath, pythonCommand, programmer, programBackend, probeUid, probeName, autoBindUvoptx: bindTokens.length > 0, wait: args.wait === true, keilSidecars: sidecarDiagnostics.commandBinding })
};
}
function keilSidecarDiagnosticRun(project: string) {
const base = project.replace(/\.uvprojx$/iu, "");
const primary = `${base}.uvoptx`;
const alternate = `${base}.uvopt`;
const script = [
"const fs=require('fs');",
"const input=JSON.parse(process.argv[1]||'{}');",
"const primary=String(input.primary||'');",
"const alternate=String(input.alternate||'');",
"let result;",
"if(fs.existsSync(primary)){result={status:'present',primary};}",
"else if(fs.existsSync(alternate)){result={status:'present',alternate};}",
"else{result={status:'missing',primary,alternate,reason:'keil_uvoptx_binding_absent'};}",
"console.log('hwpodKeilSidecar '+JSON.stringify(result));"
].join("");
const run = { command: "node", argv: ["-e", script, JSON.stringify({ primary, alternate })] };
return { step: "keil-sidecar-diagnostics", command: shellCommand([run.command, ...run.argv]), commandRun: run, commandBinding: { primary, alternate } };
}
function keilJobStatusCommand(args: any, document: any) {
const workspace = objectValue(document.spec.workspace);
const debugProbe = objectValue(document.spec.debugProbe);
const tooling = objectValue(document.spec.tooling);
const keilCliPath = text(args.keilCliPath) || text(workspace.keilCliPath) || text(debugProbe.keilCliPath) || text(tooling.keilCliPath) || "keil-cli.py";
const pythonCommand = text(args.pythonCommand) || text(workspace.pythonCommand) || text(debugProbe.pythonCommand) || text(tooling.pythonCommand) || "py -3";
const timeoutMs = numberValue(args.timeoutMs) ?? numberValue(workspace.keilCommandTimeoutMs) ?? numberValue(debugProbe.keilCommandTimeoutMs) ?? DEFAULT_KEIL_COMMAND_TIMEOUT_MS;
const jobId = requiredText(args.jobId, "jobId");
const commandTokens = [...splitCommandWords(pythonCommand), keilCliPath, "job-status", jobId];
return {
keilCliPath,
command: shellCommand(commandTokens),
commandRun: commandRun(commandTokens),
timeoutMs,
binding: clean({ kind: "keil-mdk", source: "hwpod-compiler.keil-mdk", action: "job-status", jobId, keilCliPath, pythonCommand })
};
}
function serialMonitorReadCommand(args: any, document: any) {
const ioProbe = objectValue(document.spec.ioProbe);
const uart = objectValue(ioProbe.uart);
const tooling = objectValue(document.spec.tooling);
const requestedPort = text(args.port) || text(uart.id) || "uart1";
const physicalPort = text(args.physicalPort) || text(uart.port) || text(uart.path) || text(ioProbe.port) || requestedPort;
const baudRate = numberValue(args.baudRate ?? args.baudrate ?? uart.baudRate ?? uart.baudrate ?? ioProbe.baudRate ?? ioProbe.baudrate) ?? 115200;
const serialMonitorDir = text(args.serialMonitorDir) || text(tooling.serialMonitorDir) || text(ioProbe.serialMonitorDir) || "C:\\Users\\liang\\.agents\\skills\\serial-monitor";
const commandBase = serialMonitorCommandBaseForCompiler(args, tooling, ioProbe);
const timeoutMs = numberValue(args.timeoutMs) ?? 30000;
const limit = Math.max(1, Math.min(numberValue(args.limit) ?? Math.ceil((numberValue(args.maxBytes) ?? 4096) / 80), 200));
const fetchArgs = ["fetch", "-l", String(limit), ...(args.sessionOnly === true ? ["--session-only"] : []), ...(text(args.since) ? ["-s", text(args.since)] : [])];
const sequenceRun = jsonCliSequenceCommandRun([
[...commandBase, "monitor", "start", "-p", physicalPort, "-b", String(baudRate)],
[...commandBase, ...fetchArgs]
]);
return {
physicalPort,
baudRate,
serialMonitorDir,
commandBase,
command: sequenceRun.commandLine,
commandRun: sequenceRun.commandRun,
timeoutMs,
binding: clean({ kind: "serial-monitor-cli", source: "hwpod-compiler.serial-monitor", action: "uart-read", requestedPort, physicalPort, baudRate, serialMonitorDir, sessionOnly: args.sessionOnly === true })
};
}
function serialMonitorCommandBaseForCompiler(args: any, tooling: any, ioProbe: any) {
const configured = text(args.serialMonitorCommand) || text(tooling.serialMonitorCommand) || text(ioProbe.serialMonitorCommand);
if (configured) return splitCommandWords(configured);
return ["bun", "scripts/serial-monitor-cli.ts"];
}
function jsonCliSequenceCommandRun(sequence: string[][]) {
const script = [
"const {spawnSync}=require('child_process');",
"const seq=JSON.parse(process.argv[1]);",
"for (const argv of seq) {",
"const r=spawnSync(argv[0],argv.slice(1),{encoding:'utf8'});",
"if(r.stdout) process.stdout.write(r.stdout);",
"if(r.stderr) process.stderr.write(r.stderr);",
"let ok=r.status===0;",
"try{const body=JSON.parse((r.stdout||'').trim()); if(body && body.success===false) ok=false;}catch{}",
"if(!ok) process.exit(r.status||1);",
"}",
"process.exit(0);"
].join(" ");
return {
commandLine: `node -e ${shellArg(script)} ${shellArg(JSON.stringify(sequence))}`,
commandRun: { command: "node", argv: ["-e", script, JSON.stringify(sequence)] }
};
}
function hwpodKeilProject(args: any, document: any) {
const workspace = objectValue(document.spec.workspace);
const projectWorkspace = objectValue(document.spec.projectWorkspace);
const rawProject = text(args.project ?? args.projectPath) || text(workspace.keilProject) || text(workspace.projectPath) || text(workspace.project) || text(projectWorkspace.projectPath);
if (!rawProject) throw cliError("hwpod_keil_project_required", "keil-mdk hwpod-spec requires spec.workspace.keilProject or spec.workspace.projectPath");
return resolveWorkspaceRelativePath(text(workspace.path), rawProject);
}
function hwpodKeilTarget(args: any, document: any) {
const workspace = objectValue(document.spec.workspace);
const projectWorkspace = objectValue(document.spec.projectWorkspace);
return text(args.target) || text(workspace.keilTarget) || text(workspace.targetName) || text(workspace.target) || text(projectWorkspace.targetName);
}
function resolveWorkspaceRelativePath(workspacePath: string, candidate: string) {
if (!workspacePath || isAbsoluteLikePath(candidate)) return candidate;
const separator = /^[A-Za-z]:[\\/]/u.test(workspacePath) || workspacePath.includes("\\") ? "\\" : "/";
const base = workspacePath.replace(/[\\/]+$/u, "");
const child = separator === "\\" ? candidate.replace(/\//gu, "\\") : candidate.replace(/\\/gu, "/");
return `${base}${separator}${child.replace(/^[\\/]+/u, "")}`;
}
function isAbsoluteLikePath(value: string) {
return /^[A-Za-z]:[\\/]/u.test(value) || value.startsWith("/") || value.startsWith("\\\\");
}
function dirnameForCommandPath(candidate: string) {
const value = text(candidate);
if (!value) return ".";
if (/^[A-Za-z]:[\\/]/u.test(value) || value.includes("\\")) {
const normalized = value.replace(/[\\/]+$/u, "");
const parent = normalized.replace(/[\\/][^\\/]*$/u, "");
return parent && parent !== normalized ? parent : ".";
}
return path.dirname(value);
}
function targetOption(target: string) {
return target ? ["-t", target] : [];
}
function projectTargetOption(target: string) {
return target ? ["--target", target] : [];
}
function probeOption(probeUid: string) {
return probeUid ? ["-u", probeUid] : [];
}
function probeNameOption(probeName: string) {
return probeName ? ["--probe-name", probeName] : [];
}
function splitCommandWords(command: string) {
const words = command.trim().split(/\s+/u).filter(Boolean);
if (words.length === 0) throw cliError("invalid_keil_python_command", "pythonCommand must not be empty");
return words;
}
function shellCommand(tokens: string[]) {
return tokens.map(shellArg).join(" ");
}
function commandRun(tokens: string[]) {
const [command, ...argv] = tokens;
return { command, argv };
}
function shellArg(value: string) {
const raw = String(value);
if (/^[A-Za-z0-9_./:@\\-]+$/u.test(raw)) return raw;
return `"${raw.replace(/(["^&|<>])/gu, "^$1").replace(/%/gu, "%%")}"`;
}
function normalizeIntent(value: unknown) {
const intent = requiredText(value, "intent");
if (intent === "build") return "debug.build";
if (intent === "download") return "debug.download";
if (intent === "reset") return "debug.reset";
if (intent === "uart.read") return "io.uart.read";
if (intent === "uart.write") return "io.uart.write";
if (intent === "jsonrpc.call") return "io.uart.jsonrpc";
return intent;
}
function defaultSpecDocument(parsed: ParsedArgs, now: () => string) {
const name = text(parsed.name) || "local-hwpod";
return {
apiVersion: "hwlab.pikastech.com/v1alpha1",
kind: "Hwpod",
metadata: {
name,
uid: text(parsed.uid) || `hwpod-${name}`,
createdAt: now()
},
spec: {
nodeBinding: {
nodeId: text(parsed.node ?? parsed.nodeId) || "local-node"
},
targetDevice: {
id: text(parsed.targetDevice ?? parsed.targetDeviceId) || "target-device-local"
},
workspace: {
path: text(parsed.workspace ?? parsed.workspacePath) || "."
},
debugProbe: {
id: text(parsed.debugProbe ?? parsed.debugProbeId) || "debug-probe-local",
kind: text(parsed.debugProbeKind) || "generic"
},
ioProbe: {
id: text(parsed.ioProbe ?? parsed.ioProbeId) || "io-probe-local",
kind: text(parsed.ioProbeKind) || "generic"
}
}
};
}
function normalizeHwpodSpec(document: any, specPath: string) {
const root = objectValue(document);
if (root.kind !== "Hwpod") throw cliError("invalid_hwpod_spec_kind", "hwpod-spec kind must be Hwpod", { specPath, kind: root.kind });
const spec = objectValue(root.spec);
const metadata = objectValue(root.metadata);
for (const key of ["targetDevice", "workspace", "debugProbe", "ioProbe"]) {
if (!isPlainObject(spec[key])) throw cliError("invalid_hwpod_spec_element", `hwpod-spec missing ${key}`, { specPath, element: key });
}
const workspacePath = requiredText(spec.workspace.path, "spec.workspace.path");
const nodeBinding = objectValue(spec.nodeBinding);
const nodeId = requiredText(nodeBinding.nodeId, "spec.nodeBinding.nodeId");
const name = requiredText(metadata.name ?? metadata.uid, "metadata.name");
return {
apiVersion: text(root.apiVersion) || "hwlab.pikastech.com/v1alpha1",
kind: "Hwpod",
metadata: { ...metadata, name },
spec: {
...spec,
nodeBinding: { ...nodeBinding, nodeId },
workspace: { ...spec.workspace, path: workspacePath },
targetDevice: spec.targetDevice,
debugProbe: spec.debugProbe,
ioProbe: spec.ioProbe
}
};
}
async function submitHwpodNodeOpsPlan({ parsed, env, fetchImpl, plan }: { parsed: ParsedArgs; env: EnvLike; fetchImpl?: FetchLike; plan: any }) {
const endpoint = resolveRuntimeEndpoint({ kind: "api", parsed, env });
const route = { method: "POST", path: "/v1/hwpod-node-ops" };
const url = `${endpoint.baseUrl}${route.path}`;
const headers = authHeaders(parsed, env);
const requestBody = JSON.stringify(plan);
const response = fetchImpl
? await fetchImpl(url, { method: route.method, headers, body: requestBody })
: await postJsonNative(url, { method: route.method, headers, body: requestBody, timeoutMs: numberValue(parsed.timeoutMs) ?? DEFAULT_TIMEOUT_MS });
const body = await response.json().catch(() => null);
const otelTraceId = responseHeader(response, "x-hwlab-otel-trace-id") || hwpodNodeOpsOtelTraceId(body);
const traceparent = responseHeader(response, "traceparent") || text(body?.requestMeta?.traceparent);
return { status: response.status, body: enrichHwpodNodeOpsBody(body, { otelTraceId, traceparent }), route, runtimeEndpoint: runtimeEndpointVisibility(endpoint), otelTraceId, traceparent };
}
function hwpodNodeOpsCliDiagnostic(response: any) {
const body = response?.body;
if (!body || typeof body !== "object" || body.ok !== false) return null;
const blocker = body.blocker ?? body.results?.find?.((result: any) => result?.blocker)?.blocker ?? body.error ?? {};
const otelTraceId = text(response?.otelTraceId ?? blocker?.otelTraceId ?? blocker?.diagnostic?.otelTraceId ?? body?.otelTraceId ?? body?.requestMeta?.otelTraceId);
return {
code: text(blocker?.code) || "hwpod_node_ops_failed",
summary: text(blocker?.summary ?? blocker?.message) || "hwpod-node-ops request failed",
otelTraceId: otelTraceId || null,
traceLine: otelTraceId ? `OTel traceId: ${otelTraceId}` : null,
route: response?.route,
httpStatus: response?.status,
valuesPrinted: false
};
}
function enrichHwpodNodeOpsBody(body: any, meta: { otelTraceId?: string; traceparent?: string } = {}) {
if (!body || typeof body !== "object") return body;
const otelTraceId = text(meta.otelTraceId) || hwpodNodeOpsOtelTraceId(body);
if (!otelTraceId) return body;
const enrichBlocker = (blocker: any) => {
if (!blocker || typeof blocker !== "object") return blocker;
const diagnostic = {
...(blocker.diagnostic && typeof blocker.diagnostic === "object" ? blocker.diagnostic : {}),
code: text(blocker.code) || text(blocker.diagnostic?.code) || "hwpod_node_ops_failed",
summary: text(blocker.summary ?? blocker.message ?? blocker.diagnostic?.summary) || "hwpod-node-ops request failed",
otelTraceId,
traceLine: `OTel traceId: ${otelTraceId}`,
valuesPrinted: false
};
return { ...blocker, otelTraceId, diagnostic };
};
const blocker = enrichBlocker(body.blocker);
const results = Array.isArray(body.results)
? body.results.map((result: any) => result?.blocker ? { ...result, blocker: enrichBlocker(result.blocker) } : result)
: body.results;
return {
...body,
blocker,
results,
otelTraceId,
diagnostic: body.diagnostic ?? blocker?.diagnostic ?? results?.find?.((result: any) => result?.blocker?.diagnostic)?.blocker?.diagnostic,
requestMeta: body.requestMeta && typeof body.requestMeta === "object"
? { ...body.requestMeta, otelTraceId, ...(meta.traceparent ? { traceparent: meta.traceparent } : {}) }
: body.requestMeta
};
}
function hwpodNodeOpsOtelTraceId(body: any) {
return text(body?.otelTraceId ?? body?.diagnostic?.otelTraceId ?? body?.blocker?.otelTraceId ?? body?.blocker?.diagnostic?.otelTraceId ?? body?.requestMeta?.otelTraceId ?? body?.results?.find?.((result: any) => result?.blocker?.otelTraceId)?.blocker?.otelTraceId);
}
function responseHeader(response: any, name: string) {
const headers = response?.headers;
if (!headers) return "";
if (typeof headers.get === "function") return text(headers.get(name));
const raw = headers[String(name).toLowerCase()] ?? headers[name];
return text(Array.isArray(raw) ? raw[0] : raw);
}
function authHeaders(parsed: ParsedArgs, env: EnvLike) {
const headers: Record<string, string> = { "content-type": "application/json" };
if (parsed.noAuth === true) return headers;
const apiKey = hwpodApiKey(parsed, env);
if (apiKey) headers.authorization = `Bearer ${apiKey}`;
const sessionToken = text(parsed.sessionToken ?? env.HWLAB_CLOUD_API_SESSION_TOKEN ?? env.HWLAB_SESSION_TOKEN);
if (sessionToken) headers["x-hwlab-session-token"] = sessionToken;
const cookie = text(parsed.cookie ?? env.HWLAB_SESSION_COOKIE);
if (cookie) headers.cookie = cookie;
return headers;
}
function hwpodApiKey(parsed: ParsedArgs, env: EnvLike) {
const unsupported = [
text(parsed.apiKey) ? "--api-key" : "",
text(env.HWLAB_BEARER_TOKEN) ? "HWLAB_BEARER_TOKEN" : ""
].filter(Boolean);
if (unsupported.length > 0) {
throw cliError("unsupported_api_key_source", "HWPOD auth uses only HWLAB_API_KEY; remove API key aliases and export HWLAB_API_KEY instead.", {
allowed: "HWLAB_API_KEY",
unsupported
});
}
return text(env.HWLAB_API_KEY);
}
function requestJsonNative(urlValue: string, { method, headers, timeoutMs }: { method: string; headers: Record<string, string>; timeoutMs: number }) {
return new Promise<any>((resolve, reject) => {
const url = new URL(urlValue);
const client = url.protocol === "https:" ? https : http;
const request = client.request(url, { method, headers, timeout: timeoutMs }, (response) => {
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => {
const textValue = Buffer.concat(chunks).toString("utf8");
resolve({
status: response.statusCode ?? 0,
headers: response.headers,
json: async () => textValue ? JSON.parse(textValue) : null
});
});
});
request.on("timeout", () => request.destroy(cliError("hwpod_registry_timeout", `HWPOD registry request timed out after ${timeoutMs}ms`)));
request.on("error", reject);
request.end();
});
}
function postJsonNative(urlValue: string, { method, headers, body, timeoutMs }: { method: string; headers: Record<string, string>; body: string; timeoutMs: number }) {
return new Promise<any>((resolve, reject) => {
const url = new URL(urlValue);
const client = url.protocol === "https:" ? https : http;
const request = client.request(url, {
method,
headers: {
...headers,
"content-length": String(Buffer.byteLength(body))
},
timeout: timeoutMs
}, (response) => {
const chunks: Buffer[] = [];
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
response.on("end", () => {
const textValue = Buffer.concat(chunks).toString("utf8");
resolve({
status: response.statusCode ?? 0,
headers: response.headers,
json: async () => textValue ? JSON.parse(textValue) : null
});
});
});
request.on("timeout", () => request.destroy(cliError("hwpod_node_ops_timeout", `hwpod-node-ops request timed out after ${timeoutMs}ms`)));
request.on("error", reject);
request.end(body);
});
}
function parseSimpleYaml(textValue: string, sourcePath: string) {
const root: any = {};
const stack = [{ indent: -1, value: root }];
const lines = textValue.replace(/\r\n?/gu, "\n").split("\n");
for (let index = 0; index < lines.length; index += 1) {
const rawLine = stripYamlComment(lines[index]);
if (!rawLine.trim()) continue;
const indent = rawLine.match(/^ */u)?.[0].length ?? 0;
if (indent % 2 !== 0) throw cliError("invalid_hwpod_spec_yaml", `invalid indentation at ${sourcePath}:${index + 1}`);
const line = rawLine.slice(indent);
const match = line.match(/^([A-Za-z0-9_.-]+):(?:\s*(.*))?$/u);
if (!match) throw cliError("invalid_hwpod_spec_yaml", `unsupported YAML line at ${sourcePath}:${index + 1}`, { line });
while (stack[stack.length - 1].indent >= indent) stack.pop();
const parent = stack[stack.length - 1].value;
const key = match[1];
const rawValue = match[2] ?? "";
if (rawValue.trim() === "") {
const next: any = {};
parent[key] = next;
stack.push({ indent, value: next });
} else {
parent[key] = parseScalar(rawValue.trim());
}
}
return root;
}
function stringifySimpleYaml(value: any, indent = 0): string {
return `${Object.entries(value).map(([key, child]) => {
if (isPlainObject(child)) return `${" ".repeat(indent)}${key}:\n${stringifySimpleYaml(child, indent + 2)}`;
return `${" ".repeat(indent)}${key}: ${formatScalar(child)}`;
}).join("\n")}\n`;
}
function stripYamlComment(line: string) {
let quoted: string | null = null;
for (let index = 0; index < line.length; index += 1) {
const char = line[index];
if ((char === '"' || char === "'") && line[index - 1] !== "\\") quoted = quoted === char ? null : quoted || char;
if (char === "#" && !quoted && (index === 0 || /\s/u.test(line[index - 1]))) return line.slice(0, index).trimEnd();
}
return line;
}
function parseScalar(value: string) {
if (value === "true") return true;
if (value === "false") return false;
if (value === "null") return null;
if (/^-?\d+(?:\.\d+)?$/u.test(value)) return Number(value);
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
return value.startsWith('"') ? JSON.parse(value) : value.slice(1, -1).replace(/''/gu, "'");
}
return value;
}
function formatScalar(value: any) {
if (value === null || value === undefined) return "null";
if (typeof value === "boolean" || typeof value === "number") return String(value);
const stringValue = String(value);
return /^[A-Za-z0-9_./:@-]+$/u.test(stringValue) ? stringValue : JSON.stringify(stringValue);
}
function parseOptions(argv: string[]): ParsedArgs {
const parsed: ParsedArgs = { _: [] };
for (let index = 0; index < argv.length; index += 1) {
const token = argv[index];
if (token === "--") {
parsed._.push(...argv.slice(index + 1));
break;
}
if (!token.startsWith("-") || token === "-") {
parsed._.push(token);
continue;
}
if (token === "-h") {
parsed.h = true;
parsed.help = true;
continue;
}
if (["-i", "-A", "-B", "-C"].includes(token) || /^-[ABC]\d+$/u.test(token)) {
applyShortSearchOption(parsed, token, argv[index + 1]);
if (["-A", "-B", "-C"].includes(token)) index += 1;
continue;
}
const [rawKey, rawInline] = token.slice(2).split(/=(.*)/su, 2);
const key = camelCase(rawKey);
if (BOOLEAN_OPTIONS.has(key)) {
parsed[key] = rawInline === undefined || rawInline === "" ? true : truthy(rawInline);
continue;
}
if (rawInline !== undefined && rawInline !== "") {
parsed[key] = rawInline;
continue;
}
const next = argv[index + 1];
if (next === undefined || next.startsWith("--")) {
parsed[key] = true;
} else {
parsed[key] = next;
index += 1;
}
}
return parsed;
}
function applyShortSearchOption(parsed: ParsedArgs, token: string, next: string | undefined) {
if (token === "-i") {
parsed.ignoreCase = true;
return;
}
const flag = token[1];
const inlineValue = token.length > 2 ? token.slice(2) : next;
const value = numberValue(inlineValue);
if (flag === "A") parsed.afterContext = value;
if (flag === "B") parsed.beforeContext = value;
if (flag === "C") parsed.context = value;
}
function parseJsonObject(value: unknown, name: string) {
if (value === undefined || value === null || value === "") return {};
if (isPlainObject(value)) return value;
try {
const parsed = JSON.parse(String(value));
if (!isPlainObject(parsed)) throw new Error(`${name} must be a JSON object`);
return parsed;
} catch (error) {
throw cliError(`invalid_${name}_json`, `${name} must be a JSON object`, { reason: error.message });
}
}
function setDotPath(target: any, dotPath: string, value: any) {
const parts = dotPath.split(".").filter(Boolean);
if (parts.length === 0) throw cliError("invalid_hwpod_spec_path", "spec path is empty");
let cursor = target;
for (const part of parts.slice(0, -1)) {
if (!isPlainObject(cursor[part])) cursor[part] = {};
cursor = cursor[part];
}
cursor[parts[parts.length - 1]] = value;
}
function scalarFromSetValue(value: string) {
if (value === "true" || value === "false" || value === "null" || /^-?\d+(?:\.\d+)?$/u.test(value)) return parseScalar(value);
return value;
}
function specPathFrom(parsed: ParsedArgs) {
return text(parsed.spec ?? parsed.specPath) || DEFAULT_HWPOD_SPEC_PATH;
}
function requiredWorkspacePath(parsed: ParsedArgs, positionalIndex: number) {
return requiredText(parsed.path ?? parsed.file ?? parsed.remotePath ?? parsed.remote ?? parsed._[positionalIndex], "path");
}
function result(exitCode: number, payload: any, now: () => string) {
return { exitCode, payload: { ...payload, observedAt: now() } };
}
function ok(action: string, extra: Record<string, unknown> = {}, status = "succeeded") {
return { ok: true, action, status, ...extra };
}
function failure(action: string, error: any) {
return {
ok: false,
action,
status: "failed",
error: {
code: error?.code || "hwpod_cli_error",
message: error?.message || String(error),
details: error?.details || undefined
}
};
}
function cliError(code: string, message: string, details: Record<string, unknown> = {}) {
const error = new Error(message) as Error & { code?: string; details?: Record<string, unknown> };
error.code = code;
error.details = details;
return error;
}
function requiredText(value: unknown, name: string) {
const normalized = text(value);
if (!normalized) throw cliError("required_option_missing", `${name} is required`, { name });
return normalized;
}
function requiredRawString(value: unknown, name: string) {
const normalized = rawString(value);
if (!normalized) throw cliError("required_option_missing", `${name} is required`, { name });
return normalized;
}
function text(value: unknown) {
return typeof value === "string" && value.trim() ? value.trim() : "";
}
function rawString(value: unknown) {
return typeof value === "string" ? value : "";
}
function patchText(value: unknown) {
return typeof value === "string" && value.length > 0 ? value : "";
}
function numberValue(value: unknown) {
const parsed = Number.parseInt(String(value ?? ""), 10);
return Number.isInteger(parsed) ? parsed : undefined;
}
function clipText(value: unknown, maxBytes = 4000) {
const textValue = typeof value === "string" ? value : String(value ?? "");
const buffer = Buffer.from(textValue, "utf8");
if (buffer.length <= maxBytes) return textValue;
return `${buffer.subarray(0, maxBytes).toString("utf8").replace(/\uFFFD$/u, "")}\n... clipped ...`;
}
function clean<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined && item !== "" && item !== null)) as T;
}
function compactObject(value: any) {
if (value === null || value === undefined) return null;
if (!isPlainObject(value) && !Array.isArray(value)) return value;
try {
return JSON.parse(JSON.stringify(value));
} catch {
return { value: String(value) };
}
}
function cleanTextEditArgs<T extends Record<string, unknown>>(value: T): T {
return Object.fromEntries(Object.entries(value).filter(([key, item]) => item !== undefined && item !== null && (item !== "" || ["replace", "line"].includes(key)))) as T;
}
function objectValue(value: any) {
return isPlainObject(value) ? value : {};
}
function isPlainObject(value: any) {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function camelCase(value: string) {
return value.replace(/-([a-z0-9])/giu, (_match, char) => char.toUpperCase());
}
function truthy(value: unknown) {
return /^(?:1|true|yes|on)$/iu.test(String(value ?? ""));
}