clap-ts 0.1.0 → 0.2.1
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/README.md +57 -2
- package/dist/completions.d.ts +40 -0
- package/dist/completions.js +523 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -0
- package/dist/types.d.ts +6 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -515,6 +515,62 @@ runMain(rootCommand, {
|
|
|
515
515
|
});
|
|
516
516
|
```
|
|
517
517
|
|
|
518
|
+
### Shell Completions
|
|
519
|
+
|
|
520
|
+
Add Tab completion for bash, zsh, fish, and powershell with one line:
|
|
521
|
+
|
|
522
|
+
```ts
|
|
523
|
+
import { defineCommand, runMain, withCompletions } from 'clap-ts';
|
|
524
|
+
|
|
525
|
+
const root = defineCommand({
|
|
526
|
+
meta: { name: 'my-cli', version: '1.0.0' },
|
|
527
|
+
args: { /* ... */ },
|
|
528
|
+
subCommands: { /* ... */ },
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
// Auto-adds a `completions` subcommand
|
|
532
|
+
runMain(withCompletions(root));
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
Users then enable completions in their shell:
|
|
536
|
+
|
|
537
|
+
```bash
|
|
538
|
+
# bash - add to ~/.bashrc
|
|
539
|
+
eval "$(my-cli completions bash)"
|
|
540
|
+
|
|
541
|
+
# zsh - add to ~/.zshrc
|
|
542
|
+
eval "$(my-cli completions zsh)"
|
|
543
|
+
|
|
544
|
+
# fish - save to completions dir
|
|
545
|
+
my-cli completions fish > ~/.config/fish/completions/my-cli.fish
|
|
546
|
+
|
|
547
|
+
# powershell - add to $PROFILE
|
|
548
|
+
my-cli completions powershell >> $PROFILE
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Generated scripts support flags, subcommands, aliases, enum values, and value hints (file/dir completion):
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
const cmd = defineCommand({
|
|
555
|
+
meta: { name: 'tool' },
|
|
556
|
+
args: {
|
|
557
|
+
config: { type: 'string', valueHint: 'filePath' }, // Tab completes files
|
|
558
|
+
outDir: { type: 'string', valueHint: 'dirPath' }, // Tab completes directories
|
|
559
|
+
host: { type: 'string', valueHint: 'hostname' }, // Tab completes hostnames
|
|
560
|
+
env: { type: 'string', valueParser: ['dev', 'prod'] }, // Tab shows dev, prod
|
|
561
|
+
},
|
|
562
|
+
});
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
You can also generate scripts manually without the subcommand:
|
|
566
|
+
|
|
567
|
+
```ts
|
|
568
|
+
import { generateCompletions } from 'clap-ts';
|
|
569
|
+
|
|
570
|
+
const bashScript = generateCompletions(root, 'bash');
|
|
571
|
+
const zshScript = generateCompletions(root, 'zsh', 'custom-binary-name');
|
|
572
|
+
```
|
|
573
|
+
|
|
518
574
|
### runMain
|
|
519
575
|
|
|
520
576
|
Entry point for CLI applications. Handles argv parsing, subcommand resolution, validation, help/version, and error display.
|
|
@@ -733,12 +789,11 @@ bun run bench/parse.bench.ts
|
|
|
733
789
|
| hidePossibleValues | Yes | Yes |
|
|
734
790
|
| Hidden args/commands | Yes | Yes |
|
|
735
791
|
| Type-safe parsed args | derive macro | generics |
|
|
736
|
-
| Shell completions | Yes |
|
|
792
|
+
| Shell completions (bash/zsh/fish/powershell) | Yes | Yes |
|
|
737
793
|
| Man page generation | Yes | Not yet |
|
|
738
794
|
|
|
739
795
|
## Roadmap
|
|
740
796
|
|
|
741
|
-
- Shell completion generation (bash/zsh/fish)
|
|
742
797
|
- Man page generation
|
|
743
798
|
- Markdown help output
|
|
744
799
|
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion generation -- static scripts and dynamic runtime completions.
|
|
3
|
+
* Matches clap_complete's feature set: bash, zsh, fish, powershell.
|
|
4
|
+
*
|
|
5
|
+
* Two modes:
|
|
6
|
+
* 1. Static: generateCompletions() returns a shell script string to source
|
|
7
|
+
* 2. Dynamic: completeEnv() checks env vars, outputs completions, returns true if handled
|
|
8
|
+
*/
|
|
9
|
+
import type { CommandDef, Shell } from './types.js';
|
|
10
|
+
/**
|
|
11
|
+
* Generate a shell completion script for the given command and shell.
|
|
12
|
+
*
|
|
13
|
+
* Usage:
|
|
14
|
+
* ```ts
|
|
15
|
+
* const script = generateCompletions(rootCommand, 'bash', 'my-cli');
|
|
16
|
+
* fs.writeFileSync('completions.bash', script);
|
|
17
|
+
* ```
|
|
18
|
+
*
|
|
19
|
+
* Users source the generated script in their shell config:
|
|
20
|
+
* - bash: `source completions.bash` or copy to `~/.local/share/bash-completion/completions/`
|
|
21
|
+
* - zsh: copy to a directory in `$fpath` (e.g., `~/.zsh/completions/`)
|
|
22
|
+
* - fish: copy to `~/.config/fish/completions/`
|
|
23
|
+
* - powershell: add to `$PROFILE`
|
|
24
|
+
*/
|
|
25
|
+
export declare function generateCompletions(command: CommandDef, shell: Shell, binaryName?: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Return a new command with a `completions` subcommand auto-injected.
|
|
28
|
+
* The subcommand generates shell completion scripts when invoked.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const root = defineCommand({ ... });
|
|
32
|
+
* runMain(withCompletions(root));
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* Then users run:
|
|
36
|
+
* ```bash
|
|
37
|
+
* eval "$(my-cli completions bash)"
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
export declare function withCompletions(rootCommand: CommandDef<any>): CommandDef<any>;
|
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion generation -- static scripts and dynamic runtime completions.
|
|
3
|
+
* Matches clap_complete's feature set: bash, zsh, fish, powershell.
|
|
4
|
+
*
|
|
5
|
+
* Two modes:
|
|
6
|
+
* 1. Static: generateCompletions() returns a shell script string to source
|
|
7
|
+
* 2. Dynamic: completeEnv() checks env vars, outputs completions, returns true if handled
|
|
8
|
+
*/
|
|
9
|
+
/** Extract completion-relevant data from a CommandDef tree. */
|
|
10
|
+
function buildCompletionTree(command, name) {
|
|
11
|
+
const flags = [];
|
|
12
|
+
const argsDef = command.args ?? {};
|
|
13
|
+
for (const [key, def] of Object.entries(argsDef)) {
|
|
14
|
+
if (def.type === 'positional') {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const longName = def.long ?? key;
|
|
18
|
+
flags.push({
|
|
19
|
+
key,
|
|
20
|
+
short: def.short,
|
|
21
|
+
long: longName,
|
|
22
|
+
description: def.description ?? '',
|
|
23
|
+
takesValue: def.type !== 'boolean' && def.action !== 'count',
|
|
24
|
+
possibleValues: Array.isArray(def.valueParser) ? def.valueParser : [],
|
|
25
|
+
valueHint: def.valueHint,
|
|
26
|
+
hidden: def.hidden ?? false,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
// Always include --help and --version
|
|
30
|
+
flags.push({ key: 'help', short: 'h', long: 'help', description: 'Print help', takesValue: false, possibleValues: [], hidden: false });
|
|
31
|
+
if (command.meta.version) {
|
|
32
|
+
flags.push({ key: 'version', short: 'V', long: 'version', description: 'Print version', takesValue: false, possibleValues: [], hidden: false });
|
|
33
|
+
}
|
|
34
|
+
const subcommands = [];
|
|
35
|
+
const childNodes = new Map();
|
|
36
|
+
if (command.subCommands) {
|
|
37
|
+
for (const [subName, subDef] of Object.entries(command.subCommands)) {
|
|
38
|
+
subcommands.push({
|
|
39
|
+
name: subName,
|
|
40
|
+
description: subDef.meta.description ?? '',
|
|
41
|
+
aliases: subDef.meta.aliases ?? [],
|
|
42
|
+
hidden: subDef.meta.hidden ?? false,
|
|
43
|
+
});
|
|
44
|
+
childNodes.set(subName, buildCompletionTree(subDef, subName));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
name: name ?? command.meta.name,
|
|
49
|
+
flags,
|
|
50
|
+
subcommands,
|
|
51
|
+
childNodes,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// ---- Helpers ----
|
|
55
|
+
/** Escape single quotes for shell strings. */
|
|
56
|
+
function esc(s) {
|
|
57
|
+
return s.replaceAll("'", "'\\''");
|
|
58
|
+
}
|
|
59
|
+
/** Escape double quotes for shell strings. */
|
|
60
|
+
function escDq(s) {
|
|
61
|
+
return s.replaceAll('"', '\\"').replaceAll('$', '\\$').replaceAll('`', '\\`');
|
|
62
|
+
}
|
|
63
|
+
/** Sanitize a name for use as a shell function/variable name. */
|
|
64
|
+
function sanitize(s) {
|
|
65
|
+
return s.replaceAll(/[^a-zA-Z0-9_]/g, '_');
|
|
66
|
+
}
|
|
67
|
+
// ---- Bash Generator ----
|
|
68
|
+
function generateBash(root, binaryName) {
|
|
69
|
+
const funcName = `_${sanitize(binaryName)}`;
|
|
70
|
+
const lines = [];
|
|
71
|
+
lines.push(`# bash completion for ${binaryName}`);
|
|
72
|
+
lines.push(`# Generated by clap-ts`);
|
|
73
|
+
lines.push('');
|
|
74
|
+
// Generate completion function for each command level
|
|
75
|
+
generateBashFunction(root, funcName, binaryName, lines);
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(`complete -o default -o bashdefault -F ${funcName} ${binaryName}`);
|
|
78
|
+
lines.push('');
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
function generateBashFunction(node, funcName, fullCommand, lines) {
|
|
82
|
+
lines.push(`${funcName}() {`);
|
|
83
|
+
lines.push(' local cur prev words cword');
|
|
84
|
+
lines.push(' _init_completion || return');
|
|
85
|
+
lines.push('');
|
|
86
|
+
// Build flag list
|
|
87
|
+
const visibleFlags = node.flags.filter((f) => !f.hidden);
|
|
88
|
+
const flagWords = [];
|
|
89
|
+
for (const f of visibleFlags) {
|
|
90
|
+
flagWords.push(`--${f.long}`);
|
|
91
|
+
if (f.short) {
|
|
92
|
+
flagWords.push(`-${f.short}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Build subcommand list
|
|
96
|
+
const visibleSubs = node.subcommands.filter((s) => !s.hidden);
|
|
97
|
+
const subWords = visibleSubs.map((s) => s.name);
|
|
98
|
+
// Handle value completion for flags that take values
|
|
99
|
+
const valueFlagCases = [];
|
|
100
|
+
for (const f of visibleFlags) {
|
|
101
|
+
if (!f.takesValue)
|
|
102
|
+
continue;
|
|
103
|
+
const flagNames = [`--${f.long}`];
|
|
104
|
+
if (f.short)
|
|
105
|
+
flagNames.push(`-${f.short}`);
|
|
106
|
+
const pattern = flagNames.join('|');
|
|
107
|
+
if (f.possibleValues.length > 0) {
|
|
108
|
+
valueFlagCases.push(` ${pattern})`);
|
|
109
|
+
valueFlagCases.push(` COMPREPLY=( $(compgen -W '${f.possibleValues.join(' ')}' -- "$cur") )`);
|
|
110
|
+
valueFlagCases.push(' return ;;');
|
|
111
|
+
}
|
|
112
|
+
else if (f.valueHint) {
|
|
113
|
+
const compgen = bashValueHintCompgen(f.valueHint);
|
|
114
|
+
if (compgen) {
|
|
115
|
+
valueFlagCases.push(` ${pattern})`);
|
|
116
|
+
valueFlagCases.push(` ${compgen}`);
|
|
117
|
+
valueFlagCases.push(' return ;;');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (valueFlagCases.length > 0) {
|
|
122
|
+
lines.push(' case "$prev" in');
|
|
123
|
+
lines.push(...valueFlagCases);
|
|
124
|
+
lines.push(' esac');
|
|
125
|
+
lines.push('');
|
|
126
|
+
}
|
|
127
|
+
// Handle subcommand dispatch
|
|
128
|
+
if (visibleSubs.length > 0) {
|
|
129
|
+
lines.push(' # Check for subcommand');
|
|
130
|
+
lines.push(' local subcmd=""');
|
|
131
|
+
lines.push(' for ((i=1; i < cword; i++)); do');
|
|
132
|
+
lines.push(' case "${words[i]}" in');
|
|
133
|
+
for (const sub of visibleSubs) {
|
|
134
|
+
const allNames = [sub.name, ...sub.aliases];
|
|
135
|
+
lines.push(` ${allNames.join('|')})`);
|
|
136
|
+
lines.push(` subcmd="${sub.name}"; break ;;`);
|
|
137
|
+
}
|
|
138
|
+
lines.push(' esac');
|
|
139
|
+
lines.push(' done');
|
|
140
|
+
lines.push('');
|
|
141
|
+
// Dispatch to child function
|
|
142
|
+
lines.push(' case "$subcmd" in');
|
|
143
|
+
for (const sub of visibleSubs) {
|
|
144
|
+
const childFunc = `${funcName}_${sanitize(sub.name)}`;
|
|
145
|
+
lines.push(` ${sub.name})`);
|
|
146
|
+
lines.push(` ${childFunc}; return ;;`);
|
|
147
|
+
}
|
|
148
|
+
lines.push(' esac');
|
|
149
|
+
lines.push('');
|
|
150
|
+
}
|
|
151
|
+
// Default: complete with flags and subcommands
|
|
152
|
+
const allWords = [...flagWords, ...subWords].join(' ');
|
|
153
|
+
lines.push(` COMPREPLY=( $(compgen -W '${allWords}' -- "$cur") )`);
|
|
154
|
+
lines.push('}');
|
|
155
|
+
lines.push('');
|
|
156
|
+
// Recurse for child commands
|
|
157
|
+
for (const sub of visibleSubs) {
|
|
158
|
+
const childNode = node.childNodes.get(sub.name);
|
|
159
|
+
if (childNode) {
|
|
160
|
+
const childFunc = `${funcName}_${sanitize(sub.name)}`;
|
|
161
|
+
generateBashFunction(childNode, childFunc, `${fullCommand} ${sub.name}`, lines);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function bashValueHintCompgen(hint) {
|
|
166
|
+
switch (hint) {
|
|
167
|
+
case 'filePath': return 'COMPREPLY=( $(compgen -f -- "$cur") )';
|
|
168
|
+
case 'dirPath': return 'COMPREPLY=( $(compgen -d -- "$cur") )';
|
|
169
|
+
case 'anyPath': return 'COMPREPLY=( $(compgen -f -- "$cur") )';
|
|
170
|
+
case 'executablePath': return 'COMPREPLY=( $(compgen -c -- "$cur") )';
|
|
171
|
+
case 'commandName': return 'COMPREPLY=( $(compgen -c -- "$cur") )';
|
|
172
|
+
case 'hostname': return 'COMPREPLY=( $(compgen -A hostname -- "$cur") )';
|
|
173
|
+
case 'username': return 'COMPREPLY=( $(compgen -u -- "$cur") )';
|
|
174
|
+
case 'url': return undefined;
|
|
175
|
+
case 'emailAddress': return undefined;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ---- Zsh Generator ----
|
|
179
|
+
function generateZsh(root, binaryName) {
|
|
180
|
+
const funcName = `_${sanitize(binaryName)}`;
|
|
181
|
+
const lines = [];
|
|
182
|
+
lines.push(`#compdef ${binaryName}`);
|
|
183
|
+
lines.push('');
|
|
184
|
+
lines.push(`# zsh completion for ${binaryName}`);
|
|
185
|
+
lines.push('# Generated by clap-ts');
|
|
186
|
+
lines.push('');
|
|
187
|
+
generateZshFunction(root, funcName, lines);
|
|
188
|
+
lines.push('');
|
|
189
|
+
lines.push(`if [ "$funcstack[1]" = "${funcName}" ]; then`);
|
|
190
|
+
lines.push(` ${funcName} "$@"`);
|
|
191
|
+
lines.push('else');
|
|
192
|
+
lines.push(` compdef ${funcName} ${binaryName}`);
|
|
193
|
+
lines.push('fi');
|
|
194
|
+
lines.push('');
|
|
195
|
+
return lines.join('\n');
|
|
196
|
+
}
|
|
197
|
+
function generateZshFunction(node, funcName, lines) {
|
|
198
|
+
lines.push(`${funcName}() {`);
|
|
199
|
+
lines.push(' local -a args');
|
|
200
|
+
lines.push('');
|
|
201
|
+
const visibleFlags = node.flags.filter((f) => !f.hidden);
|
|
202
|
+
const visibleSubs = node.subcommands.filter((s) => !s.hidden);
|
|
203
|
+
// Build _arguments specs for flags
|
|
204
|
+
const argSpecs = [];
|
|
205
|
+
for (const f of visibleFlags) {
|
|
206
|
+
const desc = escDq(f.description);
|
|
207
|
+
if (f.takesValue) {
|
|
208
|
+
const valueSpec = zshValueSpec(f);
|
|
209
|
+
if (f.short) {
|
|
210
|
+
argSpecs.push(`'(-${f.short} --${f.long})-${f.short}[${desc}]${valueSpec}'`);
|
|
211
|
+
argSpecs.push(`'(-${f.short} --${f.long})--${f.long}[${desc}]${valueSpec}'`);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
argSpecs.push(`'--${f.long}[${desc}]${valueSpec}'`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
if (f.short) {
|
|
219
|
+
argSpecs.push(`'(-${f.short} --${f.long})-${f.short}[${desc}]'`);
|
|
220
|
+
argSpecs.push(`'(-${f.short} --${f.long})--${f.long}[${desc}]'`);
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
argSpecs.push(`'--${f.long}[${desc}]'`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (visibleSubs.length > 0) {
|
|
228
|
+
argSpecs.push("'1: :->command'");
|
|
229
|
+
argSpecs.push("'*::arg:->args'");
|
|
230
|
+
}
|
|
231
|
+
if (argSpecs.length > 0) {
|
|
232
|
+
lines.push(' _arguments -C \\');
|
|
233
|
+
for (let i = 0; i < argSpecs.length; i++) {
|
|
234
|
+
const sep = i < argSpecs.length - 1 ? ' \\' : '';
|
|
235
|
+
lines.push(` ${argSpecs[i]}${sep}`);
|
|
236
|
+
}
|
|
237
|
+
lines.push('');
|
|
238
|
+
}
|
|
239
|
+
if (visibleSubs.length > 0) {
|
|
240
|
+
lines.push(' case "$state" in');
|
|
241
|
+
lines.push(' command)');
|
|
242
|
+
const subDescs = visibleSubs.map((s) => `'${s.name}:${escDq(s.description)}'`);
|
|
243
|
+
lines.push(` _values 'command' ${subDescs.join(' ')}`);
|
|
244
|
+
lines.push(' ;;');
|
|
245
|
+
lines.push(' args)');
|
|
246
|
+
lines.push(' case "$words[1]" in');
|
|
247
|
+
for (const sub of visibleSubs) {
|
|
248
|
+
const childFunc = `${funcName}_${sanitize(sub.name)}`;
|
|
249
|
+
const allNames = [sub.name, ...sub.aliases];
|
|
250
|
+
lines.push(` ${allNames.join('|')})`);
|
|
251
|
+
lines.push(` ${childFunc} ;;`);
|
|
252
|
+
}
|
|
253
|
+
lines.push(' esac');
|
|
254
|
+
lines.push(' ;;');
|
|
255
|
+
lines.push(' esac');
|
|
256
|
+
}
|
|
257
|
+
lines.push('}');
|
|
258
|
+
lines.push('');
|
|
259
|
+
// Recurse for child commands
|
|
260
|
+
for (const sub of visibleSubs) {
|
|
261
|
+
const childNode = node.childNodes.get(sub.name);
|
|
262
|
+
if (childNode) {
|
|
263
|
+
const childFunc = `${funcName}_${sanitize(sub.name)}`;
|
|
264
|
+
generateZshFunction(childNode, childFunc, lines);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function zshValueSpec(f) {
|
|
269
|
+
if (f.possibleValues.length > 0) {
|
|
270
|
+
const vals = f.possibleValues.join(' ');
|
|
271
|
+
return `:${f.key}:(${vals})`;
|
|
272
|
+
}
|
|
273
|
+
if (f.valueHint) {
|
|
274
|
+
return zshValueHintSpec(f.valueHint, f.key);
|
|
275
|
+
}
|
|
276
|
+
return `:${f.key}:`;
|
|
277
|
+
}
|
|
278
|
+
function zshValueHintSpec(hint, key) {
|
|
279
|
+
switch (hint) {
|
|
280
|
+
case 'filePath': return `:${key}:_files`;
|
|
281
|
+
case 'dirPath': return `:${key}:_directories`;
|
|
282
|
+
case 'anyPath': return `:${key}:_files`;
|
|
283
|
+
case 'executablePath': return `:${key}:_command_names`;
|
|
284
|
+
case 'commandName': return `:${key}:_command_names`;
|
|
285
|
+
case 'hostname': return `:${key}:_hosts`;
|
|
286
|
+
case 'username': return `:${key}:_users`;
|
|
287
|
+
case 'url': return `:${key}:_urls`;
|
|
288
|
+
case 'emailAddress': return `:${key}:`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// ---- Fish Generator ----
|
|
292
|
+
function generateFish(root, binaryName) {
|
|
293
|
+
const lines = [];
|
|
294
|
+
lines.push(`# fish completion for ${binaryName}`);
|
|
295
|
+
lines.push('# Generated by clap-ts');
|
|
296
|
+
lines.push('');
|
|
297
|
+
generateFishCommands(root, binaryName, [], lines);
|
|
298
|
+
return lines.join('\n');
|
|
299
|
+
}
|
|
300
|
+
function generateFishCommands(node, binaryName, parentSubcommands, lines) {
|
|
301
|
+
const visibleFlags = node.flags.filter((f) => !f.hidden);
|
|
302
|
+
const visibleSubs = node.subcommands.filter((s) => !s.hidden);
|
|
303
|
+
// Build condition: only show these completions when we're at this command level
|
|
304
|
+
let condition;
|
|
305
|
+
if (parentSubcommands.length === 0) {
|
|
306
|
+
if (visibleSubs.length > 0) {
|
|
307
|
+
// Root level with subcommands: show only when no subcommand has been seen
|
|
308
|
+
const allSubNames = collectAllSubcommandNames(node);
|
|
309
|
+
condition = `not __fish_seen_subcommand_from ${allSubNames.join(' ')}`;
|
|
310
|
+
}
|
|
311
|
+
else {
|
|
312
|
+
condition = '';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
const lastSub = parentSubcommands[parentSubcommands.length - 1];
|
|
317
|
+
condition = `__fish_seen_subcommand_from ${lastSub}`;
|
|
318
|
+
}
|
|
319
|
+
const condFlag = condition ? `-n '${condition}'` : '';
|
|
320
|
+
// Register flags
|
|
321
|
+
for (const f of visibleFlags) {
|
|
322
|
+
const parts = [`complete -c ${binaryName}`];
|
|
323
|
+
if (condFlag)
|
|
324
|
+
parts.push(condFlag);
|
|
325
|
+
if (f.short)
|
|
326
|
+
parts.push(`-s ${f.short}`);
|
|
327
|
+
parts.push(`-l ${f.long}`);
|
|
328
|
+
if (f.description)
|
|
329
|
+
parts.push(`-d '${esc(f.description)}'`);
|
|
330
|
+
if (f.takesValue) {
|
|
331
|
+
parts.push('-r'); // requires argument
|
|
332
|
+
if (f.possibleValues.length > 0) {
|
|
333
|
+
parts.push(`-a '${f.possibleValues.join(' ')}'`);
|
|
334
|
+
}
|
|
335
|
+
else if (f.valueHint) {
|
|
336
|
+
const fishHint = fishValueHint(f.valueHint);
|
|
337
|
+
if (fishHint)
|
|
338
|
+
parts.push(fishHint);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
lines.push(parts.join(' '));
|
|
342
|
+
}
|
|
343
|
+
// Register subcommands
|
|
344
|
+
for (const sub of visibleSubs) {
|
|
345
|
+
const parts = [`complete -c ${binaryName}`];
|
|
346
|
+
if (condFlag)
|
|
347
|
+
parts.push(condFlag);
|
|
348
|
+
parts.push(`-a '${sub.name}'`);
|
|
349
|
+
if (sub.description)
|
|
350
|
+
parts.push(`-d '${esc(sub.description)}'`);
|
|
351
|
+
lines.push(parts.join(' '));
|
|
352
|
+
// Also register aliases
|
|
353
|
+
for (const alias of sub.aliases) {
|
|
354
|
+
const aliasParts = [`complete -c ${binaryName}`];
|
|
355
|
+
if (condFlag)
|
|
356
|
+
aliasParts.push(condFlag);
|
|
357
|
+
aliasParts.push(`-a '${alias}'`);
|
|
358
|
+
if (sub.description)
|
|
359
|
+
aliasParts.push(`-d '${esc(sub.description)}'`);
|
|
360
|
+
lines.push(aliasParts.join(' '));
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
lines.push('');
|
|
364
|
+
// Recurse for child commands
|
|
365
|
+
for (const sub of visibleSubs) {
|
|
366
|
+
const childNode = node.childNodes.get(sub.name);
|
|
367
|
+
if (childNode) {
|
|
368
|
+
generateFishCommands(childNode, binaryName, [...parentSubcommands, sub.name], lines);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
function collectAllSubcommandNames(node) {
|
|
373
|
+
const names = [];
|
|
374
|
+
for (const sub of node.subcommands) {
|
|
375
|
+
if (!sub.hidden) {
|
|
376
|
+
names.push(sub.name);
|
|
377
|
+
names.push(...sub.aliases);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
return names;
|
|
381
|
+
}
|
|
382
|
+
function fishValueHint(hint) {
|
|
383
|
+
switch (hint) {
|
|
384
|
+
case 'filePath': return '-F';
|
|
385
|
+
case 'dirPath': return "-xa '(__fish_complete_directories)'";
|
|
386
|
+
case 'anyPath': return '-F';
|
|
387
|
+
case 'executablePath': return "-a '(__fish_complete_command)'";
|
|
388
|
+
case 'commandName': return "-a '(__fish_complete_command)'";
|
|
389
|
+
case 'hostname': return "-a '(__fish_print_hostnames)'";
|
|
390
|
+
case 'username': return "-a '(__fish_complete_users)'";
|
|
391
|
+
case 'url': return undefined;
|
|
392
|
+
case 'emailAddress': return undefined;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
// ---- PowerShell Generator ----
|
|
396
|
+
function generatePowerShell(root, binaryName) {
|
|
397
|
+
const lines = [];
|
|
398
|
+
lines.push(`# PowerShell completion for ${binaryName}`);
|
|
399
|
+
lines.push('# Generated by clap-ts');
|
|
400
|
+
lines.push('');
|
|
401
|
+
lines.push(`Register-ArgumentCompleter -CommandName '${binaryName}' -ScriptBlock {`);
|
|
402
|
+
lines.push(' param($wordToComplete, $commandAst, $cursorPosition)');
|
|
403
|
+
lines.push('');
|
|
404
|
+
lines.push(' $tokens = $commandAst.CommandElements | ForEach-Object { $_.ToString() }');
|
|
405
|
+
lines.push(' $tokens = $tokens[1..($tokens.Length - 1)] # Remove command name');
|
|
406
|
+
lines.push('');
|
|
407
|
+
generatePowerShellNode(root, ' ', '$tokens', 0, lines);
|
|
408
|
+
lines.push('}');
|
|
409
|
+
lines.push('');
|
|
410
|
+
return lines.join('\n');
|
|
411
|
+
}
|
|
412
|
+
function generatePowerShellNode(node, indent, tokensVar, depth, lines) {
|
|
413
|
+
const visibleFlags = node.flags.filter((f) => !f.hidden);
|
|
414
|
+
const visibleSubs = node.subcommands.filter((s) => !s.hidden);
|
|
415
|
+
// Check if we need to dispatch to a subcommand
|
|
416
|
+
if (visibleSubs.length > 0) {
|
|
417
|
+
lines.push(`${indent}# Check for subcommand at position ${depth}`);
|
|
418
|
+
lines.push(`${indent}if (${tokensVar}.Length -gt ${depth}) {`);
|
|
419
|
+
lines.push(`${indent} switch (${tokensVar}[${depth}]) {`);
|
|
420
|
+
for (const sub of visibleSubs) {
|
|
421
|
+
const allNames = [sub.name, ...sub.aliases].map((n) => `'${n}'`).join(', ');
|
|
422
|
+
lines.push(`${indent} {$_ -in ${allNames}} {`);
|
|
423
|
+
const childNode = node.childNodes.get(sub.name);
|
|
424
|
+
if (childNode) {
|
|
425
|
+
generatePowerShellNode(childNode, indent + ' ', tokensVar, depth + 1, lines);
|
|
426
|
+
}
|
|
427
|
+
lines.push(`${indent} return`);
|
|
428
|
+
lines.push(`${indent} }`);
|
|
429
|
+
}
|
|
430
|
+
lines.push(`${indent} }`);
|
|
431
|
+
lines.push(`${indent}}`);
|
|
432
|
+
lines.push('');
|
|
433
|
+
}
|
|
434
|
+
// Complete flags and subcommands at this level
|
|
435
|
+
const completions = [];
|
|
436
|
+
for (const f of visibleFlags) {
|
|
437
|
+
const desc = escDq(f.description);
|
|
438
|
+
completions.push(`${indent}[System.Management.Automation.CompletionResult]::new('--${f.long}', '--${f.long}', 'ParameterName', '${escDq(f.description)}')`);
|
|
439
|
+
if (f.short) {
|
|
440
|
+
completions.push(`${indent}[System.Management.Automation.CompletionResult]::new('-${f.short}', '-${f.short}', 'ParameterName', '${escDq(f.description)}')`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const sub of visibleSubs) {
|
|
444
|
+
completions.push(`${indent}[System.Management.Automation.CompletionResult]::new('${sub.name}', '${sub.name}', 'Command', '${escDq(sub.description)}')`);
|
|
445
|
+
}
|
|
446
|
+
for (const c of completions) {
|
|
447
|
+
lines.push(c);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
// ---- Public API: Static Generation ----
|
|
451
|
+
/**
|
|
452
|
+
* Generate a shell completion script for the given command and shell.
|
|
453
|
+
*
|
|
454
|
+
* Usage:
|
|
455
|
+
* ```ts
|
|
456
|
+
* const script = generateCompletions(rootCommand, 'bash', 'my-cli');
|
|
457
|
+
* fs.writeFileSync('completions.bash', script);
|
|
458
|
+
* ```
|
|
459
|
+
*
|
|
460
|
+
* Users source the generated script in their shell config:
|
|
461
|
+
* - bash: `source completions.bash` or copy to `~/.local/share/bash-completion/completions/`
|
|
462
|
+
* - zsh: copy to a directory in `$fpath` (e.g., `~/.zsh/completions/`)
|
|
463
|
+
* - fish: copy to `~/.config/fish/completions/`
|
|
464
|
+
* - powershell: add to `$PROFILE`
|
|
465
|
+
*/
|
|
466
|
+
export function generateCompletions(command, shell, binaryName) {
|
|
467
|
+
const name = binaryName ?? command.meta.name;
|
|
468
|
+
const root = buildCompletionTree(command, name);
|
|
469
|
+
switch (shell) {
|
|
470
|
+
case 'bash': return generateBash(root, name);
|
|
471
|
+
case 'zsh': return generateZsh(root, name);
|
|
472
|
+
case 'fish': return generateFish(root, name);
|
|
473
|
+
case 'powershell': return generatePowerShell(root, name);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// ---- Public API: Auto-inject completions subcommand ----
|
|
477
|
+
const VALID_SHELLS = ['bash', 'zsh', 'fish', 'powershell'];
|
|
478
|
+
/**
|
|
479
|
+
* Return a new command with a `completions` subcommand auto-injected.
|
|
480
|
+
* The subcommand generates shell completion scripts when invoked.
|
|
481
|
+
*
|
|
482
|
+
* ```ts
|
|
483
|
+
* const root = defineCommand({ ... });
|
|
484
|
+
* runMain(withCompletions(root));
|
|
485
|
+
* ```
|
|
486
|
+
*
|
|
487
|
+
* Then users run:
|
|
488
|
+
* ```bash
|
|
489
|
+
* eval "$(my-cli completions bash)"
|
|
490
|
+
* ```
|
|
491
|
+
*/
|
|
492
|
+
export function withCompletions(rootCommand) {
|
|
493
|
+
const completionsCmd = {
|
|
494
|
+
meta: {
|
|
495
|
+
name: 'completions',
|
|
496
|
+
description: 'Generate shell completion script',
|
|
497
|
+
aliases: ['completion'],
|
|
498
|
+
},
|
|
499
|
+
args: {
|
|
500
|
+
shell: {
|
|
501
|
+
type: 'positional',
|
|
502
|
+
valueName: 'SHELL',
|
|
503
|
+
required: true,
|
|
504
|
+
description: 'Target shell: bash, zsh, fish, or powershell',
|
|
505
|
+
},
|
|
506
|
+
},
|
|
507
|
+
run({ args }) {
|
|
508
|
+
const shell = String(args['shell']);
|
|
509
|
+
if (!VALID_SHELLS.includes(shell)) {
|
|
510
|
+
process.stderr.write(`error: invalid shell '${shell}'. Valid options: ${VALID_SHELLS.join(', ')}\n`);
|
|
511
|
+
process.exit(2);
|
|
512
|
+
}
|
|
513
|
+
process.stdout.write(generateCompletions(rootCommand, shell));
|
|
514
|
+
},
|
|
515
|
+
};
|
|
516
|
+
return {
|
|
517
|
+
...rootCommand,
|
|
518
|
+
subCommands: {
|
|
519
|
+
...rootCommand.subCommands,
|
|
520
|
+
completions: completionsCmd,
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Re-exports all public API.
|
|
5
5
|
*/
|
|
6
|
-
export type { ArgType, ArgAction, NumArgs, ValueParserFn, ArgDef, ArgsDef, StyleFn, StylesDef, CommandMeta, ArgGroup, ParsedArgs, CommandContext, CommandDef, RunOptions, ParseResult, InferArgValue, InferArgOptional, } from './types.js';
|
|
6
|
+
export type { ArgType, ArgAction, NumArgs, ValueParserFn, ValueHint, Shell, ArgDef, ArgsDef, StyleFn, StylesDef, CommandMeta, ArgGroup, ParsedArgs, CommandContext, CommandDef, RunOptions, ParseResult, InferArgValue, InferArgOptional, } from './types.js';
|
|
7
7
|
export { parseArgs, getRawArgs, collectGlobalArgs, mergeGlobalArgs, CliParseError, } from './parser.js';
|
|
8
8
|
export { validate } from './validation.js';
|
|
9
9
|
export { renderHelp, renderUsage, showHelp, showVersion, showError } from './help.js';
|
|
10
10
|
export { defineCommand, defineArgs, defineArg, runCommand, runMain } from './runner.js';
|
|
11
|
+
export { generateCompletions, withCompletions } from './completions.js';
|
package/dist/index.js
CHANGED
|
@@ -11,3 +11,5 @@ export { validate } from './validation.js';
|
|
|
11
11
|
export { renderHelp, renderUsage, showHelp, showVersion, showError } from './help.js';
|
|
12
12
|
// Runner (main API)
|
|
13
13
|
export { defineCommand, defineArgs, defineArg, runCommand, runMain } from './runner.js';
|
|
14
|
+
// Shell completions
|
|
15
|
+
export { generateCompletions, withCompletions } from './completions.js';
|
package/dist/types.d.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface NumArgs {
|
|
|
13
13
|
}
|
|
14
14
|
/** Custom value parser function. Receives raw string, returns parsed value or throws. */
|
|
15
15
|
export type ValueParserFn = (value: string) => unknown;
|
|
16
|
+
/** Hint for shell completion behavior -- guides what kind of values to complete. */
|
|
17
|
+
export type ValueHint = 'filePath' | 'dirPath' | 'anyPath' | 'executablePath' | 'commandName' | 'hostname' | 'username' | 'url' | 'emailAddress';
|
|
18
|
+
/** Supported shells for completion script generation. */
|
|
19
|
+
export type Shell = 'bash' | 'zsh' | 'fish' | 'powershell';
|
|
16
20
|
/** Full argument definition - matches clap::Arg. */
|
|
17
21
|
export interface ArgDef {
|
|
18
22
|
/** Value type for this argument. */
|
|
@@ -92,6 +96,8 @@ export interface ArgDef {
|
|
|
92
96
|
readonly last?: boolean;
|
|
93
97
|
/** Custom section heading in help output (groups args under this heading). */
|
|
94
98
|
readonly helpHeading?: string;
|
|
99
|
+
/** Hint for shell completion -- guides what kind of values to suggest (files, dirs, hosts, etc.). */
|
|
100
|
+
readonly valueHint?: ValueHint;
|
|
95
101
|
}
|
|
96
102
|
/** Record of argument name to definition. */
|
|
97
103
|
export type ArgsDef = Record<string, ArgDef>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clap-ts",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "A type-safe CLI argument parser for TypeScript, inspired by Rust's clap crate. Full clap-style parsing, validation, help generation, and subcommand support with zero dependencies.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|