padrone 1.3.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 +94 -0
  2. package/README.md +105 -284
  3. package/dist/{args-DFEI7_G_.mjs → args-D5PNDyNu.mjs} +46 -21
  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-XroimS3Q.d.mts → formatter-DtHzbP22.d.mts} +35 -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 +495 -267
  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-BS7RP5Ls.d.mts → types-Ch8Mk6Qb.d.mts} +311 -63
  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 +76 -44
  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 +401 -23
  60. package/src/completion.ts +120 -47
  61. package/src/create.ts +483 -130
  62. package/src/docs/index.ts +122 -8
  63. package/src/formatter.ts +173 -125
  64. package/src/help.ts +46 -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-DFEI7_G_.mjs.map +0 -1
  78. package/dist/chunk-y_GBKt04.mjs +0 -5
  79. package/dist/formatter-XroimS3Q.d.mts.map +0 -1
  80. package/dist/help-CgGP7hQU.mjs +0 -1229
  81. package/dist/help-CgGP7hQU.mjs.map +0 -1
  82. package/dist/types-BS7RP5Ls.d.mts.map +0 -1
package/src/create.ts CHANGED
@@ -1,20 +1,42 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
2
  import type { Schema } from 'ai';
3
- import { coerceArgs, detectUnknownArgs, extractSchemaMetadata, parsePositionalConfig, parseStdinConfig, preprocessArgs } from './args.ts';
3
+ import {
4
+ coerceArgs,
5
+ detectUnknownArgs,
6
+ extractSchemaMetadata,
7
+ isArrayField,
8
+ isAsyncStreamField,
9
+ JSON_SCHEMA_OPTS,
10
+ parsePositionalConfig,
11
+ parseStdinConfig,
12
+ preprocessArgs,
13
+ } from './args.ts';
14
+ import { type ColorConfig, type ColorTheme, colorThemes } from './colorizer.ts';
4
15
  import {
5
16
  commandSymbol,
17
+ createLazyIndicator,
18
+ createProgress,
19
+ errorResult,
6
20
  findCommandByName,
7
21
  getCommandRuntime,
8
22
  hasInteractiveConfig,
9
23
  isAsyncBranded,
24
+ lazyResolver,
25
+ makeThenable,
10
26
  mergeCommands,
11
27
  noop,
28
+ noopIndicator,
12
29
  outputValue,
13
30
  repathCommandTree,
31
+ resolveAllCommands,
32
+ resolveCommand,
33
+ resolveProgressMessage,
14
34
  runPluginChain,
15
35
  suggestSimilar,
16
36
  thenMaybe,
17
37
  warnIfUnexpectedAsync,
38
+ withDrain,
39
+ withPromiseDrain,
18
40
  wrapWithLifecycle,
19
41
  } from './command-utils.ts';
20
42
  import type { ShellType } from './completion.ts';
@@ -23,7 +45,8 @@ import { generateHelp } from './help.ts';
23
45
  import { promptInteractiveFields } from './interactive.ts';
24
46
  import { getNestedValue, parseCliInputToParts, setNestedValue } from './parse.ts';
25
47
  import { createReplIterator } from './repl-loop.ts';
