padrone 1.0.0-beta.2
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/LICENSE +21 -0
- package/README.md +330 -0
- package/index.d.mts +384 -0
- package/index.mjs +1246 -0
- package/index.mjs.map +1 -0
- package/package.json +64 -0
- package/src/colorizer.ts +41 -0
- package/src/create.ts +559 -0
- package/src/formatter.ts +499 -0
- package/src/help.ts +227 -0
- package/src/index.ts +22 -0
- package/src/options.ts +290 -0
- package/src/parse.ts +222 -0
- package/src/type-utils.ts +99 -0
- package/src/types.ts +313 -0
- package/src/utils.ts +131 -0
- package/src/zod.d.ts +5 -0
package/src/create.ts
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import type { Schema } from 'ai';
|
|
2
|
+
import { generateHelp } from './help';
|
|
3
|
+
import { extractSchemaMetadata, parsePositionalConfig, preprocessOptions } from './options';
|
|
4
|
+
import { parseCliInputToParts } from './parse';
|
|
5
|
+
import type { AnyPadroneCommand, AnyPadroneProgram, PadroneAPI, PadroneCommand, PadroneCommandBuilder, PadroneProgram } from './types';
|
|
6
|
+
import { findConfigFile, getVersion, loadConfigFile } from './utils';
|
|
7
|
+
|
|
8
|
+
const commandSymbol = Symbol('padrone_command');
|
|
9
|
+
|
|
10
|
+
const noop = <TRes>() => undefined as TRes;
|
|
11
|
+
|
|
12
|
+
export function createPadroneCommandBuilder<TBuilder extends PadroneProgram = PadroneProgram>(
|
|
13
|
+
existingCommand: AnyPadroneCommand,
|
|
14
|
+
): TBuilder & { [commandSymbol]: AnyPadroneCommand } {
|
|
15
|
+
function findCommandByName(name: string, commands?: AnyPadroneCommand[]): AnyPadroneCommand | undefined {
|
|
16
|
+
if (!commands) return undefined;
|
|
17
|
+
|
|
18
|
+
const foundByName = commands.find((cmd) => cmd.name === name);
|
|
19
|
+
if (foundByName) return foundByName;
|
|
20
|
+
|
|
21
|
+
for (const cmd of commands) {
|
|
22
|
+
if (cmd.commands && name.startsWith(`${cmd.name} `)) {
|
|
23
|
+
const subCommandName = name.slice(cmd.name.length + 1);
|
|
24
|
+
const subCommand = findCommandByName(subCommandName, cmd.commands);
|
|
25
|
+
if (subCommand) return subCommand;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const find: AnyPadroneProgram['find'] = (command) => {
|
|
32
|
+
return findCommandByName(command, existingCommand.commands) as ReturnType<AnyPadroneProgram['find']>;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Parses CLI input to find the command and extract raw options without validation.
|
|
37
|
+
*/
|
|
38
|
+
const parseCommand = (input: string | undefined) => {
|
|
39
|
+
input ??= typeof process !== 'undefined' ? (process.argv.slice(2).join(' ') as any) : undefined;
|
|
40
|
+
if (!input) return { command: existingCommand, rawOptions: {} as Record<string, unknown>, args: [] as string[] };
|
|
41
|
+
|
|
42
|
+
const parts = parseCliInputToParts(input);
|
|
43
|
+
|
|
44
|
+
const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
|
|
45
|
+
const args = parts.filter((p) => p.type === 'arg').map((p) => p.value);
|
|
46
|
+
|
|
47
|
+
let curCommand: AnyPadroneCommand | undefined = existingCommand;
|
|
48
|
+
|
|
49
|
+
// If the first term is the program name, skip it
|
|
50
|
+
if (terms[0] === existingCommand.name) terms.shift();
|
|
51
|
+
|
|
52
|
+
for (let i = 0; i < terms.length; i++) {
|
|
53
|
+
const term = terms[i] || '';
|
|
54
|
+
const found = findCommandByName(term, curCommand.commands);
|
|
55
|
+
|
|
56
|
+
if (found) {
|
|
57
|
+
curCommand = found;
|
|
58
|
+
} else {
|
|
59
|
+
args.unshift(...terms.slice(i));
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!curCommand) return { command: existingCommand, rawOptions: {} as Record<string, unknown>, args };
|
|
65
|
+
|
|
66
|
+
// Extract option metadata from the nested options object in meta
|
|
67
|
+
const optionsMeta = curCommand.meta?.options;
|
|
68
|
+
const schemaMetadata = curCommand.options
|
|
69
|
+
? extractSchemaMetadata(curCommand.options, optionsMeta)
|
|
70
|
+
: { aliases: {}, envBindings: {}, configKeys: {} };
|
|
71
|
+
const { aliases } = schemaMetadata;
|
|
72
|
+
|
|
73
|
+
// Get array options from schema (arrays are always variadic)
|
|
74
|
+
const arrayOptions = new Set<string>();
|
|
75
|
+
if (curCommand.options) {
|
|
76
|
+
try {
|
|
77
|
+
const jsonSchema = curCommand.options['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
78
|
+
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
79
|
+
for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
|
|
80
|
+
if (prop?.type === 'array') arrayOptions.add(key);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Ignore schema parsing errors
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const opts = parts.filter((p) => p.type === 'option' || p.type === 'alias');
|
|
89
|
+
const rawOptions: Record<string, unknown> = {};
|
|
90
|
+
|
|
91
|
+
for (const opt of opts) {
|
|
92
|
+
const key = opt.type === 'alias' ? aliases[opt.key] || opt.key : opt.key;
|
|
93
|
+
|
|
94
|
+
// Handle negated boolean options (--no-verbose)
|
|
95
|
+
if (opt.type === 'option' && opt.negated) {
|
|
96
|
+
rawOptions[key] = false;
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const value = opt.value ?? true;
|
|
101
|
+
|
|
102
|
+
// Handle array options - accumulate values into arrays (arrays are always variadic)
|
|
103
|
+
if (arrayOptions.has(key)) {
|
|
104
|
+
if (key in rawOptions) {
|
|
105
|
+
const existing = rawOptions[key];
|
|
106
|
+
if (Array.isArray(existing)) {
|
|
107
|
+
if (Array.isArray(value)) {
|
|
108
|
+
existing.push(...value);
|
|
109
|
+
} else {
|
|
110
|
+
existing.push(value);
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
if (Array.isArray(value)) {
|
|
114
|
+
rawOptions[key] = [existing, ...value];
|
|
115
|
+
} else {
|
|
116
|
+
rawOptions[key] = [existing, value];
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
} else {
|
|
120
|
+
rawOptions[key] = Array.isArray(value) ? value : [value];
|
|
121
|
+
}
|
|
122
|
+
} else {
|
|
123
|
+
rawOptions[key] = value;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return { command: curCommand, rawOptions, args };
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Validates raw options against the command's schema and applies preprocessing.
|
|
132
|
+
*/
|
|
133
|
+
const validateOptions = (
|
|
134
|
+
command: AnyPadroneCommand,
|
|
135
|
+
rawOptions: Record<string, unknown>,
|
|
136
|
+
args: string[],
|
|
137
|
+
parseOptions?: { env?: Record<string, string | undefined>; configData?: Record<string, unknown> },
|
|
138
|
+
) => {
|
|
139
|
+
// Extract option metadata for preprocessing
|
|
140
|
+
const optionsMeta = command.meta?.options;
|
|
141
|
+
const schemaMetadata = command.options
|
|
142
|
+
? extractSchemaMetadata(command.options, optionsMeta)
|
|
143
|
+
: { aliases: {}, envBindings: {}, configKeys: {} };
|
|
144
|
+
const { envBindings, configKeys } = schemaMetadata;
|
|
145
|
+
|
|
146
|
+
// Apply preprocessing (env and config bindings)
|
|
147
|
+
const preprocessedOptions = preprocessOptions(rawOptions, {
|
|
148
|
+
aliases: {}, // Already resolved aliases in parseCommand
|
|
149
|
+
envBindings,
|
|
150
|
+
configKeys,
|
|
151
|
+
configData: parseOptions?.configData,
|
|
152
|
+
env: parseOptions?.env,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Parse positional configuration
|
|
156
|
+
const positionalConfig = command.meta?.positional ? parsePositionalConfig(command.meta.positional) : [];
|
|
157
|
+
|
|
158
|
+
// Map positional arguments to their named options
|
|
159
|
+
if (positionalConfig.length > 0) {
|
|
160
|
+
let argIndex = 0;
|
|
161
|
+
for (const { name, variadic } of positionalConfig) {
|
|
162
|
+
if (argIndex >= args.length) break;
|
|
163
|
+
|
|
164
|
+
if (variadic) {
|
|
165
|
+
// Collect remaining args (but leave room for non-variadic args after)
|
|
166
|
+
const remainingPositionals = positionalConfig.slice(positionalConfig.indexOf({ name, variadic }) + 1);
|
|
167
|
+
const nonVariadicAfter = remainingPositionals.filter((p) => !p.variadic).length;
|
|
168
|
+
const variadicEnd = args.length - nonVariadicAfter;
|
|
169
|
+
preprocessedOptions[name] = args.slice(argIndex, variadicEnd);
|
|
170
|
+
argIndex = variadicEnd;
|
|
171
|
+
} else {
|
|
172
|
+
preprocessedOptions[name] = args[argIndex];
|
|
173
|
+
argIndex++;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const optionsParsed = command.options ? command.options['~standard'].validate(preprocessedOptions) : { value: preprocessedOptions };
|
|
179
|
+
|
|
180
|
+
if (optionsParsed instanceof Promise) {
|
|
181
|
+
throw new Error('Async validation is not supported. Schema validate() must return a synchronous result.');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Return undefined for options when there's no schema and no meaningful options
|
|
185
|
+
const hasOptions = command.options || Object.keys(preprocessedOptions).length > 0;
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
options: optionsParsed.issues ? undefined : hasOptions ? (optionsParsed.value as any) : undefined,
|
|
189
|
+
optionsResult: optionsParsed as any,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const parse: AnyPadroneProgram['parse'] = (input, parseOptions) => {
|
|
194
|
+
const { command, rawOptions, args } = parseCommand(input);
|
|
195
|
+
const { options, optionsResult } = validateOptions(command, rawOptions, args, parseOptions);
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
command: command as any,
|
|
199
|
+
options,
|
|
200
|
+
optionsResult,
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const stringify: AnyPadroneProgram['stringify'] = (command = '' as any, options) => {
|
|
205
|
+
const commandObj = typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
|
|
206
|
+
if (!commandObj) throw new Error(`Command "${command ?? ''}" not found`);
|
|
207
|
+
|
|
208
|
+
const parts: string[] = [];
|
|
209
|
+
|
|
210
|
+
if (commandObj.path) parts.push(commandObj.path);
|
|
211
|
+
|
|
212
|
+
// Get positional config to determine which options are positional
|
|
213
|
+
const positionalConfig = commandObj.meta?.positional ? parsePositionalConfig(commandObj.meta.positional) : [];
|
|
214
|
+
const positionalNames = new Set(positionalConfig.map((p) => p.name));
|
|
215
|
+
|
|
216
|
+
// Output positional arguments first in order
|
|
217
|
+
if (options && typeof options === 'object') {
|
|
218
|
+
for (const { name, variadic } of positionalConfig) {
|
|
219
|
+
const value = (options as Record<string, unknown>)[name];
|
|
220
|
+
if (value === undefined) continue;
|
|
221
|
+
|
|
222
|
+
if (variadic && Array.isArray(value)) {
|
|
223
|
+
for (const v of value) {
|
|
224
|
+
const vStr = String(v);
|
|
225
|
+
if (vStr.includes(' ')) parts.push(`"${vStr}"`);
|
|
226
|
+
else parts.push(vStr);
|
|
227
|
+
}
|
|
228
|
+
} else {
|
|
229
|
+
const argStr = String(value);
|
|
230
|
+
if (argStr.includes(' ')) parts.push(`"${argStr}"`);
|
|
231
|
+
else parts.push(argStr);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Output remaining options (non-positional)
|
|
236
|
+
for (const [key, value] of Object.entries(options)) {
|
|
237
|
+
if (value === undefined || positionalNames.has(key)) continue;
|
|
238
|
+
|
|
239
|
+
if (typeof value === 'boolean') {
|
|
240
|
+
if (value) parts.push(`--${key}`);
|
|
241
|
+
else parts.push(`--no-${key}`);
|
|
242
|
+
} else if (Array.isArray(value)) {
|
|
243
|
+
// Handle variadic options - output each value separately
|
|
244
|
+
for (const v of value) {
|
|
245
|
+
const vStr = String(v);
|
|
246
|
+
if (vStr.includes(' ')) parts.push(`--${key}="${vStr}"`);
|
|
247
|
+
else parts.push(`--${key}=${vStr}`);
|
|
248
|
+
}
|
|
249
|
+
} else if (typeof value === 'string') {
|
|
250
|
+
if (value.includes(' ')) parts.push(`--${key}="${value}"`);
|
|
251
|
+
else parts.push(`--${key}=${value}`);
|
|
252
|
+
} else {
|
|
253
|
+
parts.push(`--${key}=${value}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return parts.join(' ');
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
type DetailLevel = 'minimal' | 'standard' | 'full';
|
|
262
|
+
type FormatLevel = 'text' | 'ansi' | 'console' | 'markdown' | 'html' | 'json' | 'auto';
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Check if help or version flags/commands are present in the input.
|
|
266
|
+
* Returns the appropriate action to take, or null if normal execution should proceed.
|
|
267
|
+
*/
|
|
268
|
+
const checkBuiltinCommands = (
|
|
269
|
+
input: string | undefined,
|
|
270
|
+
): { type: 'help'; command?: AnyPadroneCommand; detail?: DetailLevel; format?: FormatLevel } | { type: 'version' } | null => {
|
|
271
|
+
if (!input) return null;
|
|
272
|
+
|
|
273
|
+
const parts = parseCliInputToParts(input);
|
|
274
|
+
const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
|
|
275
|
+
const opts = parts.filter((p) => p.type === 'option' || p.type === 'alias');
|
|
276
|
+
|
|
277
|
+
// Check for --help, -h flags (these take precedence over commands)
|
|
278
|
+
const hasHelpFlag = opts.some((p) => (p.type === 'option' && p.key === 'help') || (p.type === 'alias' && p.key === 'h'));
|
|
279
|
+
|
|
280
|
+
// Extract detail level from --detail=<level> or -d <level>
|
|
281
|
+
const getDetailLevel = (): DetailLevel | undefined => {
|
|
282
|
+
for (const opt of opts) {
|
|
283
|
+
if (opt.type === 'option' && opt.key === 'detail' && typeof opt.value === 'string') {
|
|
284
|
+
if (opt.value === 'minimal' || opt.value === 'standard' || opt.value === 'full') {
|
|
285
|
+
return opt.value;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (opt.type === 'alias' && opt.key === 'd' && typeof opt.value === 'string') {
|
|
289
|
+
if (opt.value === 'minimal' || opt.value === 'standard' || opt.value === 'full') {
|
|
290
|
+
return opt.value;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
return undefined;
|
|
295
|
+
};
|
|
296
|
+
const detail = getDetailLevel();
|
|
297
|
+
|
|
298
|
+
// Extract format from --format=<value> or -f <value>
|
|
299
|
+
const getFormat = (): FormatLevel | undefined => {
|
|
300
|
+
const validFormats: FormatLevel[] = ['text', 'ansi', 'console', 'markdown', 'html', 'json', 'auto'];
|
|
301
|
+
for (const opt of opts) {
|
|
302
|
+
if (opt.type === 'option' && opt.key === 'format' && typeof opt.value === 'string') {
|
|
303
|
+
if (validFormats.includes(opt.value as FormatLevel)) {
|
|
304
|
+
return opt.value as FormatLevel;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (opt.type === 'alias' && opt.key === 'f' && typeof opt.value === 'string') {
|
|
308
|
+
if (validFormats.includes(opt.value as FormatLevel)) {
|
|
309
|
+
return opt.value as FormatLevel;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return undefined;
|
|
314
|
+
};
|
|
315
|
+
const format = getFormat();
|
|
316
|
+
|
|
317
|
+
// Check for --version, -v, -V flags
|
|
318
|
+
const hasVersionFlag = opts.some(
|
|
319
|
+
(p) => (p.type === 'option' && p.key === 'version') || (p.type === 'alias' && (p.key === 'v' || p.key === 'V')),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// If the first term is the program name, skip it
|
|
323
|
+
const normalizedTerms = [...terms];
|
|
324
|
+
if (normalizedTerms[0] === existingCommand.name) normalizedTerms.shift();
|
|
325
|
+
|
|
326
|
+
// Check if user has defined 'help' or 'version' commands (they take precedence)
|
|
327
|
+
const userHelpCommand = findCommandByName('help', existingCommand.commands);
|
|
328
|
+
const userVersionCommand = findCommandByName('version', existingCommand.commands);
|
|
329
|
+
|
|
330
|
+
// Check for 'help' command (only if user hasn't defined one)
|
|
331
|
+
if (!userHelpCommand && normalizedTerms[0] === 'help') {
|
|
332
|
+
// help <command> - get help for specific command
|
|
333
|
+
const commandName = normalizedTerms.slice(1).join(' ');
|
|
334
|
+
const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
|
|
335
|
+
return { type: 'help', command: targetCommand, detail, format };
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check for 'version' command (only if user hasn't defined one)
|
|
339
|
+
if (!userVersionCommand && normalizedTerms[0] === 'version') {
|
|
340
|
+
return { type: 'version' };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Handle help flag - find the command being requested
|
|
344
|
+
if (hasHelpFlag) {
|
|
345
|
+
// Filter out help-related terms and flags to find the target command
|
|
346
|
+
const commandTerms = normalizedTerms.filter((t) => t !== 'help');
|
|
347
|
+
const commandName = commandTerms.join(' ');
|
|
348
|
+
const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
|
|
349
|
+
return { type: 'help', command: targetCommand, detail, format };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Handle version flag (only for root command, i.e., no subcommand terms)
|
|
353
|
+
if (hasVersionFlag && normalizedTerms.length === 0) {
|
|
354
|
+
return { type: 'version' };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return null;
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Extract the config file path from --config=<path> or -c <path> flags.
|
|
362
|
+
*/
|
|
363
|
+
const extractConfigPath = (input: string | undefined): string | undefined => {
|
|
364
|
+
if (!input) return undefined;
|
|
365
|
+
|
|
366
|
+
const parts = parseCliInputToParts(input);
|
|
367
|
+
const opts = parts.filter((p) => p.type === 'option' || p.type === 'alias');
|
|
368
|
+
|
|
369
|
+
for (const opt of opts) {
|
|
370
|
+
if (opt.type === 'option' && opt.key === 'config' && typeof opt.value === 'string') {
|
|
371
|
+
return opt.value;
|
|
372
|
+
}
|
|
373
|
+
if (opt.type === 'alias' && opt.key === 'c' && typeof opt.value === 'string') {
|
|
374
|
+
return opt.value;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
return undefined;
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const cli: AnyPadroneProgram['cli'] = (input, cliOptions) => {
|
|
381
|
+
// Resolve input from process.argv if not provided
|
|
382
|
+
const resolvedInput = input ?? (typeof process !== 'undefined' ? (process.argv.slice(2).join(' ') as any) : undefined);
|
|
383
|
+
|
|
384
|
+
// Check for built-in help/version commands and flags
|
|
385
|
+
const builtin = checkBuiltinCommands(resolvedInput);
|
|
386
|
+
|
|
387
|
+
if (builtin) {
|
|
388
|
+
if (builtin.type === 'help') {
|
|
389
|
+
const helpText = generateHelp(existingCommand, builtin.command ?? existingCommand, {
|
|
390
|
+
detail: builtin.detail,
|
|
391
|
+
format: builtin.format,
|
|
392
|
+
});
|
|
393
|
+
console.log(helpText);
|
|
394
|
+
return {
|
|
395
|
+
command: existingCommand,
|
|
396
|
+
args: undefined,
|
|
397
|
+
options: undefined,
|
|
398
|
+
result: helpText,
|
|
399
|
+
} as any;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (builtin.type === 'version') {
|
|
403
|
+
const version = getVersion(existingCommand.version);
|
|
404
|
+
console.log(version);
|
|
405
|
+
return {
|
|
406
|
+
command: existingCommand,
|
|
407
|
+
options: undefined,
|
|
408
|
+
result: version,
|
|
409
|
+
} as any;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Parse the command first (without validating options)
|
|
414
|
+
const { command, rawOptions, args } = parseCommand(resolvedInput);
|
|
415
|
+
|
|
416
|
+
// Extract config file path from --config or -c flag
|
|
417
|
+
const configPath = extractConfigPath(resolvedInput);
|
|
418
|
+
|
|
419
|
+
// Resolve config files: command's own configFiles > inherited from parent/root
|
|
420
|
+
// undefined = inherit, empty array = no config files (explicit opt-out)
|
|
421
|
+
const resolveConfigFiles = (cmd: AnyPadroneCommand): string[] | undefined => {
|
|
422
|
+
if (cmd.configFiles !== undefined) return cmd.configFiles;
|
|
423
|
+
if (cmd.parent) return resolveConfigFiles(cmd.parent);
|
|
424
|
+
return undefined;
|
|
425
|
+
};
|
|
426
|
+
const effectiveConfigFiles = resolveConfigFiles(command);
|
|
427
|
+
|
|
428
|
+
// Determine config data: explicit --config flag > auto-discovered config > provided configData
|
|
429
|
+
let configData = cliOptions?.configData;
|
|
430
|
+
if (configPath) {
|
|
431
|
+
// Explicit config path takes precedence
|
|
432
|
+
configData = loadConfigFile(configPath);
|
|
433
|
+
} else if (effectiveConfigFiles?.length) {
|
|
434
|
+
// Search for config files if configFiles is configured (inherited or own)
|
|
435
|
+
const foundConfigPath = findConfigFile(effectiveConfigFiles);
|
|
436
|
+
if (foundConfigPath) {
|
|
437
|
+
configData = loadConfigFile(foundConfigPath) ?? configData;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Validate options with config data
|
|
442
|
+
const { options, optionsResult } = validateOptions(command, rawOptions, args, {
|
|
443
|
+
...cliOptions,
|
|
444
|
+
configData,
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const res = run(command, options) as any;
|
|
448
|
+
return {
|
|
449
|
+
...res,
|
|
450
|
+
optionsResult,
|
|
451
|
+
};
|
|
452
|
+
};
|
|
453
|
+
|
|
454
|
+
const run: AnyPadroneProgram['run'] = (command, options) => {
|
|
455
|
+
const commandObj = typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
|
|
456
|
+
if (!commandObj) throw new Error(`Command "${command ?? ''}" not found`);
|
|
457
|
+
if (!commandObj.handler) throw new Error(`Command "${commandObj.path}" has no handler`);
|
|
458
|
+
|
|
459
|
+
const result = commandObj.handler(options as any);
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
command: commandObj as any,
|
|
463
|
+
options: options as any,
|
|
464
|
+
result,
|
|
465
|
+
};
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
const tool: AnyPadroneProgram['tool'] = () => {
|
|
469
|
+
return {
|
|
470
|
+
type: 'function',
|
|
471
|
+
name: existingCommand.name,
|
|
472
|
+
description: generateHelp(existingCommand, undefined, { format: 'text', detail: 'full' }),
|
|
473
|
+
strict: true,
|
|
474
|
+
inputExamples: [{ input: { command: '<command> [args...] [options...]' } }],
|
|
475
|
+
inputSchema: {
|
|
476
|
+
[Symbol.for('vercel.ai.schema') as keyof Schema & symbol]: true,
|
|
477
|
+
jsonSchema: {
|
|
478
|
+
type: 'object',
|
|
479
|
+
properties: { command: { type: 'string' } },
|
|
480
|
+
additionalProperties: false,
|
|
481
|
+
},
|
|
482
|
+
_type: undefined as unknown as { command: string },
|
|
483
|
+
validate: (value) => {
|
|
484
|
+
const command = (value as any)?.command;
|
|
485
|
+
if (typeof command === 'string') return { success: true, value: { command } };
|
|
486
|
+
return { success: false, error: new Error('Expected an object with command property as string.') };
|
|
487
|
+
},
|
|
488
|
+
} satisfies Schema<{ command: string }> as Schema<{ command: string }>,
|
|
489
|
+
title: existingCommand.description,
|
|
490
|
+
needsApproval: (input) => {
|
|
491
|
+
const { command, options } = parse(input.command);
|
|
492
|
+
if (typeof command.needsApproval === 'function') return command.needsApproval(options);
|
|
493
|
+
return !!command.needsApproval;
|
|
494
|
+
},
|
|
495
|
+
execute: (input) => {
|
|
496
|
+
return cli(input.command).result;
|
|
497
|
+
},
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
return {
|
|
502
|
+
configure(config) {
|
|
503
|
+
return createPadroneCommandBuilder({ ...existingCommand, ...config }) as any;
|
|
504
|
+
},
|
|
505
|
+
options(options, meta) {
|
|
506
|
+
return createPadroneCommandBuilder({ ...existingCommand, options, meta }) as any;
|
|
507
|
+
},
|
|
508
|
+
action(handler = noop) {
|
|
509
|
+
return createPadroneCommandBuilder({ ...existingCommand, handler }) as any;
|
|
510
|
+
},
|
|
511
|
+
command: <TName extends string, TCommand extends PadroneCommand<TName, string, any, any, any>>(
|
|
512
|
+
name: TName,
|
|
513
|
+
builderFn?: (builder: PadroneCommandBuilder<TName>) => PadroneCommandBuilder,
|
|
514
|
+
) => {
|
|
515
|
+
const initialCommand = {
|
|
516
|
+
name,
|
|
517
|
+
path: existingCommand.path ? `${existingCommand.path} ${name}` : name,
|
|
518
|
+
parent: existingCommand,
|
|
519
|
+
'~types': {} as any,
|
|
520
|
+
} satisfies PadroneCommand<TName, any>;
|
|
521
|
+
const builder = createPadroneCommandBuilder(initialCommand);
|
|
522
|
+
|
|
523
|
+
const commandObj = ((builderFn?.(builder as any) as typeof builder)?.[commandSymbol] as TCommand) ?? initialCommand;
|
|
524
|
+
return createPadroneCommandBuilder({ ...existingCommand, commands: [...(existingCommand.commands || []), commandObj] }) as any;
|
|
525
|
+
},
|
|
526
|
+
|
|
527
|
+
run,
|
|
528
|
+
find,
|
|
529
|
+
parse,
|
|
530
|
+
stringify,
|
|
531
|
+
cli,
|
|
532
|
+
tool,
|
|
533
|
+
|
|
534
|
+
api() {
|
|
535
|
+
function buildApi(command: AnyPadroneCommand) {
|
|
536
|
+
const runCommand = ((options) => run(command, options).result) as PadroneAPI<AnyPadroneCommand>;
|
|
537
|
+
if (!command.commands) return runCommand;
|
|
538
|
+
for (const cmd of command.commands) runCommand[cmd.name] = buildApi(cmd);
|
|
539
|
+
return runCommand;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
return buildApi(existingCommand);
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
help(command, options) {
|
|
546
|
+
const commandObj = !command
|
|
547
|
+
? existingCommand
|
|
548
|
+
: typeof command === 'string'
|
|
549
|
+
? findCommandByName(command, existingCommand.commands)
|
|
550
|
+
: (command as AnyPadroneCommand);
|
|
551
|
+
if (!commandObj) throw new Error(`Command "${command ?? ''}" not found`);
|
|
552
|
+
return generateHelp(existingCommand, commandObj, options);
|
|
553
|
+
},
|
|
554
|
+
|
|
555
|
+
'~types': {} as any,
|
|
556
|
+
|
|
557
|
+
[commandSymbol]: existingCommand,
|
|
558
|
+
} satisfies AnyPadroneProgram & { [commandSymbol]: AnyPadroneCommand } as unknown as TBuilder & { [commandSymbol]: AnyPadroneCommand };
|
|
559
|
+
}
|