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/create.ts CHANGED
@@ -1,21 +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,
10
25
  makeThenable,
11
26
  mergeCommands,
12
27
  noop,
28
+ noopIndicator,
13
29
  outputValue,
14
30
  repathCommandTree,
31
+ resolveAllCommands,
32
+ resolveCommand,
33
+ resolveProgressMessage,
15
34
  runPluginChain,
16
35
  suggestSimilar,
17
36
  thenMaybe,
18
37
  warnIfUnexpectedAsync,
38
+ withDrain,
39
+ withPromiseDrain,
19
40
  wrapWithLifecycle,
20
41
  } from './command-utils.ts';
21
42
  import type { ShellType } from './completion.ts';
@@ -24,7 +45,8 @@ import { generateHelp } from './help.ts';
24
45
  import { promptInteractiveFields } from './interactive.ts';
25
46
  import { getNestedValue, parseCliInputToParts, setNestedValue } from './parse.ts';
26
47
  import { createReplIterator } from './repl-loop.ts';
27
- import { resolveStdin } from './runtime.ts';
48
+ import { type PadroneProgressIndicator, resolveStdin, resolveStdinAlways } from './runtime.ts';
49
+ import { createStdinStream } from './stream.ts';
28
50
  import type {
29
51
  AnyPadroneCommand,
30
52
  AnyPadroneProgram,
@@ -65,11 +87,14 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
65
87
  : inputCommand;
66
88
 
67
89
  /** Creates the action context passed to command handlers. References `builder` which is defined later but only called at runtime. */
68
- const createActionContext = (cmd: AnyPadroneCommand): PadroneActionContext => ({
69
- runtime: getCommandRuntime(cmd),
70
- command: cmd,
71
- program: builder as any,
72
- });
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
+ };
73
98
 