26
- import { resolveStdin } from './runtime.ts';
48
+ import { type PadroneProgressIndicator, resolveStdin, resolveStdinAlways } from './runtime.ts';
49
+ import { createStdinStream } from './stream.ts';
27
50
  import type {
28
51
  AnyPadroneCommand,
29
52
  AnyPadroneProgram,
@@ -64,11 +87,14 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
64
87
  : inputCommand;
65
88
 
66
89
  /** Creates the action context passed to command handlers. References `builder` which is defined later but only called at runtime. */
67
- const createActionContext = (cmd: AnyPadroneCommand): PadroneActionContext => ({
68
- runtime: getCommandRuntime(cmd),
69
- command: cmd,
70
- program: builder as any,
71
- });
90
+ const createActionContext = (cmd: AnyPadroneCommand): PadroneActionContext => {
91
+ return {
92
+ runtime: getCommandRuntime(cmd),
93
+ command: cmd,
94
+ program: builder as any,
95
+ progress: noopIndicator,
96
+ };
97
+ };
72
98
 
73
99
  const find: AnyPadroneProgram['find'] = (command) => {
74
100
  if (typeof command !== 'string') return findCommandByName(command.path, existingCommand.commands) as any;
@@ -135,7 +161,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
135
161
  const arrayArguments = new Set<string>();
136
162
  if (curCommand.argsSchema) {
137
163
  try {
138
- const jsonSchema = curCommand.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
164
+ const jsonSchema = curCommand.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
139
165
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
140
166
  for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
141
167
  if (prop?.type === 'array') arrayArguments.add(key);
@@ -351,16 +377,24 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
351
377
  const stdinConfig = command.meta?.stdin;
352
378
  if (!stdinConfig) return {};
353
379
 
354
- const { field, as } = parseStdinConfig(stdinConfig);
380
+ const field = parseStdinConfig(stdinConfig);
355
381
 
356
382
  // Skip if the field was already provided via CLI flags
357
383
  if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
358
384
 
359
385
  const runtime = getCommandRuntime(existingCommand);
386
+
387
+ const streamInfo = isAsyncStreamField(command.argsSchema, field);
388
+ if (streamInfo) {
389
+ // Async stream: always resolve stdin (even on TTY) for interactive use
390
+ const stdinForStream = resolveStdinAlways(runtime as any);
391
+ return { [field]: createStdinStream(stdinForStream, streamInfo.itemSchema) };
392
+ }
393
+
360
394
  const stdin = resolveStdin(runtime as any);
361
395
  if (!stdin) return {};
362
396
 
363
- if (as === 'lines') {
397
+ if (isArrayField(command.argsSchema, field)) {
364
398
  return (async () => {
365
399
  const lines: string[] = [];
366
400
  for await (const line of stdin.lines()) {
@@ -417,7 +451,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
417
451
  );
418
452
  };
419
453
 
420
- return thenMaybe(parsedOrPromise, continueAfterParse) as any;
454
+ return makeThenable(thenMaybe(parsedOrPromise, continueAfterParse)) as any;
421
455
  };
422
456
 
423
457
  const stringify: AnyPadroneProgram['stringify'] = (command = '' as any, args) => {
@@ -498,10 +532,13 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
498
532
  const checkBuiltinCommands = (
499
533
  input: string | undefined,
500
534
  ):
501
- | { type: 'help'; command?: AnyPadroneCommand; detail?: DetailLevel; format?: FormatLevel }
535
+ | { type: 'help'; command?: AnyPadroneCommand; detail?: DetailLevel; format?: FormatLevel; all?: boolean }
502
536
  | { type: 'version' }
503
537
  | { type: 'completion'; shell?: ShellType; setup?: boolean }
538
+ | { type: 'man'; setup?: boolean; remove?: boolean }
504
539
  | { type: 'repl'; scope?: string }
540
+ | { type: 'mcp'; transport?: 'http' | 'stdio'; port?: number; host?: string; basePath?: string }
541
+ | { type: 'serve'; port?: number; host?: string; basePath?: string }
505
542
  | null => {
506
543
  if (!input) return null;
507
544
 
@@ -515,18 +552,21 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
515
552
  // Check for --help, -h flags (these take precedence over commands)
516
553
  const hasHelpFlag = args.some((p) => (p.type === 'named' && keyIs(p.key, 'help')) || (p.type === 'alias' && keyIs(p.key, 'h')));
517
554
 
518
- // Extract detail level from --detail=<level> or -d <level>
555
+ // Extract detail level from --detail[=<level>] or -d [<level>]
556
+ // Bare --detail (no value) defaults to 'full'
519
557
  const getDetailLevel = (): DetailLevel | undefined => {
520
558
  for (const arg of args) {
521
- if (arg.type === 'named' && keyIs(arg.key, 'detail') && typeof arg.value === 'string') {
522
- if (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full') {
559
+ if (arg.type === 'named' && keyIs(arg.key, 'detail')) {
560
+ if (typeof arg.value === 'string' && (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full')) {
523
561
  return arg.value;
524
562
  }
563
+ return 'full';
525
564
  }
526
- if (arg.type === 'alias' && keyIs(arg.key, 'd') && typeof arg.value === 'string') {
527
- if (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full') {
565
+ if (arg.type === 'alias' && keyIs(arg.key, 'd')) {
566
+ if (typeof arg.value === 'string' && (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full')) {
528
567
  return arg.value;
529
568
  }
569
+ return 'full';
530
570
  }
531
571
  }
532
572
  return undefined;
@@ -552,6 +592,9 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
552
592
  };
553
593
  const format = getFormat();
554
594
 
595
+ // Check for --all flag (show all built-in help)
596
+ const hasAllFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'all'));
597
+
555
598
  // Check for --version, -v, -V flags
556
599
  const hasVersionFlag = args.some(
557
600
  (p) => (p.type === 'named' && keyIs(p.key, 'version')) || (p.type === 'alias' && (keyIs(p.key, 'v') || keyIs(p.key, 'V'))),
@@ -572,7 +615,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
572
615
  // help <command> - get help for specific command
573
616
  const commandName = normalizedTerms.slice(1).join(' ');
574
617
  const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
575
- return { type: 'help', command: targetCommand, detail, format };
618
+ return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
576
619
  }
577
620
  if (!userHelpCommand && normalizedTerms.length > 0 && normalizedTerms[normalizedTerms.length - 1] === 'help') {
578
621
  // <command> help - get help for specific command (trailing form)
@@ -589,7 +632,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
589
632
  break;
590
633
  }
591
634
  }
592
- return { type: 'help', command: targetCommand, detail, format };
635
+ return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
593
636
  }
594
637
 
595
638
  // Check for 'version' command (only if user hasn't defined one)
@@ -606,13 +649,21 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
606
649
  return { type: 'completion', shell, setup };
607
650
  }
608
651
 
652
+ // Check for 'man' command (only if user hasn't defined one)
653
+ const userManCommand = findCommandByName('man', existingCommand.commands);
654
+ if (!userManCommand && normalizedTerms[0] === 'man') {
655
+ const setup = args.some((p) => p.type === 'named' && keyIs(p.key, 'setup'));
656
+ const remove = args.some((p) => p.type === 'named' && keyIs(p.key, 'remove'));
657
+ return { type: 'man', setup, remove };
658
+ }
659
+
609
660
  // Handle help flag - find the command being requested
610
661
  if (hasHelpFlag) {
611
662
  // Filter out help-related terms and flags to find the target command
612
663
  const commandTerms = normalizedTerms.filter((t) => t !== 'help');
613
664
  const commandName = commandTerms.join(' ');
614
665
  const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
615
- return { type: 'help', command: targetCommand, detail, format };
666
+ return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
616
667
  }
617
668
 
618
669
  // Handle version flag (only for root command, i.e., no subcommand terms)
@@ -620,6 +671,32 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
620
671
  return { type: 'version' };
621
672
  }
622
673
 
674
+ // Check for 'mcp' command (only if user hasn't defined one)
675
+ const userMcpCommand = findCommandByName('mcp', existingCommand.commands);
676
+ if (!userMcpCommand && normalizedTerms[0] === 'mcp') {
677
+ const transportArg = normalizedTerms[1];
678
+ const transport = transportArg === 'stdio' || transportArg === 'http' ? transportArg : undefined;
679
+ const portArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'port'));
680
+ const port = typeof portArg?.value === 'string' ? parseInt(portArg.value, 10) : undefined;
681
+ const hostArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'host'));
682
+ const host = typeof hostArg?.value === 'string' ? hostArg.value : undefined;
683
+ const basePathArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'base-path'));
684
+ const mcpBasePath = typeof basePathArg?.value === 'string' ? basePathArg.value : undefined;
685
+ return { type: 'mcp', transport, port: port && !Number.isNaN(port) ? port : undefined, host, basePath: mcpBasePath };
686
+ }
687
+
688
+ // Check for 'serve' command (only if user hasn't defined one)
689
+ const userServeCommand = findCommandByName('serve', existingCommand.commands);
690
+ if (!userServeCommand && normalizedTerms[0] === 'serve') {
691
+ const portArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'port'));
692
+ const port = typeof portArg?.value === 'string' ? parseInt(portArg.value, 10) : undefined;
693
+ const hostArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'host'));
694
+ const host = typeof hostArg?.value === 'string' ? hostArg.value : undefined;
695
+ const basePathArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'base-path'));
696
+ const basePath = typeof basePathArg?.value === 'string' ? basePathArg.value : undefined;
697
+ return { type: 'serve', port: port && !Number.isNaN(port) ? port : undefined, host, basePath };
698
+ }
699
+
623
700
  // Check for --repl flag
