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.
Files changed (141) hide show
  1. package/CHANGELOG.md +115 -0
  2. package/README.md +108 -283
  3. package/dist/args-Cnq0nwSM.mjs +272 -0
  4. package/dist/args-Cnq0nwSM.mjs.map +1 -0
  5. package/dist/codegen/index.d.mts +28 -3
  6. package/dist/codegen/index.d.mts.map +1 -1
  7. package/dist/codegen/index.mjs +169 -19
  8. package/dist/codegen/index.mjs.map +1 -1
  9. package/dist/commands-B_gufyR9.mjs +514 -0
  10. package/dist/commands-B_gufyR9.mjs.map +1 -0
  11. package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
  12. package/dist/completion-BEuflbDO.mjs.map +1 -0
  13. package/dist/docs/index.d.mts +22 -2
  14. package/dist/docs/index.d.mts.map +1 -1
  15. package/dist/docs/index.mjs +92 -7
  16. package/dist/docs/index.mjs.map +1 -1
  17. package/dist/errors-CL63UOzt.mjs +137 -0
  18. package/dist/errors-CL63UOzt.mjs.map +1 -0
  19. package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
  20. package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
  21. package/dist/help-B5Kk83of.mjs +849 -0
  22. package/dist/help-B5Kk83of.mjs.map +1 -0
  23. package/dist/index-BaU3X6dY.d.mts +1178 -0
  24. package/dist/index-BaU3X6dY.d.mts.map +1 -0
  25. package/dist/index.d.mts +763 -36
  26. package/dist/index.d.mts.map +1 -1
  27. package/dist/index.mjs +3608 -1534
  28. package/dist/index.mjs.map +1 -1
  29. package/dist/mcp-BM-d0nZi.mjs +377 -0
  30. package/dist/mcp-BM-d0nZi.mjs.map +1 -0
  31. package/dist/serve-Bk0JUlCj.mjs +402 -0
  32. package/dist/serve-Bk0JUlCj.mjs.map +1 -0
  33. package/dist/stream-DC4H8YTx.mjs +77 -0
  34. package/dist/stream-DC4H8YTx.mjs.map +1 -0
  35. package/dist/test.d.mts +5 -8
  36. package/dist/test.d.mts.map +1 -1
  37. package/dist/test.mjs +5 -27
  38. package/dist/test.mjs.map +1 -1
  39. package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
  40. package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
  41. package/dist/zod.d.mts +32 -0
  42. package/dist/zod.d.mts.map +1 -0
  43. package/dist/zod.mjs +50 -0
  44. package/dist/zod.mjs.map +1 -0
  45. package/package.json +20 -9
  46. package/src/cli/completions.ts +14 -11
  47. package/src/cli/docs.ts +13 -16
  48. package/src/cli/doctor.ts +213 -24
  49. package/src/cli/index.ts +28 -82
  50. package/src/cli/init.ts +12 -10
  51. package/src/cli/link.ts +22 -18
  52. package/src/cli/wrap.ts +14 -11
  53. package/src/codegen/discovery.ts +80 -28
  54. package/src/codegen/index.ts +2 -1
  55. package/src/codegen/parsers/bash.ts +179 -0
  56. package/src/codegen/schema-to-code.ts +2 -1
  57. package/src/core/args.ts +296 -0
  58. package/src/core/commands.ts +373 -0
  59. package/src/core/create.ts +268 -0
  60. package/src/{runtime.ts → core/default-runtime.ts} +70 -135
  61. package/src/{errors.ts → core/errors.ts} +22 -0
  62. package/src/core/exec.ts +259 -0
  63. package/src/core/interceptors.ts +302 -0
  64. package/src/{parse.ts → core/parse.ts} +36 -89
  65. package/src/core/program-methods.ts +301 -0
  66. package/src/core/results.ts +229 -0
  67. package/src/core/runtime.ts +246 -0
  68. package/src/core/validate.ts +247 -0
  69. package/src/docs/index.ts +124 -11
  70. package/src/extension/auto-output.ts +95 -0
  71. package/src/extension/color.ts +38 -0
  72. package/src/extension/completion.ts +49 -0
  73. package/src/extension/config.ts +262 -0
  74. package/src/extension/env.ts +101 -0
  75. package/src/extension/help.ts +192 -0
  76. package/src/extension/index.ts +43 -0
  77. package/src/extension/ink.ts +93 -0
  78. package/src/extension/interactive.ts +106 -0
  79. package/src/extension/logger.ts +214 -0
  80. package/src/extension/man.ts +51 -0
  81. package/src/extension/mcp.ts +52 -0
  82. package/src/extension/progress-renderer.ts +338 -0
  83. package/src/extension/progress.ts +299 -0
  84. package/src/extension/repl.ts +94 -0
  85. package/src/extension/serve.ts +48 -0
  86. package/src/extension/signal.ts +87 -0
  87. package/src/extension/stdin.ts +62 -0
  88. package/src/extension/suggestions.ts +114 -0
  89. package/src/extension/timing.ts +81 -0
  90. package/src/extension/tracing.ts +175 -0
  91. package/src/extension/update-check.ts +77 -0
  92. package/src/extension/utils.ts +51 -0
  93. package/src/extension/version.ts +63 -0
  94. package/src/{completion.ts → feature/completion.ts} +130 -57
  95. package/src/{interactive.ts → feature/interactive.ts} +47 -6
  96. package/src/feature/mcp.ts +387 -0
  97. package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
  98. package/src/feature/serve.ts +438 -0
  99. package/src/feature/test.ts +262 -0
  100. package/src/{update-check.ts → feature/update-check.ts} +16 -16
  101. package/src/{wrap.ts → feature/wrap.ts} +27 -27
  102. package/src/index.ts +120 -11
  103. package/src/output/colorizer.ts +154 -0
  104. package/src/{formatter.ts → output/formatter.ts} +281 -135
  105. package/src/{help.ts → output/help.ts} +62 -15
  106. package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
  107. package/src/schema/zod.ts +50 -0
  108. package/src/test.ts +2 -285
  109. package/src/types/args-meta.ts +151 -0
  110. package/src/types/builder.ts +697 -0
  111. package/src/types/command.ts +157 -0
  112. package/src/types/index.ts +59 -0
  113. package/src/types/interceptor.ts +296 -0
  114. package/src/types/preferences.ts +83 -0
  115. package/src/types/result.ts +71 -0
  116. package/src/types/schema.ts +19 -0
  117. package/src/util/dotenv.ts +244 -0
  118. package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
  119. package/src/util/stream.ts +101 -0
  120. package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
  121. package/src/{type-utils.ts → util/type-utils.ts} +99 -37
  122. package/src/util/utils.ts +51 -0
  123. package/src/zod.ts +1 -0
  124. package/dist/args-CVDbyyzG.mjs +0 -199
  125. package/dist/args-CVDbyyzG.mjs.map +0 -1
  126. package/dist/chunk-y_GBKt04.mjs +0 -5
  127. package/dist/completion.d.mts +0 -64
  128. package/dist/completion.d.mts.map +0 -1
  129. package/dist/completion.mjs.map +0 -1
  130. package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
  131. package/dist/help-CcBe91bV.mjs +0 -1254
  132. package/dist/help-CcBe91bV.mjs.map +0 -1
  133. package/dist/types-DjIdJN5G.d.mts +0 -1059
  134. package/dist/types-DjIdJN5G.d.mts.map +0 -1
  135. package/dist/update-check-EbNDkzyV.mjs.map +0 -1
  136. package/src/args.ts +0 -461
  137. package/src/colorizer.ts +0 -41
  138. package/src/command-utils.ts +0 -532
  139. package/src/create.ts +0 -1477
  140. package/src/types.ts +0 -1109
  141. package/src/utils.ts +0 -140
@@ -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
- }