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