624
701
  const hasReplFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'repl'));
625
702
  if (hasReplFlag) {
@@ -650,6 +727,35 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
650
727
  return undefined;
651
728
  };
652
729
 
730
+ /**
731
+ * Extract --color flag from input.
732
+ * - `--color` or `--color=true` → use default theme
733
+ * - `--color=false` or `--no-color` → disable colors (text format)
734
+ * - `--color=<theme>` → use the named theme
735
+ * Returns `undefined` if no --color flag is present.
736
+ */
737
+ const extractColorFlag = (input: string | undefined): { theme?: ColorTheme | ColorConfig; disableColor?: boolean } | undefined => {
738
+ if (!input) return undefined;
739
+
740
+ const parts = parseCliInputToParts(input);
741
+ const args = parts.filter((p) => p.type === 'named');
742
+ const keyIs = (key: string[], name: string) => key.length === 1 && key[0] === name;
743
+
744
+ for (const arg of args) {
745
+ if (arg.type === 'named' && keyIs(arg.key, 'no-color')) {
746
+ return { disableColor: true };
747
+ }
748
+ if (arg.type === 'named' && keyIs(arg.key, 'color')) {
749
+ if (arg.negated) return { disableColor: true };
750
+ if (arg.value === undefined || arg.value === 'true') return { theme: 'default' };
751
+ if (arg.value === 'false') return { disableColor: true };
752
+ if (typeof arg.value === 'string' && arg.value in colorThemes) return { theme: arg.value as ColorTheme };
753
+ return undefined;
754
+ }
755
+ }
756
+ return undefined;
757
+ };
758
+
653
759
  /**
654
760
  * Core execution logic shared by eval() and cli().
655
761
  * errorMode controls validation error behavior:
@@ -658,38 +764,51 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
658
764
  */
659
765
  const execCommand = (resolvedInput: string | undefined, evalOptions?: PadroneEvalPreferences, errorMode: 'soft' | 'hard' = 'soft') => {
660
766
  const baseRuntime = getCommandRuntime(existingCommand);
661
- const runtime = evalOptions?.runtime
767
+ let runtime = evalOptions?.runtime
662
768
  ? Object.assign({}, baseRuntime, Object.fromEntries(Object.entries(evalOptions.runtime).filter(([, v]) => v !== undefined)))
663
769
  : baseRuntime;
664
770
 
771
+ // Apply --color / --no-color flag to runtime
772
+ const colorFlag = extractColorFlag(resolvedInput);
773
+ if (colorFlag) {
774
+ runtime = {
775
+ ...runtime,
776
+ ...(colorFlag.disableColor ? { format: 'text' as const, theme: undefined } : { theme: colorFlag.theme }),
777
+ };
778
+ }
779
+
665
780
  // Check for built-in help/version/completion commands and flags (bypass plugins)
666
781
  const builtin = checkBuiltinCommands(resolvedInput);
667
782
 
668
783
  if (builtin) {
669
784
  if (builtin.type === 'help') {
785
+ resolveAllCommands(existingCommand);
670
786
  const helpText = generateHelp(existingCommand, builtin.command ?? existingCommand, {
671
787
  detail: builtin.detail,
672
788
  format: builtin.format ?? runtime.format,
789
+ theme: runtime.theme,
790
+ all: builtin.all,
673
791
  });
674
792
  runtime.output(helpText);
675
- return {
793
+ return withDrain({
676
794
  command: existingCommand,
677
795
  args: undefined,
678
796
  result: helpText,
679
- } as any;
797
+ }) as any;
680
798
  }
681
799
 
682
800
  if (builtin.type === 'version') {
683
801
  const version = getVersion(existingCommand.version);
684
802
  runtime.output(version);
685
- return {
803
+ return withDrain({
686
804
  command: existingCommand,
687
805
  args: undefined,
688
806
  result: version,
689
- } as any;
807
+ }) as any;
690
808
  }
691
809
 
692
810
  if (builtin.type === 'completion') {
811
+ resolveAllCommands(existingCommand);
693
812
  return import('./completion.ts').then(({ detectShell, generateCompletionOutput, setupCompletions }) => {
694
813
  if (builtin.setup) {
695
814
  const shell = builtin.shell ?? detectShell();
@@ -699,19 +818,57 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
699
818
  const result = setupCompletions(existingCommand.name, shell);
700
819
  const message = `${result.updated ? 'Updated' : 'Added'} ${existingCommand.name} completions in ${result.file}`;
701
820
  runtime.output(message);
702
- return {
821
+ return withDrain({
703
822
  command: existingCommand,
704
823
  args: undefined,
705
824
  result: message,
706
- };
825
+ });
707
826
  }
708
827
  const completionScript = generateCompletionOutput(existingCommand, builtin.shell);
709
828
  runtime.output(completionScript);
710
- return {
829
+ return withDrain({
711
830
  command: existingCommand,
712
831
  args: undefined,
713
832
  result: completionScript,
714
- };
833
+ });
834
+ }) as any;
835
+ }
836
+
837
+ if (builtin.type === 'man') {
838
+ resolveAllCommands(existingCommand);
839
+ return import('./docs/index.ts').then(({ setupManPages, removeManPages, generateDocs }) => {
840
+ if (builtin.setup) {
841
+ const result = setupManPages(existingCommand);
842
+ const message = `${result.updated ? 'Updated' : 'Installed'} ${result.written.length} man page(s) in ${result.dir}`;
843
+ runtime.output(message);
844
+ return withDrain({
845
+ command: existingCommand,
846
+ args: undefined,
847
+ result: message,
848
+ });
849
+ }
850
+ if (builtin.remove) {
851
+ const result = removeManPages(existingCommand);
852
+ const message =
853
+ result.removed.length > 0
854
+ ? `Removed ${result.removed.length} man page(s) from ${result.dir}`
855
+ : 'No man pages found to remove.';
856
+ runtime.output(message);
857
+ return withDrain({
858
+ command: existingCommand,
859
+ args: undefined,
860
+ result: message,
861
+ });
862
+ }
863
+ // Default: generate man page for the root command and print it
864
+ const result = generateDocs(existingCommand, { format: 'man' });
865
+ const manPage = result.pages[0]?.content ?? '';
866
+ runtime.output(manPage);
867
+ return withDrain({
868
+ command: existingCommand,
869
+ args: undefined,
870
+ result: manPage,
871
+ });
715
872
  }) as any;
716
873
  }
717
874
  }
@@ -731,7 +888,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
731
888
  const hasSubcommands = command.commands && command.commands.length > 0;
732
889
  const hasSchema = command.argsSchema != null;
733
890
  if (!command.action && (hasSubcommands || !hasSchema) && unmatchedTerms.length === 0) {
734
- const helpText = generateHelp(existingCommand, command, { format: runtime.format });
891
+ resolveAllCommands(existingCommand);
892
+ const helpText = generateHelp(existingCommand, command, { format: runtime.format, theme: runtime.theme });
735
893
  runtime.output(helpText);
736
894
  return {
737
895
  command: command,
@@ -784,7 +942,11 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
784
942
  runtime.output(`\nAvailable commands: ${cmdList}`);
785
943
  }
786
944
  } else {
787
- const helpText = generateHelp(existingCommand, isRootCommand ? existingCommand : command, { format: runtime.format });
945
+ resolveAllCommands(existingCommand);
946
+ const helpText = generateHelp(existingCommand, isRootCommand ? existingCommand : command, {
947
+ format: runtime.format,
948
+ theme: runtime.theme,
949
+ });
788
950
  runtime.error(helpText);
789
951
  }
790
952
  throw new RoutingError(errorMsg, { suggestions, command: command.path || command.name });
@@ -816,6 +978,40 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
816
978
  } as any;
