padrone 1.4.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 +79 -0
- package/README.md +105 -284
- package/dist/{args-CVDbyyzG.mjs → args-D5PNDyNu.mjs} +41 -18
- 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-ClUK5hcQ.d.mts → formatter-DtHzbP22.d.mts} +34 -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 +493 -265
- 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-DjIdJN5G.d.mts → types-Ch8Mk6Qb.d.mts} +310 -62
- 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 +68 -40
- 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 +380 -30
- package/src/completion.ts +120 -47
- package/src/create.ts +480 -128
- package/src/docs/index.ts +122 -8
- package/src/formatter.ts +171 -125
- package/src/help.ts +45 -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-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
- package/dist/help-CcBe91bV.mjs +0 -1254
- package/dist/help-CcBe91bV.mjs.map +0 -1
- package/dist/types-DjIdJN5G.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) {
|
|
@@ -104,33 +145,62 @@ export function thenMaybe<T, U>(value: T | Promise<T>, fn: (v: T) => U | Promise
|
|
|
104
145
|
}
|
|
105
146
|
|
|
106
147
|
/**
|
|
107
|
-
* Makes a sync result object thenable by adding
|
|
148
|
+
* Makes a sync result object thenable by adding `.then()`, `.catch()`, and `.finally()` methods.
|
|
108
149
|
* If the value is already a Promise, returns it as-is.
|
|
109
150
|
* This allows users to write `await program.cli()` or `program.cli().then(...)` regardless of sync/async.
|
|
110
151
|
*
|
|
111
|
-
* The `.then()` resolves with a plain copy (without
|
|
152
|
+
* The `.then()` resolves with a plain copy (without thenable methods) to avoid infinite
|
|
112
153
|
* recursive unwrapping by the Promise resolution algorithm.
|
|
113
154
|
*/
|
|
114
|
-
export function makeThenable<T>(value: T | Promise<T>):
|
|
155
|
+
export function makeThenable<T>(value: T | Promise<T>): Thenable<T> {
|
|
115
156
|
if (value instanceof Promise) return value as any;
|
|
116
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
|
+
};
|
|
117
165
|
// biome-ignore lint/suspicious/noThenProperty: intentional thenable shim for sync results
|
|
118
166
|
(value as any).then = (onfulfilled?: (v: T) => any, onrejected?: (reason: any) => any) => {
|
|
119
167
|
try {
|
|
120
|
-
|
|
121
|
-
const plain = { ...value } as any;
|
|
122
|
-
delete plain.then;
|
|
123
|
-
const result = onfulfilled ? onfulfilled(plain) : plain;
|
|
168
|
+
const result = onfulfilled ? onfulfilled(toPlain()) : toPlain();
|
|
124
169
|
return Promise.resolve(result);
|
|
125
170
|
} catch (err) {
|
|
126
171
|
if (onrejected) return Promise.resolve(onrejected(err));
|
|
127
172
|
return Promise.reject(err);
|
|
128
173
|
}
|
|
129
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
|
+
);
|
|
130
187
|
}
|
|
131
188
|
return value as any;
|
|
132
189
|
}
|
|
133
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
|
+
|
|
134
204
|
export function isIterator(value: unknown): value is Iterator<unknown> {
|
|
135
205
|
return typeof value === 'object' && value !== null && Symbol.iterator in value && typeof (value as any)[Symbol.iterator] === 'function';
|
|
136
206
|
}
|
|
@@ -185,6 +255,95 @@ export function outputValue(value: unknown, output: (...args: unknown[]) => void
|
|
|
185
255
|
output(value);
|
|
186
256
|
}
|
|
187
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
|
+
|
|
188
347
|
/**
|
|
189
348
|
* Runs a plugin chain for a given phase using the onion/middleware pattern.
|
|
190
349
|
* Plugins are sorted by `order` (ascending, stable), then composed so that
|
|
@@ -193,12 +352,13 @@ export function outputValue(value: unknown, output: (...args: unknown[]) => void
|
|
|
193
352
|
*/
|
|
194
353
|
export function runPluginChain<TCtx, TResult>(
|
|
195
354
|
phase: 'start' | 'parse' | 'validate' | 'execute' | 'error' | 'shutdown',
|
|
196
|
-
plugins: PadronePlugin[],
|
|
355
|
+
plugins: PadronePlugin<any, any>[],
|
|
197
356
|
ctx: TCtx,
|
|
198
357
|
core: () => TResult | Promise<TResult>,
|
|
199
358
|
): TResult | Promise<TResult> {
|
|
200
|
-
//
|
|
201
|
-
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]);
|
|
202
362
|
if (phasePlugins.length === 0) return core();
|
|
203
363
|
|
|
204
364
|
// Stable sort by order (lower = outermost). Equal order preserves registration order.
|
|
@@ -225,7 +385,7 @@ export function runPluginChain<TCtx, TResult>(
|
|
|
225
385
|
* - Always: `shutdown` plugins run (success or failure).
|
|
226
386
|
*/
|
|
227
387
|
export function wrapWithLifecycle<T>(
|
|
228
|
-
plugins: PadronePlugin[],
|
|
388
|
+
plugins: PadronePlugin<any, any>[],
|
|
229
389
|
command: AnyPadroneCommand,
|
|
230
390
|
state: Record<string, unknown>,
|
|
231
391
|
input: string | undefined,
|
|
@@ -236,10 +396,54 @@ export function wrapWithLifecycle<T>(
|
|
|
236
396
|
const hasError = plugins.some((p) => p.error);
|
|
237
397
|
const hasShutdown = plugins.some((p) => p.shutdown);
|
|
238
398
|
|
|
239
|
-
|
|
240
|
-
|
|
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
|
+
}
|
|
241
444
|
|
|
242
445
|
const runShutdown = (error?: unknown, result?: unknown) => {
|
|
446
|
+
cleanupProgress(error);
|
|
243
447
|
if (!hasShutdown) return;
|
|
244
448
|
const ctx: PluginShutdownContext = { command, state, error, result };
|
|
245
449
|
return runPluginChain('shutdown', plugins, ctx, () => {});
|
|
@@ -304,6 +508,85 @@ export function getCommandRuntime(cmd: AnyPadroneCommand): ResolvedPadroneRuntim
|
|
|
304
508
|
return resolveRuntime();
|
|
305
509
|
}
|
|
306
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
|
+
|
|
307
590
|
export function isAsyncBranded(schema: unknown): boolean {
|
|
308
591
|
return !!schema && typeof schema === 'object' && '~async' in schema && (schema as any)['~async'] === true;
|
|
309
592
|
}
|
|
@@ -336,6 +619,7 @@ export function repathCommandTree(
|
|
|
336
619
|
parentPath: string,
|
|
337
620
|
parent: AnyPadroneCommand,
|
|
338
621
|
): AnyPadroneCommand {
|
|
622
|
+
resolveCommand(cmd);
|
|
339
623
|
const newPath = parentPath ? `${parentPath} ${newName}` : newName;
|
|
340
624
|
const remounted: AnyPadroneCommand = {
|
|
341
625
|
...cmd,
|
|
@@ -363,6 +647,7 @@ export function buildReplCompleter(
|
|
|
363
647
|
inScope?: boolean;
|
|
364
648
|
},
|
|
365
649
|
): (line: string) => [string[], string] {
|
|
650
|
+
resolveAllCommands(rootCommand);
|
|
366
651
|
return (line: string): [string[], string] => {
|
|
367
652
|
const trimmed = line.trimStart();
|
|
368
653
|
const parts = trimmed.split(/\s+/);
|
|
@@ -382,9 +667,12 @@ export function buildReplCompleter(
|
|
|
382
667
|
const commandParts = parts.slice(0, -1).filter((p) => !p.startsWith('-'));
|
|
383
668
|
let targetCommand = rootCommand;
|
|
384
669
|
for (const part of commandParts) {
|
|
670
|
+
resolveCommand(targetCommand);
|
|
385
671
|
const sub = targetCommand.commands?.find((c) => c.name === part || c.aliases?.includes(part));
|
|
386
|
-
if (sub)
|
|
387
|
-
|
|
672
|
+
if (sub) {
|
|
673
|
+
resolveCommand(sub);
|
|
674
|
+
targetCommand = sub;
|
|
675
|
+
} else break;
|
|
388
676
|
}
|
|
389
677
|
|
|
390
678
|
// Get options for this command
|
|
@@ -393,7 +681,7 @@ export function buildReplCompleter(
|
|
|
393
681
|
try {
|
|
394
682
|
const argsMeta = targetCommand.meta?.fields;
|
|
395
683
|
const { flags, aliases } = extractSchemaMetadata(targetCommand.argsSchema, argsMeta, targetCommand.meta?.autoAlias);
|
|
396
|
-
const jsonSchema = targetCommand.argsSchema['~standard'].jsonSchema.input(
|
|
684
|
+
const jsonSchema = targetCommand.argsSchema['~standard'].jsonSchema.input(JSON_SCHEMA_OPTS) as Record<string, any>;
|
|
397
685
|
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
398
686
|
for (const key of Object.keys(jsonSchema.properties)) {
|
|
399
687
|
options.push(`--${key}`);
|
|
@@ -421,9 +709,12 @@ export function buildReplCompleter(
|
|
|
421
709
|
// Walk into subcommands for all but the last token
|
|
422
710
|
let targetCommand = rootCommand;
|
|
423
711
|
for (let i = 0; i < commandParts.length - 1; i++) {
|
|
712
|
+
resolveCommand(targetCommand);
|
|
424
713
|
const sub = targetCommand.commands?.find((c) => c.name === commandParts[i] || c.aliases?.includes(commandParts[i]!));
|
|
425
|
-
if (sub)
|
|
426
|
-
|
|
714
|
+
if (sub) {
|
|
715
|
+
resolveCommand(sub);
|
|
716
|
+
targetCommand = sub;
|
|
717
|
+
} else break;
|
|
427
718
|
}
|
|
428
719
|
|
|
429
720
|
const candidates: string[] = [];
|
|
@@ -505,28 +796,87 @@ export function findCommandByName(name: string, commands?: AnyPadroneCommand[]):
|
|
|
505
796
|
if (!commands) return undefined;
|
|
506
797
|
|
|
507
798
|
const foundByName = commands.find((cmd) => cmd.name === name);
|
|
508
|
-
if (foundByName) return foundByName;
|
|
799
|
+
if (foundByName) return resolveCommand(foundByName);
|
|
509
800
|
|
|
510
801
|
// Check for aliases
|
|
511
802
|
const foundByAlias = commands.find((cmd) => cmd.aliases?.includes(name));
|
|
512
|
-
if (foundByAlias) return foundByAlias;
|
|
803
|
+
if (foundByAlias) return resolveCommand(foundByAlias);
|
|
513
804
|
|
|
514
805
|
for (const cmd of commands) {
|
|
515
|
-
if (
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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
|
+
}
|
|
519
813
|
}
|
|
520
814
|
// Check aliases for nested commands
|
|
521
|
-
if (cmd.
|
|
815
|
+
if (cmd.aliases) {
|
|
522
816
|
for (const alias of cmd.aliases) {
|
|
523
817
|
if (name.startsWith(`${alias} `)) {
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
+
}
|
|
527
824
|
}
|
|
528
825
|
}
|
|
529
826
|
}
|
|
530
827
|
}
|
|
531
828
|
return undefined;
|
|
532
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
|
+
}
|