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,15 +1,23 @@
1
+ import { execFile } from 'node:child_process';
2
+ import { readFile } from 'node:fs/promises';
3
+
4
+ import { parseBashCompletions } from './parsers/bash.ts';
1
5
  import { parseFishCompletions } from './parsers/fish.ts';
2
6
  import { parseHelpOutput } from './parsers/help.ts';
3
7
  import { mergeCommandMeta } from './parsers/merge.ts';
4
8
  import { parseZshCompletions } from './parsers/zsh.ts';
5
9
  import type { CommandMeta, GeneratorLogger } from './types.ts';
6
10
 
7
- export type DiscoverySource = 'help' | 'fish' | 'zsh';
11
+ export type DiscoverySource = 'help' | 'completion' | 'bash' | 'fish' | 'zsh';
8
12
 
9
13
  export interface DiscoveryOptions {
10
14
  /** The command to discover (e.g. 'gh', 'docker', 'kubectl'). */
11
15
  command: string;
12
- /** Which parsing sources to use. Default: ['help'] */
16
+ /**
17
+ * Which parsing sources to use. Default: ['help'].
18
+ * Use `'completion'` to auto-detect the best shell completion source
19
+ * by probing `<cmd> completion <shell>` (bash → fish → zsh).
20
+ */
13
21
  sources?: DiscoverySource[];
14
22
  /** Max subcommand depth. 0 = root only, undefined = unlimited. */
15
23
  depth?: number;
@@ -35,7 +43,10 @@ export interface DiscoveryResult {
35
43
  * parsing shell completion scripts.
36
44
  */
37
45
  export async function discoverCli(options: DiscoveryOptions): Promise<DiscoveryResult> {
38
- const { command, sources = ['help'], depth, delay = 50, log, timeout = 10000 } = options;
46
+ const { command, sources: rawSources = ['help'], depth, delay = 50, log, timeout = 10000 } = options;
47
+
48
+ // Resolve 'completion' source by probing for the best available shell
49
+ const sources = await resolveSources(rawSources, command, timeout, log);
39
50
 
40
51
  const warnings: string[] = [];
41
52
  let invocations = 0;
@@ -60,7 +71,18 @@ export async function discoverCli(options: DiscoveryOptions): Promise<DiscoveryR
60
71
  results.push(helpResult);
61
72
  }
62
73
 
63
- // Source 2: Fish completions
74
+ // Source 2: Bash completions
75
+ if (sources.includes('bash')) {
76
+ log?.info(`Parsing bash completions for ${command}...`);
77
+ const bashText = await getCompletionScript(command, 'bash', timeout);
78
+ if (bashText) {
79
+ results.push(parseBashCompletions(bashText));
80
+ } else {
81
+ warnings.push('Could not obtain bash completion script');
82
+ }
83
+ }
84
+
85
+ // Source 3: Fish completions
64
86
  if (sources.includes('fish')) {
65
87
  log?.info(`Parsing fish completions for ${command}...`);
66
88
  const fishText = await getCompletionScript(command, 'fish', timeout);
@@ -71,7 +93,7 @@ export async function discoverCli(options: DiscoveryOptions): Promise<DiscoveryR
71
93
  }
72
94
  }
73
95
 
74
- // Source 3: Zsh completions
96
+ // Source 4: Zsh completions
75
97
  if (sources.includes('zsh')) {
76
98
  log?.info(`Parsing zsh completions for ${command}...`);
77
99
  const zshText = await getCompletionScript(command, 'zsh', timeout);
@@ -163,25 +185,13 @@ async function runHelp(command: string, args: string[], timeout: number): Promis
163
185
  */
164
186
  async function runCommand(command: string, args: string[], timeout: number): Promise<string | null> {
165
187
  try {
166
- const proc = Bun.spawn([command, ...args], {
167
- stdout: 'pipe',
168
- stderr: 'pipe',
169
- stdin: 'ignore',
188
+ const { stdout, stderr } = await new Promise<{ stdout: string; stderr: string }>((resolve) => {
189
+ execFile(command, args, { timeout, maxBuffer: 10 * 1024 * 1024 }, (_error, stdout, stderr) => {
190
+ // Resolve even on non-zero exit — many CLIs exit non-zero on --help
191
+ resolve({ stdout: (stdout ?? '').trim(), stderr: (stderr ?? '').trim() });
192
+ });
170
193
  });
171
194
 
172
- const timer = setTimeout(() => proc.kill(), timeout);
173
-
174
- const [_exitCode, stdoutBuf, stderrBuf] = await Promise.all([
175
- proc.exited,
176
- new Response(proc.stdout).arrayBuffer(),
177
- new Response(proc.stderr).arrayBuffer(),
178
- ]);
179
-
180
- clearTimeout(timer);
181
-
182
- const stdout = new TextDecoder().decode(stdoutBuf).trim();
183
- const stderr = new TextDecoder().decode(stderrBuf).trim();
184
-
185
195
  // Some CLIs output help to stderr, some exit non-zero on --help
186
196
  const combined = stdout || stderr;
187
197
  if (!combined) return null;
@@ -199,7 +209,7 @@ async function runCommand(command: string, args: string[], timeout: number): Pro
199
209
  * Try to get a shell completion script for a command.
200
210
  * Checks both `<cmd> completion <shell>` and well-known file paths.
201
211
  */
202
- async function getCompletionScript(command: string, shell: 'fish' | 'zsh', timeout: number): Promise<string | null> {
212
+ async function getCompletionScript(command: string, shell: 'bash' | 'fish' | 'zsh', timeout: number): Promise<string | null> {
203
213
  // Try `<cmd> completion <shell>`
204
214
  const completionArgs = ['completion', shell];
205
215
  let result = await runCommand(command, completionArgs, timeout);
@@ -213,20 +223,62 @@ async function getCompletionScript(command: string, shell: 'fish' | 'zsh', timeo
213
223
  const paths =
214
224
  shell === 'fish'
215
225
  ? [`/usr/share/fish/vendor_completions.d/${command}.fish`, `/usr/local/share/fish/vendor_completions.d/${command}.fish`]
216
- : [`/usr/share/zsh/site-functions/_${command}`, `/usr/local/share/zsh/site-functions/_${command}`];
226
+ : shell === 'bash'
227
+ ? [
228
+ `/usr/share/bash-completion/completions/${command}`,
229
+ `/usr/local/share/bash-completion/completions/${command}`,
230
+ `/etc/bash_completion.d/${command}`,
231
+ ]
232
+ : [`/usr/share/zsh/site-functions/_${command}`, `/usr/local/share/zsh/site-functions/_${command}`];
217
233
 
218
234
  for (const path of paths) {
219
235
  try {
220
- const file = Bun.file(path);
221
- if (await file.exists()) {
222
- return await file.text();
223
- }
236
+ return await readFile(path, 'utf-8');
224
237
  } catch {}
225
238
  }
226
239
 
227
240
  return null;
228
241
  }
229
242
 
243
+ /**
244
+ * Detect the best shell for completion parsing by probing the command.
245
+ * Tries `<cmd> completion <shell>` for bash, fish, zsh (in that order).
246
+ * Returns the shell name if successful, or null if no completion command exists.
247
+ */
248
+ export async function detectCompletionShell(command: string, timeout = 5000): Promise<'bash' | 'fish' | 'zsh' | null> {
249
+ for (const shell of ['bash', 'fish', 'zsh'] as const) {
250
+ const result = await getCompletionScript(command, shell, timeout);
251
+ if (result) return shell;
252
+ }
253
+ return null;
254
+ }
255
+
256
+ /**
257
+ * Resolve 'completion' entries in the sources array by probing for the best available shell.
258
+ * Other sources are passed through unchanged.
259
+ */
260
+ async function resolveSources(
261
+ sources: DiscoverySource[],
262
+ command: string,
263
+ timeout: number,
264
+ log?: GeneratorLogger,
265
+ ): Promise<DiscoverySource[]> {
266
+ if (!sources.includes('completion')) return sources;
267
+
268
+ log?.info(`Probing ${command} for completion command...`);
269
+ const shell = await detectCompletionShell(command, timeout);
270
+
271
+ return sources.flatMap((s) => {
272
+ if (s !== 'completion') return s;
273
+ if (shell) {
274
+ log?.info(` Found ${shell} completion support`);
275
+ return shell;
276
+ }
277
+ log?.info(' No completion command found, skipping');
278
+ return [];
279
+ });
280
+ }
281
+
230
282
  function sleep(ms: number): Promise<void> {
231
283
  return new Promise((resolve) => setTimeout(resolve, ms));
232
284
  }
@@ -4,7 +4,7 @@
4
4
  export { createCodeBuilder } from './code-builder.ts';
5
5
  export type { DiscoveryOptions, DiscoveryResult, DiscoverySource } from './discovery.ts';
6
6
  // Discovery
7
- export { discoverCli } from './discovery.ts';
7
+ export { detectCompletionShell, discoverCli } from './discovery.ts';
8
8
  export { createFileEmitter } from './file-emitter.ts';
9
9
  // Generators
10
10
  export { generateBarrelFile } from './generators/barrel-file.ts';
@@ -13,6 +13,7 @@ export { generateCommandFile } from './generators/command-file.ts';
13
13
  export type { CommandTreeOptions } from './generators/command-tree.ts';
14
14
  export { generateCommandTree } from './generators/command-tree.ts';
15
15
  // Parsers
16
+ export { parseBashCompletions } from './parsers/bash.ts';
16
17
  export { parseFishCompletions } from './parsers/fish.ts';
17
18
  export { parseHelpOutput } from './parsers/help.ts';
18
19
  export { mergeCommandMeta } from './parsers/merge.ts';
@@ -0,0 +1,179 @@
1
+ import type { CommandMeta, FieldMeta } from '../types.ts';
2
+
3
+ /**
4
+ * Parse bash completion scripts into CommandMeta.
5
+ *
6
+ * Bash completions typically use `complete -F <func> <command>` and define
7
+ * a function that sets COMPREPLY. Common patterns:
8
+ *
9
+ * local commands="init build deploy"
10
+ * local args="--verbose --output --format"
11
+ * case "$prev" in --format) COMPREPLY=($(compgen -W "json yaml toml" ...)) ;;
12
+ * COMPREPLY=($(compgen -W "$commands" ...))
13
+ * COMPREPLY=($(compgen -W "$args" ...))
14
+ */
15
+ export function parseBashCompletions(text: string): CommandMeta {
16
+ const result: CommandMeta = {
17
+ name: '',
18
+ arguments: [],
19
+ subcommands: [],
20
+ };
21
+
22
+ // Detect command name from `complete -F _func <command>` or `complete -o ... -F _func <command>`
23
+ const completeMatch = text.match(/complete\s+(?:[^-]|-[^F])*-F\s+\S+\s+(\S+)/);
24
+ if (completeMatch) {
25
+ result.name = completeMatch[1]!;
26
+ }
27
+
28
+ // Fallback: detect from marker comments like ###-begin-<name>-completion-###
29
+ if (!result.name) {
30
+ const markerMatch = text.match(/###-begin-(\S+)-completion-###/);
31
+ if (markerMatch) {
32
+ result.name = markerMatch[1]!;
33
+ }
34
+ }
35
+
36
+ // Join continuation lines for easier parsing
37
+ const joined = text.replace(/\\\n\s*/g, ' ');
38
+
39
+ // Collect variable values: local commands="..." or local args="..."
40
+ const variables = extractVariables(joined);
41
+
42
+ // Extract subcommand names from commands variable or compgen -W "cmd1 cmd2"
43
+ const commandWords = variables.get('commands') ?? variables.get('cmds') ?? variables.get('subcommands');
44
+ if (commandWords) {
45
+ for (const name of splitWords(commandWords)) {
46
+ result.subcommands!.push({ name });
47
+ }
48
+ }
49
+
50
+ // Extract option names from args/opts/options variable
51
+ const argWords = variables.get('args') ?? variables.get('opts') ?? variables.get('options') ?? variables.get('flags');
52
+ const optionNames = new Set<string>();
53
+
54
+ if (argWords) {
55
+ for (const word of splitWords(argWords)) {
56
+ if (word.startsWith('-')) {
57
+ optionNames.add(word);
58
+ }
59
+ }
60
+ }
61
+
62
+ // Also scan for compgen -W patterns not tied to a case statement
63
+ const compgenRegex = /compgen\s+-W\s+["']([^"']+)["']/g;
64
+ let compgenMatch: RegExpExecArray | null;
65
+ while ((compgenMatch = compgenRegex.exec(joined)) !== null) {
66
+ // Skip variable references like $args, $commands
67
+ if (compgenMatch[1]!.startsWith('$')) continue;
68
+ for (const word of splitWords(compgenMatch[1]!)) {
69
+ if (word.startsWith('-')) {
70
+ optionNames.add(word);
71
+ }
72
+ }
73
+ }
74
+
75
+ // Parse case statement for option value completions (enum detection)
76
+ const enumMap = parseCaseStatement(joined);
77
+
78
+ // Build argument fields
79
+ const seenArgs = new Set<string>();
80
+
81
+ for (const rawName of optionNames) {
82
+ // Skip builtins
83
+ if (rawName === '--help' || rawName === '-h' || rawName === '--version' || rawName === '-V') continue;
84
+
85
+ const name = rawName.replace(/^-+/, '').replace(/-([a-z])/g, (_, c: string) => c.toUpperCase());
86
+ if (!name || seenArgs.has(name)) continue;
87
+ seenArgs.add(name);
88
+
89
+ // Check if this option has enum values from case statement
90
+ const enumValues = enumMap.get(rawName);
91
+
92
+ const isShort = rawName.startsWith('-') && !rawName.startsWith('--') && rawName.length === 2;
93
+ const field: FieldMeta = {
94
+ name,
95
+ type: enumValues ? 'enum' : 'string',
96
+ enumValues,
97
+ ambiguous: !enumValues,
98
+ };
99
+
100
+ // If there's a corresponding long-form, record alias
101
+ if (isShort) {
102
+ field.aliases = [rawName];
103
+ }
104
+
105
+ result.arguments!.push(field);
106
+ }
107
+
108
+ if (result.arguments!.length === 0) delete result.arguments;
109
+ if (result.subcommands!.length === 0) delete result.subcommands;
110
+
111
+ return result;
112
+ }
113
+
114
+ /**
115
+ * Extract variable assignments: `local VAR="value"`, `VAR="value"`, `local VAR='value'`
116
+ */
117
+ function extractVariables(text: string): Map<string, string> {
118
+ const vars = new Map<string, string>();
119
+ const regex = /(?:local\s+)?(\w+)=["']([^"']+)["']/g;
120
+ let match: RegExpExecArray | null;
121
+
122
+ while ((match = regex.exec(text)) !== null) {
123
+ vars.set(match[1]!, match[2]!);
124
+ }
125
+
126
+ return vars;
127
+ }
128
+
129
+ /**
130
+ * Parse case statements to find option → enum value mappings.
131
+ *
132
+ * Matches patterns like:
133
+ * case "$prev" in
134
+ * --format) COMPREPLY=($(compgen -W "json yaml toml" ...)) ;;
135
+ * --env|--environment) COMPREPLY=($(compgen -W "dev staging prod" ...)) ;;
136
+ */
137
+ function parseCaseStatement(text: string): Map<string, string[]> {
138
+ const result = new Map<string, string[]>();
139
+
140
+ // Find case blocks on $prev or similar variables
141
+ const caseRegex = /case\s+[^i]*\bin\b\s*([\s\S]*?)esac/g;
142
+ let caseMatch: RegExpExecArray | null;
143
+
144
+ while ((caseMatch = caseRegex.exec(text)) !== null) {
145
+ const body = caseMatch[1]!;
146
+
147
+ // Split into branches by ;; delimiter
148
+ const branches = body.split(';;');
149
+ for (const branch of branches) {
150
+ // Match pattern: --opt1|--opt2) ... compgen -W "values" ...
151
+ const patternMatch = branch.match(/^\s*([-\w|]+)\)/);
152
+ if (!patternMatch) continue;
153
+
154
+ // Find compgen -W "values" within this branch
155
+ const compgenMatch = branch.match(/compgen\s+-W\s+["']([^"']+)["']/);
156
+ if (!compgenMatch) continue;
157
+
158
+ const values = compgenMatch[1]!;
159
+ const words = splitWords(values).filter((w) => !w.startsWith('$'));
160
+ if (words.length === 0) continue;
161
+
162
+ for (const pattern of patternMatch[1]!.split('|')) {
163
+ const trimmed = pattern.trim();
164
+ if (trimmed.startsWith('-')) {
165
+ result.set(trimmed, words);
166
+ }
167
+ }
168
+ }
169
+ }
170
+
171
+ return result;
172
+ }
173
+
174
+ /**
175
+ * Split a space-separated word list, filtering empty strings.
176
+ */
177
+ function splitWords(text: string): string[] {
178
+ return text.split(/\s+/).filter(Boolean);
179
+ }
@@ -1,4 +1,5 @@
1
1
  import type { StandardSchemaV1 } from '@standard-schema/spec';
2
+ import { getJsonSchema } from '../core/args.ts';
2
3
  import type { FieldMeta } from './types.ts';
3
4
 
4
5
  interface SchemaToCodeResult {
@@ -57,7 +58,7 @@ function jsonSchemaPropertyToZod(prop: Record<string, any>, required: boolean, a
57
58
  */
58
59
  export function schemaToCode(schema: StandardSchemaV1): SchemaToCodeResult {
59
60
  try {
60
- const jsonSchema = (schema as any)['~standard'].jsonSchema.input({ target: 'draft-2020-12' }) as Record<string, any>;
61
+ const jsonSchema = getJsonSchema(schema as any) as Record<string, any>;
61
62
  return jsonSchemaToCode(jsonSchema);
62
63
  } catch {
63
64
  return { code: 'z.unknown()', imports: ['z'] };
@@ -0,0 +1,296 @@
1
+ import type { StandardJSONSchemaV1, StandardSchemaV1 } from '@standard-schema/spec';
2
+ import type { PadroneFieldMeta } from '../types/args-meta.ts';
3
+ import { camelToKebab } from '../util/shell-utils.ts';
4
+ import { asyncStreamRegistry } from '../util/stream.ts';
5
+
6
+ export type { PadroneArgsSchemaMeta, PadroneFieldMeta, SingleChar, StdinConfig } from '../types/args-meta.ts';
7
+
8
+ /** Extract the JSON schema from a Standard Schema, returning it as a plain record. */
9
+ export function getJsonSchema(schema: StandardJSONSchemaV1): Record<string, any> {
10
+ return schema['~standard'].jsonSchema.input({
11
+ target: 'draft-2020-12',
12
+ libraryOptions: { unrepresentable: 'any' },
13
+ }) as Record<string, any>;
14
+ }
15
+
16
+ function getFieldJsonSchema(schema: StandardJSONSchemaV1 | undefined, field: string): Record<string, any> | undefined {
17
+ if (!schema) return undefined;
18
+ try {
19
+ const jsonSchema = getJsonSchema(schema);
20
+ if (jsonSchema.type === 'object' && jsonSchema.properties) return jsonSchema.properties[field];
21
+ } catch {}
22
+ return undefined;
23
+ }
24
+
25
+ /**
26
+ * Checks if a field in the schema is an array type (e.g. `z.string().array()`).
27
+ */
28
+ export function isArrayField(schema: StandardJSONSchemaV1 | undefined, field: string): boolean {
29
+ return getFieldJsonSchema(schema, field)?.type === 'array';
30
+ }
31
+
32
+ /**
33
+ * Checks if a field is an async stream (marked with `asyncStream()` metadata).
34
+ * Returns the item schema if provided, or `true` if it's a plain string stream.
35
+ */
36
+ export function isAsyncStreamField(schema: StandardJSONSchemaV1 | undefined, field: string): { itemSchema?: StandardSchemaV1 } | false {
37
+ const prop = getFieldJsonSchema(schema, field);
38
+ const asyncStreamId = prop?.asyncStream;
39
+ if (asyncStreamId && asyncStreamRegistry.has(asyncStreamId)) {
40
+ const meta = asyncStreamRegistry.get(asyncStreamId);
41
+ return { itemSchema: meta?.itemSchema };
42
+ }
43
+
44
+ return false;
45
+ }
46
+
47
+ /**
48
+ * Parse positional configuration to extract names and variadic info.
49
+ */
50
+ export function parsePositionalConfig(positional: readonly string[]): { name: string; variadic: boolean }[] {
51
+ return positional.map((p) => {
52
+ const variadic = p.startsWith('...');
53
+ const name = variadic ? p.slice(3) : p;
54
+ return { name, variadic };
55
+ });
56
+ }
57
+
58
+ /**
59
+ * Result type for extractSchemaMetadata function.
60
+ */
61
+ interface SchemaMetadataResult {
62
+ /** Single-char flags: maps flag char → full arg name (e.g. `{ v: 'verbose' }`) */
63
+ flags: Record<string, string>;
64
+ /** Multi-char aliases: maps alias → full arg name (e.g. `{ 'dry-run': 'dryRun' }`) */
65
+ aliases: Record<string, string>;
66
+ }
67
+
68
+ function addEntries(target: Record<string, string>, key: string, items: string | readonly string[], filter?: (item: string) => boolean) {
69
+ const list = typeof items === 'string' ? [items] : items;
70
+ for (const item of list) {
71
+ if (typeof item === 'string' && item && item !== key && !(item in target) && (!filter || filter(item))) {
72
+ target[item] = key;
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Extract all arg metadata from schema and meta in a single pass.
79
+ * Returns flags (single-char, stackable) and aliases (multi-char, long names) separately.
80
+ * When `autoAlias` is true (default), camelCase property names automatically get kebab-case aliases.
81
+ */
82
+ export function extractSchemaMetadata(
83
+ schema: StandardJSONSchemaV1,
84
+ meta?: Record<string, PadroneFieldMeta | undefined>,
85
+ autoAlias?: boolean,
86
+ ): SchemaMetadataResult {
87
+ const flags: Record<string, string> = {};
88
+ const aliases: Record<string, string> = {};
89
+
90
+ // Extract from meta object
91
+ if (meta) {
92
+ for (const [key, value] of Object.entries(meta)) {
93
+ if (!value) continue;
94
+
95
+ if (value.flags) {
96
+ addEntries(flags, key, value.flags, (item) => item.length === 1);
97
+ }
98
+ if (value.alias) {
99
+ addEntries(aliases, key, value.alias, (item) => item.length > 1);
100
+ }
101
+ }
102
+ }
103
+
104
+ // Extract from JSON schema properties
105
+ try {
106
+ const jsonSchema = getJsonSchema(schema) as Record<string, any>;
107
+ if (jsonSchema.type === 'object' && jsonSchema.properties) {
108
+ for (const [propertyName, propertySchema] of Object.entries(jsonSchema.properties as Record<string, any>)) {
109
+ if (!propertySchema) continue;
110
+
111
+ // Extract flags from schema `.meta({ flags: ... })`
112
+ const propFlags = propertySchema.flags;
113
+ if (propFlags) {
114
+ addEntries(flags, propertyName, propFlags, (item) => item.length === 1);
115
+ }
116
+
117
+ // Extract aliases from schema `.meta({ alias: ... })`
118
+ const propAlias = propertySchema.alias;
119
+ if (propAlias) {
120
+ const list = typeof propAlias === 'string' ? [propAlias] : propAlias;
121
+ if (Array.isArray(list)) {
122
+ addEntries(aliases, propertyName, list, (item) => item.length > 1);
123
+ }
124
+ }
125
+
126
+ // Auto-generate kebab-case alias for camelCase property names
127
+ if (autoAlias !== false) {
128
+ const kebab = camelToKebab(propertyName);
129
+ if (kebab && !(kebab in aliases)) {
130
+ aliases[kebab] = propertyName;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ } catch {
136
+ // Ignore errors from JSON schema generation
137
+ }
138
+
139
+ return { flags, aliases };
140
+ }
141
+
142
+ function preprocessMappings(data: Record<string, unknown>, mappings: Record<string, string>): Record<string, unknown> {
143
+ const result = { ...data };
144
+
145
+ for (const [mappedKey, fullArgName] of Object.entries(mappings)) {
146
+ if (mappedKey in data && mappedKey !== fullArgName) {
147
+ const mappedValue = data[mappedKey];
148
+ // Prefer full arg name if it exists
149
+ if (!(fullArgName in result)) result[fullArgName] = mappedValue;
150
+ delete result[mappedKey];
151
+ }
152
+ }
153
+
154
+ return result;
155
+ }
156
+
157
+ /**
158
+ * Apply values to arguments using "set if not present" semantics.
159
+ * Existing values take precedence — only fills in undefined or missing keys.
160
+ */
161
+ export function applyValues(data: Record<string, unknown>, values: Record<string, unknown>): Record<string, unknown> {
162
+ const result = { ...data };
163
+
164
+ for (const [key, value] of Object.entries(values)) {
165
+ if (key in result && result[key] !== undefined) continue;
166
+ if (value !== undefined) {
167
+ result[key] = value;
168
+ }
169
+ }
170
+
171
+ return result;
172
+ }
173
+
174
+ /** Applies flag and alias mappings to raw arguments. */
175
+ export function preprocessArgs(
176
+ data: Record<string, unknown>,
177
+ ctx: { flags?: Record<string, string>; aliases?: Record<string, string> },
178
+ ): Record<string, unknown> {
179
+ let result = { ...data };
180
+
181
+ if (ctx.flags && Object.keys(ctx.flags).length > 0) {
182
+ result = preprocessMappings(result, ctx.flags);
183
+ }
184
+ if (ctx.aliases && Object.keys(ctx.aliases).length > 0) {
185
+ result = preprocessMappings(result, ctx.aliases);
186
+ }
187
+
188
+ return result;
189
+ }
190
+
191
+ /**
192
+ * Auto-coerce CLI string values to match the expected schema types.
193
+ * Handles: string → number, string → boolean for primitive schema fields.
194
+ * Arrays of primitives are also coerced element-wise.
195
+ */
196
+ export function coerceArgs(data: Record<string, unknown>, schema: StandardJSONSchemaV1): Record<string, unknown> {
197
+ let properties: Record<string, any>;
198
+ try {
199
+ const jsonSchema = getJsonSchema(schema) as Record<string, any>;
200
+ if (jsonSchema.type !== 'object' || !jsonSchema.properties) return data;
201
+ properties = jsonSchema.properties;
202
+ } catch {
203
+ return data;
204
+ }
205
+
206
+ const result = { ...data };
207
+
208
+ for (const [key, value] of Object.entries(result)) {
209
+ const prop = properties[key];
210
+ if (!prop) continue;
211
+
212
+ const targetType = prop.type as string | undefined;
213
+
214
+ if (targetType === 'number' || targetType === 'integer') {
215
+ if (typeof value === 'string') {
216
+ const num = Number(value);
217
+ if (!Number.isNaN(num)) result[key] = num;
218
+ }
219
+ } else if (targetType === 'boolean') {
220
+ if (typeof value === 'string') {
221
+ const lower = value.toLowerCase();
222
+ if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') result[key] = true;
223
+ else if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') result[key] = false;
224
+ }
225
+ } else if (targetType === 'array') {
226
+ // Coerce single items to array
227
+ const arr = Array.isArray(value) ? value : [value];
228
+ const itemType = prop.items?.type as string | undefined;
229
+ if (itemType === 'number' || itemType === 'integer') {
230
+ result[key] = arr.map((v) => {
231
+ if (typeof v === 'string') {
232
+ const num = Number(v);
233
+ return Number.isNaN(num) ? v : num;
234
+ }
235
+ return v;
236
+ });
237
+ } else if (itemType === 'boolean') {
238
+ result[key] = arr.map((v) => {
239
+ if (typeof v === 'string') {
240
+ const lower = v.toLowerCase();
241
+ if (lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on') return true;
242
+ if (lower === 'false' || lower === '0' || lower === 'no' || lower === 'off') return false;
243
+ }
244
+ return v;
245
+ });
246
+ } else if (!Array.isArray(value)) {
247
+ result[key] = arr;
248
+ }
249
+ }
250
+ }
251
+
252
+ return result;
253
+ }
254
+
255
+ /**
256
+ * Detect unknown keys in the args that don't match any schema property.
257
+ * Returns an array of { key } for each unknown key.
258
+ * Framework-reserved keys (--config, -c) are always allowed.
259
+ */
260
+ export function detectUnknownArgs(
261
+ data: Record<string, unknown>,
262
+ schema: StandardJSONSchemaV1,
263
+ flags: Record<string, string>,
264
+ aliases: Record<string, string>,
265
+ ): { key: string }[] {
266
+ let properties: Record<string, any>;
267
+ let isLoose = false;
268
+ try {
269
+ const jsonSchema = getJsonSchema(schema) as Record<string, any>;
270
+ if (jsonSchema.type !== 'object' || !jsonSchema.properties) return [];
271
+ properties = jsonSchema.properties;
272
+ // If additionalProperties is set (true, {}, or a schema), the schema allows extra keys
273
+ if (jsonSchema.additionalProperties !== undefined && jsonSchema.additionalProperties !== false) isLoose = true;
274
+ } catch {
275
+ return [];
276
+ }
277
+
278
+ if (isLoose) return [];
279
+
280
+ const knownKeys = new Set<string>([
281
+ ...Object.keys(properties),
282
+ ...Object.keys(flags),
283
+ ...Object.values(flags),
284
+ ...Object.keys(aliases),
285
+ ...Object.values(aliases),
286
+ ]);
287
+ const unknowns: { key: string }[] = [];
288
+
289
+ for (const key of Object.keys(data)) {
290
+ if (!knownKeys.has(key)) {
291
+ unknowns.push({ key });
292
+ }
293
+ }
294
+
295
+ return unknowns;
296
+ }