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 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 | Not yet |
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.0",
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",