clap-ts 0.1.0 → 0.2.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.
@@ -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<T extends CommandDef>(rootCommand: T): T;
@@ -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.0",
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",