padrone 1.4.0 → 1.6.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.
Files changed (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. package/src/utils.ts +0 -140
@@ -0,0 +1,387 @@
1
+ import { buildInputSchema, collectEndpoints, serializeArgsToFlags } from '../core/commands.ts';
2
+ import { generateHelp } from '../output/help.ts';
3
+ import type { AnyPadroneCommand, AnyPadroneProgram } from '../types/index.ts';
4
+ import { readStreamAsText } from '../util/stream.ts';
5
+
6
+ export type PadroneMcpPreferences = {
7
+ /** Server name. Defaults to the program name. */
8
+ name?: string;
9
+ /** Server version. Defaults to the program version. */
10
+ version?: string;
11
+ /**
12
+ * Transport mode.
13
+ * - `'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.
14
+ * - `'stdio'` — Communicate over stdin/stdout with newline-delimited JSON.
15
+ */
16
+ transport?: 'http' | 'stdio';
17
+ /** HTTP port. Defaults to `3000`. Only used with `transport: 'http'`. */
18
+ port?: number;
19
+ /** HTTP host. Defaults to `'127.0.0.1'`. Only used with `transport: 'http'`. */
20
+ host?: string;
21
+ /** Base path for the MCP endpoint. Defaults to `'/mcp'`. Only used with `transport: 'http'`. */
22
+ basePath?: string;
23
+ /** CORS allowed origin. Defaults to `'*'`. Set to a specific origin or `false` to disable CORS headers. Only used with HTTP transports. */
24
+ cors?: string | false;
25
+ };
26
+
27
+ const PROTOCOL_VERSION = '2025-11-25';
28
+
29
+ type JsonRpcRequest = {
30
+ jsonrpc: '2.0';
31
+ id?: string | number;
32
+ method: string;
33
+ params?: Record<string, unknown>;
34
+ };
35
+
36
+ type JsonRpcResponse = {
37
+ jsonrpc: '2.0';
38
+ id: string | number | null;
39
+ result?: unknown;
40
+ error?: { code: number; message: string; data?: unknown };
41
+ };
42
+
43
+ /** Convert an endpoint dot-path to a valid MCP tool name. Spec allows: [A-Za-z0-9_\-\.] */
44
+ function toToolName(path: string): string {
45
+ return path.replace(/\s+/g, '.');
46
+ }
47
+
48
+ /** Convert a tool name back to a command path (dot → space). */
49
+ function toCommandPath(toolName: string): string {
50
+ return toolName.replace(/\./g, ' ');
51
+ }
52
+
53
+ /** Build MCP tool annotations from a command's metadata. */
54
+ function buildAnnotations(cmd: AnyPadroneCommand) {
55
+ if (cmd.mutation == null) return undefined;
56
+ return {
57
+ destructiveHint: cmd.mutation || undefined,
58
+ readOnlyHint: cmd.mutation === false || undefined,
59
+ };
60
+ }
61
+
62
+ /** Build an MCP tool definition from a command. */
63
+ function buildToolDefinition(name: string, cmd: AnyPadroneCommand) {
64
+ return {
65
+ name: toToolName(name),
66
+ title: cmd.title ?? undefined,
67
+ description: cmd.description || cmd.title || `Run the "${name}" command`,
68
+ inputSchema: buildInputSchema(cmd),
69
+ annotations: buildAnnotations(cmd),
70
+ };
71
+ }
72
+
73
+ /** Create the MCP request handler. Returns an async function that processes a JSON-RPC request and returns a response (or undefined for notifications). */
74
+ export function createMcpHandler(
75
+ existingCommand: AnyPadroneCommand,
76
+ evalCommand: AnyPadroneProgram['eval'],
77
+ prefs?: PadroneMcpPreferences,
78
+ ) {
79
+ const serverName = prefs?.name ?? existingCommand.name;
80
+ const serverVersion = prefs?.version ?? existingCommand.version ?? '0.0.0';
81
+
82
+ const rootTools = collectEndpoints(existingCommand.commands, '');
83
+ if (existingCommand.action || existingCommand.argsSchema) {
84
+ rootTools.unshift({ name: '', command: existingCommand });
85
+ }
86
+
87
+ const toolMap = new Map(rootTools.map((t) => [toToolName(t.name), t]));
88
+
89
+ const helpToolName = 'help';
90
+ const helpToolDef = {
91
+ name: helpToolName,
92
+ title: 'Help',
93
+ description: `Show help for the "${serverName}" program or a specific command`,
94
+ inputSchema: {
95
+ type: 'object' as const,
96
+ properties: { command: { type: 'string', description: 'Command name to get help for (omit for program help)' } },
97
+ additionalProperties: false,
98
+ },
99
+ };
100
+
101
+ return async function handleRequest(req: JsonRpcRequest): Promise<JsonRpcResponse | undefined> {
102
+ const { id, method, params } = req;
103
+
104
+ switch (method) {
105
+ case 'initialize':
106
+ return {
107
+ jsonrpc: '2.0',
108
+ id: id ?? null,
109
+ result: {
110
+ protocolVersion: PROTOCOL_VERSION,
111
+ capabilities: { tools: {} },
112
+ serverInfo: { name: serverName, version: serverVersion },
113
+ },
114
+ };
115
+
116
+ case 'notifications/initialized':
117
+ case 'notifications/cancelled':
118
+ return undefined;
119
+
120
+ case 'ping':
121
+ return { jsonrpc: '2.0', id: id ?? null, result: {} };
122
+
123
+ case 'tools/list': {
124
+ const tools = [...rootTools.map((t) => buildToolDefinition(t.name, t.command)), helpToolDef];
125
+ return { jsonrpc: '2.0', id: id ?? null, result: { tools } };
126
+ }
127
+
128
+ case 'tools/call': {
129
+ const toolName = params?.name as string;
130
+ const args = (params?.arguments ?? {}) as Record<string, unknown>;
131
+
132
+ // Built-in help tool
133
+ if (toolName === helpToolName) {
134
+ const cmdName = args.command as string | undefined;
135
+ const targetCmd = cmdName ? rootTools.find((t) => t.name === cmdName || toToolName(t.name) === cmdName)?.command : undefined;
136
+ const helpText = generateHelp(existingCommand, targetCmd ?? existingCommand, { format: 'text', detail: 'full' });
137
+ return {
138
+ jsonrpc: '2.0',
139
+ id: id ?? null,
140
+ result: { content: [{ type: 'text', text: helpText }], isError: false },
141
+ };
142
+ }
143
+
144
+ const tool = toolMap.get(toolName);
145
+ if (!tool) {
146
+ return {
147
+ jsonrpc: '2.0',
148
+ id: id ?? null,
149
+ error: { code: -32602, message: `Unknown tool: ${toolName}` },
150
+ };
151
+ }
152
+
153
+ // Build command string: convert tool name back to command path + serialize args as flags
154
+ const commandPath = toCommandPath(tool.name);
155
+ const argParts = serializeArgsToFlags(args);
156
+ const input = [commandPath, ...argParts].filter(Boolean).join(' ') || undefined;
157
+
158
+ try {
159
+ const output: string[] = [];
160
+ const errors: string[] = [];
161
+ const result = await evalCommand(input as any, {
162
+ caller: 'mcp',
163
+ runtime: {
164
+ output: (...outArgs: unknown[]) => output.push(outArgs.map(String).join(' ')),
165
+ error: (text: string) => errors.push(text),
166
+ interactive: 'unsupported',
167
+ format: 'text',
168
+ },
169
+ });
170
+
171
+ const content: { type: string; text: string }[] = [];
172
+
173
+ if (result.error) {
174
+ const errorMsg = result.error instanceof Error ? result.error.message : String(result.error);
175
+ if (errors.length) content.push({ type: 'text', text: errors.join('\n') });
176
+ content.push({ type: 'text', text: errorMsg });
177
+ return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: true } };
178
+ }
179
+
180
+ if (result.argsResult?.issues) {
181
+ const issueMessages = result.argsResult.issues.map((i: any) => `${i.path?.join('.') || 'root'}: ${i.message}`).join('\n');
182
+ content.push({ type: 'text', text: `Validation error:\n${issueMessages}` });
183
+ return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: true } };
184
+ }
185
+
186
+ if (output.length) content.push({ type: 'text', text: output.join('\n') });
187
+ if (result.result !== undefined && result.result !== null) {
188
+ const resultText = typeof result.result === 'string' ? result.result : JSON.stringify(result.result, null, 2);
189
+ content.push({ type: 'text', text: resultText });
190
+ }
191
+ if (content.length === 0) content.push({ type: 'text', text: 'Done.' });
192
+ return { jsonrpc: '2.0', id: id ?? null, result: { content, isError: false } };
193
+ } catch (err) {
194
+ const errorMsg = err instanceof Error ? err.message : String(err);
195
+ return {
196
+ jsonrpc: '2.0',
197
+ id: id ?? null,
198
+ result: { content: [{ type: 'text', text: errorMsg }], isError: true },
199
+ };
200
+ }
201
+ }
202
+
203
+ default: {
204
+ if (id !== undefined) {
205
+ return { jsonrpc: '2.0', id, error: { code: -32601, message: `Method not found: ${method}` } };
206
+ }
207
+ return undefined;
208
+ }
209
+ }
210
+ };
211
+ }
212
+
213
+ /** stdio transport: newline-delimited JSON per 2025-11-25 spec. */
214
+ async function startStdioTransport(handleRequest: (req: JsonRpcRequest) => Promise<JsonRpcResponse | undefined>): Promise<void> {
215
+ const { stdin, stdout } = await import('node:process');
216
+ const { createInterface } = await import('node:readline');
217
+
218
+ function send(msg: JsonRpcResponse) {
219
+ stdout.write(`${JSON.stringify(msg)}\n`);
220
+ }
221
+
222
+ const rl = createInterface({ input: stdin, crlfDelay: Infinity });
223
+
224
+ for await (const line of rl) {
225
+ if (!line.trim()) continue;
226
+ try {
227
+ const req = JSON.parse(line) as JsonRpcRequest;
228
+ const res = await handleRequest(req);
229
+ if (res) send(res);
230
+ } catch {
231
+ // Ignore malformed JSON
232
+ }
233
+ }
234
+ }
235
+
236
+ /** Streamable HTTP transport per 2025-11-25 spec. Responds with JSON or SSE based on client's Accept header. */
237
+ async function startHttpTransport(
238
+ handleRequest: (req: JsonRpcRequest) => Promise<JsonRpcResponse | undefined>,
239
+ prefs: PadroneMcpPreferences,
240
+ log: (msg: string) => void,
241
+ onSignal?: (callback: () => void) => () => void,
242
+ ): Promise<void> {
243
+ const http = await import('node:http');
244
+ const crypto = await import('node:crypto');
245
+
246
+ const port = prefs.port ?? 3000;
247
+ const host = prefs.host ?? '127.0.0.1';
248
+ const endpoint = prefs.basePath ?? '/mcp';
249
+
250
+ // Session management
251
+ let sessionId: string | undefined;
252
+ let negotiatedVersion: string | undefined;
253
+
254
+ const corsOrigin = prefs.cors !== false ? (prefs.cors ?? '*') : undefined;
255
+
256
+ const server = http.createServer(async (req, res) => {
257
+ // CORS headers
258
+ if (corsOrigin) {
259
+ res.setHeader('Access-Control-Allow-Origin', corsOrigin);
260
+ res.setHeader('Access-Control-Allow-Methods', 'POST, GET, DELETE, OPTIONS');
261
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, MCP-Session-Id, MCP-Protocol-Version');
262
+ res.setHeader('Access-Control-Expose-Headers', 'MCP-Session-Id');
263
+ }
264
+
265
+ if (req.method === 'OPTIONS') {
266
+ res.writeHead(corsOrigin ? 204 : 405);
267
+ res.end();
268
+ return;
269
+ }
270
+
271
+ if (req.url !== endpoint) {
272
+ res.writeHead(404, { 'Content-Type': 'application/json' });
273
+ res.end(JSON.stringify({ error: 'Not found' }));
274
+ return;
275
+ }
276
+
277
+ // DELETE: terminate session
278
+ if (req.method === 'DELETE') {
279
+ const reqSessionId = req.headers['mcp-session-id'] as string | undefined;
280
+ if (sessionId && reqSessionId === sessionId) {
281
+ sessionId = undefined;
282
+ negotiatedVersion = undefined;
283
+ res.writeHead(200);
284
+ res.end();
285
+ } else {
286
+ res.writeHead(404);
287
+ res.end();
288
+ }
289
+ return;
290
+ }
291
+
292
+ // GET: SSE stream (not implemented — return 405)
293
+ if (req.method === 'GET') {
294
+ res.writeHead(405, { 'Content-Type': 'application/json' });
295
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32601, message: 'SSE stream not supported' } }));
296
+ return;
297
+ }
298
+
299
+ if (req.method !== 'POST') {
300
+ res.writeHead(405);
301
+ res.end();
302
+ return;
303
+ }
304
+
305
+ // Validate session ID on non-initialize requests
306
+ const reqSessionId = req.headers['mcp-session-id'] as string | undefined;
307
+ if (sessionId && reqSessionId && reqSessionId !== sessionId) {
308
+ res.writeHead(404, { 'Content-Type': 'application/json' });
309
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Invalid session' } }));
310
+ return;
311
+ }
312
+
313
+ // Validate MCP-Protocol-Version header on post-init requests
314
+ const reqProtocolVersion = req.headers['mcp-protocol-version'] as string | undefined;
315
+ if (negotiatedVersion && reqProtocolVersion && reqProtocolVersion !== negotiatedVersion) {
316
+ res.writeHead(400, { 'Content-Type': 'application/json' });
317
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32600, message: 'Protocol version mismatch' } }));
318
+ return;
319
+ }
320
+
321
+ // Read request body
322
+ const body = await readStreamAsText(req as AsyncIterable<Uint8Array>);
323
+
324
+ let rpcRequest: JsonRpcRequest;
325
+ try {
326
+ rpcRequest = JSON.parse(body);
327
+ } catch {
328
+ res.writeHead(400, { 'Content-Type': 'application/json' });
329
+ res.end(JSON.stringify({ jsonrpc: '2.0', id: null, error: { code: -32700, message: 'Parse error' } }));
330
+ return;
331
+ }
332
+
333
+ const response = await handleRequest(rpcRequest);
334
+
335
+ // On initialize response: create session and set header
336
+ if (rpcRequest.method === 'initialize' && response?.result) {
337
+ sessionId = crypto.randomUUID();
338
+ negotiatedVersion = PROTOCOL_VERSION;
339
+ res.setHeader('MCP-Session-Id', sessionId);
340
+ }
341
+
342
+ if (response) {
343
+ const accept = req.headers.accept ?? '';
344
+ if (accept.includes('text/event-stream')) {
345
+ res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', Connection: 'keep-alive' });
346
+ res.write(`event: message\ndata: ${JSON.stringify(response)}\n\n`);
347
+ res.end();
348
+ } else {
349
+ res.writeHead(200, { 'Content-Type': 'application/json' });
350
+ res.end(JSON.stringify(response));
351
+ }
352
+ } else {
353
+ // Notification or response from client — no body
354
+ res.writeHead(202);
355
+ res.end();
356
+ }
357
+ });
358
+
359
+ return new Promise<void>((resolve, reject) => {
360
+ server.listen(port, host, () => {
361
+ log(`MCP server listening on http://${host}:${port}${endpoint}`);
362
+ });
363
+ server.on('error', reject);
364
+ const unsubscribe = onSignal?.(() => {
365
+ server.close(() => resolve());
366
+ });
367
+ server.on('close', () => unsubscribe?.());
368
+ });
369
+ }
370
+
371
+ export async function startMcpServer(
372
+ _program: AnyPadroneProgram,
373
+ existingCommand: AnyPadroneCommand,
374
+ evalCommand: AnyPadroneProgram['eval'],
375
+ prefs?: PadroneMcpPreferences,
376
+ ): Promise<void> {
377
+ const handleRequest = createMcpHandler(existingCommand, evalCommand, prefs);
378
+ const transport = prefs?.transport ?? 'http';
379
+
380
+ if (transport === 'stdio') {
381
+ return startStdioTransport(handleRequest);
382
+ }
383
+
384
+ const { getCommandRuntime } = await import('../core/commands.ts');
385
+ const runtime = getCommandRuntime(existingCommand);
386
+ return startHttpTransport(handleRequest, prefs ?? {}, (msg) => runtime.error(msg), runtime.onSignal);
387
+ }
@@ -1,8 +1,9 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
- import { buildReplCompleter, findCommandByName, getCommandRuntime } from './command-utils.ts';
3
- import { createTerminalReplSession, REPL_SIGINT, type ReplSessionConfig } from './runtime.ts';
4
- import type { AnyPadroneCommand, PadroneEvalPreferences, PadroneReplPreferences } from './types.ts';
5
- import { getVersion } from './utils.ts';
2
+ import { buildReplCompleter, findCommandByName, getCommandRuntime } from '../core/commands.ts';
3
+ import { createTerminalReplSession } from '../core/default-runtime.ts';
4
+ import { REPL_SIGINT, type ReplSessionConfig } from '../core/runtime.ts';
5
+ import type { AnyPadroneCommand, PadroneEvalPreferences, PadroneReplPreferences } from '../types/index.ts';
6
+ import { getVersion } from '../util/utils.ts';
6
7
 