74
99
  const find: AnyPadroneProgram['find'] = (command) => {
75
100
  if (typeof command !== 'string') return findCommandByName(command.path, existingCommand.commands) as any;
@@ -136,7 +161,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
136
161
  const arrayArguments = new Set<string>();
137
162
  if (curCommand.argsSchema) {
138
163
  try {
139
- 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>;
140
165
  if (jsonSchema.type === 'object' && jsonSchema.properties) {
141
166
  for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
142
167
  if (prop?.type === 'array') arrayArguments.add(key);
@@ -352,16 +377,24 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
352
377
  const stdinConfig = command.meta?.stdin;
353
378
  if (!stdinConfig) return {};
354
379
 
355
- const { field, as } = parseStdinConfig(stdinConfig);
380
+ const field = parseStdinConfig(stdinConfig);
356
381
 
357
382
  // Skip if the field was already provided via CLI flags
358
383
  if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
359
384
 
360
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
+
361
394
  const stdin = resolveStdin(runtime as any);
362
395
  if (!stdin) return {};
363
396
 
364
- if (as === 'lines') {
397
+ if (isArrayField(command.argsSchema, field)) {
365
398
  return (async () => {
366
399
  const lines: string[] = [];
367
400
  for await (const line of stdin.lines()) {
@@ -499,10 +532,13 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
499
532
  const checkBuiltinCommands = (
500
533
  input: string | undefined,
501
534
  ):
502
- | { type: 'help'; command?: AnyPadroneCommand; detail?: DetailLevel; format?: FormatLevel }
535
+ | { type: 'help'; command?: AnyPadroneCommand; detail?: DetailLevel; format?: FormatLevel; all?: boolean }
503
536
  | { type: 'version' }
504
537
  | { type: 'completion'; shell?: ShellType; setup?: boolean }
538
+ | { type: 'man'; setup?: boolean; remove?: boolean }
505
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 }
506
542
  | null => {
507
543
  if (!input) return null;
508
544
 
@@ -516,18 +552,21 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
516
552
  // Check for --help, -h flags (these take precedence over commands)
517
553
  const hasHelpFlag = args.some((p) => (p.type === 'named' && keyIs(p.key, 'help')) || (p.type === 'alias' && keyIs(p.key, 'h')));
518
554
 
519
- // 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'
520
557
  const getDetailLevel = (): DetailLevel | undefined => {
521
558
  for (const arg of args) {
522
- if (arg.type === 'named' && keyIs(arg.key, 'detail') && typeof arg.value === 'string') {
523
- 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')) {
524
561
  return arg.value;
525
562
  }
563
+ return 'full';
526
564
  }
527
- if (arg.type === 'alias' && keyIs(arg.key, 'd') && typeof arg.value === 'string') {
528
- 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')) {
529
567
  return arg.value;
530
568
  }
569
+ return 'full';
531
570
  }
532
571
  }
533
572
  return undefined;
@@ -553,6 +592,9 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
553
592
  };
554
593
  const format = getFormat();
555
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
+
556
598
  // Check for --version, -v, -V flags
557
599
  const hasVersionFlag = args.some(
558
600
  (p) => (p.type === 'named' && keyIs(p.key, 'version')) || (p.type === 'alias' && (keyIs(p.key, 'v') || keyIs(p.key, 'V'))),
@@ -573,7 +615,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
573
615
  // help <command> - get help for specific command
574
616
  const commandName = normalizedTerms.slice(1).join(' ');
575
617
  const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
576
- return { type: 'help', command: targetCommand, detail, format };
618
+ return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
577
619
  }
578
620
  if (!userHelpCommand && normalizedTerms.length > 0 && normalizedTerms[normalizedTerms.length - 1] === 'help') {
579
621
  // <command> help - get help for specific command (trailing form)
@@ -590,7 +632,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
590
632
  break;
591
633
  }
592
634
  }
593
- return { type: 'help', command: targetCommand, detail, format };
635
+ return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
594
636
  }
595
637
 
596
638
  // Check for 'version' command (only if user hasn't defined one)
@@ -607,13 +649,21 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
607
649
  return { type: 'completion', shell, setup };
608
650
  }
609
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
+
610
660
  // Handle help flag - find the command being requested
611
661
  if (hasHelpFlag) {
612
662
  // Filter out help-related terms and flags to find the target command
613
663
  const commandTerms = normalizedTerms.filter((t) => t !== 'help');
614
664
  const commandName = commandTerms.join(' ');
615
665
  const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
616
- return { type: 'help', command: targetCommand, detail, format };
666
+ return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
617
667
  }
618
668
 
619
669
  // Handle version flag (only for root command, i.e., no subcommand terms)
@@ -621,6 +671,32 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
621
671
  return { type: 'version' };
622
672
  }
623
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
+
624
700
  // Check for --repl flag
625
701
  const hasReplFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'repl'));
626
702
  if (hasReplFlag) {
@@ -651,6 +727,35 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
651
727
  return undefined;
652
728
  };
653
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
+
654
759
  /**
655
760
  * Core execution logic shared by eval() and cli().
656
761
  * errorMode controls validation error behavior:
@@ -659,38 +764,51 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
659
764
  */
660
765
  const execCommand = (resolvedInput: string | undefined, evalOptions?: PadroneEvalPreferences, errorMode: 'soft' | 'hard' = 'soft') => {
661
766
  const baseRuntime = getCommandRuntime(existingCommand);
662
- const runtime = evalOptions?.runtime
767
+ let runtime = evalOptions?.runtime
663
768
  ? Object.assign({}, baseRuntime, Object.fromEntries(Object.entries(evalOptions.runtime).filter(([, v]) => v !== undefined)))
664
769
  : baseRuntime;
665
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
+
666
780
  // Check for built-in help/version/completion commands and flags (bypass plugins)
667
781
  const builtin = checkBuiltinCommands(resolvedInput);
668
782
 
669
783
  if (builtin) {
670
784
  if (builtin.type === 'help') {
785
+ resolveAllCommands(existingCommand);
671
786
  const helpText = generateHelp(existingCommand, builtin.command ?? existingCommand, {
672
787
  detail: builtin.detail,
673
788
  format: builtin.format ?? runtime.format,
789
+ theme: runtime.theme,
790
+ all: builtin.all,
674
791
  });
675
792
  runtime.output(helpText);
676
- return {
793
+ return withDrain({
677
794
  command: existingCommand,
678
795
  args: undefined,
679
796
  result: helpText,
680
- } as any;
797
+ }) as any;
681
798
  }
682
799
 
683
800
  if (builtin.type === 'version') {
684
801
  const version = getVersion(existingCommand.version);
685
802
  runtime.output(version);
686
- return {
803
+ return withDrain({
687
804
  command: existingCommand,
688
805
  args: undefined,
689
806
  result: version,
690
- } as any;
807
+ }) as any;
691
808
  }
692
809
 
693
810
  if (builtin.type === 'completion') {
811
+ resolveAllCommands(existingCommand);
694
812
  return import('./completion.ts').then(({ detectShell, generateCompletionOutput, setupCompletions }) => {
695
813
  if (builtin.setup) {
696
814
  const shell = builtin.shell ?? detectShell();
@@ -700,19 +818,57 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
700
818
  const result = setupCompletions(existingCommand.name, shell);
701
819
  const message = `${result.updated ? 'Updated' : 'Added'} ${existingCommand.name} completions in ${result.file}`;
702
820
  runtime.output(message);
703
- return {
821
+ return withDrain({
704
822
  command: existingCommand,
705
823
  args: undefined,
706
824
  result: message,
707
- };
825
+ });
708
826
  }
709
827
  const completionScript = generateCompletionOutput(existingCommand, builtin.shell);
710
828
  runtime.output(completionScript);
711
- return {
829
+ return withDrain({
712
830
  command: existingCommand,
713
831
  args: undefined,
714
832
  result: completionScript,
715
- };
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
+ });
716
872
  }) as any;
717
873
  }
718
874
  }
@@ -732,7 +888,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
732
888
  const hasSubcommands = command.commands && command.commands.length > 0;
733
889
  const hasSchema = command.argsSchema != null;
734
890
  if (!command.action && (hasSubcommands || !hasSchema) && unmatchedTerms.length === 0) {
735
- const helpText = generateHelp(existingCommand, command, { format: runtime.format });
891
+ resolveAllCommands(existingCommand);
892
+ const helpText = generateHelp(existingCommand, command, { format: runtime.format, theme: runtime.theme });
736
893
  runtime.output(helpText);
737
894
  return {
738
895
  command: command,
@@ -785,7 +942,11 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
785
942
  runtime.output(`\nAvailable commands: ${cmdList}`);
786
943
  }
787
944
  } else {
788
- 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
+ });
789
950
  runtime.error(helpText);
790
951
  }
