Merge pull request #2708 from pikasTech/feat/hwpod-l0-cli
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success
Pipelines as Code CI / hwlab-nc01-v03-ci-poll- Success
增加 HWPOD L0 CLI 编译与 workspace 执行
This commit is contained in:
@@ -36,6 +36,9 @@ if (argv[0] === "tasktree") {
|
||||
console.log(JSON.stringify({ ok: false, operation: "hwpod", error: { code: error?.code ?? "hwpod_cli_error", message: error?.message ?? String(error) } }, null, 2));
|
||||
process.exitCode = 1;
|
||||
}
|
||||
} else if (argv[0] === "hwpod") {
|
||||
const { mainHwpodCli } = await import("../../src/hwpod-harness-lib.ts");
|
||||
await mainHwpodCli(argv.slice(1));
|
||||
} else if (argv[0] === "workbench") {
|
||||
const { runWorkbenchCli } = await import("../../src/workbench-cli.ts");
|
||||
try {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "bun:test";
|
||||
@@ -56,6 +56,69 @@ test("hwpod-cli workspace apply-patch accepts --patch-content alias", async () =
|
||||
}
|
||||
});
|
||||
|
||||
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";
|
||||
test("hwlab-cli hwpod L0 executes workspace edits and a bounded build helper", async () => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "hwpod-cli-l0-"));
|
||||
const specPath = path.join(root, "hwpod-spec.yaml");
|
||||
const sourcePath = path.join(root, "app.c");
|
||||
const artifactPath = path.join(root, "firmware.hex");
|
||||
const patch = "*** Begin Patch\n*** Update File: app.c\n@@\n-old\n+new\n*** End Patch\n";
|
||||
try {
|
||||
await writeFile(specPath, hwpodSpecText(root, "node build.mjs"), "utf8");
|
||||
await writeFile(sourcePath, "old\n", "utf8");
|
||||
await writeFile(path.join(root, "build.mjs"), "import { writeFileSync } from 'node:fs'; writeFileSync('firmware.hex', ':00000001FF\\n'); console.log('build completed');\n", "utf8");
|
||||
|
||||
const edited = await runHwpodCli(["workspace", "apply-patch", "--local", "--spec", specPath, "--patch-content", patch]);
|
||||
assert.equal(edited.exitCode, 0);
|
||||
assert.equal(edited.payload.action, "hwpod-cli.local");
|
||||
assert.equal(edited.payload.mode, "l0-native-function");
|
||||
assert.equal(edited.payload.transport, "native-function");
|
||||
assert.equal(await readFile(sourcePath, "utf8"), "new\n");
|
||||
|
||||
const listed = await runHwpodCli(["workspace", "ls", ".", "--local", "--spec", specPath]);
|
||||
assert.equal(listed.payload.body.results[0].output.entries.some((item: any) => item.name === "app.c"), true);
|
||||
|
||||
const searched = await runHwpodCli(["workspace", "rg", "new", ".", "--local", "--spec", specPath]);
|
||||
assert.equal(searched.payload.body.results[0].output.matchCount, 1);
|
||||
|
||||
const written = await runHwpodCli(["workspace", "write", "generated.txt", "--content", "alpha\n", "--local", "--spec", specPath]);
|
||||
assert.equal(written.exitCode, 0);
|
||||
assert.equal(await readFile(path.join(root, "generated.txt"), "utf8"), "alpha\n");
|
||||
|
||||
const replaced = await runHwpodCli(["workspace", "replace", "app.c", "--find", "new", "--replace", "newer", "--local", "--spec", specPath]);
|
||||
assert.equal(replaced.exitCode, 0);
|
||||
const inserted = await runHwpodCli(["workspace", "insert-after", "app.c", "--anchor", "newer", "--line", "marker", "--local", "--spec", specPath]);
|
||||
assert.equal(inserted.exitCode, 0);
|
||||
|
||||
const read = await runHwpodCli(["workspace", "cat", "app.c", "--local", "--spec", specPath]);
|
||||
assert.equal(read.payload.body.results[0].output.content, "newer\nmarker\n");
|
||||
|
||||
const child = Bun.spawn([
|
||||
process.execPath,
|
||||
path.join(process.cwd(), "tools/hwlab-cli/bin/hwlab-cli.ts"),
|
||||
"hwpod",
|
||||
"build",
|
||||
"--local",
|
||||
"--spec",
|
||||
specPath,
|
||||
], { cwd: process.cwd(), stdout: "pipe", stderr: "pipe" });
|
||||
const [stdout, stderr, exitCode] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
|
||||
assert.equal(exitCode, 0, stderr);
|
||||
const built = JSON.parse(stdout);
|
||||
assert.equal(built.action, "hwpod-cli.local");
|
||||
assert.equal(built.mode, "l0-native-function");
|
||||
assert.match(built.body.results[0].output.stdout, /build completed/u);
|
||||
assert.equal(await readFile(artifactPath, "utf8"), ":00000001FF\n");
|
||||
} finally {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("HWPOD L0 local mode fails closed without an explicit spec", async () => {
|
||||
const result = await runHwpodCli(["workspace", "ls", ".", "--local"]);
|
||||
assert.equal(result.exitCode, 1);
|
||||
assert.equal(result.payload.error.code, "hwpod_l0_spec_required");
|
||||
});
|
||||
|
||||
function hwpodSpecText(workspacePath = "F:\\Work\\HWLAB-CASE-F103", buildCommand = "") {
|
||||
return `kind: Hwpod\nmetadata:\n name: test-hwpod\nspec:\n nodeBinding:\n nodeId: test-node\n workspace:\n path: ${JSON.stringify(workspacePath)}\n${buildCommand ? ` buildCommand: ${JSON.stringify(buildCommand)}\n` : ""} targetDevice:\n board: D601-F103-V2\n debugProbe:\n type: daplink\n ioProbe:\n uart:\n port: COM9\n`;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ 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 { executeHwpodNodeOpsPlan } from "./hwpod-node-lib.ts";
|
||||
import { resolveRuntimeEndpoint, runtimeEndpointVisibility } from "./runtime-endpoint-resolver.ts";
|
||||
|
||||
export const DEFAULT_HWPOD_SPEC_PATH = ".hwlab/hwpod-spec.yaml";
|
||||
@@ -13,7 +14,7 @@ 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 BOOLEAN_OPTIONS = new Set(["all", "allowMultiple", "dryRun", "finalNewline", "force", "full", "help", "h", "ignoreCase", "json", "local", "noAuth", "sessionOnly", "wait"]);
|
||||
const DEFAULT_KEIL_COMMAND_TIMEOUT_MS = 30000;
|
||||
type EnvLike = Record<string, string | undefined>;
|
||||
type ParsedArgs = Record<string, unknown> & { _: string[] };
|
||||
@@ -107,11 +108,35 @@ export async function runHwpodCli(argv: string[], options: { env?: EnvLike; fetc
|
||||
if (command === "closeout") return result(0, closeout(parsed), now);
|
||||
|
||||
const { intent, args } = commandToIntent(parsed, options.stdinText);
|
||||
if (parsed.local === true && !text(parsed.spec ?? parsed.specPath)) {
|
||||
throw cliError("hwpod_l0_spec_required", "HWPOD L0 local mode requires an explicit --spec path", {
|
||||
mode: "l0-native-function",
|
||||
next: "pass --local --spec <hwpod-spec.yaml>"
|
||||
});
|
||||
}
|
||||
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);
|
||||
}
|
||||
if (parsed.local === true) {
|
||||
const body = await executeHwpodNodeOpsPlan(plan, { now });
|
||||
const exitCode = body.ok === false ? 1 : 0;
|
||||
const payload = ok("hwpod-cli.local", {
|
||||
mode: "l0-native-function",
|
||||
transport: "native-function",
|
||||
serviceRuntime: false,
|
||||
specPath: compiled.specPath,
|
||||
hwpodId: compiled.hwpodId,
|
||||
specAuthority: compiled.specAuthority,
|
||||
intent: plan.intent,
|
||||
contractVersion: HWPOD_NODE_OPS_CONTRACT_VERSION,
|
||||
compilerInvocation: compiled.compilerInvocation,
|
||||
body
|
||||
}, body.status ?? (exitCode === 0 ? "completed" : "failed"));
|
||||
if (exitCode !== 0) payload.ok = false;
|
||||
return result(exitCode, payload, 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);
|
||||
@@ -203,6 +228,8 @@ function cliHelp() {
|
||||
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",
|
||||
"hwlab-cli hwpod workspace ls . --local --spec .hwlab/hwpod-spec.yaml",
|
||||
"hwlab-cli hwpod build --local --spec .hwlab/hwpod-spec.yaml",
|
||||
"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>",
|
||||
@@ -238,7 +265,8 @@ function cliHelp() {
|
||||
"+new line",
|
||||
"*** End Patch"
|
||||
].join("\n"),
|
||||
route: "POST /v1/hwpod-node-ops"
|
||||
route: "POST /v1/hwpod-node-ops",
|
||||
localMode: "Pass --local --spec <path> to compile and execute the same plan in-process without API, worker, Temporal, Web, or Kubernetes."
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user