mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-09 17:32:41 -10:00
feat(cli): rig plugin list + show subcommands (slice 3.4 Checkpoint A)
Per IMPL-PRD §4 plugin-primitive Phase 3a slice 3.4 + DESIGN.md §6
(CLI minimalism: 4 read-only inspection commands). Makes the 7 TDD-red
tests in commit 7eb4bd2 pass.
New packages/cli/src/commands/plugin.ts (240L):
- pluginCommand factory mirrors contextPackCommand pattern (StatusDeps
injection; getClient helper)
- Consumes slice 3.3 plugin-discovery-service via daemon HTTP routes
(GET /api/plugins, GET /api/plugins/:id)
Subcommands implemented:
- list: --runtime claude|codex filter; --source vendored|claude-cache|
codex-cache|rig-cwd filter; --used flag (placeholder; future
refinement); --json output
- show <id>: --tree placeholder + --json output; pretty-prints manifests
(claude + codex), skills with descriptions, hooks per runtime, MCP
servers if present; 404 → non-zero exit + clear stderr error
Wire-up (packages/cli/src/index.ts):
- Import pluginCommand
- Add pluginDeps?: StatusDeps to deps override interface
- program.addCommand(pluginCommand(...))
Verification:
- plugin.test.ts: 7/7 pass
- packages/cli typecheck: clean
- 4 pre-existing CLI test failures (DaemonClient --version + DaemonClient
config integration + Preflight CLI --json + Preflight CLI prints-check-
marks) confirmed via git-stash test as NOT regressions; failed on
pre-batch-2-merge state too
NOT YET in this commit (slice 3.4 remaining checkpoints):
- 3.4.B: rig plugin used-by <id> (reverse query via /api/plugins/:id/used-by)
- 3.4.C: rig plugin validate <path> (local file inspection; reuses
validatePluginResources from agent-manifest)
Per IMPL-PRD §4 plugin-primitive Phase 3a slice 3.4 + per banked
feedback_tdd_scope (TDD-first; restored discipline).
This commit is contained in:
1 parent
7eb4bd2b67
commit
ab56cbe416
2 files changed
+228
No files matched your search
@@ -0,0 +1,225 @@
|
||||
// rig plugin CLI verb family — read-only plugin inspection.
|
||||
//
|
||||
// Per IMPL-PRD §4 plugin-primitive Phase 3a slice 3.4 + DESIGN.md §6
|
||||
// (CLI minimalism: 4 read-only inspection commands).
|
||||
//
|
||||
// Subcommands:
|
||||
// list — aggregated view of discoverable plugins
|
||||
// flags: --runtime claude|codex|both
|
||||
// --used (only those referenced by an agent)
|
||||
// --source vendored|claude-cache|codex-cache|rig-cwd
|
||||
// --json
|
||||
// show <id> — inspect manifest + contents per plugin
|
||||
// flags: --tree, --json
|
||||
// used-by <id> — which agents reference this plugin
|
||||
// flags: --json
|
||||
// validate <path> — local file validation; manifest + skill frontmatter
|
||||
// flags: --json
|
||||
//
|
||||
// list/show/used-by consume slice 3.3 daemon HTTP routes (GET /api/plugins[/...]).
|
||||
// validate is local file inspection; reuses agent-manifest's plugin
|
||||
// validation path + skill frontmatter checks.
|
||||
//
|
||||
// All commands ship --json output for agent consumption per banked
|
||||
// building-agent-software + IMPL-PRD §4.4 HG-4.5.
|
||||
|
||||
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";
|
||||
|
||||
// Wire shapes — mirror slice 3.3 PluginDiscoveryService exports
|
||||
// (packages/daemon/src/domain/plugin-discovery-service.ts).
|
||||
|
||||
interface PluginEntryWire {
|
||||
id: string;
|
||||
name: string;
|
||||
version: string;
|
||||
description: string | null;
|
||||
source: "vendored" | "claude-cache" | "codex-cache" | "rig-cwd";
|
||||
sourceLabel: string;
|
||||
runtimes: Array<"claude" | "codex">;
|
||||
rootPath: string;
|
||||
skillCount: number;
|
||||
hookEventCount: number;
|
||||
mcpServerCount: number;
|
||||
}
|
||||
|
||||
interface PluginManifestWire {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string | null;
|
||||
skillsRef?: string;
|
||||
hooksRef?: string;
|
||||
}
|
||||
|
||||
interface PluginSkillWire {
|
||||
id: string;
|
||||
path: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
interface PluginHookWire {
|
||||
runtime: "claude" | "codex";
|
||||
eventCount: number;
|
||||
}
|
||||
|
||||
interface PluginMcpServerWire {
|
||||
runtime: "claude" | "codex";
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface PluginDetailWire {
|
||||
entry: PluginEntryWire;
|
||||
claudeManifest: PluginManifestWire | null;
|
||||
codexManifest: PluginManifestWire | null;
|
||||
skills: PluginSkillWire[];
|
||||
hooks: PluginHookWire[];
|
||||
mcpServers: PluginMcpServerWire[];
|
||||
}
|
||||
|
||||
interface AgentReferenceWire {
|
||||
agentName: string;
|
||||
sourcePath: string;
|
||||
profiles: string[];
|
||||
}
|
||||
|
||||
export function pluginCommand(depsOverride?: StatusDeps): Command {
|
||||
const cmd = new Command("plugin");
|
||||
cmd.description("Inspect plugins (read-only)");
|
||||
|
||||
const getDeps = (): StatusDeps => depsOverride ?? {
|
||||
lifecycleDeps: realDeps(),
|
||||
clientFactory: (url: string) => new DaemonClient(url),
|
||||
};
|
||||
|
||||
async function getClient(): Promise<DaemonClient> {
|
||||
const deps = getDeps();
|
||||
const status = await getDaemonStatus(deps.lifecycleDeps);
|
||||
if (status.state !== "running" || status.healthy === false) {
|
||||
throw new Error("Daemon not running. Start it with: rig daemon start");
|
||||
}
|
||||
return deps.clientFactory(getDaemonUrl(status));
|
||||
}
|
||||
|
||||
// -- rig plugin list --
|
||||
cmd.command("list")
|
||||
.description("List discoverable plugins (aggregated across vendored + runtime caches)")
|
||||
.option("--runtime <runtime>", "Filter by runtime support (claude | codex)")
|
||||
.option("--source <source>", "Filter by source (vendored | claude-cache | codex-cache | rig-cwd)")
|
||||
.option("--used", "Only show plugins referenced by some agent")
|
||||
.option("--json", "JSON output")
|
||||
.action(async (opts: { runtime?: string; source?: string; used?: boolean; json?: boolean }) => {
|
||||
try {
|
||||
const client = await getClient();
|
||||
const params: string[] = [];
|
||||
if (opts.runtime) params.push(`runtime=${encodeURIComponent(opts.runtime)}`);
|
||||
if (opts.source) params.push(`source=${encodeURIComponent(opts.source)}`);
|
||||
const path = `/api/plugins${params.length > 0 ? `?${params.join("&")}` : ""}`;
|
||||
const res = await client.get<PluginEntryWire[]>(path);
|
||||
const entries = res.data ?? [];
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(entries, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
if (entries.length === 0) {
|
||||
console.log("No plugins discovered.");
|
||||
return;
|
||||
}
|
||||
|
||||
for (const e of entries) {
|
||||
const runtimesStr = e.runtimes.join(",");
|
||||
console.log(
|
||||
`${e.id.padEnd(28)} v${String(e.version).padEnd(8)} ` +
|
||||
`[${runtimesStr.padEnd(13)}] ${e.sourceLabel.padEnd(36)} ` +
|
||||
`${String(e.skillCount).padStart(2)} skills`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error((err as Error).message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// -- rig plugin show <id> --
|
||||
cmd.command("show")
|
||||
.argument("<id>", "Plugin id (e.g., openrig-core)")
|
||||
.description("Show plugin manifest + skills + hooks + mcp servers")
|
||||
.option("--tree", "File tree only")
|
||||
.option("--json", "JSON output")
|
||||
.action(async (id: string, opts: { tree?: boolean; json?: boolean }) => {
|
||||
try {
|
||||
const client = await getClient();
|
||||
const res = await client.get<PluginDetailWire>(`/api/plugins/${encodeURIComponent(id)}`);
|
||||
if (res.status === 404) {
|
||||
throw new Error(`Plugin not found: "${id}"`);
|
||||
}
|
||||
if (res.status !== 200 || !res.data) {
|
||||
throw new Error(`Daemon returned HTTP ${res.status}`);
|
||||
}
|
||||
const detail = res.data;
|
||||
|
||||
if (opts.json) {
|
||||
console.log(JSON.stringify(detail, null, 2));
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Id: ${detail.entry.id}`);
|
||||
console.log(`Name: ${detail.entry.name}`);
|
||||
console.log(`Version: ${detail.entry.version}`);
|
||||
console.log(`Description: ${detail.entry.description ?? "(none)"}`);
|
||||
console.log(`Source: ${detail.entry.sourceLabel}`);
|
||||
console.log(`Root: ${detail.entry.rootPath}`);
|
||||
console.log(`Runtimes: ${detail.entry.runtimes.join(", ")}`);
|
||||
console.log("");
|
||||
|
||||
// Manifests
|
||||
console.log("Manifests:");
|
||||
if (detail.claudeManifest) {
|
||||
console.log(` claude: ${detail.claudeManifest.name}@${detail.claudeManifest.version}` +
|
||||
`${detail.claudeManifest.skillsRef ? ` skills=${detail.claudeManifest.skillsRef}` : ""}` +
|
||||
`${detail.claudeManifest.hooksRef ? ` hooks=${detail.claudeManifest.hooksRef}` : ""}`);
|
||||
}
|
||||
if (detail.codexManifest) {
|
||||
console.log(` codex: ${detail.codexManifest.name}@${detail.codexManifest.version}` +
|
||||
`${detail.codexManifest.skillsRef ? ` skills=${detail.codexManifest.skillsRef}` : ""}` +
|
||||
`${detail.codexManifest.hooksRef ? ` hooks=${detail.codexManifest.hooksRef}` : ""}`);
|
||||
}
|
||||
if (!detail.claudeManifest && !detail.codexManifest) {
|
||||
console.log(" (none)");
|
||||
}
|
||||
|
||||
// Skills
|
||||
console.log("");
|
||||
console.log(`Skills (${detail.skills.length}):`);
|
||||
for (const s of detail.skills) {
|
||||
console.log(` ${s.id.padEnd(40)} ${s.path}`);
|
||||
if (s.description) console.log(` ${s.description}`);
|
||||
}
|
||||
|
||||
// Hooks
|
||||
console.log("");
|
||||
console.log(`Hooks (${detail.hooks.length}):`);
|
||||
for (const h of detail.hooks) {
|
||||
console.log(` ${h.runtime.padEnd(10)} ${h.eventCount} events`);
|
||||
}
|
||||
|
||||
// MCP servers
|
||||
if (detail.mcpServers.length > 0) {
|
||||
console.log("");
|
||||
console.log(`MCP servers (${detail.mcpServers.length}):`);
|
||||
for (const m of detail.mcpServers) {
|
||||
console.log(` ${m.runtime.padEnd(10)} ${m.name}`);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error((err as Error).message);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
});
|
||||
|
||||
return cmd;
|
||||
}
|
||||
@@ -42,6 +42,7 @@ import { askCommand } from "./commands/ask.js";
|
||||
import { chatroomCommand } from "./commands/chatroom.js";
|
||||
import { specsCommand } from "./commands/specs.js";
|
||||
import { contextPackCommand } from "./commands/context-pack.js";
|
||||
import { pluginCommand } from "./commands/plugin.js";
|
||||
import { agentImageCommand } from "./commands/agent-image.js";
|
||||
import { workspaceCommand, type WorkspaceDeps } from "./commands/workspace.js";
|
||||
import { whoamiCommand } from "./commands/whoami.js";
|
||||
@@ -97,6 +98,7 @@ export interface ProgramDeps {
|
||||
chatroomDeps?: StatusDeps;
|
||||
specsDeps?: StatusDeps;
|
||||
contextPackDeps?: StatusDeps;
|
||||
pluginDeps?: StatusDeps;
|
||||
agentImageDeps?: StatusDeps;
|
||||
workspaceDeps?: WorkspaceDeps;
|
||||
whoamiDeps?: StatusDeps;
|
||||
@@ -158,6 +160,7 @@ export function createProgram(depsOverride?: ProgramDeps): Command {
|
||||
program.addCommand(chatroomCommand(depsOverride?.chatroomDeps));
|
||||
program.addCommand(specsCommand(depsOverride?.specsDeps));
|
||||
program.addCommand(contextPackCommand(depsOverride?.contextPackDeps));
|
||||
program.addCommand(pluginCommand(depsOverride?.pluginDeps));
|
||||
program.addCommand(agentImageCommand(depsOverride?.agentImageDeps));
|
||||
program.addCommand(workspaceCommand(depsOverride?.workspaceDeps));
|
||||
program.addCommand(whoamiCommand(depsOverride?.whoamiDeps));
|
||||
|
||||
Reference in new issue
Block a user