padrone 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +115 -0
- package/README.md +108 -283
- package/dist/args-Cnq0nwSM.mjs +272 -0
- package/dist/args-Cnq0nwSM.mjs.map +1 -0
- package/dist/codegen/index.d.mts +28 -3
- package/dist/codegen/index.d.mts.map +1 -1
- package/dist/codegen/index.mjs +169 -19
- package/dist/codegen/index.mjs.map +1 -1
- package/dist/commands-B_gufyR9.mjs +514 -0
- package/dist/commands-B_gufyR9.mjs.map +1 -0
- package/dist/{completion.mjs → completion-BEuflbDO.mjs} +86 -108
- package/dist/completion-BEuflbDO.mjs.map +1 -0
- package/dist/docs/index.d.mts +22 -2
- package/dist/docs/index.d.mts.map +1 -1
- package/dist/docs/index.mjs +92 -7
- package/dist/docs/index.mjs.map +1 -1
- package/dist/errors-CL63UOzt.mjs +137 -0
- package/dist/errors-CL63UOzt.mjs.map +1 -0
- package/dist/{formatter-ClUK5hcQ.d.mts → formatter-DrvhDMrq.d.mts} +35 -6
- package/dist/formatter-DrvhDMrq.d.mts.map +1 -0
- package/dist/help-B5Kk83of.mjs +849 -0
- package/dist/help-B5Kk83of.mjs.map +1 -0
- package/dist/index-BaU3X6dY.d.mts +1178 -0
- package/dist/index-BaU3X6dY.d.mts.map +1 -0
- package/dist/index.d.mts +763 -36
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3608 -1534
- package/dist/index.mjs.map +1 -1
- package/dist/mcp-BM-d0nZi.mjs +377 -0
- package/dist/mcp-BM-d0nZi.mjs.map +1 -0
- package/dist/serve-Bk0JUlCj.mjs +402 -0
- package/dist/serve-Bk0JUlCj.mjs.map +1 -0
- package/dist/stream-DC4H8YTx.mjs +77 -0
- package/dist/stream-DC4H8YTx.mjs.map +1 -0
- package/dist/test.d.mts +5 -8
- package/dist/test.d.mts.map +1 -1
- package/dist/test.mjs +5 -27
- package/dist/test.mjs.map +1 -1
- package/dist/{update-check-EbNDkzyV.mjs → update-check-CZ2VqjnV.mjs} +16 -17
- package/dist/update-check-CZ2VqjnV.mjs.map +1 -0
- package/dist/zod.d.mts +32 -0
- package/dist/zod.d.mts.map +1 -0
- package/dist/zod.mjs +50 -0
- package/dist/zod.mjs.map +1 -0
- package/package.json +20 -9
- package/src/cli/completions.ts +14 -11
- package/src/cli/docs.ts +13 -16
- package/src/cli/doctor.ts +213 -24
- package/src/cli/index.ts +28 -82
- package/src/cli/init.ts +12 -10
- package/src/cli/link.ts +22 -18
- package/src/cli/wrap.ts +14 -11
- package/src/codegen/discovery.ts +80 -28
- package/src/codegen/index.ts +2 -1
- package/src/codegen/parsers/bash.ts +179 -0
- package/src/codegen/schema-to-code.ts +2 -1
- package/src/core/args.ts +296 -0
- package/src/core/commands.ts +373 -0
- package/src/core/create.ts +268 -0
- package/src/{runtime.ts → core/default-runtime.ts} +70 -135
- package/src/{errors.ts → core/errors.ts} +22 -0
- package/src/core/exec.ts +259 -0
- package/src/core/interceptors.ts +302 -0
- package/src/{parse.ts → core/parse.ts} +36 -89
- package/src/core/program-methods.ts +301 -0
- package/src/core/results.ts +229 -0
- package/src/core/runtime.ts +246 -0
- package/src/core/validate.ts +247 -0
- package/src/docs/index.ts +124 -11
- package/src/extension/auto-output.ts +95 -0
- package/src/extension/color.ts +38 -0
- package/src/extension/completion.ts +49 -0
- package/src/extension/config.ts +262 -0
- package/src/extension/env.ts +101 -0
- package/src/extension/help.ts +192 -0
- package/src/extension/index.ts +43 -0
- package/src/extension/ink.ts +93 -0
- package/src/extension/interactive.ts +106 -0
- package/src/extension/logger.ts +214 -0
- package/src/extension/man.ts +51 -0
- package/src/extension/mcp.ts +52 -0
- package/src/extension/progress-renderer.ts +338 -0
- package/src/extension/progress.ts +299 -0
- package/src/extension/repl.ts +94 -0
- package/src/extension/serve.ts +48 -0
- package/src/extension/signal.ts +87 -0
- package/src/extension/stdin.ts +62 -0
- package/src/extension/suggestions.ts +114 -0
- package/src/extension/timing.ts +81 -0
- package/src/extension/tracing.ts +175 -0
- package/src/extension/update-check.ts +77 -0
- package/src/extension/utils.ts +51 -0
- package/src/extension/version.ts +63 -0
- package/src/{completion.ts → feature/completion.ts} +130 -57
- package/src/{interactive.ts → feature/interactive.ts} +47 -6
- package/src/feature/mcp.ts +387 -0
- package/src/{repl-loop.ts → feature/repl-loop.ts} +26 -16
- package/src/feature/serve.ts +438 -0
- package/src/feature/test.ts +262 -0
- package/src/{update-check.ts → feature/update-check.ts} +16 -16
- package/src/{wrap.ts → feature/wrap.ts} +27 -27
- package/src/index.ts +120 -11
- package/src/output/colorizer.ts +154 -0
- package/src/{formatter.ts → output/formatter.ts} +281 -135
- package/src/{help.ts → output/help.ts} +62 -15
- package/src/{zod.d.ts → schema/zod.d.ts} +1 -1
- package/src/schema/zod.ts +50 -0
- package/src/test.ts +2 -285
- package/src/types/args-meta.ts +151 -0
- package/src/types/builder.ts +697 -0
- package/src/types/command.ts +157 -0
- package/src/types/index.ts +59 -0
- package/src/types/interceptor.ts +296 -0
- package/src/types/preferences.ts +83 -0
- package/src/types/result.ts +71 -0
- package/src/types/schema.ts +19 -0
- package/src/util/dotenv.ts +244 -0
- package/src/{shell-utils.ts → util/shell-utils.ts} +26 -9
- package/src/util/stream.ts +101 -0
- package/src/{type-helpers.ts → util/type-helpers.ts} +23 -16
- package/src/{type-utils.ts → util/type-utils.ts} +99 -37
- package/src/util/utils.ts +51 -0
- package/src/zod.ts +1 -0
- package/dist/args-CVDbyyzG.mjs +0 -199
- package/dist/args-CVDbyyzG.mjs.map +0 -1
- package/dist/chunk-y_GBKt04.mjs +0 -5
- package/dist/completion.d.mts +0 -64
- package/dist/completion.d.mts.map +0 -1
- package/dist/completion.mjs.map +0 -1
- package/dist/formatter-ClUK5hcQ.d.mts.map +0 -1
- package/dist/help-CcBe91bV.mjs +0 -1254
- package/dist/help-CcBe91bV.mjs.map +0 -1
- package/dist/types-DjIdJN5G.d.mts +0 -1059
- package/dist/types-DjIdJN5G.d.mts.map +0 -1
- package/dist/update-check-EbNDkzyV.mjs.map +0 -1
- package/src/args.ts +0 -461
- package/src/colorizer.ts +0 -41
- package/src/command-utils.ts +0 -532
- package/src/create.ts +0 -1477
- package/src/types.ts +0 -1109
- package/src/utils.ts +0 -140
package/src/codegen/discovery.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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:
|
|
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
|
|
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
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
:
|
|
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
|
-
|
|
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
|
}
|
package/src/codegen/index.ts
CHANGED
|
@@ -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)
|
|
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'] };
|
package/src/core/args.ts
ADDED
|
@@ -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
|
+
}
|