padrone 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +44 -0
- package/README.md +15 -11
- package/dist/{args-D5PNDyNu.mjs → args-Cnq0nwSM.mjs} +91 -41
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.mjs +4 -4
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +12 -82
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +4 -4
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +10 -12
- package/dist/docs/index.mjs.map +1 -1
- package/dist/{errors-BiVrBgi6.mjs → errors-DA4KzK1M.mjs} +26 -3
- package/dist/errors-DA4KzK1M.mjs.map +1 -0
- package/dist/{formatter-DtHzbP22.d.mts → formatter-DrvhDMrq.d.mts} +3 -3
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/{help-bbmu9-qd.mjs → help-BtxLgrF_.mjs} +190 -43
- package/dist/help-BtxLgrF_.mjs.map +1 -0
- package/dist/{types-Ch8Mk6Qb.d.mts → index-D6-7dz0l.d.mts} +634 -745
- package/dist/index-D6-7dz0l.d.mts.map +1 -0
- package/dist/index.d.mts +869 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3884 -1699
- package/dist/index.mjs.map +1 -1
- package/dist/{mcp-mLWIdUIu.mjs → mcp-6-Jw4Bpq.mjs} +13 -15
- package/dist/mcp-6-Jw4Bpq.mjs.map +1 -0
- package/dist/{serve-B0u43DK7.mjs → serve-YVTPzBCl.mjs} +12 -14
- package/dist/serve-YVTPzBCl.mjs.map +1 -0
- package/dist/{stream-BcC146Ud.mjs → stream-DC4H8YTx.mjs} +24 -3
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +2 -13
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-CFX1FV3v.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +2 -2
- package/dist/zod.d.mts.map +1 -1
- package/dist/zod.mjs +2 -2
- package/dist/zod.mjs.map +1 -1
- package/package.json +15 -12
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -10
- package/src/cli/doctor.ts +22 -18
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +10 -7
- package/src/cli/link.ts +20 -16
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/schema-to-code.ts +2 -2
- package/src/{args.ts → core/args.ts} +32 -225
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +301 -0
- package/src/core/default-runtime.ts +239 -0
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +12 -13
- package/src/extension/auto-output.ts +146 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +44 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +262 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +12 -12
- package/src/{interactive.ts → feature/interactive.ts} +4 -4
- package/src/{mcp.ts → feature/mcp.ts} +12 -15
- package/src/{repl-loop.ts → feature/repl-loop.ts} +10 -13
- package/src/{serve.ts → feature/serve.ts} +11 -15
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +10 -8
- package/src/index.ts +115 -30
- package/src/{formatter.ts → output/formatter.ts} +124 -176
- package/src/{help.ts → output/help.ts} +22 -8
- package/src/output/output-indicator.ts +87 -0
- package/src/output/primitives.ts +335 -0
- package/src/output/styling.ts +221 -0
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -276
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +718 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +60 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/{stream.ts → util/stream.ts} +27 -1
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +71 -33
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -50
- package/dist/args-D5PNDyNu.mjs.map +0 -1
- package/dist/chunk-CjcI7cDX.mjs +0 -15
- package/dist/command-utils-B1D-HqCd.mjs +0 -1117
- package/dist/command-utils-B1D-HqCd.mjs.map +0 -1
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/errors-BiVrBgi6.mjs.map +0 -1
- package/dist/formatter-DtHzbP22.d.mts.map +0 -1
- package/dist/help-bbmu9-qd.mjs.map +0 -1
- package/dist/mcp-mLWIdUIu.mjs.map +0 -1
- package/dist/serve-B0u43DK7.mjs.map +0 -1
- package/dist/stream-BcC146Ud.mjs.map +0 -1
- package/dist/types-Ch8Mk6Qb.d.mts.map +0 -1
- package/dist/update-check-CFX1FV3v.mjs.map +0 -1
- package/src/command-utils.ts +0 -882
- package/src/create.ts +0 -1829
- package/src/runtime.ts +0 -497
- package/src/types.ts +0 -1291
- package/src/utils.ts +0 -140
- /package/src/{colorizer.ts → output/colorizer.ts} +0 -0
package/src/create.ts
DELETED
|
@@ -1,1829 +0,0 @@
|
|
|
1
|
-
import type { StandardSchemaV1 } from '@standard-schema/spec';
|
|
2
|
-
import type { Schema } from 'ai';
|
|
3
|
-
import {
|
|
4
|
-
coerceArgs,
|
|
5
|
-
detectUnknownArgs,
|
|
6
|
-
extractSchemaMetadata,
|
|
7
|
-
isArrayField,
|
|
8
|
-
isAsyncStreamField,
|
|
9
|
-
JSON_SCHEMA_OPTS,
|
|
10
|
-
parsePositionalConfig,
|
|
11
|
-
parseStdinConfig,
|
|
12
|
-
preprocessArgs,
|
|
13
|
-
} from './args.ts';
|
|
14
|
-
import { type ColorConfig, type ColorTheme, colorThemes } from './colorizer.ts';
|
|
15
|
-
import {
|
|
16
|
-
commandSymbol,
|
|
17
|
-
createLazyIndicator,
|
|
18
|
-
createProgress,
|
|
19
|
-
errorResult,
|
|
20
|
-
findCommandByName,
|
|
21
|
-
getCommandRuntime,
|
|
22
|
-
hasInteractiveConfig,
|
|
23
|
-
isAsyncBranded,
|
|
24
|
-
lazyResolver,
|
|
25
|
-
makeThenable,
|
|
26
|
-
mergeCommands,
|
|
27
|
-
noop,
|
|
28
|
-
noopIndicator,
|
|
29
|
-
outputValue,
|
|
30
|
-
repathCommandTree,
|
|
31
|
-
resolveAllCommands,
|
|
32
|
-
resolveCommand,
|
|
33
|
-
resolveProgressMessage,
|
|
34
|
-
runPluginChain,
|
|
35
|
-
suggestSimilar,
|
|
36
|
-
thenMaybe,
|
|
37
|
-
warnIfUnexpectedAsync,
|
|
38
|
-
withDrain,
|
|
39
|
-
withPromiseDrain,
|
|
40
|
-
wrapWithLifecycle,
|
|
41
|
-
} from './command-utils.ts';
|
|
42
|
-
import type { ShellType } from './completion.ts';
|
|
43
|
-
import { ConfigError, RoutingError, ValidationError } from './errors.ts';
|
|
44
|
-
import { generateHelp } from './help.ts';
|
|
45
|
-
import { promptInteractiveFields } from './interactive.ts';
|
|
46
|
-
import { getNestedValue, parseCliInputToParts, setNestedValue } from './parse.ts';
|
|
47
|
-
import { createReplIterator } from './repl-loop.ts';
|
|
48
|
-
import { type PadroneProgressIndicator, resolveStdin, resolveStdinAlways } from './runtime.ts';
|
|
49
|
-
import { createStdinStream } from './stream.ts';
|
|
50
|
-
import type {
|
|
51
|
-
AnyPadroneCommand,
|
|
52
|
-
AnyPadroneProgram,
|
|
53
|
-
PadroneActionContext,
|
|
54
|
-
PadroneAPI,
|
|
55
|
-
PadroneCommand,
|
|
56
|
-
PadroneEvalPreferences,
|
|
57
|
-
PadronePlugin,
|
|
58
|
-
PadroneProgram,
|
|
59
|
-
PadroneReplPreferences,
|
|
60
|
-
PluginExecuteContext,
|
|
61
|
-
PluginExecuteResult,
|
|
62
|
-
PluginParseContext,
|
|
63
|
-
PluginParseResult,
|
|
64
|
-
PluginValidateContext,
|
|
65
|
-
PluginValidateResult,
|
|
66
|
-
} from './types.ts';
|
|
67
|
-
import { getVersion } from './utils.ts';
|
|
68
|
-
import { createWrapHandler } from './wrap.ts';
|
|
69
|
-
|
|
70
|
-
export { asyncSchema, buildReplCompleter } from './command-utils.ts';
|
|
71
|
-
|
|
72
|
-
export function createPadrone<TProgramName extends string>(name: TProgramName): PadroneProgram<TProgramName, '', ''> {
|
|
73
|
-
return createPadroneBuilder({ name, path: '', commands: [] } as any) as unknown as PadroneProgram<TProgramName, '', ''>;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
export function createPadroneBuilder<TBuilder extends PadroneProgram = PadroneProgram>(
|
|
77
|
-
inputCommand: AnyPadroneCommand,
|
|
78
|
-
): TBuilder & { [commandSymbol]: AnyPadroneCommand } {
|
|
79
|
-
// Re-parent direct subcommands so getCommandRuntime walks to the current root,
|
|
80
|
-
// not a stale parent from before .runtime()/.configure()/etc.
|
|
81
|
-
const existingCommand =
|
|
82
|
-
inputCommand.commands?.length && inputCommand.commands.some((c) => c.parent && c.parent !== inputCommand)
|
|
83
|
-
? {
|
|
84
|
-
...inputCommand,
|
|
85
|
-
commands: inputCommand.commands.map((c) => (c.parent && c.parent !== inputCommand ? { ...c, parent: inputCommand } : c)),
|
|
86
|
-
}
|
|
87
|
-
: inputCommand;
|
|
88
|
-
|
|
89
|
-
/** Creates the action context passed to command handlers. References `builder` which is defined later but only called at runtime. */
|
|
90
|
-
const createActionContext = (cmd: AnyPadroneCommand): PadroneActionContext => {
|
|
91
|
-
return {
|
|
92
|
-
runtime: getCommandRuntime(cmd),
|
|
93
|
-
command: cmd,
|
|
94
|
-
program: builder as any,
|
|
95
|
-
progress: noopIndicator,
|
|
96
|
-
};
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const find: AnyPadroneProgram['find'] = (command) => {
|
|
100
|
-
if (typeof command !== 'string') return findCommandByName(command.path, existingCommand.commands) as any;
|
|
101
|
-
return findCommandByName(command, existingCommand.commands) as any;
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Parses CLI input to find the command and extract raw arguments without validation.
|
|
106
|
-
*/
|
|
107
|
-
const parseCommand = (input: string | undefined) => {
|
|
108
|
-
input ??= getCommandRuntime(existingCommand).argv().join(' ') || undefined;
|
|
109
|
-
if (!input) {
|
|
110
|
-
// No input: check for default '' command
|
|
111
|
-
const defaultCommand = findCommandByName('', existingCommand.commands);
|
|
112
|
-
if (defaultCommand) {
|
|
113
|
-
return { command: defaultCommand, rawArgs: {} as Record<string, unknown>, args: [] as string[], unmatchedTerms: [] as string[] };
|
|
114
|
-
}
|
|
115
|
-
return { command: existingCommand, rawArgs: {} as Record<string, unknown>, args: [] as string[], unmatchedTerms: [] as string[] };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
const parts = parseCliInputToParts(input);
|
|
119
|
-
|
|
120
|
-
const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
|
|
121
|
-
const args = parts.filter((p) => p.type === 'arg').map((p) => p.value);
|
|
122
|
-
|
|
123
|
-
let curCommand: AnyPadroneCommand | undefined = existingCommand;
|
|
124
|
-
let unmatchedTerms: string[] = [];
|
|
125
|
-
|
|
126
|
-
// If the first term is the program name, skip it
|
|
127
|
-
if (terms[0] === existingCommand.name) terms.shift();
|
|
128
|
-
|
|
129
|
-
for (let i = 0; i < terms.length; i++) {
|
|
130
|
-
const term = terms[i] || '';
|
|
131
|
-
const found = findCommandByName(term, curCommand.commands);
|
|
132
|
-
|
|
133
|
-
if (found) {
|
|
134
|
-
curCommand = found;
|
|
135
|
-
} else {
|
|
136
|
-
unmatchedTerms = terms.slice(i);
|
|
137
|
-
args.unshift(...unmatchedTerms);
|
|
138
|
-
break;
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// If no unmatched terms remain, check for a default '' subcommand.
|
|
143
|
-
// This handles both the root level (no input) and nested commands (e.g., "advanced" with a '' subcommand).
|
|
144
|
-
if (unmatchedTerms.length === 0 && curCommand.commands?.length) {
|
|
145
|
-
const defaultCommand = findCommandByName('', curCommand.commands);
|
|
146
|
-
if (defaultCommand) {
|
|
147
|
-
curCommand = defaultCommand;
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
if (!curCommand) return { command: existingCommand, rawArgs: {} as Record<string, unknown>, args, unmatchedTerms };
|
|
152
|
-
|
|
153
|
-
// Extract argument metadata from the nested arguments object in meta
|
|
154
|
-
const argsMeta = curCommand.meta?.fields;
|
|
155
|
-
const schemaMetadata = curCommand.argsSchema
|
|
156
|
-
? extractSchemaMetadata(curCommand.argsSchema, argsMeta, curCommand.meta?.autoAlias)
|
|
157
|
-
: { flags: {}, aliases: {} };
|
|
158
|
-
const { flags, aliases } = schemaMetadata;
|
|
159
|
-
|
|
160
|
-
// Get array arguments from schema (arrays are always variadic)
|
|
161
|
-
const arrayArguments = new Set<string>();
|
|
162
|
-
if (curCommand.argsSchema) {
|
|
163
|
-
try {
|
|
164
|
-
const jsonSchema = curCommand.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
|
|
165
|
-
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
166
|
-
for (const [key, prop] of Object.entries(jsonSchema.properties as Record<string, any>)) {
|
|
167
|
-
if (prop?.type === 'array') arrayArguments.add(key);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
} catch {
|
|
171
|
-
// Ignore schema parsing errors
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
const argParts = parts.filter((p) => p.type === 'named' || p.type === 'alias');
|
|
176
|
-
const rawArgs: Record<string, unknown> = {};
|
|
177
|
-
|
|
178
|
-
for (const arg of argParts) {
|
|
179
|
-
// Resolve flags (single-char, from alias parts: -v) and aliases (multi-char, from named parts: --dry-run)
|
|
180
|
-
let key: string[];
|
|
181
|
-
if (arg.type === 'alias' && arg.key.length === 1 && flags[arg.key[0]!]) {
|
|
182
|
-
key = [flags[arg.key[0]!]!];
|
|
183
|
-
} else if (arg.type === 'named' && arg.key.length === 1 && aliases[arg.key[0]!]) {
|
|
184
|
-
key = [aliases[arg.key[0]!]!];
|
|
185
|
-
} else {
|
|
186
|
-
key = arg.key;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
const rootKey = key[0]!;
|
|
190
|
-
|
|
191
|
-
// Handle negated boolean arguments (--no-verbose)
|
|
192
|
-
if (arg.type === 'named' && arg.negated) {
|
|
193
|
-
setNestedValue(rawArgs, key, false);
|
|
194
|
-
continue;
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const value = arg.value ?? true;
|
|
198
|
-
|
|
199
|
-
// Handle array arguments - accumulate values into arrays (arrays are always variadic)
|
|
200
|
-
if (arrayArguments.has(rootKey)) {
|
|
201
|
-
const existing = getNestedValue(rawArgs, key);
|
|
202
|
-
if (existing !== undefined) {
|
|
203
|
-
if (Array.isArray(existing)) {
|
|
204
|
-
if (Array.isArray(value)) {
|
|
205
|
-
existing.push(...value);
|
|
206
|
-
} else {
|
|
207
|
-
existing.push(value);
|
|
208
|
-
}
|
|
209
|
-
} else {
|
|
210
|
-
if (Array.isArray(value)) {
|
|
211
|
-
setNestedValue(rawArgs, key, [existing, ...value]);
|
|
212
|
-
} else {
|
|
213
|
-
setNestedValue(rawArgs, key, [existing, value]);
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
} else {
|
|
217
|
-
setNestedValue(rawArgs, key, Array.isArray(value) ? value : [value]);
|
|
218
|
-
}
|
|
219
|
-
} else {
|
|
220
|
-
setNestedValue(rawArgs, key, value);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return { command: curCommand, rawArgs, args, unmatchedTerms };
|
|
225
|
-
};
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* Preprocesses raw arguments: applies env/config values and maps positional arguments.
|
|
229
|
-
* Also performs auto-coercion (string→number/boolean) and unknown arg detection.
|
|
230
|
-
*/
|
|
231
|
-
const buildCommandArgs = (
|
|
232
|
-
command: AnyPadroneCommand,
|
|
233
|
-
rawArgs: Record<string, unknown>,
|
|
234
|
-
args: string[],
|
|
235
|
-
context?: { stdinData?: Record<string, unknown>; envData?: Record<string, unknown>; configData?: Record<string, unknown> },
|
|
236
|
-
): Record<string, unknown> => {
|
|
237
|
-
// Apply preprocessing (stdin, env, and config bindings)
|
|
238
|
-
let preprocessedArgs = preprocessArgs(rawArgs, {
|
|
239
|
-
flags: {}, // Already resolved in parseCommand
|
|
240
|
-
aliases: {}, // Already resolved in parseCommand
|
|
241
|
-
stdinData: context?.stdinData,
|
|
242
|
-
envData: context?.envData,
|
|
243
|
-
configData: context?.configData,
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
// Parse positional configuration
|
|
247
|
-
const positionalConfig = command.meta?.positional ? parsePositionalConfig(command.meta.positional) : [];
|
|
248
|
-
|
|
249
|
-
// Map positional arguments to their named arguments
|
|
250
|
-
if (positionalConfig.length > 0) {
|
|
251
|
-
let argIndex = 0;
|
|
252
|
-
for (let i = 0; i < positionalConfig.length; i++) {
|
|
253
|
-
const { name, variadic } = positionalConfig[i]!;
|
|
254
|
-
if (argIndex >= args.length) break;
|
|
255
|
-
|
|
256
|
-
if (variadic) {
|
|
257
|
-
// Collect remaining args (but leave room for non-variadic args after)
|
|
258
|
-
const remainingPositionals = positionalConfig.slice(i + 1);
|
|
259
|
-
const nonVariadicAfter = remainingPositionals.filter((p) => !p.variadic).length;
|
|
260
|
-
const variadicEnd = args.length - nonVariadicAfter;
|
|
261
|
-
preprocessedArgs[name] = args.slice(argIndex, variadicEnd);
|
|
262
|
-
argIndex = variadicEnd;
|
|
263
|
-
} else if (i === positionalConfig.length - 1 && args.length > argIndex + 1) {
|
|
264
|
-
// Last non-variadic positional: join all remaining tokens (e.g. `-- Hello world` → "Hello world")
|
|
265
|
-
preprocessedArgs[name] = args.slice(argIndex).join(' ');
|
|
266
|
-
argIndex = args.length;
|
|
267
|
-
} else {
|
|
268
|
-
preprocessedArgs[name] = args[argIndex];
|
|
269
|
-
argIndex++;
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Auto-coerce CLI string values to match schema types (string→number, string→boolean)
|
|
275
|
-
if (command.argsSchema) {
|
|
276
|
-
preprocessedArgs = coerceArgs(preprocessedArgs, command.argsSchema);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return preprocessedArgs;
|
|
280
|
-
};
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Detects unknown options in args that aren't defined in the schema.
|
|
284
|
-
* Returns unknown key info with suggestions, or empty array if schema is loose.
|
|
285
|
-
*/
|
|
286
|
-
const checkUnknownArgs = (
|
|
287
|
-
command: AnyPadroneCommand,
|
|
288
|
-
preprocessedArgs: Record<string, unknown>,
|
|
289
|
-
): { key: string; suggestion: string }[] => {
|
|
290
|
-
if (!command.argsSchema) return [];
|
|
291
|
-
|
|
292
|
-
const argsMeta = command.meta?.fields;
|
|
293
|
-
const { flags, aliases } = extractSchemaMetadata(command.argsSchema, argsMeta, command.meta?.autoAlias);
|
|
294
|
-
|
|
295
|
-
return detectUnknownArgs(preprocessedArgs, command.argsSchema, flags, aliases, suggestSimilar);
|
|
296
|
-
};
|
|
297
|
-
|
|
298
|
-
/**
|
|
299
|
-
* Validates preprocessed arguments against the command's schema.
|
|
300
|
-
* First checks for unknown args (strict by default), then runs schema validation.
|
|
301
|
-
* Returns sync or async result depending on the schema's validate method.
|
|
302
|
-
*/
|
|
303
|
-
const validateCommandArgs = (command: AnyPadroneCommand, preprocessedArgs: Record<string, unknown>) => {
|
|
304
|
-
// Check for unknown args before schema validation (strict by default)
|
|
305
|
-
const unknownArgs = checkUnknownArgs(command, preprocessedArgs);
|
|
306
|
-
if (unknownArgs.length > 0) {
|
|
307
|
-
const issues: StandardSchemaV1.Issue[] = unknownArgs.map(({ key, suggestion }) => ({
|
|
308
|
-
path: [key],
|
|
309
|
-
message: suggestion ? `Unknown option: "${key}". ${suggestion}` : `Unknown option: "${key}"`,
|
|
310
|
-
}));
|
|
311
|
-
return { args: undefined, argsResult: { issues } as any };
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
const argsParsed = command.argsSchema ? command.argsSchema['~standard'].validate(preprocessedArgs) : { value: preprocessedArgs };
|
|
315
|
-
|
|
316
|
-
// Return undefined for args when there's no schema and no meaningful args
|
|
317
|
-
const hasArgs = command.argsSchema || Object.keys(preprocessedArgs).length > 0;
|
|
318
|
-
|
|
319
|
-
const buildResult = (parsed: StandardSchemaV1.Result<unknown>) => ({
|
|
320
|
-
args: parsed.issues ? undefined : hasArgs ? (parsed.value as any) : undefined,
|
|
321
|
-
argsResult: parsed as any,
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
return thenMaybe(argsParsed, buildResult);
|
|
325
|
-
};
|
|
326
|
-
|
|
327
|
-
/**
|
|
328
|
-
* Preprocesses and validates raw arguments against the command's schema.
|
|
329
|
-
* Returns sync or async result depending on the schema's validate method.
|
|
330
|
-
*/
|
|
331
|
-
const validateArgs = (
|
|
332
|
-
command: AnyPadroneCommand,
|
|
333
|
-
rawArgs: Record<string, unknown>,
|
|
334
|
-
args: string[],
|
|
335
|
-
context?: { stdinData?: Record<string, unknown>; envData?: Record<string, unknown>; configData?: Record<string, unknown> },
|
|
336
|
-
) => {
|
|
337
|
-
const preprocessedArgs = buildCommandArgs(command, rawArgs, args, context);
|
|
338
|
-
return validateCommandArgs(command, preprocessedArgs);
|
|
339
|
-
};
|
|
340
|
-
|
|
341
|
-
const parse: AnyPadroneProgram['parse'] = (input) => {
|
|
342
|
-
const state: Record<string, unknown> = {};
|
|
343
|
-
|
|
344
|
-
// Parse phase (with plugins)
|
|
345
|
-
const parseCtx: PluginParseContext = { input: input as string | undefined, command: existingCommand, state };
|
|
346
|
-
const coreParse = (): PluginParseResult => {
|
|
347
|
-
const { command, rawArgs, args } = parseCommand(parseCtx.input);
|
|
348
|
-
return { command, rawArgs, positionalArgs: args };
|
|
349
|
-
};
|
|
350
|
-
|
|
351
|
-
// Parse phase: root plugins only
|
|
352
|
-
const rootPlugins = existingCommand.plugins ?? [];
|
|
353
|
-
const parsedOrPromise = runPluginChain('parse', rootPlugins, parseCtx, coreParse);
|
|
354
|
-
|
|
355
|
-
const continueAfterParse = (parsed: PluginParseResult) => {
|
|
356
|
-
const { command } = parsed;
|
|
357
|
-
|
|
358
|
-
// Validate phase: collected from parent chain
|
|
359
|
-
const commandPlugins = collectPlugins(command);
|
|
360
|
-
const validateCtx: PluginValidateContext = {
|
|
361
|
-
command,
|
|
362
|
-
rawArgs: parsed.rawArgs,
|
|
363
|
-
positionalArgs: parsed.positionalArgs,
|
|
364
|
-
state,
|
|
365
|
-
};
|
|
366
|
-
|
|
367
|
-
const coreValidate = (): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
368
|
-
// Resolve env schema: command's own envSchema > inherited from parent/root
|
|
369
|
-
const resolveEnvSchema = (cmd: AnyPadroneCommand): AnyPadroneCommand['envSchema'] => {
|
|
370
|
-
if (cmd.envSchema !== undefined) return cmd.envSchema;
|
|
371
|
-
if (cmd.parent) return resolveEnvSchema(cmd.parent);
|
|
372
|
-
return undefined;
|
|
373
|
-
};
|
|
374
|
-
const envSchema = resolveEnvSchema(command);
|
|
375
|
-
|
|
376
|
-
const readStdinForParse = (): Record<string, unknown> | Promise<Record<string, unknown>> => {
|
|
377
|
-
const stdinConfig = command.meta?.stdin;
|
|
378
|
-
if (!stdinConfig) return {};
|
|
379
|
-
|
|
380
|
-
const field = parseStdinConfig(stdinConfig);
|
|
381
|
-
|
|
382
|
-
// Skip if the field was already provided via CLI flags
|
|
383
|
-
if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
|
|
384
|
-
|
|
385
|
-
const runtime = getCommandRuntime(existingCommand);
|
|
386
|
-
|
|
387
|
-
const streamInfo = isAsyncStreamField(command.argsSchema, field);
|
|
388
|
-
if (streamInfo) {
|
|
389
|
-
// Async stream: always resolve stdin (even on TTY) for interactive use
|
|
390
|
-
const stdinForStream = resolveStdinAlways(runtime as any);
|
|
391
|
-
return { [field]: createStdinStream(stdinForStream, streamInfo.itemSchema) };
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const stdin = resolveStdin(runtime as any);
|
|
395
|
-
if (!stdin) return {};
|
|
396
|
-
|
|
397
|
-
if (isArrayField(command.argsSchema, field)) {
|
|
398
|
-
return (async () => {
|
|
399
|
-
const lines: string[] = [];
|
|
400
|
-
for await (const line of stdin.lines()) {
|
|
401
|
-
lines.push(line);
|
|
402
|
-
}
|
|
403
|
-
return { [field]: lines };
|
|
404
|
-
})();
|
|
405
|
-
}
|
|
406
|
-
return stdin.text().then((text) => (text ? { [field]: text } : {}));
|
|
407
|
-
};
|
|
408
|
-
|
|
409
|
-
const finalize = (
|
|
410
|
-
envData: Record<string, unknown> | undefined,
|
|
411
|
-
stdinData: Record<string, unknown> | undefined,
|
|
412
|
-
): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
413
|
-
const validated = validateArgs(command, validateCtx.rawArgs, validateCtx.positionalArgs, { stdinData, envData });
|
|
414
|
-
return thenMaybe(validated, (v) => v as PluginValidateResult);
|
|
415
|
-
};
|
|
416
|
-
|
|
417
|
-
let envData: Record<string, unknown> | undefined;
|
|
418
|
-
const afterEnv = (envResult: Record<string, unknown> | undefined) => {
|
|
419
|
-
const stdinDataOrPromise = readStdinForParse();
|
|
420
|
-
return thenMaybe(stdinDataOrPromise, (stdinData) => {
|
|
421
|
-
const hasStdinData = Object.keys(stdinData).length > 0;
|
|
422
|
-
return finalize(envResult, hasStdinData ? stdinData : undefined);
|
|
423
|
-
});
|
|
424
|
-
};
|
|
425
|
-
|
|
426
|
-
if (envSchema) {
|
|
427
|
-
const runtime = getCommandRuntime(existingCommand);
|
|
428
|
-
const rawEnv = runtime.env();
|
|
429
|
-
const envValidated = envSchema['~standard'].validate(rawEnv);
|
|
430
|
-
|
|
431
|
-
return thenMaybe(envValidated, (result) => {
|
|
432
|
-
if (!result.issues) {
|
|
433
|
-
envData = result.value as unknown as Record<string, unknown>;
|
|
434
|
-
}
|
|
435
|
-
return afterEnv(envData);
|
|
436
|
-
});
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
return afterEnv(envData);
|
|
440
|
-
};
|
|
441
|
-
|
|
442
|
-
const validatedOrPromise = runPluginChain('validate', commandPlugins, validateCtx, coreValidate);
|
|
443
|
-
|
|
444
|
-
return warnIfUnexpectedAsync(
|
|
445
|
-
thenMaybe(validatedOrPromise, (v) => ({
|
|
446
|
-
command: command as any,
|
|
447
|
-
args: v.args,
|
|
448
|
-
argsResult: v.argsResult,
|
|
449
|
-
})),
|
|
450
|
-
command,
|
|
451
|
-
);
|
|
452
|
-
};
|
|
453
|
-
|
|
454
|
-
return makeThenable(thenMaybe(parsedOrPromise, continueAfterParse)) as any;
|
|
455
|
-
};
|
|
456
|
-
|
|
457
|
-
const stringify: AnyPadroneProgram['stringify'] = (command = '' as any, args) => {
|
|
458
|
-
const commandObj = typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
|
|
459
|
-
if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
|
|
460
|
-
|
|
461
|
-
const parts: string[] = [];
|
|
462
|
-
|
|
463
|
-
if (commandObj.path) parts.push(commandObj.path);
|
|
464
|
-
|
|
465
|
-
// Get positional config to determine which args are positional
|
|
466
|
-
const positionalConfig = commandObj.meta?.positional ? parsePositionalConfig(commandObj.meta.positional) : [];
|
|
467
|
-
const positionalNames = new Set(positionalConfig.map((p) => p.name));
|
|
468
|
-
|
|
469
|
-
// Output positional arguments first in order
|
|
470
|
-
if (args && typeof args === 'object') {
|
|
471
|
-
for (const { name, variadic } of positionalConfig) {
|
|
472
|
-
const value = (args as Record<string, unknown>)[name];
|
|
473
|
-
if (value === undefined) continue;
|
|
474
|
-
|
|
475
|
-
if (variadic && Array.isArray(value)) {
|
|
476
|
-
for (const v of value) {
|
|
477
|
-
const vStr = String(v);
|
|
478
|
-
if (vStr.includes(' ')) parts.push(`"${vStr}"`);
|
|
479
|
-
else parts.push(vStr);
|
|
480
|
-
}
|
|
481
|
-
} else {
|
|
482
|
-
const argStr = String(value);
|
|
483
|
-
if (argStr.includes(' ')) parts.push(`"${argStr}"`);
|
|
484
|
-
else parts.push(argStr);
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// Helper to stringify a value with a given key prefix
|
|
489
|
-
const stringifyValue = (key: string, value: unknown) => {
|
|
490
|
-
if (value === undefined) return;
|
|
491
|
-
|
|
492
|
-
if (typeof value === 'boolean') {
|
|
493
|
-
if (value) parts.push(`--${key}`);
|
|
494
|
-
else parts.push(`--no-${key}`);
|
|
495
|
-
} else if (Array.isArray(value)) {
|
|
496
|
-
// Handle variadic arguments - output each value separately
|
|
497
|
-
for (const v of value) {
|
|
498
|
-
const vStr = String(v);
|
|
499
|
-
if (vStr.includes(' ')) parts.push(`--${key}="${vStr}"`);
|
|
500
|
-
else parts.push(`--${key}=${vStr}`);
|
|
501
|
-
}
|
|
502
|
-
} else if (typeof value === 'object' && value !== null) {
|
|
503
|
-
// Handle nested objects - convert to dot notation
|
|
504
|
-
for (const [nestedKey, nestedValue] of Object.entries(value)) {
|
|
505
|
-
stringifyValue(`${key}.${nestedKey}`, nestedValue);
|
|
506
|
-
}
|
|
507
|
-
} else if (typeof value === 'string') {
|
|
508
|
-
if (value.includes(' ')) parts.push(`--${key}="${value}"`);
|
|
509
|
-
else parts.push(`--${key}=${value}`);
|
|
510
|
-
} else {
|
|
511
|
-
parts.push(`--${key}=${value}`);
|
|
512
|
-
}
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
// Output remaining arguments (non-positional)
|
|
516
|
-
for (const [key, value] of Object.entries(args)) {
|
|
517
|
-
if (value === undefined || positionalNames.has(key)) continue;
|
|
518
|
-
stringifyValue(key, value);
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
return parts.join(' ');
|
|
523
|
-
};
|
|
524
|
-
|
|
525
|
-
type DetailLevel = 'minimal' | 'standard' | 'full';
|
|
526
|
-
type FormatLevel = 'text' | 'ansi' | 'console' | 'markdown' | 'html' | 'json' | 'auto';
|
|
527
|
-
|
|
528
|
-
/**
|
|
529
|
-
* Check if help, version, or completion flags/commands are present in the input.
|
|
530
|
-
* Returns the appropriate action to take, or null if normal execution should proceed.
|
|
531
|
-
*/
|
|
532
|
-
const checkBuiltinCommands = (
|
|
533
|
-
input: string | undefined,
|
|
534
|
-
):
|
|
535
|
-
| { type: 'help'; command?: AnyPadroneCommand; detail?: DetailLevel; format?: FormatLevel; all?: boolean }
|
|
536
|
-
| { type: 'version' }
|
|
537
|
-
| { type: 'completion'; shell?: ShellType; setup?: boolean }
|
|
538
|
-
| { type: 'man'; setup?: boolean; remove?: boolean }
|
|
539
|
-
| { type: 'repl'; scope?: string }
|
|
540
|
-
| { type: 'mcp'; transport?: 'http' | 'stdio'; port?: number; host?: string; basePath?: string }
|
|
541
|
-
| { type: 'serve'; port?: number; host?: string; basePath?: string }
|
|
542
|
-
| null => {
|
|
543
|
-
if (!input) return null;
|
|
544
|
-
|
|
545
|
-
const parts = parseCliInputToParts(input);
|
|
546
|
-
const terms = parts.filter((p) => p.type === 'term').map((p) => p.value);
|
|
547
|
-
const args = parts.filter((p) => p.type === 'named' || p.type === 'alias');
|
|
548
|
-
|
|
549
|
-
// Helper to check if a key array matches a single key string
|
|
550
|
-
const keyIs = (key: string[], name: string) => key.length === 1 && key[0] === name;
|
|
551
|
-
|
|
552
|
-
// Check for --help, -h flags (these take precedence over commands)
|
|
553
|
-
const hasHelpFlag = args.some((p) => (p.type === 'named' && keyIs(p.key, 'help')) || (p.type === 'alias' && keyIs(p.key, 'h')));
|
|
554
|
-
|
|
555
|
-
// Extract detail level from --detail[=<level>] or -d [<level>]
|
|
556
|
-
// Bare --detail (no value) defaults to 'full'
|
|
557
|
-
const getDetailLevel = (): DetailLevel | undefined => {
|
|
558
|
-
for (const arg of args) {
|
|
559
|
-
if (arg.type === 'named' && keyIs(arg.key, 'detail')) {
|
|
560
|
-
if (typeof arg.value === 'string' && (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full')) {
|
|
561
|
-
return arg.value;
|
|
562
|
-
}
|
|
563
|
-
return 'full';
|
|
564
|
-
}
|
|
565
|
-
if (arg.type === 'alias' && keyIs(arg.key, 'd')) {
|
|
566
|
-
if (typeof arg.value === 'string' && (arg.value === 'minimal' || arg.value === 'standard' || arg.value === 'full')) {
|
|
567
|
-
return arg.value;
|
|
568
|
-
}
|
|
569
|
-
return 'full';
|
|
570
|
-
}
|
|
571
|
-
}
|
|
572
|
-
return undefined;
|
|
573
|
-
};
|
|
574
|
-
const detail = getDetailLevel();
|
|
575
|
-
|
|
576
|
-
// Extract format from --format=<value> or -f <value>
|
|
577
|
-
const getFormat = (): FormatLevel | undefined => {
|
|
578
|
-
const validFormats: FormatLevel[] = ['text', 'ansi', 'console', 'markdown', 'html', 'json', 'auto'];
|
|
579
|
-
for (const arg of args) {
|
|
580
|
-
if (arg.type === 'named' && keyIs(arg.key, 'format') && typeof arg.value === 'string') {
|
|
581
|
-
if (validFormats.includes(arg.value as FormatLevel)) {
|
|
582
|
-
return arg.value as FormatLevel;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
if (arg.type === 'alias' && keyIs(arg.key, 'f') && typeof arg.value === 'string') {
|
|
586
|
-
if (validFormats.includes(arg.value as FormatLevel)) {
|
|
587
|
-
return arg.value as FormatLevel;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
return undefined;
|
|
592
|
-
};
|
|
593
|
-
const format = getFormat();
|
|
594
|
-
|
|
595
|
-
// Check for --all flag (show all built-in help)
|
|
596
|
-
const hasAllFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'all'));
|
|
597
|
-
|
|
598
|
-
// Check for --version, -v, -V flags
|
|
599
|
-
const hasVersionFlag = args.some(
|
|
600
|
-
(p) => (p.type === 'named' && keyIs(p.key, 'version')) || (p.type === 'alias' && (keyIs(p.key, 'v') || keyIs(p.key, 'V'))),
|
|
601
|
-
);
|
|
602
|
-
|
|
603
|
-
// If the first term is the program name, skip it
|
|
604
|
-
const normalizedTerms = [...terms];
|
|
605
|
-
if (normalizedTerms[0] === existingCommand.name) normalizedTerms.shift();
|
|
606
|
-
|
|
607
|
-
// Check if user has defined 'help', 'version', or 'completion' commands (they take precedence)
|
|
608
|
-
const userHelpCommand = findCommandByName('help', existingCommand.commands);
|
|
609
|
-
const userVersionCommand = findCommandByName('version', existingCommand.commands);
|
|
610
|
-
const userCompletionCommand = findCommandByName('completion', existingCommand.commands);
|
|
611
|
-
|
|
612
|
-
// Check for 'help' command (only if user hasn't defined one)
|
|
613
|
-
// Supports both 'help <command>' and '<command> help' forms
|
|
614
|
-
if (!userHelpCommand && normalizedTerms[0] === 'help') {
|
|
615
|
-
// help <command> - get help for specific command
|
|
616
|
-
const commandName = normalizedTerms.slice(1).join(' ');
|
|
617
|
-
const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
|
|
618
|
-
return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
|
|
619
|
-
}
|
|
620
|
-
if (!userHelpCommand && normalizedTerms.length > 0 && normalizedTerms[normalizedTerms.length - 1] === 'help') {
|
|
621
|
-
// <command> help - get help for specific command (trailing form)
|
|
622
|
-
const commandTerms = normalizedTerms.slice(0, -1);
|
|
623
|
-
// Walk the command tree to find the deepest matching command
|
|
624
|
-
let targetCommand: AnyPadroneCommand | undefined;
|
|
625
|
-
let current = existingCommand;
|
|
626
|
-
for (const term of commandTerms) {
|
|
627
|
-
const found = findCommandByName(term, current.commands);
|
|
628
|
-
if (found) {
|
|
629
|
-
targetCommand = found;
|
|
630
|
-
current = found;
|
|
631
|
-
} else {
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
// Check for 'version' command (only if user hasn't defined one)
|
|
639
|
-
if (!userVersionCommand && normalizedTerms[0] === 'version') {
|
|
640
|
-
return { type: 'version' };
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
// Check for 'completion' command (only if user hasn't defined one)
|
|
644
|
-
if (!userCompletionCommand && normalizedTerms[0] === 'completion') {
|
|
645
|
-
const shellArg = normalizedTerms[1] as ShellType | undefined;
|
|
646
|
-
const validShells: ShellType[] = ['bash', 'zsh', 'fish', 'powershell'];
|
|
647
|
-
const shell = shellArg && validShells.includes(shellArg) ? shellArg : undefined;
|
|
648
|
-
const setup = args.some((p) => p.type === 'named' && keyIs(p.key, 'setup'));
|
|
649
|
-
return { type: 'completion', shell, setup };
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// Check for 'man' command (only if user hasn't defined one)
|
|
653
|
-
const userManCommand = findCommandByName('man', existingCommand.commands);
|
|
654
|
-
if (!userManCommand && normalizedTerms[0] === 'man') {
|
|
655
|
-
const setup = args.some((p) => p.type === 'named' && keyIs(p.key, 'setup'));
|
|
656
|
-
const remove = args.some((p) => p.type === 'named' && keyIs(p.key, 'remove'));
|
|
657
|
-
return { type: 'man', setup, remove };
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Handle help flag - find the command being requested
|
|
661
|
-
if (hasHelpFlag) {
|
|
662
|
-
// Filter out help-related terms and flags to find the target command
|
|
663
|
-
const commandTerms = normalizedTerms.filter((t) => t !== 'help');
|
|
664
|
-
const commandName = commandTerms.join(' ');
|
|
665
|
-
const targetCommand = commandName ? findCommandByName(commandName, existingCommand.commands) : undefined;
|
|
666
|
-
return { type: 'help', command: targetCommand, detail, format, all: hasAllFlag || undefined };
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
// Handle version flag (only for root command, i.e., no subcommand terms)
|
|
670
|
-
if (hasVersionFlag && normalizedTerms.length === 0) {
|
|
671
|
-
return { type: 'version' };
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
// Check for 'mcp' command (only if user hasn't defined one)
|
|
675
|
-
const userMcpCommand = findCommandByName('mcp', existingCommand.commands);
|
|
676
|
-
if (!userMcpCommand && normalizedTerms[0] === 'mcp') {
|
|
677
|
-
const transportArg = normalizedTerms[1];
|
|
678
|
-
const transport = transportArg === 'stdio' || transportArg === 'http' ? transportArg : undefined;
|
|
679
|
-
const portArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'port'));
|
|
680
|
-
const port = typeof portArg?.value === 'string' ? parseInt(portArg.value, 10) : undefined;
|
|
681
|
-
const hostArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'host'));
|
|
682
|
-
const host = typeof hostArg?.value === 'string' ? hostArg.value : undefined;
|
|
683
|
-
const basePathArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'base-path'));
|
|
684
|
-
const mcpBasePath = typeof basePathArg?.value === 'string' ? basePathArg.value : undefined;
|
|
685
|
-
return { type: 'mcp', transport, port: port && !Number.isNaN(port) ? port : undefined, host, basePath: mcpBasePath };
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
// Check for 'serve' command (only if user hasn't defined one)
|
|
689
|
-
const userServeCommand = findCommandByName('serve', existingCommand.commands);
|
|
690
|
-
if (!userServeCommand && normalizedTerms[0] === 'serve') {
|
|
691
|
-
const portArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'port'));
|
|
692
|
-
const port = typeof portArg?.value === 'string' ? parseInt(portArg.value, 10) : undefined;
|
|
693
|
-
const hostArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'host'));
|
|
694
|
-
const host = typeof hostArg?.value === 'string' ? hostArg.value : undefined;
|
|
695
|
-
const basePathArg = args.find((p) => p.type === 'named' && keyIs(p.key, 'base-path'));
|
|
696
|
-
const basePath = typeof basePathArg?.value === 'string' ? basePathArg.value : undefined;
|
|
697
|
-
return { type: 'serve', port: port && !Number.isNaN(port) ? port : undefined, host, basePath };
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
// Check for --repl flag
|
|
701
|
-
const hasReplFlag = args.some((p) => p.type === 'named' && keyIs(p.key, 'repl'));
|
|
702
|
-
if (hasReplFlag) {
|
|
703
|
-
const scope = normalizedTerms.length > 0 ? normalizedTerms.join(' ') : undefined;
|
|
704
|
-
return { type: 'repl', scope };
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
return null;
|
|
708
|
-
};
|
|
709
|
-
|
|
710
|
-
/**
|
|
711
|
-
* Extract the config file path from --config=<path> or -c <path> flags.
|
|
712
|
-
*/
|
|
713
|
-
const extractConfigPath = (input: string | undefined): string | undefined => {
|
|
714
|
-
if (!input) return undefined;
|
|
715
|
-
|
|
716
|
-
const parts = parseCliInputToParts(input);
|
|
717
|
-
const args = parts.filter((p) => p.type === 'named' || p.type === 'alias');
|
|
718
|
-
|
|
719
|
-
for (const arg of args) {
|
|
720
|
-
if (arg.type === 'named' && arg.key.length === 1 && arg.key[0] === 'config' && typeof arg.value === 'string') {
|
|
721
|
-
return arg.value;
|
|
722
|
-
}
|
|
723
|
-
if (arg.type === 'alias' && arg.key.length === 1 && arg.key[0] === 'c' && typeof arg.value === 'string') {
|
|
724
|
-
return arg.value;
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
return undefined;
|
|
728
|
-
};
|
|
729
|
-
|
|
730
|
-
/**
|
|
731
|
-
* Extract --color flag from input.
|
|
732
|
-
* - `--color` or `--color=true` → use default theme
|
|
733
|
-
* - `--color=false` or `--no-color` → disable colors (text format)
|
|
734
|
-
* - `--color=<theme>` → use the named theme
|
|
735
|
-
* Returns `undefined` if no --color flag is present.
|
|
736
|
-
*/
|
|
737
|
-
const extractColorFlag = (input: string | undefined): { theme?: ColorTheme | ColorConfig; disableColor?: boolean } | undefined => {
|
|
738
|
-
if (!input) return undefined;
|
|
739
|
-
|
|
740
|
-
const parts = parseCliInputToParts(input);
|
|
741
|
-
const args = parts.filter((p) => p.type === 'named');
|
|
742
|
-
const keyIs = (key: string[], name: string) => key.length === 1 && key[0] === name;
|
|
743
|
-
|
|
744
|
-
for (const arg of args) {
|
|
745
|
-
if (arg.type === 'named' && keyIs(arg.key, 'no-color')) {
|
|
746
|
-
return { disableColor: true };
|
|
747
|
-
}
|
|
748
|
-
if (arg.type === 'named' && keyIs(arg.key, 'color')) {
|
|
749
|
-
if (arg.negated) return { disableColor: true };
|
|
750
|
-
if (arg.value === undefined || arg.value === 'true') return { theme: 'default' };
|
|
751
|
-
if (arg.value === 'false') return { disableColor: true };
|
|
752
|
-
if (typeof arg.value === 'string' && arg.value in colorThemes) return { theme: arg.value as ColorTheme };
|
|
753
|
-
return undefined;
|
|
754
|
-
}
|
|
755
|
-
}
|
|
756
|
-
return undefined;
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
/**
|
|
760
|
-
* Core execution logic shared by eval() and cli().
|
|
761
|
-
* errorMode controls validation error behavior:
|
|
762
|
-
* - 'soft': return result with issues (eval behavior)
|
|
763
|
-
* - 'hard': print error + help and throw (cli-without-input behavior)
|
|
764
|
-
*/
|
|
765
|
-
const execCommand = (resolvedInput: string | undefined, evalOptions?: PadroneEvalPreferences, errorMode: 'soft' | 'hard' = 'soft') => {
|
|
766
|
-
const baseRuntime = getCommandRuntime(existingCommand);
|
|
767
|
-
let runtime = evalOptions?.runtime
|
|
768
|
-
? Object.assign({}, baseRuntime, Object.fromEntries(Object.entries(evalOptions.runtime).filter(([, v]) => v !== undefined)))
|
|
769
|
-
: baseRuntime;
|
|
770
|
-
|
|
771
|
-
// Apply --color / --no-color flag to runtime
|
|
772
|
-
const colorFlag = extractColorFlag(resolvedInput);
|
|
773
|
-
if (colorFlag) {
|
|
774
|
-
runtime = {
|
|
775
|
-
...runtime,
|
|
776
|
-
...(colorFlag.disableColor ? { format: 'text' as const, theme: undefined } : { theme: colorFlag.theme }),
|
|
777
|
-
};
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Check for built-in help/version/completion commands and flags (bypass plugins)
|
|
781
|
-
const builtin = checkBuiltinCommands(resolvedInput);
|
|
782
|
-
|
|
783
|
-
if (builtin) {
|
|
784
|
-
if (builtin.type === 'help') {
|
|
785
|
-
resolveAllCommands(existingCommand);
|
|
786
|
-
const helpText = generateHelp(existingCommand, builtin.command ?? existingCommand, {
|
|
787
|
-
detail: builtin.detail,
|
|
788
|
-
format: builtin.format ?? runtime.format,
|
|
789
|
-
theme: runtime.theme,
|
|
790
|
-
all: builtin.all,
|
|
791
|
-
});
|
|
792
|
-
runtime.output(helpText);
|
|
793
|
-
return withDrain({
|
|
794
|
-
command: existingCommand,
|
|
795
|
-
args: undefined,
|
|
796
|
-
result: helpText,
|
|
797
|
-
}) as any;
|
|
798
|
-
}
|
|
799
|
-
|
|
800
|
-
if (builtin.type === 'version') {
|
|
801
|
-
const version = getVersion(existingCommand.version);
|
|
802
|
-
runtime.output(version);
|
|
803
|
-
return withDrain({
|
|
804
|
-
command: existingCommand,
|
|
805
|
-
args: undefined,
|
|
806
|
-
result: version,
|
|
807
|
-
}) as any;
|
|
808
|
-
}
|
|
809
|
-
|
|
810
|
-
if (builtin.type === 'completion') {
|
|
811
|
-
resolveAllCommands(existingCommand);
|
|
812
|
-
return import('./completion.ts').then(({ detectShell, generateCompletionOutput, setupCompletions }) => {
|
|
813
|
-
if (builtin.setup) {
|
|
814
|
-
const shell = builtin.shell ?? detectShell();
|
|
815
|
-
if (!shell) {
|
|
816
|
-
throw new Error('Could not detect shell. Specify one: completion bash --setup');
|
|
817
|
-
}
|
|
818
|
-
const result = setupCompletions(existingCommand.name, shell);
|
|
819
|
-
const message = `${result.updated ? 'Updated' : 'Added'} ${existingCommand.name} completions in ${result.file}`;
|
|
820
|
-
runtime.output(message);
|
|
821
|
-
return withDrain({
|
|
822
|
-
command: existingCommand,
|
|
823
|
-
args: undefined,
|
|
824
|
-
result: message,
|
|
825
|
-
});
|
|
826
|
-
}
|
|
827
|
-
const completionScript = generateCompletionOutput(existingCommand, builtin.shell);
|
|
828
|
-
runtime.output(completionScript);
|
|
829
|
-
return withDrain({
|
|
830
|
-
command: existingCommand,
|
|
831
|
-
args: undefined,
|
|
832
|
-
result: completionScript,
|
|
833
|
-
});
|
|
834
|
-
}) as any;
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (builtin.type === 'man') {
|
|
838
|
-
resolveAllCommands(existingCommand);
|
|
839
|
-
return import('./docs/index.ts').then(({ setupManPages, removeManPages, generateDocs }) => {
|
|
840
|
-
if (builtin.setup) {
|
|
841
|
-
const result = setupManPages(existingCommand);
|
|
842
|
-
const message = `${result.updated ? 'Updated' : 'Installed'} ${result.written.length} man page(s) in ${result.dir}`;
|
|
843
|
-
runtime.output(message);
|
|
844
|
-
return withDrain({
|
|
845
|
-
command: existingCommand,
|
|
846
|
-
args: undefined,
|
|
847
|
-
result: message,
|
|
848
|
-
});
|
|
849
|
-
}
|
|
850
|
-
if (builtin.remove) {
|
|
851
|
-
const result = removeManPages(existingCommand);
|
|
852
|
-
const message =
|
|
853
|
-
result.removed.length > 0
|
|
854
|
-
? `Removed ${result.removed.length} man page(s) from ${result.dir}`
|
|
855
|
-
: 'No man pages found to remove.';
|
|
856
|
-
runtime.output(message);
|
|
857
|
-
return withDrain({
|
|
858
|
-
command: existingCommand,
|
|
859
|
-
args: undefined,
|
|
860
|
-
result: message,
|
|
861
|
-
});
|
|
862
|
-
}
|
|
863
|
-
// Default: generate man page for the root command and print it
|
|
864
|
-
const result = generateDocs(existingCommand, { format: 'man' });
|
|
865
|
-
const manPage = result.pages[0]?.content ?? '';
|
|
866
|
-
runtime.output(manPage);
|
|
867
|
-
return withDrain({
|
|
868
|
-
command: existingCommand,
|
|
869
|
-
args: undefined,
|
|
870
|
-
result: manPage,
|
|
871
|
-
});
|
|
872
|
-
}) as any;
|
|
873
|
-
}
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
// Shared plugin state for this execution
|
|
877
|
-
const state: Record<string, unknown> = {};
|
|
878
|
-
const rootPlugins = existingCommand.plugins ?? [];
|
|
879
|
-
|
|
880
|
-
const runPipeline = () => {
|
|
881
|
-
// ── Phase 1: Parse ──────────────────────────────────────────────────
|
|
882
|
-
const parseCtx: PluginParseContext = { input: resolvedInput, command: existingCommand, state };
|
|
883
|
-
|
|
884
|
-
const coreParse = (): PluginParseResult => {
|
|
885
|
-
const { command, rawArgs, args, unmatchedTerms } = parseCommand(parseCtx.input);
|
|
886
|
-
|
|
887
|
-
// Default help: command with no action → show its help when there's nothing to execute.
|
|
888
|
-
const hasSubcommands = command.commands && command.commands.length > 0;
|
|
889
|
-
const hasSchema = command.argsSchema != null;
|
|
890
|
-
if (!command.action && (hasSubcommands || !hasSchema) && unmatchedTerms.length === 0) {
|
|
891
|
-
resolveAllCommands(existingCommand);
|
|
892
|
-
const helpText = generateHelp(existingCommand, command, { format: runtime.format, theme: runtime.theme });
|
|
893
|
-
runtime.output(helpText);
|
|
894
|
-
return {
|
|
895
|
-
command: command,
|
|
896
|
-
rawArgs: { '~help': helpText } as Record<string, unknown>,
|
|
897
|
-
positionalArgs: [],
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
// Reject unmatched terms when the matched command doesn't accept positional args
|
|
902
|
-
if (unmatchedTerms.length > 0) {
|
|
903
|
-
const hasPositionalConfig = command.meta?.positional && command.meta.positional.length > 0;
|
|
904
|
-
if (!hasPositionalConfig) {
|
|
905
|
-
const isRootCommand = command === existingCommand;
|
|
906
|
-
const commandDisplayName = command.name || command.aliases?.[0] || command.path || '(default)';
|
|
907
|
-
|
|
908
|
-
// Collect candidate names for fuzzy suggestion
|
|
909
|
-
const candidateNames: string[] = [];
|
|
910
|
-
if (isRootCommand && existingCommand.commands) {
|
|
911
|
-
for (const cmd of existingCommand.commands) {
|
|
912
|
-
if (!cmd.hidden) {
|
|
913
|
-
candidateNames.push(cmd.name);
|
|
914
|
-
if (cmd.aliases) candidateNames.push(...cmd.aliases);
|
|
915
|
-
}
|
|
916
|
-
}
|
|
917
|
-
} else if (command.commands) {
|
|
918
|
-
for (const cmd of command.commands) {
|
|
919
|
-
if (!cmd.hidden) {
|
|
920
|
-
candidateNames.push(cmd.name);
|
|
921
|
-
if (cmd.aliases) candidateNames.push(...cmd.aliases);
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const suggestion = suggestSimilar(unmatchedTerms[0]!, candidateNames);
|
|
927
|
-
const suggestions = suggestion ? [suggestion] : [];
|
|
928
|
-
const baseMsg = isRootCommand
|
|
929
|
-
? `Unknown command: ${unmatchedTerms[0]}`
|
|
930
|
-
: `Unexpected arguments for '${commandDisplayName}': ${unmatchedTerms.join(' ')}`;
|
|
931
|
-
const errorMsg = suggestions.length ? `${baseMsg}\n\n ${suggestions[0]}` : baseMsg;
|
|
932
|
-
|
|
933
|
-
if (errorMode === 'hard') {
|
|
934
|
-
runtime.error(errorMsg);
|
|
935
|
-
// When we have a suggestion, show a compact single-line "Available commands" note
|
|
936
|
-
// instead of the full help text to avoid overwhelming the user
|
|
937
|
-
if (suggestions.length > 0) {
|
|
938
|
-
const targetCmd = isRootCommand ? existingCommand : command;
|
|
939
|
-
const visibleCommands = (targetCmd.commands ?? []).filter((c) => !c.hidden && c.name);
|
|
940
|
-
if (visibleCommands.length > 0) {
|
|
941
|
-
const cmdList = visibleCommands.map((c) => c.name).join(', ');
|
|
942
|
-
runtime.output(`\nAvailable commands: ${cmdList}`);
|
|
943
|
-
}
|
|
944
|
-
} else {
|
|
945
|
-
resolveAllCommands(existingCommand);
|
|
946
|
-
const helpText = generateHelp(existingCommand, isRootCommand ? existingCommand : command, {
|
|
947
|
-
format: runtime.format,
|
|
948
|
-
theme: runtime.theme,
|
|
949
|
-
});
|
|
950
|
-
runtime.error(helpText);
|
|
951
|
-
}
|
|
952
|
-
throw new RoutingError(errorMsg, { suggestions, command: command.path || command.name });
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
// Soft mode: throw too — this is a routing error, not a validation issue
|
|
956
|
-
throw new RoutingError(errorMsg, { suggestions, command: command.path || command.name });
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
return { command, rawArgs, positionalArgs: args };
|
|
961
|
-
};
|
|
962
|
-
|
|
963
|
-
// Parse phase: root plugins only
|
|
964
|
-
const parsedOrPromise = runPluginChain('parse', rootPlugins, parseCtx, coreParse);
|
|
965
|
-
|
|
966
|
-
// ── Phases 2 & 3 chained after parse ────────────────────────────────
|
|
967
|
-
const continueAfterParse = (parsed: PluginParseResult) => {
|
|
968
|
-
const { command } = parsed;
|
|
969
|
-
// Validate/execute: collected from parent chain
|
|
970
|
-
const commandPlugins = collectPlugins(command);
|
|
971
|
-
|
|
972
|
-
// Short-circuit: parse returned a help result
|
|
973
|
-
if (parsed.rawArgs['~help']) {
|
|
974
|
-
return {
|
|
975
|
-
command: command,
|
|
976
|
-
args: undefined,
|
|
977
|
-
result: parsed.rawArgs['~help'],
|
|
978
|
-
} as any;
|
|
979
|
-
}
|
|
980
|
-
|
|
981
|
-
// ── Auto-progress: start before validation ───────────────────────
|
|
982
|
-
const progressConfig = command.progress;
|
|
983
|
-
if (progressConfig && runtime.progress) {
|
|
984
|
-
const isObj = typeof progressConfig === 'object';
|
|
985
|
-
const defaultMsg = typeof progressConfig === 'string' ? progressConfig : `Running ${command.name}...`;
|
|
986
|
-
const progressMsg = isObj ? (progressConfig.progress ?? defaultMsg) : defaultMsg;
|
|
987
|
-
const validationMsg = isObj ? (progressConfig.validation ?? '') : '';
|
|
988
|
-
state._progressSuccess = isObj ? progressConfig.success : undefined;
|
|
989
|
-
state._progressError = isObj ? progressConfig.error : undefined;
|
|
990
|
-
state._progressMsg = progressMsg;
|
|
991
|
-
state._progressValidationMsg = validationMsg || undefined;
|
|
992
|
-
const spinnerConfig = isObj ? progressConfig.spinner : undefined;
|
|
993
|
-
const progressOptions = spinnerConfig !== undefined ? { spinner: spinnerConfig } : undefined;
|
|
994
|
-
const indicator = createProgress(runtime, validationMsg || progressMsg, progressOptions);
|
|
995
|
-
state._progress = indicator;
|
|
996
|
-
|
|
997
|
-
const originalOutput = runtime.output;
|
|
998
|
-
const originalError = runtime.error;
|
|
999
|
-
runtime.output = (...args: unknown[]) => {
|
|
1000
|
-
indicator.pause();
|
|
1001
|
-
originalOutput(...args);
|
|
1002
|
-
indicator.resume();
|
|
1003
|
-
};
|
|
1004
|
-
runtime.error = (text: string) => {
|
|
1005
|
-
indicator.pause();
|
|
1006
|
-
originalError(text);
|
|
1007
|
-
indicator.resume();
|
|
1008
|
-
};
|
|
1009
|
-
state._restoreOutput = () => {
|
|
1010
|
-
runtime.output = originalOutput;
|
|
1011
|
-
runtime.error = originalError;
|
|
1012
|
-
};
|
|
1013
|
-
}
|
|
1014
|
-
|
|
1015
|
-
// ── Phase 2: Validate ───────────────────────────────────────────
|
|
1016
|
-
const validateCtx: PluginValidateContext = {
|
|
1017
|
-
command,
|
|
1018
|
-
rawArgs: parsed.rawArgs,
|
|
1019
|
-
positionalArgs: parsed.positionalArgs,
|
|
1020
|
-
state,
|
|
1021
|
-
};
|
|
1022
|
-
|
|
1023
|
-
const coreValidate = (): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
1024
|
-
// Determine interactivity
|
|
1025
|
-
let flagInteractive: boolean | undefined;
|
|
1026
|
-
if (hasInteractiveConfig(command.meta)) {
|
|
1027
|
-
if (validateCtx.rawArgs.interactive !== undefined) {
|
|
1028
|
-
flagInteractive = validateCtx.rawArgs.interactive !== false && validateCtx.rawArgs.interactive !== 'false';
|
|
1029
|
-
delete validateCtx.rawArgs.interactive;
|
|
1030
|
-
}
|
|
1031
|
-
if (validateCtx.rawArgs.i !== undefined) {
|
|
1032
|
-
flagInteractive = validateCtx.rawArgs.i !== false && validateCtx.rawArgs.i !== 'false';
|
|
1033
|
-
delete validateCtx.rawArgs.i;
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
// Strip --color / --no-color from rawArgs (handled globally)
|
|
1038
|
-
delete validateCtx.rawArgs.color;
|
|
1039
|
-
delete validateCtx.rawArgs['no-color'];
|
|
1040
|
-
|
|
1041
|
-
const runtimeDefault: boolean | undefined =
|
|
1042
|
-
runtime.interactive === 'forced' ? true : runtime.interactive === 'disabled' ? false : undefined;
|
|
1043
|
-
const effectiveInteractive: boolean | undefined = flagInteractive ?? evalOptions?.interactive ?? runtimeDefault;
|
|
1044
|
-
// Suppress interactive prompts when the command reads stdin — prompts share stdin which is already consumed/closed.
|
|
1045
|
-
const commandUsesStdin = !!command.meta?.stdin;
|
|
1046
|
-
const stdinIsPiped =
|
|
1047
|
-
commandUsesStdin && (runtime.stdin ? !runtime.stdin.isTTY : typeof process !== 'undefined' && process.stdin?.isTTY !== true);
|
|
1048
|
-
const interactivitySuppressed =
|
|
1049
|
-
runtime.interactive === 'unsupported' || effectiveInteractive === false || (stdinIsPiped && effectiveInteractive !== true);
|
|
1050
|
-
const forceInteractive = !interactivitySuppressed && effectiveInteractive === true;
|
|
1051
|
-
|
|
1052
|
-
// Extract config file path from --config or -c flag
|
|
1053
|
-
const configPath = extractConfigPath(parseCtx.input);
|
|
1054
|
-
|
|
1055
|
-
// Resolve config files: command's own configFiles > inherited from parent/root
|
|
1056
|
-
const resolveConfigFiles = (cmd: AnyPadroneCommand): string[] | undefined => {
|
|
1057
|
-
if (cmd.configFiles !== undefined) return cmd.configFiles;
|
|
1058
|
-
if (cmd.parent) return resolveConfigFiles(cmd.parent);
|
|
1059
|
-
return undefined;
|
|
1060
|
-
};
|
|
1061
|
-
const effectiveConfigFiles = resolveConfigFiles(command);
|
|
1062
|
-
|
|
1063
|
-
// Resolve config schema: command's own configSchema > inherited from parent/root
|
|
1064
|
-
const resolveConfigSchema = (cmd: AnyPadroneCommand): AnyPadroneCommand['configSchema'] => {
|
|
1065
|
-
if (cmd.configSchema !== undefined) return cmd.configSchema;
|
|
1066
|
-
if (cmd.parent) return resolveConfigSchema(cmd.parent);
|
|
1067
|
-
return undefined;
|
|
1068
|
-
};
|
|
1069
|
-
const configSchema = resolveConfigSchema(command);
|
|
1070
|
-
|
|
1071
|
-
// Resolve env schema: command's own envSchema > inherited from parent/root
|
|
1072
|
-
const resolveEnvSchema = (cmd: AnyPadroneCommand): AnyPadroneCommand['envSchema'] => {
|
|
1073
|
-
if (cmd.envSchema !== undefined) return cmd.envSchema;
|
|
1074
|
-
if (cmd.parent) return resolveEnvSchema(cmd.parent);
|
|
1075
|
-
return undefined;
|
|
1076
|
-
};
|
|
1077
|
-
const envSchema = resolveEnvSchema(command);
|
|
1078
|
-
|
|
1079
|
-
// Determine config data: explicit --config flag > auto-discovered config
|
|
1080
|
-
let configData: Record<string, unknown> | undefined;
|
|
1081
|
-
if (configPath) {
|
|
1082
|
-
configData = runtime.loadConfigFile(configPath);
|
|
1083
|
-
} else if (effectiveConfigFiles?.length) {
|
|
1084
|
-
const foundConfigPath = runtime.findFile(effectiveConfigFiles);
|
|
1085
|
-
if (foundConfigPath) {
|
|
1086
|
-
configData = runtime.loadConfigFile(foundConfigPath) ?? configData;
|
|
1087
|
-
}
|
|
1088
|
-
}
|
|
1089
|
-
|
|
1090
|
-
// Step 1: Validate config data against schema if provided
|
|
1091
|
-
const validateConfig = (): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> => {
|
|
1092
|
-
if (configData && configSchema) {
|
|
1093
|
-
const configValidated = configSchema['~standard'].validate(configData);
|
|
1094
|
-
return thenMaybe(configValidated, (result) => {
|
|
1095
|
-
if (result.issues) {
|
|
1096
|
-
const issueMessages = result.issues
|
|
1097
|
-
.map((i: StandardSchemaV1.Issue) => ` - ${i.path?.join('.') || 'root'}: ${i.message}`)
|
|
1098
|
-
.join('\n');
|
|
1099
|
-
throw new ConfigError(`Invalid config file:\n${issueMessages}`, {
|
|
1100
|
-
command: command.path || command.name,
|
|
1101
|
-
});
|
|
1102
|
-
}
|
|
1103
|
-
return result.value as unknown as Record<string, unknown>;
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
return configData;
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
|
-
// Step 2: Validate env vars
|
|
1110
|
-
const validateEnv = (): Record<string, unknown> | undefined | Promise<Record<string, unknown> | undefined> => {
|
|
1111
|
-
let envData: Record<string, unknown> | undefined;
|
|
1112
|
-
if (envSchema) {
|
|
1113
|
-
const rawEnv = runtime.env();
|
|
1114
|
-
const envValidated = envSchema['~standard'].validate(rawEnv);
|
|
1115
|
-
return thenMaybe(envValidated, (result) => {
|
|
1116
|
-
if (!result.issues) {
|
|
1117
|
-
envData = result.value as unknown as Record<string, unknown>;
|
|
1118
|
-
}
|
|
1119
|
-
return envData;
|
|
1120
|
-
});
|
|
1121
|
-
}
|
|
1122
|
-
return envData;
|
|
1123
|
-
};
|
|
1124
|
-
|
|
1125
|
-
// Step 3: Read stdin if configured and not already provided via CLI
|
|
1126
|
-
const readStdin = (): Record<string, unknown> | Promise<Record<string, unknown>> => {
|
|
1127
|
-
const stdinConfig = command.meta?.stdin;
|
|
1128
|
-
if (!stdinConfig) return {};
|
|
1129
|
-
|
|
1130
|
-
const field = parseStdinConfig(stdinConfig);
|
|
1131
|
-
|
|
1132
|
-
// Skip if the field was already provided via CLI flags (highest precedence)
|
|
1133
|
-
if (field in validateCtx.rawArgs && validateCtx.rawArgs[field] !== undefined) return {};
|
|
1134
|
-
|
|
1135
|
-
const streamInfo = isAsyncStreamField(command.argsSchema, field);
|
|
1136
|
-
if (streamInfo) {
|
|
1137
|
-
// Async stream: always resolve stdin (even on TTY) for interactive use
|
|
1138
|
-
const stdinForStream = resolveStdinAlways(runtime as any);
|
|
1139
|
-
return { [field]: createStdinStream(stdinForStream, streamInfo.itemSchema) };
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
// Resolve stdin: use runtime's custom stdin, or default if piped.
|
|
1143
|
-
// Returns undefined when stdin is a TTY or unavailable.
|
|
1144
|
-
const stdin = resolveStdin(runtime as any);
|
|
1145
|
-
if (!stdin) return {};
|
|
1146
|
-
|
|
1147
|
-
if (isArrayField(command.argsSchema, field)) {
|
|
1148
|
-
return (async () => {
|
|
1149
|
-
const lines: string[] = [];
|
|
1150
|
-
for await (const line of stdin.lines()) {
|
|
1151
|
-
lines.push(line);
|
|
1152
|
-
}
|
|
1153
|
-
return { [field]: lines };
|
|
1154
|
-
})();
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// Default: read all as text
|
|
1158
|
-
return stdin.text().then((text) => {
|
|
1159
|
-
// Don't inject empty stdin
|
|
1160
|
-
if (!text) return {};
|
|
1161
|
-
return { [field]: text };
|
|
1162
|
-
});
|
|
1163
|
-
};
|
|
1164
|
-
|
|
1165
|
-
// Step 4: Preprocess, interactive prompt, and validate
|
|
1166
|
-
const finalizeValidation = (
|
|
1167
|
-
validatedConfigData: Record<string, unknown> | undefined,
|
|
1168
|
-
envData: Record<string, unknown> | undefined,
|
|
1169
|
-
stdinData: Record<string, unknown> | undefined,
|
|
1170
|
-
): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
1171
|
-
const preprocessedArgs = buildCommandArgs(command, validateCtx.rawArgs, validateCtx.positionalArgs, {
|
|
1172
|
-
stdinData,
|
|
1173
|
-
envData,
|
|
1174
|
-
configData: validatedConfigData,
|
|
1175
|
-
});
|
|
1176
|
-
|
|
1177
|
-
// Early validation: check provided args for errors before prompting.
|
|
1178
|
-
// This catches unknown options and invalid values on explicitly-provided fields
|
|
1179
|
-
// so the user isn't asked interactive questions for a doomed command.
|
|
1180
|
-
const willPrompt = !interactivitySuppressed && runtime.prompt && hasInteractiveConfig(command.meta);
|
|
1181
|
-
if (willPrompt) {
|
|
1182
|
-
const unknowns = checkUnknownArgs(command, preprocessedArgs);
|
|
1183
|
-
if (unknowns.length > 0) {
|
|
1184
|
-
const issues: StandardSchemaV1.Issue[] = unknowns.map(({ key, suggestion }) => ({
|
|
1185
|
-
path: [key],
|
|
1186
|
-
message: suggestion ? `Unknown option: "${key}". ${suggestion}` : `Unknown option: "${key}"`,
|
|
1187
|
-
}));
|
|
1188
|
-
return { args: undefined, argsResult: { issues } as any };
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
// Run schema validation on what we have so far (before prompting fills missing fields).
|
|
1192
|
-
// Only fail on issues for fields the user explicitly provided — skip issues for
|
|
1193
|
-
// missing/undefined fields since those will be filled by interactive prompts.
|
|
1194
|
-
if (command.argsSchema) {
|
|
1195
|
-
const providedKeys = new Set(Object.keys(preprocessedArgs).filter((k) => preprocessedArgs[k] !== undefined));
|
|
1196
|
-
const earlyCheck = command.argsSchema['~standard'].validate(preprocessedArgs);
|
|
1197
|
-
const checkForProvidedFieldErrors = (result: StandardSchemaV1.Result<unknown>): PluginValidateResult | undefined => {
|
|
1198
|
-
if (!result.issues) return undefined;
|
|
1199
|
-
// Only keep issues whose path starts with a key the user actually provided
|
|
1200
|
-
const providedFieldIssues = result.issues.filter((issue) => {
|
|
1201
|
-
const rootKey = issue.path?.[0];
|
|
1202
|
-
return rootKey !== undefined && providedKeys.has(String(rootKey));
|
|
1203
|
-
});
|
|
1204
|
-
if (providedFieldIssues.length > 0) {
|
|
1205
|
-
return { args: undefined, argsResult: { issues: providedFieldIssues } as any };
|
|
1206
|
-
}
|
|
1207
|
-
return undefined;
|
|
1208
|
-
};
|
|
1209
|
-
const earlyResult = thenMaybe(earlyCheck, (result) => {
|
|
1210
|
-
const errors = checkForProvidedFieldErrors(result);
|
|
1211
|
-
if (errors) return errors;
|
|
1212
|
-
return undefined;
|
|
1213
|
-
});
|
|
1214
|
-
if (earlyResult instanceof Promise) {
|
|
1215
|
-
return earlyResult.then((err) => {
|
|
1216
|
-
if (err) return err;
|
|
1217
|
-
return continueWithPrompt(preprocessedArgs);
|
|
1218
|
-
});
|
|
1219
|
-
}
|
|
1220
|
-
if (earlyResult) return earlyResult;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
return continueWithPrompt(preprocessedArgs);
|
|
1225
|
-
};
|
|
1226
|
-
|
|
1227
|
-
const continueWithPrompt = (preprocessedArgs: Record<string, unknown>): PluginValidateResult | Promise<PluginValidateResult> => {
|
|
1228
|
-
const willPrompt = !interactivitySuppressed && runtime.prompt && hasInteractiveConfig(command.meta);
|
|
1229
|
-
const afterInteractive = willPrompt
|
|
1230
|
-
? promptInteractiveFields(preprocessedArgs, command, runtime, forceInteractive || undefined)
|
|
1231
|
-
: preprocessedArgs;
|
|
1232
|
-
|
|
1233
|
-
return thenMaybe(afterInteractive, (filledArgs) => {
|
|
1234
|
-
const validated = validateCommandArgs(command, filledArgs);
|
|
1235
|
-
return thenMaybe(validated, (v) => v as PluginValidateResult);
|
|
1236
|
-
});
|
|
1237
|
-
};
|
|
1238
|
-
|
|
1239
|
-
// Chain: config → env → stdin → validate
|
|
1240
|
-
const validatedConfig = validateConfig();
|
|
1241
|
-
return thenMaybe(validatedConfig, (cfgData) => {
|
|
1242
|
-
const validatedEnv = validateEnv();
|
|
1243
|
-
return thenMaybe(validatedEnv, (envData) => {
|
|
1244
|
-
const stdinDataOrPromise = readStdin();
|
|
1245
|
-
return thenMaybe(stdinDataOrPromise, (stdinData) => {
|
|
1246
|
-
const hasStdinData = Object.keys(stdinData).length > 0;
|
|
1247
|
-
return finalizeValidation(cfgData, envData, hasStdinData ? stdinData : undefined);
|
|
1248
|
-
});
|
|
1249
|
-
});
|
|
1250
|
-
});
|
|
1251
|
-
};
|
|
1252
|
-
|
|
1253
|
-
const validatedOrPromise = runPluginChain('validate', commandPlugins, validateCtx, coreValidate);
|
|
1254
|
-
|
|
1255
|
-
// ── Phase 3: Execute (or handle validation errors) ──────────────
|
|
1256
|
-
const continueAfterValidate = (v: PluginValidateResult) => {
|
|
1257
|
-
// Handle validation failures
|
|
1258
|
-
if (v.argsResult?.issues) {
|
|
1259
|
-
// Collect known option names for fuzzy suggestion on unknown keys
|
|
1260
|
-
let knownOptions: string[] | undefined;
|
|
1261
|
-
const getKnownOptions = () => {
|
|
1262
|
-
if (knownOptions) return knownOptions;
|
|
1263
|
-
knownOptions = [];
|
|
1264
|
-
if (command.argsSchema) {
|
|
1265
|
-
try {
|
|
1266
|
-
const js = command.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
|
|
1267
|
-
if (js.type === 'object' && js.properties) knownOptions = Object.keys(js.properties);
|
|
1268
|
-
} catch {
|
|
1269
|
-
/* ignore */
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
return knownOptions;
|
|
1273
|
-
};
|
|
1274
|
-
|
|
1275
|
-
const issueMessages = v.argsResult.issues
|
|
1276
|
-
.map((i: StandardSchemaV1.Issue) => {
|
|
1277
|
-
const base = ` - ${i.path?.join('.') || 'root'}: ${i.message}`;
|
|
1278
|
-
// Try to suggest for unrecognized key errors
|
|
1279
|
-
const issueAny = i as any;
|
|
1280
|
-
const unrecognizedKeys: string[] | undefined =
|
|
1281
|
-
issueAny.keys ?? i.message?.match(/[Uu]nrecognized key(?:s)?[^"]*"([^"]+)"/)?.slice(1);
|
|
1282
|
-
if (unrecognizedKeys?.length) {
|
|
1283
|
-
const hints = unrecognizedKeys.map((k: string) => suggestSimilar(k, getKnownOptions())).filter(Boolean);
|
|
1284
|
-
if (hints.length) return `${base}\n ${hints.join('\n ')}`;
|
|
1285
|
-
}
|
|
1286
|
-
return base;
|
|
1287
|
-
})
|
|
1288
|
-
.join('\n');
|
|
1289
|
-
|
|
1290
|
-
if (errorMode === 'hard') {
|
|
1291
|
-
resolveAllCommands(existingCommand);
|
|
1292
|
-
const helpText = generateHelp(existingCommand, command, { format: runtime.format, theme: runtime.theme });
|
|
1293
|
-
runtime.error(`Validation error:\n${issueMessages}`);
|
|
1294
|
-
runtime.error(helpText);
|
|
1295
|
-
throw new ValidationError(`Validation error:\n${issueMessages}`, v.argsResult.issues as any, {
|
|
1296
|
-
suggestions: v.argsResult.issues.flatMap((i: any) => {
|
|
1297
|
-
const keys: string[] | undefined = i.keys ?? i.message?.match(/[Uu]nrecognized key(?:s)?[^"]*"([^"]+)"/)?.slice(1);
|
|
1298
|
-
if (!keys?.length) return [];
|
|
1299
|
-
return keys.map((k: string) => suggestSimilar(k, getKnownOptions())).filter(Boolean);
|
|
1300
|
-
}),
|
|
1301
|
-
command: command.path || command.name,
|
|
1302
|
-
});
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
// Soft mode: return result with issues, skip the action
|
|
1306
|
-
return withDrain({
|
|
1307
|
-
command: command as any,
|
|
1308
|
-
args: undefined,
|
|
1309
|
-
argsResult: v.argsResult,
|
|
1310
|
-
result: undefined,
|
|
1311
|
-
});
|
|
1312
|
-
}
|
|
1313
|
-
|
|
1314
|
-
// Update auto-progress message from validation to execute phase
|
|
1315
|
-
const activeIndicator = state._progress as import('./runtime.ts').PadroneProgressIndicator | undefined;
|
|
1316
|
-
if (activeIndicator && state._progressMsg && state._progressValidationMsg) {
|
|
1317
|
-
activeIndicator.update(state._progressMsg as string);
|
|
1318
|
-
}
|
|
1319
|
-
|
|
1320
|
-
const executeCtx: PluginExecuteContext = {
|
|
1321
|
-
command,
|
|
1322
|
-
args: v.args,
|
|
1323
|
-
state,
|
|
1324
|
-
};
|
|
1325
|
-
|
|
1326
|
-
const coreExecute = (): PluginExecuteResult => {
|
|
1327
|
-
const handler = command.action ?? noop;
|
|
1328
|
-
const ctx: PadroneActionContext = {
|
|
1329
|
-
...createActionContext(command),
|
|
1330
|
-
runtime,
|
|
1331
|
-
progress: (state._progress as PadroneProgressIndicator) ?? createLazyIndicator(runtime, state),
|
|
1332
|
-
};
|
|
1333
|
-
const result = handler(executeCtx.args as any, ctx);
|
|
1334
|
-
return { result };
|
|
1335
|
-
};
|
|
1336
|
-
|
|
1337
|
-
const executedOrPromise = runPluginChain('execute', commandPlugins, executeCtx, coreExecute);
|
|
1338
|
-
|
|
1339
|
-
return thenMaybe(executedOrPromise, (e) => {
|
|
1340
|
-
const finalize = (result: unknown) => {
|
|
1341
|
-
// Clean up progress before auto-output so the spinner clears first
|
|
1342
|
-
const indicator = state._progress as import('./runtime.ts').PadroneProgressIndicator | undefined;
|
|
1343
|
-
if (indicator) {
|
|
1344
|
-
const hasProgressConfig = '_progressMsg' in state;
|
|
1345
|
-
if (!hasProgressConfig) {
|
|
1346
|
-
// Lazy/manual indicator: just stop silently
|
|
1347
|
-
indicator.stop();
|
|
1348
|
-
} else {
|
|
1349
|
-
const { message: successMsg, indicator: successIcon } = resolveProgressMessage(state._progressSuccess, result);
|
|
1350
|
-
indicator.succeed(successMsg, successIcon !== undefined ? { indicator: successIcon } : undefined);
|
|
1351
|
-
}
|
|
1352
|
-
(state._restoreOutput as (() => void) | undefined)?.();
|
|
1353
|
-
state._progress = undefined;
|
|
1354
|
-
state._restoreOutput = undefined;
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
const commandResult = withDrain({
|
|
1358
|
-
command: command as any,
|
|
1359
|
-
args: v.args,
|
|
1360
|
-
argsResult: v.argsResult,
|
|
1361
|
-
result,
|
|
1362
|
-
});
|
|
1363
|
-
|
|
1364
|
-
if (command.autoOutput ?? evalOptions?.autoOutput ?? true) {
|
|
1365
|
-
const outputOrPromise = outputValue(result, runtime.output);
|
|
1366
|
-
if (outputOrPromise instanceof Promise) {
|
|
1367
|
-
return outputOrPromise.then(() => commandResult);
|
|
1368
|
-
}
|
|
1369
|
-
}
|
|
1370
|
-
|
|
1371
|
-
return commandResult;
|
|
1372
|
-
};
|
|
1373
|
-
|
|
1374
|
-
// If the action returned a Promise, wait for it before finalizing
|
|
1375
|
-
if (e.result instanceof Promise) {
|
|
1376
|
-
return e.result.then(finalize, (err: unknown) => {
|
|
1377
|
-
const indicator = state._progress as import('./runtime.ts').PadroneProgressIndicator | undefined;
|
|
1378
|
-
if (indicator) {
|
|
1379
|
-
const hasProgressConfig = '_progressMsg' in state;
|
|
1380
|
-
if (!hasProgressConfig) {
|
|
1381
|
-
indicator.stop();
|
|
1382
|
-
} else {
|
|
1383
|
-
const fallback = err instanceof Error ? err.message : String(err);
|
|
1384
|
-
const { message: errorMsg, indicator: errorIcon } = resolveProgressMessage(state._progressError, err, fallback);
|
|
1385
|
-
indicator.fail(errorMsg, errorIcon !== undefined ? { indicator: errorIcon } : undefined);
|
|
1386
|
-
}
|
|
1387
|
-
(state._restoreOutput as (() => void) | undefined)?.();
|
|
1388
|
-
state._progress = undefined;
|
|
1389
|
-
state._restoreOutput = undefined;
|
|
1390
|
-
}
|
|
1391
|
-
throw err;
|
|
1392
|
-
});
|
|
1393
|
-
}
|
|
1394
|
-
|
|
1395
|
-
return finalize(e.result);
|
|
1396
|
-
});
|
|
1397
|
-
};
|
|
1398
|
-
|
|
1399
|
-
return thenMaybe(warnIfUnexpectedAsync(validatedOrPromise, command), continueAfterValidate) as any;
|
|
1400
|
-
};
|
|
1401
|
-
|
|
1402
|
-
return thenMaybe(parsedOrPromise, continueAfterParse) as any;
|
|
1403
|
-
};
|
|
1404
|
-
|
|
1405
|
-
return wrapWithLifecycle(rootPlugins, existingCommand, state, resolvedInput, runPipeline, (result) =>
|
|
1406
|
-
withDrain({
|
|
1407
|
-
command: existingCommand,
|
|
1408
|
-
args: undefined,
|
|
1409
|
-
argsResult: undefined,
|
|
1410
|
-
result,
|
|
1411
|
-
}),
|
|
1412
|
-
) as any;
|
|
1413
|
-
};
|
|
1414
|
-
|
|
1415
|
-
const evalCommand: AnyPadroneProgram['eval'] = (input, evalOptions) => {
|
|
1416
|
-
try {
|
|
1417
|
-
const result = execCommand(input as string, evalOptions, 'soft');
|
|
1418
|
-
if (result instanceof Promise) return withPromiseDrain(result.catch((err: unknown) => errorResult(err))) as any;
|
|
1419
|
-
return makeThenable(result);
|
|
1420
|
-
} catch (err) {
|
|
1421
|
-
return makeThenable(errorResult(err)) as any;
|
|
1422
|
-
}
|
|
1423
|
-
};
|
|
1424
|
-
|
|
1425
|
-
/**
|
|
1426
|
-
* Collects plugins from the command's parent chain (root → ... → target).
|
|
1427
|
-
* Root/program plugins come first (outermost), target command's plugins last (innermost).
|
|
1428
|
-
*
|
|
1429
|
-
* The `programRoot` parameter provides the current program command, because
|
|
1430
|
-
* subcommands' `.parent` references may be stale (builders are immutable — each
|
|
1431
|
-
* method returns a new builder, so a subcommand's parent was captured before
|
|
1432
|
-
* `.use()` was called on the program). We substitute `programRoot` for the
|
|
1433
|
-
* top of the chain to ensure program-level plugins are always included.
|
|
1434
|
-
*/
|
|
1435
|
-
const collectPlugins = (cmd: AnyPadroneCommand): PadronePlugin<any, any>[] => {
|
|
1436
|
-
const chain: PadronePlugin<any, any>[][] = [];
|
|
1437
|
-
let current: AnyPadroneCommand | undefined = cmd;
|
|
1438
|
-
while (current) {
|
|
1439
|
-
// If this is the root (no parent), use existingCommand's plugins instead
|
|
1440
|
-
// to pick up plugins added after subcommands were defined.
|
|
1441
|
-
if (!current.parent) {
|
|
1442
|
-
if (existingCommand.plugins?.length) chain.unshift(existingCommand.plugins);
|
|
1443
|
-
} else {
|
|
1444
|
-
if (current.plugins?.length) chain.unshift(current.plugins);
|
|
1445
|
-
}
|
|
1446
|
-
current = current.parent;
|
|
1447
|
-
}
|
|
1448
|
-
return chain.flat();
|
|
1449
|
-
};
|
|
1450
|
-
|
|
1451
|
-
// Forward declaration — assigned by the repl method in the return object, used by cli() for --repl.
|
|
1452
|
-
const replFn = (options?: PadroneReplPreferences) => {
|
|
1453
|
-
return createReplIterator({ existingCommand, evalCommand, replActiveRef }, options);
|
|
1454
|
-
};
|
|
1455
|
-
const replActiveRef = { value: false };
|
|
1456
|
-
|
|
1457
|
-
const cli: AnyPadroneProgram['cli'] = (cliOptions) => {
|
|
1458
|
-
try {
|
|
1459
|
-
const runtime = getCommandRuntime(existingCommand);
|
|
1460
|
-
const resolvedInput = (runtime.argv().join(' ') || undefined) as string | undefined;
|
|
1461
|
-
|
|
1462
|
-
// Check for --repl flag and mcp command before normal execution
|
|
1463
|
-
const builtin = checkBuiltinCommands(resolvedInput);
|
|
1464
|
-
|
|
1465
|
-
if (cliOptions?.repl !== false && builtin?.type === 'repl') {
|
|
1466
|
-
const replPrefs: PadroneReplPreferences = {
|
|
1467
|
-
...(typeof cliOptions?.repl === 'object' ? cliOptions.repl : {}),
|
|
1468
|
-
scope: builtin.scope,
|
|
1469
|
-
autoOutput: (typeof cliOptions?.repl === 'object' ? cliOptions.repl.autoOutput : undefined) ?? cliOptions?.autoOutput,
|
|
1470
|
-
};
|
|
1471
|
-
const repl = replFn(replPrefs);
|
|
1472
|
-
const drainRepl = async () => {
|
|
1473
|
-
const { value } = await repl.drain();
|
|
1474
|
-
return withDrain({ command: existingCommand, args: undefined, result: value }) as any;
|
|
1475
|
-
};
|
|
1476
|
-
return withPromiseDrain(drainRepl()) as any;
|
|
1477
|
-
}
|
|
1478
|
-
|
|
1479
|
-
if (cliOptions?.mcp !== false && builtin?.type === 'mcp') {
|
|
1480
|
-
const basePrefs = typeof cliOptions?.mcp === 'object' ? cliOptions.mcp : {};
|
|
1481
|
-
const mcpPrefs = {
|
|
1482
|
-
...basePrefs,
|
|
1483
|
-
transport: builtin.transport ?? basePrefs.transport,
|
|
1484
|
-
port: builtin.port ?? basePrefs.port,
|
|
1485
|
-
host: builtin.host ?? basePrefs.host,
|
|
1486
|
-
basePath: builtin.basePath ?? basePrefs.basePath,
|
|
1487
|
-
};
|
|
1488
|
-
const startMcp = async () => {
|
|
1489
|
-
const { startMcpServer } = await import('./mcp.ts');
|
|
1490
|
-
await startMcpServer(builder as any, existingCommand, evalCommand, mcpPrefs);
|
|
1491
|
-
return withDrain({ command: existingCommand, args: undefined, result: undefined }) as any;
|
|
1492
|
-
};
|
|
1493
|
-
return withPromiseDrain(startMcp()) as any;
|
|
1494
|
-
}
|
|
1495
|
-
|
|
1496
|
-
if (cliOptions?.serve !== false && builtin?.type === 'serve') {
|
|
1497
|
-
const basePrefs = typeof cliOptions?.serve === 'object' ? cliOptions.serve : {};
|
|
1498
|
-
const servePrefs = {
|
|
1499
|
-
...basePrefs,
|
|
1500
|
-
port: builtin.port ?? basePrefs.port,
|
|
1501
|
-
host: builtin.host ?? basePrefs.host,
|
|
1502
|
-
basePath: builtin.basePath ?? basePrefs.basePath,
|
|
1503
|
-
};
|
|
1504
|
-
const startServe = async () => {
|
|
1505
|
-
const { startServeServer } = await import('./serve.ts');
|
|
1506
|
-
await startServeServer(builder as any, existingCommand, evalCommand, servePrefs);
|
|
1507
|
-
return withDrain({ command: existingCommand, args: undefined, result: undefined }) as any;
|
|
1508
|
-
};
|
|
1509
|
-
return withPromiseDrain(startServe()) as any;
|
|
1510
|
-
}
|
|
1511
|
-
|
|
1512
|
-
// Start background update check (non-blocking)
|
|
1513
|
-
let updateCheckPromise: Promise<(() => void) | undefined> | undefined;
|
|
1514
|
-
if (existingCommand.updateCheck) {
|
|
1515
|
-
// Respect --no-update-check flag
|
|
1516
|
-
const hasNoUpdateCheckFlag =
|
|
1517
|
-
resolvedInput &&
|
|
1518
|
-
parseCliInputToParts(resolvedInput).some((p) => p.type === 'named' && p.key.length === 1 && p.key[0] === 'no-update-check');
|
|
1519
|
-
if (!hasNoUpdateCheckFlag) {
|
|
1520
|
-
const currentVersion = getVersion(existingCommand.version);
|
|
1521
|
-
updateCheckPromise = import('./update-check.ts').then(({ createUpdateChecker }) =>
|
|
1522
|
-
createUpdateChecker(existingCommand.name, currentVersion, existingCommand.updateCheck!, runtime),
|
|
1523
|
-
);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
|
|
1527
|
-
const result = execCommand(resolvedInput, cliOptions, 'hard');
|
|
1528
|
-
|
|
1529
|
-
// Show update notification after command output
|
|
1530
|
-
if (updateCheckPromise) {
|
|
1531
|
-
if (result instanceof Promise) {
|
|
1532
|
-
return withPromiseDrain(
|
|
1533
|
-
result
|
|
1534
|
-
.then(async (r) => {
|
|
1535
|
-
const showUpdateNotification = await updateCheckPromise;
|
|
1536
|
-
showUpdateNotification?.();
|
|
1537
|
-
return r;
|
|
1538
|
-
})
|
|
1539
|
-
.catch((err: unknown) => errorResult(err)),
|
|
1540
|
-
) as any;
|
|
1541
|
-
}
|
|
1542
|
-
// For sync results, schedule notification for next tick (non-blocking)
|
|
1543
|
-
updateCheckPromise.then((show) => show?.());
|
|
1544
|
-
}
|
|
1545
|
-
|
|
1546
|
-
if (result instanceof Promise) return withPromiseDrain(result.catch((err: unknown) => errorResult(err))) as any;
|
|
1547
|
-
return makeThenable(result);
|
|
1548
|
-
} catch (err) {
|
|
1549
|
-
return makeThenable(errorResult(err)) as any;
|
|
1550
|
-
}
|
|
1551
|
-
};
|
|
1552
|
-
|
|
1553
|
-
const run: AnyPadroneProgram['run'] = (command, args) => {
|
|
1554
|
-
try {
|
|
1555
|
-
const commandObj =
|
|
1556
|
-
typeof command === 'string' ? findCommandByName(command, existingCommand.commands) : (command as AnyPadroneCommand);
|
|
1557
|
-
if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
|
|
1558
|
-
if (!commandObj.action) throw new RoutingError(`Command "${commandObj.path}" has no action`, { command: commandObj.path });
|
|
1559
|
-
|
|
1560
|
-
const state: Record<string, unknown> = {};
|
|
1561
|
-
const executeCtx: PluginExecuteContext = { command: commandObj, args, state };
|
|
1562
|
-
|
|
1563
|
-
const coreExecute = (): PluginExecuteResult => {
|
|
1564
|
-
const result = commandObj.action!(executeCtx.args as any, createActionContext(commandObj));
|
|
1565
|
-
return { result };
|
|
1566
|
-
};
|
|
1567
|
-
|
|
1568
|
-
const commandObjPlugins = collectPlugins(commandObj);
|
|
1569
|
-
const executedOrPromise = runPluginChain('execute', commandObjPlugins, executeCtx, coreExecute);
|
|
1570
|
-
|
|
1571
|
-
const toResult = (e: PluginExecuteResult) =>
|
|
1572
|
-
withDrain({
|
|
1573
|
-
command: commandObj as any,
|
|
1574
|
-
args: args as any,
|
|
1575
|
-
result: e.result,
|
|
1576
|
-
});
|
|
1577
|
-
|
|
1578
|
-
if (executedOrPromise instanceof Promise) {
|
|
1579
|
-
return executedOrPromise.then(toResult).catch((err: unknown) => errorResult(err, { command: commandObj, args })) as any;
|
|
1580
|
-
}
|
|
1581
|
-
return toResult(executedOrPromise);
|
|
1582
|
-
} catch (err) {
|
|
1583
|
-
return errorResult(err) as any;
|
|
1584
|
-
}
|
|
1585
|
-
};
|
|
1586
|
-
|
|
1587
|
-
const tool: AnyPadroneProgram['tool'] = () => {
|
|
1588
|
-
resolveAllCommands(existingCommand);
|
|
1589
|
-
const helpText = generateHelp(existingCommand, undefined, { format: 'text' });
|
|
1590
|
-
|
|
1591
|
-
const description = `Run a command. Pass the full command string including arguments. Use "help <command>" for detailed usage.\n\n${helpText}`;
|
|
1592
|
-
|
|
1593
|
-
return {
|
|
1594
|
-
type: 'function',
|
|
1595
|
-
name: existingCommand.name,
|
|
1596
|
-
strict: true,
|
|
1597
|
-
title: existingCommand.description,
|
|
1598
|
-
description,
|
|
1599
|
-
inputExamples: [{ input: { command: '<command> [positionals...] [arguments...]' } }],
|
|
1600
|
-
inputSchema: {
|
|
1601
|
-
[Symbol.for('vercel.ai.schema') as keyof Schema & symbol]: true,
|
|
1602
|
-
jsonSchema: {
|
|
1603
|
-
type: 'object',
|
|
1604
|
-
properties: { command: { type: 'string' } },
|
|
1605
|
-
additionalProperties: false,
|
|
1606
|
-
},
|
|
1607
|
-
_type: undefined as unknown as { command: string },
|
|
1608
|
-
validate: (value) => {
|
|
1609
|
-
const command = (value as any)?.command;
|
|
1610
|
-
if (typeof command === 'string') return { success: true, value: { command } };
|
|
1611
|
-
return { success: false, error: new Error('Expected an object with command property as string.') };
|
|
1612
|
-
},
|
|
1613
|
-
} satisfies Schema<{ command: string }> as Schema<{ command: string }>,
|
|
1614
|
-
needsApproval: async (input) => {
|
|
1615
|
-
const parsed = await parse(input.command);
|
|
1616
|
-
if (typeof parsed.command.needsApproval === 'function') return parsed.command.needsApproval(parsed.args);
|
|
1617
|
-
if (parsed.command.needsApproval != null) return !!parsed.command.needsApproval;
|
|
1618
|
-
return !!parsed.command.mutation;
|
|
1619
|
-
},
|
|
1620
|
-
execute: async (input) => {
|
|
1621
|
-
const output: string[] = [];
|
|
1622
|
-
const errors: string[] = [];
|
|
1623
|
-
const result = await evalCommand(input.command, {
|
|
1624
|
-
autoOutput: false,
|
|
1625
|
-
runtime: {
|
|
1626
|
-
output: (...args) => output.push(args.map(String).join(' ')),
|
|
1627
|
-
error: (text) => errors.push(text),
|
|
1628
|
-
interactive: 'unsupported',
|
|
1629
|
-
format: 'text',
|
|
1630
|
-
},
|
|
1631
|
-
});
|
|
1632
|
-
return { result: result.result, logs: output.join('\n'), error: errors.join('\n') };
|
|
1633
|
-
},
|
|
1634
|
-
};
|
|
1635
|
-
};
|
|
1636
|
-
|
|
1637
|
-
const builder = {
|
|
1638
|
-
configure(config) {
|
|
1639
|
-
return createPadroneBuilder({ ...existingCommand, ...config }) as any;
|
|
1640
|
-
},
|
|
1641
|
-
runtime(runtimeConfig) {
|
|
1642
|
-
return createPadroneBuilder({ ...existingCommand, runtime: { ...existingCommand.runtime, ...runtimeConfig } }) as any;
|
|
1643
|
-
},
|
|
1644
|
-
async() {
|
|
1645
|
-
return createPadroneBuilder({ ...existingCommand, isAsync: true }) as any;
|
|
1646
|
-
},
|
|
1647
|
-
arguments(schema, meta) {
|
|
1648
|
-
// If schema is a function, call it with parent's arguments as base
|
|
1649
|
-
const resolvedArgs = typeof schema === 'function' ? schema(existingCommand.argsSchema as any) : schema;
|
|
1650
|
-
const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedArgs) || hasInteractiveConfig(meta);
|
|
1651
|
-
return createPadroneBuilder({ ...existingCommand, argsSchema: resolvedArgs, meta, isAsync }) as any;
|
|
1652
|
-
},
|
|
1653
|
-
configFile(file, schema) {
|
|
1654
|
-
const configFiles = file === undefined ? undefined : Array.isArray(file) ? file : [file];
|
|
1655
|
-
const resolvedConfig = typeof schema === 'function' ? schema(existingCommand.argsSchema) : (schema ?? existingCommand.argsSchema);
|
|
1656
|
-
const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedConfig);
|
|
1657
|
-
return createPadroneBuilder({ ...existingCommand, configFiles, configSchema: resolvedConfig as any, isAsync }) as any;
|
|
1658
|
-
},
|
|
1659
|
-
env(schema) {
|
|
1660
|
-
const resolvedEnv = typeof schema === 'function' ? schema(existingCommand.argsSchema) : schema;
|
|
1661
|
-
const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedEnv);
|
|
1662
|
-
return createPadroneBuilder({ ...existingCommand, envSchema: resolvedEnv as any, isAsync }) as any;
|
|
1663
|
-
},
|
|
1664
|
-
progress(config = true) {
|
|
1665
|
-
const progress = typeof config === 'boolean' || typeof config === 'string' ? config : { ...config };
|
|
1666
|
-
return createPadroneBuilder({ ...existingCommand, progress }) as any;
|
|
1667
|
-
},
|
|
1668
|
-
action(handler = noop) {
|
|
1669
|
-
const baseHandler = existingCommand.action ?? noop;
|
|
1670
|
-
return createPadroneBuilder({
|
|
1671
|
-
...existingCommand,
|
|
1672
|
-
action: (args: any, ctx: any) => (handler as any)(args, ctx, baseHandler),
|
|
1673
|
-
}) as any;
|
|
1674
|
-
},
|
|
1675
|
-
wrap(config) {
|
|
1676
|
-
const handler = createWrapHandler(config, existingCommand.argsSchema as any, existingCommand.meta?.positional);
|
|
1677
|
-
return createPadroneBuilder({ ...existingCommand, action: handler }) as any;
|
|
1678
|
-
},
|
|
1679
|
-
command(nameOrNames, builderFn) {
|
|
1680
|
-
// Extract name and aliases from the input
|
|
1681
|
-
const name = Array.isArray(nameOrNames) ? nameOrNames[0] : nameOrNames;
|
|
1682
|
-
const aliases = Array.isArray(nameOrNames) && nameOrNames.length > 1 ? (nameOrNames.slice(1) as string[]) : undefined;
|
|
1683
|
-
|
|
1684
|
-
// Check if a command with this name already exists (override case)
|
|
1685
|
-
const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
|
|
1686
|
-
|
|
1687
|
-
// For override case, resolve the existing lazy command first so the builder starts with full state
|
|
1688
|
-
if (existingSubcommand) resolveCommand(existingSubcommand);
|
|
1689
|
-
|
|
1690
|
-
const initialCommand: AnyPadroneCommand = existingSubcommand
|
|
1691
|
-
? { ...existingSubcommand, aliases: aliases ?? existingSubcommand.aliases, parent: existingCommand }
|
|
1692
|
-
: ({
|
|
1693
|
-
name,
|
|
1694
|
-
path: existingCommand.path ? `${existingCommand.path} ${name}` : name,
|
|
1695
|
-
aliases,
|
|
1696
|
-
parent: existingCommand,
|
|
1697
|
-
'~types': {} as any,
|
|
1698
|
-
} satisfies PadroneCommand);
|
|
1699
|
-
|
|
1700
|
-
// Lazy initialization: defer builderFn invocation until the command is actually needed
|
|
1701
|
-
if (builderFn) {
|
|
1702
|
-
const lazyCmd: AnyPadroneCommand = { ...initialCommand };
|
|
1703
|
-
(lazyCmd as any)[lazyResolver] = (target: AnyPadroneCommand) => {
|
|
1704
|
-
const builder = createPadroneBuilder(target);
|
|
1705
|
-
const commandObj = ((builderFn(builder as any) as unknown as typeof builder)?.[commandSymbol] as AnyPadroneCommand) ?? target;
|
|
1706
|
-
const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, commandObj) : commandObj;
|
|
1707
|
-
Object.assign(target, mergedCommandObj);
|
|
1708
|
-
};
|
|
1709
|
-
|
|
1710
|
-
const commands = existingCommand.commands || [];
|
|
1711
|
-
const existingIndex = commands.findIndex((c) => c.name === name);
|
|
1712
|
-
const updatedCommands =
|
|
1713
|
-
existingIndex >= 0
|
|
1714
|
-
? [...commands.slice(0, existingIndex), lazyCmd, ...commands.slice(existingIndex + 1)]
|
|
1715
|
-
: [...commands, lazyCmd];
|
|
1716
|
-
|
|
1717
|
-
return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
|
|
1718
|
-
}
|
|
1719
|
-
|
|
1720
|
-
// No builderFn: use the initial command as-is (no lazy resolution needed)
|
|
1721
|
-
const commands = existingCommand.commands || [];
|
|
1722
|
-
const existingIndex = commands.findIndex((c) => c.name === name);
|
|
1723
|
-
const updatedCommands =
|
|
1724
|
-
existingIndex >= 0
|
|
1725
|
-
? [...commands.slice(0, existingIndex), initialCommand, ...commands.slice(existingIndex + 1)]
|
|
1726
|
-
: [...commands, initialCommand];
|
|
1727
|
-
|
|
1728
|
-
return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
|
|
1729
|
-
},
|
|
1730
|
-
|
|
1731
|
-
mount(nameOrNames, program) {
|
|
1732
|
-
const name = Array.isArray(nameOrNames) ? nameOrNames[0] : nameOrNames;
|
|
1733
|
-
const aliases = Array.isArray(nameOrNames) && nameOrNames.length > 1 ? (nameOrNames.slice(1) as string[]) : undefined;
|
|
1734
|
-
|
|
1735
|
-
// Extract the underlying command from the program
|
|
1736
|
-
const programCommand = (program as any)[commandSymbol] as AnyPadroneCommand | undefined;
|
|
1737
|
-
if (!programCommand) throw new RoutingError('Cannot mount: not a valid Padrone program');
|
|
1738
|
-
|
|
1739
|
-
// Re-path the command tree under the new name
|
|
1740
|
-
const remounted = repathCommandTree(programCommand, name, existingCommand.path || '', existingCommand);
|
|
1741
|
-
remounted.aliases = aliases;
|
|
1742
|
-
|
|
1743
|
-
// Merge with existing command if one with the same name exists
|
|
1744
|
-
const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
|
|
1745
|
-
const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, remounted) : remounted;
|
|
1746
|
-
|
|
1747
|
-
const commands = existingCommand.commands || [];
|
|
1748
|
-
const existingIndex = commands.findIndex((c) => c.name === name);
|
|
1749
|
-
const updatedCommands =
|
|
1750
|
-
existingIndex >= 0
|
|
1751
|
-
? [...commands.slice(0, existingIndex), mergedCommandObj, ...commands.slice(existingIndex + 1)]
|
|
1752
|
-
: [...commands, mergedCommandObj];
|
|
1753
|
-
|
|
1754
|
-
return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
|
|
1755
|
-
},
|
|
1756
|
-
|
|
1757
|
-
use(plugin: PadronePlugin<any, any>) {
|
|
1758
|
-
return createPadroneBuilder({
|
|
1759
|
-
...existingCommand,
|
|
1760
|
-
plugins: [...(existingCommand.plugins ?? []), plugin],
|
|
1761
|
-
}) as any;
|
|
1762
|
-
},
|
|
1763
|
-
|
|
1764
|
-
updateCheck(config = {}) {
|
|
1765
|
-
return createPadroneBuilder({ ...existingCommand, updateCheck: config }) as any;
|
|
1766
|
-
},
|
|
1767
|
-
|
|
1768
|
-
run,
|
|
1769
|
-
find,
|
|
1770
|
-
parse,
|
|
1771
|
-
stringify,
|
|
1772
|
-
eval: evalCommand,
|
|
1773
|
-
cli,
|
|
1774
|
-
tool,
|
|
1775
|
-
|
|
1776
|
-
repl: replFn,
|
|
1777
|
-
|
|
1778
|
-
api() {
|
|
1779
|
-
resolveAllCommands(existingCommand);
|
|
1780
|
-
function buildApi(command: AnyPadroneCommand) {
|
|
1781
|
-
const runCommand = ((args) => run(command, args).result) as PadroneAPI<AnyPadroneCommand>;
|
|
1782
|
-
if (!command.commands) return runCommand;
|
|
1783
|
-
for (const cmd of command.commands) runCommand[cmd.name] = buildApi(cmd);
|
|
1784
|
-
return runCommand;
|
|
1785
|
-
}
|
|
1786
|
-
|
|
1787
|
-
return buildApi(existingCommand);
|
|
1788
|
-
},
|
|
1789
|
-
|
|
1790
|
-
help(command, prefs) {
|
|
1791
|
-
resolveAllCommands(existingCommand);
|
|
1792
|
-
const commandObj = !command
|
|
1793
|
-
? existingCommand
|
|
1794
|
-
: typeof command === 'string'
|
|
1795
|
-
? findCommandByName(command, existingCommand.commands)
|
|
1796
|
-
: (command as AnyPadroneCommand);
|
|
1797
|
-
if (!commandObj) throw new RoutingError(`Command "${command ?? ''}" not found`);
|
|
1798
|
-
const runtime = getCommandRuntime(existingCommand);
|
|
1799
|
-
return generateHelp(existingCommand, commandObj, {
|
|
1800
|
-
...prefs,
|
|
1801
|
-
format: prefs?.format ?? runtime.format,
|
|
1802
|
-
theme: prefs?.theme ?? runtime.theme,
|
|
1803
|
-
});
|
|
1804
|
-
},
|
|
1805
|
-
|
|
1806
|
-
async completion(shell) {
|
|
1807
|
-
resolveAllCommands(existingCommand);
|
|
1808
|
-
const { generateCompletionOutput } = await import('./completion.ts');
|
|
1809
|
-
return generateCompletionOutput(existingCommand, shell as ShellType | undefined);
|
|
1810
|
-
},
|
|
1811
|
-
|
|
1812
|
-
async mcp(prefs) {
|
|
1813
|
-
resolveAllCommands(existingCommand);
|
|
1814
|
-
const { startMcpServer } = await import('./mcp.ts');
|
|
1815
|
-
return startMcpServer(builder as any, existingCommand, evalCommand, prefs);
|
|
1816
|
-
},
|
|
1817
|
-
|
|
1818
|
-
async serve(prefs) {
|
|
1819
|
-
resolveAllCommands(existingCommand);
|
|
1820
|
-
const { startServeServer } = await import('./serve.ts');
|
|
1821
|
-
return startServeServer(builder as any, existingCommand, evalCommand, prefs);
|
|
1822
|
-
},
|
|
1823
|
-
|
|
1824
|
-
'~types': {} as any,
|
|
1825
|
-
|
|
1826
|
-
[commandSymbol]: existingCommand,
|
|
1827
|
-
} satisfies AnyPadroneProgram & { [commandSymbol]: AnyPadroneCommand } as any;
|
|
1828
|
-
return builder as TBuilder & { [commandSymbol]: AnyPadroneCommand };
|
|
1829
|
-
}
|