817
979
  }
818
980
 
981
+ // ── Auto-progress: start before validation ───────────────────────
982
+ const progressConfig = command.progress;
983
+ if (progressConfig && runtime.progress) {
984
+ const isObj = typeof progressConfig === 'object';
985
+ const defaultMsg = typeof progressConfig === 'string' ? progressConfig : `Running ${command.name}...`;
986
+ const progressMsg = isObj ? (progressConfig.progress ?? defaultMsg) : defaultMsg;
987
+ const validationMsg = isObj ? (progressConfig.validation ?? '') : '';
988
+ state._progressSuccess = isObj ? progressConfig.success : undefined;
989
+ state._progressError = isObj ? progressConfig.error : undefined;
990
+ state._progressMsg = progressMsg;
991
+ state._progressValidationMsg = validationMsg || undefined;
992
+ const spinnerConfig = isObj ? progressConfig.spinner : undefined;
993
+ const progressOptions = spinnerConfig !== undefined ? { spinner: spinnerConfig } : undefined;
994
+ const indicator = createProgress(runtime, validationMsg || progressMsg, progressOptions);
995
+ state._progress = indicator;
996
+
997
+ const originalOutput = runtime.output;
998
+ const originalError = runtime.error;
999
+ runtime.output = (...args: unknown[]) => {
1000
+ indicator.pause();
1001
+ originalOutput(...args);
1002
+ indicator.resume();
1003
+ };
1004
+ runtime.error = (text: string) => {
1005
+ indicator.pause();
1006
+ originalError(text);
1007
+ indicator.resume();
1008
+ };
1009
+ state._restoreOutput = () => {
1010
+ runtime.output = originalOutput;
1011
+ runtime.error = originalError;
1012
+ };
1013
+ }
1014
+
819
1015
  // ── Phase 2: Validate ───────────────────────────────────────────
820
1016
  const validateCtx: PluginValidateContext = {
821
1017
  command,
@@ -838,6 +1034,10 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
838
1034
  }
839
1035
  }
840
1036
 
1037
+ // Strip --color / --no-color from rawArgs (handled globally)
1038
+ delete validateCtx.rawArgs.color;
1039
+ delete validateCtx.rawArgs['no-color'];
1040
+
841
1041
  const runtimeDefault: boolean | undefined =
842
1042
  runtime.interactive === 'forced' ? true : runtime.interactive === 'disabled' ? false : undefined;
843
1043
  const effectiveInteractive: boolean | undefined = flagInteractive ?? evalOptions?.interactive ?? runtimeDefault;
@@ -927,17 +1127,24 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
927
1127
  const stdinConfig = command.meta?.stdin;
928
1128
  if (!stdinConfig) return {};
929
1129
 
930
- const { field, as } = parseStdinConfig(stdinConfig);
1130
+ const field = parseStdinConfig(stdinConfig);
931
1131
 
932
1132
  // Skip if the field was already provided via CLI flags (highest precedence)
933
1133
  if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
934
1134
 
1135
+ const streamInfo = isAsyncStreamField(command.argsSchema, field);
1136
+ if (streamInfo) {
1137
+ // Async stream: always resolve stdin (even on TTY) for interactive use
1138
+ const stdinForStream = resolveStdinAlways(runtime as any);
1139
+ return { [field]: createStdinStream(stdinForStream, streamInfo.itemSchema) };
1140
+ }
1141
+
935
1142
  // Resolve stdin: use runtime's custom stdin, or default if piped.
936
1143
  // Returns undefined when stdin is a TTY or unavailable.
937
1144
  const stdin = resolveStdin(runtime as any);
938
1145
  if (!stdin) return {};
939
1146
 
