padrone 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. package/src/utils.ts +0 -140
@@ -1,104 +1,13 @@
1
- import type { HelpFormat } from './formatter.ts';
2
- import { findConfigFile, loadConfigFile } from './utils.ts';
3
-
4
- /**
5
- * Controls interactive prompting capability and default behavior at the runtime level.
6
- * - `'supported'` — capable; caller decides.
7
- * - `'unsupported'` — hard veto; nothing can override.
8
- * - `'forced'` — capable and forces prompts by default.
9
- * - `'disabled'` — capable but suppresses prompts by default.
10
- */
11
- export type InteractiveMode = 'supported' | 'unsupported' | 'forced' | 'disabled';
12
-
13
- /**
14
- * Configuration passed to the runtime's `prompt` function for interactive field prompting.
15
- * The prompt type and choices are auto-detected from the field's JSON schema.
16
- */
17
- export type InteractivePromptConfig = {
18
- /** The field name being prompted. */
19
- name: string;
20
- /** Human-readable message/label for the prompt, derived from the field's description or name. */
21
- message: string;
22
- /** The prompt type, auto-detected from the JSON schema. */
23
- type: 'input' | 'confirm' | 'select' | 'multiselect' | 'password';
24
- /** Available choices for select/multiselect prompts. */
25
- choices?: { label: string; value: unknown }[];
26
- /** Default value from the schema. */
27
- default?: unknown;
28
- };
29
-
30
- /**
31
- * Defines the execution context for a Padrone program.
32
- * Abstracts all environment-dependent I/O so the CLI framework
33
- * can run outside of a terminal (e.g., web UIs, chat interfaces, testing).
34
- *
35
- * All fields are optional — unspecified fields fall back to the Node.js/Bun defaults.
36
- */
37
- export type PadroneRuntime = {
38
- /** Write normal output (replaces console.log). Receives the raw value — runtime handles formatting. */
39
- output?: (...args: unknown[]) => void;
40
- /** Write error output (replaces console.error). */
41
- error?: (text: string) => void;
42
- /** Return the raw CLI arguments (replaces process.argv.slice(2)). */
43
- argv?: () => string[];
44
- /** Return environment variables (replaces process.env). */
45
- env?: () => Record<string, string | undefined>;
46
- /** Default help output format. */
47
- format?: HelpFormat | 'auto';
48
- /** Load and parse a config file by path. Return undefined if not found or unparsable. */
49
- loadConfigFile?: (path: string) => Record<string, unknown> | undefined;
50
- /** Find the first existing file from a list of candidate names. */
51
- findFile?: (names: string[]) => string | undefined;
52
- /**
53
- * Standard input abstraction. Provides methods to read piped data from stdin.
54
- * When not provided, defaults to reading from `process.stdin`.
55
- *
56
- * Used by commands that declare a `stdin` field in their arguments meta.
57
- * The framework reads stdin automatically during the validate phase and
58
- * injects the data into the specified argument field.
59
- */
60
- stdin?: {
61
- /** Whether stdin is a TTY (interactive terminal) vs a pipe/file. */
62
- isTTY?: boolean;
63
- /** Read all of stdin as a string. */
64
- text: () => Promise<string>;
65
- /** Async iterable of lines for streaming. */
66
- lines: () => AsyncIterable<string>;
67
- };
68
- /**
69
- * Controls interactive prompting capability and default behavior.
70
- * - `'supported'` — runtime can handle prompts; caller (flag/pref) decides whether to prompt. This is the default when `prompt` is provided.
71
- * - `'unsupported'` — runtime cannot handle prompts; hard veto that nothing can override.
72
- * - `'forced'` — runtime supports prompts and forces them by default (prompts even for provided values).
73
- * - `'disabled'` — runtime supports prompts but suppresses them by default.
74
- *
75
- * `'unsupported'` is the only immutable state. For the others, the `--interactive`/`-i` flag
76
- * and `cli()` preferences can override the default behavior.
77
- */
78
- interactive?: InteractiveMode;
79
- /**
80
- * Prompt the user for input. Called during `cli()` for fields marked as interactive.
81
- * When `interactive` is `true` and this is not provided, defaults to an Enquirer-based terminal prompt.
82
- */
83
- prompt?: (config: InteractivePromptConfig) => Promise<unknown>;
84
- /**
85
- * Read a line of input from the user. Used by `repl()` for custom runtimes
86
- * (web UIs, chat interfaces, testing).
87
- * Returns the input string, `null` on EOF (e.g. Ctrl+D, closed connection),
88
- * or `REPL_SIGINT` when the user presses Ctrl+C.
89
- *
90
- * When not provided, `repl()` uses a built-in Node.js readline session
91
- * with command history (up/down arrows) and tab completion.
92
- */
93
- readLine?: (prompt: string) => Promise<string | typeof REPL_SIGINT | null>;
94
- };
95
-
96
- /**
97
- * Internal resolved runtime where all fields are guaranteed to be present.
98
- * The `prompt`, `interactive`, and `readLine` fields remain optional since not all runtimes provide them.
99
- */
100
- export type ResolvedPadroneRuntime = Required<Omit<PadroneRuntime, 'prompt' | 'interactive' | 'readLine' | 'stdin'>> &
101
- Pick<PadroneRuntime, 'prompt' | 'interactive' | 'readLine' | 'stdin'>;
1
+ import { readStreamAsText } from '../util/stream.ts';
2
+ import type {
3
+ InteractiveMode,
4
+ InteractivePromptConfig,
5
+ PadroneRuntime,
6
+ PadroneSignal,
7
+ ReplSessionConfig,
8
+ ResolvedPadroneRuntime,
9
+ } from './runtime.ts';
10
+ import { REPL_SIGINT } from './runtime.ts';
102
11
 