791
952
  throw new RoutingError(errorMsg, { suggestions, command: command.path || command.name });
@@ -817,6 +978,40 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
817
978
  } as any;
818
979
  }
819
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
+
820
1015
  // ── Phase 2: Validate ───────────────────────────────────────────
821
1016
  const validateCtx: PluginValidateContext = {
822
1017
  command,
@@ -839,6 +1034,10 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
839
1034
  }
840
1035
  }
841
1036
 
1037
+ // Strip --color / --no-color from rawArgs (handled globally)
1038
+ delete validateCtx.rawArgs.color;
1039
+ delete validateCtx.rawArgs['no-color'];
1040
+
842
1041
  const runtimeDefault: boolean | undefined =
843
1042
  runtime.interactive === 'forced' ? true : runtime.interactive === 'disabled' ? false : undefined;
844
1043
  const effectiveInteractive: boolean | undefined = flagInteractive ?? evalOptions?.interactive ?? runtimeDefault;
@@ -928,17 +1127,24 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
928
1127
  const stdinConfig = command.meta?.stdin;
929
1128
  if (!stdinConfig) return {};
930
1129
 
931
- const { field, as } = parseStdinConfig(stdinConfig);
1130
+ const field = parseStdinConfig(stdinConfig);
932
1131
 
933
1132
  // Skip if the field was already provided via CLI flags (highest precedence)
