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
@@ -0,0 +1,373 @@
1
+ import type { AnyPadroneCommand } from '../types/index.ts';
2
+ import { extractSchemaMetadata, getJsonSchema } from './args.ts';
3
+ import { resolveRuntime } from './default-runtime.ts';
4
+ import type { ResolvedPadroneRuntime } from './runtime.ts';
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Lazy command resolution
8
+ // ---------------------------------------------------------------------------
9
+
10
+ export const lazyResolver = Symbol('lazyResolver');
11
+
12
+ /** Resolves a lazy command in place by calling its stored resolver. No-op if already resolved. */
13
+ export function resolveCommand(cmd: AnyPadroneCommand): AnyPadroneCommand {
14
+ const resolver = (cmd as any)[lazyResolver];
15
+ if (resolver) {
16
+ delete (cmd as any)[lazyResolver];
17
+ resolver(cmd);
18
+ }
19
+ return cmd;
20
+ }
21
+
22
+ /** Recursively resolves a command and all its descendants. */
23
+ export function resolveAllCommands(cmd: AnyPadroneCommand): void {
24
+ resolveCommand(cmd);
25
+ if (cmd.commands) {
26
+ for (const sub of cmd.commands) resolveAllCommands(sub);
27
+ }
28
+ }
29
+
30
+ /** Checks whether a value is a Padrone program/builder. */
31
+ export function isPadroneProgram(value: unknown): value is object {
32
+ return !!value && typeof value === 'object' && commandSymbol in value;
33
+ }
34
+
35
+ /** Extracts the underlying command from a program/builder and resolves the full command tree. */
36
+ export function getCommand(program: object): AnyPadroneCommand {
37
+ const cmd = commandSymbol in program ? ((program as any)[commandSymbol] as AnyPadroneCommand) : (program as AnyPadroneCommand);
38
+ resolveAllCommands(cmd);
39
+ return cmd;
40
+ }
41
+
42
+ export const commandSymbol = Symbol('padrone_command');
43
+
44
+ /** Config keys that are merged when overriding a command. */
45
+ export const configKeys = ['title', 'description', 'version', 'deprecated', 'hidden', 'mutation', 'needsApproval'] as const;
46
+
47
+ /**
48
+ * Merges an existing command with an override.
49
+ * - Config fields are shallow-merged (new overrides old).
50
+ * - Action, arguments, meta, config schema, env schema are taken from the override if set.
51
+ * - Subcommands are recursively merged by name.
52
+ */
53
+ export function mergeCommands(existing: AnyPadroneCommand, override: AnyPadroneCommand): AnyPadroneCommand {
54
+ resolveCommand(existing);
55
+ resolveCommand(override);
56
+ const merged: AnyPadroneCommand = { ...existing };
57
+
58
+ // Merge config fields
59
+ for (const key of configKeys) {
60
+ if (override[key] !== undefined) (merged as any)[key] = override[key];
61
+ }
62
+
63
+ // Override fields: take from override if explicitly set (not inherited from existing via spread)
64
+ if (override.action !== existing.action) merged.action = override.action;
65
+ if (override.argsSchema !== existing.argsSchema) merged.argsSchema = override.argsSchema;
66
+ if (override.meta !== existing.meta) merged.meta = override.meta;
67
+ if (override.isAsync !== existing.isAsync) merged.isAsync = override.isAsync || existing.isAsync;
68
+ if (override.runtime !== existing.runtime) merged.runtime = override.runtime;
69
+ if (override.interceptors !== existing.interceptors) merged.interceptors = override.interceptors;
70
+ if (override.aliases !== existing.aliases) merged.aliases = override.aliases;
71
+ // Recursively merge subcommands by name
72
+ if (override.commands) {
73
+ const baseCommands = [...(existing.commands || [])];
74
+ for (const overrideChild of override.commands) {
75
+ const existingIndex = baseCommands.findIndex((c) => c.name === overrideChild.name);
76
+ if (existingIndex >= 0) {
77
+ baseCommands[existingIndex] = mergeCommands(baseCommands[existingIndex]!, overrideChild);
78
+ } else {
79
+ baseCommands.push(overrideChild);
80
+ }
81
+ }
82
+ merged.commands = baseCommands;
83
+ }
84
+
85
+ return merged;
86
+ }
87
+
88
+ /**
89
+ * Resolves the runtime for a command by walking up the parent chain.
90
+ * Returns a fully resolved runtime with all defaults filled in.
91
+ */
92
+ export function getCommandRuntime(cmd: AnyPadroneCommand): ResolvedPadroneRuntime {
93
+ let current: AnyPadroneCommand | undefined = cmd;
94
+ while (current) {
95
+ if (current.runtime) return resolveRuntime(current.runtime);
96
+ current = current.parent;
97
+ }
98
+ return resolveRuntime();
99
+ }
100
+
101
+ /**
102
+ * Recursively re-paths a command tree under a new parent path, updating parent references.
103
+ */
104
+ export function repathCommandTree(
105
+ cmd: AnyPadroneCommand,
106
+ newName: string,
107
+ parentPath: string,
108
+ parent: AnyPadroneCommand,
109
+ ): AnyPadroneCommand {
110
+ resolveCommand(cmd);
111
+ const newPath = parentPath ? `${parentPath} ${newName}` : newName;
112
+ const remounted: AnyPadroneCommand = {
113
+ ...cmd,
114
+ name: newName,
115
+ path: newPath,
116
+ parent,
117
+ version: undefined,
118
+ };
119
+
120
+ if (cmd.commands?.length) {
121
+ remounted.commands = cmd.commands.map((child) => repathCommandTree(child, child.name, newPath, remounted));
122
+ }
123
+
124
+ return remounted;
125
+ }
126
+
127
+ /**
128
+ * Builds a completer function for the REPL from the command tree.
129
+ * Completes command names, subcommand names, option names (--foo), and aliases (-f).
130
+ * Also includes dot-prefixed built-in REPL commands (.exit, .clear, .scope, .help, .history).
131
+ */
132
+ export function buildReplCompleter(
133
+ rootCommand: AnyPadroneCommand,
134
+ builtins: {
135
+ inScope?: boolean;
136
+ },
137
+ ): (line: string) => [string[], string] {
138
+ resolveAllCommands(rootCommand);
139
+ return (line: string): [string[], string] => {
140
+ const trimmed = line.trimStart();
141
+ const parts = trimmed.split(/\s+/);
142
+ const lastPart = parts[parts.length - 1] ?? '';
143
+
144
+ // If we're completing a dot-command
145
+ if (lastPart.startsWith('.')) {
146
+ const dotCmds = ['.exit', '.clear', '.help', '.history'];
147
+ if (rootCommand.commands?.some((c) => c.commands?.length) || builtins.inScope) dotCmds.push('.scope');
148
+ const hits = dotCmds.filter((c) => c.startsWith(lastPart));
149
+ return [hits.length ? hits : dotCmds, lastPart];
150
+ }
151
+
152
+ // If we're completing an option (starts with -)
153
+ if (lastPart.startsWith('-')) {
154
+ // Find which command we're in
155
+ const commandParts = parts.slice(0, -1).filter((p) => !p.startsWith('-'));
156
+ let targetCommand = rootCommand;
157
+ for (const part of commandParts) {
158
+ resolveCommand(targetCommand);
159
+ const sub = targetCommand.commands?.find((c) => c.name === part || c.aliases?.includes(part));
160
+ if (sub) {
161
+ resolveCommand(sub);
162
+ targetCommand = sub;
163
+ } else break;
164
+ }
165
+
166
+ // Get options for this command
167
+ const options: string[] = [];
168
+ if (targetCommand.argsSchema) {
169
+ try {
170
+ const argsMeta = targetCommand.meta?.fields;
171
+ const { flags, aliases } = extractSchemaMetadata(targetCommand.argsSchema, argsMeta, targetCommand.meta?.autoAlias);
172
+ const jsonSchema = getJsonSchema(targetCommand.argsSchema) as Record<string, any>;
173
+ if (jsonSchema.type === 'object' && jsonSchema.properties) {
174
+ for (const key of Object.keys(jsonSchema.properties)) {
175
+ options.push(`--${key}`);
176
+ }
177
+ for (const flag of Object.keys(flags)) {
178
+ options.push(`-${flag}`);
179
+ }
180
+ for (const alias of Object.keys(aliases)) {
181
+ options.push(`--${alias}`);
182
+ }
183
+ }
184
+ } catch {
185
+ // Ignore schema parsing errors
186
+ }
187
+ }
188
+ // Add global flags
189
+ options.push('--help', '-h');
190
+
191
+ const hits = options.filter((o) => o.startsWith(lastPart));
192
+ return [hits.length ? hits : options, lastPart];
193
+ }
194
+
195
+ // Completing command names
196
+ const commandParts = parts.filter((p) => !p.startsWith('-'));
197
+ // Walk into subcommands for all but the last token
198
+ let targetCommand = rootCommand;
199
+ for (let i = 0; i < commandParts.length - 1; i++) {
200
+ resolveCommand(targetCommand);
201
+ const sub = targetCommand.commands?.find((c) => c.name === commandParts[i] || c.aliases?.includes(commandParts[i]!));
202
+ if (sub) {
203
+ resolveCommand(sub);
204
+ targetCommand = sub;
205
+ } else break;
206
+ }
207
+
208
+ const candidates: string[] = [];
209
+
210
+ // Add subcommand names and aliases
211
+ if (targetCommand.commands) {
212
+ for (const cmd of targetCommand.commands) {
213
+ if (!cmd.hidden) {
214
+ candidates.push(cmd.name);
215
+ if (cmd.aliases) candidates.push(...cmd.aliases);
216
+ }
217
+ }
218
+ }
219
+
220
+ // Add dot-commands and `..` shorthand at the root level (relative to current scope)
221
+ if (targetCommand === rootCommand) {
222
+ candidates.push('.help', '.exit', '.clear', '.history');
223
+ if (rootCommand.commands?.some((c) => c.commands?.length) || builtins.inScope) candidates.push('.scope');
224
+ if (builtins.inScope) candidates.push('..');
225
+ }
226
+
227
+ const hits = candidates.filter((c) => c.startsWith(lastPart));
228
+ return [hits.length ? hits : candidates, lastPart];
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Computes the Levenshtein edit distance between two strings.
234
+ */
235
+ function levenshtein(a: string, b: string): number {
236
+ const m = a.length;
237
+ const n = b.length;
238
+ const dp: number[] = Array.from({ length: n + 1 }, (_, i) => i);
239
+
240
+ for (let i = 1; i <= m; i++) {
241
+ let prev = dp[0]!;
242
+ dp[0] = i;
243
+ for (let j = 1; j <= n; j++) {
244
+ const temp = dp[j]!;
245
+ dp[j] = a[i - 1] === b[j - 1] ? prev : 1 + Math.min(prev, dp[j]!, dp[j - 1]!);
246
+ prev = temp;
247
+ }
248
+ }
249
+
250
+ return dp[n]!;
251
+ }
252
+
253
+ /**
254
+ * Finds close matches from a list of candidates using Levenshtein distance
255
+ * and prefix/substring matching (for inputs longer than 3 characters).
256
+ * Returns up to 3 matching candidate names (raw, unformatted).
257
+ */
258
+ export function suggestSimilar(input: string, candidates: string[]): string[] {
259
+ if (candidates.length === 0) return [];
260
+
261
+ const lower = input.toLowerCase();
262
+ const matches: { candidate: string; score: number }[] = [];
263
+
264
+ for (const candidate of candidates) {
265
+ const candidateLower = candidate.toLowerCase();
266
+ if (candidateLower === lower) continue;
267
+
268
+ const dist = levenshtein(lower, candidateLower);
269
+ const maxLen = Math.max(input.length, candidate.length);
270
+ const threshold = Math.min(3, Math.max(1, Math.ceil(maxLen * 0.4)));
271
+
272
+ if (dist > 0 && dist <= threshold) {
273
+ matches.push({ candidate, score: dist });
274
+ } else if (lower.length >= 3) {
275
+ // Prefix or substring match for longer inputs
276
+ if (candidateLower.startsWith(lower) || candidateLower.includes(lower)) {
277
+ matches.push({ candidate, score: threshold + 1 });
278
+ }
279
+ }
280
+ }
281
+
282
+ matches.sort((a, b) => a.score - b.score);
283
+ return matches.slice(0, 3).map((m) => m.candidate);
284
+ }
285
+
286
+ export function findCommandByName(name: string, commands?: AnyPadroneCommand[]): AnyPadroneCommand | undefined {
287
+ if (!commands) return undefined;
288
+
289
+ const foundByName = commands.find((cmd) => cmd.name === name);
290
+ if (foundByName) return resolveCommand(foundByName);
291
+
292
+ // Check for aliases
293
+ const foundByAlias = commands.find((cmd) => cmd.aliases?.includes(name));
294
+ if (foundByAlias) return resolveCommand(foundByAlias);
295
+
296
+ for (const cmd of commands) {
297
+ if (name.startsWith(`${cmd.name} `)) {
298
+ resolveCommand(cmd);
299
+ if (cmd.commands) {
300
+ const subCommandName = name.slice(cmd.name.length + 1);
301
+ const subCommand = findCommandByName(subCommandName, cmd.commands);
302
+ if (subCommand) return subCommand;
303
+ }
304
+ }
305
+ // Check aliases for nested commands
306
+ if (cmd.aliases) {
307
+ for (const alias of cmd.aliases) {
308
+ if (name.startsWith(`${alias} `)) {
309
+ resolveCommand(cmd);
310
+ if (cmd.commands) {
311
+ const subCommandName = name.slice(alias.length + 1);
312
+ const subCommand = findCommandByName(subCommandName, cmd.commands);
313
+ if (subCommand) return subCommand;
314
+ }
315
+ }
316
+ }
317
+ }
318
+ }
319
+ return undefined;
320
+ }
321
+
322
+ // ---------------------------------------------------------------------------
323
+ // Shared utilities for MCP and serve
324
+ // ---------------------------------------------------------------------------
325
+
326
+ export type CollectedEndpoint = { name: string; command: AnyPadroneCommand };
327
+
328
+ /** Collect all actionable commands recursively. Hidden commands are excluded. */
329
+ export function collectEndpoints(commands: AnyPadroneCommand[] | undefined, prefix: string): CollectedEndpoint[] {
330
+ if (!commands) return [];
331
+ const endpoints: CollectedEndpoint[] = [];
332
+ for (const cmd of commands) {
333
+ resolveCommand(cmd);
334
+ if (cmd.hidden) continue;
335
+ const path = cmd.name ? (prefix ? `${prefix}.${cmd.name}` : cmd.name) : prefix;
336
+ if (cmd.action || cmd.argsSchema) {
337
+ endpoints.push({ name: path, command: cmd });
338
+ }
339
+ if (cmd.commands?.length) {
340
+ endpoints.push(...collectEndpoints(cmd.commands, path));
341
+ }
342
+ }
343
+ return endpoints;
344
+ }
345
+
346
+ /** Build the JSON Schema for a command's arguments. */
347
+ export function buildInputSchema(cmd: AnyPadroneCommand): Record<string, unknown> {
348
+ if (!cmd.argsSchema) {
349
+ return { type: 'object', additionalProperties: false };
350
+ }
351
+ try {
352
+ return getJsonSchema(cmd.argsSchema) as Record<string, unknown>;
353
+ } catch {
354
+ return { type: 'object', additionalProperties: false };
355
+ }
356
+ }
357
+
358
+ /** Serialize a record of args into CLI flag strings. */
359
+ export function serializeArgsToFlags(args: Record<string, unknown>): string[] {
360
+ const parts: string[] = [];
361
+ for (const [key, value] of Object.entries(args)) {
362
+ if (value === undefined) continue;
363
+ if (typeof value === 'boolean') {
364
+ parts.push(value ? `--${key}` : `--no-${key}`);
365
+ } else if (Array.isArray(value)) {
366
+ for (const v of value) parts.push(`--${key}=${String(v)}`);
367
+ } else {
368
+ const strVal = String(value);
369
+ parts.push(strVal.includes(' ') ? `--${key}="${strVal}"` : `--${key}=${strVal}`);
370
+ }
371
+ }
372
+ return parts;
373
+ }
@@ -0,0 +1,268 @@
1
+ import { padroneAutoOutput } from '../extension/auto-output.ts';
2
+ import { padroneColor } from '../extension/color.ts';
3
+ import type { HelpCommand } from '../extension/help.ts';
4
+ import { padroneHelp } from '../extension/help.ts';
5
+ import { padroneInteractive } from '../extension/interactive.ts';
6
+ import { padroneRepl } from '../extension/repl.ts';
7
+ import { padroneSignalHandling } from '../extension/signal.ts';
8
+ import { padroneStdin } from '../extension/stdin.ts';
9
+ import { padroneSuggestions } from '../extension/suggestions.ts';
10
+ import type { VersionCommand } from '../extension/version.ts';
11
+ import { padroneVersion } from '../extension/version.ts';
12
+ import { createWrapHandler } from '../feature/wrap.ts';
13
+ import type {
14
+ AnyPadroneCommand,
15
+ AnyPadroneProgram,
16
+ InterceptorFactory,
17
+ InterceptorMeta,
18
+ PadroneCommand,
19
+ PadroneInterceptorFn,
20
+ PadroneProgram,
21
+ PadroneSchema,
22
+ RegisteredInterceptor,
23
+ } from '../types/index.ts';
24
+ import { commandSymbol, findCommandByName, lazyResolver, mergeCommands, repathCommandTree, resolveCommand } from './commands.ts';
25
+ import { RoutingError } from './errors.ts';
26
+ import type { ExecContext } from './exec.ts';
27
+ import { collectInterceptors, errorResultWithSignal, execCommand } from './exec.ts';
28
+ import { toRegisteredInterceptor } from './interceptors.ts';
29
+ import { createProgramMethods } from './program-methods.ts';
30
+ import { hasInteractiveConfig, isAsyncBranded, makeThenable, noop, withPromiseDrain } from './results.ts';
31
+ import { parseCommand } from './validate.ts';
32
+
33
+ export { buildReplCompleter } from './commands.ts';
34
+ export { asyncSchema } from './results.ts';
35
+
36
+ /**
37
+ * Options for configuring which built-in extensions are applied by default.
38
+ */
39
+ export type PadroneBuiltins = {
40
+ /** Enable `help` command, `--help` / `-h` flags, and default help display. Defaults to `true`. */
41
+ help?: boolean;
42
+ /** Enable `version` command and `--version` / `-v` / `-V` flags. Defaults to `true`. */
43
+ version?: boolean;
44
+ /** Enable `repl` command and `--repl` flag. Defaults to `true`. */
45
+ repl?: boolean;
46
+ /** Enable `--color` / `--no-color` flag support. Defaults to `true`. */
47
+ color?: boolean;
48
+ /** Enable "Did you mean?" suggestions for unknown commands and options. Defaults to `true`. */
49
+ suggestions?: boolean;
50
+ /** Enable signal handling (SIGINT, SIGTERM, SIGHUP). Defaults to `true`. */
51
+ signal?: boolean;
52
+ /** Enable automatic result output for `cli()`. Defaults to `true`. */
53
+ autoOutput?: boolean;
54
+ /** Enable stdin piping support. Defaults to `true`. */
55
+ stdin?: boolean;
56
+ /** Enable interactive prompting for missing arguments. Defaults to `true`. */
57
+ interactive?: boolean;
58
+ };
59
+
60
+ export type PadroneOptions = { builtins?: PadroneBuiltins };
61
+
62
+ // biome-ignore lint/complexity/noBannedTypes: empty object signals "all defaults enabled"
63
+ type DefaultBuiltins = {};
64
+
65
+ type BuiltinCommands<B> = [...(B extends { help: false } ? [] : [HelpCommand]), ...(B extends { version: false } ? [] : [VersionCommand])];
66
+
67
+ export function createPadrone<TProgramName extends string, const TBuiltins extends PadroneBuiltins = DefaultBuiltins>(
68
+ name: TProgramName,
69
+ options?: { builtins?: TBuiltins },
70
+ ): PadroneProgram<TProgramName, '', '', PadroneSchema<void>, void, BuiltinCommands<TBuiltins>> {
71
+ let builder: any = createPadroneBuilder({ name, path: '', commands: [] } as any);
72
+
73
+ const b = options?.builtins;
74
+ if (b?.help !== false) builder = builder.extend(padroneHelp());
75
+ if (b?.version !== false) builder = builder.extend(padroneVersion());
76
+ if (b?.repl !== false) builder = builder.extend(padroneRepl());
77
+ if (b?.color !== false) builder = builder.extend(padroneColor());
78
+ if (b?.suggestions !== false) builder = builder.extend(padroneSuggestions());
79
+ if (b?.signal !== false) builder = builder.extend(padroneSignalHandling());
80
+ if (b?.autoOutput !== false) builder = builder.extend(padroneAutoOutput());
81
+ if (b?.stdin !== false) builder = builder.extend(padroneStdin());
82
+ if (b?.interactive !== false) builder = builder.extend(padroneInteractive());
83
+
84
+ return builder as any;
85
+ }
86
+
87
+ export function createPadroneBuilder<TBuilder extends PadroneProgram = PadroneProgram>(
88
+ inputCommand: AnyPadroneCommand,
89
+ ): TBuilder & { [commandSymbol]: AnyPadroneCommand } {
90
+ // Re-parent direct subcommands so getCommandRuntime walks to the current root,
91
+ // not a stale parent from before .runtime()/.configure()/etc.
92
+ const existingCommand =
93
+ inputCommand.commands?.length && inputCommand.commands.some((c) => c.parent && c.parent !== inputCommand)
94
+ ? {
95
+ ...inputCommand,
96
+ commands: inputCommand.commands.map((c) => (c.parent && c.parent !== inputCommand ? { ...c, parent: inputCommand } : c)),
97
+ }
98
+ : inputCommand;
99
+
100
+ const parseCommandFn = (input: string | undefined) => parseCommand(input, existingCommand, findCommandByName);
101
+ const collectInterceptorsFn = (cmd: AnyPadroneCommand) => collectInterceptors(cmd, existingCommand);
102
+
103
+ // Execution context shared by exec and program methods.
104
+ // `builder` is assigned after the builder object is created (forward ref resolved at runtime only).
105
+ const execCtx: ExecContext = {
106
+ rootCommand: existingCommand,
107
+ builder: undefined as any,
108
+ parseCommandFn,
109
+ collectInterceptorsFn,
110
+ };
111
+
112
+ const evalCommand: AnyPadroneProgram['eval'] = (input, evalOptions) => {
113
+ try {
114
+ const result = execCommand(input as string, execCtx, evalOptions, 'soft', evalOptions?.caller ?? 'eval');
115
+ if (result instanceof Promise) return withPromiseDrain(result.catch((err: unknown) => errorResultWithSignal(err))) as any;
116
+ return makeThenable(result);
117
+ } catch (err) {
118
+ return makeThenable(errorResultWithSignal(err)) as any;
119
+ }
120
+ };
121
+
122
+ const programMethods = createProgramMethods(execCtx, evalCommand);
123
+
124
+ const builder = {
125
+ extend(extension: (builder: any) => any) {
126
+ return extension(builder);
127
+ },
128
+ configure(config) {
129
+ return createPadroneBuilder({ ...existingCommand, ...config }) as any;
130
+ },
131
+ runtime(runtimeConfig) {
132
+ return createPadroneBuilder({ ...existingCommand, runtime: { ...existingCommand.runtime, ...runtimeConfig } }) as any;
133
+ },
134
+ async() {
135
+ return createPadroneBuilder({ ...existingCommand, isAsync: true }) as any;
136
+ },
137
+ context(transform?: (ctx: unknown) => unknown) {
138
+ if (!transform) return createPadroneBuilder({ ...existingCommand }) as any;
139
+ const existing = existingCommand.contextTransform;
140
+ const composed = existing ? (ctx: unknown) => transform(existing(ctx)) : transform;
141
+ return createPadroneBuilder({ ...existingCommand, contextTransform: composed }) as any;
142
+ },
143
+ arguments(schema, meta) {
144
+ const resolvedArgs = typeof schema === 'function' ? schema(existingCommand.argsSchema as any) : schema;
145
+ const isAsync = existingCommand.isAsync || isAsyncBranded(resolvedArgs) || hasInteractiveConfig(meta);
146
+ return createPadroneBuilder({ ...existingCommand, argsSchema: resolvedArgs, meta, isAsync }) as any;
147
+ },
148
+ action(handler = noop) {
149
+ const baseHandler = existingCommand.action ?? noop;
150
+ return createPadroneBuilder({
151
+ ...existingCommand,
152
+ action: (args: any, ctx: any) => (handler as any)(args, ctx, baseHandler),
153
+ }) as any;
154
+ },
155
+ wrap(config) {
156
+ const handler = createWrapHandler(config, existingCommand.argsSchema as any, existingCommand.meta?.positional);
157
+ return createPadroneBuilder({ ...existingCommand, action: handler }) as any;
158
+ },
159
+ command(nameOrNames, builderFn) {
160
+ const name = Array.isArray(nameOrNames) ? nameOrNames[0] : nameOrNames;
161
+ const aliases = Array.isArray(nameOrNames) && nameOrNames.length > 1 ? (nameOrNames.slice(1) as string[]) : undefined;
162
+
163
+ const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
164
+ if (existingSubcommand) resolveCommand(existingSubcommand);
165
+
166
+ const initialCommand: AnyPadroneCommand = existingSubcommand
167
+ ? { ...existingSubcommand, aliases: aliases ?? existingSubcommand.aliases, parent: existingCommand }
168
+ : ({
169
+ name,
170
+ path: existingCommand.path ? `${existingCommand.path} ${name}` : name,
171
+ aliases,
172
+ parent: existingCommand,
173
+ '~types': {} as any,
174
+ } satisfies PadroneCommand);
175
+
176
+ if (builderFn) {
177
+ const lazyCmd: AnyPadroneCommand = { ...initialCommand };
178
+ (lazyCmd as any)[lazyResolver] = (target: AnyPadroneCommand) => {
179
+ const savedParent = target.parent;
180
+ const b = createPadroneBuilder(target);
181
+ const commandObj = ((builderFn(b as any) as unknown as typeof b)?.[commandSymbol] as AnyPadroneCommand) ?? target;
182
+ const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, commandObj) : commandObj;
183
+ Object.assign(target, mergedCommandObj);
184
+ // Restore parent: mergeCommands copies the existing command's parent which may be stale
185
+ // (e.g. when an extension is applied twice, the merged parent predates re-parenting).
186
+ target.parent = savedParent;
187
+ };
188
+
189
+ const commands = existingCommand.commands || [];
190
+ const existingIndex = commands.findIndex((c) => c.name === name);
191
+ const updatedCommands =
192
+ existingIndex >= 0
193
+ ? [...commands.slice(0, existingIndex), lazyCmd, ...commands.slice(existingIndex + 1)]
194
+ : [...commands, lazyCmd];
195
+
196
+ return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
197
+ }
198
+
199
+ const commands = existingCommand.commands || [];
200
+ const existingIndex = commands.findIndex((c) => c.name === name);
201
+ const updatedCommands =
202
+ existingIndex >= 0
203
+ ? [...commands.slice(0, existingIndex), initialCommand, ...commands.slice(existingIndex + 1)]
204
+ : [...commands, initialCommand];
205
+
206
+ return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
207
+ },
208
+
209
+ mount(nameOrNames: string | readonly string[], program: unknown, options?: { context?: (ctx: unknown) => unknown }) {
210
+ const name = Array.isArray(nameOrNames) ? nameOrNames[0] : nameOrNames;
211
+ const aliases = Array.isArray(nameOrNames) && nameOrNames.length > 1 ? (nameOrNames.slice(1) as string[]) : undefined;
212
+
213
+ const programCommand = (program as any)[commandSymbol] as AnyPadroneCommand | undefined;
214
+ if (!programCommand) throw new RoutingError('Cannot mount: not a valid Padrone program');
215
+
216
+ const remounted = repathCommandTree(programCommand, name, existingCommand.path || '', existingCommand);
217
+ remounted.aliases = aliases;
218
+
219
+ if (options?.context) {
220
+ const existing = remounted.contextTransform;
221
+ remounted.contextTransform = existing ? (ctx: unknown) => existing(options.context!(ctx)) : options.context;
222
+ }
223
+
224
+ const existingSubcommand = existingCommand.commands?.find((c) => c.name === name) as AnyPadroneCommand | undefined;
225
+ const mergedCommandObj = existingSubcommand ? mergeCommands(existingSubcommand, remounted) : remounted;
226
+
227
+ const commands = existingCommand.commands || [];
228
+ const existingIndex = commands.findIndex((c) => c.name === name);
229
+ const updatedCommands =
230
+ existingIndex >= 0
231
+ ? [...commands.slice(0, existingIndex), mergedCommandObj, ...commands.slice(existingIndex + 1)]
232
+ : [...commands, mergedCommandObj];
233
+
234
+ return createPadroneBuilder({ ...existingCommand, commands: updatedCommands }) as any;
235
+ },
236
+
237
+ intercept(metaOrFn: InterceptorMeta | PadroneInterceptorFn<any, any, any>, factory?: InterceptorFactory<any, any, any>) {
238
+ const registered: RegisteredInterceptor = toRegisteredInterceptor(metaOrFn, factory);
239
+ return createPadroneBuilder({
240
+ ...existingCommand,
241
+ interceptors: [...(existingCommand.interceptors ?? []), registered],
242
+ }) as any;
243
+ },
244
+
245
+ ...programMethods,
246
+
247
+ get info() {
248
+ return {
249
+ name: existingCommand.name,
250
+ title: existingCommand.title,
251
+ description: existingCommand.description,
252
+ version: existingCommand.version,
253
+ examples: existingCommand.examples,
254
+ deprecated: existingCommand.deprecated,
255
+ commands: (existingCommand.commands ?? []).map((c) => c.name),
256
+ };
257
+ },
258
+
259
+ '~types': {} as any,
260
+
261
+ [commandSymbol]: existingCommand,
262
+ } satisfies AnyPadroneProgram & { [commandSymbol]: AnyPadroneCommand } as any;
263
+
264
+ // Fix forward reference: execCtx.builder needs to reference the builder after it's created
265
+ execCtx.builder = builder;
266
+
267
+ return builder as TBuilder & { [commandSymbol]: AnyPadroneCommand };
268
+ }