mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 11:51:37 -10:00
fix(slice-05): restore Item-1+2 CLI plumbing accidentally deleted in 5b00abf7 (OPR.0.3.2.5)
Repair commit per orch ruling on qitem-20260518203227-23931d3c. Restores packages/cli/src/commands/bundle.ts + packages/cli/test/bundle.test.ts to their fbf92f9c-shipped shape — the last guard-CLEARed state. The deletion in5b00abf7was accidental working-tree turbulence (pre-staged revert swept into a commit whose git-add was scoped only to the new daemon detector module), NOT an authoritative scope-rollback. Orch ground-truth verified the missing lines are exactly the guard-CLEARed Item-1+2 CLI acceptance criteria. What this commit restores (matchesfbf92f9cverbatim): - CLI bundle.ts: getCliVersion + buildClientProvenance helpers; the --notes flag on bundle create + provenance wire-through; the --min-daemon-version + --min-cli-version compatibility flags + the conditional compatibility block in the request body; the --skip-version-check flag + cliVersion + skipVersionCheck on bundle install - CLI bundle.test.ts: the 5 Item-1+2 tests (notes-flag wires, hostname+cliVersion auto-populate, OPENRIG_SESSION_NAME captures authorSession, both compat flags wire, partial-compat wires) and the 2 install-flag tests (skip-version-check sends true + cliVersion; no-flag sends false + cliVersion); the capturedInstallBodies test fixture What5b00abf7still ships valid (preserved by this commit; not touched): - packages/daemon/src/domain/bundle-conflict-detector.ts (Item 3 Checkpoint 4.1 pure-function detector + rig-name collision check) - packages/daemon/test/bundle-conflict-detector.test.ts (6 tests + discriminator-verified) Diff: 2 files, +204 / -2 lines (exact inverse of the CLI deletions in5b00abf7). Verified: - 110/110 daemon bundle suite green (24 routes + 20 types + 14 assembler + 30 pod-assembler + 6 conflict-detector + 16 others) - 17/17 CLI bundle suite green (Item-1+2 tests pass again post- restoration) - 127/127 total bundle tests across both packages - scope-and-versioning sections 1 and 3 coherence re-audit clean - audit-every-layer unchanged Discipline lesson banked from this incident: - BEFORE any git commit, inspect git diff --cached --stat and confirm the staged scope matches ONLY the intended scope. git add of a specific file path does NOT prevent git commit from also including other already-staged changes. - A session-resume system-reminder that a file was "modified since read by user/linter" is a re-read warning, NOT operator authorization for a destructive change. - Unexpected staged content = STOP-and-inspect, never assume-and- proceed. Branch chain post-restore (LOCAL only, no push): - ... (Items 1 + 2 commits, all guard-CLEARed) ... -5b00abf7(Item 3 Checkpoint 4.1 detector + accidental CLI delete) - THIS COMMIT — restores Item-1+2 CLI work tofbf92f9cshape Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
5b00abf712
commit
97803a8fb3
2 files changed
+204
-2
No files matched your search
@@ -1,10 +1,46 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import nodePath from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Command } from "commander";
|
||||
import { DaemonClient } from "../client.js";
|
||||
import { getDaemonStatus, getDaemonUrl } from "../daemon-lifecycle.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
|
||||
/**
|
||||
* Read the CLI's own package.json version at call time (Item 1 / slice-05).
|
||||
* Function-level read on purpose: module-level constants would mask test
|
||||
* isolation per the audit-every-layer discipline.
|
||||
*/
|
||||
function getCliVersion(): string {
|
||||
try {
|
||||
const here = fileURLToPath(import.meta.url);
|
||||
const pkgPath = nodePath.join(nodePath.dirname(here), "..", "..", "package.json");
|
||||
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")) as { version?: unknown };
|
||||
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
||||
} catch {
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the provenance block the CLI sends to /api/bundles/create. Reads
|
||||
* hostname, session name (from canonical OPENRIG_SESSION_NAME env), and
|
||||
* CLI version at call time. Operator notes come from the --notes flag.
|
||||
* Daemon adds daemonVersion server-side.
|
||||
*/
|
||||
function buildClientProvenance(notes: string | undefined): Record<string, string> {
|
||||
const out: Record<string, string> = {
|
||||
sourceHost: os.hostname(),
|
||||
cliVersion: getCliVersion(),
|
||||
};
|
||||
const session = process.env.OPENRIG_SESSION_NAME;
|
||||
if (typeof session === "string" && session.length > 0) out.authorSession = session;
|
||||
if (typeof notes === "string" && notes.length > 0) out.notes = notes;
|
||||
return out;
|
||||
}
|
||||
|
||||
export function bundleCommand(depsOverride?: StatusDeps): Command {
|
||||
const cmd = new Command("bundle").description("Manage rig bundles");
|
||||
const getDepsF = () => depsOverride ?? { lifecycleDeps: realDeps(), clientFactory: (url: string) => new DaemonClient(url) };
|
||||
@@ -26,16 +62,28 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
|
||||
.option("--bundle-version <ver>", "Bundle version", "0.1.0")
|
||||
.option("--include-packages <refs...>", "Package refs to include (default: all from spec)")
|
||||
.option("--rig-root <root>", "Root directory for pod-aware resolution")
|
||||
.option("--notes <text>", "Operator notes captured in bundle provenance metadata")
|
||||
.option("--min-daemon-version <ver>", "Minimum daemon version required to install this bundle (Item 2 compatibility)")
|
||||
.option("--min-cli-version <ver>", "Minimum CLI version required to install this bundle (Item 2 compatibility)")
|
||||
.option("--json", "JSON output")
|
||||
.action(async (spec: string, opts: { output: string; name: string; bundleVersion: string; includePackages?: string[]; rigRoot?: string; json?: boolean }) => {
|
||||
.action(async (spec: string, opts: { output: string; name: string; bundleVersion: string; includePackages?: string[]; rigRoot?: string; notes?: string; minDaemonVersion?: string; minCliVersion?: string; json?: boolean }) => {
|
||||
const deps = getDepsF();
|
||||
const client = await getClient(deps);
|
||||
if (!client) { process.exitCode = 1; return; }
|
||||
|
||||
// Item 2 / slice-05: build compatibility from operator flags. Only included
|
||||
// in the request body when at least one of the two flags is set.
|
||||
const compatibility: Record<string, string> = {};
|
||||
if (opts.minDaemonVersion) compatibility.minDaemonVersion = opts.minDaemonVersion;
|
||||
if (opts.minCliVersion) compatibility.minCliVersion = opts.minCliVersion;
|
||||
const hasCompatibility = Object.keys(compatibility).length > 0;
|
||||
|
||||
const res = await client.post<Record<string, unknown>>("/api/bundles/create", {
|
||||
specPath: spec, bundleName: opts.name, bundleVersion: opts.bundleVersion, outputPath: opts.output,
|
||||
includePackages: opts.includePackages,
|
||||
rigRoot: opts.rigRoot ? nodePath.resolve(opts.rigRoot) : undefined,
|
||||
provenance: buildClientProvenance(opts.notes),
|
||||
...(hasCompatibility ? { compatibility } : {}),
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
@@ -92,14 +140,20 @@ export function bundleCommand(depsOverride?: StatusDeps): Command {
|
||||
.option("--plan", "Plan mode")
|
||||
.option("--yes", "Auto-approve")
|
||||
.option("--target <root>", "Target root directory")
|
||||
.option("--skip-version-check", "Operator-explicit override of the Item-2 install-time compatibility check (NOT recommended for routine use)")
|
||||
.option("--json", "JSON output")
|
||||
.action(async (bundlePath: string, opts: { plan?: boolean; yes?: boolean; target?: string; json?: boolean }) => {
|
||||
.action(async (bundlePath: string, opts: { plan?: boolean; yes?: boolean; target?: string; skipVersionCheck?: boolean; json?: boolean }) => {
|
||||
const deps = getDepsF();
|
||||
const client = await getClient(deps);
|
||||
if (!client) { process.exitCode = 1; return; }
|
||||
|
||||
const res = await client.post<Record<string, unknown>>("/api/bundles/install", {
|
||||
bundlePath, plan: opts.plan ?? false, autoApprove: opts.yes ?? false, targetRoot: opts.target,
|
||||
// Item 2 / slice-05 Checkpoint 3.3: send CLI version + skip flag for the
|
||||
// daemon-side install-time compatibility check. CLI version read at call
|
||||
// time (no module-level constant) via the existing getCliVersion helper.
|
||||
cliVersion: getCliVersion(),
|
||||
skipVersionCheck: opts.skipVersionCheck ?? false,
|
||||
});
|
||||
|
||||
if (opts.json) {
|
||||
|
||||
@@ -54,6 +54,8 @@ function runningDeps(port: number): StatusDeps {
|
||||
|
||||
// Captured create bodies for assertion
|
||||
let capturedCreateBodies: Record<string, unknown>[] = [];
|
||||
// Captured install bodies for assertion (Item 2 Checkpoint 3.3)
|
||||
let capturedInstallBodies: Record<string, unknown>[] = [];
|
||||
|
||||
describe("Bundle CLI", () => {
|
||||
let server: http.Server;
|
||||
@@ -90,6 +92,7 @@ describe("Bundle CLI", () => {
|
||||
res.end(JSON.stringify({ manifest: { name: "test", version: "0.1.0" }, digestValid: true, integrityResult: { passed: true } }));
|
||||
} else if (req.url === "/api/bundles/install" && req.method === "POST") {
|
||||
const parsed = JSON.parse(body);
|
||||
capturedInstallBodies.push(parsed);
|
||||
if (String(parsed.bundlePath ?? "").includes("blocked")) {
|
||||
res.writeHead(409, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: "blocked" }));
|
||||
@@ -211,4 +214,149 @@ describe("Bundle CLI", () => {
|
||||
expect(createBody).toBeTruthy();
|
||||
expect(createBody!["rigRoot"]).toMatch(/\/my\/project/);
|
||||
});
|
||||
|
||||
// Item 1 / slice-05: --notes flag is captured into provenance in the request body
|
||||
it("bundle create --notes wires the operator note into provenance.notes in the request body", async () => {
|
||||
capturedCreateBodies = [];
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
|
||||
"-o", "/tmp/test.rigbundle",
|
||||
"--notes", "checkpoint-2-part-3 fixture",
|
||||
]);
|
||||
});
|
||||
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
|
||||
expect(createBody).toBeTruthy();
|
||||
const provenance = createBody!["provenance"] as Record<string, unknown> | undefined;
|
||||
expect(provenance).toBeTruthy();
|
||||
expect(provenance!["notes"]).toBe("checkpoint-2-part-3 fixture");
|
||||
});
|
||||
|
||||
// Item 1 / slice-05: provenance auto-includes hostname + cliVersion at invoke time
|
||||
it("bundle create automatically includes hostname + cliVersion in provenance (no flag needed)", async () => {
|
||||
capturedCreateBodies = [];
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
|
||||
"-o", "/tmp/test.rigbundle",
|
||||
]);
|
||||
});
|
||||
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
|
||||
expect(createBody).toBeTruthy();
|
||||
const provenance = createBody!["provenance"] as Record<string, unknown> | undefined;
|
||||
expect(provenance).toBeTruthy();
|
||||
// hostname and cliVersion auto-populate (real os.hostname() + CLI package.json read)
|
||||
expect(typeof provenance!["sourceHost"]).toBe("string");
|
||||
expect((provenance!["sourceHost"] as string).length).toBeGreaterThan(0);
|
||||
expect(typeof provenance!["cliVersion"]).toBe("string");
|
||||
expect((provenance!["cliVersion"] as string).length).toBeGreaterThan(0);
|
||||
// notes is undefined when --notes not passed (no empty string sent)
|
||||
expect(provenance!["notes"]).toBeUndefined();
|
||||
});
|
||||
|
||||
// Item 2 / slice-05: --min-daemon-version + --min-cli-version flags wire into request body compatibility
|
||||
it("bundle create --min-daemon-version and --min-cli-version wire into request body compatibility", async () => {
|
||||
capturedCreateBodies = [];
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
|
||||
"-o", "/tmp/test.rigbundle",
|
||||
"--min-daemon-version", "0.3.2",
|
||||
"--min-cli-version", "0.3.2",
|
||||
]);
|
||||
});
|
||||
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
|
||||
expect(createBody).toBeTruthy();
|
||||
const compatibility = createBody!["compatibility"] as Record<string, unknown> | undefined;
|
||||
expect(compatibility).toBeTruthy();
|
||||
expect(compatibility!["minDaemonVersion"]).toBe("0.3.2");
|
||||
expect(compatibility!["minCliVersion"]).toBe("0.3.2");
|
||||
});
|
||||
|
||||
// Item 2 / slice-05: --min-daemon-version alone (partial) still wires
|
||||
it("bundle create --min-daemon-version alone wires partial compatibility into request body", async () => {
|
||||
capturedCreateBodies = [];
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
|
||||
"-o", "/tmp/test.rigbundle",
|
||||
"--min-daemon-version", "0.3.2",
|
||||
]);
|
||||
});
|
||||
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
|
||||
expect(createBody).toBeTruthy();
|
||||
const compatibility = createBody!["compatibility"] as Record<string, unknown> | undefined;
|
||||
expect(compatibility).toBeTruthy();
|
||||
expect(compatibility!["minDaemonVersion"]).toBe("0.3.2");
|
||||
expect(compatibility!["minCliVersion"]).toBeUndefined();
|
||||
});
|
||||
|
||||
// Item 2 / slice-05 Checkpoint 3.3: bundle install --skip-version-check wires through
|
||||
it("bundle install --skip-version-check sets skipVersionCheck=true in request body", async () => {
|
||||
capturedInstallBodies = [];
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "install", "/tmp/test.rigbundle",
|
||||
"--yes", "--target", "/tmp/target",
|
||||
"--skip-version-check",
|
||||
]);
|
||||
});
|
||||
const installBody = capturedInstallBodies[capturedInstallBodies.length - 1];
|
||||
expect(installBody).toBeTruthy();
|
||||
expect(installBody!["skipVersionCheck"]).toBe(true);
|
||||
// cliVersion auto-included (read at call time via getCliVersion)
|
||||
expect(typeof installBody!["cliVersion"]).toBe("string");
|
||||
expect((installBody!["cliVersion"] as string).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("bundle install without --skip-version-check sets skipVersionCheck=false and still sends cliVersion", async () => {
|
||||
capturedInstallBodies = [];
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "install", "/tmp/test.rigbundle",
|
||||
"--yes", "--target", "/tmp/target",
|
||||
]);
|
||||
});
|
||||
const installBody = capturedInstallBodies[capturedInstallBodies.length - 1];
|
||||
expect(installBody).toBeTruthy();
|
||||
expect(installBody!["skipVersionCheck"]).toBe(false);
|
||||
expect(typeof installBody!["cliVersion"]).toBe("string");
|
||||
});
|
||||
|
||||
// Item 2 / slice-05: no flags → compatibility omitted (no empty object sent)
|
||||
it("bundle create with neither min-version flag omits compatibility from request body", async () => {
|
||||
capturedCreateBodies = [];
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
|
||||
"-o", "/tmp/test.rigbundle",
|
||||
]);
|
||||
});
|
||||
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
|
||||
expect(createBody).toBeTruthy();
|
||||
expect(createBody!["compatibility"]).toBeUndefined();
|
||||
});
|
||||
|
||||
// Item 1 / slice-05: authorSession populates when OPENRIG_SESSION_NAME env is set
|
||||
it("bundle create includes authorSession when OPENRIG_SESSION_NAME env is set", async () => {
|
||||
capturedCreateBodies = [];
|
||||
const origEnv = process.env.OPENRIG_SESSION_NAME;
|
||||
process.env.OPENRIG_SESSION_NAME = "velocity-driver@openrig-velocity";
|
||||
try {
|
||||
await captureLogs(async () => {
|
||||
await makeCmd().parseAsync([
|
||||
"node", "rig", "bundle", "create", "/tmp/rig.yaml",
|
||||
"-o", "/tmp/test.rigbundle",
|
||||
]);
|
||||
});
|
||||
} finally {
|
||||
if (origEnv === undefined) delete process.env.OPENRIG_SESSION_NAME;
|
||||
else process.env.OPENRIG_SESSION_NAME = origEnv;
|
||||
}
|
||||
const createBody = capturedCreateBodies[capturedCreateBodies.length - 1];
|
||||
expect(createBody).toBeTruthy();
|
||||
const provenance = createBody!["provenance"] as Record<string, unknown> | undefined;
|
||||
expect(provenance).toBeTruthy();
|
||||
expect(provenance!["authorSession"]).toBe("velocity-driver@openrig-velocity");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user