103
12
  /**
104
13
  * Default terminal prompt implementation powered by Enquirer.
@@ -128,25 +37,6 @@ async function defaultTerminalPrompt(config: InteractivePromptConfig): Promise<u
128
37
  return response[config.name];
129
38
  }
130
39
 
131
- /**
132
- * Internal session config for the REPL's persistent readline interface.
133
- */
134
- export type ReplSessionConfig = {
135
- completer?: (line: string) => [string[], string];
136
- history?: string[];
137
- };
138
-
139
- /**
140
- * Creates a persistent Node.js readline session for the REPL.
141
- * Enables up/down arrow history navigation and tab completion.
142
- * Used internally by `repl()` when no custom `readLine` is provided.
143
- */
144
- /**
145
- * Sentinel value returned by the terminal REPL session when Ctrl+C is pressed.
146
- * Distinguished from empty string (user pressed enter) and null (EOF/Ctrl+D).
147
- */
148
- export const REPL_SIGINT = Symbol('REPL_SIGINT');
149
-
150
40
  export function createTerminalReplSession(config: ReplSessionConfig) {
151
41
  // History accumulates across per-call interfaces, giving us
152
42
  // up/down arrow navigation without a persistent stdin listener
@@ -217,9 +107,6 @@ function detectInteractiveMode(): InteractiveMode {
217
107
  return 'supported';
218
108
  }
219
109
 
220
- /**
221
- * Creates the default Node.js/Bun runtime.
222
- */
223
110
  /**
224
111
  * Creates a default stdin reader from `process.stdin`.
225
112
  * Only created when a command actually declares a `stdin` meta field.
@@ -234,11 +121,7 @@ function createDefaultStdin(): NonNullable<PadroneRuntime['stdin']> {
234
121
  },
235
122
  async text() {
236
123
  if (typeof process === 'undefined') return '';
237
- const chunks: Buffer[] = [];
238
- for await (const chunk of process.stdin) {
239
- chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
240
- }
241
- return Buffer.concat(chunks).toString('utf-8');
124
+ return readStreamAsText(process.stdin);
242
125
  },
243
126
  async *lines() {
244
127
  if (typeof process === 'undefined') return;
@@ -255,6 +138,46 @@ function createDefaultStdin(): NonNullable<PadroneRuntime['stdin']> {
255
138
  };
256
139
  }
257
140
 
141
+ /**
142
+ * Default signal listener that wires to `process.on(signal)`.
143
+ * Returns an unsubscribe function that removes all listeners.
144
+ */
145
+ function defaultOnSignal(callback: (signal: PadroneSignal) => void): () => void {
146
+ if (typeof process === 'undefined') return () => {};
147
+ const signals: PadroneSignal[] = ['SIGINT', 'SIGTERM', 'SIGHUP'];
148
+ const handlers = new Map<PadroneSignal, () => void>();
149
+ for (const sig of signals) {
150
+ const handler = () => callback(sig);
151
+ handlers.set(sig, handler);
152
+ process.on(sig, handler);
153
+ }
154
+ return () => {
155
+ for (const [sig, handler] of handlers) {
156
+ process.removeListener(sig, handler);
157
+ }
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Creates the default Node.js/Bun runtime.
163
+ */
164
+ function defaultExit(code: number): never {
165
+ if (typeof process !== 'undefined') process.exit(code);
166
+ throw new Error(`Exit with code ${code}`);
167
+ }
168
+
169
+ function getTerminalInfo(): PadroneRuntime['terminal'] {
170
+ if (typeof process === 'undefined') return undefined;
171
+ return {
172
+ get columns() {
173
+ return process.stdout?.columns;
174
+ },
175
+ get isTTY() {
176
+ return process.stdout?.isTTY === true;
177
+ },
178
+ };
179
+ }
180
+
258
181
  export function createDefaultRuntime(): ResolvedPadroneRuntime {
259
182
  return {
260
183
  output: (...args) => console.log(...args),
@@ -262,16 +185,14 @@ export function createDefaultRuntime(): ResolvedPadroneRuntime {
262
185
  argv: () => (typeof process !== 'undefined' ? process.argv.slice(2) : []),
263
186
  env: () => (typeof process !== 'undefined' ? (process.env as Record<string, string | undefined>) : {}),
264
187
  format: 'auto',
265
- loadConfigFile,
266
- findFile: findConfigFile,
267
188
  prompt: defaultTerminalPrompt,
268
189
  interactive: detectInteractiveMode(),
190
+ onSignal: defaultOnSignal,
191
+ terminal: getTerminalInfo(),
192
+ exit: defaultExit,
269
193
  };
270
194
  }
271
195
 
272
- /**
273
- * Merges a partial runtime with the default runtime.
274
- */
275
196
  /**
276
197
  * Returns the stdin abstraction: custom runtime stdin > default process.stdin.
277
198
  * Returns `undefined` when no custom stdin is provided and process.stdin is not piped.
@@ -285,6 +206,18 @@ export function resolveStdin(partial?: PadroneRuntime): NonNullable<PadroneRunti
285
206
  return defaultStdin;
286
207
  }
287
208
 
209
+ /**
210
+ * Like `resolveStdin`, but always returns a stdin source even when it's a TTY.
211
+ * Used for async streams which support interactive (non-piped) input.
212
+ */
213
+ export function resolveStdinAlways(partial?: PadroneRuntime): NonNullable<PadroneRuntime['stdin']> {
214
+ if (partial?.stdin) return partial.stdin;
215
+ return createDefaultStdin();
216
+ }
217
+
218
+ /**
219
+ * Merges a partial runtime with the default runtime.
220
+ */
288
221
  export function resolveRuntime(partial?: PadroneRuntime): ResolvedPadroneRuntime {
289
222
  const defaults = createDefaultRuntime();
290
223
  if (!partial) return defaults;
@@ -294,11 +227,13 @@ export function resolveRuntime(partial?: PadroneRuntime): ResolvedPadroneRuntime
294
227
  argv: partial.argv ?? defaults.argv,
295
228
  env: partial.env ?? defaults.env,
296
229
  format: partial.format ?? defaults.format,
297
- loadConfigFile: partial.loadConfigFile ?? defaults.loadConfigFile,
298
- findFile: partial.findFile ?? defaults.findFile,
299
230
  interactive: partial.interactive ?? defaults.interactive,
300
231
  prompt: partial.prompt ?? defaults.prompt,
301
232
  readLine: partial.readLine ?? defaults.readLine,
302
233
  stdin: partial.stdin,
234
+ theme: partial.theme,
235
+ onSignal: partial.onSignal ?? defaults.onSignal,
236
+ terminal: partial.terminal ?? defaults.terminal,
237
+ exit: partial.exit ?? defaults.exit,
303
238
  };
304
239
  }
@@ -7,6 +7,8 @@
7
7
  * and to present formatted, actionable error messages.
8
8
  */
9
9
 
10
+ import type { PadroneSignal } from './runtime.ts';
11
+
10
12
  export type PadroneErrorOptions = {
11
13
  /** Process exit code. Defaults to 1. */
12
14
  exitCode?: number;
@@ -129,3 +131,23 @@ export class ActionError extends PadroneError {
129
131
  this.name = 'ActionError';
130
132
  }
131
133
  }
134
+
135
+ /**
136
+ * Thrown when command execution is interrupted by a process signal (SIGINT, SIGTERM, SIGHUP).
137
+ * Carries the signal name and the conventional exit code (128 + signal number).
138
+ */
139
+ export class SignalError extends PadroneError {
140
+ readonly signal: PadroneSignal;
141
+
142
+ constructor(signal: PadroneSignal, options?: { cause?: unknown }) {
143
+ super(`Process interrupted by ${signal}`, { exitCode: signalExitCode(signal), cause: options?.cause });
144
+ this.name = 'SignalError';
145
+ this.signal = signal;
146
+ }
147
+ }
148
+
149
+ /** Maps a signal name to its conventional exit code (128 + signal number). */
150
+ export function signalExitCode(signal: PadroneSignal): number {
151
+ const codes: Record<string, number> = { SIGINT: 130, SIGTERM: 143, SIGHUP: 129 };
152
+ return codes[signal] ?? 1;
153
+ }
@@ -0,0 +1,259 @@
1
+ import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type {
3
+ AnyPadroneCommand,
4
+ AnyPadroneProgram,
5
+ InterceptorExecuteContext,
6
+ InterceptorExecuteResult,
7
+ InterceptorParseContext,
8
+ InterceptorParseResult,
9
+ InterceptorValidateContext,
10
+ InterceptorValidateResult,
11
+ PadroneActionContext,
12
+ PadroneEvalPreferences,
13
+ RegisteredInterceptor,
14
+ ResolvedInterceptor,
15
+ } from '../types/index.ts';
16
+ import { getCommandRuntime } from './commands.ts';
17
+ import { RoutingError, SignalError, ValidationError } from './errors.ts';
18
+ import { resolveRegisteredInterceptors, runInterceptorChain, wrapWithLifecycle } from './interceptors.ts';
19
+ import { errorResult, noop, thenMaybe, warnIfUnexpectedAsync, withDrain } from './results.ts';
20
+ import { buildCommandArgs, formatIssueMessages, validateCommandArgs } from './validate.ts';
21
+
22
+ export type ExecContext = {
23
+ rootCommand: AnyPadroneCommand;
24
+ builder: AnyPadroneProgram;
25
+ parseCommandFn: (input: string | undefined) => {
26
+ command: AnyPadroneCommand;
27
+ rawArgs: Record<string, unknown>;
28
+ args: string[];
29
+ unmatchedTerms: string[];
30
+ };
31
+ collectInterceptorsFn: (cmd: AnyPadroneCommand) => RegisteredInterceptor[];
32
+ };
33
+
34
+ /**
35
+ * Collects registered interceptors from the command's parent chain (root → ... → target).
36
+ * Root/program interceptors come first (outermost), target command's interceptors last (innermost).
37
+ */
38
+ export function collectInterceptors(cmd: AnyPadroneCommand, rootCommand: AnyPadroneCommand): RegisteredInterceptor[] {
39
+ const chain: RegisteredInterceptor[][] = [];
40
+ let current: AnyPadroneCommand | undefined = cmd;
41
+ while (current) {
42
+ const isTarget = current === cmd;
43
+ if (!current.parent) {
44
+ if (rootCommand.interceptors?.length) {
45
+ const isRootTarget = cmd === rootCommand || !cmd.parent;
46
+ chain.unshift(isRootTarget ? rootCommand.interceptors : rootCommand.interceptors.filter((i) => i.meta.inherit !== false));
47
+ }
48
+ } else {
49
+ if (current.interceptors?.length) {
50
+ chain.unshift(isTarget ? current.interceptors : current.interceptors.filter((i) => i.meta.inherit !== false));
51
+ }
52
+ }
53
+ current = current.parent;
54
+ }
55
+ return chain.flat();
56
+ }
57
+
58
+ /** Wraps an error into a result, preserving any signal info from the pipeline. */
59
+ export function errorResultWithSignal(err: unknown) {
60
+ const result = errorResult(err);
61
+ if (err instanceof SignalError) {
62
+ (result as any).signal = err.signal;
63
+ (result as any).exitCode = err.exitCode;
64
+ }
65
+ return result;
66
+ }
67
+
68
+ /** Resolve context by walking the command parent chain and applying transforms from root to target. */
69
+ function resolveContext(command: AnyPadroneCommand, initialContext: unknown): unknown {
70
+ const chain: AnyPadroneCommand[] = [];
71
+ let current: AnyPadroneCommand | undefined = command;
72
+ while (current) {
73
+ chain.unshift(current);
74
+ current = current.parent;
75
+ }
76
+ let resolved = initialContext;
77
+ for (const cmd of chain) {
78
+ if (cmd.contextTransform) resolved = cmd.contextTransform(resolved);
79
+ }
80
+ return resolved;
81
+ }
82
+
83
+ /** Validate parse result — reject unmatched terms when the command doesn't accept positional args. */
84
+ function validateParseResult(
85
+ parseResult: { command: AnyPadroneCommand; rawArgs: Record<string, unknown>; args: string[]; unmatchedTerms: string[] },
86
+ rootCommand: AnyPadroneCommand,
87
+ ): InterceptorParseResult {
88
+ const { command, rawArgs, args, unmatchedTerms } = parseResult;
89
+
90
+ if (unmatchedTerms.length > 0) {
91
+ const hasPositionalConfig = command.meta?.positional && command.meta.positional.length > 0;
92
+ if (!hasPositionalConfig) {
93
+ const isRootCommand = command === rootCommand;
94
+ const commandDisplayName = command.name || command.aliases?.[0] || command.path || '(default)';
95
+ const errorMsg = isRootCommand
96
+ ? `Unknown command: ${unmatchedTerms[0]}`
97
+ : `Unexpected arguments for '${commandDisplayName}': ${unmatchedTerms.join(' ')}`;
98
+
99
+ throw new RoutingError(errorMsg, { command: command.path || command.name });
100
+ }
101
+ }
102
+
103
+ return { command, rawArgs, positionalArgs: args };
104
+ }
105
+
106
+ /** Handle validation issues based on error mode: throw (hard) or return result with issues (soft). */
107
+ function handleValidationIssues(argsResult: StandardSchemaV1.FailureResult, command: AnyPadroneCommand, errorMode: 'soft' | 'hard') {
108
+ if (errorMode === 'hard') {
109
+ const issueMessages = formatIssueMessages(argsResult.issues);
110
+ throw new ValidationError(`Validation error:\n${issueMessages}`, argsResult.issues as any, {
111
+ command: command.path || command.name,
112
+ });
113
+ }
114
+
115
+ return withDrain({
116
+ command: command as any,
117
+ args: undefined,
118
+ argsResult,
119
+ result: undefined,
120
+ });
121
+ }
122
+
123
+ /**
124
+ * Core execution logic shared by eval() and cli().
125
+ * errorMode controls validation error behavior:
126
+ * - 'soft': return result with issues (eval behavior)
127
+ * - 'hard': print error + help and throw (cli-without-input behavior)
128
+ */
129
+ export function execCommand(
130
+ resolvedInput: string | undefined,
131
+ ctx: ExecContext,
132
+ evalOptions?: PadroneEvalPreferences,
133
+ errorMode: 'soft' | 'hard' = 'soft',
134
+ caller: PadroneActionContext['caller'] = 'eval',
135
+ ) {
136
+ const { rootCommand, parseCommandFn, collectInterceptorsFn } = ctx;
137
+ const baseRuntime = getCommandRuntime(rootCommand);
138
+ const runtime = evalOptions?.runtime
139
+ ? Object.assign({}, baseRuntime, Object.fromEntries(Object.entries(evalOptions.runtime).filter(([, v]) => v !== undefined)))
140
+ : baseRuntime;
141
+
142
+ // Inert signal — the signal extension overrides this via next({ signal }) in the start phase.
143
+ const inertSignal = new AbortController().signal;
144
+
145
+ // Pipeline state accumulated as phases complete — propagated to error/shutdown contexts.
146
+ const pipelineState: { rawArgs?: Record<string, unknown>; positionalArgs?: string[]; args?: unknown } = {};
147
+
148
+ const initialContext = evalOptions?.context;
149
+
150
+ // Factory resolution cache — ensures each factory is called at most once per execution,
151
+ // so root interceptor closures are shared when they appear in both root and command chains.
152
+ const factoryCache = new Map<RegisteredInterceptor, ResolvedInterceptor>();
153
+ const rootRegistered = rootCommand.interceptors ?? [];
154
+ const rootInterceptors = resolveRegisteredInterceptors(rootRegistered, factoryCache);
155
+
156
+ const runPipeline = (signal: AbortSignal, pipelineContext: unknown) => {
157
+ // ── Phase 1: Parse ──────────────────────────────────────────────────
158
+ const parseCtx: InterceptorParseContext = {
159
+ input: resolvedInput,
160
+ command: rootCommand,
161
+ signal,
162
+ context: pipelineContext,
163
+ runtime,
164
+ program: ctx.builder,
165
+ caller,
166
+ };
167
+
168
+ const coreParse = (parseCtx: InterceptorParseContext): InterceptorParseResult =>
169
+ validateParseResult(parseCommandFn(parseCtx.input), rootCommand);
170
+
171
+ const parsedOrPromise = runInterceptorChain('parse', rootInterceptors, parseCtx, coreParse);
172
+
173
+ // ── Phases 2 & 3 chained after parse ────────────────────────────────
174
+ const continueAfterParse = (parsed: InterceptorParseResult) => {
175
+ const { command } = parsed;
176
+ pipelineState.rawArgs = parsed.rawArgs;
177
+ pipelineState.positionalArgs = parsed.positionalArgs;
178
+ const commandInterceptors = resolveRegisteredInterceptors(collectInterceptorsFn(command), factoryCache);
179
+ const context = resolveContext(command, pipelineContext);
180
+
181
+ // ── Phase 2: Validate ───────────────────────────────────────────
182
+ const validateCtx: InterceptorValidateContext = {
183
+ ...parseCtx,
184
+ command,
185
+ rawArgs: parsed.rawArgs,
186
+ positionalArgs: parsed.positionalArgs,
187
+ context,
188
+ evalInteractive: evalOptions?.interactive,
189
+ };
190
+
191
+ const coreValidate = (validateCtx: InterceptorValidateContext): InterceptorValidateResult | Promise<InterceptorValidateResult> => {
192
+ const preprocessedArgs = buildCommandArgs(validateCtx.command, validateCtx.rawArgs, validateCtx.positionalArgs);
193
+ const validated = validateCommandArgs(validateCtx.command, preprocessedArgs);
194
+ return thenMaybe(validated, (v) => v as InterceptorValidateResult);
195
+ };
196
+
197
+ const validatedOrPromise = runInterceptorChain('validate', commandInterceptors, validateCtx, coreValidate);
198
+
199
+ // ── Phase 3: Execute (or handle validation errors) ──────────────
200
+ const continueAfterValidate = (v: InterceptorValidateResult) => {
201
+ pipelineState.args = v.args;
202
+ if (v.argsResult?.issues) return handleValidationIssues(v.argsResult as StandardSchemaV1.FailureResult, command, errorMode);
203
+
204
+ const executeCtx: InterceptorExecuteContext = {
205
+ ...validateCtx,
206
+ args: v.args,
207
+ };
208
+
209
+ const coreExecute = (executeCtx: InterceptorExecuteContext): InterceptorExecuteResult => {
210
+ const handler = command.action ?? noop;
211
+ const effectiveRuntime = executeCtx.runtime;
212
+ const actionCtx: PadroneActionContext = {
213
+ runtime: effectiveRuntime,
214
+ command: executeCtx.command,
215
+ program: ctx.builder as any,
216
+ signal: executeCtx.signal,
217
+ context: executeCtx.context,
218
+ caller,
219
+ };
220
+ const result = handler(executeCtx.args as any, actionCtx);
221
+ return { result };
222
+ };
223
+
224
+ const executedOrPromise = runInterceptorChain('execute', commandInterceptors, executeCtx, coreExecute);
225
+
226
+ return thenMaybe(executedOrPromise, (e) => {
227
+ const finalize = (result: unknown) =>
228
+ withDrain({
229
+ command: command as any,
230
+ args: v.args,
231
+ argsResult: v.argsResult,
232
+ result,
233
+ });
234
+
235
+ if (e.result instanceof Promise) return e.result.then(finalize);
236
+ return finalize(e.result);
237
+ });
238
+ };
239
+
240
+ return thenMaybe(warnIfUnexpectedAsync(validatedOrPromise, command), continueAfterValidate) as any;
241
+ };
242
+
243
+ return thenMaybe(parsedOrPromise, continueAfterParse) as any;
244
+ };
245
+
246
+ return wrapWithLifecycle(
247
+ rootInterceptors,
248
+ rootCommand,
249
+ resolvedInput,
250
+ runPipeline,
251
+ (result) => withDrain({ command: rootCommand, args: undefined, argsResult: undefined, result }),
252
+ inertSignal,
253
+ initialContext,
254
+ runtime,
255
+ ctx.builder,
256
+ caller,
257
+ pipelineState,
258
+ ) as any;
259
+ }