padrone 1.5.0 → 1.7.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 +44 -0
- package/README.md +15 -11
- package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.mjs +4 -4
- 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} +12 -82
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +4 -4
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +10 -12
- package/dist/docs/index.mjs.map +1 -1
- package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
- package/dist/errors-DA4KzK1M.mjs.map +1 -0
- package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
- package/dist/help-BtxLgrF_.mjs.map +1 -0
- package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
- package/dist/index-D6-7dz0l.d.mts.map +1 -0
- package/dist/index.d.mts +869 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3884 -1699
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
- package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
- package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
- package/dist/serve-YVTPzBCl.mjs.map +1 -0
- package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
- 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 +2 -13
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +2 -2
- package/dist/zod.d.mts.map +1 -1
- package/dist/zod.mjs +2 -2
- package/dist/zod.mjs.map +1 -1
- package/package.json +15 -12
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -10
- package/src/cli/doctor.ts +22 -18
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +10 -7
- package/src/cli/link.ts +20 -16
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/schema-to-code.ts +2 -2
- package/src/{args.ts → core/args.ts} +32 -225
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +301 -0
- package/src/core/default-runtime.ts +239 -0
- 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 +12 -13
- package/src/extension/auto-output.ts +146 -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 +44 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +262 -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} +12 -12
- package/src/{interactive.ts → feature/interactive.ts} +4 -4
- package/src/{mcp.ts → feature/mcp.ts} +12 -15
- package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
- package/src/{serve.ts → feature/serve.ts} +11 -15
- 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} +10 -8
- package/src/index.ts +115 -30
- package/src/{formatter.ts → output/formatter.ts} +124 -176
- package/src/{help.ts → output/help.ts} +22 -8
- package/src/output/output-indicator.ts +87 -0
- package/src/output/primitives.ts +335 -0
- package/src/output/styling.ts +221 -0
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -276
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +718 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +60 -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/{stream.ts → util/stream.ts} +27 -1
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +71 -33
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -50
- package/dist/args-D5PNDyNu.mjs.map +0 -1
- package/dist/chunk-CjcI7cDX.mjs +0 -15
- package/dist/command-utils-B1D-HqCd.mjs +0 -1117
- package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/errors-BiVrBgi6.mjs.map +0 -1
- package/dist/formatter-DtHzbP22.d.mts.map +0 -1
- package/dist/help-bbmu9-qd.mjs.map +0 -1
- package/dist/mcp-mLWIdUIu.mjs.map +0 -1
- package/dist/serve-B0u43DK7.mjs.map +0 -1
- package/dist/stream-BcC146Ud.mjs.map +0 -1
- package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
- package/dist/update-check-CFX1FV3v.mjs.map +0 -1
- package/src/command-utils.ts +0 -882
- package/src/create.ts +0 -1829
- package/src/runtime.ts +0 -497
- package/src/types.ts +0 -1291
- package/src/utils.ts +0 -140
- /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
|
@@ -0,0 +1,239 @@
|
|
|
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';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Default terminal prompt implementation powered by Enquirer.
|
|
14
|
+
* Lazily imported to avoid loading Enquirer when not needed.
|
|
15
|
+
*/
|
|
16
|
+
async function defaultTerminalPrompt(config: InteractivePromptConfig): Promise<unknown> {
|
|
17
|
+
const Enquirer = (await import('enquirer')).default;
|
|
18
|
+
|
|
19
|
+
const question: Record<string, unknown> = {
|
|
20
|
+
type: config.type,
|
|
21
|
+
name: config.name,
|
|
22
|
+
message: config.message,
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
if (config.default !== undefined) {
|
|
26
|
+
question.initial = config.default;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (config.choices) {
|
|
30
|
+
question.choices = config.choices.map((c) => ({
|
|
31
|
+
name: String(c.value),
|
|
32
|
+
message: c.label,
|
|
33
|
+
}));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const response = (await Enquirer.prompt(question as any)) as Record<string, unknown>;
|
|
37
|
+
return response[config.name];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function createTerminalReplSession(config: ReplSessionConfig) {
|
|
41
|
+
// History accumulates across per-call interfaces, giving us
|
|
42
|
+
// up/down arrow navigation without a persistent stdin listener
|
|
43
|
+
// that would conflict with Enquirer or other stdin consumers.
|
|
44
|
+
let history: string[] = config.history ? [...config.history] : [];
|
|
45
|
+
let currentCompleter = config.completer;
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
/** Update the tab completer (e.g. when REPL scope changes). Takes effect on the next question. */
|
|
49
|
+
set completer(fn: ((line: string) => [string[], string]) | undefined) {
|
|
50
|
+
currentCompleter = fn;
|
|
51
|
+
},
|
|
52
|
+
async question(prompt: string): Promise<string | typeof REPL_SIGINT | null> {
|
|
53
|
+
const { createInterface } = await import('node:readline');
|
|
54
|
+
const opts: Record<string, unknown> = {
|
|
55
|
+
input: process.stdin,
|
|
56
|
+
output: process.stdout,
|
|
57
|
+
terminal: true,
|
|
58
|
+
history: [...history],
|
|
59
|
+
historySize: Math.max(history.length, 1000),
|
|
60
|
+
};
|
|
61
|
+
if (currentCompleter) {
|
|
62
|
+
opts.completer = currentCompleter;
|
|
63
|
+
}
|
|
64
|
+
const rl = createInterface(opts as any);
|
|
65
|
+
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
let resolved = false;
|
|
68
|
+
const settle = (value: string | typeof REPL_SIGINT | null) => {
|
|
69
|
+
if (resolved) return;
|
|
70
|
+
resolved = true;
|
|
71
|
+
rl.close();
|
|
72
|
+
resolve(value);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
rl.question(prompt, (answer) => {
|
|
76
|
+
// Grab updated history (includes the new entry) before closing.
|
|
77
|
+
if (Array.isArray((rl as any).history)) history = [...(rl as any).history];
|
|
78
|
+
settle(answer);
|
|
79
|
+
});
|
|
80
|
+
// Ctrl+C: cancel current line, print newline, resolve SIGINT sentinel.
|
|
81
|
+
rl.once('SIGINT', () => {
|
|
82
|
+
process.stdout.write('\n');
|
|
83
|
+
settle(REPL_SIGINT);
|
|
84
|
+
});
|
|
85
|
+
// EOF (Ctrl+D) fires close without the question callback.
|
|
86
|
+
rl.once('close', () => {
|
|
87
|
+
// Write newline so zsh doesn't show '%' (partial-line indicator).
|
|
88
|
+
process.stdout.write('\n');
|
|
89
|
+
settle(null);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
},
|
|
93
|
+
close() {
|
|
94
|
+
// No persistent interface to clean up.
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Auto-detect interactive mode when not explicitly set.
|
|
101
|
+
* Returns 'disabled' in CI environments or non-TTY contexts, 'supported' otherwise.
|
|
102
|
+
*/
|
|
103
|
+
function detectInteractiveMode(): InteractiveMode {
|
|
104
|
+
if (typeof process === 'undefined') return 'disabled';
|
|
105
|
+
if (process.env.CI || process.env.CONTINUOUS_INTEGRATION) return 'disabled';
|
|
106
|
+
if (!process.stdout?.isTTY) return 'disabled';
|
|
107
|
+
return 'supported';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Creates a default stdin reader from `process.stdin`.
|
|
112
|
+
* Only created when a command actually declares a `stdin` meta field.
|
|
113
|
+
*/
|
|
114
|
+
function createDefaultStdin(): NonNullable<PadroneRuntime['stdin']> {
|
|
115
|
+
return {
|
|
116
|
+
get isTTY() {
|
|
117
|
+
// process.stdin.isTTY is `true` when interactive terminal, `undefined` when piped/redirected.
|
|
118
|
+
// Node.js never sets it to `false` — it's either `true` or absent.
|
|
119
|
+
if (typeof process === 'undefined') return true;
|
|
120
|
+
return process.stdin?.isTTY === true;
|
|
121
|
+
},
|
|
122
|
+
async text() {
|
|
123
|
+
if (typeof process === 'undefined') return '';
|
|
124
|
+
return readStreamAsText(process.stdin);
|
|
125
|
+
},
|
|
126
|
+
async *lines() {
|
|
127
|
+
if (typeof process === 'undefined') return;
|
|
128
|
+
const { createInterface } = await import('node:readline');
|
|
129
|
+
const rl = createInterface({ input: process.stdin });
|
|
130
|
+
try {
|
|
131
|
+
for await (const line of rl) {
|
|
132
|
+
yield line;
|
|
133
|
+
}
|
|
134
|
+
} finally {
|
|
135
|
+
rl.close();
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
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
|
+
|
|
181
|
+
export function createDefaultRuntime(): ResolvedPadroneRuntime {
|
|
182
|
+
return {
|
|
183
|
+
output: (...args) => console.log(...args),
|
|
184
|
+
error: (text) => console.error(text),
|
|
185
|
+
argv: () => (typeof process !== 'undefined' ? process.argv.slice(2) : []),
|
|
186
|
+
env: () => (typeof process !== 'undefined' ? (process.env as Record<string, string | undefined>) : {}),
|
|
187
|
+
format: 'auto',
|
|
188
|
+
prompt: defaultTerminalPrompt,
|
|
189
|
+
interactive: detectInteractiveMode(),
|
|
190
|
+
onSignal: defaultOnSignal,
|
|
191
|
+
terminal: getTerminalInfo(),
|
|
192
|
+
exit: defaultExit,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Returns the stdin abstraction: custom runtime stdin > default process.stdin.
|
|
198
|
+
* Returns `undefined` when no custom stdin is provided and process.stdin is not piped.
|
|
199
|
+
*/
|
|
200
|
+
export function resolveStdin(partial?: PadroneRuntime): NonNullable<PadroneRuntime['stdin']> | undefined {
|
|
201
|
+
if (partial?.stdin) return partial.stdin;
|
|
202
|
+
const defaultStdin = createDefaultStdin();
|
|
203
|
+
// Only use default stdin if it's actually piped (isTTY === false).
|
|
204
|
+
// This avoids accidentally blocking on stdin in tests/CI.
|
|
205
|
+
if (defaultStdin.isTTY) return undefined;
|
|
206
|
+
return defaultStdin;
|
|
207
|
+
}
|
|
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
|
+
*/
|
|
221
|
+
export function resolveRuntime(partial?: PadroneRuntime): ResolvedPadroneRuntime {
|
|
222
|
+
const defaults = createDefaultRuntime();
|
|
223
|
+
if (!partial) return defaults;
|
|
224
|
+
return {
|
|
225
|
+
output: partial.output ?? defaults.output,
|
|
226
|
+
error: partial.error ?? defaults.error,
|
|
227
|
+
argv: partial.argv ?? defaults.argv,
|
|
228
|
+
env: partial.env ?? defaults.env,
|
|
229
|
+
format: partial.format ?? defaults.format,
|
|
230
|
+
interactive: partial.interactive ?? defaults.interactive,
|
|
231
|
+
prompt: partial.prompt ?? defaults.prompt,
|
|
232
|
+
readLine: partial.readLine ?? defaults.readLine,
|
|
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,
|
|
238
|
+
};
|
|
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
|
+
}
|