hwpod: support stdin workspace patches
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "bun:test";
|
||||
|
||||
import { runHwpodCli } from "../src/hwpod-harness-lib.ts";
|
||||
|
||||
test("hwpod-cli help exposes workspace cat and apply-patch stdin usage", async () => {
|
||||
const result = await runHwpodCli(["help"], { now: () => "2026-06-06T00:00:00.000Z" });
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.action, "hwpod-cli.help");
|
||||
assert.equal(result.payload.usage.some((item: string) => /workspace cat/u.test(item)), true);
|
||||
assert.equal(result.payload.usage.some((item: string) => /workspace apply-patch/u.test(item)), true);
|
||||
assert.match(result.payload.applyPatchExample, /\*\*\* Begin Patch/u);
|
||||
});
|
||||
|
||||
test("hwpod-cli workspace apply-patch passes stdin patch to hwpod-node plan", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwpod-cli-test-"));
|
||||
const specPath = path.join(root, "hwpod-spec.yaml");
|
||||
const patch = "*** Begin Patch\n*** Update File: app.c\n@@\n old\n+new\n*** End Patch\n";
|
||||
try {
|
||||
await writeFile(specPath, hwpodSpecText(), "utf8");
|
||||
const result = await runHwpodCli(["workspace", "apply-patch", "--spec", specPath, "--reason", "test", "--dry-run"], {
|
||||
stdinText: patch,
|
||||
now: () => "2026-06-06T00:00:00.000Z"
|
||||
});
|
||||
|
||||
assert.equal(result.exitCode, 0);
|
||||
assert.equal(result.payload.action, "hwpod-cli.plan");
|
||||
assert.equal(result.payload.intent, "workspace.apply-patch");
|
||||
assert.equal(result.payload.plan.ops[0].op, "workspace.apply-patch");
|
||||
assert.equal(result.payload.plan.ops[0].args.patch, patch);
|
||||
assert.equal(result.payload.plan.ops[0].args.reason, "test");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function hwpodSpecText() {
|
||||
return "kind: Hwpod\nmetadata:\n name: test-hwpod\nspec:\n nodeBinding:\n nodeId: test-node\n workspace:\n path: F:\\\\Work\\\\HWLAB-CASE-F103\n targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n";
|
||||
}
|
||||
@@ -33,11 +33,29 @@ export async function mainHwpodCtl(argv = process.argv.slice(2), options: any =
|
||||
}
|
||||
|
||||
export async function mainHwpodCli(argv = process.argv.slice(2), options: any = {}) {
|
||||
const result = await runHwpodCli(argv, options);
|
||||
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" && subcommand === "apply-patch" && parsed.patch === undefined && parsed.patchText === undefined && parsed.patchBase64 === 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; now?: () => string } = {}) {
|
||||
const now = options.now ?? (() => new Date().toISOString());
|
||||
try {
|
||||
@@ -165,10 +183,24 @@ function cliHelp() {
|
||||
usage: [
|
||||
"bun tools/hwpod-cli.ts inspect --dry-run",
|
||||
"bun tools/hwpod-cli.ts workspace ls . --dry-run",
|
||||
"bun tools/hwpod-cli.ts workspace cat projects/01_baseline/User/main.c --spec .hwlab/hwpod-spec.yaml",
|
||||
"cat patch.txt | bun tools/hwpod-cli.ts workspace apply-patch --spec .hwlab/hwpod-spec.yaml --reason \"edit subject workspace through hwpod-node\"",
|
||||
"bun tools/hwpod-cli.ts build --dry-run",
|
||||
"bun tools/hwpod-cli.ts uart read --port uart1 --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.",
|
||||
applyPatch: "Apply a Codex apply_patch envelope through hwpod-node. Patch content may come from stdin, --patch/--patchText, or --patchBase64."
|
||||
},
|
||||
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"
|
||||
});
|
||||
}
|
||||
@@ -291,7 +323,7 @@ function commandToIntent(parsed: ParsedArgs, stdinText?: string) {
|
||||
if (subcommand === "ls") return { intent: "workspace.ls", args: { path: text(parsed.path ?? parsed._[2] ?? ".") } };
|
||||
if (subcommand === "cat") return { intent: "workspace.cat", args: { path: requiredText(parsed.path ?? parsed._[2], "path") } };
|
||||
if (subcommand === "rg") return { intent: "workspace.rg", args: { pattern: requiredText(parsed.pattern ?? parsed._[2], "pattern"), path: text(parsed.path ?? parsed._[3] ?? "."), ignoreCase: parsed.ignoreCase === true } };
|
||||
if (subcommand === "apply-patch") return { intent: "workspace.apply-patch", args: { patch: text(parsed.patch ?? parsed.patchText ?? stdinText), patchBase64: text(parsed.patchBase64), reason: text(parsed.reason) } };
|
||||
if (subcommand === "apply-patch") return { intent: "workspace.apply-patch", args: { patch: patchText(parsed.patch ?? parsed.patchText ?? stdinText), patchBase64: text(parsed.patchBase64), 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), reason: text(parsed.reason) }) };
|
||||
@@ -321,7 +353,7 @@ function opsForIntent(intent: string, args: any, document: any) {
|
||||
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: { ...common, pattern: requiredText(args.pattern, "pattern"), path: text(args.path) || ".", ignoreCase: args.ignoreCase === true } }];
|
||||
if (intent === "workspace.apply-patch") return [{ op: "workspace.apply-patch", args: clean({ ...common, patch: text(args.patch), patchBase64: text(args.patchBase64), reason: text(args.reason) }) }];
|
||||
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 === "debug.build") return debugBuildOps(common, args, document);
|
||||
if (intent === "debug.download") return [{ op: "debug.download", args: debugDownloadArgs(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) }) }];
|
||||
@@ -794,6 +826,10 @@ function text(value: unknown) {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : "";
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user