feat(cli): scaffold nc CLI with list-groups command
Adds a transport-agnostic CLI control plane shared between three eventual callers (host shell, Claude in project, container agent) — though only the host-side socket transport is wired in this commit. Container DB transport and approval flow land alongside the first risky command. - src/cli/frame.ts: wire format (RequestFrame, ResponseFrame, CallerContext) - src/cli/registry.ts: command registry with RiskClass - src/cli/dispatch.ts: transport-agnostic dispatcher - src/cli/transport.ts: Transport interface - src/cli/socket-client.ts: SocketTransport against data/nc.sock - src/cli/socket-server.ts: host-side listener (chmod 0600, line-delimited JSON) - src/cli/format.ts: human table / --json output modes - src/cli/client.ts: `nc` argv -> frame -> transport -> stdout - src/cli/commands/list-groups.ts: first command (riskClass: safe) - bin/nc: bash launcher (resolves project root via symlink) - src/index.ts: start/stop server + import command barrel `data/nc.sock` is intentionally separate from `data/cli.sock` (which the existing chat-style channel adapter still owns). Verified end-to-end: `nc list-groups`, `nc list groups`, `--json`, unknown-command error, host-down ENOENT message with start instructions. typecheck clean; eslint reports only the same `no-catch-all` warnings the rest of the codebase has. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
36
src/cli/registry.ts
Normal file
36
src/cli/registry.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Command registry — single source of truth for what `nc` can do.
|
||||
*
|
||||
* Each command file under `commands/` calls `register()` at top level,
|
||||
* and `commands/index.ts` imports them all for side effects so the
|
||||
* registry is populated before the host's CLI server accepts connections.
|
||||
*/
|
||||
import type { CallerContext } from './frame.js';
|
||||
|
||||
export type RiskClass = 'safe' | 'requires-admin' | 'requires-owner';
|
||||
|
||||
export type CommandDef<TArgs = unknown, TData = unknown> = {
|
||||
name: string;
|
||||
description: string;
|
||||
riskClass: RiskClass;
|
||||
/** Validates `frame.args` and produces the typed handler input. Throws on invalid. */
|
||||
parseArgs: (raw: Record<string, unknown>) => TArgs;
|
||||
handler: (args: TArgs, ctx: CallerContext) => Promise<TData>;
|
||||
};
|
||||
|
||||
const registry = new Map<string, CommandDef>();
|
||||
|
||||
export function register<TArgs, TData>(def: CommandDef<TArgs, TData>): void {
|
||||
if (registry.has(def.name)) {
|
||||
throw new Error(`CLI command "${def.name}" already registered`);
|
||||
}
|
||||
registry.set(def.name, def as CommandDef);
|
||||
}
|
||||
|
||||
export function lookup(name: string): CommandDef | undefined {
|
||||
return registry.get(name);
|
||||
}
|
||||
|
||||
export function listCommands(): CommandDef[] {
|
||||
return [...registry.values()].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
Reference in New Issue
Block a user