padrone 1.4.0 → 1.6.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 +115 -0
- package/README.md +108 -283
- package/dist/args-Cnq0nwSM.mjs +272 -0
- package/dist/args-Cnq0nwSM.mjs.map +1 -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/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +92 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-CL63UOzt.mjs +137 -0
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/help-B5Kk83of.mjs +849 -0
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/index-BaU3X6dY.d.mts +1178 -0
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +763 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3608 -1534
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-BM-d0nZi.mjs +377 -0
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/serve-Bk0JUlCj.mjs +402 -0
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/stream-DC4H8YTx.mjs +77 -0
- 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 +5 -27
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- 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 +20 -9
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -16
- package/src/cli/doctor.ts +213 -24
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +12 -10
- package/src/cli/link.ts +22 -18
- package/src/cli/wrap.ts +14 -11
- 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/core/args.ts +296 -0
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/{runtime.ts → core/default-runtime.ts} +70 -135
- 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 +124 -11
- package/src/extension/auto-output.ts +95 -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 +43 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +214 -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} +130 -57
- package/src/{interactive.ts → feature/interactive.ts} +47 -6
- package/src/feature/mcp.ts +387 -0
- package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
- package/src/feature/serve.ts +438 -0
- 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} +27 -27
- package/src/index.ts +120 -11
- package/src/output/colorizer.ts +154 -0
- package/src/{formatter.ts → output/formatter.ts} +281 -135
- package/src/{help.ts → output/help.ts} +62 -15
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -285
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +697 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +59 -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/util/stream.ts +101 -0
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +99 -37
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -0
- package/dist/args-CVDbyyzG.mjs +0 -199
- package/dist/args-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- 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 +0 -1059
- package/dist/types-DjIdJN5G.d.mts.map +0 -1
- package/dist/update-check-EbNDkzyV.mjs.map +0 -1
- package/src/args.ts +0 -461
- package/src/colorizer.ts +0 -41
- package/src/command-utils.ts +0 -532
- package/src/create.ts +0 -1477
- package/src/types.ts +0 -1109
- package/src/utils.ts +0 -140
package/src/command-utils.ts
DELETED
|
@@ -1,532 +0,0 @@
|
|
|
1
|
-
import { extractSchemaMetadata } from './args.ts';
|
|
2
|
-
import { type ResolvedPadroneRuntime, resolveRuntime } from './runtime.ts';
|
|
3
|
-
import type {
|
|
4
|
-
AnyPadroneCommand,
|
|
5
|
-
PadronePlugin,
|
|
6
|
-
PadroneSchema,
|
|
7
|
-
PluginErrorContext,
|
|
8
|
-
PluginErrorResult,
|
|
9
|
-
PluginShutdownContext,
|
|
10
|
-
PluginStartContext,
|
|
11
|
-
} from './types.ts';
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* Brands a schema as async, signaling that its `validate()` may return a Promise.
|
|
15
|
-
* When an async-branded schema is passed to `.arguments()`, `.configFile()`, or `.env()`,
|
|
16
|
-
* the command's `parse()` and `cli()` will return Promises.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* ```ts
|
|
20
|
-
* const schema = asyncSchema(z.object({
|
|
21
|
-
* name: z.string(),
|
|
22
|
-
* }).check(async (data) => {
|
|
23
|
-
* // async validation logic
|
|
24
|
-
* }));
|
|
25
|
-
*
|
|
26
|
-
* const program = createPadrone('app')
|
|
27
|
-
* .command('greet', (c) => c.arguments(schema).action((args) => args.name));
|
|
28
|
-
*
|
|
29
|
-
* // parse() now returns Promise<PadroneParseResult>
|
|
30
|
-
* const result = await program.parse('greet --name world');
|
|
31
|
-
* ```
|
|
32
|
-
*/
|
|
33
|
-
export function asyncSchema<T extends PadroneSchema>(schema: T): T & { '~async': true } {
|
|
34
|
-
return Object.assign(schema, { '~async': true as const });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export const commandSymbol = Symbol('padrone_command');
|
|
38
|
-
|
|
39
|
-
export const noop = <TRes>() => undefined as TRes;
|
|
40
|
-
|
|
41
|
-
/** Config keys that are merged when overriding a command. */
|
|
42
|
-
export const configKeys = [
|
|
43
|
-
'title',
|
|
44
|
-
'description',
|
|
45
|
-
'version',
|
|
46
|
-
'deprecated',
|
|
47
|
-
'hidden',
|
|
48
|
-
'needsApproval',
|
|
49
|
-
'autoOutput',
|
|
50
|
-
'updateCheck',
|
|
51
|
-
] as const;
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Merges an existing command with an override.
|
|
55
|
-
* - Config fields are shallow-merged (new overrides old).
|
|
56
|
-
* - Action, arguments, meta, config schema, env schema are taken from the override if set.
|
|
57
|
-
* - Subcommands are recursively merged by name.
|
|
58
|
-
*/
|
|
59
|
-
export function mergeCommands(existing: AnyPadroneCommand, override: AnyPadroneCommand): AnyPadroneCommand {
|
|
60
|
-
const merged: AnyPadroneCommand = { ...existing };
|
|
61
|
-
|
|
62
|
-
// Merge config fields
|
|
63
|
-
for (const key of configKeys) {
|
|
64
|
-
if (override[key] !== undefined) (merged as any)[key] = override[key];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Override fields: take from override if explicitly set (not inherited from existing via spread)
|
|
68
|
-
if (override.action !== existing.action) merged.action = override.action;
|
|
69
|
-
if (override.argsSchema !== existing.argsSchema) merged.argsSchema = override.argsSchema;
|
|
70
|
-
if (override.meta !== existing.meta) merged.meta = override.meta;
|
|
71
|
-
if (override.configSchema !== existing.configSchema) merged.configSchema = override.configSchema;
|
|
72
|
-
if (override.envSchema !== existing.envSchema) merged.envSchema = override.envSchema;
|
|
73
|
-
if (override.configFiles !== existing.configFiles) merged.configFiles = override.configFiles;
|
|
74
|
-
if (override.isAsync !== existing.isAsync) merged.isAsync = override.isAsync || existing.isAsync;
|
|
75
|
-
if (override.runtime !== existing.runtime) merged.runtime = override.runtime;
|
|
76
|
-
if (override.plugins !== existing.plugins) merged.plugins = override.plugins;
|
|
77
|
-
if (override.aliases !== existing.aliases) merged.aliases = override.aliases;
|
|
78
|
-
|
|
79
|
-
// Recursively merge subcommands by name
|
|
80
|
-
if (override.commands) {
|
|
81
|
-
const baseCommands = [...(existing.commands || [])];
|
|
82
|
-
for (const overrideChild of override.commands) {
|
|
83
|
-
const existingIndex = baseCommands.findIndex((c) => c.name === overrideChild.name);
|
|
84
|
-
if (existingIndex >= 0) {
|
|
85
|
-
baseCommands[existingIndex] = mergeCommands(baseCommands[existingIndex]!, overrideChild);
|
|
86
|
-
} else {
|
|
87
|
-
baseCommands.push(overrideChild);
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
merged.commands = baseCommands;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return merged;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Maps over a value that may or may not be a Promise.
|
|
98
|
-
* If the value is a Promise, chains with `.then()`. Otherwise, calls the function synchronously.
|
|
99
|
-
* This preserves sync behavior for sync schemas and async behavior for async schemas.
|
|
100
|
-
*/
|
|
101
|
-
export function thenMaybe<T, U>(value: T | Promise<T>, fn: (v: T) => U | Promise<U>): U | Promise<U> {
|
|
102
|
-
if (value instanceof Promise) return value.then(fn);
|
|
103
|
-
return fn(value);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Makes a sync result object thenable by adding a `.then()` method.
|
|
108
|
-
* If the value is already a Promise, returns it as-is.
|
|
109
|
-
* This allows users to write `await program.cli()` or `program.cli().then(...)` regardless of sync/async.
|
|
110
|
-
*
|
|
111
|
-
* The `.then()` resolves with a plain copy (without `.then`) to avoid infinite
|
|
112
|
-
* recursive unwrapping by the Promise resolution algorithm.
|
|
113
|
-
*/
|
|
114
|
-
export function makeThenable<T>(value: T | Promise<T>): T & PromiseLike<T> {
|
|
115
|
-
if (value instanceof Promise) return value as any;
|
|
116
|
-
if (value !== null && typeof value === 'object' && !('then' in value)) {
|
|
117
|
-
// biome-ignore lint/suspicious/noThenProperty: intentional thenable shim for sync results
|
|
118
|
-
(value as any).then = (onfulfilled?: (v: T) => any, onrejected?: (reason: any) => any) => {
|
|
119
|
-
try {
|
|
120
|
-
// Resolve with a plain copy to prevent infinite thenable unwrapping
|
|
121
|
-
const plain = { ...value } as any;
|
|
122
|
-
delete plain.then;
|
|
123
|
-
const result = onfulfilled ? onfulfilled(plain) : plain;
|
|
124
|
-
return Promise.resolve(result);
|
|
125
|
-
} catch (err) {
|
|
126
|
-
if (onrejected) return Promise.resolve(onrejected(err));
|
|
127
|
-
return Promise.reject(err);
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
return value as any;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export function isIterator(value: unknown): value is Iterator<unknown> {
|
|
135
|
-
return typeof value === 'object' && value !== null && Symbol.iterator in value && typeof (value as any)[Symbol.iterator] === 'function';
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
export function isAsyncIterator(value: unknown): value is AsyncIterator<unknown> {
|
|
139
|
-
return (
|
|
140
|
-
typeof value === 'object' &&
|
|
141
|
-
value !== null &&
|
|
142
|
-
Symbol.asyncIterator in value &&
|
|
143
|
-
typeof (value as any)[Symbol.asyncIterator] === 'function'
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Writes a command's return value to output, handling promises, iterators, and async iterators.
|
|
149
|
-
* Values are passed directly to the output function without stringification —
|
|
150
|
-
* runtimes like Node/Bun already format objects via console.log.
|
|
151
|
-
* Returns void or a Promise depending on whether async consumption is needed.
|
|
152
|
-
*/
|
|
153
|
-
export function outputValue(value: unknown, output: (...args: unknown[]) => void): void | Promise<void> {
|
|
154
|
-
if (value == null) return;
|
|
155
|
-
|
|
156
|
-
// Async iterator — consume and output each yielded value
|
|
157
|
-
if (isAsyncIterator(value)) {
|
|
158
|
-
return (async () => {
|
|
159
|
-
const iter = (value as any)[Symbol.asyncIterator]();
|
|
160
|
-
while (true) {
|
|
161
|
-
const { done, value: item } = await iter.next();
|
|
162
|
-
if (done) break;
|
|
163
|
-
if (item != null) output(item);
|
|
164
|
-
}
|
|
165
|
-
})();
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Sync iterator (but not a plain string/array which also have Symbol.iterator)
|
|
169
|
-
if (typeof value !== 'string' && !Array.isArray(value) && isIterator(value)) {
|
|
170
|
-
const iter = (value as any)[Symbol.iterator]();
|
|
171
|
-
while (true) {
|
|
172
|
-
const { done, value: item } = iter.next();
|
|
173
|
-
if (done) break;
|
|
174
|
-
if (item != null) output(item);
|
|
175
|
-
}
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
// Promise — await then output
|
|
180
|
-
if (value instanceof Promise) {
|
|
181
|
-
return value.then((resolved) => outputValue(resolved, output));
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
// Pass value directly — runtime handles formatting
|
|
185
|
-
output(value);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
/**
|
|
189
|
-
* Runs a plugin chain for a given phase using the onion/middleware pattern.
|
|
190
|
-
* Plugins are sorted by `order` (ascending, stable), then composed so that
|
|
191
|
-
* the first plugin in sorted order is the outermost wrapper.
|
|
192
|
-
* If no plugins handle this phase, `core` is called directly.
|
|
193
|
-
*/
|
|
194
|
-
export function runPluginChain<TCtx, TResult>(
|
|
195
|
-
phase: 'start' | 'parse' | 'validate' | 'execute' | 'error' | 'shutdown',
|
|
196
|
-
plugins: PadronePlugin[],
|
|
197
|
-
ctx: TCtx,
|
|
198
|
-
core: () => TResult | Promise<TResult>,
|
|
199
|
-
): TResult | Promise<TResult> {
|
|
200
|
-
// Filter to plugins that have a handler for this phase, preserve insertion order
|
|
201
|
-
const phasePlugins = plugins.filter((p) => p[phase]);
|
|
202
|
-
if (phasePlugins.length === 0) return core();
|
|
203
|
-
|
|
204
|
-
// Stable sort by order (lower = outermost). Equal order preserves registration order.
|
|
205
|
-
phasePlugins.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
206
|
-
|
|
207
|
-
// Build chain from inside out: last plugin wraps core, first plugin is outermost
|
|
208
|
-
let next = core;
|
|
209
|
-
for (let i = phasePlugins.length - 1; i >= 0; i--) {
|
|
210
|
-
const handler = phasePlugins[i]![phase]! as unknown as (
|
|
211
|
-
ctx: TCtx,
|
|
212
|
-
next: () => TResult | Promise<TResult>,
|
|
213
|
-
) => TResult | Promise<TResult>;
|
|
214
|
-
const prevNext = next;
|
|
215
|
-
next = () => handler(ctx, prevNext);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
return next();
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Wraps a pipeline with start → error → shutdown lifecycle hooks.
|
|
223
|
-
* - `start` plugins wrap the pipeline (onion pattern, root plugins only).
|
|
224
|
-
* - On error: `error` plugins run (can transform/suppress the error).
|
|
225
|
-
* - Always: `shutdown` plugins run (success or failure).
|
|
226
|
-
*/
|
|
227
|
-
export function wrapWithLifecycle<T>(
|
|
228
|
-
plugins: PadronePlugin[],
|
|
229
|
-
command: AnyPadroneCommand,
|
|
230
|
-
state: Record<string, unknown>,
|
|
231
|
-
input: string | undefined,
|
|
232
|
-
pipeline: () => T | Promise<T>,
|
|
233
|
-
wrapErrorResult?: (result: unknown) => T,
|
|
234
|
-
): T | Promise<T> {
|
|
235
|
-
const hasStart = plugins.some((p) => p.start);
|
|
236
|
-
const hasError = plugins.some((p) => p.error);
|
|
237
|
-
const hasShutdown = plugins.some((p) => p.shutdown);
|
|
238
|
-
|
|
239
|
-
// Fast path: no lifecycle plugins
|
|
240
|
-
if (!hasStart && !hasError && !hasShutdown) return pipeline();
|
|
241
|
-
|
|
242
|
-
const runShutdown = (error?: unknown, result?: unknown) => {
|
|
243
|
-
if (!hasShutdown) return;
|
|
244
|
-
const ctx: PluginShutdownContext = { command, state, error, result };
|
|
245
|
-
return runPluginChain('shutdown', plugins, ctx, () => {});
|
|
246
|
-
};
|
|
247
|
-
|
|
248
|
-
const runError = (error: unknown): T | Promise<T> => {
|
|
249
|
-
if (!hasError) {
|
|
250
|
-
const s = runShutdown(error);
|
|
251
|
-
if (s instanceof Promise)
|
|
252
|
-
return s.then(() => {
|
|
253
|
-
throw error;
|
|
254
|
-
});
|
|
255
|
-
throw error;
|
|
256
|
-
}
|
|
257
|
-
const ctx: PluginErrorContext = { command, state, error };
|
|
258
|
-
const errorResult = runPluginChain('error', plugins, ctx, (): PluginErrorResult => ({ error }));
|
|
259
|
-
return thenMaybe(errorResult, (er) => {
|
|
260
|
-
if (er.error !== undefined) {
|
|
261
|
-
const s = runShutdown(er.error);
|
|
262
|
-
return thenMaybe(s as void | Promise<void>, () => {
|
|
263
|
-
throw er.error;
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
const wrapped = wrapErrorResult ? wrapErrorResult(er.result) : (er.result as T);
|
|
267
|
-
const s = runShutdown(undefined, wrapped);
|
|
268
|
-
return thenMaybe(s as void | Promise<void>, () => wrapped);
|
|
269
|
-
});
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const handleSuccess = (result: T): T | Promise<T> => {
|
|
273
|
-
const s = runShutdown(undefined, result);
|
|
274
|
-
if (s instanceof Promise) return s.then(() => result);
|
|
275
|
-
return result;
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
// Run start phase wrapping the pipeline
|
|
279
|
-
const startCtx: PluginStartContext = { command, state, input };
|
|
280
|
-
let result: T | Promise<T>;
|
|
281
|
-
try {
|
|
282
|
-
result = (hasStart ? runPluginChain('start', plugins, startCtx, pipeline) : pipeline()) as T | Promise<T>;
|
|
283
|
-
} catch (e) {
|
|
284
|
-
return runError(e);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
if (result instanceof Promise) {
|
|
288
|
-
return result.then(handleSuccess, runError);
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
return handleSuccess(result);
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Resolves the runtime for a command by walking up the parent chain.
|
|
296
|
-
* Returns a fully resolved runtime with all defaults filled in.
|
|
297
|
-
*/
|
|
298
|
-
export function getCommandRuntime(cmd: AnyPadroneCommand): ResolvedPadroneRuntime {
|
|
299
|
-
let current: AnyPadroneCommand | undefined = cmd;
|
|
300
|
-
while (current) {
|
|
301
|
-
if (current.runtime) return resolveRuntime(current.runtime);
|
|
302
|
-
current = current.parent;
|
|
303
|
-
}
|
|
304
|
-
return resolveRuntime();
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
export function isAsyncBranded(schema: unknown): boolean {
|
|
308
|
-
return !!schema && typeof schema === 'object' && '~async' in schema && (schema as any)['~async'] === true;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
export function hasInteractiveConfig(meta: unknown): boolean {
|
|
312
|
-
if (!meta || typeof meta !== 'object') return false;
|
|
313
|
-
const m = meta as Record<string, unknown>;
|
|
314
|
-
return m.interactive === true || Array.isArray(m.interactive) || m.optionalInteractive === true || Array.isArray(m.optionalInteractive);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export function warnIfUnexpectedAsync<T>(value: T, command: AnyPadroneCommand): T {
|
|
318
|
-
if (typeof process !== 'undefined' && process.env?.NODE_ENV === 'production') return value;
|
|
319
|
-
if (value instanceof Promise && !command.isAsync) {
|
|
320
|
-
const runtime = getCommandRuntime(command);
|
|
321
|
-
runtime.error(
|
|
322
|
-
`[padrone] Command "${command.path || command.name}" returned a Promise from validation, ` +
|
|
323
|
-
`but was not marked as async. Use \`.async()\` on the builder or \`asyncSchema()\` to brand your schema. ` +
|
|
324
|
-
`Without this, TypeScript will infer a sync return type and the result will be a Promise at runtime.`,
|
|
325
|
-
);
|
|
326
|
-
}
|
|
327
|
-
return value;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/**
|
|
331
|
-
* Recursively re-paths a command tree under a new parent path, updating parent references.
|
|
332
|
-
*/
|
|
333
|
-
export function repathCommandTree(
|
|
334
|
-
cmd: AnyPadroneCommand,
|
|
335
|
-
newName: string,
|
|
336
|
-
parentPath: string,
|
|
337
|
-
parent: AnyPadroneCommand,
|
|
338
|
-
): AnyPadroneCommand {
|
|
339
|
-
const newPath = parentPath ? `${parentPath} ${newName}` : newName;
|
|
340
|
-
const remounted: AnyPadroneCommand = {
|
|
341
|
-
...cmd,
|
|
342
|
-
name: newName,
|
|
343
|
-
path: newPath,
|
|
344
|
-
parent,
|
|
345
|
-
version: undefined,
|
|
346
|
-
};
|
|
347
|
-
|
|
348
|
-
if (cmd.commands?.length) {
|
|
349
|
-
remounted.commands = cmd.commands.map((child) => repathCommandTree(child, child.name, newPath, remounted));
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
return remounted;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Builds a completer function for the REPL from the command tree.
|
|
357
|
-
* Completes command names, subcommand names, option names (--foo), and aliases (-f).
|
|
358
|
-
* Also includes dot-prefixed built-in REPL commands (.exit, .clear, .scope, .help, .history).
|
|
359
|
-
*/
|
|
360
|
-
export function buildReplCompleter(
|
|
361
|
-
rootCommand: AnyPadroneCommand,
|
|
362
|
-
builtins: {
|
|
363
|
-
inScope?: boolean;
|
|
364
|
-
},
|
|
365
|
-
): (line: string) => [string[], string] {
|
|
366
|
-
return (line: string): [string[], string] => {
|
|
367
|
-
const trimmed = line.trimStart();
|
|
368
|
-
const parts = trimmed.split(/\s+/);
|
|
369
|
-
const lastPart = parts[parts.length - 1] ?? '';
|
|
370
|
-
|
|
371
|
-
// If we're completing a dot-command
|
|
372
|
-
if (lastPart.startsWith('.')) {
|
|
373
|
-
const dotCmds = ['.exit', '.clear', '.help', '.history'];
|
|
374
|
-
if (rootCommand.commands?.some((c) => c.commands?.length) || builtins.inScope) dotCmds.push('.scope');
|
|
375
|
-
const hits = dotCmds.filter((c) => c.startsWith(lastPart));
|
|
376
|
-
return [hits.length ? hits : dotCmds, lastPart];
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
// If we're completing an option (starts with -)
|
|
380
|
-
if (lastPart.startsWith('-')) {
|
|
381
|
-
// Find which command we're in
|
|
382
|
-
const commandParts = parts.slice(0, -1).filter((p) => !p.startsWith('-'));
|
|
383
|
-
let targetCommand = rootCommand;
|
|
384
|
-
for (const part of commandParts) {
|
|
385
|
-
const sub = targetCommand.commands?.find((c) => c.name === part || c.aliases?.includes(part));
|
|
386
|
-
if (sub) targetCommand = sub;
|
|
387
|
-
else break;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Get options for this command
|
|
391
|
-
const options: string[] = [];
|
|
392
|
-
if (targetCommand.argsSchema) {
|
|
393
|
-
try {
|
|
394
|
-
const argsMeta = targetCommand.meta?.fields;
|
|
395
|
-
const { flags, aliases } = extractSchemaMetadata(targetCommand.argsSchema, argsMeta, targetCommand.meta?.autoAlias);
|
|
396
|
-
const jsonSchema = targetCommand.argsSchema['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
|
|
397
|
-
if (jsonSchema.type === 'object' && jsonSchema.properties) {
|
|
398
|
-
for (const key of Object.keys(jsonSchema.properties)) {
|
|
399
|
-
options.push(`--${key}`);
|
|
400
|
-
}
|
|
401
|
-
for (const flag of Object.keys(flags)) {
|
|
402
|
-
options.push(`-${flag}`);
|
|
403
|
-
}
|
|
404
|
-
for (const alias of Object.keys(aliases)) {
|
|
405
|
-
options.push(`--${alias}`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
} catch {
|
|
409
|
-
// Ignore schema parsing errors
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
// Add global flags
|
|
413
|
-
options.push('--help', '-h');
|
|
414
|
-
|
|
415
|
-
const hits = options.filter((o) => o.startsWith(lastPart));
|
|
416
|
-
return [hits.length ? hits : options, lastPart];
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
// Completing command names
|
|
420
|
-
const commandParts = parts.filter((p) => !p.startsWith('-'));
|
|
421
|
-
// Walk into subcommands for all but the last token
|
|
422
|
-
let targetCommand = rootCommand;
|
|
423
|
-
for (let i = 0; i < commandParts.length - 1; i++) {
|
|
424
|
-
const sub = targetCommand.commands?.find((c) => c.name === commandParts[i] || c.aliases?.includes(commandParts[i]!));
|
|
425
|
-
if (sub) targetCommand = sub;
|
|
426
|
-
else break;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
const candidates: string[] = [];
|
|
430
|
-
|
|
431
|
-
// Add subcommand names and aliases
|
|
432
|
-
if (targetCommand.commands) {
|
|
433
|
-
for (const cmd of targetCommand.commands) {
|
|
434
|
-
if (!cmd.hidden) {
|
|
435
|
-
candidates.push(cmd.name);
|
|
436
|
-
if (cmd.aliases) candidates.push(...cmd.aliases);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
// Add dot-commands and `..` shorthand at the root level (relative to current scope)
|
|
442
|
-
if (targetCommand === rootCommand) {
|
|
443
|
-
candidates.push('.help', '.exit', '.clear', '.history');
|
|
444
|
-
if (rootCommand.commands?.some((c) => c.commands?.length) || builtins.inScope) candidates.push('.scope');
|
|
445
|
-
if (builtins.inScope) candidates.push('..');
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const hits = candidates.filter((c) => c.startsWith(lastPart));
|
|
449
|
-
return [hits.length ? hits : candidates, lastPart];
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* Computes the Levenshtein edit distance between two strings.
|
|
455
|
-
*/
|
|
456
|
-
function levenshtein(a: string, b: string): number {
|
|
457
|
-
const m = a.length;
|
|
458
|
-
const n = b.length;
|
|
459
|
-
const dp: number[] = Array.from({ length: n + 1 }, (_, i) => i);
|
|
460
|
-
|
|
461
|
-
for (let i = 1; i <= m; i++) {
|
|
462
|
-
let prev = dp[0]!;
|
|
463
|
-
dp[0] = i;
|
|
464
|
-
for (let j = 1; j <= n; j++) {
|
|
465
|
-
const temp = dp[j]!;
|
|
466
|
-
dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j]!, dp[j - 1]!);
|
|
467
|
-
prev = temp;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
return dp[n]!;
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Finds the closest match from a list of candidates using Levenshtein distance.
|
|
476
|
-
* Returns the suggestion string (e.g. 'Did you mean "deploy"?') or empty string if no good match.
|
|
477
|
-
* Threshold: distance must be at most 40% of the longer string's length (min 1, max 3).
|
|
478
|
-
*/
|
|
479
|
-
export function suggestSimilar(input: string, candidates: string[]): string {
|
|
480
|
-
if (candidates.length === 0) return '';
|
|
481
|
-
|
|
482
|
-
const lower = input.toLowerCase();
|
|
483
|
-
let bestDist = Infinity;
|
|
484
|
-
let bestMatch = '';
|
|
485
|
-
|
|
486
|
-
for (const candidate of candidates) {
|
|
487
|
-
const dist = levenshtein(lower, candidate.toLowerCase());
|
|
488
|
-
if (dist < bestDist) {
|
|
489
|
-
bestDist = dist;
|
|
490
|
-
bestMatch = candidate;
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
const maxLen = Math.max(input.length, bestMatch.length);
|
|
495
|
-
const threshold = Math.min(3, Math.max(1, Math.ceil(maxLen * 0.4)));
|
|
496
|
-
|
|
497
|
-
if (bestDist > 0 && bestDist <= threshold) {
|
|
498
|
-
return `Did you mean "${bestMatch}"?`;
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
return '';
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
export function findCommandByName(name: string, commands?: AnyPadroneCommand[]): AnyPadroneCommand | undefined {
|
|
505
|
-
if (!commands) return undefined;
|
|
506
|
-
|
|
507
|
-
const foundByName = commands.find((cmd) => cmd.name === name);
|
|
508
|
-
if (foundByName) return foundByName;
|
|
509
|
-
|
|
510
|
-
// Check for aliases
|
|
511
|
-
const foundByAlias = commands.find((cmd) => cmd.aliases?.includes(name));
|
|
512
|
-
if (foundByAlias) return foundByAlias;
|
|
513
|
-
|
|
514
|
-
for (const cmd of commands) {
|
|
515
|
-
if (cmd.commands && name.startsWith(`${cmd.name} `)) {
|
|
516
|
-
const subCommandName = name.slice(cmd.name.length + 1);
|
|
517
|
-
const subCommand = findCommandByName(subCommandName, cmd.commands);
|
|
518
|
-
if (subCommand) return subCommand;
|
|
519
|
-
}
|
|
520
|
-
// Check aliases for nested commands
|
|
521
|
-
if (cmd.commands && cmd.aliases) {
|
|
522
|
-
for (const alias of cmd.aliases) {
|
|
523
|
-
if (name.startsWith(`${alias} `)) {
|
|
524
|
-
const subCommandName = name.slice(alias.length + 1);
|
|
525
|
-
const subCommand = findCommandByName(subCommandName, cmd.commands);
|
|
526
|
-
if (subCommand) return subCommand;
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
return undefined;
|
|
532
|
-
}
|