934
1133
  if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
935
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
+
936
1142
  // Resolve stdin: use runtime's custom stdin, or default if piped.
937
1143
  // Returns undefined when stdin is a TTY or unavailable.
938
1144
  const stdin = resolveStdin(runtime as any);
939
1145
  if (!stdin) return {};
940
1146
 
941
- if (as === 'lines') {
1147
+ if (isArrayField(command.argsSchema, field)) {
942
1148
  return (async () => {
943
1149
  const lines: string[] = [];
944
1150
  for await (const line of stdin.lines()) {
@@ -1057,7 +1263,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1057
1263
  knownOptions = [];
1058
1264
  if (command.argsSchema) {
1059
1265
  try {
1060
- 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>;
1061
1267
  if (js.type === 'object' && js.properties) knownOptions = Object.keys(js.properties);
1062
1268
  } catch {
1063
1269
  /* ignore */
@@ -1082,7 +1288,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1082
1288
  .join('\n');
1083
1289
 
1084
1290
  if (errorMode === 'hard') {
1085
- const helpText = generateHelp(existingCommand, command, { format: runtime.format });
1291
+ resolveAllCommands(existingCommand);
1292
+ const helpText = generateHelp(existingCommand, command, { format: runtime.format, theme: runtime.theme });
1086
1293
  runtime.error(`Validation error:\n${issueMessages}`);
1087
1294
  runtime.error(helpText);
1088
1295
  throw new ValidationError(`Validation error:\n${issueMessages}`, v.argsResult.issues as any, {
@@ -1096,12 +1303,18 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1096
1303
  }
1097
1304
 
1098
1305
  // Soft mode: return result with issues, skip the action
1099
- return {
1306
+ return withDrain({
1100
1307
  command: command as any,
1101
1308
  args: undefined,
1102
1309
  argsResult: v.argsResult,
1103
1310
  result: undefined,
1104
- };
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);
1105
1318
  }
1106
1319
 
1107
1320
  const executeCtx: PluginExecuteContext = {
@@ -1112,7 +1325,11 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1112
1325
 
1113
1326
  const coreExecute = (): PluginExecuteResult => {
1114
1327
  const handler = command.action ?? noop;
1115
- 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
+ };
1116
1333
  const result = handler(executeCtx.args as any, ctx);
1117
1334
  return { result };
1118
1335
  };
@@ -1120,21 +1337,62 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1120
1337
  const executedOrPromise = runPluginChain('execute', commandPlugins, executeCtx, coreExecute);
1121
1338
 
1122
1339
  return thenMaybe(executedOrPromise, (e) => {
1123
- const commandResult = {
1124
- command: command as any,
1125
- args: v.args,
1126
- argsResult: v.argsResult,
1127
- result: e.result,
1128
- };
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
+ });
1129
1363
 
1130
- if (command.autoOutput ?? evalOptions?.autoOutput ?? true) {
1131
- const outputOrPromise = outputValue(e.result, runtime.output);
1132
- if (outputOrPromise instanceof Promise) {
1133
- 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
+ }
1134
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
+ });
1135
1393
  }
1136
1394
 
1137
- return commandResult;
1395
+ return finalize(e.result);
1138
1396
  });
1139
1397
  };
1140
1398
 
@@ -1144,16 +1402,24 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1144
1402
  return thenMaybe(parsedOrPromise, continueAfterParse) as any;
1145
1403
  };
1146
1404
 
1147
- return wrapWithLifecycle(rootPlugins, existingCommand, state, resolvedInput, runPipeline, (result) => ({
1148
- command: existingCommand,
1149
- args: undefined,
1150
- argsResult: undefined,
1151
- result,
1152
- })) 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;
1153
1413
  };
1154
1414
 
1155
1415
  const evalCommand: AnyPadroneProgram['eval'] = (input, evalOptions) => {
1156
- return makeThenable(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
+ }
1157
1423
  };
1158
1424
 
1159
1425
  /**
@@ -1166,8 +1432,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1166
1432
  * `.use()` was called on the program). We substitute `programRoot` for the
1167
1433
  * top of the chain to ensure program-level plugins are always included.
1168
1434
  */
1169
- const collectPlugins = (cmd: AnyPadroneCommand): PadronePlugin[] => {
1170
- const chain: PadronePlugin[][] = [];
1435
+ const collectPlugins = (cmd: AnyPadroneCommand): PadronePlugin<any, any>[] => {
1436
+ const chain: PadronePlugin<any, any>[][] = [];
1171
1437
  let current: AnyPadroneCommand | undefined = cmd;
1172
1438
  while (current) {
1173
1439
  // If this is the root (no parent), use existingCommand's plugins instead
@@ -1183,94 +1449,143 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1183
1449
  };
1184
1450
 
1185
1451
  // Forward declaration — assigned by the repl method in the return object, used by cli() for --repl.
1186
- let replFn: (options?: PadroneReplPreferences) => AsyncIterable<any>;
1452
+ const replFn = (options?: PadroneReplPreferences) => {
1453
+ return createReplIterator({ existingCommand, evalCommand, replActiveRef }, options);
1454
+ };
1187
1455
  const replActiveRef = { value: false };
1188
1456
 
1189
1457
  const cli: AnyPadroneProgram['cli'] = (cliOptions) => {
1190
- const runtime = getCommandRuntime(existingCommand);
1191
- 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;
1192
1461
 
1193
- // Check for --repl flag before normal execution
1194
- if (cliOptions?.repl !== false) {
1462
+ // Check for --repl flag and mcp command before normal execution
1195
1463
  const builtin = checkBuiltinCommands(resolvedInput);
1196
- if (builtin?.type === 'repl') {
1464
+
1465
+ if (cliOptions?.repl !== false && builtin?.type === 'repl') {
1197
1466
  const replPrefs: PadroneReplPreferences = {
1198
1467
  ...(typeof cliOptions?.repl === 'object' ? cliOptions.repl : {}),
1199
1468
  scope: builtin.scope,
1200
1469
  autoOutput: (typeof cliOptions?.repl === 'object' ? cliOptions.repl.autoOutput : undefined) ?? cliOptions?.autoOutput,
1201
1470
  };
1471
+ const repl = replFn(replPrefs);
1202
1472
  const drainRepl = async () => {
1203
- for await (const _ of replFn(replPrefs)) {
1204
- // Results are handled by command actions
1205
- }
1206
- 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;
1207
1475
  };
1208
- return drainRepl() as any;
1476
+ return withPromiseDrain(drainRepl()) as any;
1209
1477
  }
1210
- }
1211
1478
 
1212
- // Start background update check (non-blocking)
1213
- let updateCheckPromise: Promise<(() => void) | undefined> | undefined;
1214
- if (existingCommand.updateCheck) {
1215
- // Respect --no-update-check flag
1216
- const hasNoUpdateCheckFlag =
1217
- resolvedInput &&
1218
- parseCliInputToParts(resolvedInput).some((p) => p.type === 'named' && p.key.length === 1 && p.key[0] === 'no-update-check');
1219
- if (!hasNoUpdateCheckFlag) {
1220
- const currentVersion = getVersion(existingCommand.version);
1221
- updateCheckPromise = import('./update-check.ts').then(({ createUpdateChecker }) =>
1222
- createUpdateChecker(existingCommand.name, currentVersion, existingCommand.updateCheck!, runtime),
1223
- );
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;
1224
1494
  }
1225
- }
1226
1495
 
1227
- 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
+ }
1228
1511
 
1229
- // Show update notification after command output
1230
- if (updateCheckPromise) {
1231
- if (result instanceof Promise) {
1232
- return result.then(async (r) => {
1233
- const showUpdateNotification = await updateCheckPromise;
1234
- showUpdateNotification?.();
1235
- return r;
1236
- }) 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?.());
1237
1544
  }
1238
- // For sync results, schedule notification for next tick (non-blocking)
1239
- updateCheckPromise.then((show) => show?.());
1240
- }
1241
1545
 
1242
- return makeThenable(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
+ }
1243
1551
  };
1244
1552
 
1245
1553
  const run: AnyPadroneProgram['run'] = (command, args) => {
1246
- const commandObj = typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
1247
- if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
1248
- 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 });
1249
1559
 
1250
- const state: Record<string, unknown> = {};
1251
- const executeCtx: PluginExecuteContext = { command: commandObj, args, state };
1560
+ const state: Record<string, unknown> = {};
1561
+ const executeCtx: PluginExecuteContext = { command: commandObj, args, state };
1252
1562
 
1253
- const coreExecute = (): PluginExecuteResult => {
1254
- const result = commandObj.action!(executeCtx.args as any, createActionContext(commandObj));
1255
- return { result };
1256
- };
1563
+ const coreExecute = (): PluginExecuteResult => {
1564
+ const result = commandObj.action!(executeCtx.args as any, createActionContext(commandObj));
1565
+ return { result };
1566
+ };
1257
1567
 
1258
- const commandObjPlugins = collectPlugins(commandObj);
1259
- const executedOrPromise = runPluginChain('execute', commandObjPlugins, executeCtx, coreExecute);
1568
+ const commandObjPlugins = collectPlugins(commandObj);
1569
+ const executedOrPromise = runPluginChain('execute', commandObjPlugins, executeCtx, coreExecute);
1260
1570
 
1261
- const toResult = (e: PluginExecuteResult) => ({
1262
- command: commandObj as any,
1263
- args: args as any,
1264
- result: e.result,
1265
- });
1571
+ const toResult = (e: PluginExecuteResult) =>
1572
+ withDrain({
1573
+ command: commandObj as any,
1574
+ args: args as any,
1575
+ result: e.result,
1576
+ });
1266
1577
 
1267
- if (executedOrPromise instanceof Promise) {
1268
- 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;
1269
1584
  }
1270
- return toResult(executedOrPromise);
1271
1585
  };
1272
1586
 
1273
1587
  const tool: AnyPadroneProgram['tool'] = () => {
1588
+ resolveAllCommands(existingCommand);
1274
1589
  const helpText = generateHelp(existingCommand, undefined, { format: 'text' });
1275
1590
 
1276
1591
  const description = `Run a command. Pass the full command string including arguments. Use "help <command>" for detailed usage.\n\n${helpText}`;
@@ -1299,7 +1614,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1299
1614
  needsApproval: async (input) => {
1300
1615
  const parsed = await parse(input.command);
1301
1616
  if (typeof parsed.command.needsApproval === 'function') return parsed.command.needsApproval(parsed.args);
1302
- return !!parsed.command.needsApproval;
1617
+ if (parsed.command.needsApproval != null) return !!parsed.command.needsApproval;
1618
+ return !!parsed.command.mutation;
1303
1619
  },
1304
1620
  execute: async (input) => {
1305
1621
  const output: string[] = [];
@@ -1345,6 +1661,10 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1345
1661
  const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedEnv);
1346
1662
  return createPadroneBuilder({ ...existingCommand, envSchema: resolvedEnv as any, isAsync }) as any;
1347
1663
  },
1664
+ progress(config = true) {
1665
+ const progress = typeof config === 'boolean' || typeof config === 'string' ? config : { ...config };
1666
+ return createPadroneBuilder({ ...existingCommand, progress }) as any;
1667
+ },
1348
1668
  action(handler = noop) {
1349
1669
  const baseHandler = existingCommand.action ?? noop;
1350
1670
  return createPadroneBuilder({
@@ -1364,6 +1684,9 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1364
1684
  // Check if a command with this name already exists (override case)
1365
1685
  const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
1366
1686
 
1687
+ // For override case, resolve the existing lazy command first so the builder starts with full state
1688
+ if (existingSubcommand) resolveCommand(existingSubcommand);
1689
+
1367
1690
  const initialCommand: AnyPadroneCommand = existingSubcommand
1368
1691
  ? { ...existingSubcommand, aliases: aliases ?? existingSubcommand.aliases, parent: existingCommand }
1369
1692
  : ({
@@ -1374,21 +1697,33 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1374
1697
  '~types': {} as any,
1375
1698
  } satisfies PadroneCommand);
1376
1699
 
1377
- 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
+ };
1378
1709
 
1379
- const commandObj =
1380
- ((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];
1381
1716
 
1382
- // Merge subcommands when overriding: existing subcommands that aren't replaced are kept
1383
- const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, commandObj) : commandObj;
1717
+ return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
1718
+ }
1384
1719
 
1385
- // Replace existing command or append new one
1720
+ // No builderFn: use the initial command as-is (no lazy resolution needed)
1386
1721
  const commands = existingCommand.commands || [];
1387
1722
  const existingIndex = commands.findIndex((c) => c.name === name);
1388
1723
  const updatedCommands =
1389
1724
  existingIndex >= 0
1390
- ? [...commands.slice(0, existingIndex), mergedCommandObj, ...commands.slice(existingIndex + 1)]
1391
- : [...commands, mergedCommandObj];
1725
+ ? [...commands.slice(0, existingIndex), initialCommand, ...commands.slice(existingIndex + 1)]
1726
+ : [...commands, initialCommand];
1392
1727
 
1393
1728
  return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
1394
1729
  },
@@ -1419,7 +1754,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1419
1754
  return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
1420
1755
  },