7
8
  export type ReplDeps = {
8
9
  existingCommand: AnyPadroneCommand;
@@ -13,7 +14,7 @@ export type ReplDeps = {
13
14
  /**
14
15
  * Creates a REPL async iterable for running commands interactively.
15
16
  */
16
- export function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferences): AsyncIterable<any> {
17
+ export function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferences): AsyncIterable<any> & { drain: () => Promise<any> } {
17
18
  const { existingCommand, evalCommand, replActiveRef } = deps;
18
19
 
19
20
  if (replActiveRef.value) {
@@ -25,9 +26,8 @@ export function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferen
25
26
  const runtime = getCommandRuntime(existingCommand);
26
27
 
27
28
  const programName = existingCommand.name || 'padrone';
28
- const useAnsi =
29
- runtime.format === 'ansi' ||
30
- (runtime.format === 'auto' && typeof process !== 'undefined' && !process.env.NO_COLOR && !process.env.CI && process.stdout?.isTTY);
29
+ const env = runtime.env();
30
+ const useAnsi = runtime.format === 'ansi' || (runtime.format === 'auto' && !env.NO_COLOR && !env.CI && runtime.terminal?.isTTY === true);
31
31
 
32
32
  // Track command history for .history built-in
33
33
  const commandHistory: string[] = [];
@@ -60,7 +60,7 @@ export function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferen
60
60
  runtime.output(options.greeting);
61
61
  } else {
62
62
  const displayName = existingCommand.title || programName;
63
- const version = existingCommand.version ? getVersion(existingCommand.version) : undefined;
63
+ const version = existingCommand.version ? await getVersion(existingCommand.version) : undefined;
64
64
  const greeting = version ? `Welcome to ${displayName} v${version}` : `Welcome to ${displayName}`;
65
65
  runtime.output(greeting);
66
66
  }
@@ -264,10 +264,7 @@ export function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferen
264
264
 
265
265
  const emitSpacingLine = (value: boolean | string) => {
266
266
  if (typeof value === 'string') {
267
- const sep =
268
- value.length === 1
269
- ? value.repeat(typeof process !== 'undefined' && process.stdout?.columns ? process.stdout.columns : 80)
270
- : value;
267
+ const sep = value.length === 1 ? value.repeat(runtime.terminal?.columns ?? 80) : value;
271
268
  runtime.output(sep);
272
269
  } else if (value) {
273
270
  runtime.output('');
@@ -289,9 +286,12 @@ export function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferen
289
286
  const scopedInput = scopePath ? (evalInput ? `${scopePath} ${evalInput}` : scopePath) : evalInput;
290
287
 
291
288
  try {
292
- const replEvalPrefs: PadroneEvalPreferences | undefined = options?.autoOutput === false ? { autoOutput: false } : undefined;
289
+ const replEvalPrefs: PadroneEvalPreferences | undefined = { caller: 'repl' };
293
290
  const result = await evalCommand(scopedInput, replEvalPrefs);
294
- if (result.argsResult?.issues) {
291
+ if (result.error) {
292
+ const msg = result.error instanceof Error ? result.error.message : String(result.error);
293
+ runtime.error(prefixLines ? prefixLines(msg) : msg);
294
+ } else if (result.argsResult?.issues) {
295
295
  const issueMessages = result.argsResult.issues
296
296
  .map((i: StandardSchemaV1.Issue) => ` - ${i.path?.join('.') || 'root'}: ${i.message}`)
297
297
  .join('\n');
@@ -313,5 +313,15 @@ export function createReplIterator(deps: ReplDeps, options?: PadroneReplPreferen
313
313
  }
314
314
  }
315
315
 
316
- return replIterator() as any;
316
+ const iterable = replIterator();
317
+ (iterable as any).drain = async () => {
318
+ try {
319
+ const results: any[] = [];
320
+ for await (const result of iterable) results.push(result);
321
+ return { value: results };
322
+ } catch (err) {
323
+ return { error: err };
324
+ }
325
+ };
326
+ return iterable as any;
317
327
  }