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.
- package/CHANGELOG.md +115 -0
- package/README.md +108 -283
- package/dist/args-Cnq0nwSM.mjs +272 -0
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.d.mts +28 -3
- package/dist/codegen/index.d.mts.map +1 -1
- package/dist/codegen/index.mjs +169 -19
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +92 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-CL63UOzt.mjs +137 -0
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/help-B5Kk83of.mjs +849 -0
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/index-BaU3X6dY.d.mts +1178 -0
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +763 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3608 -1534
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-BM-d0nZi.mjs +377 -0
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/serve-Bk0JUlCj.mjs +402 -0
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/stream-DC4H8YTx.mjs +77 -0
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +5 -27
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +32 -0
- package/dist/zod.d.mts.map +1 -0
- package/dist/zod.mjs +50 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +20 -9
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -16
- package/src/cli/doctor.ts +213 -24
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +12 -10
- package/src/cli/link.ts +22 -18
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/discovery.ts +80 -28
- package/src/codegen/index.ts +2 -1
- package/src/codegen/parsers/bash.ts +179 -0
- package/src/codegen/schema-to-code.ts +2 -1
- package/src/core/args.ts +296 -0
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/{runtime.ts → core/default-runtime.ts} +70 -135
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +124 -11
- package/src/extension/auto-output.ts +95 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +43 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +214 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +130 -57
- package/src/{interactive.ts → feature/interactive.ts} +47 -6
- package/src/feature/mcp.ts +387 -0
- package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
- package/src/feature/serve.ts +438 -0
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +27 -27
- package/src/index.ts +120 -11
- package/src/output/colorizer.ts +154 -0
- package/src/{formatter.ts → output/formatter.ts} +281 -135
- package/src/{help.ts → output/help.ts} +62 -15
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -285
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +697 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +59 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/util/stream.ts +101 -0
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +99 -37
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -0
- package/dist/args-CVDbyyzG.mjs +0 -199
- package/dist/args-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
- package/dist/help-CcBe91bV.mjs +0 -1254
- package/dist/help-CcBe91bV.mjs.map +0 -1
- package/dist/types-DjIdJN5G.d.mts +0 -1059
- package/dist/types-DjIdJN5G.d.mts.map +0 -1
- package/dist/update-check-EbNDkzyV.mjs.map +0 -1
- package/src/args.ts +0 -461
- package/src/colorizer.ts +0 -41
- package/src/command-utils.ts +0 -532
- package/src/create.ts +0 -1477
- package/src/types.ts +0 -1109
- package/src/utils.ts +0 -140
|
@@ -1,104 +1,13 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/src/core/exec.ts
ADDED
|
@@ -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
|
+
}
|