1421
1756
 
1422
- use(plugin: PadronePlugin) {
1757
+ use(plugin: PadronePlugin<any, any>) {
1423
1758
  return createPadroneBuilder({
1424
1759
  ...existingCommand,
1425
1760
  plugins: [...(existingCommand.plugins ?? []), plugin],
@@ -1438,11 +1773,10 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1438
1773
  cli,
1439
1774
  tool,
1440
1775
 
1441
- repl: (replFn = (options?: PadroneReplPreferences) => {
1442
- return createReplIterator({ existingCommand, evalCommand, replActiveRef }, options);
1443
- }),
1776
+ repl: replFn,
1444
1777
 
1445
1778
  api() {
1779
+ resolveAllCommands(existingCommand);
1446
1780
  function buildApi(command: AnyPadroneCommand) {
1447
1781
  const runCommand = ((args) => run(command, args).result) as PadroneAPI<AnyPadroneCommand>;
1448
1782
  if (!command.commands) return runCommand;
@@ -1454,6 +1788,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1454
1788
  },
1455
1789
 
1456
1790
  help(command, prefs) {
1791
+ resolveAllCommands(existingCommand);
1457
1792
  const commandObj = !command
1458
1793
  ? existingCommand
1459
1794
  : typeof command === 'string'
@@ -1461,14 +1796,31 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
1461
1796
  : (command as AnyPadroneCommand);
1462
1797
  if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
1463
1798
  const runtime = getCommandRuntime(existingCommand);
1464
- 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
+ });
1465
1804
  },
1466
1805
 
1467
1806
  async completion(shell) {
1807
+ resolveAllCommands(existingCommand);
1468
1808
  const { generateCompletionOutput } = await import('./completion.ts');
1469
1809
  return generateCompletionOutput(existingCommand, shell as ShellType | undefined);
1470
1810
  },
1471
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
+
1472
1824
  '~types': {} as any,
1473
1825
 
1474
1826
  [commandSymbol]: existingCommand,