mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-07 05:56:21 -10:00
fix(cli): warn on unrecognized --kind in rig specs commands (#2365)
`rig specs ls`, `show` and `preview` accept any --kind. For a value other than rig, agent or workflow, the command prints one stderr warning naming the supported kinds, still queries the daemon with the value URL-encoded, and keeps only the entries whose kind matches exactly. A known kind trusts the daemon's filtered list as before, so `rig up`'s library lookup is unchanged. When a --kind filter matches nothing, `specs ls` prints "No <kind> specs found" instead of the empty-library message. The warning goes only to stderr, so stdout and --json stay parseable. Command +35/-10, tests +105/-1.
This commit is contained in:
1 parent
22b574983e
commit
085d59248f
2 files changed
+140
-11
No files matched your search
@@ -20,6 +20,14 @@ import { daemonMayAnswer } from "../daemon-reachability.js";
|
||||
import { realDeps } from "./daemon.js";
|
||||
import type { StatusDeps } from "./status.js";
|
||||
|
||||
const KNOWN_SPEC_KINDS = ["rig", "agent", "workflow"] as const;
|
||||
|
||||
function warnIfUnknownKind(kind?: string): void {
|
||||
if (kind !== undefined && !KNOWN_SPEC_KINDS.includes(kind as (typeof KNOWN_SPEC_KINDS)[number])) {
|
||||
console.error(`Warning: unrecognized --kind '${kind}'. Supported kinds: ${KNOWN_SPEC_KINDS.join(", ")}`);
|
||||
}
|
||||
}
|
||||
|
||||
interface LibraryEntry {
|
||||
id: string;
|
||||
kind: "rig" | "agent" | "workflow";
|
||||
@@ -147,10 +155,16 @@ function installSpecSource(source: AddSpecSource, userRoot: string): string {
|
||||
export async function resolveLibrarySpec(
|
||||
client: DaemonClient,
|
||||
nameOrId: string,
|
||||
opts?: { kind?: LibraryEntry["kind"] },
|
||||
opts?: { kind?: string },
|
||||
): Promise<LibraryEntry> {
|
||||
const res = await client.get<LibraryEntry[]>(opts?.kind ? `/api/specs/library?kind=${opts.kind}` : "/api/specs/library");
|
||||
const entries = res.data ?? [];
|
||||
const res = await client.get<LibraryEntry[]>(
|
||||
opts?.kind !== undefined ? `/api/specs/library?kind=${encodeURIComponent(opts.kind)}` : "/api/specs/library",
|
||||
);
|
||||
const rawEntries = res.data ?? [];
|
||||
const entries =
|
||||
opts?.kind !== undefined && !KNOWN_SPEC_KINDS.includes(opts.kind as (typeof KNOWN_SPEC_KINDS)[number])
|
||||
? rawEntries.filter((e) => e.kind === opts.kind)
|
||||
: rawEntries;
|
||||
|
||||
// Try exact ID match first
|
||||
const byId = entries.find((e) => e.id === nameOrId);
|
||||
@@ -219,11 +233,16 @@ Examples:
|
||||
.option("--json", "JSON output")
|
||||
.action(async (opts: { kind?: string; json?: boolean }) => {
|
||||
try {
|
||||
warnIfUnknownKind(opts.kind);
|
||||
const client = await getClient();
|
||||
const url = opts.kind ? `/api/specs/library?kind=${opts.kind}` : "/api/specs/library";
|
||||
const url = opts.kind !== undefined ? `/api/specs/library?kind=${encodeURIComponent(opts.kind)}` : "/api/specs/library";
|
||||
const res = await client.get<LibraryEntry[]>(url);
|
||||
if (res.status !== 200) throw new Error(`Daemon returned HTTP ${res.status}`);
|
||||
const entries = res.data ?? [];
|
||||
const rawEntries = res.data ?? [];
|
||||
const entries =
|
||||
opts.kind !== undefined && !KNOWN_SPEC_KINDS.includes(opts.kind as (typeof KNOWN_SPEC_KINDS)[number])
|
||||
? rawEntries.filter((e) => e.kind === opts.kind)
|
||||
: rawEntries;
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(entries, null, 2));
|
||||
@@ -231,7 +250,11 @@ Examples:
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log("No specs in library. Add specs with: rig specs add <path>");
|
||||
if (opts.kind !== undefined) {
|
||||
console.log(opts.kind ? `No ${opts.kind} specs found` : "No specs found");
|
||||
} else {
|
||||
console.log("No specs in library. Add specs with: rig specs add <path>");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -250,10 +273,11 @@ Examples:
|
||||
.description("Show spec metadata and path")
|
||||
.option("--kind <kind>", "Disambiguate by kind (rig, agent, or workflow)")
|
||||
.option("--json", "JSON output")
|
||||
.action(async (nameOrId: string, opts: { kind?: LibraryEntry["kind"]; json?: boolean }) => {
|
||||
.action(async (nameOrId: string, opts: { kind?: string; json?: boolean }) => {
|
||||
try {
|
||||
warnIfUnknownKind(opts.kind);
|
||||
const client = await getClient();
|
||||
const entry = await resolveLibrarySpec(client, nameOrId, opts.kind ? { kind: opts.kind } : undefined);
|
||||
const entry = await resolveLibrarySpec(client, nameOrId, opts.kind !== undefined ? { kind: opts.kind } : undefined);
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(entry, null, 2));
|
||||
@@ -278,10 +302,11 @@ Examples:
|
||||
.description("Show structured spec review, including managed app details")
|
||||
.option("--kind <kind>", "Disambiguate by kind (rig, agent, or workflow)")
|
||||
.option("--json", "JSON output")
|
||||
.action(async (nameOrId: string, opts: { kind?: LibraryEntry["kind"]; json?: boolean }) => {
|
||||
.action(async (nameOrId: string, opts: { kind?: string; json?: boolean }) => {
|
||||
try {
|
||||
warnIfUnknownKind(opts.kind);
|
||||
const client = await getClient();
|
||||
const entry = await resolveLibrarySpec(client, nameOrId, opts.kind ? { kind: opts.kind } : undefined);
|
||||
const entry = await resolveLibrarySpec(client, nameOrId, opts.kind !== undefined ? { kind: opts.kind } : undefined);
|
||||
const res = await client.get<Record<string, unknown>>(`/api/specs/library/${encodeURIComponent(entry.id)}/review`);
|
||||
const review = res.data;
|
||||
const teamChoice = entry.sourceType === "builtin" && entry.kind === "rig" && ["starter", "factory"].includes(entry.name)
|
||||
|
||||
@@ -19,6 +19,28 @@ function captureLogs(fn: () => Promise<void>): Promise<{ logs: string[]; exitCod
|
||||
});
|
||||
}
|
||||
|
||||
function captureOutputs(fn: () => Promise<void>): Promise<{ stdout: string[]; stderr: string[]; exitCode: number | undefined }> {
|
||||
return new Promise(async (resolve) => {
|
||||
const stdout: string[] = [];
|
||||
const stderr: string[] = [];
|
||||
const origLog = console.log;
|
||||
const origErr = console.error;
|
||||
const origExitCode = process.exitCode;
|
||||
process.exitCode = undefined;
|
||||
console.log = (...args: unknown[]) => stdout.push(args.join(" "));
|
||||
console.error = (...args: unknown[]) => stderr.push(args.join(" "));
|
||||
try {
|
||||
await fn();
|
||||
} finally {
|
||||
console.log = origLog;
|
||||
console.error = origErr;
|
||||
}
|
||||
const exitCode = process.exitCode;
|
||||
process.exitCode = origExitCode;
|
||||
resolve({ stdout, stderr, exitCode });
|
||||
});
|
||||
}
|
||||
|
||||
function runningDeps(port: number): StatusDeps {
|
||||
return { lifecycleDeps: { ...mockLifecycleDeps(), exists: vi.fn((p: string) => p === STATE_FILE), readFile: vi.fn((p: string) => { if (p === STATE_FILE) return JSON.stringify({ pid: 123, port, db: "test.sqlite", startedAt: "2026-04-03T00:00:00Z" } as DaemonState); return null; }), fetch: vi.fn(async () => ({ ok: true })) }, clientFactory: (baseUrl) => new DaemonClient(baseUrl) };
|
||||
}
|
||||
@@ -38,10 +60,13 @@ const RIG_REVIEW = {
|
||||
describe("Specs CLI", () => {
|
||||
let server: http.Server;
|
||||
let port: number;
|
||||
const libraryRequests: string[] = [];
|
||||
|
||||
beforeAll(async () => {
|
||||
server = http.createServer((req, res) => {
|
||||
const url = decodeURIComponent(req.url ?? "");
|
||||
const rawUrl = req.url ?? "";
|
||||
libraryRequests.push(rawUrl);
|
||||
const url = decodeURIComponent(rawUrl);
|
||||
if (url === "/api/specs/library" || url.startsWith("/api/specs/library?")) {
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
const kind = new URL(`http://localhost${url}`).searchParams.get("kind");
|
||||
@@ -112,6 +137,63 @@ describe("Specs CLI", () => {
|
||||
expect(parsed.length).toBe(2);
|
||||
});
|
||||
|
||||
it("specs ls --kind warns on unrecognized kind and still queries daemon", async () => {
|
||||
const requestStart = libraryRequests.length;
|
||||
const { stdout, stderr, exitCode } = await captureOutputs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "ls", "--kind", "invalid"]);
|
||||
});
|
||||
expect(stderr.join("\n")).toContain("Warning: unrecognized --kind 'invalid'. Supported kinds: rig, agent, workflow");
|
||||
expect(stdout.join("\n")).toContain("No invalid specs found");
|
||||
expect(exitCode).toBeUndefined();
|
||||
expect(libraryRequests.slice(requestStart)).toContain("/api/specs/library?kind=invalid");
|
||||
});
|
||||
|
||||
it("specs ls --kind '' warns on empty kind and still queries daemon", async () => {
|
||||
const requestStart = libraryRequests.length;
|
||||
const { stdout, stderr, exitCode } = await captureOutputs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "ls", "--kind", ""]);
|
||||
});
|
||||
expect(stderr.join("\n")).toContain("Warning: unrecognized --kind ''. Supported kinds: rig, agent, workflow");
|
||||
expect(stdout.join("\n")).toContain("No specs found");
|
||||
expect(exitCode).toBeUndefined();
|
||||
expect(libraryRequests.slice(requestStart)).toContain("/api/specs/library?kind=");
|
||||
});
|
||||
|
||||
it("specs ls --kind encodes special characters in daemon request", async () => {
|
||||
const requestStart = libraryRequests.length;
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "ls", "--kind", "rig#draft"]);
|
||||
expect(libraryRequests.slice(requestStart)).toContain("/api/specs/library?kind=rig%23draft");
|
||||
});
|
||||
|
||||
it.each(["rig", "agent", "workflow"])("specs ls --kind %s runs without warning", async (kind) => {
|
||||
const requestStart = libraryRequests.length;
|
||||
const { stderr, exitCode } = await captureOutputs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "ls", "--kind", kind]);
|
||||
});
|
||||
expect(stderr.join("\n")).not.toContain("Warning:");
|
||||
expect(exitCode).toBeUndefined();
|
||||
expect(libraryRequests.slice(requestStart)).toContain(`/api/specs/library?kind=${kind}`);
|
||||
});
|
||||
|
||||
it("specs ls with --kind reports 'No <kind> specs found' when empty", async () => {
|
||||
const { stdout, stderr } = await captureOutputs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "ls", "--kind", "workflow"]);
|
||||
});
|
||||
expect(stdout.join("\n")).toContain("No workflow specs found");
|
||||
expect(stdout.join("\n")).not.toContain("No specs in library");
|
||||
expect(stderr.join("\n")).not.toContain("Warning:");
|
||||
});
|
||||
|
||||
it("specs ls --kind invalid --json emits parseable JSON on stdout and warning on stderr", async () => {
|
||||
const { stdout, stderr } = await captureOutputs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "ls", "--kind", "invalid", "--json"]);
|
||||
});
|
||||
expect(stderr.join("\n")).toContain("Warning: unrecognized --kind 'invalid'. Supported kinds: rig, agent, workflow");
|
||||
const parsed = JSON.parse(stdout.join("\n"));
|
||||
expect(Array.isArray(parsed)).toBe(true);
|
||||
expect(parsed.length).toBe(0);
|
||||
});
|
||||
|
||||
it("specs show resolves by name and prints metadata", async () => {
|
||||
const { logs } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "show", "review-rig"]);
|
||||
@@ -174,6 +256,28 @@ describe("Specs CLI", () => {
|
||||
expect(exitCode).toBeUndefined();
|
||||
});
|
||||
|
||||
it("specs show --kind warns on unrecognized kind and still queries daemon", async () => {
|
||||
const requestStart = libraryRequests.length;
|
||||
const { stderr, exitCode } = await captureOutputs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "show", "review-rig", "--kind", "invalid"]);
|
||||
});
|
||||
expect(stderr.join("\n")).toContain("Warning: unrecognized --kind 'invalid'. Supported kinds: rig, agent, workflow");
|
||||
expect(stderr.join("\n")).toContain("Spec 'review-rig' not found in invalid library");
|
||||
expect(exitCode).toBe(1);
|
||||
expect(libraryRequests.slice(requestStart)).toContain("/api/specs/library?kind=invalid");
|
||||
});
|
||||
|
||||
it("specs preview --kind warns on unrecognized kind and still queries daemon", async () => {
|
||||
const requestStart = libraryRequests.length;
|
||||
const { stderr, exitCode } = await captureOutputs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "preview", "review-rig", "--kind", "invalid"]);
|
||||
});
|
||||
expect(stderr.join("\n")).toContain("Warning: unrecognized --kind 'invalid'. Supported kinds: rig, agent, workflow");
|
||||
expect(stderr.join("\n")).toContain("Spec 'review-rig' not found in invalid library");
|
||||
expect(exitCode).toBe(1);
|
||||
expect(libraryRequests.slice(requestStart)).toContain("/api/specs/library?kind=invalid");
|
||||
});
|
||||
|
||||
it("specs show not-found error points people back to managed apps in the library", async () => {
|
||||
const { logs, exitCode } = await captureLogs(async () => {
|
||||
await makeCmd().parseAsync(["node", "rig", "specs", "show", "missing-app"]);
|
||||
|
||||
Reference in new issue
Block a user