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/command-utils.ts
DELETED
|
@@ -1,882 +0,0 @@
|
|
|
1
|
-
import { extractSchemaMetadata, JSON_SCHEMA_OPTS } from './args.ts';
|
|
2
|
-
import { type PadroneProgressIndicator, type ResolvedPadroneRuntime, resolveRuntime } from './runtime.ts';
|
|
3
|
-
import type { Thenable } from './type-utils.ts';
|
|
4
|
-
import type {
|
|
5
|
-
AnyPadroneCommand,
|
|
6
|
-
PadronePlugin,
|
|
7
|
-
PadroneSchema,
|
|
8
|
-
PluginErrorContext,
|
|
9
|
-
PluginErrorResult,
|
|
10
|
-
PluginShutdownContext,
|
|
11
|
-
PluginStartContext,
|
|
12
|
-
} from './types.ts';
|
|
13
|
-
|
|
14
|
-
// ---------------------------------------------------------------------------
|
|
15
|
-
// Lazy command resolution
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
export const lazyResolver = Symbol('lazyResolver');
|
|
19
|
-
|
|
20
|
-
/** Resolves a lazy command in place by calling its stored resolver. No-op if already resolved. */
|
|
21
|
-
export function resolveCommand(cmd: AnyPadroneCommand): AnyPadroneCommand {
|
|
22
|
-
const resolver = (cmd as any)[lazyResolver];
|
|
23
|
-
if (resolver) {
|
|
24
|
-
delete (cmd as any)[lazyResolver];
|
|
25
|
-
resolver(cmd);
|
|
26
|
-
}
|
|
27
|
-
return cmd;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Recursively resolves a command and all its descendants. */
|
|
31
|
-
export function resolveAllCommands(cmd: AnyPadroneCommand): void {
|
|
32
|
-
resolveCommand(cmd);
|
|
33
|
-
if (cmd.commands) {
|
|
34
|
-
for (const sub of cmd.commands) resolveAllCommands(sub);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Checks whether a value is a Padrone program/builder. */
|
|
39
|
-
export function isPadroneProgram(value: unknown): value is object {
|
|
40
|
-
return !!value && typeof value === 'object' && commandSymbol in value;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** Extracts the underlying command from a program/builder and resolves the full command tree. */
|
|
44
|
-
export function getCommand(program: object): AnyPadroneCommand {
|
|
45
|
-
const cmd = commandSymbol in program ? ((program as any)[commandSymbol] as AnyPadroneCommand) : (program as AnyPadroneCommand);
|
|
46
|
-
resolveAllCommands(cmd);
|
|
47
|
-
return cmd;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Brands a schema as async, signaling that its `validate()` may return a Promise.
|
|
52
|
-
* When an async-branded schema is passed to `.arguments()`, `.configFile()`, or `.env()`,
|
|
53
|
-
* the command's `parse()` and `cli()` will return Promises.
|
|
54
|
-
*
|
|
55
|
-
* @example
|
|
56
|
-
* ```ts
|
|
57
|
-
* const schema = asyncSchema(z.object({
|
|
58
|
-
* name: z.string(),
|
|
59
|
-
* }).check(async (data) => {
|
|
60
|
-
* // async validation logic
|
|
61
|
-
* }));
|
|
62
|
-
*
|
|
63
|
-
* const program = createPadrone('app')
|
|
64
|
-
* .command('greet', (c) => c.arguments(schema).action((args) => args.name));
|
|
65
|
-
*
|
|
66
|
-
* // parse() now returns Promise<PadroneParseResult>
|
|
67
|
-
* const result = await program.parse('greet --name world');
|
|
68
|
-
* ```
|
|
69
|
-
*/
|
|
70
|
-
export function asyncSchema<T extends PadroneSchema>(schema: T): T & { '~async': true } {
|
|
71
|
-
return Object.assign(schema, { '~async': true as const });
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const commandSymbol = Symbol('padrone_command');
|
|
75
|
-
|
|
76
|
-
export const noop = <TRes>() => undefined as TRes;
|
|
77
|
-
|
|
78
|
-
/** Config keys that are merged when overriding a command. */
|
|
79
|
-
export const configKeys = [
|
|
80
|
-
'title',
|
|
81
|
-
'description',
|
|
82
|
-
'version',
|
|
83
|
-
'deprecated',
|
|
84
|
-
'hidden',
|
|
85
|
-
'mutation',
|
|
86
|
-
'needsApproval',
|
|
87
|
-
'autoOutput',
|
|
88
|
-
'updateCheck',
|
|
89
|
-
] as const;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Merges an existing command with an override.
|
|
93
|
-
* - Config fields are shallow-merged (new overrides old).
|
|
94
|
-
* - Action, arguments, meta, config schema, env schema are taken from the override if set.
|
|
95
|
-
* - Subcommands are recursively merged by name.
|
|
96
|
-
*/
|
|
97
|
-
export function mergeCommands(existing: AnyPadroneCommand, override: AnyPadroneCommand): AnyPadroneCommand {
|
|
98
|
-
resolveCommand(existing);
|
|
99
|
-
resolveCommand(override);
|
|
100
|
-
const merged: AnyPadroneCommand = { ...existing };
|
|
101
|
-
|
|
102
|
-
// Merge config fields
|
|
103
|
-
for (const key of configKeys) {
|
|
104
|
-
if (override[key] !== undefined) (merged as any)[key] = override[key];
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Override fields: take from override if explicitly set (not inherited from existing via spread)
|
|
108
|
-
if (override.action !== existing.action) merged.action = override.action;
|
|
109
|
-
if (override.argsSchema !== existing.argsSchema) merged.argsSchema = override.argsSchema;
|
|
110
|
-
if (override.meta !== existing.meta) merged.meta = override.meta;
|
|
111
|
-
if (override.configSchema !== existing.configSchema) merged.configSchema = override.configSchema;
|
|
112
|
-
if (override.envSchema !== existing.envSchema) merged.envSchema = override.envSchema;
|
|
113
|
-
if (override.configFiles !== existing.configFiles) merged.configFiles = override.configFiles;
|
|
114
|
-
if (override.isAsync !== existing.isAsync) merged.isAsync = override.isAsync || existing.isAsync;
|
|
115
|
-
if (override.runtime !== existing.runtime) merged.runtime = override.runtime;
|
|
116
|
-
if (override.plugins !== existing.plugins) merged.plugins = override.plugins;
|
|
117
|
-
if (override.aliases !== existing.aliases) merged.aliases = override.aliases;
|
|
118
|
-
if (override.progress !== existing.progress) merged.progress = override.progress;
|
|
119
|
-
|
|
120
|
-
// Recursively merge subcommands by name
|
|
121
|
-
if (override.commands) {
|
|
122
|
-
const baseCommands = [...(existing.commands || [])];
|
|
123
|
-
for (const overrideChild of override.commands) {
|
|
124
|
-
const existingIndex = baseCommands.findIndex((c) => c.name === overrideChild.name);
|
|
125
|
-
if (existingIndex >= 0) {
|
|
126
|
-
baseCommands[existingIndex] = mergeCommands(baseCommands[existingIndex]!, overrideChild);
|
|
127
|
-
} else {
|
|
128
|
-
baseCommands.push(overrideChild);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
merged.commands = baseCommands;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
return merged;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
/**
|
|
138
|
-
* Maps over a value that may or may not be a Promise.
|
|
139
|
-
* If the value is a Promise, chains with `.then()`. Otherwise, calls the function synchronously.
|
|
140
|
-
* This preserves sync behavior for sync schemas and async behavior for async schemas.
|
|
141
|
-
*/
|
|
142
|
-
export function thenMaybe<T, U>(value: T | Promise<T>, fn: (v: T) => U | Promise<U>): U | Promise<U> {
|
|
143
|
-
if (value instanceof Promise) return value.then(fn);
|
|
144
|
-
return fn(value);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Makes a sync result object thenable by adding `.then()`, `.catch()`, and `.finally()` methods.
|
|
149
|
-
* If the value is already a Promise, returns it as-is.
|
|
150
|
-
* This allows users to write `await program.cli()` or `program.cli().then(...)` regardless of sync/async.
|
|
151
|
-
*
|
|
152
|
-
* The `.then()` resolves with a plain copy (without thenable methods) to avoid infinite
|
|
153
|
-
* recursive unwrapping by the Promise resolution algorithm.
|
|
154
|
-
*/
|
|
155
|
-
export function makeThenable<T>(value: T | Promise<T>): Thenable<T> {
|
|
156
|
-
if (value instanceof Promise) return value as any;
|
|
157
|
-
if (value !== null && typeof value === 'object' && !('then' in value)) {
|
|
158
|
-
const toPlain = () => {
|
|
159
|
-
const plain = { ...value } as any;
|
|
160
|
-
delete plain.then;
|
|
161
|
-
delete plain.catch;
|
|
162
|
-
delete plain.finally;
|
|
163
|
-
return plain as T;
|
|
164
|
-
};
|
|
165
|
-
// biome-ignore lint/suspicious/noThenProperty: intentional thenable shim for sync results
|
|
166
|
-
(value as any).then = (onfulfilled?: (v: T) => any, onrejected?: (reason: any) => any) => {
|
|
167
|
-
try {
|
|
168
|
-
const result = onfulfilled ? onfulfilled(toPlain()) : toPlain();
|
|
169
|
-
return Promise.resolve(result);
|
|
170
|
-
} catch (err) {
|
|
171
|
-
if (onrejected) return Promise.resolve(onrejected(err));
|
|
172
|
-
return Promise.reject(err);
|
|
173
|
-
}
|
|
174
|
-
};
|
|
175
|
-
(value as any).catch = (onrejected?: (reason: any) => any) => (value as any).then(undefined, onrejected);
|
|
176
|
-
(value as any).finally = (onfinally?: () => void) =>
|
|
177
|
-
(value as any).then(
|
|
178
|
-
(v: any) => {
|
|
179
|
-
onfinally?.();
|
|
180
|
-
return v;
|
|
181
|
-
},
|
|
182
|
-
(err: any) => {
|
|
183
|
-
onfinally?.();
|
|
184
|
-
throw err;
|
|
185
|
-
},
|
|
186
|
-
);
|
|
187
|
-
}
|
|
188
|
-
return value as any;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
/**
|
|
192
|
-
* Wraps a Promise to include a `drain()` method at the top level.
|
|
193
|
-
* This allows `await promise.drain()` without first awaiting the promise.
|
|
194
|
-
* Since cli/eval never reject, this just delegates to the resolved result's `drain()`.
|
|
195
|
-
*/
|
|
196
|
-
export function withPromiseDrain<T extends Promise<any>>(promise: T): T & { drain: () => Promise<any> } {
|
|
197
|
-
(promise as any).drain = async () => {
|
|
198
|
-
const resolved = await promise;
|
|
199
|
-
return resolved.drain();
|
|
200
|
-
};
|
|
201
|
-
return promise as any;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
export function isIterator(value: unknown): value is Iterator<unknown> {
|
|
205
|
-
return typeof value === 'object' && value !== null && Symbol.iterator in value && typeof (value as any)[Symbol.iterator] === 'function';
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
export function isAsyncIterator(value: unknown): value is AsyncIterator<unknown> {
|
|
209
|
-
return (
|
|
210
|
-
typeof value === 'object' &&
|
|
211
|
-
value !== null &&
|
|
212
|
-
Symbol.asyncIterator in value &&
|
|
213
|
-
typeof (value as any)[Symbol.asyncIterator] === 'function'
|
|
214
|
-
);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Writes a command's return value to output, handling promises, iterators, and async iterators.
|
|
219
|
-
* Values are passed directly to the output function without stringification —
|
|
220
|
-
* runtimes like Node/Bun already format objects via console.log.
|
|
221
|
-
* Returns void or a Promise depending on whether async consumption is needed.
|
|
222
|
-
*/
|
|
223
|
-
export function outputValue(value: unknown, output: (...args: unknown[]) => void): void | Promise<void> {
|
|
224
|
-
if (value == null) return;
|
|
225
|
-
|
|
226
|
-
// Async iterator — consume and output each yielded value
|
|
227
|
-
if (isAsyncIterator(value)) {
|
|
228
|
-
return (async () => {
|
|
229
|
-
const iter = (value as any)[Symbol.asyncIterator]();
|
|
230
|
-
while (true) {
|
|
231
|
-
const { done, value: item } = await iter.next();
|
|
232
|
-
if (done) break;
|
|
233
|
-
if (item != null) output(item);
|
|
234
|
-
}
|
|
235
|
-
})();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Sync iterator (but not a plain string/array which also have Symbol.iterator)
|
|
239
|
-
if (typeof value !== 'string' && !Array.isArray(value) && isIterator(value)) {
|
|
240
|
-
const iter = (value as any)[Symbol.iterator]();
|
|
241
|
-
while (true) {
|
|
242
|
-
const { done, value: item } = iter.next();
|
|
243
|
-
if (done) break;
|
|
244
|
-
if (item != null) output(item);
|
|
245
|
-
}
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Promise — await then output
|
|
250
|
-
if (value instanceof Promise) {
|
|
251
|
-
return value.then((resolved) => outputValue(resolved, output));
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// Pass value directly — runtime handles formatting
|
|
255
|
-
output(value);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* Resolves a result value by unwrapping Promises and collecting iterables into arrays.
|
|
260
|
-
* This is the runtime counterpart of the `Drained<T>` type.
|
|
261
|
-
*/
|
|
262
|
-
export async function drainValue(value: unknown): Promise<unknown> {
|
|
263
|
-
// Unwrap promises first
|
|
264
|
-
if (value instanceof Promise) {
|
|
265
|
-
return drainValue(await value);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Async iterator — collect into array
|
|
269
|
-
if (isAsyncIterator(value)) {
|
|
270
|
-
const items: unknown[] = [];
|
|
271
|
-
const iter = (value as any)[Symbol.asyncIterator]();
|
|
272
|
-
while (true) {
|
|
273
|
-
const { done, value: item } = await iter.next();
|
|
274
|
-
if (done) break;
|
|
275
|
-
items.push(item);
|
|
276
|
-
}
|
|
277
|
-
return items;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Sync iterator (but not string/array)
|
|
281
|
-
if (typeof value !== 'string' && !Array.isArray(value) && isIterator(value)) {
|
|
282
|
-
const items: unknown[] = [];
|
|
283
|
-
const iter = (value as any)[Symbol.iterator]();
|
|
284
|
-
while (true) {
|
|
285
|
-
const { done, value: item } = iter.next();
|
|
286
|
-
if (done) break;
|
|
287
|
-
items.push(item);
|
|
288
|
-
}
|
|
289
|
-
return items;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return value;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Attaches a `drain()` method to a command result object.
|
|
297
|
-
* If the result has an `error` field, `drain()` returns `{ error }`.
|
|
298
|
-
* Otherwise, resolves the result (unwrapping Promises, collecting iterables), catches errors,
|
|
299
|
-
* and returns a discriminated union `{ value } | { error }` that never throws.
|
|
300
|
-
*/
|
|
301
|
-
export function withDrain<T extends Record<string, unknown>>(obj: T): T & { drain: () => Promise<any> } {
|
|
302
|
-
(obj as any).drain = async () => {
|
|
303
|
-
if ('error' in obj && obj.error !== undefined) {
|
|
304
|
-
return { error: obj.error };
|
|
305
|
-
}
|
|
306
|
-
try {
|
|
307
|
-
const value = await drainValue(obj.result);
|
|
308
|
-
return { value };
|
|
309
|
-
} catch (err) {
|
|
310
|
-
return { error: err };
|
|
311
|
-
}
|
|
312
|
-
};
|
|
313
|
-
return obj as any;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Creates an error command result with a `drain()` that returns the error.
|
|
318
|
-
*/
|
|
319
|
-
export function errorResult(error: unknown, partial?: { command?: unknown; args?: unknown; argsResult?: unknown }) {
|
|
320
|
-
return withDrain({
|
|
321
|
-
error,
|
|
322
|
-
result: undefined,
|
|
323
|
-
command: partial?.command,
|
|
324
|
-
args: partial?.args,
|
|
325
|
-
argsResult: partial?.argsResult,
|
|
326
|
-
});
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Deduplicates plugins by `id`. When multiple plugins share the same `id`,
|
|
331
|
-
* only the last one in the array is kept. Plugins without an `id` are always kept.
|
|
332
|
-
*/
|
|
333
|
-
function deduplicatePlugins(plugins: PadronePlugin<any, any>[]): PadronePlugin<any, any>[] {
|
|
334
|
-
// Fast path: no ids at all
|
|
335
|
-
if (!plugins.some((p) => p.id)) return plugins;
|
|
336
|
-
|
|
337
|
-
// Find the last index for each id
|
|
338
|
-
const lastIndex = new Map<string, number>();
|
|
339
|
-
for (let i = 0; i < plugins.length; i++) {
|
|
340
|
-
const id = plugins[i]!.id;
|
|
341
|
-
if (id) lastIndex.set(id, i);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return plugins.filter((p, i) => !p.id || lastIndex.get(p.id) === i);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Runs a plugin chain for a given phase using the onion/middleware pattern.
|
|
349
|
-
* Plugins are sorted by `order` (ascending, stable), then composed so that
|
|
350
|
-
* the first plugin in sorted order is the outermost wrapper.
|
|
351
|
-
* If no plugins handle this phase, `core` is called directly.
|
|
352
|
-
*/
|
|
353
|
-
export function runPluginChain<TCtx, TResult>(
|
|
354
|
-
phase: 'start' | 'parse' | 'validate' | 'execute' | 'error' | 'shutdown',
|
|
355
|
-
plugins: PadronePlugin<any, any>[],
|
|
356
|
-
ctx: TCtx,
|
|
357
|
-
core: () => TResult | Promise<TResult>,
|
|
358
|
-
): TResult | Promise<TResult> {
|
|
359
|
-
// Deduplicate by id (last wins), then filter to plugins that have a handler for this phase
|
|
360
|
-
const deduped = deduplicatePlugins(plugins);
|
|
361
|
-
const phasePlugins = deduped.filter((p) => p[phase]);
|
|
362
|
-
if (phasePlugins.length === 0) return core();
|
|
363
|
-
|
|
364
|
-
// Stable sort by order (lower = outermost). Equal order preserves registration order.
|
|
365
|
-
phasePlugins.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
366
|
-
|
|
367
|
-
// Build chain from inside out: last plugin wraps core, first plugin is outermost
|
|
368
|
-
let next = core;
|
|
369
|
-
for (let i = phasePlugins.length - 1; i >= 0; i--) {
|
|
370
|
-
const handler = phasePlugins[i]![phase]! as unknown as (
|
|
371
|
-
ctx: TCtx,
|
|
372
|
-
next: () => TResult | Promise<TResult>,
|
|
373
|
-
) => TResult | Promise<TResult>;
|
|
374
|
-
const prevNext = next;
|
|
375
|
-
next = () => handler(ctx, prevNext);
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
return next();
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Wraps a pipeline with start → error → shutdown lifecycle hooks.
|
|
383
|
-
* - `start` plugins wrap the pipeline (onion pattern, root plugins only).
|
|
384
|
-
* - On error: `error` plugins run (can transform/suppress the error).
|
|
385
|
-
* - Always: `shutdown` plugins run (success or failure).
|
|
386
|
-
*/
|
|
387
|
-
export function wrapWithLifecycle<T>(
|
|
388
|
-
plugins: PadronePlugin<any, any>[],
|
|
389
|
-
command: AnyPadroneCommand,
|
|
390
|
-
state: Record<string, unknown>,
|
|
391
|
-
input: string | undefined,
|
|
392
|
-
pipeline: () => T | Promise<T>,
|
|
393
|
-
wrapErrorResult?: (result: unknown) => T,
|
|
394
|
-
): T | Promise<T> {
|
|
395
|
-
const hasStart = plugins.some((p) => p.start);
|
|
396
|
-
const hasError = plugins.some((p) => p.error);
|
|
397
|
-
const hasShutdown = plugins.some((p) => p.shutdown);
|
|
398
|
-
|
|
399
|
-
const cleanupProgress = (error?: unknown, result?: unknown) => {
|
|
400
|
-
const indicator = state._progress as PadroneProgressIndicator | undefined;
|
|
401
|
-
if (indicator) {
|
|
402
|
-
// If there's no progress config (lazy/manual indicator), just stop it silently
|
|
403
|
-
const hasProgressConfig = '_progressMsg' in state;
|
|
404
|
-
if (!hasProgressConfig) {
|
|
405
|
-
indicator.stop();
|
|
406
|
-
} else if (error !== undefined) {
|
|
407
|
-
const fallback = error instanceof Error ? error.message : String(error);
|
|
408
|
-
const { message: errorMsg, indicator: errorIcon } = resolveProgressMessage(state._progressError, error, fallback);
|
|
409
|
-
indicator.fail(errorMsg, errorIcon !== undefined ? { indicator: errorIcon } : undefined);
|
|
410
|
-
} else {
|
|
411
|
-
const { message: successMsg, indicator: successIcon } = resolveProgressMessage(state._progressSuccess, result);
|
|
412
|
-
indicator.succeed(successMsg, successIcon !== undefined ? { indicator: successIcon } : undefined);
|
|
413
|
-
}
|
|
414
|
-
(state._restoreOutput as (() => void) | undefined)?.();
|
|
415
|
-
state._progress = undefined;
|
|
416
|
-
state._restoreOutput = undefined;
|
|
417
|
-
}
|
|
418
|
-
};
|
|
419
|
-
|
|
420
|
-
// Fast path: no lifecycle plugins — still need progress cleanup
|
|
421
|
-
if (!hasStart && !hasError && !hasShutdown) {
|
|
422
|
-
let result: T | Promise<T>;
|
|
423
|
-
try {
|
|
424
|
-
result = pipeline();
|
|
425
|
-
} catch (e) {
|
|
426
|
-
cleanupProgress(e);
|
|
427
|
-
throw e;
|
|
428
|
-
}
|
|
429
|
-
if (result instanceof Promise) {
|
|
430
|
-
return result.then(
|
|
431
|
-
(r) => {
|
|
432
|
-
cleanupProgress();
|
|
433
|
-
return r;
|
|
434
|
-
},
|
|
435
|
-
(e) => {
|
|
436
|
-
cleanupProgress(e);
|
|
437
|
-
throw e;
|
|
438
|
-
},
|
|
439
|
-
);
|
|
440
|
-
}
|
|
441
|
-
cleanupProgress();
|
|
442
|
-
return result;
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
const runShutdown = (error?: unknown, result?: unknown) => {
|
|
446
|
-
cleanupProgress(error);
|
|
447
|
-
if (!hasShutdown) return;
|
|
448
|
-
const ctx: PluginShutdownContext = { command, state, error, result };
|
|
449
|
-
return runPluginChain('shutdown', plugins, ctx, () => {});
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const runError = (error: unknown): T | Promise<T> => {
|
|
453
|
-
if (!hasError) {
|
|
454
|
-
const s = runShutdown(error);
|
|
455
|
-
if (s instanceof Promise)
|
|
456
|
-
return s.then(() => {
|
|
457
|
-
throw error;
|
|
458
|
-
});
|
|
459
|
-
throw error;
|
|
460
|
-
}
|
|
461
|
-
const ctx: PluginErrorContext = { command, state, error };
|
|
462
|
-
const errorResult = runPluginChain('error', plugins, ctx, (): PluginErrorResult => ({ error }));
|
|
463
|
-
return thenMaybe(errorResult, (er) => {
|
|
464
|
-
if (er.error !== undefined) {
|
|
465
|
-
const s = runShutdown(er.error);
|
|
466
|
-
return thenMaybe(s as void | Promise<void>, () => {
|
|
467
|
-
throw er.error;
|
|
468
|
-
});
|
|
469
|
-
}
|
|
470
|
-
const wrapped = wrapErrorResult ? wrapErrorResult(er.result) : (er.result as T);
|
|
471
|
-
const s = runShutdown(undefined, wrapped);
|
|
472
|
-
return thenMaybe(s as void | Promise<void>, () => wrapped);
|
|
473
|
-
});
|
|
474
|
-
};
|
|
475
|
-
|
|
476
|
-
const handleSuccess = (result: T): T | Promise<T> => {
|
|
477
|
-
const s = runShutdown(undefined, result);
|
|
478
|
-
if (s instanceof Promise) return s.then(() => result);
|
|
479
|
-
return result;
|
|
480
|
-
};
|
|
481
|
-
|
|
482
|
-
// Run start phase wrapping the pipeline
|
|
483
|
-
const startCtx: PluginStartContext = { command, state, input };
|
|
484
|
-
let result: T | Promise<T>;
|
|
485
|
-
try {
|
|
486
|
-
result = (hasStart ? runPluginChain('start', plugins, startCtx, pipeline) : pipeline()) as T | Promise<T>;
|
|
487
|
-
} catch (e) {
|
|
488
|
-
return runError(e);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
if (result instanceof Promise) {
|
|
492
|
-
return result.then(handleSuccess, runError);
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
return handleSuccess(result);
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Resolves the runtime for a command by walking up the parent chain.
|
|
500
|
-
* Returns a fully resolved runtime with all defaults filled in.
|
|
501
|
-
*/
|
|
502
|
-
export function getCommandRuntime(cmd: AnyPadroneCommand): ResolvedPadroneRuntime {
|
|
503
|
-
let current: AnyPadroneCommand | undefined = cmd;
|
|
504
|
-
while (current) {
|
|
505
|
-
if (current.runtime) return resolveRuntime(current.runtime);
|
|
506
|
-
current = current.parent;
|
|
507
|
-
}
|
|
508
|
-
return resolveRuntime();
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
/** No-op progress indicator returned when the runtime doesn't provide a `progress` factory. */
|
|
512
|
-
const noopIndicator: PadroneProgressIndicator = {
|
|
513
|
-
update() {},
|
|
514
|
-
succeed() {},
|
|
515
|
-
fail() {},
|
|
516
|
-
stop() {},
|
|
517
|
-
pause() {},
|
|
518
|
-
resume() {},
|
|
519
|
-
};
|
|
520
|
-
|
|
521
|
-
/** Creates a progress indicator from the runtime, or returns a no-op if unavailable. */
|
|
522
|
-
export function createProgress(
|
|
523
|
-
runtime: ResolvedPadroneRuntime,
|
|
524
|
-
message: string,
|
|
525
|
-
options?: import('./runtime.ts').PadroneProgressOptions,
|
|
526
|
-
): PadroneProgressIndicator {
|
|
527
|
-
return runtime.progress?.(message, options) ?? noopIndicator;
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Creates a lazy progress indicator that defers real indicator creation until first use.
|
|
532
|
-
* This allows `ctx.progress` to work even without `.progress()` config, as long as the
|
|
533
|
-
* runtime provides a progress factory.
|
|
534
|
-
*/
|
|
535
|
-
export function createLazyIndicator(runtime: ResolvedPadroneRuntime, state: Record<string, unknown>): PadroneProgressIndicator {
|
|
536
|
-
if (!runtime.progress) return noopIndicator;
|
|
537
|
-
|
|
538
|
-
let real: PadroneProgressIndicator | undefined;
|
|
539
|
-
const ensure = (message?: string) => {
|
|
540
|
-
if (!real) {
|
|
541
|
-
real = runtime.progress!(message ?? '', undefined);
|
|
542
|
-
state._progress = real;
|
|
543
|
-
}
|
|
544
|
-
return real;
|
|
545
|
-
};
|
|
546
|
-
|
|
547
|
-
return {
|
|
548
|
-
update(msg) {
|
|
549
|
-
ensure(msg).update(msg);
|
|
550
|
-
},
|
|
551
|
-
succeed(msg) {
|
|
552
|
-
if (real) real.succeed(msg);
|
|
553
|
-
},
|
|
554
|
-
fail(msg) {
|
|
555
|
-
if (real) real.fail(msg);
|
|
556
|
-
},
|
|
557
|
-
stop() {
|
|
558
|
-
if (real) real.stop();
|
|
559
|
-
},
|
|
560
|
-
pause() {
|
|
561
|
-
if (real) real.pause();
|
|
562
|
-
},
|
|
563
|
-
resume() {
|
|
564
|
-
if (real) real.resume();
|
|
565
|
-
},
|
|
566
|
-
};
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Resolves a progress message field (static or callback) into the arguments for succeed/fail.
|
|
571
|
-
* Handles string, null, `{ message, indicator }` objects, and callback functions.
|
|
572
|
-
*/
|
|
573
|
-
export function resolveProgressMessage(
|
|
574
|
-
field: unknown,
|
|
575
|
-
value: unknown,
|
|
576
|
-
fallback?: string,
|
|
577
|
-
): { message: string | null | undefined; indicator?: string } {
|
|
578
|
-
const raw = typeof field === 'function' ? (field as (v: unknown) => unknown)(value) : field;
|
|
579
|
-
if (raw === undefined) return { message: fallback };
|
|
580
|
-
if (raw === null || typeof raw === 'string') return { message: raw };
|
|
581
|
-
if (typeof raw === 'object' && raw !== null) {
|
|
582
|
-
const obj = raw as { message?: string | null; indicator?: string };
|
|
583
|
-
return { message: obj.message, indicator: obj.indicator };
|
|
584
|
-
}
|
|
585
|
-
return { message: fallback };
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
export { noopIndicator };
|
|
589
|
-
|
|
590
|
-
export function isAsyncBranded(schema: unknown): boolean {
|
|
591
|
-
return !!schema && typeof schema === 'object' && '~async' in schema && (schema as any)['~async'] === true;
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
export function hasInteractiveConfig(meta: unknown): boolean {
|
|
595
|
-
if (!meta || typeof meta !== 'object') return false;
|
|
596
|
-
const m = meta as Record<string, unknown>;
|
|
597
|
-
return m.interactive === true || Array.isArray(m.interactive) || m.optionalInteractive === true || Array.isArray(m.optionalInteractive);
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
export function warnIfUnexpectedAsync<T>(value: T, command: AnyPadroneCommand): T {
|
|
601
|
-
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return value;
|
|
602
|
-
if (value instanceof Promise && !command.isAsync) {
|
|
603
|
-
const runtime = getCommandRuntime(command);
|
|
604
|
-
runtime.error(
|
|
605
|
-
`[padrone] Command "${command.path || command.name}" returned a Promise from validation, ` +
|
|
606
|
-
`but was not marked as async. Use \`.async()\` on the builder or \`asyncSchema()\` to brand your schema. ` +
|
|
607
|
-
`Without this, TypeScript will infer a sync return type and the result will be a Promise at runtime.`,
|
|
608
|
-
);
|
|
609
|
-
}
|
|
610
|
-
return value;
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
/**
|
|
614
|
-
* Recursively re-paths a command tree under a new parent path, updating parent references.
|
|
615
|
-
*/
|
|
616
|
-
export function repathCommandTree(
|
|
617
|
-
cmd: AnyPadroneCommand,
|
|
618
|
-
newName: string,
|
|
619
|
-
parentPath: string,
|
|
620
|
-
parent: AnyPadroneCommand,
|
|
621
|
-
): AnyPadroneCommand {
|
|
622
|
-
resolveCommand(cmd);
|
|
623
|
-
const newPath = parentPath ? `${parentPath} ${newName}` : newName;
|
|
624
|
-
const remounted: AnyPadroneCommand = {
|
|
625
|
-
...cmd,
|
|
626
|
-
name: newName,
|
|
627
|
-
path: newPath,
|
|
628
|
-
parent,
|
|
629
|
-
version: undefined,
|
|
630
|
-
};
|
|
631
|
-
|
|
632
|
-
if (cmd.commands?.length) {
|
|
633
|
-
remounted.commands = cmd.commands.map((child) => repathCommandTree(child, child.name, newPath, remounted));
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
return remounted;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
/**
|
|
640
|
-
* Builds a completer function for the REPL from the command tree.
|
|
641
|
-
* Completes command names, subcommand names, option names (--foo), and aliases (-f).
|
|
642
|
-
* Also includes dot-prefixed built-in REPL commands (.exit, .clear, .scope, .help, .history).
|
|
643
|
-
*/
|
|
644
|
-
export function buildReplCompleter(
|
|
645
|
-
rootCommand: AnyPadroneCommand,
|
|
646
|
-
builtins: {
|
|
647
|
-
inScope?: boolean;
|
|
648
|
-
},
|
|
649
|
-
): (line: string) => [string[], string] {
|
|
650
|
-
resolveAllCommands(rootCommand);
|
|
651
|
-
return (line: string): [string[], string] => {
|
|
652
|
-
const trimmed = line.trimStart();
|
|
653
|
-
const parts = trimmed.split(/\s+/);
|
|
654
|
-
const lastPart = parts[parts.length - 1] ?? '';
|
|
655
|
-
|
|
656
|
-
// If we're completing a dot-command
|
|
657
|
-
if (lastPart.startsWith('.')) {
|
|
658
|
-
const dotCmds = ['.exit', '.clear', '.help', '.history'];
|
|
659
|
-
if (rootCommand.commands?.some((c) => c.commands?.length) || builtins.inScope) dotCmds.push('.scope');
|
|
660
|
-
const hits = dotCmds.filter((c) => c.startsWith(lastPart));
|
|
661
|
-
return [hits.length ? hits : dotCmds, lastPart];
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
// If we're completing an option (starts with -)
|
|
665
|
-
if (lastPart.startsWith('-')) {
|
|
666
|
-
// Find which command we're in
|
|
667
|
-
const commandParts = parts.slice(0, -1).filter((p) => !p.startsWith('-'));
|
|
668
|
-
let targetCommand = rootCommand;
|
|
669
|
-
for (const part of commandParts) {
|
|
670
|
-
resolveCommand(targetCommand);
|
|
671
|
-
const sub = targetCommand.commands?.find((c) => c.name === part || c.aliases?.includes(part));
|
|
672
|
-
if (sub) {
|
|
673
|
-
resolveCommand(sub);
|
|
674
|
-
targetCommand = sub;
|
|
675
|
-
} else break;
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
// Get options for this command
|
|
679
|
-
const options: string[] = [];
|
|
680
|
-
if (targetCommand.argsSchema) {
|
|
681
|
-
try {
|
|
682
|
-
const argsMeta = targetCommand.meta?.fields;
|
|
683
|
-
const { flags, aliases } = extractSchemaMetadata(targetCommand.argsSchema, argsMeta, targetCommand.meta?.autoAlias);
|
|
684
|
-
const jsonSchema = targetCommand.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
|
|
685
|
-
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
686
|
-
for (const key of Object.keys(jsonSchema.properties)) {
|
|
687
|
-
options.push(`--${key}`);
|
|
688
|
-
}
|
|
689
|
-
for (const flag of Object.keys(flags)) {
|
|
690
|
-
options.push(`-${flag}`);
|
|
691
|
-
}
|
|
692
|
-
for (const alias of Object.keys(aliases)) {
|
|
693
|
-
options.push(`--${alias}`);
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
} catch {
|
|
697
|
-
// Ignore schema parsing errors
|
|
698
|
-
}
|
|
699
|
-
}
|
|
700
|
-
// Add global flags
|
|
701
|
-
options.push('--help', '-h');
|
|
702
|
-
|
|
703
|
-
const hits = options.filter((o) => o.startsWith(lastPart));
|
|
704
|
-
return [hits.length ? hits : options, lastPart];
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Completing command names
|
|
708
|
-
const commandParts = parts.filter((p) => !p.startsWith('-'));
|
|
709
|
-
// Walk into subcommands for all but the last token
|
|
710
|
-
let targetCommand = rootCommand;
|
|
711
|
-
for (let i = 0; i < commandParts.length - 1; i++) {
|
|
712
|
-
resolveCommand(targetCommand);
|
|
713
|
-
const sub = targetCommand.commands?.find((c) => c.name === commandParts[i] || c.aliases?.includes(commandParts[i]!));
|
|
714
|
-
if (sub) {
|
|
715
|
-
resolveCommand(sub);
|
|
716
|
-
targetCommand = sub;
|
|
717
|
-
} else break;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
const candidates: string[] = [];
|
|
721
|
-
|
|
722
|
-
// Add subcommand names and aliases
|
|
723
|
-
if (targetCommand.commands) {
|
|
724
|
-
for (const cmd of targetCommand.commands) {
|
|
725
|
-
if (!cmd.hidden) {
|
|
726
|
-
candidates.push(cmd.name);
|
|
727
|
-
if (cmd.aliases) candidates.push(...cmd.aliases);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
|
|
732
|
-
// Add dot-commands and `..` shorthand at the root level (relative to current scope)
|
|
733
|
-
if (targetCommand === rootCommand) {
|
|
734
|
-
candidates.push('.help', '.exit', '.clear', '.history');
|
|
735
|
-
if (rootCommand.commands?.some((c) => c.commands?.length) || builtins.inScope) candidates.push('.scope');
|
|
736
|
-
if (builtins.inScope) candidates.push('..');
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
const hits = candidates.filter((c) => c.startsWith(lastPart));
|
|
740
|
-
return [hits.length ? hits : candidates, lastPart];
|
|
741
|
-
};
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
/**
|
|
745
|
-
* Computes the Levenshtein edit distance between two strings.
|
|
746
|
-
*/
|
|
747
|
-
function levenshtein(a: string, b: string): number {
|
|
748
|
-
const m = a.length;
|
|
749
|
-
const n = b.length;
|
|
750
|
-
const dp: number[] = Array.from({ length: n + 1 }, (_, i) => i);
|
|
751
|
-
|
|
752
|
-
for (let i = 1; i <= m; i++) {
|
|
753
|
-
let prev = dp[0]!;
|
|
754
|
-
dp[0] = i;
|
|
755
|
-
for (let j = 1; j <= n; j++) {
|
|
756
|
-
const temp = dp[j]!;
|
|
757
|
-
dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j]!, dp[j - 1]!);
|
|
758
|
-
prev = temp;
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
|
|
762
|
-
return dp[n]!;
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
/**
|
|
766
|
-
* Finds the closest match from a list of candidates using Levenshtein distance.
|
|
767
|
-
* Returns the suggestion string (e.g. 'Did you mean "deploy"?') or empty string if no good match.
|
|
768
|
-
* Threshold: distance must be at most 40% of the longer string's length (min 1, max 3).
|
|
769
|
-
*/
|
|
770
|
-
export function suggestSimilar(input: string, candidates: string[]): string {
|
|
771
|
-
if (candidates.length === 0) return '';
|
|
772
|
-
|
|
773
|
-
const lower = input.toLowerCase();
|
|
774
|
-
let bestDist = Infinity;
|
|
775
|
-
let bestMatch = '';
|
|
776
|
-
|
|
777
|
-
for (const candidate of candidates) {
|
|
778
|
-
const dist = levenshtein(lower, candidate.toLowerCase());
|
|
779
|
-
if (dist < bestDist) {
|
|
780
|
-
bestDist = dist;
|
|
781
|
-
bestMatch = candidate;
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
const maxLen = Math.max(input.length, bestMatch.length);
|
|
786
|
-
const threshold = Math.min(3, Math.max(1, Math.ceil(maxLen * 0.4)));
|
|
787
|
-
|
|
788
|
-
if (bestDist > 0 && bestDist <= threshold) {
|
|
789
|
-
return `Did you mean "${bestMatch}"?`;
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
return '';
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
export function findCommandByName(name: string, commands?: AnyPadroneCommand[]): AnyPadroneCommand | undefined {
|
|
796
|
-
if (!commands) return undefined;
|
|
797
|
-
|
|
798
|
-
const foundByName = commands.find((cmd) => cmd.name === name);
|
|
799
|
-
if (foundByName) return resolveCommand(foundByName);
|
|
800
|
-
|
|
801
|
-
// Check for aliases
|
|
802
|
-
const foundByAlias = commands.find((cmd) => cmd.aliases?.includes(name));
|
|
803
|
-
if (foundByAlias) return resolveCommand(foundByAlias);
|
|
804
|
-
|
|
805
|
-
for (const cmd of commands) {
|
|
806
|
-
if (name.startsWith(`${cmd.name} `)) {
|
|
807
|
-
resolveCommand(cmd);
|
|
808
|
-
if (cmd.commands) {
|
|
809
|
-
const subCommandName = name.slice(cmd.name.length + 1);
|
|
810
|
-
const subCommand = findCommandByName(subCommandName, cmd.commands);
|
|
811
|
-
if (subCommand) return subCommand;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
// Check aliases for nested commands
|
|
815
|
-
if (cmd.aliases) {
|
|
816
|
-
for (const alias of cmd.aliases) {
|
|
817
|
-
if (name.startsWith(`${alias} `)) {
|
|
818
|
-
resolveCommand(cmd);
|
|
819
|
-
if (cmd.commands) {
|
|
820
|
-
const subCommandName = name.slice(alias.length + 1);
|
|
821
|
-
const subCommand = findCommandByName(subCommandName, cmd.commands);
|
|
822
|
-
if (subCommand) return subCommand;
|
|
823
|
-
}
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
}
|
|
828
|
-
return undefined;
|
|
829
|
-
}
|
|
830
|
-
|
|
831
|
-
// ---------------------------------------------------------------------------
|
|
832
|
-
// Shared utilities for MCP and serve
|
|
833
|
-
// ---------------------------------------------------------------------------
|
|
834
|
-
|
|
835
|
-
export type CollectedEndpoint = { name: string; command: AnyPadroneCommand };
|
|
836
|
-
|
|
837
|
-
/** Collect all actionable commands recursively. Hidden commands are excluded. */
|
|
838
|
-
export function collectEndpoints(commands: AnyPadroneCommand[] | undefined, prefix: string): CollectedEndpoint[] {
|
|
839
|
-
if (!commands) return [];
|
|
840
|
-
const endpoints: CollectedEndpoint[] = [];
|
|
841
|
-
for (const cmd of commands) {
|
|
842
|
-
resolveCommand(cmd);
|
|
843
|
-
if (cmd.hidden) continue;
|
|
844
|
-
const path = cmd.name ? (prefix ? `${prefix}.${cmd.name}` : cmd.name) : prefix;
|
|
845
|
-
if (cmd.action || cmd.argsSchema) {
|
|
846
|
-
endpoints.push({ name: path, command: cmd });
|
|
847
|
-
}
|
|
848
|
-
if (cmd.commands?.length) {
|
|
849
|
-
endpoints.push(...collectEndpoints(cmd.commands, path));
|
|
850
|
-
}
|
|
851
|
-
}
|
|
852
|
-
return endpoints;
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/** Build the JSON Schema for a command's arguments. */
|
|
856
|
-
export function buildInputSchema(cmd: AnyPadroneCommand): Record<string, unknown> {
|
|
857
|
-
if (!cmd.argsSchema) {
|
|
858
|
-
return { type: 'object', additionalProperties: false };
|
|
859
|
-
}
|
|
860
|
-
try {
|
|
861
|
-
return cmd.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, unknown>;
|
|
862
|
-
} catch {
|
|
863
|
-
return { type: 'object', additionalProperties: false };
|
|
864
|
-
}
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
/** Serialize a record of args into CLI flag strings. */
|
|
868
|
-
export function serializeArgsToFlags(args: Record<string, unknown>): string[] {
|
|
869
|
-
const parts: string[] = [];
|
|
870
|
-
for (const [key, value] of Object.entries(args)) {
|
|
871
|
-
if (value === undefined) continue;
|
|
872
|
-
if (typeof value === 'boolean') {
|
|
873
|
-
parts.push(value ? `--${key}` : `--no-${key}`);
|
|
874
|
-
} else if (Array.isArray(value)) {
|
|
875
|
-
for (const v of value) parts.push(`--${key}=${String(v)}`);
|
|
876
|
-
} else {
|
|
877
|
-
const strVal = String(value);
|
|
878
|
-
parts.push(strVal.includes(' ') ? `--${key}="${strVal}"` : `--${key}=${strVal}`);
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
return parts;
|
|
882
|
-
}
|