padrone 1.3.0 → 1.5.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 +94 -0
- package/README.md +105 -284
- package/dist/{args-DFEI7_G_.mjs → args-D5PNDyNu.mjs} +46 -21
- package/dist/args-D5PNDyNu.mjs.map +1 -0
- package/dist/chunk-CjcI7cDX.mjs +15 -0
- package/dist/codegen/index.d.mts +28 -3
- package/dist/codegen/index.d.mts.map +1 -1
- package/dist/codegen/index.mjs +169 -19
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/command-utils-B1D-HqCd.mjs +1117 -0
- package/dist/command-utils-B1D-HqCd.mjs.map +1 -0
- package/dist/completion.d.mts +1 -1
- package/dist/completion.d.mts.map +1 -1
- package/dist/completion.mjs +77 -29
- package/dist/completion.mjs.map +1 -1
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +94 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-BiVrBgi6.mjs +114 -0
- package/dist/errors-BiVrBgi6.mjs.map +1 -0
- package/dist/{formatter-XroimS3Q.d.mts → formatter-DtHzbP22.d.mts} +35 -5
- package/dist/formatter-DtHzbP22.d.mts.map +1 -0
- package/dist/help-bbmu9-qd.mjs +735 -0
- package/dist/help-bbmu9-qd.mjs.map +1 -0
- package/dist/index.d.mts +32 -3
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +495 -267
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-mLWIdUIu.mjs +379 -0
- package/dist/mcp-mLWIdUIu.mjs.map +1 -0
- package/dist/serve-B0u43DK7.mjs +404 -0
- package/dist/serve-B0u43DK7.mjs.map +1 -0
- package/dist/stream-BcC146Ud.mjs +56 -0
- package/dist/stream-BcC146Ud.mjs.map +1 -0
- package/dist/test.d.mts +1 -1
- package/dist/test.mjs +4 -15
- package/dist/test.mjs.map +1 -1
- package/dist/{types-BS7RP5Ls.d.mts → types-Ch8Mk6Qb.d.mts} +311 -63
- package/dist/types-Ch8Mk6Qb.d.mts.map +1 -0
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CFX1FV3v.mjs} +2 -2
- package/dist/{update-check-EbNDkzyV.mjs.map → update-check-CFX1FV3v.mjs.map} +1 -1
- package/dist/zod.d.mts +32 -0
- package/dist/zod.d.mts.map +1 -0
- package/dist/zod.mjs +50 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +10 -2
- package/src/args.ts +76 -44
- package/src/cli/docs.ts +1 -7
- package/src/cli/doctor.ts +195 -10
- package/src/cli/index.ts +1 -1
- package/src/cli/init.ts +2 -3
- package/src/cli/link.ts +2 -2
- package/src/codegen/discovery.ts +80 -28
- package/src/codegen/index.ts +2 -1
- package/src/codegen/parsers/bash.ts +179 -0
- package/src/codegen/schema-to-code.ts +2 -1
- package/src/colorizer.ts +126 -13
- package/src/command-utils.ts +401 -23
- package/src/completion.ts +120 -47
- package/src/create.ts +483 -130
- package/src/docs/index.ts +122 -8
- package/src/formatter.ts +173 -125
- package/src/help.ts +46 -12
- package/src/index.ts +29 -1
- package/src/interactive.ts +45 -4
- package/src/mcp.ts +390 -0
- package/src/repl-loop.ts +16 -3
- package/src/runtime.ts +195 -2
- package/src/serve.ts +442 -0
- package/src/stream.ts +75 -0
- package/src/test.ts +7 -16
- package/src/type-utils.ts +28 -4
- package/src/types.ts +212 -30
- package/src/wrap.ts +23 -25
- package/src/zod.ts +50 -0
- package/dist/args-DFEI7_G_.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/formatter-XroimS3Q.d.mts.map +0 -1
- package/dist/help-CgGP7hQU.mjs +0 -1229
- package/dist/help-CgGP7hQU.mjs.map +0 -1
- package/dist/types-BS7RP5Ls.d.mts.map +0 -1
package/src/command-utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { extractSchemaMetadata } from './args.ts';
|
|
2
|
-
import { type ResolvedPadroneRuntime, resolveRuntime } from './runtime.ts';
|
|
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';
|
|
3
4
|
import type {
|
|
4
5
|
AnyPadroneCommand,
|
|
5
6
|
PadronePlugin,
|
|
@@ -10,6 +11,42 @@ import type {
|
|
|
10
11
|
PluginStartContext,
|
|
11
12
|
} from './types.ts';
|
|
12
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
|
+
|
|
13
50
|
/**
|
|
14
51
|
* Brands a schema as async, signaling that its `validate()` may return a Promise.
|
|
15
52
|
* When an async-branded schema is passed to `.arguments()`, `.configFile()`, or `.env()`,
|
|
@@ -45,6 +82,7 @@ export const configKeys = [
|
|
|
45
82
|
'version',
|
|
46
83
|
'deprecated',
|
|
47
84
|
'hidden',
|
|
85
|
+
'mutation',
|
|
48
86
|
'needsApproval',
|
|
49
87
|
'autoOutput',
|
|
50
88
|
'updateCheck',
|
|
@@ -57,6 +95,8 @@ export const configKeys = [
|
|
|
57
95
|
* - Subcommands are recursively merged by name.
|
|
58
96
|
*/
|
|
59
97
|
export function mergeCommands(existing: AnyPadroneCommand, override: AnyPadroneCommand): AnyPadroneCommand {
|
|
98
|
+
resolveCommand(existing);
|
|
99
|
+
resolveCommand(override);
|
|
60
100
|
const merged: AnyPadroneCommand = { ...existing };
|
|
61
101
|
|
|
62
102
|
// Merge config fields
|
|
@@ -75,6 +115,7 @@ export function mergeCommands(existing: AnyPadroneCommand, override: AnyPadroneC
|
|
|
75
115
|
if (override.runtime !== existing.runtime) merged.runtime = override.runtime;
|
|
76
116
|
if (override.plugins !== existing.plugins) merged.plugins = override.plugins;
|
|
77
117
|
if (override.aliases !== existing.aliases) merged.aliases = override.aliases;
|
|
118
|
+
if (override.progress !== existing.progress) merged.progress = override.progress;
|
|
78
119
|
|
|
79
120
|
// Recursively merge subcommands by name
|
|
80
121
|
if (override.commands) {
|
|
@@ -103,6 +144,63 @@ export function thenMaybe<T, U>(value: T | Promise<T>, fn: (v: T) => U | Promise
|
|
|
103
144
|
return fn(value);
|
|
104
145
|
}
|
|
105
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
|
+
|
|
106
204
|
export function isIterator(value: unknown): value is Iterator<unknown> {
|
|
107
205
|
return typeof value === 'object' && value !== null && Symbol.iterator in value && typeof (value as any)[Symbol.iterator] === 'function';
|
|
108
206
|
}
|
|
@@ -157,6 +255,95 @@ export function outputValue(value: unknown, output: (...args: unknown[]) => void
|
|
|
157
255
|
output(value);
|
|
158
256
|
}
|
|
159
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
|
+
|
|
160
347
|
/**
|
|
161
348
|
* Runs a plugin chain for a given phase using the onion/middleware pattern.
|
|
162
349
|
* Plugins are sorted by `order` (ascending, stable), then composed so that
|
|
@@ -165,12 +352,13 @@ export function outputValue(value: unknown, output: (...args: unknown[]) => void
|
|
|
165
352
|
*/
|
|
166
353
|
export function runPluginChain<TCtx, TResult>(
|
|
167
354
|
phase: 'start' | 'parse' | 'validate' | 'execute' | 'error' | 'shutdown',
|
|
168
|
-
plugins: PadronePlugin[],
|
|
355
|
+
plugins: PadronePlugin<any, any>[],
|
|
169
356
|
ctx: TCtx,
|
|
170
357
|
core: () => TResult | Promise<TResult>,
|
|
171
358
|
): TResult | Promise<TResult> {
|
|
172
|
-
//
|
|
173
|
-
const
|
|
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]);
|
|
174
362
|
if (phasePlugins.length === 0) return core();
|
|
175
363
|
|
|
176
364
|
// Stable sort by order (lower = outermost). Equal order preserves registration order.
|
|
@@ -197,7 +385,7 @@ export function runPluginChain<TCtx, TResult>(
|
|
|
197
385
|
* - Always: `shutdown` plugins run (success or failure).
|
|
198
386
|
*/
|
|
199
387
|
export function wrapWithLifecycle<T>(
|
|
200
|
-
plugins: PadronePlugin[],
|
|
388
|
+
plugins: PadronePlugin<any, any>[],
|
|
201
389
|
command: AnyPadroneCommand,
|
|
202
390
|
state: Record<string, unknown>,
|
|
203
391
|
input: string | undefined,
|
|
@@ -208,10 +396,54 @@ export function wrapWithLifecycle<T>(
|
|
|
208
396
|
const hasError = plugins.some((p) => p.error);
|
|
209
397
|
const hasShutdown = plugins.some((p) => p.shutdown);
|
|
210
398
|
|
|
211
|
-
|
|
212
|
-
|
|
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
|
+
}
|
|
213
444
|
|
|
214
445
|
const runShutdown = (error?: unknown, result?: unknown) => {
|
|
446
|
+
cleanupProgress(error);
|
|
215
447
|
if (!hasShutdown) return;
|
|
216
448
|
const ctx: PluginShutdownContext = { command, state, error, result };
|
|
217
449
|
return runPluginChain('shutdown', plugins, ctx, () => {});
|
|
@@ -276,6 +508,85 @@ export function getCommandRuntime(cmd: AnyPadroneCommand): ResolvedPadroneRuntim
|
|
|
276
508
|
return resolveRuntime();
|
|
277
509
|
}
|
|
278
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
|
+
|
|
279
590
|
export function isAsyncBranded(schema: unknown): boolean {
|
|
280
591
|
return !!schema && typeof schema === 'object' && '~async' in schema && (schema as any)['~async'] === true;
|
|
281
592
|
}
|
|
@@ -308,6 +619,7 @@ export function repathCommandTree(
|
|
|
308
619
|
parentPath: string,
|
|
309
620
|
parent: AnyPadroneCommand,
|
|
310
621
|
): AnyPadroneCommand {
|
|
622
|
+
resolveCommand(cmd);
|
|
311
623
|
const newPath = parentPath ? `${parentPath} ${newName}` : newName;
|
|
312
624
|
const remounted: AnyPadroneCommand = {
|
|
313
625
|
...cmd,
|
|
@@ -335,6 +647,7 @@ export function buildReplCompleter(
|
|
|
335
647
|
inScope?: boolean;
|
|
336
648
|
},
|
|
337
649
|
): (line: string) => [string[], string] {
|
|
650
|
+
resolveAllCommands(rootCommand);
|
|
338
651
|
return (line: string): [string[], string] => {
|
|
339
652
|
const trimmed = line.trimStart();
|
|
340
653
|
const parts = trimmed.split(/\s+/);
|
|
@@ -354,9 +667,12 @@ export function buildReplCompleter(
|
|
|
354
667
|
const commandParts = parts.slice(0, -1).filter((p) => !p.startsWith('-'));
|
|
355
668
|
let targetCommand = rootCommand;
|
|
356
669
|
for (const part of commandParts) {
|
|
670
|
+
resolveCommand(targetCommand);
|
|
357
671
|
const sub = targetCommand.commands?.find((c) => c.name === part || c.aliases?.includes(part));
|
|
358
|
-
if (sub)
|
|
359
|
-
|
|
672
|
+
if (sub) {
|
|
673
|
+
resolveCommand(sub);
|
|
674
|
+
targetCommand = sub;
|
|
675
|
+
} else break;
|
|
360
676
|
}
|
|
361
677
|
|
|
362
678
|
// Get options for this command
|
|
@@ -365,7 +681,7 @@ export function buildReplCompleter(
|
|
|
365
681
|
try {
|
|
366
682
|
const argsMeta = targetCommand.meta?.fields;
|
|
367
683
|
const { flags, aliases } = extractSchemaMetadata(targetCommand.argsSchema, argsMeta, targetCommand.meta?.autoAlias);
|
|
368
|
-
const jsonSchema = targetCommand.argsSchema['~standard'].jsonSchema.input(
|
|
684
|
+
const jsonSchema = targetCommand.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
|
|
369
685
|
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
370
686
|
for (const key of Object.keys(jsonSchema.properties)) {
|
|
371
687
|
options.push(`--${key}`);
|
|
@@ -393,9 +709,12 @@ export function buildReplCompleter(
|
|
|
393
709
|
// Walk into subcommands for all but the last token
|
|
394
710
|
let targetCommand = rootCommand;
|
|
395
711
|
for (let i = 0; i < commandParts.length - 1; i++) {
|
|
712
|
+
resolveCommand(targetCommand);
|
|
396
713
|
const sub = targetCommand.commands?.find((c) => c.name === commandParts[i] || c.aliases?.includes(commandParts[i]!));
|
|
397
|
-
if (sub)
|
|
398
|
-
|
|
714
|
+
if (sub) {
|
|
715
|
+
resolveCommand(sub);
|
|
716
|
+
targetCommand = sub;
|
|
717
|
+
} else break;
|
|
399
718
|
}
|
|
400
719
|
|
|
401
720
|
const candidates: string[] = [];
|
|
@@ -477,28 +796,87 @@ export function findCommandByName(name: string, commands?: AnyPadroneCommand[]):
|
|
|
477
796
|
if (!commands) return undefined;
|
|
478
797
|
|
|
479
798
|
const foundByName = commands.find((cmd) => cmd.name === name);
|
|
480
|
-
if (foundByName) return foundByName;
|
|
799
|
+
if (foundByName) return resolveCommand(foundByName);
|
|
481
800
|
|
|
482
801
|
// Check for aliases
|
|
483
802
|
const foundByAlias = commands.find((cmd) => cmd.aliases?.includes(name));
|
|
484
|
-
if (foundByAlias) return foundByAlias;
|
|
803
|
+
if (foundByAlias) return resolveCommand(foundByAlias);
|
|
485
804
|
|
|
486
805
|
for (const cmd of commands) {
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
813
|
}
|
|
492
814
|
// Check aliases for nested commands
|
|
493
|
-
if (cmd.
|
|
815
|
+
if (cmd.aliases) {
|
|
494
816
|
for (const alias of cmd.aliases) {
|
|
495
817
|
if (name.startsWith(`${alias} `)) {
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
}
|
|
499
824
|
}
|
|
500
825
|
}
|
|
501
826
|
}
|
|
502
827
|
}
|
|
503
828
|
return undefined;
|
|
504
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
|
+
}
|