padrone 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/README.md +2 -1
- package/dist/{args-Cnq0nwSM.mjs → args-DrCXxXeP.mjs} +20 -4
- package/dist/args-DrCXxXeP.mjs.map +1 -0
- package/dist/codegen/index.mjs +1 -1
- package/dist/{commands-B_gufyR9.mjs → commands-DLR0rFgq.mjs} +2 -2
- package/dist/{commands-B_gufyR9.mjs.map → commands-DLR0rFgq.mjs.map} +1 -1
- package/dist/{completion-BEuflbDO.mjs → completion-UnBKfGuk.mjs} +2 -2
- package/dist/{completion-BEuflbDO.mjs.map → completion-UnBKfGuk.mjs.map} +1 -1
- package/dist/docs/index.d.mts +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{formatter-DrvhDMrq.d.mts → formatter-CY3KrOEd.d.mts} +3 -2
- package/dist/formatter-CY3KrOEd.d.mts.map +1 -0
- package/dist/{help-BtxLgrF_.mjs → help-B-ZMYyn-.mjs} +16 -6
- package/dist/help-B-ZMYyn-.mjs.map +1 -0
- package/dist/{index-C0Tab27T.d.mts → index-C2n3k4e8.d.mts} +328 -115
- package/dist/index-C2n3k4e8.d.mts.map +1 -0
- package/dist/index.d.mts +17 -160
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +78 -65
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-6-Jw4Bpq.mjs → mcp-D6PdtjIs.mjs} +4 -4
- package/dist/{mcp-6-Jw4Bpq.mjs.map → mcp-D6PdtjIs.mjs.map} +1 -1
- package/dist/{serve-YVTPzBCl.mjs → serve-PaCLsNoD.mjs} +4 -4
- package/dist/{serve-YVTPzBCl.mjs.map → serve-PaCLsNoD.mjs.map} +1 -1
- package/dist/test.d.mts +1 -1
- package/dist/zod.d.mts +1 -1
- package/package.json +3 -3
- package/src/core/args.ts +24 -1
- package/src/core/create.ts +21 -14
- package/src/core/exec.ts +4 -3
- package/src/core/interceptors.ts +12 -6
- package/src/core/program-methods.ts +12 -2
- package/src/core/validate.ts +26 -7
- package/src/extension/auto-output.ts +1 -1
- package/src/extension/config.ts +2 -1
- package/src/extension/env.ts +5 -4
- package/src/extension/index.ts +1 -0
- package/src/extension/interactive.ts +2 -1
- package/src/extension/logger.ts +1 -1
- package/src/extension/progress.ts +2 -2
- package/src/extension/tracing.ts +1 -1
- package/src/index.ts +5 -1
- package/src/output/formatter.ts +6 -1
- package/src/output/help.ts +15 -3
- package/src/types/args-meta.ts +10 -0
- package/src/types/builder.ts +75 -2
- package/src/types/index.ts +2 -0
- package/src/types/interceptor.ts +12 -12
- package/src/util/type-utils.ts +22 -0
- package/dist/args-Cnq0nwSM.mjs.map +0 -1
- package/dist/formatter-DrvhDMrq.d.mts.map +0 -1
- package/dist/help-BtxLgrF_.mjs.map +0 -1
- package/dist/index-C0Tab27T.d.mts.map +0 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { m as serializeArgsToFlags, r as collectEndpoints, t as buildInputSchema } from "./commands-
|
|
1
|
+
import { m as serializeArgsToFlags, r as collectEndpoints, t as buildInputSchema } from "./commands-DLR0rFgq.mjs";
|
|
2
2
|
import { a as readStreamAsText } from "./stream-DC4H8YTx.mjs";
|
|
3
|
-
import { t as generateHelp } from "./help-
|
|
3
|
+
import { t as generateHelp } from "./help-B-ZMYyn-.mjs";
|
|
4
4
|
//#region src/feature/mcp.ts
|
|
5
5
|
const PROTOCOL_VERSION = "2025-11-25";
|
|
6
6
|
/** Convert an endpoint dot-path to a valid MCP tool name. Spec allows: [A-Za-z0-9_\-\.] */
|
|
@@ -367,11 +367,11 @@ async function startHttpTransport(handleRequest, prefs, log, onSignal) {
|
|
|
367
367
|
async function startMcpServer(_program, existingCommand, evalCommand, prefs) {
|
|
368
368
|
const handleRequest = createMcpHandler(existingCommand, evalCommand, prefs);
|
|
369
369
|
if ((prefs?.transport ?? "http") === "stdio") return startStdioTransport(handleRequest);
|
|
370
|
-
const { getCommandRuntime } = await import("./commands-
|
|
370
|
+
const { getCommandRuntime } = await import("./commands-DLR0rFgq.mjs").then((n) => n.a);
|
|
371
371
|
const runtime = getCommandRuntime(existingCommand);
|
|
372
372
|
return startHttpTransport(handleRequest, prefs ?? {}, (msg) => runtime.error(msg), runtime.onSignal);
|
|
373
373
|
}
|
|
374
374
|
//#endregion
|
|
375
375
|
export { startMcpServer };
|
|
376
376
|
|
|
377
|
-
//# sourceMappingURL=mcp-
|
|
377
|
+
//# sourceMappingURL=mcp-D6PdtjIs.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mcp-6-Jw4Bpq.mjs","names":[],"sources":["../src/feature/mcp.ts"],"sourcesContent":["import { buildInputSchema, collectEndpoints, serializeArgsToFlags } from '../core/commands.ts';\nimport { generateHelp } from '../output/help.ts';\nimport type { AnyPadroneCommand, AnyPadroneProgram } from '../types/index.ts';\nimport { readStreamAsText } from '../util/stream.ts';\n\nexport type PadroneMcpPreferences = {\n /** Server name. Defaults to the program name. */\n name?: string;\n /** Server version. Defaults to the program version. */\n version?: string;\n /**\n * Transport mode.\n * - `'http'` — Start a Streamable HTTP server (default). Responds with `application/json` or `text/event-stream` based on the client's `Accept` header. Use `port` and `host` to configure.\n * - `'stdio'` — Communicate over stdin/stdout with newline-delimited JSON.\n */\n transport?: 'http' | 'stdio';\n /** HTTP port. Defaults to `3000`. Only used with `transport: 'http'`. */\n port?: number;\n /** HTTP host. Defaults to `'127.0.0.1'`. Only used with `transport: 'http'`. */\n host?: string;\n /** Base path for the MCP endpoint. Defaults to `'/mcp'`. Only used with `transport: 'http'`. */\n basePath?: string;\n /** CORS allowed origin. Defaults to `'*'`. Set to a specific origin or `false` to disable CORS headers. Only used with HTTP transports. */\n cors?: string | false;\n};\n\nconst PROTOCOL_VERSION = '2025-11-25';\n\ntype JsonRpcRequest = {\n jsonrpc: '2.0';\n id?: string | number;\n method: string;\n params?: Record<string, unknown>;\n};\n\ntype JsonRpcResponse = {\n jsonrpc: '2.0';\n id: string | number | null;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n};\n\n/** Convert an endpoint dot-path to a valid MCP tool name. Spec allows: [A-Za-z0-9_\\-\\.] */\nfunction toToolName(path: string): string {\n return path.replace(/\\s+/g, '.');\n}\n\n/** Convert a tool name back to a command path (dot → space). */\nfunction toCommandPath(toolName: string): string {\n return toolName.replace(/\\./g, ' ');\n}\n\n/** Build MCP tool annotations from a command's metadata. */\nfunction buildAnnotations(cmd: AnyPadroneCommand) {\n if (cmd.mutation == null) return undefined;\n return {\n destructiveHint: cmd.mutation || undefined,\n readOnlyHint: cmd.mutation === false || undefined,\n };\n}\n\n/** Build an MCP tool definition from a command. */\nfunction buildToolDefinition(name: string, cmd: AnyPadroneCommand) {\n return {\n name: toToolName(name),\n title: cmd.title ?? undefined,\n description: cmd.description || cmd.title || `Run the \"${name}\" command`,\n inputSchema: buildInputSchema(cmd),\n annotations: buildAnnotations(cmd),\n };\n}\n\n/** Create the MCP request handler. Returns an async function that processes a JSON-RPC request and returns a response (or undefined for notifications). */\nexport function createMcpHandler(\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneMcpPreferences,\n) {\n const serverName = prefs?.name ?? existingCommand.name;\n const serverVersion = prefs?.version ?? existingCommand.version ?? '0.0.0';\n\n const rootTools = collectEndpoints(existingCommand.commands, '');\n if (existingCommand.action || existingCommand.argsSchema) {\n rootTools.unshift({ name: '', command: existingCommand });\n }\n\n const toolMap = new Map(rootTools.map((t) => [toToolName(t.name), t]));\n\n const helpToolName = 'help';\n const helpToolDef = {\n name: helpToolName,\n title: 'Help',\n description: `Show help for the \"${serverName}\" program or a specific command`,\n inputSchema: {\n type: 'object' as const,\n properties: { command: { type: 'string', description: 'Command name to get help for (omit for program help)' } },\n additionalProperties: false,\n },\n };\n\n return async function handleRequest(req: JsonRpcRequest): Promise<JsonRpcResponse | undefined> {\n const { id, method, params } = req;\n\n switch (method) {\n case 'initialize':\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n result: {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: { name: serverName, version: serverVersion },\n },\n };\n\n case 'notifications/initialized':\n case 'notifications/cancelled':\n return undefined;\n\n case 'ping':\n return { jsonrpc: '2.0', id: id ?? null, result: {} };\n\n case 'tools/list': {\n const tools = [...rootTools.map((t) => buildToolDefinition(t.name, t.command)), helpToolDef];\n return { jsonrpc: '2.0', id: id ?? null, result: { tools } };\n }\n\n case 'tools/call': {\n const toolName = params?.name as string;\n const args = (params?.arguments ?? {}) as Record<string, unknown>;\n\n // Built-in help tool\n if (toolName === helpToolName) {\n const cmdName = args.command as string | undefined;\n const targetCmd = cmdName ? rootTools.find((t) => t.name === cmdName || toToolName(t.name) === cmdName)?.command : undefined;\n const helpText = generateHelp(existingCommand, targetCmd ?? existingCommand, { format: 'text', detail: 'full' });\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n result: { content: [{ type: 'text', text: helpText }], isError: false },\n };\n }\n\n const tool = toolMap.get(toolName);\n if (!tool) {\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n error: { code: -32602, message: `Unknown tool: ${toolName}` },\n };\n }\n\n // Build command string: convert tool name back to command path + serialize args as flags\n const commandPath = toCommandPath(tool.name);\n const argParts = serializeArgsToFlags(args);\n const input = [commandPath, ...argParts].filter(Boolean).join(' ') || undefined;\n\n try {\n const output: string[] = [];\n const errors: string[] = [];\n const result = await evalCommand(input as any, {\n caller: 'mcp',\n runtime: {\n output: (...outArgs: unknown[]) => output.push(outArgs.map(String).join(' ')),\n error: (text: string) => errors.push(text),\n interactive: 'unsupported',\n format: 'text',\n },\n });\n\n const content: { type: string; text: string }[] = [];\n\n if (result.error) {\n const errorMsg = result.error instanceof Error ? result.error.message : String(result.error);\n if (errors.length) content.push({ type: 'text', text: errors.join('\\n') });\n content.push({ type: 'text', text: errorMsg });\n return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: true } };\n }\n\n if (result.argsResult?.issues) {\n const issueMessages = result.argsResult.issues.map((i: any) => `${i.path?.join('.') || 'root'}: ${i.message}`).join('\\n');\n content.push({ type: 'text', text: `Validation error:\\n${issueMessages}` });\n return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: true } };\n }\n\n if (output.length) content.push({ type: 'text', text: output.join('\\n') });\n if (result.result !== undefined && result.result !== null) {\n const resultText = typeof result.result === 'string' ? result.result : JSON.stringify(result.result, null, 2);\n content.push({ type: 'text', text: resultText });\n }\n if (content.length === 0) content.push({ type: 'text', text: 'Done.' });\n return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: false } };\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n result: { content: [{ type: 'text', text: errorMsg }], isError: true },\n };\n }\n }\n\n default: {\n if (id !== undefined) {\n return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };\n }\n return undefined;\n }\n }\n };\n}\n\n/** stdio transport: newline-delimited JSON per 2025-11-25 spec. */\nasync function startStdioTransport(handleRequest: (req: JsonRpcRequest) => Promise<JsonRpcResponse | undefined>): Promise<void> {\n const { stdin, stdout } = await import('node:process');\n const { createInterface } = await import('node:readline');\n\n function send(msg: JsonRpcResponse) {\n stdout.write(`${JSON.stringify(msg)}\\n`);\n }\n\n const rl = createInterface({ input: stdin, crlfDelay: Infinity });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n try {\n const req = JSON.parse(line) as JsonRpcRequest;\n const res = await handleRequest(req);\n if (res) send(res);\n } catch {\n // Ignore malformed JSON\n }\n }\n}\n\n/** Streamable HTTP transport per 2025-11-25 spec. Responds with JSON or SSE based on client's Accept header. */\nasync function startHttpTransport(\n handleRequest: (req: JsonRpcRequest) => Promise<JsonRpcResponse | undefined>,\n prefs: PadroneMcpPreferences,\n log: (msg: string) => void,\n onSignal?: (callback: () => void) => () => void,\n): Promise<void> {\n const http = await import('node:http');\n const crypto = await import('node:crypto');\n\n const port = prefs.port ?? 3000;\n const host = prefs.host ?? '127.0.0.1';\n const endpoint = prefs.basePath ?? '/mcp';\n\n // Session management\n let sessionId: string | undefined;\n let negotiatedVersion: string | undefined;\n\n const corsOrigin = prefs.cors !== false ? (prefs.cors ?? '*') : undefined;\n\n const server = http.createServer(async (req, res) => {\n // CORS headers\n if (corsOrigin) {\n res.setHeader('Access-Control-Allow-Origin', corsOrigin);\n res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-Id, MCP-Protocol-Version');\n res.setHeader('Access-Control-Expose-Headers', 'MCP-Session-Id');\n }\n\n if (req.method === 'OPTIONS') {\n res.writeHead(corsOrigin ? 204 : 405);\n res.end();\n return;\n }\n\n if (req.url !== endpoint) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not found' }));\n return;\n }\n\n // DELETE: terminate session\n if (req.method === 'DELETE') {\n const reqSessionId = req.headers['mcp-session-id'] as string | undefined;\n if (sessionId && reqSessionId === sessionId) {\n sessionId = undefined;\n negotiatedVersion = undefined;\n res.writeHead(200);\n res.end();\n } else {\n res.writeHead(404);\n res.end();\n }\n return;\n }\n\n // GET: SSE stream (not implemented — return 405)\n if (req.method === 'GET') {\n res.writeHead(405, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32601, message: 'SSE stream not supported' } }));\n return;\n }\n\n if (req.method !== 'POST') {\n res.writeHead(405);\n res.end();\n return;\n }\n\n // Validate session ID on non-initialize requests\n const reqSessionId = req.headers['mcp-session-id'] as string | undefined;\n if (sessionId && reqSessionId && reqSessionId !== sessionId) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Invalid session' } }));\n return;\n }\n\n // Validate MCP-Protocol-Version header on post-init requests\n const reqProtocolVersion = req.headers['mcp-protocol-version'] as string | undefined;\n if (negotiatedVersion && reqProtocolVersion && reqProtocolVersion !== negotiatedVersion) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Protocol version mismatch' } }));\n return;\n }\n\n // Read request body\n const body = await readStreamAsText(req as AsyncIterable<Uint8Array>);\n\n let rpcRequest: JsonRpcRequest;\n try {\n rpcRequest = JSON.parse(body);\n } catch {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));\n return;\n }\n\n const response = await handleRequest(rpcRequest);\n\n // On initialize response: create session and set header\n if (rpcRequest.method === 'initialize' && response?.result) {\n sessionId = crypto.randomUUID();\n negotiatedVersion = PROTOCOL_VERSION;\n res.setHeader('MCP-Session-Id', sessionId);\n }\n\n if (response) {\n const accept = req.headers.accept ?? '';\n if (accept.includes('text/event-stream')) {\n res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });\n res.write(`event: message\\ndata: ${JSON.stringify(response)}\\n\\n`);\n res.end();\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(response));\n }\n } else {\n // Notification or response from client — no body\n res.writeHead(202);\n res.end();\n }\n });\n\n return new Promise<void>((resolve, reject) => {\n server.listen(port, host, () => {\n log(`MCP server listening on http://${host}:${port}${endpoint}`);\n });\n server.on('error', reject);\n const unsubscribe = onSignal?.(() => {\n server.close(() => resolve());\n });\n server.on('close', () => unsubscribe?.());\n });\n}\n\nexport async function startMcpServer(\n _program: AnyPadroneProgram,\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneMcpPreferences,\n): Promise<void> {\n const handleRequest = createMcpHandler(existingCommand, evalCommand, prefs);\n const transport = prefs?.transport ?? 'http';\n\n if (transport === 'stdio') {\n return startStdioTransport(handleRequest);\n }\n\n const { getCommandRuntime } = await import('../core/commands.ts');\n const runtime = getCommandRuntime(existingCommand);\n return startHttpTransport(handleRequest, prefs ?? {}, (msg) => runtime.error(msg), runtime.onSignal);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"mcp-D6PdtjIs.mjs","names":[],"sources":["../src/feature/mcp.ts"],"sourcesContent":["import { buildInputSchema, collectEndpoints, serializeArgsToFlags } from '../core/commands.ts';\nimport { generateHelp } from '../output/help.ts';\nimport type { AnyPadroneCommand, AnyPadroneProgram } from '../types/index.ts';\nimport { readStreamAsText } from '../util/stream.ts';\n\nexport type PadroneMcpPreferences = {\n /** Server name. Defaults to the program name. */\n name?: string;\n /** Server version. Defaults to the program version. */\n version?: string;\n /**\n * Transport mode.\n * - `'http'` — Start a Streamable HTTP server (default). Responds with `application/json` or `text/event-stream` based on the client's `Accept` header. Use `port` and `host` to configure.\n * - `'stdio'` — Communicate over stdin/stdout with newline-delimited JSON.\n */\n transport?: 'http' | 'stdio';\n /** HTTP port. Defaults to `3000`. Only used with `transport: 'http'`. */\n port?: number;\n /** HTTP host. Defaults to `'127.0.0.1'`. Only used with `transport: 'http'`. */\n host?: string;\n /** Base path for the MCP endpoint. Defaults to `'/mcp'`. Only used with `transport: 'http'`. */\n basePath?: string;\n /** CORS allowed origin. Defaults to `'*'`. Set to a specific origin or `false` to disable CORS headers. Only used with HTTP transports. */\n cors?: string | false;\n};\n\nconst PROTOCOL_VERSION = '2025-11-25';\n\ntype JsonRpcRequest = {\n jsonrpc: '2.0';\n id?: string | number;\n method: string;\n params?: Record<string, unknown>;\n};\n\ntype JsonRpcResponse = {\n jsonrpc: '2.0';\n id: string | number | null;\n result?: unknown;\n error?: { code: number; message: string; data?: unknown };\n};\n\n/** Convert an endpoint dot-path to a valid MCP tool name. Spec allows: [A-Za-z0-9_\\-\\.] */\nfunction toToolName(path: string): string {\n return path.replace(/\\s+/g, '.');\n}\n\n/** Convert a tool name back to a command path (dot → space). */\nfunction toCommandPath(toolName: string): string {\n return toolName.replace(/\\./g, ' ');\n}\n\n/** Build MCP tool annotations from a command's metadata. */\nfunction buildAnnotations(cmd: AnyPadroneCommand) {\n if (cmd.mutation == null) return undefined;\n return {\n destructiveHint: cmd.mutation || undefined,\n readOnlyHint: cmd.mutation === false || undefined,\n };\n}\n\n/** Build an MCP tool definition from a command. */\nfunction buildToolDefinition(name: string, cmd: AnyPadroneCommand) {\n return {\n name: toToolName(name),\n title: cmd.title ?? undefined,\n description: cmd.description || cmd.title || `Run the \"${name}\" command`,\n inputSchema: buildInputSchema(cmd),\n annotations: buildAnnotations(cmd),\n };\n}\n\n/** Create the MCP request handler. Returns an async function that processes a JSON-RPC request and returns a response (or undefined for notifications). */\nexport function createMcpHandler(\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneMcpPreferences,\n) {\n const serverName = prefs?.name ?? existingCommand.name;\n const serverVersion = prefs?.version ?? existingCommand.version ?? '0.0.0';\n\n const rootTools = collectEndpoints(existingCommand.commands, '');\n if (existingCommand.action || existingCommand.argsSchema) {\n rootTools.unshift({ name: '', command: existingCommand });\n }\n\n const toolMap = new Map(rootTools.map((t) => [toToolName(t.name), t]));\n\n const helpToolName = 'help';\n const helpToolDef = {\n name: helpToolName,\n title: 'Help',\n description: `Show help for the \"${serverName}\" program or a specific command`,\n inputSchema: {\n type: 'object' as const,\n properties: { command: { type: 'string', description: 'Command name to get help for (omit for program help)' } },\n additionalProperties: false,\n },\n };\n\n return async function handleRequest(req: JsonRpcRequest): Promise<JsonRpcResponse | undefined> {\n const { id, method, params } = req;\n\n switch (method) {\n case 'initialize':\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n result: {\n protocolVersion: PROTOCOL_VERSION,\n capabilities: { tools: {} },\n serverInfo: { name: serverName, version: serverVersion },\n },\n };\n\n case 'notifications/initialized':\n case 'notifications/cancelled':\n return undefined;\n\n case 'ping':\n return { jsonrpc: '2.0', id: id ?? null, result: {} };\n\n case 'tools/list': {\n const tools = [...rootTools.map((t) => buildToolDefinition(t.name, t.command)), helpToolDef];\n return { jsonrpc: '2.0', id: id ?? null, result: { tools } };\n }\n\n case 'tools/call': {\n const toolName = params?.name as string;\n const args = (params?.arguments ?? {}) as Record<string, unknown>;\n\n // Built-in help tool\n if (toolName === helpToolName) {\n const cmdName = args.command as string | undefined;\n const targetCmd = cmdName ? rootTools.find((t) => t.name === cmdName || toToolName(t.name) === cmdName)?.command : undefined;\n const helpText = generateHelp(existingCommand, targetCmd ?? existingCommand, { format: 'text', detail: 'full' });\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n result: { content: [{ type: 'text', text: helpText }], isError: false },\n };\n }\n\n const tool = toolMap.get(toolName);\n if (!tool) {\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n error: { code: -32602, message: `Unknown tool: ${toolName}` },\n };\n }\n\n // Build command string: convert tool name back to command path + serialize args as flags\n const commandPath = toCommandPath(tool.name);\n const argParts = serializeArgsToFlags(args);\n const input = [commandPath, ...argParts].filter(Boolean).join(' ') || undefined;\n\n try {\n const output: string[] = [];\n const errors: string[] = [];\n const result = await evalCommand(input as any, {\n caller: 'mcp',\n runtime: {\n output: (...outArgs: unknown[]) => output.push(outArgs.map(String).join(' ')),\n error: (text: string) => errors.push(text),\n interactive: 'unsupported',\n format: 'text',\n },\n });\n\n const content: { type: string; text: string }[] = [];\n\n if (result.error) {\n const errorMsg = result.error instanceof Error ? result.error.message : String(result.error);\n if (errors.length) content.push({ type: 'text', text: errors.join('\\n') });\n content.push({ type: 'text', text: errorMsg });\n return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: true } };\n }\n\n if (result.argsResult?.issues) {\n const issueMessages = result.argsResult.issues.map((i: any) => `${i.path?.join('.') || 'root'}: ${i.message}`).join('\\n');\n content.push({ type: 'text', text: `Validation error:\\n${issueMessages}` });\n return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: true } };\n }\n\n if (output.length) content.push({ type: 'text', text: output.join('\\n') });\n if (result.result !== undefined && result.result !== null) {\n const resultText = typeof result.result === 'string' ? result.result : JSON.stringify(result.result, null, 2);\n content.push({ type: 'text', text: resultText });\n }\n if (content.length === 0) content.push({ type: 'text', text: 'Done.' });\n return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: false } };\n } catch (err) {\n const errorMsg = err instanceof Error ? err.message : String(err);\n return {\n jsonrpc: '2.0',\n id: id ?? null,\n result: { content: [{ type: 'text', text: errorMsg }], isError: true },\n };\n }\n }\n\n default: {\n if (id !== undefined) {\n return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };\n }\n return undefined;\n }\n }\n };\n}\n\n/** stdio transport: newline-delimited JSON per 2025-11-25 spec. */\nasync function startStdioTransport(handleRequest: (req: JsonRpcRequest) => Promise<JsonRpcResponse | undefined>): Promise<void> {\n const { stdin, stdout } = await import('node:process');\n const { createInterface } = await import('node:readline');\n\n function send(msg: JsonRpcResponse) {\n stdout.write(`${JSON.stringify(msg)}\\n`);\n }\n\n const rl = createInterface({ input: stdin, crlfDelay: Infinity });\n\n for await (const line of rl) {\n if (!line.trim()) continue;\n try {\n const req = JSON.parse(line) as JsonRpcRequest;\n const res = await handleRequest(req);\n if (res) send(res);\n } catch {\n // Ignore malformed JSON\n }\n }\n}\n\n/** Streamable HTTP transport per 2025-11-25 spec. Responds with JSON or SSE based on client's Accept header. */\nasync function startHttpTransport(\n handleRequest: (req: JsonRpcRequest) => Promise<JsonRpcResponse | undefined>,\n prefs: PadroneMcpPreferences,\n log: (msg: string) => void,\n onSignal?: (callback: () => void) => () => void,\n): Promise<void> {\n const http = await import('node:http');\n const crypto = await import('node:crypto');\n\n const port = prefs.port ?? 3000;\n const host = prefs.host ?? '127.0.0.1';\n const endpoint = prefs.basePath ?? '/mcp';\n\n // Session management\n let sessionId: string | undefined;\n let negotiatedVersion: string | undefined;\n\n const corsOrigin = prefs.cors !== false ? (prefs.cors ?? '*') : undefined;\n\n const server = http.createServer(async (req, res) => {\n // CORS headers\n if (corsOrigin) {\n res.setHeader('Access-Control-Allow-Origin', corsOrigin);\n res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-Id, MCP-Protocol-Version');\n res.setHeader('Access-Control-Expose-Headers', 'MCP-Session-Id');\n }\n\n if (req.method === 'OPTIONS') {\n res.writeHead(corsOrigin ? 204 : 405);\n res.end();\n return;\n }\n\n if (req.url !== endpoint) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Not found' }));\n return;\n }\n\n // DELETE: terminate session\n if (req.method === 'DELETE') {\n const reqSessionId = req.headers['mcp-session-id'] as string | undefined;\n if (sessionId && reqSessionId === sessionId) {\n sessionId = undefined;\n negotiatedVersion = undefined;\n res.writeHead(200);\n res.end();\n } else {\n res.writeHead(404);\n res.end();\n }\n return;\n }\n\n // GET: SSE stream (not implemented — return 405)\n if (req.method === 'GET') {\n res.writeHead(405, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32601, message: 'SSE stream not supported' } }));\n return;\n }\n\n if (req.method !== 'POST') {\n res.writeHead(405);\n res.end();\n return;\n }\n\n // Validate session ID on non-initialize requests\n const reqSessionId = req.headers['mcp-session-id'] as string | undefined;\n if (sessionId && reqSessionId && reqSessionId !== sessionId) {\n res.writeHead(404, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Invalid session' } }));\n return;\n }\n\n // Validate MCP-Protocol-Version header on post-init requests\n const reqProtocolVersion = req.headers['mcp-protocol-version'] as string | undefined;\n if (negotiatedVersion && reqProtocolVersion && reqProtocolVersion !== negotiatedVersion) {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Protocol version mismatch' } }));\n return;\n }\n\n // Read request body\n const body = await readStreamAsText(req as AsyncIterable<Uint8Array>);\n\n let rpcRequest: JsonRpcRequest;\n try {\n rpcRequest = JSON.parse(body);\n } catch {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));\n return;\n }\n\n const response = await handleRequest(rpcRequest);\n\n // On initialize response: create session and set header\n if (rpcRequest.method === 'initialize' && response?.result) {\n sessionId = crypto.randomUUID();\n negotiatedVersion = PROTOCOL_VERSION;\n res.setHeader('MCP-Session-Id', sessionId);\n }\n\n if (response) {\n const accept = req.headers.accept ?? '';\n if (accept.includes('text/event-stream')) {\n res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });\n res.write(`event: message\\ndata: ${JSON.stringify(response)}\\n\\n`);\n res.end();\n } else {\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(response));\n }\n } else {\n // Notification or response from client — no body\n res.writeHead(202);\n res.end();\n }\n });\n\n return new Promise<void>((resolve, reject) => {\n server.listen(port, host, () => {\n log(`MCP server listening on http://${host}:${port}${endpoint}`);\n });\n server.on('error', reject);\n const unsubscribe = onSignal?.(() => {\n server.close(() => resolve());\n });\n server.on('close', () => unsubscribe?.());\n });\n}\n\nexport async function startMcpServer(\n _program: AnyPadroneProgram,\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneMcpPreferences,\n): Promise<void> {\n const handleRequest = createMcpHandler(existingCommand, evalCommand, prefs);\n const transport = prefs?.transport ?? 'http';\n\n if (transport === 'stdio') {\n return startStdioTransport(handleRequest);\n }\n\n const { getCommandRuntime } = await import('../core/commands.ts');\n const runtime = getCommandRuntime(existingCommand);\n return startHttpTransport(handleRequest, prefs ?? {}, (msg) => runtime.error(msg), runtime.onSignal);\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { m as serializeArgsToFlags, r as collectEndpoints, t as buildInputSchema } from "./commands-
|
|
1
|
+
import { m as serializeArgsToFlags, r as collectEndpoints, t as buildInputSchema } from "./commands-DLR0rFgq.mjs";
|
|
2
2
|
import { a as readStreamAsText } from "./stream-DC4H8YTx.mjs";
|
|
3
|
-
import { t as generateHelp } from "./help-
|
|
3
|
+
import { t as generateHelp } from "./help-B-ZMYyn-.mjs";
|
|
4
4
|
import { i as RoutingError, o as ValidationError } from "./errors-DA4KzK1M.mjs";
|
|
5
5
|
//#region src/feature/serve.ts
|
|
6
6
|
/** Convert an endpoint dot-path to a URL path segment. */
|
|
@@ -372,7 +372,7 @@ async function startServeServer(_program, existingCommand, evalCommand, prefs) {
|
|
|
372
372
|
const body = await response.text();
|
|
373
373
|
res.end(body);
|
|
374
374
|
});
|
|
375
|
-
const { getCommandRuntime } = await import("./commands-
|
|
375
|
+
const { getCommandRuntime } = await import("./commands-DLR0rFgq.mjs").then((n) => n.a);
|
|
376
376
|
const runtime = getCommandRuntime(existingCommand);
|
|
377
377
|
return new Promise((resolve, reject) => {
|
|
378
378
|
server.listen(port, host, () => {
|
|
@@ -399,4 +399,4 @@ async function readBody(req) {
|
|
|
399
399
|
//#endregion
|
|
400
400
|
export { startServeServer };
|
|
401
401
|
|
|
402
|
-
//# sourceMappingURL=serve-
|
|
402
|
+
//# sourceMappingURL=serve-PaCLsNoD.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serve-YVTPzBCl.mjs","names":[],"sources":["../src/feature/serve.ts"],"sourcesContent":["import { buildInputSchema, type CollectedEndpoint, collectEndpoints, serializeArgsToFlags } from '../core/commands.ts';\nimport { RoutingError, ValidationError } from '../core/errors.ts';\nimport { generateHelp } from '../output/help.ts';\nimport type { AnyPadroneCommand, AnyPadroneProgram } from '../types/index.ts';\nimport { readStreamAsText } from '../util/stream.ts';\n\nexport type PadroneServePreferences = {\n /** Port to listen on. Default: 3000 */\n port?: number;\n /** Host to bind to. Default: '127.0.0.1' */\n host?: string;\n /** Base path prefix for all routes. Default: '/' */\n basePath?: string;\n /** CORS allowed origin. Default: '*'. Set to `false` to disable CORS headers. */\n cors?: string | false;\n /** Control built-in utility endpoints. All enabled by default. */\n builtins?: {\n /** GET /_health — returns 200 OK. */\n health?: boolean;\n /** GET /_help and GET /_help/:command — returns help text. */\n help?: boolean;\n /** GET /_schema and GET /_schema/:command — returns JSON Schema. */\n schema?: boolean;\n /** GET /_docs — Scalar OpenAPI docs viewer. */\n docs?: boolean;\n };\n /** Hook to run before each request. Return a Response to short-circuit. */\n onRequest?: (req: Request) => Response | void | Promise<Response | void>;\n /** Transform errors into responses. */\n onError?: (error: unknown, req: Request) => Response;\n};\n\n/** Convert an endpoint dot-path to a URL path segment. */\nfunction toUrlPath(name: string): string {\n return name.replace(/\\./g, '/');\n}\n\n/** Convert a URL path segment back to a command path (slash → space). */\nfunction toCommandPath(urlPath: string): string {\n return urlPath.replace(/\\//g, ' ');\n}\n\nfunction jsonResponse(body: unknown, status = 200, headers?: Record<string, string>): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json', ...headers },\n });\n}\n\nfunction errorToStatus(error: unknown): number {\n if (error instanceof RoutingError) return 404;\n if (error instanceof ValidationError) return 400;\n return 500;\n}\n\nfunction errorToResponse(error: unknown): Response {\n const status = errorToStatus(error);\n if (error instanceof ValidationError) {\n return jsonResponse(\n {\n ok: false,\n error: 'validation',\n message: error.message,\n issues: error.issues.map((i) => ({ path: i.path?.map(String), message: i.message })),\n },\n status,\n );\n }\n if (error instanceof RoutingError) {\n return jsonResponse({ ok: false, error: 'not_found', message: error.message, suggestions: error.suggestions }, status);\n }\n const message = error instanceof Error ? error.message : String(error);\n return jsonResponse({ ok: false, error: 'action_error', message }, status);\n}\n\n/** Generate an OpenAPI 3.1.0 spec from the command tree. */\nfunction buildOpenApiSpec(existingCommand: AnyPadroneCommand, endpoints: CollectedEndpoint[], basePath: string): Record<string, unknown> {\n const paths: Record<string, unknown> = {};\n\n const responseSchema = {\n '200': {\n description: 'Successful response',\n content: { 'application/json': { schema: { type: 'object', properties: { ok: { type: 'boolean', const: true }, result: {} } } } },\n },\n '400': {\n description: 'Validation error',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean', const: false },\n error: { type: 'string', const: 'validation' },\n message: { type: 'string' },\n issues: { type: 'array', items: { type: 'object', properties: { path: { type: 'array' }, message: { type: 'string' } } } },\n },\n },\n },\n },\n },\n '404': {\n description: 'Command not found',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean', const: false },\n error: { type: 'string', const: 'not_found' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n '500': {\n description: 'Action error',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean', const: false },\n error: { type: 'string', const: 'action_error' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n };\n\n for (const { name, command: cmd } of endpoints) {\n const urlPath = `${basePath}${toUrlPath(name)}`;\n const inputSchema = buildInputSchema(cmd);\n const description = cmd.description || cmd.title || `Run the \"${name}\" command`;\n const pathItem: Record<string, unknown> = {};\n\n const postOp = {\n summary: cmd.title || name,\n description,\n operationId: `post_${name.replace(/\\./g, '_')}`,\n requestBody: { content: { 'application/json': { schema: inputSchema } } },\n responses: responseSchema,\n };\n\n if (cmd.mutation) {\n pathItem.post = postOp;\n } else {\n // GET: args as query parameters\n const properties = (inputSchema.properties ?? {}) as Record<string, Record<string, unknown>>;\n const queryParams = Object.entries(properties).map(([key, schema]) => ({\n name: key,\n in: 'query',\n schema,\n required: (inputSchema.required as string[] | undefined)?.includes(key) ?? false,\n }));\n pathItem.get = {\n summary: cmd.title || name,\n description,\n operationId: `get_${name.replace(/\\./g, '_')}`,\n parameters: queryParams,\n responses: responseSchema,\n };\n pathItem.post = postOp;\n }\n\n paths[urlPath] = pathItem;\n }\n\n return {\n openapi: '3.1.0',\n info: {\n title: existingCommand.title || existingCommand.name,\n description: existingCommand.description,\n version: existingCommand.version ?? '0.0.0',\n },\n paths,\n };\n}\n\nfunction scalarDocsHtml(openapiUrl: string, title: string): string {\n return `<!doctype html>\n<html>\n<head>\n <title>${title} — API Docs</title>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n</head>\n<body>\n <script id=\"api-reference\" data-url=\"${openapiUrl}\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n</body>\n</html>`;\n}\n\n/** Create the serve request handler. */\nexport function createServeHandler(\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneServePreferences,\n): (req: Request) => Promise<Response> {\n const basePath = (prefs?.basePath ?? '/').replace(/\\/$/, '/');\n const corsOrigin = prefs?.cors !== false ? (prefs?.cors ?? '*') : undefined;\n const builtins = { health: true, help: true, schema: true, docs: true, ...prefs?.builtins };\n\n const endpoints = collectEndpoints(existingCommand.commands, '');\n if (existingCommand.action || existingCommand.argsSchema) {\n endpoints.unshift({ name: '', command: existingCommand });\n }\n\n const routeMap = new Map<string, CollectedEndpoint>();\n for (const ep of endpoints) {\n routeMap.set(toUrlPath(ep.name), ep);\n }\n\n let cachedOpenApiSpec: Record<string, unknown> | undefined;\n const getOpenApiSpec = () => (cachedOpenApiSpec ??= buildOpenApiSpec(existingCommand, endpoints, basePath));\n\n function addCorsHeaders(res: Response): Response {\n if (!corsOrigin) return res;\n const headers = new Headers(res.headers);\n headers.set('Access-Control-Allow-Origin', corsOrigin);\n headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n headers.set('Access-Control-Allow-Headers', 'Content-Type');\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n }\n\n async function evalAndRespond(commandString: string, request: Request): Promise<Response> {\n const output: string[] = [];\n const errors: string[] = [];\n const result = await evalCommand(commandString || (undefined as any), {\n caller: 'serve',\n runtime: {\n output: (...args: unknown[]) => output.push(args.map(String).join(' ')),\n error: (text: string) => errors.push(text),\n interactive: 'unsupported',\n format: 'json',\n },\n });\n\n if (result.error) {\n return prefs?.onError ? prefs.onError(result.error, request) : errorToResponse(result.error);\n }\n\n if (result.argsResult?.issues) {\n const issues = (result.argsResult.issues as { path?: PropertyKey[]; message: string }[]).map((i) => ({\n path: i.path?.map(String),\n message: i.message,\n }));\n return jsonResponse({ ok: false, error: 'validation', issues }, 400);\n }\n\n return jsonResponse({ ok: true, result: result.result ?? null });\n }\n\n return async function handleRequest(req: Request): Promise<Response> {\n // CORS preflight\n if (req.method === 'OPTIONS') {\n return addCorsHeaders(new Response(null, { status: corsOrigin ? 204 : 405 }));\n }\n\n // onRequest hook\n if (prefs?.onRequest) {\n const hookResponse = await prefs.onRequest(req);\n if (hookResponse) return addCorsHeaders(hookResponse);\n }\n\n const url = new URL(req.url, 'http://localhost');\n let pathname = url.pathname;\n\n // Strip basePath prefix\n if (basePath !== '/' && pathname.startsWith(basePath)) {\n pathname = pathname.slice(basePath.length - 1);\n }\n // Remove leading slash for route matching\n const routePath = pathname.replace(/^\\//, '');\n\n // Built-in endpoints\n if (req.method === 'GET') {\n if (builtins.health && routePath === '_health') {\n return addCorsHeaders(jsonResponse({ status: 'ok' }));\n }\n\n if (builtins.schema && routePath === '_schema') {\n const schemaMap: Record<string, unknown> = {};\n for (const ep of endpoints) {\n schemaMap[toUrlPath(ep.name) || '/'] = buildInputSchema(ep.command);\n }\n return addCorsHeaders(jsonResponse(schemaMap));\n }\n\n if (builtins.schema && routePath.startsWith('_schema/')) {\n const cmdPath = routePath.slice('_schema/'.length);\n const ep = routeMap.get(cmdPath);\n if (!ep) return addCorsHeaders(jsonResponse({ ok: false, error: 'not_found', message: `Command not found: ${cmdPath}` }, 404));\n return addCorsHeaders(jsonResponse(buildInputSchema(ep.command)));\n }\n\n if (builtins.help && routePath === '_help') {\n const accept = req.headers.get('accept') ?? '';\n const format = accept.includes('application/json') ? 'json' : 'markdown';\n const helpText = generateHelp(existingCommand, existingCommand, { format, detail: 'full' });\n if (format === 'json') return addCorsHeaders(jsonResponse(JSON.parse(helpText)));\n return addCorsHeaders(new Response(helpText, { status: 200, headers: { 'Content-Type': 'text/markdown' } }));\n }\n\n if (builtins.help && routePath.startsWith('_help/')) {\n const cmdPath = routePath.slice('_help/'.length);\n const ep = routeMap.get(cmdPath);\n if (!ep) return addCorsHeaders(jsonResponse({ ok: false, error: 'not_found', message: `Command not found: ${cmdPath}` }, 404));\n const accept = req.headers.get('accept') ?? '';\n const format = accept.includes('application/json') ? 'json' : 'markdown';\n const helpText = generateHelp(existingCommand, ep.command, { format, detail: 'full' });\n if (format === 'json') return addCorsHeaders(jsonResponse(JSON.parse(helpText)));\n return addCorsHeaders(new Response(helpText, { status: 200, headers: { 'Content-Type': 'text/markdown' } }));\n }\n\n if (builtins.docs && routePath === '_openapi') {\n return addCorsHeaders(jsonResponse(getOpenApiSpec()));\n }\n\n if (builtins.docs && routePath === '_docs') {\n const openapiUrl = `${basePath}_openapi`;\n const title = existingCommand.title || existingCommand.name;\n const html = scalarDocsHtml(openapiUrl, title);\n return addCorsHeaders(new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } }));\n }\n }\n\n // Route to command\n const endpoint = routeMap.get(routePath);\n if (!endpoint) {\n return addCorsHeaders(jsonResponse({ ok: false, error: 'not_found', message: `Command not found: ${routePath || '/'}` }, 404));\n }\n\n // Enforce method based on mutation flag\n if (endpoint.command.mutation && req.method === 'GET') {\n return addCorsHeaders(\n new Response(JSON.stringify({ ok: false, error: 'method_not_allowed', message: 'Mutation commands only accept POST' }), {\n status: 405,\n headers: { 'Content-Type': 'application/json', Allow: 'POST' },\n }),\n );\n }\n\n if (req.method !== 'GET' && req.method !== 'POST') {\n return addCorsHeaders(new Response(null, { status: 405, headers: { Allow: endpoint.command.mutation ? 'POST' : 'GET, POST' } }));\n }\n\n // Build command string from request\n const commandPath = toCommandPath(routePath);\n let argParts: string[];\n\n if (req.method === 'POST') {\n try {\n const body = (await req.json()) as Record<string, unknown>;\n argParts = serializeArgsToFlags(body);\n } catch {\n return addCorsHeaders(jsonResponse({ ok: false, error: 'bad_request', message: 'Invalid JSON body' }, 400));\n }\n } else {\n // GET: query string → flags\n argParts = [];\n for (const [key, value] of url.searchParams.entries()) {\n if (key === '_') {\n // Positional args\n argParts.push(value);\n } else {\n argParts.push(value === '' ? `--${key}` : `--${key}=${value}`);\n }\n }\n }\n\n const commandString = [commandPath, ...argParts].filter(Boolean).join(' ');\n const response = await evalAndRespond(commandString, req);\n return addCorsHeaders(response);\n };\n}\n\n/** Start the serve HTTP server. */\nexport async function startServeServer(\n _program: AnyPadroneProgram,\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneServePreferences,\n): Promise<void> {\n const handler = createServeHandler(existingCommand, evalCommand, prefs);\n const http = await import('node:http');\n\n const port = prefs?.port ?? 3000;\n const host = prefs?.host ?? '127.0.0.1';\n const basePath = (prefs?.basePath ?? '/').replace(/\\/$/, '/');\n\n const server = http.createServer(async (req, res) => {\n const url = `http://${host}:${port}${req.url}`;\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) headers.set(key, Array.isArray(value) ? value.join(', ') : value);\n }\n\n const fetchReq = new Request(url, {\n method: req.method,\n headers,\n body: req.method !== 'GET' && req.method !== 'HEAD' ? await readBody(req) : undefined,\n });\n\n const response = await handler(fetchReq);\n const resHeaders: Record<string, string> = {};\n response.headers.forEach((v, k) => {\n resHeaders[k] = v;\n });\n res.writeHead(response.status, resHeaders);\n const body = await response.text();\n res.end(body);\n });\n\n const { getCommandRuntime } = await import('../core/commands.ts');\n const runtime = getCommandRuntime(existingCommand);\n\n return new Promise<void>((resolve, reject) => {\n server.listen(port, host, () => {\n runtime.error(`REST server listening on http://${host}:${port}${basePath}`);\n const builtins = { health: true, help: true, schema: true, docs: true, ...prefs?.builtins };\n if (builtins.docs) runtime.error(`API docs: http://${host}:${port}${basePath}_docs`);\n });\n server.on('error', reject);\n const unsubscribe = runtime.onSignal?.(() => {\n server.close(() => resolve());\n });\n server.on('close', () => unsubscribe?.());\n });\n}\n\n/** Read the full body from a Node.js IncomingMessage. */\nasync function readBody(req: import('node:http').IncomingMessage): Promise<string> {\n return readStreamAsText(req as AsyncIterable<Uint8Array>);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"serve-PaCLsNoD.mjs","names":[],"sources":["../src/feature/serve.ts"],"sourcesContent":["import { buildInputSchema, type CollectedEndpoint, collectEndpoints, serializeArgsToFlags } from '../core/commands.ts';\nimport { RoutingError, ValidationError } from '../core/errors.ts';\nimport { generateHelp } from '../output/help.ts';\nimport type { AnyPadroneCommand, AnyPadroneProgram } from '../types/index.ts';\nimport { readStreamAsText } from '../util/stream.ts';\n\nexport type PadroneServePreferences = {\n /** Port to listen on. Default: 3000 */\n port?: number;\n /** Host to bind to. Default: '127.0.0.1' */\n host?: string;\n /** Base path prefix for all routes. Default: '/' */\n basePath?: string;\n /** CORS allowed origin. Default: '*'. Set to `false` to disable CORS headers. */\n cors?: string | false;\n /** Control built-in utility endpoints. All enabled by default. */\n builtins?: {\n /** GET /_health — returns 200 OK. */\n health?: boolean;\n /** GET /_help and GET /_help/:command — returns help text. */\n help?: boolean;\n /** GET /_schema and GET /_schema/:command — returns JSON Schema. */\n schema?: boolean;\n /** GET /_docs — Scalar OpenAPI docs viewer. */\n docs?: boolean;\n };\n /** Hook to run before each request. Return a Response to short-circuit. */\n onRequest?: (req: Request) => Response | void | Promise<Response | void>;\n /** Transform errors into responses. */\n onError?: (error: unknown, req: Request) => Response;\n};\n\n/** Convert an endpoint dot-path to a URL path segment. */\nfunction toUrlPath(name: string): string {\n return name.replace(/\\./g, '/');\n}\n\n/** Convert a URL path segment back to a command path (slash → space). */\nfunction toCommandPath(urlPath: string): string {\n return urlPath.replace(/\\//g, ' ');\n}\n\nfunction jsonResponse(body: unknown, status = 200, headers?: Record<string, string>): Response {\n return new Response(JSON.stringify(body), {\n status,\n headers: { 'Content-Type': 'application/json', ...headers },\n });\n}\n\nfunction errorToStatus(error: unknown): number {\n if (error instanceof RoutingError) return 404;\n if (error instanceof ValidationError) return 400;\n return 500;\n}\n\nfunction errorToResponse(error: unknown): Response {\n const status = errorToStatus(error);\n if (error instanceof ValidationError) {\n return jsonResponse(\n {\n ok: false,\n error: 'validation',\n message: error.message,\n issues: error.issues.map((i) => ({ path: i.path?.map(String), message: i.message })),\n },\n status,\n );\n }\n if (error instanceof RoutingError) {\n return jsonResponse({ ok: false, error: 'not_found', message: error.message, suggestions: error.suggestions }, status);\n }\n const message = error instanceof Error ? error.message : String(error);\n return jsonResponse({ ok: false, error: 'action_error', message }, status);\n}\n\n/** Generate an OpenAPI 3.1.0 spec from the command tree. */\nfunction buildOpenApiSpec(existingCommand: AnyPadroneCommand, endpoints: CollectedEndpoint[], basePath: string): Record<string, unknown> {\n const paths: Record<string, unknown> = {};\n\n const responseSchema = {\n '200': {\n description: 'Successful response',\n content: { 'application/json': { schema: { type: 'object', properties: { ok: { type: 'boolean', const: true }, result: {} } } } },\n },\n '400': {\n description: 'Validation error',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean', const: false },\n error: { type: 'string', const: 'validation' },\n message: { type: 'string' },\n issues: { type: 'array', items: { type: 'object', properties: { path: { type: 'array' }, message: { type: 'string' } } } },\n },\n },\n },\n },\n },\n '404': {\n description: 'Command not found',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean', const: false },\n error: { type: 'string', const: 'not_found' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n '500': {\n description: 'Action error',\n content: {\n 'application/json': {\n schema: {\n type: 'object',\n properties: {\n ok: { type: 'boolean', const: false },\n error: { type: 'string', const: 'action_error' },\n message: { type: 'string' },\n },\n },\n },\n },\n },\n };\n\n for (const { name, command: cmd } of endpoints) {\n const urlPath = `${basePath}${toUrlPath(name)}`;\n const inputSchema = buildInputSchema(cmd);\n const description = cmd.description || cmd.title || `Run the \"${name}\" command`;\n const pathItem: Record<string, unknown> = {};\n\n const postOp = {\n summary: cmd.title || name,\n description,\n operationId: `post_${name.replace(/\\./g, '_')}`,\n requestBody: { content: { 'application/json': { schema: inputSchema } } },\n responses: responseSchema,\n };\n\n if (cmd.mutation) {\n pathItem.post = postOp;\n } else {\n // GET: args as query parameters\n const properties = (inputSchema.properties ?? {}) as Record<string, Record<string, unknown>>;\n const queryParams = Object.entries(properties).map(([key, schema]) => ({\n name: key,\n in: 'query',\n schema,\n required: (inputSchema.required as string[] | undefined)?.includes(key) ?? false,\n }));\n pathItem.get = {\n summary: cmd.title || name,\n description,\n operationId: `get_${name.replace(/\\./g, '_')}`,\n parameters: queryParams,\n responses: responseSchema,\n };\n pathItem.post = postOp;\n }\n\n paths[urlPath] = pathItem;\n }\n\n return {\n openapi: '3.1.0',\n info: {\n title: existingCommand.title || existingCommand.name,\n description: existingCommand.description,\n version: existingCommand.version ?? '0.0.0',\n },\n paths,\n };\n}\n\nfunction scalarDocsHtml(openapiUrl: string, title: string): string {\n return `<!doctype html>\n<html>\n<head>\n <title>${title} — API Docs</title>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n</head>\n<body>\n <script id=\"api-reference\" data-url=\"${openapiUrl}\"></script>\n <script src=\"https://cdn.jsdelivr.net/npm/@scalar/api-reference\"></script>\n</body>\n</html>`;\n}\n\n/** Create the serve request handler. */\nexport function createServeHandler(\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneServePreferences,\n): (req: Request) => Promise<Response> {\n const basePath = (prefs?.basePath ?? '/').replace(/\\/$/, '/');\n const corsOrigin = prefs?.cors !== false ? (prefs?.cors ?? '*') : undefined;\n const builtins = { health: true, help: true, schema: true, docs: true, ...prefs?.builtins };\n\n const endpoints = collectEndpoints(existingCommand.commands, '');\n if (existingCommand.action || existingCommand.argsSchema) {\n endpoints.unshift({ name: '', command: existingCommand });\n }\n\n const routeMap = new Map<string, CollectedEndpoint>();\n for (const ep of endpoints) {\n routeMap.set(toUrlPath(ep.name), ep);\n }\n\n let cachedOpenApiSpec: Record<string, unknown> | undefined;\n const getOpenApiSpec = () => (cachedOpenApiSpec ??= buildOpenApiSpec(existingCommand, endpoints, basePath));\n\n function addCorsHeaders(res: Response): Response {\n if (!corsOrigin) return res;\n const headers = new Headers(res.headers);\n headers.set('Access-Control-Allow-Origin', corsOrigin);\n headers.set('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n headers.set('Access-Control-Allow-Headers', 'Content-Type');\n return new Response(res.body, { status: res.status, statusText: res.statusText, headers });\n }\n\n async function evalAndRespond(commandString: string, request: Request): Promise<Response> {\n const output: string[] = [];\n const errors: string[] = [];\n const result = await evalCommand(commandString || (undefined as any), {\n caller: 'serve',\n runtime: {\n output: (...args: unknown[]) => output.push(args.map(String).join(' ')),\n error: (text: string) => errors.push(text),\n interactive: 'unsupported',\n format: 'json',\n },\n });\n\n if (result.error) {\n return prefs?.onError ? prefs.onError(result.error, request) : errorToResponse(result.error);\n }\n\n if (result.argsResult?.issues) {\n const issues = (result.argsResult.issues as { path?: PropertyKey[]; message: string }[]).map((i) => ({\n path: i.path?.map(String),\n message: i.message,\n }));\n return jsonResponse({ ok: false, error: 'validation', issues }, 400);\n }\n\n return jsonResponse({ ok: true, result: result.result ?? null });\n }\n\n return async function handleRequest(req: Request): Promise<Response> {\n // CORS preflight\n if (req.method === 'OPTIONS') {\n return addCorsHeaders(new Response(null, { status: corsOrigin ? 204 : 405 }));\n }\n\n // onRequest hook\n if (prefs?.onRequest) {\n const hookResponse = await prefs.onRequest(req);\n if (hookResponse) return addCorsHeaders(hookResponse);\n }\n\n const url = new URL(req.url, 'http://localhost');\n let pathname = url.pathname;\n\n // Strip basePath prefix\n if (basePath !== '/' && pathname.startsWith(basePath)) {\n pathname = pathname.slice(basePath.length - 1);\n }\n // Remove leading slash for route matching\n const routePath = pathname.replace(/^\\//, '');\n\n // Built-in endpoints\n if (req.method === 'GET') {\n if (builtins.health && routePath === '_health') {\n return addCorsHeaders(jsonResponse({ status: 'ok' }));\n }\n\n if (builtins.schema && routePath === '_schema') {\n const schemaMap: Record<string, unknown> = {};\n for (const ep of endpoints) {\n schemaMap[toUrlPath(ep.name) || '/'] = buildInputSchema(ep.command);\n }\n return addCorsHeaders(jsonResponse(schemaMap));\n }\n\n if (builtins.schema && routePath.startsWith('_schema/')) {\n const cmdPath = routePath.slice('_schema/'.length);\n const ep = routeMap.get(cmdPath);\n if (!ep) return addCorsHeaders(jsonResponse({ ok: false, error: 'not_found', message: `Command not found: ${cmdPath}` }, 404));\n return addCorsHeaders(jsonResponse(buildInputSchema(ep.command)));\n }\n\n if (builtins.help && routePath === '_help') {\n const accept = req.headers.get('accept') ?? '';\n const format = accept.includes('application/json') ? 'json' : 'markdown';\n const helpText = generateHelp(existingCommand, existingCommand, { format, detail: 'full' });\n if (format === 'json') return addCorsHeaders(jsonResponse(JSON.parse(helpText)));\n return addCorsHeaders(new Response(helpText, { status: 200, headers: { 'Content-Type': 'text/markdown' } }));\n }\n\n if (builtins.help && routePath.startsWith('_help/')) {\n const cmdPath = routePath.slice('_help/'.length);\n const ep = routeMap.get(cmdPath);\n if (!ep) return addCorsHeaders(jsonResponse({ ok: false, error: 'not_found', message: `Command not found: ${cmdPath}` }, 404));\n const accept = req.headers.get('accept') ?? '';\n const format = accept.includes('application/json') ? 'json' : 'markdown';\n const helpText = generateHelp(existingCommand, ep.command, { format, detail: 'full' });\n if (format === 'json') return addCorsHeaders(jsonResponse(JSON.parse(helpText)));\n return addCorsHeaders(new Response(helpText, { status: 200, headers: { 'Content-Type': 'text/markdown' } }));\n }\n\n if (builtins.docs && routePath === '_openapi') {\n return addCorsHeaders(jsonResponse(getOpenApiSpec()));\n }\n\n if (builtins.docs && routePath === '_docs') {\n const openapiUrl = `${basePath}_openapi`;\n const title = existingCommand.title || existingCommand.name;\n const html = scalarDocsHtml(openapiUrl, title);\n return addCorsHeaders(new Response(html, { status: 200, headers: { 'Content-Type': 'text/html' } }));\n }\n }\n\n // Route to command\n const endpoint = routeMap.get(routePath);\n if (!endpoint) {\n return addCorsHeaders(jsonResponse({ ok: false, error: 'not_found', message: `Command not found: ${routePath || '/'}` }, 404));\n }\n\n // Enforce method based on mutation flag\n if (endpoint.command.mutation && req.method === 'GET') {\n return addCorsHeaders(\n new Response(JSON.stringify({ ok: false, error: 'method_not_allowed', message: 'Mutation commands only accept POST' }), {\n status: 405,\n headers: { 'Content-Type': 'application/json', Allow: 'POST' },\n }),\n );\n }\n\n if (req.method !== 'GET' && req.method !== 'POST') {\n return addCorsHeaders(new Response(null, { status: 405, headers: { Allow: endpoint.command.mutation ? 'POST' : 'GET, POST' } }));\n }\n\n // Build command string from request\n const commandPath = toCommandPath(routePath);\n let argParts: string[];\n\n if (req.method === 'POST') {\n try {\n const body = (await req.json()) as Record<string, unknown>;\n argParts = serializeArgsToFlags(body);\n } catch {\n return addCorsHeaders(jsonResponse({ ok: false, error: 'bad_request', message: 'Invalid JSON body' }, 400));\n }\n } else {\n // GET: query string → flags\n argParts = [];\n for (const [key, value] of url.searchParams.entries()) {\n if (key === '_') {\n // Positional args\n argParts.push(value);\n } else {\n argParts.push(value === '' ? `--${key}` : `--${key}=${value}`);\n }\n }\n }\n\n const commandString = [commandPath, ...argParts].filter(Boolean).join(' ');\n const response = await evalAndRespond(commandString, req);\n return addCorsHeaders(response);\n };\n}\n\n/** Start the serve HTTP server. */\nexport async function startServeServer(\n _program: AnyPadroneProgram,\n existingCommand: AnyPadroneCommand,\n evalCommand: AnyPadroneProgram['eval'],\n prefs?: PadroneServePreferences,\n): Promise<void> {\n const handler = createServeHandler(existingCommand, evalCommand, prefs);\n const http = await import('node:http');\n\n const port = prefs?.port ?? 3000;\n const host = prefs?.host ?? '127.0.0.1';\n const basePath = (prefs?.basePath ?? '/').replace(/\\/$/, '/');\n\n const server = http.createServer(async (req, res) => {\n const url = `http://${host}:${port}${req.url}`;\n const headers = new Headers();\n for (const [key, value] of Object.entries(req.headers)) {\n if (value) headers.set(key, Array.isArray(value) ? value.join(', ') : value);\n }\n\n const fetchReq = new Request(url, {\n method: req.method,\n headers,\n body: req.method !== 'GET' && req.method !== 'HEAD' ? await readBody(req) : undefined,\n });\n\n const response = await handler(fetchReq);\n const resHeaders: Record<string, string> = {};\n response.headers.forEach((v, k) => {\n resHeaders[k] = v;\n });\n res.writeHead(response.status, resHeaders);\n const body = await response.text();\n res.end(body);\n });\n\n const { getCommandRuntime } = await import('../core/commands.ts');\n const runtime = getCommandRuntime(existingCommand);\n\n return new Promise<void>((resolve, reject) => {\n server.listen(port, host, () => {\n runtime.error(`REST server listening on http://${host}:${port}${basePath}`);\n const builtins = { health: true, help: true, schema: true, docs: true, ...prefs?.builtins };\n if (builtins.docs) runtime.error(`API docs: http://${host}:${port}${basePath}_docs`);\n });\n server.on('error', reject);\n const unsubscribe = runtime.onSignal?.(() => {\n server.close(() => resolve());\n });\n server.on('close', () => unsubscribe?.());\n });\n}\n\n/** Read the full body from a Node.js IncomingMessage. */\nasync function readBody(req: import('node:http').IncomingMessage): Promise<string> {\n return readStreamAsText(req as AsyncIterable<Uint8Array>);\n}\n"],"mappings":"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"}
|
package/dist/test.d.mts
CHANGED
package/dist/zod.d.mts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "padrone",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "Create type-safe, interactive CLI apps with Zod schemas",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -106,10 +106,10 @@
|
|
|
106
106
|
},
|
|
107
107
|
"devDependencies": {
|
|
108
108
|
"@types/react": "^19.2.14",
|
|
109
|
-
"ai": "^6.0.
|
|
109
|
+
"ai": "^6.0.146",
|
|
110
110
|
"ink": "^6.8.0",
|
|
111
111
|
"react": "^19.2.4",
|
|
112
|
-
"tsdown": "^0.21.
|
|
112
|
+
"tsdown": "^0.21.7",
|
|
113
113
|
"zod": "^4.3.6"
|
|
114
114
|
},
|
|
115
115
|
"peerDependencies": {
|
package/src/core/args.ts
CHANGED
|
@@ -63,6 +63,10 @@ interface SchemaMetadataResult {
|
|
|
63
63
|
flags: Record<string, string>;
|
|
64
64
|
/** Multi-char aliases: maps alias → full arg name (e.g. `{ 'dry-run': 'dryRun' }`) */
|
|
65
65
|
aliases: Record<string, string>;
|
|
66
|
+
/** Negative keywords: maps keyword → target arg name (e.g. `{ remote: 'local' }`) */
|
|
67
|
+
negatives: Record<string, string>;
|
|
68
|
+
/** Args that have custom negation set (even if empty), disabling the `--no-` prefix */
|
|
69
|
+
customNegation: Set<string>;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
function addEntries(target: Record<string, string>, key: string, items: string | readonly string[], filter?: (item: string) => boolean) {
|
|
@@ -86,6 +90,8 @@ export function extractSchemaMetadata(
|
|
|
86
90
|
): SchemaMetadataResult {
|
|
87
91
|
const flags: Record<string, string> = {};
|
|
88
92
|
const aliases: Record<string, string> = {};
|
|
93
|
+
const negatives: Record<string, string> = {};
|
|
94
|
+
const customNegation = new Set<string>();
|
|
89
95
|
|
|
90
96
|
// Extract from meta object
|
|
91
97
|
if (meta) {
|
|
@@ -98,6 +104,10 @@ export function extractSchemaMetadata(
|
|
|
98
104
|
if (value.alias) {
|
|
99
105
|
addEntries(aliases, key, value.alias, (item) => item.length > 1);
|
|
100
106
|
}
|
|
107
|
+
if (value.negative !== undefined) {
|
|
108
|
+
customNegation.add(key);
|
|
109
|
+
addEntries(negatives, key, value.negative);
|
|
110
|
+
}
|
|
101
111
|
}
|
|
102
112
|
}
|
|
103
113
|
|
|
@@ -123,6 +133,16 @@ export function extractSchemaMetadata(
|
|
|
123
133
|
}
|
|
124
134
|
}
|
|
125
135
|
|
|
136
|
+
// Extract negative keywords from schema `.meta({ negative: ... })`
|
|
137
|
+
const propNegative = propertySchema.negative;
|
|
138
|
+
if (propNegative !== undefined && !customNegation.has(propertyName)) {
|
|
139
|
+
customNegation.add(propertyName);
|
|
140
|
+
const list = typeof propNegative === 'string' ? [propNegative] : propNegative;
|
|
141
|
+
if (Array.isArray(list)) {
|
|
142
|
+
addEntries(negatives, propertyName, list);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
126
146
|
// Auto-generate kebab-case alias for camelCase property names
|
|
127
147
|
if (autoAlias !== false) {
|
|
128
148
|
const kebab = camelToKebab(propertyName);
|
|
@@ -136,7 +156,7 @@ export function extractSchemaMetadata(
|
|
|
136
156
|
// Ignore errors from JSON schema generation
|
|
137
157
|
}
|
|
138
158
|
|
|
139
|
-
return { flags, aliases };
|
|
159
|
+
return { flags, aliases, negatives, customNegation };
|
|
140
160
|
}
|
|
141
161
|
|
|
142
162
|
function preprocessMappings(data: Record<string, unknown>, mappings: Record<string, string>): Record<string, unknown> {
|
|
@@ -262,6 +282,7 @@ export function detectUnknownArgs(
|
|
|
262
282
|
schema: StandardJSONSchemaV1,
|
|
263
283
|
flags: Record<string, string>,
|
|
264
284
|
aliases: Record<string, string>,
|
|
285
|
+
negatives?: Record<string, string>,
|
|
265
286
|
): { key: string }[] {
|
|
266
287
|
let properties: Record<string, any>;
|
|
267
288
|
let isLoose = false;
|
|
@@ -283,6 +304,8 @@ export function detectUnknownArgs(
|
|
|
283
304
|
...Object.values(flags),
|
|
284
305
|
...Object.keys(aliases),
|
|
285
306
|
...Object.values(aliases),
|
|
307
|
+
...(negatives ? Object.keys(negatives) : []),
|
|
308
|
+
...(negatives ? Object.values(negatives) : []),
|
|
286
309
|
]);
|
|
287
310
|
const unknowns: { key: string }[] = [];
|
|
288
311
|
|
package/src/core/create.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
AnyPadroneCommand,
|
|
15
15
|
AnyPadroneProgram,
|
|
16
16
|
CommandTypesBase,
|
|
17
|
+
DefineCommandBuilder,
|
|
18
|
+
DefineCommandContext,
|
|
17
19
|
InterceptorFactory,
|
|
18
20
|
InterceptorMeta,
|
|
19
21
|
PadroneBuilder,
|
|
@@ -158,7 +160,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
158
160
|
const handler = createWrapHandler(config, existingCommand.argsSchema as any, existingCommand.meta?.positional);
|
|
159
161
|
return createPadroneBuilder({ ...existingCommand, action: handler }) as any;
|
|
160
162
|
},
|
|
161
|
-
command(nameOrNames, builderFn) {
|
|
163
|
+
command(nameOrNames: string | readonly string[], builderFn?: (builder: any) => any) {
|
|
162
164
|
const name = Array.isArray(nameOrNames) ? nameOrNames[0] : nameOrNames;
|
|
163
165
|
const aliases = Array.isArray(nameOrNames) && nameOrNames.length > 1 ? (nameOrNames.slice(1) as string[]) : undefined;
|
|
164
166
|
|
|
@@ -274,28 +276,33 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
274
276
|
* Use this when defining commands in separate files — the parent program retains exact type information
|
|
275
277
|
* about the subcommand's args, result, and nested commands.
|
|
276
278
|
*
|
|
277
|
-
*
|
|
279
|
+
* The builder's context includes `DefineCommandContext` by default (optional `logger`, `tracing`, `progress`).
|
|
280
|
+
* Override globally via module augmentation on `DefineCommandContext`, or per-command via `.requires()`.
|
|
281
|
+
*
|
|
282
|
+
* @example Direct form (most common)
|
|
278
283
|
* ```ts
|
|
279
|
-
* // my-command.ts
|
|
280
284
|
* export const myCommand = defineCommand((c) =>
|
|
281
285
|
* c.arguments(z.object({ name: z.string() }))
|
|
282
286
|
* .action((args) => console.log(args.name))
|
|
283
287
|
* );
|
|
284
|
-
*
|
|
285
|
-
* // cli.ts
|
|
286
|
-
* createPadrone('test').command('my-command', myCommand)
|
|
287
288
|
* ```
|
|
288
289
|
*
|
|
289
|
-
* @example With context
|
|
290
|
+
* @example With required interceptor context
|
|
290
291
|
* ```ts
|
|
291
|
-
* export const
|
|
292
|
-
*
|
|
293
|
-
*
|
|
294
|
-
* );
|
|
292
|
+
* export const adminCommand = defineCommand()
|
|
293
|
+
* .requires<{ adminDb: AdminDB }>()
|
|
294
|
+
* .define((c) => c.action((_args, ctx) => ctx.context.adminDb.query(...)));
|
|
295
295
|
* ```
|
|
296
296
|
*/
|
|
297
297
|
export function defineCommand<TContext = unknown, TOut extends CommandTypesBase = CommandTypesBase>(
|
|
298
|
-
fn: (builder: PadroneBuilder<string, string, string, PadroneSchema<void>, void, [], any, false, TContext>) => TOut,
|
|
299
|
-
): typeof fn
|
|
300
|
-
|
|
298
|
+
fn: (builder: PadroneBuilder<string, string, string, PadroneSchema<void>, void, [], any, false, TContext, DefineCommandContext>) => TOut,
|
|
299
|
+
): typeof fn;
|
|
300
|
+
export function defineCommand(): DefineCommandBuilder;
|
|
301
|
+
export function defineCommand(fn?: any): any {
|
|
302
|
+
if (fn) return fn;
|
|
303
|
+
const builder: DefineCommandBuilder = {
|
|
304
|
+
requires: () => builder as any,
|
|
305
|
+
define: (f: any) => f,
|
|
306
|
+
};
|
|
307
|
+
return builder;
|
|
301
308
|
}
|
package/src/core/exec.ts
CHANGED
|
@@ -159,7 +159,7 @@ export function execCommand(
|
|
|
159
159
|
input: resolvedInput,
|
|
160
160
|
command: rootCommand,
|
|
161
161
|
signal,
|
|
162
|
-
context: pipelineContext,
|
|
162
|
+
context: pipelineContext as object,
|
|
163
163
|
runtime,
|
|
164
164
|
program: ctx.builder,
|
|
165
165
|
caller,
|
|
@@ -184,12 +184,13 @@ export function execCommand(
|
|
|
184
184
|
command,
|
|
185
185
|
rawArgs: parsed.rawArgs,
|
|
186
186
|
positionalArgs: parsed.positionalArgs,
|
|
187
|
-
context,
|
|
187
|
+
context: context as object,
|
|
188
188
|
evalInteractive: evalOptions?.interactive,
|
|
189
189
|
};
|
|
190
190
|
|
|
191
191
|
const coreValidate = (validateCtx: InterceptorValidateContext): InterceptorValidateResult | Promise<InterceptorValidateResult> => {
|
|
192
|
-
const preprocessedArgs = buildCommandArgs(validateCtx.command, validateCtx.rawArgs, validateCtx.positionalArgs);
|
|
192
|
+
const { args: preprocessedArgs, issues } = buildCommandArgs(validateCtx.command, validateCtx.rawArgs, validateCtx.positionalArgs);
|
|
193
|
+
if (issues) return { args: undefined, argsResult: { issues } as any };
|
|
193
194
|
const validated = validateCommandArgs(validateCtx.command, preprocessedArgs);
|
|
194
195
|
return thenMaybe(validated, (v) => v as InterceptorValidateResult);
|
|
195
196
|
};
|
package/src/core/interceptors.ts
CHANGED
|
@@ -173,9 +173,15 @@ export function runInterceptorChain<TCtx extends object, TResult>(
|
|
|
173
173
|
) => TResult | Promise<TResult>;
|
|
174
174
|
const prevNext = next;
|
|
175
175
|
next = (currentCtx: TCtx) =>
|
|
176
|
-
handler(currentCtx, (overrides?: Record<string, unknown>) =>
|
|
177
|
-
|
|
178
|
-
|
|
176
|
+
handler(currentCtx, (overrides?: Record<string, unknown>) => {
|
|
177
|
+
if (!overrides) return prevNext(currentCtx);
|
|
178
|
+
// Auto-merge context: `next({ context: { user } })` merges into existing context
|
|
179
|
+
// instead of replacing it, so interceptors can't accidentally drop context.
|
|
180
|
+
if (overrides.context != null && typeof overrides.context === 'object') {
|
|
181
|
+
overrides = { ...overrides, context: Object.assign({}, (currentCtx as Record<string, unknown>).context, overrides.context) };
|
|
182
|
+
}
|
|
183
|
+
return prevNext(Object.assign({}, currentCtx, overrides) as TCtx);
|
|
184
|
+
});
|
|
179
185
|
}
|
|
180
186
|
|
|
181
187
|
return next(ctx);
|
|
@@ -220,7 +226,7 @@ export function wrapWithLifecycle<T>(
|
|
|
220
226
|
error,
|
|
221
227
|
result,
|
|
222
228
|
signal: effectiveSignal,
|
|
223
|
-
context: effectiveContext,
|
|
229
|
+
context: effectiveContext as object,
|
|
224
230
|
runtime: runtime!,
|
|
225
231
|
program: program!,
|
|
226
232
|
caller,
|
|
@@ -243,7 +249,7 @@ export function wrapWithLifecycle<T>(
|
|
|
243
249
|
input,
|
|
244
250
|
error,
|
|
245
251
|
signal: effectiveSignal,
|
|
246
|
-
context: effectiveContext,
|
|
252
|
+
context: effectiveContext as object,
|
|
247
253
|
runtime: runtime!,
|
|
248
254
|
program: program!,
|
|
249
255
|
caller,
|
|
@@ -272,7 +278,7 @@ export function wrapWithLifecycle<T>(
|
|
|
272
278
|
const startCtx: InterceptorStartContext = {
|
|
273
279
|
command,
|
|
274
280
|
signal: effectiveSignal,
|
|
275
|
-
context: effectiveContext,
|
|
281
|
+
context: effectiveContext as object,
|
|
276
282
|
runtime: runtime!,
|
|
277
283
|
program: program!,
|
|
278
284
|
input,
|
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
PadroneAPI,
|
|
12
12
|
PadroneReplPreferences,
|
|
13
13
|
} from '../types/index.ts';
|
|
14
|
-
import { parsePositionalConfig } from './args.ts';
|
|
14
|
+
import { extractSchemaMetadata, parsePositionalConfig } from './args.ts';
|
|
15
15
|
import { findCommandByName, getCommandRuntime, resolveAllCommands } from './commands.ts';
|
|
16
16
|
import { RoutingError } from './errors.ts';
|
|
17
17
|
import type { ExecContext } from './exec.ts';
|
|
@@ -37,6 +37,15 @@ export function createProgramMethods(ctx: ExecContext, evalCommand: AnyPadronePr
|
|
|
37
37
|
const positionalConfig = commandObj.meta?.positional ? parsePositionalConfig(commandObj.meta.positional) : [];
|
|
38
38
|
const positionalNames = new Set(positionalConfig.map((p) => p.name));
|
|
39
39
|
|
|
40
|
+
// Build reverse map: arg name → first negative keyword (for stringify)
|
|
41
|
+
const negativeKeyword: Record<string, string> = {};
|
|
42
|
+
if (commandObj.argsSchema) {
|
|
43
|
+
const { negatives } = extractSchemaMetadata(commandObj.argsSchema, commandObj.meta?.fields, commandObj.meta?.autoAlias);
|
|
44
|
+
for (const [keyword, argName] of Object.entries(negatives)) {
|
|
45
|
+
if (!(argName in negativeKeyword)) negativeKeyword[argName] = keyword;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
40
49
|
if (args && typeof args === 'object') {
|
|
41
50
|
for (const { name, variadic } of positionalConfig) {
|
|
42
51
|
const value = (args as Record<string, unknown>)[name];
|
|
@@ -60,6 +69,7 @@ export function createProgramMethods(ctx: ExecContext, evalCommand: AnyPadronePr
|
|
|
60
69
|
|
|
61
70
|
if (typeof value === 'boolean') {
|
|
62
71
|
if (value) parts.push(`--${key}`);
|
|
72
|
+
else if (negativeKeyword[key]) parts.push(`--${negativeKeyword[key]}`);
|
|
63
73
|
else parts.push(`--no-${key}`);
|
|
64
74
|
} else if (Array.isArray(value)) {
|
|
65
75
|
for (const v of value) {
|
|
@@ -117,7 +127,7 @@ export function createProgramMethods(ctx: ExecContext, evalCommand: AnyPadronePr
|
|
|
117
127
|
positionalArgs: [],
|
|
118
128
|
args,
|
|
119
129
|
signal: inertSignal,
|
|
120
|
-
context: resolvedCtx,
|
|
130
|
+
context: resolvedCtx as object,
|
|
121
131
|
runtime: commandRuntime,
|
|
122
132
|
program: ctx.builder as any,
|
|
123
133
|
caller: 'run',
|