padrone 1.4.0 → 1.5.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 (82) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +105 -284
  3. package/dist/{args-CVDbyyzG.mjs → args-D5PNDyNu.mjs} +41 -18
  4. package/dist/args-D5PNDyNu.mjs.map +1 -0
  5. package/dist/chunk-CjcI7cDX.mjs +15 -0
  6. package/dist/codegen/index.d.mts +28 -3
  7. package/dist/codegen/index.d.mts.map +1 -1
  8. package/dist/codegen/index.mjs +169 -19
  9. package/dist/codegen/index.mjs.map +1 -1
  10. package/dist/command-utils-B1D-HqCd.mjs +1117 -0
  11. package/dist/command-utils-B1D-HqCd.mjs.map +1 -0
  12. package/dist/completion.d.mts +1 -1
  13. package/dist/completion.d.mts.map +1 -1
  14. package/dist/completion.mjs +77 -29
  15. package/dist/completion.mjs.map +1 -1
  16. package/dist/docs/index.d.mts +22 -2
  17. package/dist/docs/index.d.mts.map +1 -1
  18. package/dist/docs/index.mjs +94 -7
  19. package/dist/docs/index.mjs.map +1 -1
  20. package/dist/errors-BiVrBgi6.mjs +114 -0
  21. package/dist/errors-BiVrBgi6.mjs.map +1 -0
  22. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DtHzbP22.d.mts} +34 -5
  23. package/dist/formatter-DtHzbP22.d.mts.map +1 -0
  24. package/dist/help-bbmu9-qd.mjs +735 -0
  25. package/dist/help-bbmu9-qd.mjs.map +1 -0
  26. package/dist/index.d.mts +32 -3
  27. package/dist/index.d.mts.map +1 -1
  28. package/dist/index.mjs +493 -265
  29. package/dist/index.mjs.map +1 -1
  30. package/dist/mcp-mLWIdUIu.mjs +379 -0
  31. package/dist/mcp-mLWIdUIu.mjs.map +1 -0
  32. package/dist/serve-B0u43DK7.mjs +404 -0
  33. package/dist/serve-B0u43DK7.mjs.map +1 -0
  34. package/dist/stream-BcC146Ud.mjs +56 -0
  35. package/dist/stream-BcC146Ud.mjs.map +1 -0
  36. package/dist/test.d.mts +1 -1
  37. package/dist/test.mjs +4 -15
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{types-DjIdJN5G.d.mts → types-Ch8Mk6Qb.d.mts} +310 -62
  40. package/dist/types-Ch8Mk6Qb.d.mts.map +1 -0
  41. package/dist/{update-check-EbNDkzyV.mjs → update-check-CFX1FV3v.mjs} +2 -2
  42. package/dist/{update-check-EbNDkzyV.mjs.map → update-check-CFX1FV3v.mjs.map} +1 -1
  43. package/dist/zod.d.mts +32 -0
  44. package/dist/zod.d.mts.map +1 -0
  45. package/dist/zod.mjs +50 -0
  46. package/dist/zod.mjs.map +1 -0
  47. package/package.json +10 -2
  48. package/src/args.ts +68 -40
  49. package/src/cli/docs.ts +1 -7
  50. package/src/cli/doctor.ts +195 -10
  51. package/src/cli/index.ts +1 -1
  52. package/src/cli/init.ts +2 -3
  53. package/src/cli/link.ts +2 -2
  54. package/src/codegen/discovery.ts +80 -28
  55. package/src/codegen/index.ts +2 -1
  56. package/src/codegen/parsers/bash.ts +179 -0
  57. package/src/codegen/schema-to-code.ts +2 -1
  58. package/src/colorizer.ts +126 -13
  59. package/src/command-utils.ts +380 -30
  60. package/src/completion.ts +120 -47
  61. package/src/create.ts +480 -128
  62. package/src/docs/index.ts +122 -8
  63. package/src/formatter.ts +171 -125
  64. package/src/help.ts +45 -12
  65. package/src/index.ts +29 -1
  66. package/src/interactive.ts +45 -4
  67. package/src/mcp.ts +390 -0
  68. package/src/repl-loop.ts +16 -3
  69. package/src/runtime.ts +195 -2
  70. package/src/serve.ts +442 -0
  71. package/src/stream.ts +75 -0
  72. package/src/test.ts +7 -16
  73. package/src/type-utils.ts +28 -4
  74. package/src/types.ts +212 -30
  75. package/src/wrap.ts +23 -25
  76. package/src/zod.ts +50 -0
  77. package/dist/args-CVDbyyzG.mjs.map +0 -1
  78. package/dist/chunk-y_GBKt04.mjs +0 -5
  79. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  80. package/dist/help-CcBe91bV.mjs +0 -1254
  81. package/dist/help-CcBe91bV.mjs.map +0 -1
  82. package/dist/types-DjIdJN5G.d.mts.map +0 -1
