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 in 5b00abf7 was 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 (matches fbf92f9c verbatim):
- 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

What 5b00abf7 still 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 in
5b00abf7).

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 to fbf92f9c shape

Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
mvschwarzandClaude Opus 4.7 committed 2026-05-18 13:37:52 -07:00
1 parent 5b00abf712
commit 97803a8fb3
2 files changed
+204 -2

No files matched your search

+56 -2
View File
@@ -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) {
+148
View File
@@ -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");
});
});