padrone 1.0.0 → 1.2.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 +51 -0
- package/LICENSE +1 -1
- package/README.md +92 -49
- package/dist/args-CKNh7Dm9.mjs +175 -0
- package/dist/args-CKNh7Dm9.mjs.map +1 -0
- package/dist/chunk-y_GBKt04.mjs +5 -0
- package/dist/codegen/index.d.mts +305 -0
- package/dist/codegen/index.d.mts.map +1 -0
- package/dist/codegen/index.mjs +1348 -0
- package/dist/codegen/index.mjs.map +1 -0
- package/dist/completion.d.mts +64 -0
- package/dist/completion.d.mts.map +1 -0
- package/dist/completion.mjs +417 -0
- package/dist/completion.mjs.map +1 -0
- package/dist/docs/index.d.mts +34 -0
- package/dist/docs/index.d.mts.map +1 -0
- package/dist/docs/index.mjs +404 -0
- package/dist/docs/index.mjs.map +1 -0
- package/dist/formatter-Dvx7jFXr.d.mts +82 -0
- package/dist/formatter-Dvx7jFXr.d.mts.map +1 -0
- package/dist/help-mUIX0T0V.mjs +1195 -0
- package/dist/help-mUIX0T0V.mjs.map +1 -0
- package/dist/index.d.mts +122 -438
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +1240 -1161
- package/dist/index.mjs.map +1 -1
- package/dist/test.d.mts +112 -0
- package/dist/test.d.mts.map +1 -0
- package/dist/test.mjs +138 -0
- package/dist/test.mjs.map +1 -0
- package/dist/types-qrtt0135.d.mts +1037 -0
- package/dist/types-qrtt0135.d.mts.map +1 -0
- package/dist/update-check-EbNDkzyV.mjs +146 -0
- package/dist/update-check-EbNDkzyV.mjs.map +1 -0
- package/package.json +61 -20
- package/src/args.ts +365 -0
- package/src/cli/completions.ts +29 -0
- package/src/cli/docs.ts +86 -0
- package/src/cli/doctor.ts +312 -0
- package/src/cli/index.ts +159 -0
- package/src/cli/init.ts +135 -0
- package/src/cli/link.ts +320 -0
- package/src/cli/wrap.ts +152 -0
- package/src/codegen/README.md +118 -0
- package/src/codegen/code-builder.ts +226 -0
- package/src/codegen/discovery.ts +232 -0
- package/src/codegen/file-emitter.ts +73 -0
- package/src/codegen/generators/barrel-file.ts +16 -0
- package/src/codegen/generators/command-file.ts +184 -0
- package/src/codegen/generators/command-tree.ts +124 -0
- package/src/codegen/index.ts +33 -0
- package/src/codegen/parsers/fish.ts +163 -0
- package/src/codegen/parsers/help.ts +378 -0
- package/src/codegen/parsers/merge.ts +158 -0
- package/src/codegen/parsers/zsh.ts +221 -0
- package/src/codegen/schema-to-code.ts +199 -0
- package/src/codegen/template.ts +69 -0
- package/src/codegen/types.ts +143 -0
- package/src/colorizer.ts +2 -2
- package/src/command-utils.ts +501 -0
- package/src/completion.ts +110 -97
- package/src/create.ts +1044 -284
- package/src/docs/index.ts +607 -0
- package/src/errors.ts +131 -0
- package/src/formatter.ts +149 -63
- package/src/help.ts +151 -55
- package/src/index.ts +13 -15
- package/src/interactive.ts +169 -0
- package/src/parse.ts +31 -16
- package/src/repl-loop.ts +317 -0
- package/src/runtime.ts +304 -0
- package/src/shell-utils.ts +83 -0
- package/src/test.ts +285 -0
- package/src/type-helpers.ts +12 -12
- package/src/type-utils.ts +124 -14
- package/src/types.ts +803 -144
- package/src/update-check.ts +244 -0
- package/src/wrap.ts +185 -0
- package/src/zod.d.ts +2 -2
- package/src/options.ts +0 -180
package/src/create.ts
CHANGED
|
@@ -1,51 +1,74 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
1
2
|
import type { Schema } from 'ai';
|
|
2
|
-
import {
|
|
3
|
+
import { coerceArgs, detectUnknownArgs, extractSchemaMetadata, parsePositionalConfig, parseStdinConfig, preprocessArgs } from './args.ts';
|
|
4
|
+
import {
|
|
5
|
+
commandSymbol,
|
|
6
|
+
findCommandByName,
|
|
7
|
+
getCommandRuntime,
|
|
8
|
+
hasInteractiveConfig,
|
|
9
|
+
isAsyncBranded,
|
|
10
|
+
mergeCommands,
|
|
11
|
+
noop,
|
|
12
|
+
outputValue,
|
|
13
|
+
repathCommandTree,
|
|
14
|
+
runPluginChain,
|
|
15
|
+
suggestSimilar,
|
|
16
|
+
thenMaybe,
|
|
17
|
+
warnIfUnexpectedAsync,
|
|
18
|
+
wrapWithLifecycle,
|
|
19
|
+
} from './command-utils.ts';
|
|
20
|
+
import type { ShellType } from './completion.ts';
|
|
21
|
+
import { ConfigError, RoutingError, ValidationError } from './errors.ts';
|
|
3
22
|
import { generateHelp } from './help.ts';
|
|
4
|
-
import {
|
|
23
|
+
import { promptInteractiveFields } from './interactive.ts';
|
|
5
24
|
import { getNestedValue, parseCliInputToParts, setNestedValue } from './parse.ts';
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
25
|
+
import { createReplIterator } from './repl-loop.ts';
|
|
26
|
+
import { resolveStdin } from './runtime.ts';
|
|
27
|
+
import type {
|
|
28
|
+
AnyPadroneCommand,
|
|
29
|
+
AnyPadroneProgram,
|
|
30
|
+
PadroneActionContext,
|
|
31
|
+
PadroneAPI,
|
|
32
|
+
PadroneCommand,
|
|
33
|
+
PadroneEvalPreferences,
|
|
34
|
+
PadronePlugin,
|
|
35
|
+
PadroneProgram,
|
|
36
|
+
PadroneReplPreferences,
|
|
37
|
+
PluginExecuteContext,
|
|
38
|
+
PluginExecuteResult,
|
|
39
|
+
PluginParseContext,
|
|
40
|
+
PluginParseResult,
|
|
41
|
+
PluginValidateContext,
|
|
42
|
+
PluginValidateResult,
|
|
43
|
+
} from './types.ts';
|
|
44
|
+
import { getVersion } from './utils.ts';
|
|
45
|
+
import { createWrapHandler } from './wrap.ts';
|
|
46
|
+
|
|
47
|
+
export { asyncSchema, buildReplCompleter } from './command-utils.ts';
|
|
12
48
|
|
|
13
49
|
export function createPadrone<TProgramName extends string>(name: TProgramName): PadroneProgram<TProgramName, '', ''> {
|
|
14
50
|
return createPadroneBuilder({ name, path: '', commands: [] } as any) as unknown as PadroneProgram<TProgramName, '', ''>;
|
|
15
51
|
}
|
|
16
52
|
|
|
17
53
|
export function createPadroneBuilder<TBuilder extends PadroneProgram = PadroneProgram>(
|
|
18
|
-
|
|
54
|
+
inputCommand: AnyPadroneCommand,
|
|
19
55
|
): TBuilder & { [commandSymbol]: AnyPadroneCommand } {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
const foundByAlias = commands.find((cmd) => cmd.aliases?.includes(name));
|
|
28
|
-
if (foundByAlias) return foundByAlias;
|
|
29
|
-
|
|
30
|
-
for (const cmd of commands) {
|
|
31
|
-
if (cmd.commands && name.startsWith(`${cmd.name} `)) {
|
|
32
|
-
const subCommandName = name.slice(cmd.name.length + 1);
|
|
33
|
-
const subCommand = findCommandByName(subCommandName, cmd.commands);
|
|
34
|
-
if (subCommand) return subCommand;
|
|
35
|
-
}
|
|
36
|
-
// Check aliases for nested commands
|
|
37
|
-
if (cmd.commands && cmd.aliases) {
|
|
38
|
-
for (const alias of cmd.aliases) {
|
|
39
|
-
if (name.startsWith(`${alias} `)) {
|
|
40
|
-
const subCommandName = name.slice(alias.length + 1);
|
|
41
|
-
const subCommand = findCommandByName(subCommandName, cmd.commands);
|
|
42
|
-
if (subCommand) return subCommand;
|
|
43
|
-
}
|
|
56
|
+
// Re-parent direct subcommands so getCommandRuntime walks to the current root,
|
|
57
|
+
// not a stale parent from before .runtime()/.configure()/etc.
|
|
58
|
+
const existingCommand =
|
|
59
|
+
inputCommand.commands?.length && inputCommand.commands.some((c) => c.parent && c.parent !== inputCommand)
|
|
60
|
+
? {
|
|
61
|
+
...inputCommand,
|
|
62
|
+
commands: inputCommand.commands.map((c) => (c.parent && c.parent !== inputCommand ? { ...c, parent: inputCommand } : c)),
|
|
44
63
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
64
|
+
: inputCommand;
|
|
65
|
+
|
|
66
|
+
/** Creates the action context passed to command handlers. References `builder` which is defined later but only called at runtime. */
|
|
67
|
+
const createActionContext = (cmd: AnyPadroneCommand): PadroneActionContext => ({
|
|
68
|
+
runtime: getCommandRuntime(cmd),
|
|
69
|
+
command: cmd,
|
|
70
|
+
program: builder as any,
|
|
71
|
+
});
|
|
49
72
|
|
|
50
73
|
const find: AnyPadroneProgram['find'] = (command) => {
|
|
51
74
|
if (typeof command !== 'string') return findCommandByName(command.path, existingCommand.commands) as any;
|
|
@@ -53,11 +76,18 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
53
76
|
};
|
|
54
77
|
|
|
55
78
|
/**
|
|
56
|
-
* Parses CLI input to find the command and extract raw
|
|
79
|
+
* Parses CLI input to find the command and extract raw arguments without validation.
|
|
57
80
|
*/
|
|
58
81
|
const parseCommand = (input: string | undefined) => {
|
|
59
|
-
input ??=
|
|
60
|
-
if (!input)
|
|
82
|
+
input ??= getCommandRuntime(existingCommand).argv().join(' ') || undefined;
|
|
83
|
+
if (!input) {
|
|
84
|
+
// No input: check for default '' command
|
|
85
|
+
const defaultCommand = findCommandByName('', existingCommand.commands);
|
|
86
|
+
if (defaultCommand) {
|
|
87
|
+
return { command: defaultCommand, rawArgs: {} as Record<string, unknown>, args: [] as string[], unmatchedTerms: [] as string[] };
|
|
88
|
+
}
|
|
89
|
+
return { command: existingCommand, rawArgs: {} as Record<string, unknown>, args: [] as string[], unmatchedTerms: [] as string[] };
|
|
90
|
+
}
|
|
61
91
|
|
|
62
92
|
const parts = parseCliInputToParts(input);
|
|
63
93
|
|
|
@@ -65,6 +95,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
65
95
|
const args = parts.filter((p) => p.type === 'arg').map((p) => p.value);
|
|
66
96
|
|
|
67
97
|
let curCommand: AnyPadroneCommand | undefined = existingCommand;
|
|
98
|
+
let unmatchedTerms: string[] = [];
|
|
68
99
|
|
|
69
100
|
// If the first term is the program name, skip it
|
|
70
101
|
if (terms[0] === existingCommand.name) terms.shift();
|
|
@@ -76,26 +107,36 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
76
107
|
if (found) {
|
|
77
108
|
curCommand = found;
|
|
78
109
|
} else {
|
|
79
|
-
|
|
110
|
+
unmatchedTerms = terms.slice(i);
|
|
111
|
+
args.unshift(...unmatchedTerms);
|
|
80
112
|
break;
|
|
81
113
|
}
|
|
82
114
|
}
|
|
83
115
|
|
|
84
|
-
|
|
116
|
+
// If no unmatched terms remain, check for a default '' subcommand.
|
|
117
|
+
// This handles both the root level (no input) and nested commands (e.g., "advanced" with a '' subcommand).
|
|
118
|
+
if (unmatchedTerms.length === 0 && curCommand.commands?.length) {
|
|
119
|
+
const defaultCommand = findCommandByName('', curCommand.commands);
|
|
120
|
+
if (defaultCommand) {
|
|
121
|
+
curCommand = defaultCommand;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!curCommand) return { command: existingCommand, rawArgs: {} as Record<string, unknown>, args, unmatchedTerms };
|
|
85
126
|
|
|
86
|
-
// Extract
|
|
87
|
-
const
|
|
88
|
-
const schemaMetadata = curCommand.
|
|
127
|
+
// Extract argument metadata from the nested arguments object in meta
|
|
128
|
+
const argsMeta = curCommand.meta?.fields;
|
|
129
|
+
const schemaMetadata = curCommand.argsSchema ? extractSchemaMetadata(curCommand.argsSchema, argsMeta) : { aliases: {} };
|
|
89
130
|
const { aliases } = schemaMetadata;
|
|
90
131
|
|
|
91
|
-
// Get array
|
|
92
|
-
const
|
|
93
|
-
if (curCommand.
|
|
132
|
+
// Get array arguments from schema (arrays are always variadic)
|
|
133
|
+
const arrayArguments = new Set<string>();
|
|
134
|
+
if (curCommand.argsSchema) {
|
|
94
135
|
try {
|
|
95
|
-
const jsonSchema = curCommand.
|
|
136
|
+
const jsonSchema = curCommand.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
96
137
|
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
97
138
|
for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
|
|
98
|
-
if (prop?.type === 'array')
|
|
139
|
+
if (prop?.type === 'array') arrayArguments.add(key);
|
|
99
140
|
}
|
|
100
141
|
}
|
|
101
142
|
} catch {
|
|
@@ -103,27 +144,27 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
103
144
|
}
|
|
104
145
|
}
|
|
105
146
|
|
|
106
|
-
const
|
|
107
|
-
const
|
|
147
|
+
const argParts = parts.filter((p) => p.type === 'named' || p.type === 'alias');
|
|
148
|
+
const rawArgs: Record<string, unknown> = {};
|
|
108
149
|
|
|
109
|
-
for (const
|
|
150
|
+
for (const arg of argParts) {
|
|
110
151
|
// For aliases, resolve to the full key name (aliases map single char to full key name)
|
|
111
|
-
//
|
|
112
|
-
const key: string[] =
|
|
152
|
+
// arg.key is now a string[] - for aliases it's always single element like ['v']
|
|
153
|
+
const key: string[] = arg.type === 'alias' && arg.key.length === 1 && aliases[arg.key[0]!] ? [aliases[arg.key[0]!]!] : arg.key;
|
|
113
154
|
|
|
114
155
|
const rootKey = key[0]!;
|
|
115
156
|
|
|
116
|
-
// Handle negated boolean
|
|
117
|
-
if (
|
|
118
|
-
setNestedValue(
|
|
157
|
+
// Handle negated boolean arguments (--no-verbose)
|
|
158
|
+
if (arg.type === 'named' && arg.negated) {
|
|
159
|
+
setNestedValue(rawArgs, key, false);
|
|
119
160
|
continue;
|
|
120
161
|
}
|
|
121
162
|
|
|
122
|
-
const value =
|
|
163
|
+
const value = arg.value ?? true;
|
|
123
164
|
|
|
124
|
-
// Handle array
|
|
125
|
-
if (
|
|
126
|
-
const existing = getNestedValue(
|
|
165
|
+
// Handle array arguments - accumulate values into arrays (arrays are always variadic)
|
|
166
|
+
if (arrayArguments.has(rootKey)) {
|
|
167
|
+
const existing = getNestedValue(rawArgs, key);
|
|
127
168
|
if (existing !== undefined) {
|
|
128
169
|
if (Array.isArray(existing)) {
|
|
129
170
|
if (Array.isArray(value)) {
|
|
@@ -133,129 +174,259 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
133
174
|
}
|
|
134
175
|
} else {
|
|
135
176
|
if (Array.isArray(value)) {
|
|
136
|
-
setNestedValue(
|
|
177
|
+
setNestedValue(rawArgs, key, [existing, ...value]);
|
|
137
178
|
} else {
|
|
138
|
-
setNestedValue(
|
|
179
|
+
setNestedValue(rawArgs, key, [existing, value]);
|
|
139
180
|
}
|
|
140
181
|
}
|
|
141
182
|
} else {
|
|
142
|
-
setNestedValue(
|
|
183
|
+
setNestedValue(rawArgs, key, Array.isArray(value) ? value : [value]);
|
|
143
184
|
}
|
|
144
185
|
} else {
|
|
145
|
-
setNestedValue(
|
|
186
|
+
setNestedValue(rawArgs, key, value);
|
|
146
187
|
}
|
|
147
188
|
}
|
|
148
189
|
|
|
149
|
-
return { command: curCommand,
|
|
190
|
+
return { command: curCommand, rawArgs, args, unmatchedTerms };
|
|
150
191
|
};
|
|
151
192
|
|
|
152
193
|
/**
|
|
153
|
-
*
|
|
194
|
+
* Preprocesses raw arguments: applies env/config values and maps positional arguments.
|
|
195
|
+
* Also performs auto-coercion (string→number/boolean) and unknown arg detection.
|
|
154
196
|
*/
|
|
155
|
-
const
|
|
197
|
+
const buildCommandArgs = (
|
|
156
198
|
command: AnyPadroneCommand,
|
|
157
|
-
|
|
199
|
+
rawArgs: Record<string, unknown>,
|
|
158
200
|
args: string[],
|
|
159
|
-
|
|
160
|
-
) => {
|
|
161
|
-
// Apply preprocessing (env and config bindings)
|
|
162
|
-
|
|
201
|
+
context?: { stdinData?: Record<string, unknown>; envData?: Record<string, unknown>; configData?: Record<string, unknown> },
|
|
202
|
+
): Record<string, unknown> => {
|
|
203
|
+
// Apply preprocessing (stdin, env, and config bindings)
|
|
204
|
+
let preprocessedArgs = preprocessArgs(rawArgs, {
|
|
163
205
|
aliases: {}, // Already resolved aliases in parseCommand
|
|
164
|
-
|
|
165
|
-
|
|
206
|
+
stdinData: context?.stdinData,
|
|
207
|
+
envData: context?.envData,
|
|
208
|
+
configData: context?.configData,
|
|
166
209
|
});
|
|
167
210
|
|
|
168
211
|
// Parse positional configuration
|
|
169
212
|
const positionalConfig = command.meta?.positional ? parsePositionalConfig(command.meta.positional) : [];
|
|
170
213
|
|
|
171
|
-
// Map positional arguments to their named
|
|
214
|
+
// Map positional arguments to their named arguments
|
|
172
215
|
if (positionalConfig.length > 0) {
|
|
173
216
|
let argIndex = 0;
|
|
174
|
-
for (
|
|
217
|
+
for (let i = 0; i < positionalConfig.length; i++) {
|
|
218
|
+
const { name, variadic } = positionalConfig[i]!;
|
|
175
219
|
if (argIndex >= args.length) break;
|
|
176
220
|
|
|
177
221
|
if (variadic) {
|
|
178
222
|
// Collect remaining args (but leave room for non-variadic args after)
|
|
179
|
-
const remainingPositionals = positionalConfig.slice(
|
|
223
|
+
const remainingPositionals = positionalConfig.slice(i + 1);
|
|
180
224
|
const nonVariadicAfter = remainingPositionals.filter((p) => !p.variadic).length;
|
|
181
225
|
const variadicEnd = args.length - nonVariadicAfter;
|
|
182
|
-
|
|
226
|
+
preprocessedArgs[name] = args.slice(argIndex, variadicEnd);
|
|
183
227
|
argIndex = variadicEnd;
|
|
228
|
+
} else if (i === positionalConfig.length - 1 && args.length > argIndex + 1) {
|
|
229
|
+
// Last non-variadic positional: join all remaining tokens (e.g. `-- Hello world` → "Hello world")
|
|
230
|
+
preprocessedArgs[name] = args.slice(argIndex).join(' ');
|
|
231
|
+
argIndex = args.length;
|
|
184
232
|
} else {
|
|
185
|
-
|
|
233
|
+
preprocessedArgs[name] = args[argIndex];
|
|
186
234
|
argIndex++;
|
|
187
235
|
}
|
|
188
236
|
}
|
|
189
237
|
}
|
|
190
238
|
|
|
191
|
-
|
|
239
|
+
// Auto-coerce CLI string values to match schema types (string→number, string→boolean)
|
|
240
|
+
if (command.argsSchema) {
|
|
241
|
+
preprocessedArgs = coerceArgs(preprocessedArgs, command.argsSchema);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return preprocessedArgs;
|
|
245
|
+
};
|
|
192
246
|
|
|
193
|
-
|
|
194
|
-
|
|
247
|
+
/**
|
|
248
|
+
* Detects unknown options in args that aren't defined in the schema.
|
|
249
|
+
* Returns unknown key info with suggestions, or empty array if schema is loose.
|
|
250
|
+
*/
|
|
251
|
+
const checkUnknownArgs = (
|
|
252
|
+
command: AnyPadroneCommand,
|
|
253
|
+
preprocessedArgs: Record<string, unknown>,
|
|
254
|
+
): { key: string; suggestion: string }[] => {
|
|
255
|
+
if (!command.argsSchema) return [];
|
|
256
|
+
|
|
257
|
+
const argsMeta = command.meta?.fields;
|
|
258
|
+
const { aliases } = extractSchemaMetadata(command.argsSchema, argsMeta);
|
|
259
|
+
|
|
260
|
+
return detectUnknownArgs(preprocessedArgs, command.argsSchema, aliases, suggestSimilar);
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Validates preprocessed arguments against the command's schema.
|
|
265
|
+
* First checks for unknown args (strict by default), then runs schema validation.
|
|
266
|
+
* Returns sync or async result depending on the schema's validate method.
|
|
267
|
+
*/
|
|
268
|
+
const validateCommandArgs = (command: AnyPadroneCommand, preprocessedArgs: Record<string, unknown>) => {
|
|
269
|
+
// Check for unknown args before schema validation (strict by default)
|
|
270
|
+
const unknownArgs = checkUnknownArgs(command, preprocessedArgs);
|
|
271
|
+
if (unknownArgs.length > 0) {
|
|
272
|
+
const issues: StandardSchemaV1.Issue[] = unknownArgs.map(({ key, suggestion }) => ({
|
|
273
|
+
path: [key],
|
|
274
|
+
message: suggestion ? `Unknown option: "${key}". ${suggestion}` : `Unknown option: "${key}"`,
|
|
275
|
+
}));
|
|
276
|
+
return { args: undefined, argsResult: { issues } as any };
|
|
195
277
|
}
|
|
196
278
|
|
|
197
|
-
|
|
198
|
-
const hasOptions = command.options || Object.keys(preprocessedOptions).length > 0;
|
|
279
|
+
const argsParsed = command.argsSchema ? command.argsSchema['~standard'].validate(preprocessedArgs) : { value: preprocessedArgs };
|
|
199
280
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
281
|
+
// Return undefined for args when there's no schema and no meaningful args
|
|
282
|
+
const hasArgs = command.argsSchema || Object.keys(preprocessedArgs).length > 0;
|
|
283
|
+
|
|
284
|
+
const buildResult = (parsed: StandardSchemaV1.Result<unknown>) => ({
|
|
285
|
+
args: parsed.issues ? undefined : hasArgs ? (parsed.value as any) : undefined,
|
|
286
|
+
argsResult: parsed as any,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
return thenMaybe(argsParsed, buildResult);
|
|
204
290
|
};
|
|
205
291
|
|
|
206
|
-
|
|
207
|
-
|
|
292
|
+
/**
|
|
293
|
+
* Preprocesses and validates raw arguments against the command's schema.
|
|
294
|
+
* Returns sync or async result depending on the schema's validate method.
|
|
295
|
+
*/
|
|
296
|
+
const validateArgs = (
|
|
297
|
+
command: AnyPadroneCommand,
|
|
298
|
+
rawArgs: Record<string, unknown>,
|
|
299
|
+
args: string[],
|
|
300
|
+
context?: { stdinData?: Record<string, unknown>; envData?: Record<string, unknown>; configData?: Record<string, unknown> },
|
|
301
|
+
) => {
|
|
302
|
+
const preprocessedArgs = buildCommandArgs(command, rawArgs, args, context);
|
|
303
|
+
return validateCommandArgs(command, preprocessedArgs);
|
|
304
|
+
};
|
|
208
305
|
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
306
|
+
const parse: AnyPadroneProgram['parse'] = (input) => {
|
|
307
|
+
const state: Record<string, unknown> = {};
|
|
308
|
+
|
|
309
|
+
// Parse phase (with plugins)
|
|
310
|
+
const parseCtx: PluginParseContext = { input: input as string | undefined, command: existingCommand, state };
|
|
311
|
+
const coreParse = (): PluginParseResult => {
|
|
312
|
+
const { command, rawArgs, args } = parseCommand(parseCtx.input);
|
|
313
|
+
return { command, rawArgs, positionalArgs: args };
|
|
214
314
|
};
|
|
215
|
-
const envSchema = resolveEnvSchema(command);
|
|
216
|
-
|
|
217
|
-
// Validate env vars against schema if provided
|
|
218
|
-
let envData: Record<string, unknown> | undefined = parseOptions?.envData;
|
|
219
|
-
if (envSchema && !envData) {
|
|
220
|
-
const rawEnv = parseOptions?.env ?? (typeof process !== 'undefined' ? process.env : {});
|
|
221
|
-
const envValidated = envSchema['~standard'].validate(rawEnv);
|
|
222
|
-
if (envValidated instanceof Promise) {
|
|
223
|
-
throw new Error('Async validation is not supported. Env schema validate() must return a synchronous result.');
|
|
224
|
-
}
|
|
225
|
-
// For env vars, we don't throw on validation errors - just use the transformed value if valid
|
|
226
|
-
if (!envValidated.issues) {
|
|
227
|
-
envData = envValidated.value as unknown as Record<string, unknown>;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
315
|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
});
|
|
316
|
+
// Parse phase: root plugins only
|
|
317
|
+
const rootPlugins = existingCommand.plugins ?? [];
|
|
318
|
+
const parsedOrPromise = runPluginChain('parse', rootPlugins, parseCtx, coreParse);
|
|
235
319
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
320
|
+
const continueAfterParse = (parsed: PluginParseResult) => {
|
|
321
|
+
const { command } = parsed;
|
|
322
|
+
|
|
323
|
+
// Validate phase: collected from parent chain
|
|
324
|
+
const commandPlugins = collectPlugins(command);
|
|
325
|
+
const validateCtx: PluginValidateContext = {
|
|
326
|
+
command,
|
|
327
|
+
rawArgs: parsed.rawArgs,
|
|
328
|
+
positionalArgs: parsed.positionalArgs,
|
|
329
|
+
state,
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
const coreValidate = (): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
333
|
+
// Resolve env schema: command's own envSchema > inherited from parent/root
|
|
334
|
+
const resolveEnvSchema = (cmd: AnyPadroneCommand): AnyPadroneCommand['envSchema'] => {
|
|
335
|
+
if (cmd.envSchema !== undefined) return cmd.envSchema;
|
|
336
|
+
if (cmd.parent) return resolveEnvSchema(cmd.parent);
|
|
337
|
+
return undefined;
|
|
338
|
+
};
|
|
339
|
+
const envSchema = resolveEnvSchema(command);
|
|
340
|
+
|
|
341
|
+
const readStdinForParse = (): Record<string, unknown> | Promise<Record<string, unknown>> => {
|
|
342
|
+
const stdinConfig = command.meta?.stdin;
|
|
343
|
+
if (!stdinConfig) return {};
|
|
344
|
+
|
|
345
|
+
const { field, as } = parseStdinConfig(stdinConfig);
|
|
346
|
+
|
|
347
|
+
// Skip if the field was already provided via CLI flags
|
|
348
|
+
if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
|
|
349
|
+
|
|
350
|
+
const runtime = getCommandRuntime(existingCommand);
|
|
351
|
+
const stdin = resolveStdin(runtime as any);
|
|
352
|
+
if (!stdin) return {};
|
|
353
|
+
|
|
354
|
+
if (as === 'lines') {
|
|
355
|
+
return (async () => {
|
|
356
|
+
const lines: string[] = [];
|
|
357
|
+
for await (const line of stdin.lines()) {
|
|
358
|
+
lines.push(line);
|
|
359
|
+
}
|
|
360
|
+
return { [field]: lines };
|
|
361
|
+
})();
|
|
362
|
+
}
|
|
363
|
+
return stdin.text().then((text) => (text ? { [field]: text } : {}));
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const finalize = (
|
|
367
|
+
envData: Record<string, unknown> | undefined,
|
|
368
|
+
stdinData: Record<string, unknown> | undefined,
|
|
369
|
+
): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
370
|
+
const validated = validateArgs(command, validateCtx.rawArgs, validateCtx.positionalArgs, { stdinData, envData });
|
|
371
|
+
return thenMaybe(validated, (v) => v as PluginValidateResult);
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
let envData: Record<string, unknown> | undefined;
|
|
375
|
+
const afterEnv = (envResult: Record<string, unknown> | undefined) => {
|
|
376
|
+
const stdinDataOrPromise = readStdinForParse();
|
|
377
|
+
return thenMaybe(stdinDataOrPromise, (stdinData) => {
|
|
378
|
+
const hasStdinData = Object.keys(stdinData).length > 0;
|
|
379
|
+
return finalize(envResult, hasStdinData ? stdinData : undefined);
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (envSchema) {
|
|
384
|
+
const runtime = getCommandRuntime(existingCommand);
|
|
385
|
+
const rawEnv = runtime.env();
|
|
386
|
+
const envValidated = envSchema['~standard'].validate(rawEnv);
|
|
387
|
+
|
|
388
|
+
return thenMaybe(envValidated, (result) => {
|
|
389
|
+
if (!result.issues) {
|
|
390
|
+
envData = result.value as unknown as Record<string, unknown>;
|
|
391
|
+
}
|
|
392
|
+
return afterEnv(envData);
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return afterEnv(envData);
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const validatedOrPromise = runPluginChain('validate', commandPlugins, validateCtx, coreValidate);
|
|
400
|
+
|
|
401
|
+
return warnIfUnexpectedAsync(
|
|
402
|
+
thenMaybe(validatedOrPromise, (v) => ({
|
|
403
|
+
command: command as any,
|
|
404
|
+
args: v.args,
|
|
405
|
+
argsResult: v.argsResult,
|
|
406
|
+
})),
|
|
407
|
+
command,
|
|
408
|
+
);
|
|
240
409
|
};
|
|
410
|
+
|
|
411
|
+
return thenMaybe(parsedOrPromise, continueAfterParse) as any;
|
|
241
412
|
};
|
|
242
413
|
|
|
243
|
-
const stringify: AnyPadroneProgram['stringify'] = (command = '' as any,
|
|
414
|
+
const stringify: AnyPadroneProgram['stringify'] = (command = '' as any, args) => {
|
|
244
415
|
const commandObj = typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
|
|
245
|
-
if (!commandObj) throw new
|
|
416
|
+
if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
|
|
246
417
|
|
|
247
418
|
const parts: string[] = [];
|
|
248
419
|
|
|
249
420
|
if (commandObj.path) parts.push(commandObj.path);
|
|
250
421
|
|
|
251
|
-
// Get positional config to determine which
|
|
422
|
+
// Get positional config to determine which args are positional
|
|
252
423
|
const positionalConfig = commandObj.meta?.positional ? parsePositionalConfig(commandObj.meta.positional) : [];
|
|
253
424
|
const positionalNames = new Set(positionalConfig.map((p) => p.name));
|
|
254
425
|
|
|
255
426
|
// Output positional arguments first in order
|
|
256
|
-
if (
|
|
427
|
+
if (args && typeof args === 'object') {
|
|
257
428
|
for (const { name, variadic } of positionalConfig) {
|
|
258
|
-
const value = (
|
|
429
|
+
const value = (args as Record<string, unknown>)[name];
|
|
259
430
|
if (value === undefined) continue;
|
|
260
431
|
|
|
261
432
|
if (variadic && Array.isArray(value)) {
|
|
@@ -279,7 +450,7 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
279
450
|
if (value) parts.push(`--${key}`);
|
|
280
451
|
else parts.push(`--no-${key}`);
|
|
281
452
|
} else if (Array.isArray(value)) {
|
|
282
|
-
// Handle variadic
|
|
453
|
+
// Handle variadic arguments - output each value separately
|
|
283
454
|
for (const v of value) {
|
|
284
455
|
const vStr = String(v);
|
|
285
456
|
if (vStr.includes(' ')) parts.push(`--${key}="${vStr}"`);
|
|
@@ -298,8 +469,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
298
469
|
}
|
|
299
470
|
};
|
|
300
471
|
|
|
301
|
-
// Output remaining
|
|
302
|
-
for (const [key, value] of Object.entries(
|
|
472
|
+
// Output remaining arguments (non-positional)
|
|
473
|
+
for (const [key, value] of Object.entries(args)) {
|
|
303
474
|
if (value === undefined || positionalNames.has(key)) continue;
|
|
304
475
|
stringifyValue(key, value);
|
|
305
476
|
}
|
|
@@ -320,31 +491,32 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
320
491
|
):
|
|
321
492
|
| { type: 'help'; command?: AnyPadroneCommand; detail?: DetailLevel; format?: FormatLevel }
|
|
322
493
|
| { type: 'version' }
|
|
323
|
-
| { type: 'completion'; shell?: ShellType }
|
|
494
|
+
| { type: 'completion'; shell?: ShellType; setup?: boolean }
|
|
495
|
+
| { type: 'repl'; scope?: string }
|
|
324
496
|
| null => {
|
|
325
497
|
if (!input) return null;
|
|
326
498
|
|
|
327
499
|
const parts = parseCliInputToParts(input);
|
|
328
500
|
const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
|
|
329
|
-
const
|
|
501
|
+
const args = parts.filter((p) => p.type === 'named' || p.type === 'alias');
|
|
330
502
|
|
|
331
503
|
// Helper to check if a key array matches a single key string
|
|
332
504
|
const keyIs = (key: string[], name: string) => key.length === 1 && key[0] === name;
|
|
333
505
|
|
|
334
506
|
// Check for --help, -h flags (these take precedence over commands)
|
|
335
|
-
const hasHelpFlag =
|
|
507
|
+
const hasHelpFlag = args.some((p) => (p.type === 'named' && keyIs(p.key, 'help')) || (p.type === 'alias' && keyIs(p.key, 'h')));
|
|
336
508
|
|
|
337
509
|
// Extract detail level from --detail=<level> or -d <level>
|
|
338
510
|
const getDetailLevel = (): DetailLevel | undefined => {
|
|
339
|
-
for (const
|
|
340
|
-
if (
|
|
341
|
-
if (
|
|
342
|
-
return
|
|
511
|
+
for (const arg of args) {
|
|
512
|
+
if (arg.type === 'named' && keyIs(arg.key, 'detail') && typeof arg.value === 'string') {
|
|
513
|
+
if (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full') {
|
|
514
|
+
return arg.value;
|
|
343
515
|
}
|
|
344
516
|
}
|
|
345
|
-
if (
|
|
346
|
-
if (
|
|
347
|
-
return
|
|
517
|
+
if (arg.type === 'alias' && keyIs(arg.key, 'd') && typeof arg.value === 'string') {
|
|
518
|
+
if (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full') {
|
|
519
|
+
return arg.value;
|
|
348
520
|
}
|
|
349
521
|
}
|
|
350
522
|
}
|
|
@@ -355,15 +527,15 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
355
527
|
// Extract format from --format=<value> or -f <value>
|
|
356
528
|
const getFormat = (): FormatLevel | undefined => {
|
|
357
529
|
const validFormats: FormatLevel[] = ['text', 'ansi', 'console', 'markdown', 'html', 'json', 'auto'];
|
|
358
|
-
for (const
|
|
359
|
-
if (
|
|
360
|
-
if (validFormats.includes(
|
|
361
|
-
return
|
|
530
|
+
for (const arg of args) {
|
|
531
|
+
if (arg.type === 'named' && keyIs(arg.key, 'format') && typeof arg.value === 'string') {
|
|
532
|
+
if (validFormats.includes(arg.value as FormatLevel)) {
|
|
533
|
+
return arg.value as FormatLevel;
|
|
362
534
|
}
|
|
363
535
|
}
|
|
364
|
-
if (
|
|
365
|
-
if (validFormats.includes(
|
|
366
|
-
return
|
|
536
|
+
if (arg.type === 'alias' && keyIs(arg.key, 'f') && typeof arg.value === 'string') {
|
|
537
|
+
if (validFormats.includes(arg.value as FormatLevel)) {
|
|
538
|
+
return arg.value as FormatLevel;
|
|
367
539
|
}
|
|
368
540
|
}
|
|
369
541
|
}
|
|
@@ -372,8 +544,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
372
544
|
const format = getFormat();
|
|
373
545
|
|
|
374
546
|
// Check for --version, -v, -V flags
|
|
375
|
-
const hasVersionFlag =
|
|
376
|
-
(p) => (p.type === '
|
|
547
|
+
const hasVersionFlag = args.some(
|
|
548
|
+
(p) => (p.type === 'named' && keyIs(p.key, 'version')) || (p.type === 'alias' && (keyIs(p.key, 'v') || keyIs(p.key, 'V'))),
|
|
377
549
|
);
|
|
378
550
|
|
|
379
551
|
// If the first term is the program name, skip it
|
|
@@ -386,12 +558,30 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
386
558
|
const userCompletionCommand = findCommandByName('completion', existingCommand.commands);
|
|
387
559
|
|
|
388
560
|
// Check for 'help' command (only if user hasn't defined one)
|
|
561
|
+
// Supports both 'help <command>' and '<command> help' forms
|
|
389
562
|
if (!userHelpCommand && normalizedTerms[0] === 'help') {
|
|
390
563
|
// help <command> - get help for specific command
|
|
391
564
|
const commandName = normalizedTerms.slice(1).join(' ');
|
|
392
565
|
const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
|
|
393
566
|
return { type: 'help', command: targetCommand, detail, format };
|
|
394
567
|
}
|
|
568
|
+
if (!userHelpCommand && normalizedTerms.length > 0 && normalizedTerms[normalizedTerms.length - 1] === 'help') {
|
|
569
|
+
// <command> help - get help for specific command (trailing form)
|
|
570
|
+
const commandTerms = normalizedTerms.slice(0, -1);
|
|
571
|
+
// Walk the command tree to find the deepest matching command
|
|
572
|
+
let targetCommand: AnyPadroneCommand | undefined;
|
|
573
|
+
let current = existingCommand;
|
|
574
|
+
for (const term of commandTerms) {
|
|
575
|
+
const found = findCommandByName(term, current.commands);
|
|
576
|
+
if (found) {
|
|
577
|
+
targetCommand = found;
|
|
578
|
+
current = found;
|
|
579
|
+
} else {
|
|
580
|
+
break;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return { type: 'help', command: targetCommand, detail, format };
|
|
584
|
+
}
|
|
395
585
|
|
|
396
586
|
// Check for 'version' command (only if user hasn't defined one)
|
|
397
587
|
if (!userVersionCommand && normalizedTerms[0] === 'version') {
|
|
@@ -403,7 +593,8 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
403
593
|
const shellArg = normalizedTerms[1] as ShellType | undefined;
|
|
404
594
|
const validShells: ShellType[] = ['bash', 'zsh', 'fish', 'powershell'];
|
|
405
595
|
const shell = shellArg && validShells.includes(shellArg) ? shellArg : undefined;
|
|
406
|
-
|
|
596
|
+
const setup = args.some((p) => p.type === 'named' && keyIs(p.key, 'setup'));
|
|
597
|
+
return { type: 'completion', shell, setup };
|
|
407
598
|
}
|
|
408
599
|
|
|
409
600
|
// Handle help flag - find the command being requested
|
|
@@ -420,6 +611,13 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
420
611
|
return { type: 'version' };
|
|
421
612
|
}
|
|
422
613
|
|
|
614
|
+
// Check for --repl flag
|
|
615
|
+
const hasReplFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'repl'));
|
|
616
|
+
if (hasReplFlag) {
|
|
617
|
+
const scope = normalizedTerms.length > 0 ? normalizedTerms.join(' ') : undefined;
|
|
618
|
+
return { type: 'repl', scope };
|
|
619
|
+
}
|
|
620
|
+
|
|
423
621
|
return null;
|
|
424
622
|
};
|
|
425
623
|
|
|
@@ -430,171 +628,642 @@ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadronePr
|
|
|
430
628
|
if (!input) return undefined;
|
|
431
629
|
|
|
432
630
|
const parts = parseCliInputToParts(input);
|
|
433
|
-
const
|
|
631
|
+
const args = parts.filter((p) => p.type === 'named' || p.type === 'alias');
|
|
434
632
|
|
|
435
|
-
for (const
|
|
436
|
-
if (
|
|
437
|
-
return
|
|
633
|
+
for (const arg of args) {
|
|
634
|
+
if (arg.type === 'named' && arg.key.length === 1 && arg.key[0] === 'config' && typeof arg.value === 'string') {
|
|
635
|
+
return arg.value;
|
|
438
636
|
}
|
|
439
|
-
if (
|
|
440
|
-
return
|
|
637
|
+
if (arg.type === 'alias' && arg.key.length === 1 && arg.key[0] === 'c' && typeof arg.value === 'string') {
|
|
638
|
+
return arg.value;
|
|
441
639
|
}
|
|
442
640
|
}
|
|
443
641
|
return undefined;
|
|
444
642
|
};
|
|
445
643
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
644
|
+
/**
|
|
645
|
+
* Core execution logic shared by eval() and cli().
|
|
646
|
+
* errorMode controls validation error behavior:
|
|
647
|
+
* - 'soft': return result with issues (eval behavior)
|
|
648
|
+
* - 'hard': print error + help and throw (cli-without-input behavior)
|
|
649
|
+
*/
|
|
650
|
+
const execCommand = (resolvedInput: string | undefined, evalOptions?: PadroneEvalPreferences, errorMode: 'soft' | 'hard' = 'soft') => {
|
|
651
|
+
const baseRuntime = getCommandRuntime(existingCommand);
|
|
652
|
+
const runtime = evalOptions?.runtime
|
|
653
|
+
? Object.assign({}, baseRuntime, Object.fromEntries(Object.entries(evalOptions.runtime).filter(([, v]) => v !== undefined)))
|
|
654
|
+
: baseRuntime;
|
|
449
655
|
|
|
450
|
-
// Check for built-in help/version/completion commands and flags
|
|
656
|
+
// Check for built-in help/version/completion commands and flags (bypass plugins)
|
|
451
657
|
const builtin = checkBuiltinCommands(resolvedInput);
|
|
452
658
|
|
|
453
659
|
if (builtin) {
|
|
454
660
|
if (builtin.type === 'help') {
|
|
455
661
|
const helpText = generateHelp(existingCommand, builtin.command ?? existingCommand, {
|
|
456
662
|
detail: builtin.detail,
|
|
457
|
-
format: builtin.format,
|
|
663
|
+
format: builtin.format ?? runtime.format,
|
|
458
664
|
});
|
|
459
|
-
|
|
665
|
+
runtime.output(helpText);
|
|
460
666
|
return {
|
|
461
667
|
command: existingCommand,
|
|
462
668
|
args: undefined,
|
|
463
|
-
options: undefined,
|
|
464
669
|
result: helpText,
|
|
465
670
|
} as any;
|
|
466
671
|
}
|
|
467
672
|
|
|
468
673
|
if (builtin.type === 'version') {
|
|
469
674
|
const version = getVersion(existingCommand.version);
|
|
470
|
-
|
|
675
|
+
runtime.output(version);
|
|
471
676
|
return {
|
|
472
677
|
command: existingCommand,
|
|
473
|
-
|
|
678
|
+
args: undefined,
|
|
474
679
|
result: version,
|
|
475
680
|
} as any;
|
|
476
681
|
}
|
|
477
682
|
|
|
478
683
|
if (builtin.type === 'completion') {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
684
|
+
return import('./completion.ts').then(({ detectShell, generateCompletionOutput, setupCompletions }) => {
|
|
685
|
+
if (builtin.setup) {
|
|
686
|
+
const shell = builtin.shell ?? detectShell();
|
|
687
|
+
if (!shell) {
|
|
688
|
+
throw new Error('Could not detect shell. Specify one: completion bash --setup');
|
|
689
|
+
}
|
|
690
|
+
const result = setupCompletions(existingCommand.name, shell);
|
|
691
|
+
const message = `${result.updated ? 'Updated' : 'Added'} ${existingCommand.name} completions in ${result.file}`;
|
|
692
|
+
runtime.output(message);
|
|
693
|
+
return {
|
|
694
|
+
command: existingCommand,
|
|
695
|
+
args: undefined,
|
|
696
|
+
result: message,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
const completionScript = generateCompletionOutput(existingCommand, builtin.shell);
|
|
700
|
+
runtime.output(completionScript);
|
|
701
|
+
return {
|
|
702
|
+
command: existingCommand,
|
|
703
|
+
args: undefined,
|
|
704
|
+
result: completionScript,
|
|
705
|
+
};
|
|
706
|
+
}) as any;
|
|
486
707
|
}
|
|
487
708
|
}
|
|
488
709
|
|
|
489
|
-
//
|
|
490
|
-
const
|
|
710
|
+
// Shared plugin state for this execution
|
|
711
|
+
const state: Record<string, unknown> = {};
|
|
712
|
+
const rootPlugins = existingCommand.plugins ?? [];
|
|
713
|
+
|
|
714
|
+
const runPipeline = () => {
|
|
715
|
+
// ── Phase 1: Parse ──────────────────────────────────────────────────
|
|
716
|
+
const parseCtx: PluginParseContext = { input: resolvedInput, command: existingCommand, state };
|
|
717
|
+
|
|
718
|
+
const coreParse = (): PluginParseResult => {
|
|
719
|
+
const { command, rawArgs, args, unmatchedTerms } = parseCommand(parseCtx.input);
|
|
720
|
+
|
|
721
|
+
// Default help: command with no action → show its help when there's nothing to execute.
|
|
722
|
+
const hasSubcommands = command.commands && command.commands.length > 0;
|
|
723
|
+
const hasSchema = command.argsSchema != null;
|
|
724
|
+
if (!command.action && (hasSubcommands || !hasSchema) && unmatchedTerms.length === 0) {
|
|
725
|
+
const helpText = generateHelp(existingCommand, command, { format: runtime.format });
|
|
726
|
+
runtime.output(helpText);
|
|
727
|
+
return {
|
|
728
|
+
command: command,
|
|
729
|
+
rawArgs: { '~help': helpText } as Record<string, unknown>,
|
|
730
|
+
positionalArgs: [],
|
|
731
|
+
};
|
|
732
|
+
}
|
|
491
733
|
|
|
492
|
-
|
|
493
|
-
|
|
734
|
+
// Reject unmatched terms when the matched command doesn't accept positional args
|
|
735
|
+
if (unmatchedTerms.length > 0) {
|
|
736
|
+
const hasPositionalConfig = command.meta?.positional && command.meta.positional.length > 0;
|
|
737
|
+
if (!hasPositionalConfig) {
|
|
738
|
+
const isRootCommand = command === existingCommand;
|
|
739
|
+
const commandDisplayName = command.name || command.aliases?.[0] || command.path || '(default)';
|
|
740
|
+
|
|
741
|
+
// Collect candidate names for fuzzy suggestion
|
|
742
|
+
const candidateNames: string[] = [];
|
|
743
|
+
if (isRootCommand && existingCommand.commands) {
|
|
744
|
+
for (const cmd of existingCommand.commands) {
|
|
745
|
+
if (!cmd.hidden) {
|
|
746
|
+
candidateNames.push(cmd.name);
|
|
747
|
+
if (cmd.aliases) candidateNames.push(...cmd.aliases);
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
} else if (command.commands) {
|
|
751
|
+
for (const cmd of command.commands) {
|
|
752
|
+
if (!cmd.hidden) {
|
|
753
|
+
candidateNames.push(cmd.name);
|
|
754
|
+
if (cmd.aliases) candidateNames.push(...cmd.aliases);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
494
758
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
759
|
+
const suggestion = suggestSimilar(unmatchedTerms[0]!, candidateNames);
|
|
760
|
+
const suggestions = suggestion ? [suggestion] : [];
|
|
761
|
+
const baseMsg = isRootCommand
|
|
762
|
+
? `Unknown command: ${unmatchedTerms[0]}`
|
|
763
|
+
: `Unexpected arguments for '${commandDisplayName}': ${unmatchedTerms.join(' ')}`;
|
|
764
|
+
const errorMsg = suggestions.length ? `${baseMsg}\n\n ${suggestions[0]}` : baseMsg;
|
|
765
|
+
|
|
766
|
+
if (errorMode === 'hard') {
|
|
767
|
+
runtime.error(errorMsg);
|
|
768
|
+
// When we have a suggestion, show a compact single-line "Available commands" note
|
|
769
|
+
// instead of the full help text to avoid overwhelming the user
|
|
770
|
+
if (suggestions.length > 0) {
|
|
771
|
+
const targetCmd = isRootCommand ? existingCommand : command;
|
|
772
|
+
const visibleCommands = (targetCmd.commands ?? []).filter((c) => !c.hidden && c.name);
|
|
773
|
+
if (visibleCommands.length > 0) {
|
|
774
|
+
const cmdList = visibleCommands.map((c) => c.name).join(', ');
|
|
775
|
+
runtime.output(`\nAvailable commands: ${cmdList}`);
|
|
776
|
+
}
|
|
777
|
+
} else {
|
|
778
|
+
const helpText = generateHelp(existingCommand, isRootCommand ? existingCommand : command, { format: runtime.format });
|
|
779
|
+
runtime.error(helpText);
|
|
780
|
+
}
|
|
781
|
+
throw new RoutingError(errorMsg, { suggestions, command: command.path || command.name });
|
|
782
|
+
}
|
|
503
783
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
return undefined;
|
|
509
|
-
};
|
|
510
|
-
const configSchema = resolveConfigSchema(command);
|
|
784
|
+
// Soft mode: throw too — this is a routing error, not a validation issue
|
|
785
|
+
throw new RoutingError(errorMsg, { suggestions, command: command.path || command.name });
|
|
786
|
+
}
|
|
787
|
+
}
|
|
511
788
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
789
|
+
return { command, rawArgs, positionalArgs: args };
|
|
790
|
+
};
|
|
791
|
+
|
|
792
|
+
// Parse phase: root plugins only
|
|
793
|
+
const parsedOrPromise = runPluginChain('parse', rootPlugins, parseCtx, coreParse);
|
|
794
|
+
|
|
795
|
+
// ── Phases 2 & 3 chained after parse ────────────────────────────────
|
|
796
|
+
const continueAfterParse = (parsed: PluginParseResult) => {
|
|
797
|
+
const { command } = parsed;
|
|
798
|
+
// Validate/execute: collected from parent chain
|
|
799
|
+
const commandPlugins = collectPlugins(command);
|
|
800
|
+
|
|
801
|
+
// Short-circuit: parse returned a help result
|
|
802
|
+
if (parsed.rawArgs['~help']) {
|
|
803
|
+
return {
|
|
804
|
+
command: command,
|
|
805
|
+
args: undefined,
|
|
806
|
+
result: parsed.rawArgs['~help'],
|
|
807
|
+
} as any;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// ── Phase 2: Validate ───────────────────────────────────────────
|
|
811
|
+
const validateCtx: PluginValidateContext = {
|
|
812
|
+
command,
|
|
813
|
+
rawArgs: parsed.rawArgs,
|
|
814
|
+
positionalArgs: parsed.positionalArgs,
|
|
815
|
+
state,
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
const coreValidate = (): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
819
|
+
// Determine interactivity
|
|
820
|
+
let flagInteractive: boolean | undefined;
|
|
821
|
+
if (hasInteractiveConfig(command.meta)) {
|
|
822
|
+
if (validateCtx.rawArgs.interactive !== undefined) {
|
|
823
|
+
flagInteractive = validateCtx.rawArgs.interactive !== false && validateCtx.rawArgs.interactive !== 'false';
|
|
824
|
+
delete validateCtx.rawArgs.interactive;
|
|
825
|
+
}
|
|
826
|
+
if (validateCtx.rawArgs.i !== undefined) {
|
|
827
|
+
flagInteractive = validateCtx.rawArgs.i !== false && validateCtx.rawArgs.i !== 'false';
|
|
828
|
+
delete validateCtx.rawArgs.i;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
const runtimeDefault: boolean | undefined =
|
|
833
|
+
runtime.interactive === 'forced' ? true : runtime.interactive === 'disabled' ? false : undefined;
|
|
834
|
+
const effectiveInteractive: boolean | undefined = flagInteractive ?? evalOptions?.interactive ?? runtimeDefault;
|
|
835
|
+
// Suppress interactive prompts when the command reads stdin — prompts share stdin which is already consumed/closed.
|
|
836
|
+
const commandUsesStdin = !!command.meta?.stdin;
|
|
837
|
+
const stdinIsPiped =
|
|
838
|
+
commandUsesStdin && (runtime.stdin ? !runtime.stdin.isTTY : typeof process !== 'undefined' && process.stdin?.isTTY !== true);
|
|
839
|
+
const interactivitySuppressed =
|
|
840
|
+
runtime.interactive === 'unsupported' || effectiveInteractive === false || (stdinIsPiped && effectiveInteractive !== true);
|
|
841
|
+
const forceInteractive = !interactivitySuppressed && effectiveInteractive === true;
|
|
842
|
+
|
|
843
|
+
// Extract config file path from --config or -c flag
|
|
844
|
+
const configPath = extractConfigPath(parseCtx.input);
|
|
845
|
+
|
|
846
|
+
// Resolve config files: command's own configFiles > inherited from parent/root
|
|
847
|
+
const resolveConfigFiles = (cmd: AnyPadroneCommand): string[] | undefined => {
|
|
848
|
+
if (cmd.configFiles !== undefined) return cmd.configFiles;
|
|
849
|
+
if (cmd.parent) return resolveConfigFiles(cmd.parent);
|
|
850
|
+
return undefined;
|
|
851
|
+
};
|
|
852
|
+
const effectiveConfigFiles = resolveConfigFiles(command);
|
|
853
|
+
|
|
854
|
+
// Resolve config schema: command's own configSchema > inherited from parent/root
|
|
855
|
+
const resolveConfigSchema = (cmd: AnyPadroneCommand): AnyPadroneCommand['configSchema'] => {
|
|
856
|
+
if (cmd.configSchema !== undefined) return cmd.configSchema;
|
|
857
|
+
if (cmd.parent) return resolveConfigSchema(cmd.parent);
|
|
858
|
+
return undefined;
|
|
859
|
+
};
|
|
860
|
+
const configSchema = resolveConfigSchema(command);
|
|
861
|
+
|
|
862
|
+
// Resolve env schema: command's own envSchema > inherited from parent/root
|
|
863
|
+
const resolveEnvSchema = (cmd: AnyPadroneCommand): AnyPadroneCommand['envSchema'] => {
|
|
864
|
+
if (cmd.envSchema !== undefined) return cmd.envSchema;
|
|
865
|
+
if (cmd.parent) return resolveEnvSchema(cmd.parent);
|
|
866
|
+
return undefined;
|
|
867
|
+
};
|
|
868
|
+
const envSchema = resolveEnvSchema(command);
|
|
869
|
+
|
|
870
|
+
// Determine config data: explicit --config flag > auto-discovered config
|
|
871
|
+
let configData: Record<string, unknown> | undefined;
|
|
872
|
+
if (configPath) {
|
|
873
|
+
configData = runtime.loadConfigFile(configPath);
|
|
874
|
+
} else if (effectiveConfigFiles?.length) {
|
|
875
|
+
const foundConfigPath = runtime.findFile(effectiveConfigFiles);
|
|
876
|
+
if (foundConfigPath) {
|
|
877
|
+
configData = runtime.loadConfigFile(foundConfigPath) ?? configData;
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// Step 1: Validate config data against schema if provided
|
|
882
|
+
const validateConfig = (): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> => {
|
|
883
|
+
if (configData && configSchema) {
|
|
884
|
+
const configValidated = configSchema['~standard'].validate(configData);
|
|
885
|
+
return thenMaybe(configValidated, (result) => {
|
|
886
|
+
if (result.issues) {
|
|
887
|
+
const issueMessages = result.issues
|
|
888
|
+
.map((i: StandardSchemaV1.Issue) => ` - ${i.path?.join('.') || 'root'}: ${i.message}`)
|
|
889
|
+
.join('\n');
|
|
890
|
+
throw new ConfigError(`Invalid config file:\n${issueMessages}`, {
|
|
891
|
+
command: command.path || command.name,
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return result.value as unknown as Record<string, unknown>;
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
return configData;
|
|
898
|
+
};
|
|
899
|
+
|
|
900
|
+
// Step 2: Validate env vars
|
|
901
|
+
const validateEnv = (): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> => {
|
|
902
|
+
let envData: Record<string, unknown> | undefined;
|
|
903
|
+
if (envSchema) {
|
|
904
|
+
const rawEnv = runtime.env();
|
|
905
|
+
const envValidated = envSchema['~standard'].validate(rawEnv);
|
|
906
|
+
return thenMaybe(envValidated, (result) => {
|
|
907
|
+
if (!result.issues) {
|
|
908
|
+
envData = result.value as unknown as Record<string, unknown>;
|
|
909
|
+
}
|
|
910
|
+
return envData;
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
return envData;
|
|
914
|
+
};
|
|
915
|
+
|
|
916
|
+
// Step 3: Read stdin if configured and not already provided via CLI
|
|
917
|
+
const readStdin = (): Record<string, unknown> | Promise<Record<string, unknown>> => {
|
|
918
|
+
const stdinConfig = command.meta?.stdin;
|
|
919
|
+
if (!stdinConfig) return {};
|
|
920
|
+
|
|
921
|
+
const { field, as } = parseStdinConfig(stdinConfig);
|
|
922
|
+
|
|
923
|
+
// Skip if the field was already provided via CLI flags (highest precedence)
|
|
924
|
+
if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
|
|
925
|
+
|
|
926
|
+
// Resolve stdin: use runtime's custom stdin, or default if piped.
|
|
927
|
+
// Returns undefined when stdin is a TTY or unavailable.
|
|
928
|
+
const stdin = resolveStdin(runtime as any);
|
|
929
|
+
if (!stdin) return {};
|
|
930
|
+
|
|
931
|
+
if (as === 'lines') {
|
|
932
|
+
return (async () => {
|
|
933
|
+
const lines: string[] = [];
|
|
934
|
+
for await (const line of stdin.lines()) {
|
|
935
|
+
lines.push(line);
|
|
936
|
+
}
|
|
937
|
+
return { [field]: lines };
|
|
938
|
+
})();
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// Default: read all as text
|
|
942
|
+
return stdin.text().then((text) => {
|
|
943
|
+
// Don't inject empty stdin
|
|
944
|
+
if (!text) return {};
|
|
945
|
+
return { [field]: text };
|
|
946
|
+
});
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
// Step 4: Preprocess, interactive prompt, and validate
|
|
950
|
+
const finalizeValidation = (
|
|
951
|
+
validatedConfigData: Record<string, unknown> | undefined,
|
|
952
|
+
envData: Record<string, unknown> | undefined,
|
|
953
|
+
stdinData: Record<string, unknown> | undefined,
|
|
954
|
+
): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
955
|
+
const preprocessedArgs = buildCommandArgs(command, validateCtx.rawArgs, validateCtx.positionalArgs, {
|
|
956
|
+
stdinData,
|
|
957
|
+
envData,
|
|
958
|
+
configData: validatedConfigData,
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
// Early validation: check provided args for errors before prompting.
|
|
962
|
+
// This catches unknown options and invalid values on explicitly-provided fields
|
|
963
|
+
// so the user isn't asked interactive questions for a doomed command.
|
|
964
|
+
const willPrompt = !interactivitySuppressed && runtime.prompt && hasInteractiveConfig(command.meta);
|
|
965
|
+
if (willPrompt) {
|
|
966
|
+
const unknowns = checkUnknownArgs(command, preprocessedArgs);
|
|
967
|
+
if (unknowns.length > 0) {
|
|
968
|
+
const issues: StandardSchemaV1.Issue[] = unknowns.map(({ key, suggestion }) => ({
|
|
969
|
+
path: [key],
|
|
970
|
+
message: suggestion ? `Unknown option: "${key}". ${suggestion}` : `Unknown option: "${key}"`,
|
|
971
|
+
}));
|
|
972
|
+
return { args: undefined, argsResult: { issues } as any };
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// Run schema validation on what we have so far (before prompting fills missing fields).
|
|
976
|
+
// Only fail on issues for fields the user explicitly provided — skip issues for
|
|
977
|
+
// missing/undefined fields since those will be filled by interactive prompts.
|
|
978
|
+
if (command.argsSchema) {
|
|
979
|
+
const providedKeys = new Set(Object.keys(preprocessedArgs).filter((k) => preprocessedArgs[k] !== undefined));
|
|
980
|
+
const earlyCheck = command.argsSchema['~standard'].validate(preprocessedArgs);
|
|
981
|
+
const checkForProvidedFieldErrors = (result: StandardSchemaV1.Result<unknown>): PluginValidateResult | undefined => {
|
|
982
|
+
if (!result.issues) return undefined;
|
|
983
|
+
// Only keep issues whose path starts with a key the user actually provided
|
|
984
|
+
const providedFieldIssues = result.issues.filter((issue) => {
|
|
985
|
+
const rootKey = issue.path?.[0];
|
|
986
|
+
return rootKey !== undefined && providedKeys.has(String(rootKey));
|
|
987
|
+
});
|
|
988
|
+
if (providedFieldIssues.length > 0) {
|
|
989
|
+
return { args: undefined, argsResult: { issues: providedFieldIssues } as any };
|
|
990
|
+
}
|
|
991
|
+
return undefined;
|
|
992
|
+
};
|
|
993
|
+
const earlyResult = thenMaybe(earlyCheck, (result) => {
|
|
994
|
+
const errors = checkForProvidedFieldErrors(result);
|
|
995
|
+
if (errors) return errors;
|
|
996
|
+
return undefined;
|
|
997
|
+
});
|
|
998
|
+
if (earlyResult instanceof Promise) {
|
|
999
|
+
return earlyResult.then((err) => {
|
|
1000
|
+
if (err) return err;
|
|
1001
|
+
return continueWithPrompt(preprocessedArgs);
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
if (earlyResult) return earlyResult;
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return continueWithPrompt(preprocessedArgs);
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
const continueWithPrompt = (preprocessedArgs: Record<string, unknown>): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
1012
|
+
const willPrompt = !interactivitySuppressed && runtime.prompt && hasInteractiveConfig(command.meta);
|
|
1013
|
+
const afterInteractive = willPrompt
|
|
1014
|
+
? promptInteractiveFields(preprocessedArgs, command, runtime, forceInteractive || undefined)
|
|
1015
|
+
: preprocessedArgs;
|
|
1016
|
+
|
|
1017
|
+
return thenMaybe(afterInteractive, (filledArgs) => {
|
|
1018
|
+
const validated = validateCommandArgs(command, filledArgs);
|
|
1019
|
+
return thenMaybe(validated, (v) => v as PluginValidateResult);
|
|
1020
|
+
});
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// Chain: config → env → stdin → validate
|
|
1024
|
+
const validatedConfig = validateConfig();
|
|
1025
|
+
return thenMaybe(validatedConfig, (cfgData) => {
|
|
1026
|
+
const validatedEnv = validateEnv();
|
|
1027
|
+
return thenMaybe(validatedEnv, (envData) => {
|
|
1028
|
+
const stdinDataOrPromise = readStdin();
|
|
1029
|
+
return thenMaybe(stdinDataOrPromise, (stdinData) => {
|
|
1030
|
+
const hasStdinData = Object.keys(stdinData).length > 0;
|
|
1031
|
+
return finalizeValidation(cfgData, envData, hasStdinData ? stdinData : undefined);
|
|
1032
|
+
});
|
|
1033
|
+
});
|
|
1034
|
+
});
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
const validatedOrPromise = runPluginChain('validate', commandPlugins, validateCtx, coreValidate);
|
|
1038
|
+
|
|
1039
|
+
// ── Phase 3: Execute (or handle validation errors) ──────────────
|
|
1040
|
+
const continueAfterValidate = (v: PluginValidateResult) => {
|
|
1041
|
+
// Handle validation failures
|
|
1042
|
+
if (v.argsResult?.issues) {
|
|
1043
|
+
// Collect known option names for fuzzy suggestion on unknown keys
|
|
1044
|
+
let knownOptions: string[] | undefined;
|
|
1045
|
+
const getKnownOptions = () => {
|
|
1046
|
+
if (knownOptions) return knownOptions;
|
|
1047
|
+
knownOptions = [];
|
|
1048
|
+
if (command.argsSchema) {
|
|
1049
|
+
try {
|
|
1050
|
+
const js = command.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
1051
|
+
if (js.type === 'object' && js.properties) knownOptions = Object.keys(js.properties);
|
|
1052
|
+
} catch {
|
|
1053
|
+
/* ignore */
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return knownOptions;
|
|
1057
|
+
};
|
|
1058
|
+
|
|
1059
|
+
const issueMessages = v.argsResult.issues
|
|
1060
|
+
.map((i: StandardSchemaV1.Issue) => {
|
|
1061
|
+
const base = ` - ${i.path?.join('.') || 'root'}: ${i.message}`;
|
|
1062
|
+
// Try to suggest for unrecognized key errors
|
|
1063
|
+
const issueAny = i as any;
|
|
1064
|
+
const unrecognizedKeys: string[] | undefined =
|
|
1065
|
+
issueAny.keys ?? i.message?.match(/[Uu]nrecognized key(?:s)?[^"]*"([^"]+)"/)?.slice(1);
|
|
1066
|
+
if (unrecognizedKeys?.length) {
|
|
1067
|
+
const hints = unrecognizedKeys.map((k: string) => suggestSimilar(k, getKnownOptions())).filter(Boolean);
|
|
1068
|
+
if (hints.length) return `${base}\n ${hints.join('\n ')}`;
|
|
1069
|
+
}
|
|
1070
|
+
return base;
|
|
1071
|
+
})
|
|
1072
|
+
.join('\n');
|
|
1073
|
+
|
|
1074
|
+
if (errorMode === 'hard') {
|
|
1075
|
+
const helpText = generateHelp(existingCommand, command, { format: runtime.format });
|
|
1076
|
+
runtime.error(`Validation error:\n${issueMessages}`);
|
|
1077
|
+
runtime.error(helpText);
|
|
1078
|
+
throw new ValidationError(`Validation error:\n${issueMessages}`, v.argsResult.issues as any, {
|
|
1079
|
+
suggestions: v.argsResult.issues.flatMap((i: any) => {
|
|
1080
|
+
const keys: string[] | undefined = i.keys ?? i.message?.match(/[Uu]nrecognized key(?:s)?[^"]*"([^"]+)"/)?.slice(1);
|
|
1081
|
+
if (!keys?.length) return [];
|
|
1082
|
+
return keys.map((k: string) => suggestSimilar(k, getKnownOptions())).filter(Boolean);
|
|
1083
|
+
}),
|
|
1084
|
+
command: command.path || command.name,
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
// Soft mode: return result with issues, skip the action
|
|
1089
|
+
return {
|
|
1090
|
+
command: command as any,
|
|
1091
|
+
args: undefined,
|
|
1092
|
+
argsResult: v.argsResult,
|
|
1093
|
+
result: undefined,
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
const executeCtx: PluginExecuteContext = {
|
|
1098
|
+
command,
|
|
1099
|
+
args: v.args,
|
|
1100
|
+
state,
|
|
1101
|
+
};
|
|
1102
|
+
|
|
1103
|
+
const coreExecute = (): PluginExecuteResult => {
|
|
1104
|
+
const handler = command.action ?? noop;
|
|
1105
|
+
const ctx = evalOptions?.runtime ? { ...createActionContext(command), runtime } : createActionContext(command);
|
|
1106
|
+
const result = handler(executeCtx.args as any, ctx);
|
|
1107
|
+
return { result };
|
|
1108
|
+
};
|
|
1109
|
+
|
|
1110
|
+
const executedOrPromise = runPluginChain('execute', commandPlugins, executeCtx, coreExecute);
|
|
1111
|
+
|
|
1112
|
+
return thenMaybe(executedOrPromise, (e) => {
|
|
1113
|
+
const commandResult = {
|
|
1114
|
+
command: command as any,
|
|
1115
|
+
args: v.args,
|
|
1116
|
+
argsResult: v.argsResult,
|
|
1117
|
+
result: e.result,
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
if (command.autoOutput ?? evalOptions?.autoOutput ?? true) {
|
|
1121
|
+
const outputOrPromise = outputValue(e.result, runtime.output);
|
|
1122
|
+
if (outputOrPromise instanceof Promise) {
|
|
1123
|
+
return outputOrPromise.then(() => commandResult);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return commandResult;
|
|
1128
|
+
});
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
return warnIfUnexpectedAsync(thenMaybe(validatedOrPromise, continueAfterValidate), command) as any;
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
return thenMaybe(parsedOrPromise, continueAfterParse) as any;
|
|
517
1135
|
};
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
1136
|
+
|
|
1137
|
+
return wrapWithLifecycle(rootPlugins, existingCommand, state, resolvedInput, runPipeline, (result) => ({
|
|
1138
|
+
command: existingCommand,
|
|
1139
|
+
args: undefined,
|
|
1140
|
+
argsResult: undefined,
|
|
1141
|
+
result,
|
|
1142
|
+
})) as any;
|
|
1143
|
+
};
|
|
1144
|
+
|
|
1145
|
+
const evalCommand: AnyPadroneProgram['eval'] = (input, evalOptions) => {
|
|
1146
|
+
return execCommand(input as string, evalOptions, 'soft');
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* Collects plugins from the command's parent chain (root → ... → target).
|
|
1151
|
+
* Root/program plugins come first (outermost), target command's plugins last (innermost).
|
|
1152
|
+
*
|
|
1153
|
+
* The `programRoot` parameter provides the current program command, because
|
|
1154
|
+
* subcommands' `.parent` references may be stale (builders are immutable — each
|
|
1155
|
+
* method returns a new builder, so a subcommand's parent was captured before
|
|
1156
|
+
* `.use()` was called on the program). We substitute `programRoot` for the
|
|
1157
|
+
* top of the chain to ensure program-level plugins are always included.
|
|
1158
|
+
*/
|
|
1159
|
+
const collectPlugins = (cmd: AnyPadroneCommand): PadronePlugin[] => {
|
|
1160
|
+
const chain: PadronePlugin[][] = [];
|
|
1161
|
+
let current: AnyPadroneCommand | undefined = cmd;
|
|
1162
|
+
while (current) {
|
|
1163
|
+
// If this is the root (no parent), use existingCommand's plugins instead
|
|
1164
|
+
// to pick up plugins added after subcommands were defined.
|
|
1165
|
+
if (!current.parent) {
|
|
1166
|
+
if (existingCommand.plugins?.length) chain.unshift(existingCommand.plugins);
|
|
1167
|
+
} else {
|
|
1168
|
+
if (current.plugins?.length) chain.unshift(current.plugins);
|
|
530
1169
|
}
|
|
1170
|
+
current = current.parent;
|
|
531
1171
|
}
|
|
1172
|
+
return chain.flat();
|
|
1173
|
+
};
|
|
532
1174
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
1175
|
+
// Forward declaration — assigned by the repl method in the return object, used by cli() for --repl.
|
|
1176
|
+
let replFn: (options?: PadroneReplPreferences) => AsyncIterable<any>;
|
|
1177
|
+
const replActiveRef = { value: false };
|
|
1178
|
+
|
|
1179
|
+
const cli: AnyPadroneProgram['cli'] = (cliOptions) => {
|
|
1180
|
+
const runtime = getCommandRuntime(existingCommand);
|
|
1181
|
+
const resolvedInput = (runtime.argv().join(' ') || undefined) as string | undefined;
|
|
1182
|
+
|
|
1183
|
+
// Check for --repl flag before normal execution
|
|
1184
|
+
if (cliOptions?.repl !== false) {
|
|
1185
|
+
const builtin = checkBuiltinCommands(resolvedInput);
|
|
1186
|
+
if (builtin?.type === 'repl') {
|
|
1187
|
+
const replPrefs: PadroneReplPreferences = {
|
|
1188
|
+
...(typeof cliOptions?.repl === 'object' ? cliOptions.repl : {}),
|
|
1189
|
+
scope: builtin.scope,
|
|
1190
|
+
autoOutput: (typeof cliOptions?.repl === 'object' ? cliOptions.repl.autoOutput : undefined) ?? cliOptions?.autoOutput,
|
|
1191
|
+
};
|
|
1192
|
+
const drainRepl = async () => {
|
|
1193
|
+
for await (const _ of replFn(replPrefs)) {
|
|
1194
|
+
// Results are handled by command actions
|
|
1195
|
+
}
|
|
1196
|
+
return { command: existingCommand, args: undefined, result: undefined } as any;
|
|
1197
|
+
};
|
|
1198
|
+
return drainRepl() as any;
|
|
542
1199
|
}
|
|
543
|
-
configData = configValidated.value as unknown as Record<string, unknown>;
|
|
544
1200
|
}
|
|
545
1201
|
|
|
546
|
-
//
|
|
547
|
-
let
|
|
548
|
-
if (
|
|
549
|
-
|
|
550
|
-
const
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
1202
|
+
// Start background update check (non-blocking)
|
|
1203
|
+
let updateCheckPromise: Promise<(() => void) | undefined> | undefined;
|
|
1204
|
+
if (existingCommand.updateCheck) {
|
|
1205
|
+
// Respect --no-update-check flag
|
|
1206
|
+
const hasNoUpdateCheckFlag =
|
|
1207
|
+
resolvedInput &&
|
|
1208
|
+
parseCliInputToParts(resolvedInput).some((p) => p.type === 'named' && p.key.length === 1 && p.key[0] === 'no-update-check');
|
|
1209
|
+
if (!hasNoUpdateCheckFlag) {
|
|
1210
|
+
const currentVersion = getVersion(existingCommand.version);
|
|
1211
|
+
updateCheckPromise = import('./update-check.ts').then(({ createUpdateChecker }) =>
|
|
1212
|
+
createUpdateChecker(existingCommand.name, currentVersion, existingCommand.updateCheck!, runtime),
|
|
1213
|
+
);
|
|
558
1214
|
}
|
|
559
1215
|
}
|
|
560
1216
|
|
|
561
|
-
|
|
562
|
-
const { options, optionsResult } = validateOptions(command, rawOptions, args, {
|
|
563
|
-
envData,
|
|
564
|
-
configData,
|
|
565
|
-
});
|
|
1217
|
+
const result = execCommand(resolvedInput, cliOptions, 'hard');
|
|
566
1218
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1219
|
+
// Show update notification after command output
|
|
1220
|
+
if (updateCheckPromise) {
|
|
1221
|
+
if (result instanceof Promise) {
|
|
1222
|
+
return result.then(async (r) => {
|
|
1223
|
+
const showUpdateNotification = await updateCheckPromise;
|
|
1224
|
+
showUpdateNotification?.();
|
|
1225
|
+
return r;
|
|
1226
|
+
}) as any;
|
|
1227
|
+
}
|
|
1228
|
+
// For sync results, schedule notification for next tick (non-blocking)
|
|
1229
|
+
updateCheckPromise.then((show) => show?.());
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1232
|
+
return result;
|
|
572
1233
|
};
|
|
573
1234
|
|
|
574
|
-
const run: AnyPadroneProgram['run'] = (command,
|
|
1235
|
+
const run: AnyPadroneProgram['run'] = (command, args) => {
|
|
575
1236
|
const commandObj = typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
|
|
576
|
-
if (!commandObj) throw new
|
|
577
|
-
if (!commandObj.
|
|
1237
|
+
if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
|
|
1238
|
+
if (!commandObj.action) throw new RoutingError(`Command "${commandObj.path}" has no action`, { command: commandObj.path });
|
|
578
1239
|
|
|
579
|
-
const
|
|
1240
|
+
const state: Record<string, unknown> = {};
|
|
1241
|
+
const executeCtx: PluginExecuteContext = { command: commandObj, args, state };
|
|
580
1242
|
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
result,
|
|
1243
|
+
const coreExecute = (): PluginExecuteResult => {
|
|
1244
|
+
const result = commandObj.action!(executeCtx.args as any, createActionContext(commandObj));
|
|
1245
|
+
return { result };
|
|
585
1246
|
};
|
|
1247
|
+
|
|
1248
|
+
const commandObjPlugins = collectPlugins(commandObj);
|
|
1249
|
+
const executedOrPromise = runPluginChain('execute', commandObjPlugins, executeCtx, coreExecute);
|
|
1250
|
+
|
|
1251
|
+
const toResult = (e: PluginExecuteResult) => ({
|
|
1252
|
+
command: commandObj as any,
|
|
1253
|
+
args: args as any,
|
|
1254
|
+
result: e.result,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
if (executedOrPromise instanceof Promise) {
|
|
1258
|
+
return executedOrPromise.then(toResult) as any;
|
|
1259
|
+
}
|
|
1260
|
+
return toResult(executedOrPromise);
|
|
586
1261
|
};
|
|
587
1262
|
|
|
588
1263
|
const tool: AnyPadroneProgram['tool'] = () => {
|
|
589
|
-
const helpText = generateHelp(existingCommand, undefined, { format: 'text'
|
|
590
|
-
|
|
591
|
-
const description = `\n
|
|
592
|
-
This is a CLI tool created with Padrone. You can run any of the defined commands described in the help text below. If you need assistance, refer to the documentation or use the help command.
|
|
1264
|
+
const helpText = generateHelp(existingCommand, undefined, { format: 'text' });
|
|
593
1265
|
|
|
594
|
-
<
|
|
595
|
-
${helpText}
|
|
596
|
-
</help_output>
|
|
597
|
-
`;
|
|
1266
|
+
const description = `Run a command. Pass the full command string including arguments. Use "help <command>" for detailed usage.\n\n${helpText}`;
|
|
598
1267
|
|
|
599
1268
|
return {
|
|
600
1269
|
type: 'function',
|
|
@@ -602,7 +1271,7 @@ ${helpText}
|
|
|
602
1271
|
strict: true,
|
|
603
1272
|
title: existingCommand.description,
|
|
604
1273
|
description,
|
|
605
|
-
inputExamples: [{ input: { command: '<command> [
|
|
1274
|
+
inputExamples: [{ input: { command: '<command> [positionals...] [arguments...]' } }],
|
|
606
1275
|
inputSchema: {
|
|
607
1276
|
[Symbol.for('vercel.ai.schema') as keyof Schema & symbol]: true,
|
|
608
1277
|
jsonSchema: {
|
|
@@ -617,67 +1286,155 @@ ${helpText}
|
|
|
617
1286
|
return { success: false, error: new Error('Expected an object with command property as string.') };
|
|
618
1287
|
},
|
|
619
1288
|
} satisfies Schema<{ command: string }> as Schema<{ command: string }>,
|
|
620
|
-
needsApproval: (input) => {
|
|
621
|
-
const
|
|
622
|
-
if (typeof command.needsApproval === 'function') return command.needsApproval(
|
|
623
|
-
return !!command.needsApproval;
|
|
1289
|
+
needsApproval: async (input) => {
|
|
1290
|
+
const parsed = await parse(input.command);
|
|
1291
|
+
if (typeof parsed.command.needsApproval === 'function') return parsed.command.needsApproval(parsed.args);
|
|
1292
|
+
return !!parsed.command.needsApproval;
|
|
624
1293
|
},
|
|
625
|
-
execute: (input) => {
|
|
626
|
-
|
|
1294
|
+
execute: async (input) => {
|
|
1295
|
+
const output: string[] = [];
|
|
1296
|
+
const errors: string[] = [];
|
|
1297
|
+
const result = await evalCommand(input.command, {
|
|
1298
|
+
autoOutput: false,
|
|
1299
|
+
runtime: {
|
|
1300
|
+
output: (...args) => output.push(args.map(String).join(' ')),
|
|
1301
|
+
error: (text) => errors.push(text),
|
|
1302
|
+
interactive: 'unsupported',
|
|
1303
|
+
format: 'text',
|
|
1304
|
+
},
|
|
1305
|
+
});
|
|
1306
|
+
return { result: result.result, logs: output.join('\n'), error: errors.join('\n') };
|
|
627
1307
|
},
|
|
628
1308
|
};
|
|
629
1309
|
};
|
|
630
1310
|
|
|
631
|
-
|
|
1311
|
+
const builder = {
|
|
632
1312
|
configure(config) {
|
|
633
1313
|
return createPadroneBuilder({ ...existingCommand, ...config }) as any;
|
|
634
1314
|
},
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
1315
|
+
runtime(runtimeConfig) {
|
|
1316
|
+
return createPadroneBuilder({ ...existingCommand, runtime: { ...existingCommand.runtime, ...runtimeConfig } }) as any;
|
|
1317
|
+
},
|
|
1318
|
+
async() {
|
|
1319
|
+
return createPadroneBuilder({ ...existingCommand, isAsync: true }) as any;
|
|
1320
|
+
},
|
|
1321
|
+
arguments(schema, meta) {
|
|
1322
|
+
// If schema is a function, call it with parent's arguments as base
|
|
1323
|
+
const resolvedArgs = typeof schema === 'function' ? schema(existingCommand.argsSchema as any) : schema;
|
|
1324
|
+
const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedArgs) || hasInteractiveConfig(meta);
|
|
1325
|
+
return createPadroneBuilder({ ...existingCommand, argsSchema: resolvedArgs, meta, isAsync }) as any;
|
|
639
1326
|
},
|
|
640
1327
|
configFile(file, schema) {
|
|
641
1328
|
const configFiles = file === undefined ? undefined : Array.isArray(file) ? file : [file];
|
|
642
|
-
const resolvedConfig = typeof schema === 'function' ? schema(existingCommand.
|
|
643
|
-
|
|
1329
|
+
const resolvedConfig = typeof schema === 'function' ? schema(existingCommand.argsSchema) : (schema ?? existingCommand.argsSchema);
|
|
1330
|
+
const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedConfig);
|
|
1331
|
+
return createPadroneBuilder({ ...existingCommand, configFiles, configSchema: resolvedConfig as any, isAsync }) as any;
|
|
644
1332
|
},
|
|
645
1333
|
env(schema) {
|
|
646
|
-
const resolvedEnv = typeof schema === 'function' ? schema(existingCommand.
|
|
647
|
-
|
|
1334
|
+
const resolvedEnv = typeof schema === 'function' ? schema(existingCommand.argsSchema) : schema;
|
|
1335
|
+
const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedEnv);
|
|
1336
|
+
return createPadroneBuilder({ ...existingCommand, envSchema: resolvedEnv as any, isAsync }) as any;
|
|
648
1337
|
},
|
|
649
1338
|
action(handler = noop) {
|
|
650
|
-
|
|
1339
|
+
const baseHandler = existingCommand.action ?? noop;
|
|
1340
|
+
return createPadroneBuilder({
|
|
1341
|
+
...existingCommand,
|
|
1342
|
+
action: (args: any, ctx: any) => (handler as any)(args, ctx, baseHandler),
|
|
1343
|
+
}) as any;
|
|
1344
|
+
},
|
|
1345
|
+
wrap(config) {
|
|
1346
|
+
const handler = createWrapHandler(config, existingCommand.argsSchema as any, existingCommand.meta?.positional);
|
|
1347
|
+
return createPadroneBuilder({ ...existingCommand, action: handler }) as any;
|
|
651
1348
|
},
|
|
652
1349
|
command(nameOrNames, builderFn) {
|
|
653
1350
|
// Extract name and aliases from the input
|
|
654
1351
|
const name = Array.isArray(nameOrNames) ? nameOrNames[0] : nameOrNames;
|
|
655
1352
|
const aliases = Array.isArray(nameOrNames) && nameOrNames.length > 1 ? (nameOrNames.slice(1) as string[]) : undefined;
|
|
656
1353
|
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
parent: existingCommand
|
|
662
|
-
|
|
663
|
-
|
|
1354
|
+
// Check if a command with this name already exists (override case)
|
|
1355
|
+
const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
|
|
1356
|
+
|
|
1357
|
+
const initialCommand: AnyPadroneCommand = existingSubcommand
|
|
1358
|
+
? { ...existingSubcommand, aliases: aliases ?? existingSubcommand.aliases, parent: existingCommand }
|
|
1359
|
+
: ({
|
|
1360
|
+
name,
|
|
1361
|
+
path: existingCommand.path ? `${existingCommand.path} ${name}` : name,
|
|
1362
|
+
aliases,
|
|
1363
|
+
parent: existingCommand,
|
|
1364
|
+
'~types': {} as any,
|
|
1365
|
+
} satisfies PadroneCommand);
|
|
1366
|
+
|
|
664
1367
|
const builder = createPadroneBuilder(initialCommand);
|
|
665
1368
|
|
|
666
1369
|
const commandObj =
|
|
667
1370
|
((builderFn?.(builder as any) as unknown as typeof builder)?.[commandSymbol] as AnyPadroneCommand) ?? initialCommand;
|
|
668
|
-
|
|
1371
|
+
|
|
1372
|
+
// Merge subcommands when overriding: existing subcommands that aren't replaced are kept
|
|
1373
|
+
const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, commandObj) : commandObj;
|
|
1374
|
+
|
|
1375
|
+
// Replace existing command or append new one
|
|
1376
|
+
const commands = existingCommand.commands || [];
|
|
1377
|
+
const existingIndex = commands.findIndex((c) => c.name === name);
|
|
1378
|
+
const updatedCommands =
|
|
1379
|
+
existingIndex >= 0
|
|
1380
|
+
? [...commands.slice(0, existingIndex), mergedCommandObj, ...commands.slice(existingIndex + 1)]
|
|
1381
|
+
: [...commands, mergedCommandObj];
|
|
1382
|
+
|
|
1383
|
+
return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
|
|
1384
|
+
},
|
|
1385
|
+
|
|
1386
|
+
mount(nameOrNames, program) {
|
|
1387
|
+
const name = Array.isArray(nameOrNames) ? nameOrNames[0] : nameOrNames;
|
|
1388
|
+
const aliases = Array.isArray(nameOrNames) && nameOrNames.length > 1 ? (nameOrNames.slice(1) as string[]) : undefined;
|
|
1389
|
+
|
|
1390
|
+
// Extract the underlying command from the program
|
|
1391
|
+
const programCommand = (program as any)[commandSymbol] as AnyPadroneCommand | undefined;
|
|
1392
|
+
if (!programCommand) throw new RoutingError('Cannot mount: not a valid Padrone program');
|
|
1393
|
+
|
|
1394
|
+
// Re-path the command tree under the new name
|
|
1395
|
+
const remounted = repathCommandTree(programCommand, name, existingCommand.path || '', existingCommand);
|
|
1396
|
+
remounted.aliases = aliases;
|
|
1397
|
+
|
|
1398
|
+
// Merge with existing command if one with the same name exists
|
|
1399
|
+
const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
|
|
1400
|
+
const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, remounted) : remounted;
|
|
1401
|
+
|
|
1402
|
+
const commands = existingCommand.commands || [];
|
|
1403
|
+
const existingIndex = commands.findIndex((c) => c.name === name);
|
|
1404
|
+
const updatedCommands =
|
|
1405
|
+
existingIndex >= 0
|
|
1406
|
+
? [...commands.slice(0, existingIndex), mergedCommandObj, ...commands.slice(existingIndex + 1)]
|
|
1407
|
+
: [...commands, mergedCommandObj];
|
|
1408
|
+
|
|
1409
|
+
return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
|
|
1410
|
+
},
|
|
1411
|
+
|
|
1412
|
+
use(plugin: PadronePlugin) {
|
|
1413
|
+
return createPadroneBuilder({
|
|
1414
|
+
...existingCommand,
|
|
1415
|
+
plugins: [...(existingCommand.plugins ?? []), plugin],
|
|
1416
|
+
}) as any;
|
|
1417
|
+
},
|
|
1418
|
+
|
|
1419
|
+
updateCheck(config = {}) {
|
|
1420
|
+
return createPadroneBuilder({ ...existingCommand, updateCheck: config }) as any;
|
|
669
1421
|
},
|
|
670
1422
|
|
|
671
1423
|
run,
|
|
672
1424
|
find,
|
|
673
1425
|
parse,
|
|
674
1426
|
stringify,
|
|
1427
|
+
eval: evalCommand,
|
|
675
1428
|
cli,
|
|
676
1429
|
tool,
|
|
677
1430
|
|
|
1431
|
+
repl: (replFn = (options?: PadroneReplPreferences) => {
|
|
1432
|
+
return createReplIterator({ existingCommand, evalCommand, replActiveRef }, options);
|
|
1433
|
+
}),
|
|
1434
|
+
|
|
678
1435
|
api() {
|
|
679
1436
|
function buildApi(command: AnyPadroneCommand) {
|
|
680
|
-
const runCommand = ((
|
|
1437
|
+
const runCommand = ((args) => run(command, args).result) as PadroneAPI<AnyPadroneCommand>;
|
|
681
1438
|
if (!command.commands) return runCommand;
|
|
682
1439
|
for (const cmd of command.commands) runCommand[cmd.name] = buildApi(cmd);
|
|
683
1440
|
return runCommand;
|
|
@@ -686,17 +1443,19 @@ ${helpText}
|
|
|
686
1443
|
return buildApi(existingCommand);
|
|
687
1444
|
},
|
|
688
1445
|
|
|
689
|
-
help(command,
|
|
1446
|
+
help(command, prefs) {
|
|
690
1447
|
const commandObj = !command
|
|
691
1448
|
? existingCommand
|
|
692
1449
|
: typeof command === 'string'
|
|
693
1450
|
? findCommandByName(command, existingCommand.commands)
|
|
694
1451
|
: (command as AnyPadroneCommand);
|
|
695
|
-
if (!commandObj) throw new
|
|
696
|
-
|
|
1452
|
+
if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
|
|
1453
|
+
const runtime = getCommandRuntime(existingCommand);
|
|
1454
|
+
return generateHelp(existingCommand, commandObj, { ...prefs, format: prefs?.format ?? runtime.format });
|
|
697
1455
|
},
|
|
698
1456
|
|
|
699
|
-
completion(shell) {
|
|
1457
|
+
async completion(shell) {
|
|
1458
|
+
const { generateCompletionOutput } = await import('./completion.ts');
|
|
700
1459
|
return generateCompletionOutput(existingCommand, shell as ShellType | undefined);
|
|
701
1460
|
},
|
|
702
1461
|
|
|
@@ -704,4 +1463,5 @@ ${helpText}
|
|
|
704
1463
|
|
|
705
1464
|
[commandSymbol]: existingCommand,
|
|
706
1465
|
} satisfies AnyPadroneProgram & { [commandSymbol]: AnyPadroneCommand } as any;
|
|
1466
|
+
return builder as TBuilder & { [commandSymbol]: AnyPadroneCommand };
|
|
707
1467
|
}
|