padrone 1.8.1 → 1.9.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 +13 -1
- package/dist/{args-DrCXxXeP.mjs → args-WmyGc59s.mjs} +45 -33
- package/dist/{args-DrCXxXeP.mjs.map → args-WmyGc59s.mjs.map} +1 -1
- package/dist/codegen/index.mjs +1 -1
- package/dist/{commands-DLR0rFgq.mjs → commands-ohEApqIw.mjs} +2 -2
- package/dist/commands-ohEApqIw.mjs.map +1 -0
- package/dist/{completion-UnBKfGuk.mjs → completion-D8qkAinX.mjs} +2 -2
- package/dist/{completion-UnBKfGuk.mjs.map → completion-D8qkAinX.mjs.map} +1 -1
- package/dist/docs/index.mjs +2 -2
- package/dist/{help-B-ZMYyn-.mjs → help-CeI45CJx.mjs} +3 -3
- package/dist/{help-B-ZMYyn-.mjs.map → help-CeI45CJx.mjs.map} +1 -1
- package/dist/{index-Guyz-CBm.d.mts → index-C3Ed1LYN.d.mts} +17 -8
- package/dist/index-C3Ed1LYN.d.mts.map +1 -0
- package/dist/index.d.mts +17 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +99 -41
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-D6PdtjIs.mjs → mcp-wCoVFTXz.mjs} +4 -4
- package/dist/{mcp-D6PdtjIs.mjs.map → mcp-wCoVFTXz.mjs.map} +1 -1
- package/dist/{serve-PaCLsNoD.mjs → serve-ICZFl3xr.mjs} +4 -4
- package/dist/{serve-PaCLsNoD.mjs.map → serve-ICZFl3xr.mjs.map} +1 -1
- package/dist/test.d.mts +1 -1
- package/dist/zod.d.mts +1 -1
- package/package.json +1 -1
- package/src/cli/link.ts +51 -19
- package/src/core/args.ts +63 -37
- package/src/core/exec.ts +8 -1
- package/src/core/interceptors.ts +7 -4
- package/src/core/runtime.ts +11 -2
- package/src/extension/auto-output.ts +28 -3
- package/src/extension/progress-renderer.ts +32 -8
- package/src/extension/progress.ts +19 -9
- package/src/index.ts +1 -1
- package/src/types/builder.ts +2 -2
- package/src/types/index.ts +1 -0
- package/src/types/interceptor.ts +7 -0
- package/dist/commands-DLR0rFgq.mjs.map +0 -1
- package/dist/index-Guyz-CBm.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-ohEApqIw.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-CeI45CJx.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-ohEApqIw.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-wCoVFTXz.mjs.map
|
|
@@ -1 +1 @@
|
|
|
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
|
+
{"version":3,"file":"mcp-wCoVFTXz.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":";;;;AA0BA,MAAM,mBAAmB;;AAiBzB,SAAS,WAAW,MAAsB;AACxC,QAAO,KAAK,QAAQ,QAAQ,IAAI;;;AAIlC,SAAS,cAAc,UAA0B;AAC/C,QAAO,SAAS,QAAQ,OAAO,IAAI;;;AAIrC,SAAS,iBAAiB,KAAwB;AAChD,KAAI,IAAI,YAAY,KAAM,QAAO,KAAA;AACjC,QAAO;EACL,iBAAiB,IAAI,YAAY,KAAA;EACjC,cAAc,IAAI,aAAa,SAAS,KAAA;EACzC;;;AAIH,SAAS,oBAAoB,MAAc,KAAwB;AACjE,QAAO;EACL,MAAM,WAAW,KAAK;EACtB,OAAO,IAAI,SAAS,KAAA;EACpB,aAAa,IAAI,eAAe,IAAI,SAAS,YAAY,KAAK;EAC9D,aAAa,iBAAiB,IAAI;EAClC,aAAa,iBAAiB,IAAI;EACnC;;;AAIH,SAAgB,iBACd,iBACA,aACA,OACA;CACA,MAAM,aAAa,OAAO,QAAQ,gBAAgB;CAClD,MAAM,gBAAgB,OAAO,WAAW,gBAAgB,WAAW;CAEnE,MAAM,YAAY,iBAAiB,gBAAgB,UAAU,GAAG;AAChE,KAAI,gBAAgB,UAAU,gBAAgB,WAC5C,WAAU,QAAQ;EAAE,MAAM;EAAI,SAAS;EAAiB,CAAC;CAG3D,MAAM,UAAU,IAAI,IAAI,UAAU,KAAK,MAAM,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC;CAEtE,MAAM,eAAe;CACrB,MAAM,cAAc;EAClB,MAAM;EACN,OAAO;EACP,aAAa,sBAAsB,WAAW;EAC9C,aAAa;GACX,MAAM;GACN,YAAY,EAAE,SAAS;IAAE,MAAM;IAAU,aAAa;IAAwD,EAAE;GAChH,sBAAsB;GACvB;EACF;AAED,QAAO,eAAe,cAAc,KAA2D;EAC7F,MAAM,EAAE,IAAI,QAAQ,WAAW;AAE/B,UAAQ,QAAR;GACE,KAAK,aACH,QAAO;IACL,SAAS;IACT,IAAI,MAAM;IACV,QAAQ;KACN,iBAAiB;KACjB,cAAc,EAAE,OAAO,EAAE,EAAE;KAC3B,YAAY;MAAE,MAAM;MAAY,SAAS;MAAe;KACzD;IACF;GAEH,KAAK;GACL,KAAK,0BACH;GAEF,KAAK,OACH,QAAO;IAAE,SAAS;IAAO,IAAI,MAAM;IAAM,QAAQ,EAAE;IAAE;GAEvD,KAAK,cAAc;IACjB,MAAM,QAAQ,CAAC,GAAG,UAAU,KAAK,MAAM,oBAAoB,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,YAAY;AAC5F,WAAO;KAAE,SAAS;KAAO,IAAI,MAAM;KAAM,QAAQ,EAAE,OAAO;KAAE;;GAG9D,KAAK,cAAc;IACjB,MAAM,WAAW,QAAQ;IACzB,MAAM,OAAQ,QAAQ,aAAa,EAAE;AAGrC,QAAI,aAAa,cAAc;KAC7B,MAAM,UAAU,KAAK;KAErB,MAAM,WAAW,aAAa,kBADZ,UAAU,UAAU,MAAM,MAAM,EAAE,SAAS,WAAW,WAAW,EAAE,KAAK,KAAK,QAAQ,EAAE,UAAU,KAAA,MACvD,iBAAiB;MAAE,QAAQ;MAAQ,QAAQ;MAAQ,CAAC;AAChH,YAAO;MACL,SAAS;MACT,IAAI,MAAM;MACV,QAAQ;OAAE,SAAS,CAAC;QAAE,MAAM;QAAQ,MAAM;QAAU,CAAC;OAAE,SAAS;OAAO;MACxE;;IAGH,MAAM,OAAO,QAAQ,IAAI,SAAS;AAClC,QAAI,CAAC,KACH,QAAO;KACL,SAAS;KACT,IAAI,MAAM;KACV,OAAO;MAAE,MAAM;MAAQ,SAAS,iBAAiB;MAAY;KAC9D;IAMH,MAAM,QAAQ,CAFM,cAAc,KAAK,KAAK,EAEhB,GADX,qBAAqB,KAAK,CACH,CAAC,OAAO,QAAQ,CAAC,KAAK,IAAI,IAAI,KAAA;AAEtE,QAAI;KACF,MAAM,SAAmB,EAAE;KAC3B,MAAM,SAAmB,EAAE;KAC3B,MAAM,SAAS,MAAM,YAAY,OAAc;MAC7C,QAAQ;MACR,SAAS;OACP,SAAS,GAAG,YAAuB,OAAO,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,IAAI,CAAC;OAC7E,QAAQ,SAAiB,OAAO,KAAK,KAAK;OAC1C,aAAa;OACb,QAAQ;OACT;MACF,CAAC;KAEF,MAAM,UAA4C,EAAE;AAEpD,SAAI,OAAO,OAAO;MAChB,MAAM,WAAW,OAAO,iBAAiB,QAAQ,OAAO,MAAM,UAAU,OAAO,OAAO,MAAM;AAC5F,UAAI,OAAO,OAAQ,SAAQ,KAAK;OAAE,MAAM;OAAQ,MAAM,OAAO,KAAK,KAAK;OAAE,CAAC;AAC1E,cAAQ,KAAK;OAAE,MAAM;OAAQ,MAAM;OAAU,CAAC;AAC9C,aAAO;OAAE,SAAS;OAAO,IAAI,MAAM;OAAM,QAAQ;QAAE;QAAS,SAAS;QAAM;OAAE;;AAG/E,SAAI,OAAO,YAAY,QAAQ;MAC7B,MAAM,gBAAgB,OAAO,WAAW,OAAO,KAAK,MAAW,GAAG,EAAE,MAAM,KAAK,IAAI,IAAI,OAAO,IAAI,EAAE,UAAU,CAAC,KAAK,KAAK;AACzH,cAAQ,KAAK;OAAE,MAAM;OAAQ,MAAM,sBAAsB;OAAiB,CAAC;AAC3E,aAAO;OAAE,SAAS;OAAO,IAAI,MAAM;OAAM,QAAQ;QAAE;QAAS,SAAS;QAAM;OAAE;;AAG/E,SAAI,OAAO,OAAQ,SAAQ,KAAK;MAAE,MAAM;MAAQ,MAAM,OAAO,KAAK,KAAK;MAAE,CAAC;AAC1E,SAAI,OAAO,WAAW,KAAA,KAAa,OAAO,WAAW,MAAM;MACzD,MAAM,aAAa,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS,KAAK,UAAU,OAAO,QAAQ,MAAM,EAAE;AAC7G,cAAQ,KAAK;OAAE,MAAM;OAAQ,MAAM;OAAY,CAAC;;AAElD,SAAI,QAAQ,WAAW,EAAG,SAAQ,KAAK;MAAE,MAAM;MAAQ,MAAM;MAAS,CAAC;AACvE,YAAO;MAAE,SAAS;MAAO,IAAI,MAAM;MAAM,QAAQ;OAAE;OAAS,SAAS;OAAO;MAAE;aACvE,KAAK;KACZ,MAAM,WAAW,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;AACjE,YAAO;MACL,SAAS;MACT,IAAI,MAAM;MACV,QAAQ;OAAE,SAAS,CAAC;QAAE,MAAM;QAAQ,MAAM;QAAU,CAAC;OAAE,SAAS;OAAM;MACvE;;;GAIL;AACE,QAAI,OAAO,KAAA,EACT,QAAO;KAAE,SAAS;KAAO;KAAI,OAAO;MAAE,MAAM;MAAQ,SAAS,qBAAqB;MAAU;KAAE;AAEhG;;;;;AAOR,eAAe,oBAAoB,eAA6F;CAC9H,MAAM,EAAE,OAAO,WAAW,MAAM,OAAO;CACvC,MAAM,EAAE,oBAAoB,MAAM,OAAO;CAEzC,SAAS,KAAK,KAAsB;AAClC,SAAO,MAAM,GAAG,KAAK,UAAU,IAAI,CAAC,IAAI;;CAG1C,MAAM,KAAK,gBAAgB;EAAE,OAAO;EAAO,WAAW;EAAU,CAAC;AAEjE,YAAW,MAAM,QAAQ,IAAI;AAC3B,MAAI,CAAC,KAAK,MAAM,CAAE;AAClB,MAAI;GAEF,MAAM,MAAM,MAAM,cADN,KAAK,MAAM,KAAK,CACQ;AACpC,OAAI,IAAK,MAAK,IAAI;UACZ;;;;AAOZ,eAAe,mBACb,eACA,OACA,KACA,UACe;CACf,MAAM,OAAO,MAAM,OAAO;CAC1B,MAAM,SAAS,MAAM,OAAO;CAE5B,MAAM,OAAO,MAAM,QAAQ;CAC3B,MAAM,OAAO,MAAM,QAAQ;CAC3B,MAAM,WAAW,MAAM,YAAY;CAGnC,IAAI;CACJ,IAAI;CAEJ,MAAM,aAAa,MAAM,SAAS,QAAS,MAAM,QAAQ,MAAO,KAAA;CAEhE,MAAM,SAAS,KAAK,aAAa,OAAO,KAAK,QAAQ;AAEnD,MAAI,YAAY;AACd,OAAI,UAAU,+BAA+B,WAAW;AACxD,OAAI,UAAU,gCAAgC,6BAA6B;AAC3E,OAAI,UAAU,gCAAgC,qDAAqD;AACnG,OAAI,UAAU,iCAAiC,iBAAiB;;AAGlE,MAAI,IAAI,WAAW,WAAW;AAC5B,OAAI,UAAU,aAAa,MAAM,IAAI;AACrC,OAAI,KAAK;AACT;;AAGF,MAAI,IAAI,QAAQ,UAAU;AACxB,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,EAAE,OAAO,aAAa,CAAC,CAAC;AAC/C;;AAIF,MAAI,IAAI,WAAW,UAAU;GAC3B,MAAM,eAAe,IAAI,QAAQ;AACjC,OAAI,aAAa,iBAAiB,WAAW;AAC3C,gBAAY,KAAA;AACZ,wBAAoB,KAAA;AACpB,QAAI,UAAU,IAAI;AAClB,QAAI,KAAK;UACJ;AACL,QAAI,UAAU,IAAI;AAClB,QAAI,KAAK;;AAEX;;AAIF,MAAI,IAAI,WAAW,OAAO;AACxB,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU;IAAE,SAAS;IAAO,IAAI;IAAM,OAAO;KAAE,MAAM;KAAQ,SAAS;KAA4B;IAAE,CAAC,CAAC;AACnH;;AAGF,MAAI,IAAI,WAAW,QAAQ;AACzB,OAAI,UAAU,IAAI;AAClB,OAAI,KAAK;AACT;;EAIF,MAAM,eAAe,IAAI,QAAQ;AACjC,MAAI,aAAa,gBAAgB,iBAAiB,WAAW;AAC3D,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU;IAAE,SAAS;IAAO,IAAI;IAAM,OAAO;KAAE,MAAM;KAAQ,SAAS;KAAmB;IAAE,CAAC,CAAC;AAC1G;;EAIF,MAAM,qBAAqB,IAAI,QAAQ;AACvC,MAAI,qBAAqB,sBAAsB,uBAAuB,mBAAmB;AACvF,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU;IAAE,SAAS;IAAO,IAAI;IAAM,OAAO;KAAE,MAAM;KAAQ,SAAS;KAA6B;IAAE,CAAC,CAAC;AACpH;;EAIF,MAAM,OAAO,MAAM,iBAAiB,IAAiC;EAErE,IAAI;AACJ,MAAI;AACF,gBAAa,KAAK,MAAM,KAAK;UACvB;AACN,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU;IAAE,SAAS;IAAO,IAAI;IAAM,OAAO;KAAE,MAAM;KAAQ,SAAS;KAAe;IAAE,CAAC,CAAC;AACtG;;EAGF,MAAM,WAAW,MAAM,cAAc,WAAW;AAGhD,MAAI,WAAW,WAAW,gBAAgB,UAAU,QAAQ;AAC1D,eAAY,OAAO,YAAY;AAC/B,uBAAoB;AACpB,OAAI,UAAU,kBAAkB,UAAU;;AAG5C,MAAI,SAEF,MADe,IAAI,QAAQ,UAAU,IAC1B,SAAS,oBAAoB,EAAE;AACxC,OAAI,UAAU,KAAK;IAAE,gBAAgB;IAAqB,iBAAiB;IAAY,YAAY;IAAc,CAAC;AAClH,OAAI,MAAM,yBAAyB,KAAK,UAAU,SAAS,CAAC,MAAM;AAClE,OAAI,KAAK;SACJ;AACL,OAAI,UAAU,KAAK,EAAE,gBAAgB,oBAAoB,CAAC;AAC1D,OAAI,IAAI,KAAK,UAAU,SAAS,CAAC;;OAE9B;AAEL,OAAI,UAAU,IAAI;AAClB,OAAI,KAAK;;GAEX;AAEF,QAAO,IAAI,SAAe,SAAS,WAAW;AAC5C,SAAO,OAAO,MAAM,YAAY;AAC9B,OAAI,kCAAkC,KAAK,GAAG,OAAO,WAAW;IAChE;AACF,SAAO,GAAG,SAAS,OAAO;EAC1B,MAAM,cAAc,iBAAiB;AACnC,UAAO,YAAY,SAAS,CAAC;IAC7B;AACF,SAAO,GAAG,eAAe,eAAe,CAAC;GACzC;;AAGJ,eAAsB,eACpB,UACA,iBACA,aACA,OACe;CACf,MAAM,gBAAgB,iBAAiB,iBAAiB,aAAa,MAAM;AAG3E,MAFkB,OAAO,aAAa,YAEpB,QAChB,QAAO,oBAAoB,cAAc;CAG3C,MAAM,EAAE,sBAAsB,MAAM,OAAO,2BAAA,MAAA,MAAA,EAAA,EAAA;CAC3C,MAAM,UAAU,kBAAkB,gBAAgB;AAClD,QAAO,mBAAmB,eAAe,SAAS,EAAE,GAAG,QAAQ,QAAQ,MAAM,IAAI,EAAE,QAAQ,SAAS"}
|
|
@@ -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-ohEApqIw.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-CeI45CJx.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-ohEApqIw.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-ICZFl3xr.mjs.map
|
|
@@ -1 +1 @@
|
|
|
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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"}
|
|
1
|
+
{"version":3,"file":"serve-ICZFl3xr.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
package/src/cli/link.ts
CHANGED
|
@@ -8,6 +8,8 @@ import { detectShell, getRcFile, type ShellType, writeToRcFile } from '../util/s
|
|
|
8
8
|
export const linkSchema = z.object({
|
|
9
9
|
entry: z.string().optional().describe('Entry file (auto-detected from package.json bin field)'),
|
|
10
10
|
name: z.string().optional().describe('Command name (auto-detected from package.json)'),
|
|
11
|
+
script: z.string().optional().describe('Use a package.json script instead of bin entry (e.g. "start", "dev")'),
|
|
12
|
+
pm: z.enum(['bun', 'npm', 'pnpm', 'yarn']).optional().describe('Package manager to use (auto-detected from lockfile)'),
|
|
11
13
|
list: z.boolean().optional().default(false).describe('List all linked programs'),
|
|
12
14
|
setup: z.boolean().optional().default(false).describe('Add ~/.padrone/bin to PATH in shell config'),
|
|
13
15
|
});
|
|
@@ -58,6 +60,8 @@ export interface DetectedEntry {
|
|
|
58
60
|
name: string;
|
|
59
61
|
/** Full run command prefix parsed from scripts (e.g. "bun --conditions=padrone@dev") */
|
|
60
62
|
runPrefix?: string;
|
|
63
|
+
/** When set, the shim should run this script via the package manager instead of the entry directly */
|
|
64
|
+
scriptCommand?: string;
|
|
61
65
|
}
|
|
62
66
|
|
|
63
67
|
function parseRunPrefix(script: string, entryRelative: string, dir: string): string | undefined {
|
|
@@ -74,12 +78,21 @@ function parseRunPrefix(script: string, entryRelative: string, dir: string): str
|
|
|
74
78
|
return undefined;
|
|
75
79
|
}
|
|
76
80
|
|
|
77
|
-
export function detectEntry(dir: string): DetectedEntry | undefined {
|
|
81
|
+
export function detectEntry(dir: string, options?: { script?: string }): DetectedEntry | undefined {
|
|
78
82
|
const pkgPath = resolve(dir, 'package.json');
|
|
79
83
|
if (!existsSync(pkgPath)) return undefined;
|
|
80
84
|
|
|
81
85
|
try {
|
|
82
86
|
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
87
|
+
const scripts = pkg.scripts as Record<string, string> | undefined;
|
|
88
|
+
|
|
89
|
+
// When --script is specified, use the package.json script directly
|
|
90
|
+
if (options?.script) {
|
|
91
|
+
const scriptName = options.script;
|
|
92
|
+
if (!scripts?.[scriptName]) return undefined;
|
|
93
|
+
const name = pkg.name || basename(dir);
|
|
94
|
+
return { entry: resolve(dir, 'package.json'), name, scriptCommand: scriptName };
|
|
95
|
+
}
|
|
83
96
|
|
|
84
97
|
let entryRelative: string | undefined;
|
|
85
98
|
let name: string | undefined;
|
|
@@ -110,7 +123,6 @@ export function detectEntry(dir: string): DetectedEntry | undefined {
|
|
|
110
123
|
|
|
111
124
|
// Check start/dev scripts for runtime flags
|
|
112
125
|
let runPrefix: string | undefined;
|
|
113
|
-
const scripts = pkg.scripts as Record<string, string> | undefined;
|
|
114
126
|
if (scripts) {
|
|
115
127
|
for (const key of ['start', 'dev']) {
|
|
116
128
|
if (scripts[key]) {
|
|
@@ -157,30 +169,40 @@ async function setupPath(shell: ShellType): Promise<{ file: string; updated: boo
|
|
|
157
169
|
return writeToRcFile(rcFile, snippet, PATH_BEGIN_MARKER, PATH_END_MARKER);
|
|
158
170
|
}
|
|
159
171
|
|
|
160
|
-
function
|
|
172
|
+
function detectPackageManager(dir: string): string {
|
|
161
173
|
let current = dir;
|
|
162
174
|
while (true) {
|
|
163
175
|
if (existsSync(resolve(current, 'bun.lock')) || existsSync(resolve(current, 'bun.lockb'))) return 'bun';
|
|
164
|
-
if (
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
existsSync(resolve(current, 'pnpm-lock.yaml'))
|
|
168
|
-
)
|
|
169
|
-
return 'node';
|
|
176
|
+
if (existsSync(resolve(current, 'pnpm-lock.yaml'))) return 'pnpm';
|
|
177
|
+
if (existsSync(resolve(current, 'yarn.lock'))) return 'yarn';
|
|
178
|
+
if (existsSync(resolve(current, 'package-lock.json'))) return 'npm';
|
|
170
179
|
const parent = dirname(current);
|
|
171
180
|
if (parent === current) break;
|
|
172
181
|
current = parent;
|
|
173
182
|
}
|
|
174
|
-
return '
|
|
183
|
+
return 'npm';
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function detectRuntime(dir: string): string {
|
|
187
|
+
const pm = detectPackageManager(dir);
|
|
188
|
+
return pm === 'bun' ? 'bun' : 'node';
|
|
175
189
|
}
|
|
176
190
|
|
|
177
|
-
function createShim(name: string, entry: string, dir: string, runPrefix?: string) {
|
|
191
|
+
function createShim(name: string, entry: string, dir: string, runPrefix?: string, scriptCommand?: string, pm?: string) {
|
|
178
192
|
mkdirSync(BIN_DIR, { recursive: true });
|
|
179
193
|
const shimPath = resolve(BIN_DIR, sanitizeBinName(name));
|
|
180
194
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
195
|
+
let shim: string;
|
|
196
|
+
if (scriptCommand) {
|
|
197
|
+
const resolvedPm = pm ?? detectPackageManager(dir);
|
|
198
|
+
const cwdFlag = resolvedPm === 'npm' ? `--prefix="${dir}"` : resolvedPm === 'pnpm' ? `--dir="${dir}"` : `--cwd="${dir}"`;
|
|
199
|
+
shim = ['#!/usr/bin/env sh', `# Linked by padrone — do not edit`, `${resolvedPm} ${cwdFlag} run ${scriptCommand} -- "$@"`, ''].join(
|
|
200
|
+
'\n',
|
|
201
|
+
);
|
|
202
|
+
} else {
|
|
203
|
+
const prefix = runPrefix ?? detectRuntime(dir);
|
|
204
|
+
shim = ['#!/usr/bin/env sh', `# Linked by padrone — do not edit`, `${prefix} "${entry}" "$@"`, ''].join('\n');
|
|
205
|
+
}
|
|
184
206
|
|
|
185
207
|
writeFileSync(shimPath, shim);
|
|
186
208
|
chmodSync(shimPath, 0o755);
|
|
@@ -215,6 +237,7 @@ export async function runLink(args: LinkArgs, ctx: PadroneActionContext) {
|
|
|
215
237
|
let entry: string;
|
|
216
238
|
let name: string;
|
|
217
239
|
let runPrefix: string | undefined;
|
|
240
|
+
let scriptCommand: string | undefined;
|
|
218
241
|
|
|
219
242
|
const resolvedArg = args.entry ? resolve(dir, args.entry) : undefined;
|
|
220
243
|
|
|
@@ -230,15 +253,24 @@ export async function runLink(args: LinkArgs, ctx: PadroneActionContext) {
|
|
|
230
253
|
|
|
231
254
|
if (targetDir || !resolvedArg) {
|
|
232
255
|
// Detect entry from the target directory's package.json
|
|
233
|
-
const detected = detectEntry(targetDir ?? dir);
|
|
256
|
+
const detected = detectEntry(targetDir ?? dir, { script: args.script });
|
|
234
257
|
if (!detected) {
|
|
235
|
-
|
|
258
|
+
if (args.script) {
|
|
259
|
+
error(`Script "${args.script}" not found in package.json.`);
|
|
260
|
+
} else {
|
|
261
|
+
error('Could not detect entry point. Provide an entry file or add a "bin" field to package.json.');
|
|
262
|
+
}
|
|
236
263
|
process.exit(1);
|
|
237
264
|
}
|
|
238
265
|
entry = detected.entry;
|
|
239
266
|
name = sanitizeBinName(args.name || detected.name);
|
|
240
267
|
runPrefix = detected.runPrefix;
|
|
268
|
+
scriptCommand = detected.scriptCommand;
|
|
241
269
|
} else {
|
|
270
|
+
if (args.script) {
|
|
271
|
+
error('--script cannot be used with an explicit entry file.');
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
242
274
|
// Explicit file path
|
|
243
275
|
entry = resolvedArg;
|
|
244
276
|
name = sanitizeBinName(args.name || basename(entry).replace(/\.[cm]?[jt]sx?$/, ''));
|
|
@@ -250,14 +282,14 @@ export async function runLink(args: LinkArgs, ctx: PadroneActionContext) {
|
|
|
250
282
|
process.exit(1);
|
|
251
283
|
}
|
|
252
284
|
|
|
253
|
-
if (!existsSync(entry)) {
|
|
285
|
+
if (!scriptCommand && !existsSync(entry)) {
|
|
254
286
|
error(`Entry file not found: ${entry}`);
|
|
255
287
|
process.exit(1);
|
|
256
288
|
}
|
|
257
289
|
|
|
258
290
|
const entryDir = targetDir ?? (existsSync(resolve(dirname(entry), 'package.json')) ? dirname(entry) : dir);
|
|
259
291
|
|
|
260
|
-
createShim(name, entry, entryDir, runPrefix);
|
|
292
|
+
createShim(name, entry, entryDir, runPrefix, scriptCommand, args.pm);
|
|
261
293
|
|
|
262
294
|
const links = readLinks();
|
|
263
295
|
links[name] = {
|
|
@@ -268,7 +300,7 @@ export async function runLink(args: LinkArgs, ctx: PadroneActionContext) {
|
|
|
268
300
|
};
|
|
269
301
|
writeLinks(links);
|
|
270
302
|
|
|
271
|
-
output(`Linked ${name} → ${entry}`);
|
|
303
|
+
output(`Linked ${name} → ${scriptCommand ? `${scriptCommand} (script)` : entry}`);
|
|
272
304
|
|
|
273
305
|
if (!isInPath(BIN_DIR)) {
|
|
274
306
|
if (args.setup) {
|
package/src/core/args.ts
CHANGED
|
@@ -208,10 +208,58 @@ export function preprocessArgs(
|
|
|
208
208
|
return result;
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
/**
|
|
212
|
+
* Walk a JSON schema fragment and collect the set of allowed primitive types,
|
|
213
|
+
* descending into `anyOf` / `oneOf` (used for unions). For variants whose type
|
|
214
|
+
* is `array`, item types are collected separately into `itemTypes`.
|
|
215
|
+
*/
|
|
216
|
+
function collectAllowedTypes(prop: Record<string, any> | undefined, types: Set<string>, itemTypes: Set<string>): void {
|
|
217
|
+
if (!prop) return;
|
|
218
|
+
|
|
219
|
+
if (prop.type !== undefined) {
|
|
220
|
+
const list = Array.isArray(prop.type) ? prop.type : [prop.type];
|
|
221
|
+
for (const t of list) {
|
|
222
|
+
if (typeof t === 'string') types.add(t);
|
|
223
|
+
}
|
|
224
|
+
if (list.includes('array') && prop.items) {
|
|
225
|
+
collectAllowedTypes(prop.items, itemTypes, new Set());
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const variants = prop.anyOf ?? prop.oneOf;
|
|
230
|
+
if (Array.isArray(variants)) {
|
|
231
|
+
for (const variant of variants) collectAllowedTypes(variant, types, itemTypes);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Coerce a single CLI string to a primitive based on the set of allowed types. */
|
|
236
|
+
function coerceScalar(value: unknown, allowedTypes: Set<string>): unknown {
|
|
237
|
+
if (typeof value !== 'string') return value;
|
|
238
|
+
|
|
239
|
+
if (allowedTypes.has('boolean')) {
|
|
240
|
+
const lower = value.toLowerCase();
|
|
241
|
+
if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') return true;
|
|
242
|
+
if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (allowedTypes.has('number') || allowedTypes.has('integer')) {
|
|
246
|
+
const trimmed = value.trim();
|
|
247
|
+
if (trimmed !== '') {
|
|
248
|
+
const num = Number(trimmed);
|
|
249
|
+
if (!Number.isNaN(num)) return num;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return value;
|
|
254
|
+
}
|
|
255
|
+
|
|
211
256
|
/**
|
|
212
257
|
* Auto-coerce CLI string values to match the expected schema types.
|
|
213
258
|
* Handles: string → number, string → boolean for primitive schema fields.
|
|
214
259
|
* Arrays of primitives are also coerced element-wise.
|
|
260
|
+
* Union types (`anyOf` / `oneOf`) are coerced to the most specific matching
|
|
261
|
+
* primitive — e.g. `--test true` for `z.union([z.boolean(), z.string()])`
|
|
262
|
+
* becomes the boolean `true` rather than the string "true".
|
|
215
263
|
*/
|
|
216
264
|
export function coerceArgs(data: Record<string, unknown>, schema: StandardJSONSchemaV1): Record<string, unknown> {
|
|
217
265
|
let properties: Record<string, any>;
|
|
@@ -229,43 +277,21 @@ export function coerceArgs(data: Record<string, unknown>, schema: StandardJSONSc
|
|
|
229
277
|
const prop = properties[key];
|
|
230
278
|
if (!prop) continue;
|
|
231
279
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
} else if (
|
|
246
|
-
|
|
247
|
-
const arr = Array.isArray(value) ? value : [value];
|
|
248
|
-
const itemType = prop.items?.type as string | undefined;
|
|
249
|
-
if (itemType === 'number' || itemType === 'integer') {
|
|
250
|
-
result[key] = arr.map((v) => {
|
|
251
|
-
if (typeof v === 'string') {
|
|
252
|
-
const num = Number(v);
|
|
253
|
-
return Number.isNaN(num) ? v : num;
|
|
254
|
-
}
|
|
255
|
-
return v;
|
|
256
|
-
});
|
|
257
|
-
} else if (itemType === 'boolean') {
|
|
258
|
-
result[key] = arr.map((v) => {
|
|
259
|
-
if (typeof v === 'string') {
|
|
260
|
-
const lower = v.toLowerCase();
|
|
261
|
-
if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') return true;
|
|
262
|
-
if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') return false;
|
|
263
|
-
}
|
|
264
|
-
return v;
|
|
265
|
-
});
|
|
266
|
-
} else if (!Array.isArray(value)) {
|
|
267
|
-
result[key] = arr;
|
|
268
|
-
}
|
|
280
|
+
const types = new Set<string>();
|
|
281
|
+
const itemTypes = new Set<string>();
|
|
282
|
+
collectAllowedTypes(prop, types, itemTypes);
|
|
283
|
+
|
|
284
|
+
const isArrayValue = Array.isArray(value);
|
|
285
|
+
const allowsArray = types.has('array');
|
|
286
|
+
const allowsScalar = types.has('string') || types.has('boolean') || types.has('number') || types.has('integer');
|
|
287
|
+
|
|
288
|
+
if (isArrayValue && allowsArray) {
|
|
289
|
+
result[key] = value.map((v) => coerceScalar(v, itemTypes));
|
|
290
|
+
} else if (!isArrayValue && allowsArray && !allowsScalar) {
|
|
291
|
+
// Wrap single value into an array when only array shapes are allowed
|
|
292
|
+
result[key] = [coerceScalar(value, itemTypes)];
|
|
293
|
+
} else if (!isArrayValue) {
|
|
294
|
+
result[key] = coerceScalar(value, types);
|
|
269
295
|
}
|
|
270
296
|
}
|
|
271
297
|
|