import { z } from "zod"; import { CLIHelpError, formatCLIHelp, parseCLIArgs } from "./cli"; import type { HandlerDef } from "./handler"; import { isGroupDef } from "./handler"; import { serveHTTP } from "./http"; import { serveIPC } from "./ipc"; import { serveMCP } from "./mcp"; import { generateManifest } from "./manifest"; function isHandlerDef(value: unknown): value is HandlerDef { if (!value || typeof value !== "object") { return false; } const candidate = value as Partial; return ( candidate.input instanceof z.ZodType && candidate.output instanceof z.ZodType && typeof candidate.fn === "function" ); } export abstract class Clip { abstract domain: string; abstract patterns: string[]; dependencies: Record = {}; entities: Record> = {}; /** * How long (ms) the process stays alive after the last invoke completes. * - `30_000` (default): exit after 30s idle * - `0`: exit immediately after each invoke * - `Infinity`: never exit (persistent) */ idleTimeout: number = 30_000; protected readonly commands = new Map(); protected readonly commandDescriptions = new Map(); protected readonly commandGroups = new Map(); async start(): Promise { const args = process.argv.slice(2); const [modeOrCommand, ...restArgs] = args; if (!modeOrCommand || modeOrCommand === "--help") { console.log(this.printHelp()); return; } if (modeOrCommand === "--mcp") { return serveMCP(this); } if (modeOrCommand === "--ipc") { return serveIPC(this); } if (modeOrCommand === "--web") { const portArg = restArgs[0]; if (portArg === undefined) { return serveHTTP(this); } const port = Number(portArg); if (Number.isNaN(port) || !Number.isFinite(port) || port !== Math.floor(port) || port < 0) { console.error(`Invalid port: ${portArg}`); return; } return serveHTTP(this, port); } if (modeOrCommand === "--manifest") { console.log(this.toManifest()); return; } // Resolve command: collect non-flag tokens as command path, greedy match longest const { commandName, commandArgs } = this.resolveCommand(args); if (!commandName) { // Check if first token is a group name → show group help if (this.commandGroups.has(modeOrCommand)) { console.log(this.printGroupHelp(modeOrCommand)); return; } console.error(`Unknown command: ${modeOrCommand}`); console.log(this.printHelp()); return; } const commandHandler = this.commands.get(commandName)!; if (!(commandHandler.input instanceof z.ZodObject)) { console.error(`CLI mode only supports object input schemas for command: ${commandName}`); return; } try { const input = parseCLIArgs(commandArgs, commandHandler.input); const output = await commandHandler.fn(input, ""); let parsedOutput: unknown; try { parsedOutput = commandHandler.output.parse(output); } catch { parsedOutput = output; } console.log(JSON.stringify(parsedOutput)); } catch (error) { if (error instanceof CLIHelpError) { console.log(this.printCommandHelp(commandName, commandHandler)); return; } console.error(error instanceof Error ? error.message : String(error)); } } private resolveCommand(args: string[]): { commandName: string | null; commandArgs: string[] } { // Collect non-flag tokens as potential command path const cmdParts: string[] = []; let flagStart = args.length; for (let i = 0; i < args.length; i++) { const arg = args[i]!; if (arg.startsWith("-")) { flagStart = i; break; } cmdParts.push(arg); } const remainingArgs = args.slice(flagStart); // Greedy: try longest match first for (let len = cmdParts.length; len > 0; len--) { const candidate = cmdParts.slice(0, len).join(" "); if (this.commands.has(candidate)) { // Unused command tokens become part of args (shouldn't happen with one-level nesting) const extra = cmdParts.slice(len); return { commandName: candidate, commandArgs: [...extra, ...remainingArgs] }; } } // Check if it's "group --help" if (cmdParts.length > 0 && this.commandGroups.has(cmdParts[0]!) && remainingArgs.includes("--help")) { return { commandName: null, commandArgs: [] }; } return { commandName: null, commandArgs: [] }; } toManifest(): string { return generateManifest(this); } printHelp(): string { const name = getClipName(this); const lines: string[] = [ name ? `${name} (${this.domain})` : this.domain, "", ]; if (this.patterns.length > 0) { lines.push("Patterns:"); for (const pattern of this.patterns) { lines.push(` ${pattern}`); } lines.push(""); } lines.push("Usage:"); lines.push(" bun run