package/src/stream.ts ADDED
@@ -0,0 +1,75 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { PadroneSchema } from './types.ts';
3
+
4
+ export interface AsyncStreamMeta {
5
+ [x: string]: unknown;
6
+ readonly asyncStream: number;
7
+ readonly itemSchema?: StandardSchemaV1;
8
+ }
9
+
10
+ let asyncStreamIdCounter = 1;
11
+ export const asyncStreamRegistry = new Map<number, AsyncStreamMeta>();
12
+
13
+ /**
14
+ * Returns metadata to mark a schema field as an async stream via `.meta()`.
15
+ *
16
+ * When used with `stdin`, padrone pipes stdin data as an `AsyncIterable` instead of
17
+ * buffering it. Each line is validated against the item schema (if provided) as it arrives.
18
+ *
19
+ * @param itemSchema - Optional item schema for per-item validation.
20
+ * Non-string schemas cause each stdin line to be `JSON.parse`'d before validation.
21
+ *
22
+ * @example
23
+ * ```ts
24
+ * import { asyncStream } from 'padrone';
25
+ *
26
+ * // String lines
27
+ * z.object({ lines: z.custom<AsyncIterable<string>>().meta(asyncStream()) })
28
+ *
29
+ * // Typed items — each line JSON.parse'd and validated
30
+ * z.object({ records: z.custom<AsyncIterable<{ name: string }>>().meta(asyncStream(recordSchema)) })
31
+ * ```
32
+ */
33
+ export function asyncStream<T = string>(itemSchema?: PadroneSchema<T>): AsyncStreamMeta {
34
+ const id = asyncStreamIdCounter++;
35
+ const meta: AsyncStreamMeta = itemSchema ? { asyncStream: id, itemSchema } : { asyncStream: id };
36
+ asyncStreamRegistry.set(id, meta);
37
+ return meta;
38
+ }
39
+
40
+ /** Stdin interface matching PadroneRuntime.stdin */
41
+ interface StdinSource {
42
+ isTTY?: boolean;
43
+ text(): Promise<string>;
44
+ lines(): AsyncIterable<string>;
45
+ }
46
+
47
+ /**
48
+ * Creates an `AsyncIterable` from a stdin source, optionally validating each item.
49
+ * When no stdin is available (TTY / undefined), yields nothing.
50
+ *
51
+ * - No item schema: yields raw string lines
52
+ * - With item schema: `JSON.parse`s each line, validates, then yields
53
+ */
54
+ export function createStdinStream(stdin: StdinSource | undefined, itemSchema?: StandardSchemaV1): AsyncIterable<unknown> {
55
+ if (!stdin) return emptyAsyncIterable;
56
+
57
+ if (!itemSchema) return stdin.lines();
58
+
59
+ return {
60
+ async *[Symbol.asyncIterator]() {
61
+ for await (const line of stdin.lines()) {
62
+ const result = itemSchema['~standard'].validate(line);
63
+ const resolved = result instanceof Promise ? await result : result;
64
+ if ('issues' in resolved && resolved.issues) {
65
+ throw new Error(`Stream item validation failed: ${resolved.issues.map((i) => i.message).join(', ')}`);
66
+ }
67
+ yield (resolved as { value: unknown }).value;
68
+ }
69
+ },
70
+ };
71
+ }
72
+
73
+ const emptyAsyncIterable: AsyncIterable<never> = {
74
+ async *[Symbol.asyncIterator]() {},
75
+ };
package/src/test.ts CHANGED
@@ -153,21 +153,11 @@ export function testCli(program: TestableProgram): TestCliBuilder {
153
153
  const runtime = buildRuntime(stdout, stderr, { envVars, promptAnswers, configFiles, stdinData });
154
154
  const testProgram = program.runtime(runtime);
155
155
 
156
- try {
157
- const evalResult = await testProgram.eval(runInput ?? input ?? '', { autoOutput: false });
158
- return toTestResult(evalResult, stdout, stderr);
159
- } catch (err) {
160
- stderr.push(err instanceof Error ? err.message : String(err));
161
- return {
162
- command: undefined as unknown as AnyPadroneCommand,
163
- args: undefined,
164
- result: undefined,
165
- issues: undefined,
166
- stdout,
167
- stderr,
168
- error: err,
169
- };
156
+ const evalResult = await testProgram.eval(runInput ?? input ?? '', { autoOutput: false });
157
+ if (evalResult.error) {
158
+ stderr.push(evalResult.error instanceof Error ? evalResult.error.message : String(evalResult.error));
170
159
  }
160
+ return toTestResult(evalResult, stdout, stderr);
171
161
  },
172
162
 
173
163
  async repl(inputs: string[]) {
@@ -186,7 +176,7 @@ export function testCli(program: TestableProgram): TestCliBuilder {
186
176
 
187
177
  for await (const r of testProgram.repl({ greeting: false, hint: false })) {
188
178
  results.push({
189
- command: r.command,
179
+ command: r.command!,
190
180
  args: r.args,
191
181
  result: r.result,
192
182
  issues: r.argsResult?.issues as TestCliResult['issues'],
@@ -202,9 +192,10 @@ export function testCli(program: TestableProgram): TestCliBuilder {
202
192
 
203
193
  function toTestResult(evalResult: PadroneCommandResult, stdout: unknown[], stderr: string[]): TestCliResult {
204
194
  return {
205
- command: evalResult.command,
195
+ command: evalResult.command!,
206
196
  args: evalResult.args,
207
197
  result: evalResult.result,
198
+ error: evalResult.error,
208
199
  issues: evalResult.argsResult?.issues as TestCliResult['issues'],
209
200
  stdout,
210
201
  stderr,
package/src/type-utils.ts CHANGED
@@ -36,9 +36,9 @@ export type OrAsync<TExisting extends boolean, TSchema> = TExisting extends true
36
36
  * Detects whether argument meta contains interactive or optionalInteractive configuration.
37
37
  * When either is `true` or a `string[]`, the command requires async execution for prompting.
38
38
  */
39
- export type HasInteractive<TMeta> = TMeta extends { interactive: true | string[] }
39
+ export type HasInteractive<TMeta> = TMeta extends { interactive: true | readonly string[] }
40
40
  ? true
41
- : TMeta extends { optionalInteractive: true | string[] }
41
+ : TMeta extends { optionalInteractive: true | readonly string[] }
42
42
  ? true
43
43
  : false;
44
44
 
@@ -52,14 +52,38 @@ export type OrAsyncMeta<TExisting extends boolean, TMeta> = TExisting extends tr
52
52
  ? true
53
53
  : false;
54
54
 
55
+ /**
56
+ * Unwraps a result type by resolving Promises and collecting iterables into arrays.
57
+ * - `AsyncIterable<U>` → `U[]`
58
+ * - `Iterable<U>` (excluding strings) → `U[]`
59
+ * - `Promise<U>` → `Drained<U>` (recursively unwraps)
60
+ * - `T` → `T`
61
+ */
62
+ export type Drained<T> =
63
+ T extends Promise<infer U>
64
+ ? Drained<U>
65
+ : T extends AsyncIterable<infer U>
66
+ ? U[]
67
+ : T extends string
68
+ ? T
69
+ : T extends Iterable<infer U>
70
+ ? U[]
71
+ : T;
72
+
73
+ /**
74
+ * A sync value augmented with Promise-like methods (.then, .catch, .finally).
75
+ * Unlike a real Promise, properties of T are accessible synchronously.
76
+ */
77
+ export type Thenable<T> = T & PromiseLike<T> & { catch: Promise<T>['catch']; finally: Promise<T>['finally'] };
78
+
55
79
  /**
56
80
  * Conditionally wraps a type in Promise based on the TAsync flag.
57
81
  * - `true` → `Promise<T>`
58
- * - `false` → `T & PromiseLike<T>` (thenable: supports `.then()` and `await`)
82
+ * - `false` → `T & Thenable<T>` (thenable: supports `.then()`, `.catch()`, `.finally()`, and `await`)
59
83
  * - `boolean` (union of true|false) → `Promise<T>` (safe default when async-ness is uncertain)
60
84
  * - `any` → `T` (for generic/any typed commands like AnyPadroneCommand)
61
85
  */
62
- export type MaybePromise<T, TAsync> = IsAny<TAsync> extends true ? T : true extends TAsync ? Promise<T> : T & PromiseLike<T>;
86
+ export type MaybePromise<T, TAsync> = IsAny<TAsync> extends true ? T : true extends TAsync ? Promise<T> : Thenable<T>;
63
87
 
64
88
  type SplitString<TName extends string, TSplitBy extends string = ' '> = TName extends `${infer FirstPart}${TSplitBy}${infer RestParts}`
65
89
  ? [FirstPart, ...SplitString<RestParts, TSplitBy>]
package/src/types.ts CHANGED
@@ -2,8 +2,11 @@ import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/sp
2
2
  import type { Tool } from 'ai';
3
3
  import type { PadroneArgsSchemaMeta } from './args.ts';
4
4
  import type { HelpPreferences } from './help.ts';
5
- import type { PadroneRuntime, ResolvedPadroneRuntime } from './runtime.ts';
5
+ import type { PadroneMcpPreferences } from './mcp.ts';
6
+ import type { PadroneProgressIndicator, PadroneRuntime, PadroneSpinnerConfig, ResolvedPadroneRuntime } from './runtime.ts';
7
+ import type { PadroneServePreferences } from './serve.ts';
6
8
  import type {
9
+ Drained,
7
10
  FindDirectChild,
8
11
  FlattenCommands,
9
12
  FullCommandName,
@@ -36,6 +39,11 @@ export type PadroneActionContext = {
36
39
  command: AnyPadroneCommand;
37
40
  /** The root program instance. */
38
41
  program: AnyPadroneProgram;
42
+ /**
43
+ * The active auto-managed progress indicator, or a no-op if none is configured.
44
+ * Use `.update()` to change the in-progress message mid-execution.
45
+ */
46
+ progress: PadroneProgressIndicator;
39
47
  };
40
48
 
41
49
  /**
@@ -178,8 +186,24 @@ export type PadroneCommand<
178
186
  aliases?: TAliases;
179
187
  deprecated?: boolean | string;
180
188
  hidden?: boolean;
189
+ /** Group name for organizing this command under a labeled section in help output. */
190
+ group?: string;
191
+ /** Whether this command performs a mutation (create, update, delete). Affects HTTP method in serve (POST-only) and MCP tool annotations (destructiveHint). */
192
+ mutation?: boolean;
181
193
  needsApproval?: boolean | ((args: TArgs) => Promise<boolean> | boolean);
182
194
  autoOutput?: boolean;
195
+ /** Usage examples shown in help output. Each entry is a command-line invocation string. */
196
+ examples?: string[];
197
+ /**
198
+ * Auto-start a progress indicator when the command's execute phase begins.
199
+ * - `true` — generic message based on command name.
200
+ * - `string` — custom message for all states.
201
+ * - `PadroneProgressConfig` — separate messages for progress, success, and error states.
202
+ *
203
+ * The indicator is automatically stopped on success (`.succeed()`) or failure (`.fail()`).
204
+ * Requires a `progress` factory on the runtime — silently skipped if not available.
205
+ */
206
+ progress?: boolean | string | PadroneProgressPrefs;
183
207
  argsSchema?: TArgs;
184
208
  configSchema?: TConfig;
185
209
  envSchema?: TEnv;
@@ -193,7 +217,7 @@ export type PadroneCommand<
193
217
  runtime?: PadroneRuntime;
194
218
 
195
219
  /** Plugins registered on this command. Collected from the parent chain at execution time. */
196
- plugins?: PadronePlugin[];
220
+ plugins?: PadronePlugin<any, any>[];
197
221
 
198
222
  /** Update check configuration. Only used on the root program. */
199
223
  updateCheck?: UpdateCheckConfig;
@@ -233,6 +257,33 @@ type CommandTypesBase = {
233
257
  /**
234
258
  * Configuration for a command.
235
259
  */
260
+ /**
261
+ * A progress message value: a plain string, `null` to suppress, or an object with a message and custom indicator icon.
262
+ */
263
+ export type PadroneProgressMessage = string | null | { message?: string | null; indicator?: string };
264
+
265
+ /**
266
+ * Progress indicator configuration with per-state messages and optional dynamic callbacks.
267
+ *
268
+ * The `success` and `error` fields accept either a static value or a callback function:
269
+ * - `string` — static message.
270
+ * - `null` — suppress the message entirely.
271
+ * - `{ message, indicator }` — custom message with a per-call indicator icon override.
272
+ * - `(result) => PadroneProgressMessage` / `(error) => PadroneProgressMessage` — dynamic from the actual result/error.
273
+ */
274
+ export type PadroneProgressPrefs<TRes = unknown> = {
275
+ /** Message shown during async validation. Defaults to `''` (spinner only). */
276
+ validation?: string;
277
+ /** Message shown while the command's action is running. */
278
+ progress?: string;
279
+ /** Message shown when the command succeeds. `null` to suppress. Defaults to the `progress` message. */
280
+ success?: PadroneProgressMessage | ((result: TRes) => PadroneProgressMessage);
281
+ /** Message shown when the command fails. `null` to suppress. Defaults to the error message. */
282
+ error?: PadroneProgressMessage | ((error: unknown) => PadroneProgressMessage);
283
+ /** Spinner configuration: a preset name, custom frames/interval, or `false` to disable. */
284
+ spinner?: PadroneSpinnerConfig;
285
+ };
286
+
236
287
  export type PadroneCommandConfig = {
237
288
  /** A short title for the command, displayed in help. */
238
289
  title?: string;
@@ -244,12 +295,23 @@ export type PadroneCommandConfig = {
244
295
  deprecated?: boolean | string;
245
296
  /** Whether the command should be hidden from help output. */
246
297
  hidden?: boolean;
298
+ /** Group name for organizing this command under a labeled section in help output. */
299
+ group?: string;
247
300
  /**
248
301
  * Automatically write this command's return value to output in CLI/eval/REPL mode.
249
302
  * Overrides the `autoOutput` setting in eval/cli preferences for this command.
250
303
  * See `PadroneEvalPreferences.autoOutput` for serialization details.
251
304
  */
252
305
  autoOutput?: boolean;
306
+ /** Usage examples shown in help output. Each entry is a command-line invocation string. */
307
+ examples?: string[];
308
+ /**
309
+ * Whether this command performs a mutation (create, update, delete).
310
+ * - In `serve()`: mutation commands accept POST only; non-mutation commands accept GET and POST.
311
+ * - In `mcp()`: sets `annotations.destructiveHint` on the tool definition.
312
+ * - In `tool()`: defaults `needsApproval` to `true` when not explicitly set.
313
+ */
314
+ mutation?: boolean;
253
315
  };
254
316
 
255
317
  /**
@@ -325,7 +387,7 @@ export type PadroneBuilderMethods<
325
387
  * On subcommands, only validate and execute plugins apply (parse is handled by the root program).
326
388
  */
327
389
  use: (
328
- plugin: PadronePlugin,
390
+ plugin: PadronePlugin<StandardSchemaV1.InferOutput<TArgs>, TRes>,
329
391
  ) => BuilderOrProgram<TReturn, TProgramName, TName, TParentName, TArgs, TRes, TCommands, TParentArgs, TConfig, TEnv, TAsync>;
330
392
 
331
393
  configure: (
@@ -463,6 +525,31 @@ export type PadroneBuilderMethods<
463
525
  OrAsync<TAsync, TNewEnv>
464
526
  >;
465
527
 
528
+ /**
529
+ * Configures an auto-managed progress indicator for this command.
530
+ * The indicator starts before validation and is automatically stopped on success or failure.
531
+ *
532
+ * - `true` — generic message based on command name.
533
+ * - `string` — custom message for all states.
534
+ * - `PadroneProgressConfig` — full control: per-state messages, dynamic callbacks, spinner config.
535
+ *
536
+ * Requires a `progress` factory on the runtime — silently skipped if not available.
537
+ *
538
+ * @example
539
+ * ```ts
540
+ * .progress('Deploying...')
541
+ * .progress({
542
+ * progress: 'Deploying...',
543
+ * success: (result) => `Deployed ${result.version}`,
544
+ * error: (err) => `Deploy failed: ${err.message}`,
545
+ * spinner: 'line',
546
+ * })
547
+ * ```
548
+ */
549
+ progress: (
550
+ config?: boolean | string | PadroneProgressPrefs<Awaited<TRes>>,
551
+ ) => BuilderOrProgram<TReturn, TProgramName, TName, TParentName, TArgs, TRes, TCommands, TParentArgs, TConfig, TEnv, TAsync>;
552
+
466
553
  /**
467
554
  * Defines the handler function to be executed when the command is run.
468
555
  * When overriding an existing command, the previous handler is passed as the third `base` parameter.
@@ -479,6 +566,8 @@ export type PadroneBuilderMethods<
479
566
  * Wraps an external CLI tool with optional schema transformation.
480
567
  * The config can include a schema that transforms command arguments to external CLI arguments.
481
568
  *
569
+ * @experimental This API is experimental and may change in future releases.
570
+ *
482
571
  * @example
483
572
  * ```ts
484
573
  * // No transformation - pass arguments as-is
@@ -769,8 +858,8 @@ export type PadroneProgram<
769
858
  eval: <const TCommand extends PossibleCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>], true, true>>(
770
859
  input: TCommand | SafeString,
771
860
  prefs?: PadroneEvalPreferences,
772
- ) => MaybePromise<
773
- PadroneCommandResult<PickCommandByPossibleCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>], TCommand>>,
861
+ ) => MaybePromiseCommandResult<
862
+ PickCommandByPossibleCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>], TCommand>,
774
863
  PickCommandByPossibleCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>], TCommand>['~types']['async']
775
864
  >;
776
865
 
@@ -781,7 +870,7 @@ export type PadroneProgram<
781
870
  */
782
871
  cli: (
783
872
  prefs?: PadroneCliPreferences<PossibleCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>>,
784
- ) => MaybePromise<PadroneCommandResult<FlattenCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>>, TAsync>;
873
+ ) => MaybePromiseCommandResult<FlattenCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>, TAsync>;
785
874
 
786
875
  /**
787
876
  * Parses CLI input (or the provided input string) into command and arguments without executing anything.
@@ -831,9 +920,11 @@ export type PadroneProgram<
831
920
  * - History persistence: save/load history across sessions (currently in-memory only)
832
921
  * - Middleware/hooks: onBeforeCommand, onAfterCommand, error interceptors (design alongside general middleware system)
833
922
  */
834
- repl: (
835
- options?: PadroneReplPreferences<PossibleCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>>,
836
- ) => AsyncIterable<PadroneCommandResult<FlattenCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>>>;
923
+ repl: (options?: PadroneReplPreferences<PossibleCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>>) => AsyncIterable<
924
+ PadroneCommandResult<FlattenCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>>
925
+ > & {
926
+ drain: () => Promise<PadroneDrainResult<PadroneCommandResult<FlattenCommands<[PadroneCommand<'', '', TArgs, TRes, TCommands>]>>[]>>;
927
+ };
837
928
 
838
929
  /**
839
930
  * Returns a tool definition that can be passed to AI SDK.
@@ -863,6 +954,34 @@ export type PadroneProgram<
863
954
  * ```
864
955
  */
865
956
  completion: (shell?: 'bash' | 'zsh' | 'fish' | 'powershell') => Promise<string>;
957
+
958
+ /**
959
+ * Starts a Model Context Protocol (MCP) server that exposes commands as tools for AI assistants.
960
+ * Communicates over stdio using JSON-RPC with Content-Length framing.
961
+ * Returns a Promise that resolves when the connection closes.
962
+ *
963
+ * @experimental This API is experimental and may change in future releases.
964
+ *
965
+ * @example
966
+ * ```ts
967
+ * await program.mcp();
968
+ * ```
969
+ */
970
+ mcp: (prefs?: PadroneMcpPreferences) => Promise<void>;
971
+
972
+ /**
973
+ * Starts a REST HTTP server that exposes commands as endpoints.
974
+ * Each command becomes a route: `users list` → `GET/POST /users/list`.
975
+ * Commands with `mutation: true` only accept POST.
976
+ *
977
+ * @experimental This API is experimental and may change in future releases.
978
+ *
979
+ * @example
980
+ * ```ts
981
+ * await program.serve({ port: 3000 });
982
+ * ```
983
+ */
984
+ serve: (prefs?: PadroneServePreferences) => Promise<void>;
866
985
  };
867
986
 
868
987
  export type AnyPadroneProgram = PadroneProgram<string, string, string, any, any, [...AnyPadroneCommand[]]>;
@@ -959,10 +1078,48 @@ export type PadroneEvalPreferences = {
959
1078
  export type PadroneCliPreferences<TScope extends string = string> = PadroneEvalPreferences & {
960
1079
  /** REPL preferences used when `--repl` flag is passed. Set to `false` to disable the `--repl` flag. */
961
1080
  repl?: PadroneReplPreferences<TScope> | false;
1081
+ /** MCP server preferences used when `mcp` command is used. Set to `false` to disable. */
1082
+ mcp?: PadroneMcpPreferences | false;
1083
+ /** REST server preferences used when `serve` command is used. Set to `false` to disable. */
1084
+ serve?: PadroneServePreferences | false;
962
1085
  };
963
1086
 
964
- export type PadroneCommandResult<TCommand extends AnyPadroneCommand = AnyPadroneCommand> = PadroneParseResult<TCommand> & {
965
- result: GetResults<TCommand>;
1087
+ /**
1088
+ * Result of `drain()` — a discriminated union that never throws.
1089
+ * On success, `value` holds the fully resolved/collected result; on failure, `error` holds the error.
1090
+ */
1091
+ export type PadroneDrainResult<TResult> = { value: Drained<TResult>; error?: never } | { error: unknown; value?: never };
1092
+
1093
+ /**
1094
+ * Result returned by `eval()`, `cli()`, and `run()`. Never thrown — errors are captured in the `error` field.
1095
+ * Discriminated union: check `error` to distinguish success from failure.
1096
+ *
1097
+ * On success: `command`, `args`, `argsResult`, `result` are populated; `error` is absent.
1098
+ * On failure: `error` is populated; `command` may be present if routing succeeded.
1099
+ */
1100
+ export type PadroneCommandResult<TCommand extends AnyPadroneCommand = AnyPadroneCommand> =
1101
+ | (PadroneParseResult<TCommand> & {
1102
+ result: GetResults<TCommand>;
1103
+ error?: never;
1104
+ /** Flattens the result: awaits Promises, collects iterables, catches errors. Never throws. */
1105
+ drain: () => Promise<PadroneDrainResult<GetResults<TCommand>>>;
1106
+ })
1107
+ | {
1108
+ command?: TCommand;
1109
+ args?: GetArguments<'out', TCommand>;
1110
+ argsResult?: StandardSchemaV1.Result<GetArguments<'out', TCommand>>;
1111
+ error: unknown;
1112
+ result?: never;
1113
+ /** Returns `{ error }` since there is no result to drain. */
1114
+ drain: () => Promise<PadroneDrainResult<GetResults<TCommand>>>;
1115
+ };
1116
+
1117
+ /**
1118
+ * Like `MaybePromise<PadroneCommandResult<TCommand>, TAsync>` but ensures `drain()` is available
1119
+ * at the outer level in all cases — both sync (Thenable) and async (Promise).
1120
+ */
1121
+ type MaybePromiseCommandResult<TCommand extends AnyPadroneCommand, TAsync> = MaybePromise<PadroneCommandResult<TCommand>, TAsync> & {
1122
+ drain: () => Promise<PadroneDrainResult<GetResults<TCommand>>>;
966
1123
  };
967
1124
 
968
1125
  export type PadroneParseResult<TCommand extends AnyPadroneCommand = AnyPadroneCommand> = {
@@ -1020,20 +1177,20 @@ export type PluginValidateContext = PluginBaseContext & {
1020
1177
  };
1021
1178
 
1022
1179
  /** Result returned by the validate phase's `next()`. */
1023
- export type PluginValidateResult = {
1024
- args: unknown;
1025
- argsResult: StandardSchemaV1.Result<unknown>;
1180
+ export type PluginValidateResult<TArgs = unknown> = {
1181
+ args: TArgs;
1182
+ argsResult: StandardSchemaV1.Result<TArgs>;
1026
1183
  };
1027
1184
 
1028
1185
  /** Context for the execute phase. */
1029
- export type PluginExecuteContext = PluginBaseContext & {
1186
+ export type PluginExecuteContext<TArgs = unknown> = PluginBaseContext & {
1030
1187
  /** Validated arguments that will be passed to the action. Mutable — modify before `next()` to override. */
1031
- args: unknown;
1188
+ args: TArgs;
1032
1189
  };
1033
1190
 
1034
1191
  /** Result returned by the execute phase's `next()`. */
1035
- export type PluginExecuteResult = {
1036
- result: unknown;
1192
+ export type PluginExecuteResult<TResult = unknown> = {
1193
+ result: TResult;
1037
1194
  };
1038
1195
 
1039
1196
  /** Context for the start phase. Runs before parsing, wraps the entire pipeline. */
@@ -1049,26 +1206,45 @@ export type PluginErrorContext = PluginBaseContext & {
1049
1206
  };
1050
1207
 
1051
1208
  /** Result returned by the error phase's `next()`. */
1052
- export type PluginErrorResult = {
1209
+ export type PluginErrorResult<TResult = unknown> = {
1053
1210
  /** The error (possibly transformed). Set to `undefined` to suppress the error. */
1054
1211
  error?: unknown;
1055
1212
  /** A replacement result when suppressing the error. */
1056
- result?: unknown;
1213
+ result?: TResult;
1057
1214
  };
1058
1215
 
1059
1216
  /** Context for the shutdown phase. Always runs after the pipeline (success or failure). */
1060
- export type PluginShutdownContext = PluginBaseContext & {
1217
+ export type PluginShutdownContext<TResult = unknown> = PluginBaseContext & {
1061
1218
  /** The error, if the pipeline failed (after error phase processing). */
1062
1219
  error?: unknown;
1063
1220
  /** The pipeline result, if it succeeded. */
1064
- result?: unknown;
1221
+ result?: TResult;
1065
1222
  };
1066
1223
 
1067
- type PluginPhaseHandler<TCtx, TResult> = (ctx: TCtx, next: () => TResult | Promise<TResult>) => TResult | Promise<TResult>;
1224
+ /**
1225
+ * A phase handler function for the plugin middleware chain.
1226
+ *
1227
+ * - `TCtx` — the context object available to the handler.
1228
+ * - `TNextResult` — the typed result returned by `next()`, giving the handler type-safe access to downstream output.
1229
+ * - `TReturn` — the type the handler itself returns. Defaults to `TNextResult` but can be wider,
1230
+ * allowing plugins to transform or replace the result (e.g., error-recovery plugins returning a different type).
1231
+ */
1232
+ type PluginPhaseHandler<TCtx, TNextResult, TReturn = TNextResult> = (
1233
+ ctx: TCtx,
1234
+ next: () => TNextResult | Promise<TNextResult>,
1235
+ ) => TReturn | Promise<TReturn>;
1068
1236
 
1069
1237
  /**
1070
1238
  * A Padrone plugin that can intercept the parse, validate, and execute phases of command execution.
1071
- * Plugins are registered at the program level with `.use()` and apply to all commands.
1239
+ * Plugins are registered at the program or subcommand level with `.use()`.
1240
+ *
1241
+ * Type parameters:
1242
+ * - `TArgs` — the validated arguments type (output of the args schema). Provides typed `ctx.args` in the execute phase
1243
+ * and typed `args` in the validate result from `next()`.
1244
+ * - `TResult` — the command's return type. Provides typed `result` in execute/error/shutdown phases.
1245
+ *
1246
+ * When registered inline on a builder, these are inferred from the command's types automatically.
1247
+ * For reusable plugins that work with any command, use `PadronePlugin<any, any>`.
1072
1248
  *
1073
1249
  * Each phase handler receives a context and a `next()` function (onion/middleware pattern):
1074
1250
  * - Call `next()` to proceed to the next plugin or the core operation.
@@ -1077,9 +1253,15 @@ type PluginPhaseHandler<TCtx, TResult> = (ctx: TCtx, next: () => TResult | Promi
1077
1253
  * - Modify context fields before `next()` to alter inputs.
1078
1254
  * - Transform the return value of `next()` to alter outputs.
1079
1255
  */
1080
- export type PadronePlugin = {
1081
- /** Unique name for this plugin. Used for identification and future disable/override support. */
1256
+ export type PadronePlugin<TArgs = unknown, TResult = unknown> = {
1257
+ /** Display name for this plugin. Used for identification in logs and debugging. */
1082
1258
  name: string;
1259
+ /**
1260
+ * Optional unique identifier for deduplication. When multiple plugins share the same `id`,
1261
+ * only the last one registered is kept. Useful for allowing downstream code to override
1262
+ * a plugin without accumulating duplicates.
1263
+ */
1264
+ id?: string;
1083
1265
  /**
1084
1266
  * Ordering hint. Lower values run as outer layers (earlier before `next()`, later after).
1085
1267
  * Plugins with the same order preserve registration order. Defaults to `0`.
@@ -1093,17 +1275,17 @@ export type PadronePlugin = {
1093
1275
  /** Intercepts command routing and raw argument extraction. */
1094
1276
  parse?: PluginPhaseHandler<PluginParseContext, PluginParseResult>;
1095
1277
  /** Intercepts argument preprocessing, interactive prompting, and schema validation. */
1096
- validate?: PluginPhaseHandler<PluginValidateContext, PluginValidateResult>;
1278
+ validate?: PluginPhaseHandler<PluginValidateContext, PluginValidateResult<TArgs>, PluginValidateResult>;
1097
1279
  /** Intercepts handler execution. */
1098
- execute?: PluginPhaseHandler<PluginExecuteContext, PluginExecuteResult>;
1280
+ execute?: PluginPhaseHandler<PluginExecuteContext<TArgs>, PluginExecuteResult<TResult>, PluginExecuteResult>;
1099
1281
  /**
1100
1282
  * Called when the pipeline throws an error. `next()` passes to the next error handler
1101
1283
  * (innermost returns `{ error }` unchanged). Return `{ result }` without `error` to suppress.
1102
1284
  */
1103
- error?: PluginPhaseHandler<PluginErrorContext, PluginErrorResult>;
1285
+ error?: PluginPhaseHandler<PluginErrorContext, PluginErrorResult<TResult>, PluginErrorResult>;
1104
1286
  /**
1105
1287
  * Always runs after the pipeline completes (success or failure). `next()` calls the next shutdown handler.
1106
1288
  * Use for cleanup: closing connections, flushing logs, etc.
1107
1289
  */
1108
- shutdown?: PluginPhaseHandler<PluginShutdownContext, void>;
1290
+ shutdown?: PluginPhaseHandler<PluginShutdownContext<TResult>, void>;
1109
1291
  };
package/src/wrap.ts CHANGED
@@ -58,7 +58,7 @@ export type WrapResult = {
58
58
  /**
59
59
  * Converts parsed arguments to CLI arguments for an external command.
60
60
  */
61
- function argsToCliArgs(input: Record<string, unknown> | undefined, positional: string[] = []): string[] {
61
+ function argsToCliArgs(input: Record<string, unknown> | undefined, positional: readonly string[] = []): string[] {
62
62
  const args: string[] = [];
63
63
 
64
64
  // Handle undefined or null input
@@ -122,7 +122,7 @@ function argsToCliArgs(input: Record<string, unknown> | undefined, positional: s
122
122
  export function createWrapHandler<TCommandArgs extends PadroneSchema, TWrapArgs extends PadroneSchema>(
123
123
  config: WrapConfig<TCommandArgs, TWrapArgs>,
124
124
  commandArguments: TCommandArgs,
125
- commandPositional?: string[],
125
+ commandPositional?: readonly string[],
126
126
  ): (args: StandardSchemaV1.InferOutput<TCommandArgs>) => Promise<WrapResult> {
127
127
  return async (args: StandardSchemaV1.InferOutput<TCommandArgs>): Promise<WrapResult> => {
128
128
  const { command, args: fixedArgs = [], inheritStdio = true, positional = commandPositional, schema: wrapSchema } = config;
@@ -153,33 +153,31 @@ export function createWrapHandler<TCommandArgs extends PadroneSchema, TWrapArgs
153
153
  const allArgs = [...fixedArgs, ...regularArgs];
154
154
 
155
155
  // Execute the external command
156
- const proc = Bun.spawn([command, ...allArgs], {
157
- stdout: inheritStdio ? 'inherit' : 'pipe',
158
- stderr: inheritStdio ? 'inherit' : 'pipe',
159
- stdin: inheritStdio ? 'inherit' : 'ignore',
160
- });
156
+ const { spawn } = await import('node:child_process');
161
157
 
162
- const exitCode = await proc.exited;
158
+ return new Promise<WrapResult>((resolve, reject) => {
159
+ const proc = spawn(command, allArgs, {
160
+ stdio: inheritStdio ? 'inherit' : ['ignore', 'pipe', 'pipe'],
161
+ });
163
162
 
164
- let stdout: string | undefined;
165
- let stderr: string | undefined;
163
+ const stdoutChunks: Buffer[] = [];
164
+ const stderrChunks: Buffer[] = [];
166
165
 
167
- if (!inheritStdio) {
168
- if (proc.stdout) {
169
- const stdoutBuffer = await new Response(proc.stdout).arrayBuffer();
170
- stdout = new TextDecoder().decode(stdoutBuffer);
171
- }
172
- if (proc.stderr) {
173
- const stderrBuffer = await new Response(proc.stderr).arrayBuffer();
174
- stderr = new TextDecoder().decode(stderrBuffer);
166
+ if (!inheritStdio) {
167
+ proc.stdout!.on('data', (chunk: Buffer) => stdoutChunks.push(chunk));
168
+ proc.stderr!.on('data', (chunk: Buffer) => stderrChunks.push(chunk));
175
169
  }
176
- }
177
170
 
178
- return {
179
- exitCode,
180
- stdout,
181
- stderr,
182
- success: exitCode === 0,
183
- };
171
+ proc.on('error', reject);
172
+ proc.on('close', (code) => {
173
+ const exitCode = code ?? 1;
174
+ resolve({
175
+ exitCode,
176
+ stdout: inheritStdio ? undefined : Buffer.concat(stdoutChunks).toString(),
177
+ stderr: inheritStdio ? undefined : Buffer.concat(stderrChunks).toString(),
178
+ success: exitCode === 0,
179
+ });
180
+ });
181
+ });
184
182
  };
185
183
  }