940
- if (as === 'lines') {
1147
+ if (isArrayField(command.argsSchema, field)) {
941
1148
  return (async () => {
942
1149
  const lines: string[] = [];
943
1150
  for await (const line of stdin.lines()) {
@@ -1056,7 +1263,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1056
1263
  knownOptions = [];
1057
1264
  if (command.argsSchema) {
1058
1265
  try {
1059
- const js = command.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
1266
+ const js = command.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
1060
1267
  if (js.type === 'object' && js.properties) knownOptions = Object.keys(js.properties);
1061
1268
  } catch {
1062
1269
  /* ignore */
@@ -1081,7 +1288,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1081
1288
  .join('\n');
1082
1289
 
1083
1290
  if (errorMode === 'hard') {
1084
- const helpText = generateHelp(existingCommand, command, { format: runtime.format });
1291
+ resolveAllCommands(existingCommand);
1292
+ const helpText = generateHelp(existingCommand, command, { format: runtime.format, theme: runtime.theme });
1085
1293
  runtime.error(`Validation error:\n${issueMessages}`);
1086
1294
  runtime.error(helpText);
1087
1295
  throw new ValidationError(`Validation error:\n${issueMessages}`, v.argsResult.issues as any, {
@@ -1095,12 +1303,18 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1095
1303
  }
1096
1304
 
1097
1305
  // Soft mode: return result with issues, skip the action
1098
- return {
1306
+ return withDrain({
1099
1307
  command: command as any,
1100
1308
  args: undefined,
1101
1309
  argsResult: v.argsResult,
1102
1310
  result: undefined,
1103
- };
1311
+ });
1312
+ }
1313
+
1314
+ // Update auto-progress message from validation to execute phase
1315
+ const activeIndicator = state._progress as import('./runtime.ts').PadroneProgressIndicator | undefined;
1316
+ if (activeIndicator && state._progressMsg && state._progressValidationMsg) {
1317
+ activeIndicator.update(state._progressMsg as string);
1104
1318
  }
1105
1319
 
1106
1320
  const executeCtx: PluginExecuteContext = {
@@ -1111,7 +1325,11 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1111
1325
 
1112
1326
  const coreExecute = (): PluginExecuteResult => {
1113
1327
  const handler = command.action ?? noop;
1114
- const ctx = evalOptions?.runtime ? { ...createActionContext(command), runtime } : createActionContext(command);
1328
+ const ctx: PadroneActionContext = {
1329
+ ...createActionContext(command),
1330
+ runtime,
1331
+ progress: (state._progress as PadroneProgressIndicator) ?? createLazyIndicator(runtime, state),
1332
+ };
1115
1333
  const result = handler(executeCtx.args as any, ctx);
1116
1334
  return { result };
1117
1335
  };
@@ -1119,40 +1337,89 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1119
1337
  const executedOrPromise = runPluginChain('execute', commandPlugins, executeCtx, coreExecute);
1120
1338
 
1121
1339
  return thenMaybe(executedOrPromise, (e) => {
1122
- const commandResult = {
1123
- command: command as any,
1124
- args: v.args,
1125
- argsResult: v.argsResult,
1126
- result: e.result,
1127
- };
1340
+ const finalize = (result: unknown) => {
1341
+ // Clean up progress before auto-output so the spinner clears first
1342
+ const indicator = state._progress as import('./runtime.ts').PadroneProgressIndicator | undefined;
1343
+ if (indicator) {
1344
+ const hasProgressConfig = '_progressMsg' in state;
1345
+ if (!hasProgressConfig) {
1346
+ // Lazy/manual indicator: just stop silently
1347
+ indicator.stop();
1348
+ } else {
1349
+ const { message: successMsg, indicator: successIcon } = resolveProgressMessage(state._progressSuccess, result);
1350
+ indicator.succeed(successMsg, successIcon !== undefined ? { indicator: successIcon } : undefined);
1351
+ }
1352
+ (state._restoreOutput as (() => void) | undefined)?.();
1353
+ state._progress = undefined;
1354
+ state._restoreOutput = undefined;
1355
+ }
1356
+
1357
+ const commandResult = withDrain({
1358
+ command: command as any,
1359
+ args: v.args,
1360
+ argsResult: v.argsResult,
1361
+ result,
1362
+ });
1128
1363
 
1129
- if (command.autoOutput ?? evalOptions?.autoOutput ?? true) {
1130
- const outputOrPromise = outputValue(e.result, runtime.output);
1131
- if (outputOrPromise instanceof Promise) {
1132
- return outputOrPromise.then(() => commandResult);
1364
+ if (command.autoOutput ?? evalOptions?.autoOutput ?? true) {
1365
+ const outputOrPromise = outputValue(result, runtime.output);
1366
+ if (outputOrPromise instanceof Promise) {
1367
+ return outputOrPromise.then(() => commandResult);
1368
+ }
1133
1369
  }
1370
+
1371
+ return commandResult;
1372
+ };
1373
+
1374
+ // If the action returned a Promise, wait for it before finalizing
1375
+ if (e.result instanceof Promise) {
1376
+ return e.result.then(finalize, (err: unknown) => {
1377
+ const indicator = state._progress as import('./runtime.ts').PadroneProgressIndicator | undefined;
1378
+ if (indicator) {
1379
+ const hasProgressConfig = '_progressMsg' in state;
1380
+ if (!hasProgressConfig) {
1381
+ indicator.stop();
1382
+ } else {
1383
+ const fallback = err instanceof Error ? err.message : String(err);
1384
+ const { message: errorMsg, indicator: errorIcon } = resolveProgressMessage(state._progressError, err, fallback);
1385
+ indicator.fail(errorMsg, errorIcon !== undefined ? { indicator: errorIcon } : undefined);
1386
+ }
1387
+ (state._restoreOutput as (() => void) | undefined)?.();
1388
+ state._progress = undefined;
1389
+ state._restoreOutput = undefined;
1390
+ }
1391
+ throw err;
1392
+ });
1134
1393
  }
1135
1394
 
1136
- return commandResult;
1395
+ return finalize(e.result);
1137
1396
  });
1138
1397
  };
1139
1398
 
1140
- return warnIfUnexpectedAsync(thenMaybe(validatedOrPromise, continueAfterValidate), command) as any;
1399
+ return thenMaybe(warnIfUnexpectedAsync(validatedOrPromise, command), continueAfterValidate) as any;
1141
1400
  };
1142
1401
 
1143
1402
  return thenMaybe(parsedOrPromise, continueAfterParse) as any;
1144
1403
  };
1145
1404
 
1146
- return wrapWithLifecycle(rootPlugins, existingCommand, state, resolvedInput, runPipeline, (result) => ({
1147
- command: existingCommand,
1148
- args: undefined,
1149
- argsResult: undefined,
1150
- result,
1151
- })) as any;
1405
+ return wrapWithLifecycle(rootPlugins, existingCommand, state, resolvedInput, runPipeline, (result) =>
1406
+ withDrain({
1407
+ command: existingCommand,
1408
+ args: undefined,
1409
+ argsResult: undefined,
1410
+ result,
1411
+ }),
1412
+ ) as any;
1152
1413
  };
1153
1414
 
1154
1415
  const evalCommand: AnyPadroneProgram['eval'] = (input, evalOptions) => {
1155
- return execCommand(input as string, evalOptions, 'soft');
1416
+ try {
1417
+ const result = execCommand(input as string, evalOptions, 'soft');
1418
+ if (result instanceof Promise) return withPromiseDrain(result.catch((err: unknown) => errorResult(err))) as any;
1419
+ return makeThenable(result);
1420
+ } catch (err) {
1421
+ return makeThenable(errorResult(err)) as any;
1422
+ }
1156
1423
  };
1157
1424
 
1158
1425
  /**
@@ -1165,8 +1432,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1165
1432
  * `.use()` was called on the program). We substitute `programRoot` for the
1166
1433
  * top of the chain to ensure program-level plugins are always included.
1167
1434
  */
1168
- const collectPlugins = (cmd: AnyPadroneCommand): PadronePlugin[] => {
1169
- const chain: PadronePlugin[][] = [];
1435
+ const collectPlugins = (cmd: AnyPadroneCommand): PadronePlugin<any, any>[] => {
1436
+ const chain: PadronePlugin<any, any>[][] = [];
1170
1437
  let current: AnyPadroneCommand | undefined = cmd;
1171
1438
  while (current) {
1172
1439
  // If this is the root (no parent), use existingCommand's plugins instead
@@ -1182,94 +1449,143 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1182
1449
  };
1183
1450
 
1184
1451
  // Forward declaration — assigned by the repl method in the return object, used by cli() for --repl.
1185
- let replFn: (options?: PadroneReplPreferences) => AsyncIterable<any>;
1452
+ const replFn = (options?: PadroneReplPreferences) => {
1453
+ return createReplIterator({ existingCommand, evalCommand, replActiveRef }, options);
1454
+ };
1186
1455
  const replActiveRef = { value: false };
1187
1456
 
1188
1457
  const cli: AnyPadroneProgram['cli'] = (cliOptions) => {
1189
- const runtime = getCommandRuntime(existingCommand);
1190
- const resolvedInput = (runtime.argv().join(' ') || undefined) as string | undefined;
1458
+ try {
1459
+ const runtime = getCommandRuntime(existingCommand);
1460
+ const resolvedInput = (runtime.argv().join(' ') || undefined) as string | undefined;
1191
1461
 
1192
- // Check for --repl flag before normal execution
1193
- if (cliOptions?.repl !== false) {
1462
+ // Check for --repl flag and mcp command before normal execution
1194
1463
  const builtin = checkBuiltinCommands(resolvedInput);
1195
- if (builtin?.type === 'repl') {
1464
+
1465
+ if (cliOptions?.repl !== false && builtin?.type === 'repl') {
1196
1466
  const replPrefs: PadroneReplPreferences = {
1197
1467
  ...(typeof cliOptions?.repl === 'object' ? cliOptions.repl : {}),
1198
1468
  scope: builtin.scope,
1199
1469
  autoOutput: (typeof cliOptions?.repl === 'object' ? cliOptions.repl.autoOutput : undefined) ?? cliOptions?.autoOutput,
1200
1470
  };
1471
+ const repl = replFn(replPrefs);
1201
1472
  const drainRepl = async () => {
1202
- for await (const _ of replFn(replPrefs)) {
1203
- // Results are handled by command actions
1204
- }
1205
- return { command: existingCommand, args: undefined, result: undefined } as any;
1473
+ const { value } = await repl.drain();
1474
+ return withDrain({ command: existingCommand, args: undefined, result: value }) as any;
1206
1475
  };
1207
- return drainRepl() as any;
1476
+ return withPromiseDrain(drainRepl()) as any;
1208
1477
  }
1209
- }
1210
1478
 
1211
- // Start background update check (non-blocking)
1212
- let updateCheckPromise: Promise<(() => void) | undefined> | undefined;
1213
- if (existingCommand.updateCheck) {
1214
- // Respect --no-update-check flag
1215
- const hasNoUpdateCheckFlag =
1216
- resolvedInput &&
1217
- parseCliInputToParts(resolvedInput).some((p) => p.type === 'named' && p.key.length === 1 && p.key[0] === 'no-update-check');
1218
- if (!hasNoUpdateCheckFlag) {
1219
- const currentVersion = getVersion(existingCommand.version);
1220
- updateCheckPromise = import('./update-check.ts').then(({ createUpdateChecker }) =>
1221
- createUpdateChecker(existingCommand.name, currentVersion, existingCommand.updateCheck!, runtime),
1222
- );
1479
+ if (cliOptions?.mcp !== false && builtin?.type === 'mcp') {
1480
+ const basePrefs = typeof cliOptions?.mcp === 'object' ? cliOptions.mcp : {};
1481
+ const mcpPrefs = {
1482
+ ...basePrefs,
1483
+ transport: builtin.transport ?? basePrefs.transport,
1484
+ port: builtin.port ?? basePrefs.port,
1485
+ host: builtin.host ?? basePrefs.host,
1486
+ basePath: builtin.basePath ?? basePrefs.basePath,
1487
+ };
1488
+ const startMcp = async () => {
1489
+ const { startMcpServer } = await import('./mcp.ts');
1490
+ await startMcpServer(builder as any, existingCommand, evalCommand, mcpPrefs);
1491
+ return withDrain({ command: existingCommand, args: undefined, result: undefined }) as any;
1492
+ };
1493
+ return withPromiseDrain(startMcp()) as any;
1223
1494
  }
1224
- }
1225
1495
 
1226
- const result = execCommand(resolvedInput, cliOptions, 'hard');
1496
+ if (cliOptions?.serve !== false && builtin?.type === 'serve') {
1497
+ const basePrefs = typeof cliOptions?.serve === 'object' ? cliOptions.serve : {};
1498
+ const servePrefs = {
1499
+ ...basePrefs,
1500
+ port: builtin.port ?? basePrefs.port,
1501
+ host: builtin.host ?? basePrefs.host,
1502
+ basePath: builtin.basePath ?? basePrefs.basePath,
1503
+ };
1504
+ const startServe = async () => {
1505
+ const { startServeServer } = await import('./serve.ts');
1506
+ await startServeServer(builder as any, existingCommand, evalCommand, servePrefs);
1507
+ return withDrain({ command: existingCommand, args: undefined, result: undefined }) as any;
1508
+ };
1509
+ return withPromiseDrain(startServe()) as any;
1510
+ }
1227
1511
 
1228
- // Show update notification after command output
1229
- if (updateCheckPromise) {
1230
- if (result instanceof Promise) {
1231
- return result.then(async (r) => {
1232
- const showUpdateNotification = await updateCheckPromise;
1233
- showUpdateNotification?.();
1234
- return r;
1235
- }) as any;
1512
+ // Start background update check (non-blocking)
1513
+ let updateCheckPromise: Promise<(() => void) | undefined> | undefined;
1514
+ if (existingCommand.updateCheck) {
1515
+ // Respect --no-update-check flag
1516
+ const hasNoUpdateCheckFlag =
1517
+ resolvedInput &&
1518
+ parseCliInputToParts(resolvedInput).some((p) => p.type === 'named' && p.key.length === 1 && p.key[0] === 'no-update-check');
1519
+ if (!hasNoUpdateCheckFlag) {
1520
+ const currentVersion = getVersion(existingCommand.version);
1521
+ updateCheckPromise = import('./update-check.ts').then(({ createUpdateChecker }) =>
1522
+ createUpdateChecker(existingCommand.name, currentVersion, existingCommand.updateCheck!, runtime),
1523
+ );
1524
+ }
1525
+ }
1526
+
1527
+ const result = execCommand(resolvedInput, cliOptions, 'hard');
1528
+
1529
+ // Show update notification after command output
1530
+ if (updateCheckPromise) {
1531
+ if (result instanceof Promise) {
1532
+ return withPromiseDrain(
1533
+ result
1534
+ .then(async (r) => {
1535
+ const showUpdateNotification = await updateCheckPromise;
1536
+ showUpdateNotification?.();
1537
+ return r;
1538
+ })
1539
+ .catch((err: unknown) => errorResult(err)),
1540
+ ) as any;
1541
+ }
1542
+ // For sync results, schedule notification for next tick (non-blocking)
1543
+ updateCheckPromise.then((show) => show?.());
1236
1544
  }
1237
- // For sync results, schedule notification for next tick (non-blocking)
1238
- updateCheckPromise.then((show) => show?.());
1239
- }
1240
1545
 
1241
- return result;
1546
+ if (result instanceof Promise) return withPromiseDrain(result.catch((err: unknown) => errorResult(err))) as any;
1547
+ return makeThenable(result);
1548
+ } catch (err) {
1549
+ return makeThenable(errorResult(err)) as any;
1550
+ }
1242
1551
  };
1243
1552
 
1244
1553
  const run: AnyPadroneProgram['run'] = (command, args) => {
1245
- const commandObj = typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
1246
- if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
1247
- if (!commandObj.action) throw new RoutingError(`Command "${commandObj.path}" has no action`, { command: commandObj.path });
1554
+ try {
1555
+ const commandObj =
1556
+ typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
1557
+ if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
1558
+ if (!commandObj.action) throw new RoutingError(`Command "${commandObj.path}" has no action`, { command: commandObj.path });
1248
1559
 
1249
- const state: Record<string, unknown> = {};
1250
- const executeCtx: PluginExecuteContext = { command: commandObj, args, state };
1560
+ const state: Record<string, unknown> = {};
1561
+ const executeCtx: PluginExecuteContext = { command: commandObj, args, state };
1251
1562
 
1252
- const coreExecute = (): PluginExecuteResult => {
1253
- const result = commandObj.action!(executeCtx.args as any, createActionContext(commandObj));
1254
- return { result };
1255
- };
1563
+ const coreExecute = (): PluginExecuteResult => {
1564
+ const result = commandObj.action!(executeCtx.args as any, createActionContext(commandObj));
1565
+ return { result };
1566
+ };
1256
1567
 
1257
- const commandObjPlugins = collectPlugins(commandObj);
1258
- const executedOrPromise = runPluginChain('execute', commandObjPlugins, executeCtx, coreExecute);
1568
+ const commandObjPlugins = collectPlugins(commandObj);
1569
+ const executedOrPromise = runPluginChain('execute', commandObjPlugins, executeCtx, coreExecute);
1259
1570
 
1260
- const toResult = (e: PluginExecuteResult) => ({
1261
- command: commandObj as any,
1262
- args: args as any,
1263
- result: e.result,
1264
- });
1571
+ const toResult = (e: PluginExecuteResult) =>
1572
+ withDrain({
1573
+ command: commandObj as any,
1574
+ args: args as any,
1575
+ result: e.result,
1576
+ });
1265
1577
 
1266
- if (executedOrPromise instanceof Promise) {
1267
- return executedOrPromise.then(toResult) as any;
1578
+ if (executedOrPromise instanceof Promise) {
1579
+ return executedOrPromise.then(toResult).catch((err: unknown) => errorResult(err, { command: commandObj, args })) as any;
1580
+ }
1581
+ return toResult(executedOrPromise);
1582
+ } catch (err) {
1583
+ return errorResult(err) as any;
1268
1584
  }
1269
- return toResult(executedOrPromise);
1270
1585
  };
1271
1586
 
1272
1587
  const tool: AnyPadroneProgram['tool'] = () => {
1588
+ resolveAllCommands(existingCommand);
1273
1589
  const helpText = generateHelp(existingCommand, undefined, { format: 'text' });
1274
1590
 
1275
1591
  const description = `Run a command. Pass the full command string including arguments. Use "help <command>" for detailed usage.\n\n${helpText}`;
@@ -1298,7 +1614,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1298
1614
  needsApproval: async (input) => {
1299
1615
  const parsed = await parse(input.command);
1300
1616
  if (typeof parsed.command.needsApproval === 'function') return parsed.command.needsApproval(parsed.args);
1301
- return !!parsed.command.needsApproval;
1617
+ if (parsed.command.needsApproval != null) return !!parsed.command.needsApproval;
1618
+ return !!parsed.command.mutation;
1302
1619
  },
1303
1620
  execute: async (input) => {
1304
1621
  const output: string[] = [];
@@ -1344,6 +1661,10 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1344
1661
  const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedEnv);
1345
1662
  return createPadroneBuilder({ ...existingCommand, envSchema: resolvedEnv as any, isAsync }) as any;
1346
1663
  },
1664
+ progress(config = true) {
1665
+ const progress = typeof config === 'boolean' || typeof config === 'string' ? config : { ...config };
1666
+ return createPadroneBuilder({ ...existingCommand, progress }) as any;
1667
+ },
1347
1668
  action(handler = noop) {
1348
1669
  const baseHandler = existingCommand.action ?? noop;
1349
1670
  return createPadroneBuilder({
@@ -1363,6 +1684,9 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1363
1684
  // Check if a command with this name already exists (override case)
1364
1685
  const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
1365
1686
 
1687
+ // For override case, resolve the existing lazy command first so the builder starts with full state
1688
+ if (existingSubcommand) resolveCommand(existingSubcommand);
1689
+
1366
1690
  const initialCommand: AnyPadroneCommand = existingSubcommand
1367
1691
  ? { ...existingSubcommand, aliases: aliases ?? existingSubcommand.aliases, parent: existingCommand }
1368
1692
  : ({
@@ -1373,21 +1697,33 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1373
1697
  '~types': {} as any,
1374
1698
  } satisfies PadroneCommand);
1375
1699
 
1376
- const builder = createPadroneBuilder(initialCommand);
1700
+ // Lazy initialization: defer builderFn invocation until the command is actually needed
1701
+ if (builderFn) {
1702
+ const lazyCmd: AnyPadroneCommand = { ...initialCommand };
1703
+ (lazyCmd as any)[lazyResolver] = (target: AnyPadroneCommand) => {
1704
+ const builder = createPadroneBuilder(target);
1705
+ const commandObj = ((builderFn(builder as any) as unknown as typeof builder)?.[commandSymbol] as AnyPadroneCommand) ?? target;
1706
+ const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, commandObj) : commandObj;
1707
+ Object.assign(target, mergedCommandObj);
1708
+ };
1377
1709
 
1378
- const commandObj =
1379
- ((builderFn?.(builder as any) as unknown as typeof builder)?.[commandSymbol] as AnyPadroneCommand) ?? initialCommand;
1710
+ const commands = existingCommand.commands || [];
1711
+ const existingIndex = commands.findIndex((c) => c.name === name);
1712
+ const updatedCommands =
1713
+ existingIndex >= 0
1714
+ ? [...commands.slice(0, existingIndex), lazyCmd, ...commands.slice(existingIndex + 1)]
1715
+ : [...commands, lazyCmd];
1380
1716
 
1381
- // Merge subcommands when overriding: existing subcommands that aren't replaced are kept
1382
- const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, commandObj) : commandObj;
1717
+ return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
1718
+ }
1383
1719
 
1384
- // Replace existing command or append new one
1720
+ // No builderFn: use the initial command as-is (no lazy resolution needed)
1385
1721
  const commands = existingCommand.commands || [];
1386
1722
  const existingIndex = commands.findIndex((c) => c.name === name);
1387
1723
  const updatedCommands =
1388
1724
  existingIndex >= 0
1389
- ? [...commands.slice(0, existingIndex), mergedCommandObj, ...commands.slice(existingIndex + 1)]
1390
- : [...commands, mergedCommandObj];
1725
+ ? [...commands.slice(0, existingIndex), initialCommand, ...commands.slice(existingIndex + 1)]
1726
+ : [...commands, initialCommand];
1391
1727
 
1392
1728
  return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
1393
1729
  },
@@ -1418,7 +1754,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1418
1754
  return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
1419
1755
  },
1420
1756
 
1421
- use(plugin: PadronePlugin) {
1757
+ use(plugin: PadronePlugin<any, any>) {
1422
1758
  return createPadroneBuilder({
1423
1759
  ...existingCommand,
1424
1760
  plugins: [...(existingCommand.plugins ?? []), plugin],
@@ -1437,11 +1773,10 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1437
1773
  cli,
1438
1774
  tool,
1439
1775
 
1440
- repl: (replFn = (options?: PadroneReplPreferences) => {
1441
- return createReplIterator({ existingCommand, evalCommand, replActiveRef }, options);
1442
- }),
1776
+ repl: replFn,
1443
1777
 
1444
1778
  api() {
1779
+ resolveAllCommands(existingCommand);
1445
1780
  function buildApi(command: AnyPadroneCommand) {
1446
1781
  const runCommand = ((args) => run(command, args).result) as PadroneAPI<AnyPadroneCommand>;
1447
1782
  if (!command.commands) return runCommand;
@@ -1453,6 +1788,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1453
1788
  },
1454
1789
 
1455
1790
  help(command, prefs) {
1791
+ resolveAllCommands(existingCommand);
1456
1792
  const commandObj = !command
1457
1793
  ? existingCommand
1458
1794
  : typeof command === 'string'
@@ -1460,14 +1796,31 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1460
1796
  : (command as AnyPadroneCommand);
1461
1797
  if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
1462
1798
  const runtime = getCommandRuntime(existingCommand);
1463
- return generateHelp(existingCommand, commandObj, { ...prefs, format: prefs?.format ?? runtime.format });
1799
+ return generateHelp(existingCommand, commandObj, {
1800
+ ...prefs,
1801
+ format: prefs?.format ?? runtime.format,
1802
+ theme: prefs?.theme ?? runtime.theme,
1803
+ });
1464
1804
  },
1465
1805
 
1466
1806
  async completion(shell) {
1807
+ resolveAllCommands(existingCommand);
1467
1808
  const { generateCompletionOutput } = await import('./completion.ts');
1468
1809
  return generateCompletionOutput(existingCommand, shell as ShellType | undefined);
1469
1810
  },
1470
1811
 
1812
+ async mcp(prefs) {
1813
+ resolveAllCommands(existingCommand);
1814
+ const { startMcpServer } = await import('./mcp.ts');
1815
+ return startMcpServer(builder as any, existingCommand, evalCommand, prefs);
1816
+ },
1817
+
1818
+ async serve(prefs) {
1819
+ resolveAllCommands(existingCommand);
1820
+ const { startServeServer } = await import('./serve.ts');
1821
+ return startServeServer(builder as any, existingCommand, evalCommand, prefs);
1822
+ },
1823
+
1471
1824
  '~types': {} as any,
1472
1825
 
1473
1826
  [commandSymbol]: existingCommand,