@boneskull/bargs 4.0.1 → 4.1.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 @@
1
+ {"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAa,aAAa,EAAE,iBAAiB,EAAE,oBAAmB;AAE9E;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAoB5C;;GAEG;AACH,KAAK,YAAY,GACb;IACE,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,sCAAsC;IACtC,GAAG,EAAE;QACH,sCAAsC;QACtC,eAAe,EAAE,aAAa,CAAC;QAC/B,0CAA0C;QAC1C,mBAAmB,EAAE,iBAAiB,CAAC;KACxC,CAAC;IACF,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yCAAyC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAuBN;;GAEG;AACH,UAAU,gBAAgB;IACxB,wDAAwD;IACxD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,4CAA4C;IAC5C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,yDAAyD;IACzD,YAAY,CAAC,EAAE;QACb,4BAA4B;QAC5B,eAAe,EAAE,aAAa,CAAC;QAC/B,gCAAgC;QAChC,mBAAmB,EAAE,iBAAiB,CAAC;KACxC,CAAC;IACF,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;CACd;AAyKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,wBAAwB,GACnC,SAAS,MAAM,EACf,OAAO,KAAK,KACX,MAWF,CAAC;AAscF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,GAClC,OAAO,gBAAgB,EACvB,OAAO,KAAK,EACZ,OAAO,MAAM,EAAE,KACd,MAAM,EAuDR,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,KAO7C,CAAC"}
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Shell completion script generation for bargs CLIs.
3
+ *
4
+ * Provides dynamic shell completion support for bash, zsh, and fish shells. The
5
+ * generated scripts call back to the CLI to get completion candidates.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ import type { OptionsSchema, PositionalsSchema } from "./types.js";
10
+ /**
11
+ * Supported shell types for completion script generation.
12
+ *
13
+ * @group Completion
14
+ */
15
+ export type Shell = 'bash' | 'fish' | 'zsh';
16
+ /**
17
+ * Command entry in internal state.
18
+ */
19
+ type CommandEntry = {
20
+ /** Alternative names for this command */
21
+ aliases?: string[];
22
+ /** Command definition with schemas */
23
+ cmd: {
24
+ /** Options schema for this command */
25
+ __optionsSchema: OptionsSchema;
26
+ /** Positionals schema for this command */
27
+ __positionalsSchema: PositionalsSchema;
28
+ };
29
+ /** Command description for help text */
30
+ description?: string;
31
+ /** Discriminator for leaf commands */
32
+ type: 'command';
33
+ } | {
34
+ /** Alternative names for this command */
35
+ aliases?: string[];
36
+ /** Nested CLI builder for subcommands */
37
+ builder: unknown;
38
+ /** Command description for help text */
39
+ description?: string;
40
+ /** Discriminator for nested command groups */
41
+ type: 'nested';
42
+ };
43
+ /**
44
+ * Internal CLI state structure (matches bargs.ts InternalCliState).
45
+ */
46
+ interface InternalCliState {
47
+ /** Map of command aliases to canonical command names */
48
+ aliasMap: Map<string, string>;
49
+ /** Map of command names to their entries */
50
+ commands: Map<string, CommandEntry>;
51
+ /** Global parser with options and positionals schemas */
52
+ globalParser?: {
53
+ /** Global options schema */
54
+ __optionsSchema: OptionsSchema;
55
+ /** Global positionals schema */
56
+ __positionalsSchema: PositionalsSchema;
57
+ };
58
+ /** CLI executable name */
59
+ name: string;
60
+ }
61
+ /**
62
+ * Generate a shell completion script for the given CLI.
63
+ *
64
+ * The generated script calls back to the CLI with `--get-bargs-completions` to
65
+ * get completion candidates dynamically.
66
+ *
67
+ * @example
68
+ *
69
+ * ```typescript
70
+ * // Output script for bash
71
+ * console.log(generateCompletionScript('mytool', 'bash'));
72
+ * // Redirect to shell config: mytool --completion-script bash >> ~/.bashrc
73
+ * ```
74
+ *
75
+ * @function
76
+ * @param cliName - The name of the CLI executable
77
+ * @param shell - The target shell ('bash', 'zsh', or 'fish')
78
+ * @returns The completion script as a string
79
+ * @group Completion
80
+ */
81
+ export declare const generateCompletionScript: (cliName: string, shell: Shell) => string;
82
+ /**
83
+ * Get completion candidates for the current command line state.
84
+ *
85
+ * Analyzes the provided words to determine context and returns appropriate
86
+ * completion suggestions.
87
+ *
88
+ * @function
89
+ * @param state - Internal CLI state containing commands and options
90
+ * @param shell - The shell requesting completions (affects output format)
91
+ * @param words - The command line words (COMP_WORDS in bash)
92
+ * @returns Array of completion candidates (one per line when output)
93
+ * @group Completion
94
+ */
95
+ export declare const getCompletionCandidates: (state: InternalCliState, shell: Shell, words: string[]) => string[];
96
+ /**
97
+ * Validate that a shell name is supported.
98
+ *
99
+ * @function
100
+ * @param shell - The shell name to validate
101
+ * @returns The validated shell type
102
+ * @throws Error if the shell is not supported
103
+ * @group Completion
104
+ */
105
+ export declare const validateShell: (shell: string) => Shell;
106
+ export {};
107
+ //# sourceMappingURL=completion.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion.d.ts","sourceRoot":"","sources":["../src/completion.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAa,aAAa,EAAE,iBAAiB,EAAE,mBAAmB;AAE9E;;;;GAIG;AACH,MAAM,MAAM,KAAK,GAAG,MAAM,GAAG,MAAM,GAAG,KAAK,CAAC;AAoB5C;;GAEG;AACH,KAAK,YAAY,GACb;IACE,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,sCAAsC;IACtC,GAAG,EAAE;QACH,sCAAsC;QACtC,eAAe,EAAE,aAAa,CAAC;QAC/B,0CAA0C;QAC1C,mBAAmB,EAAE,iBAAiB,CAAC;KACxC,CAAC;IACF,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,IAAI,EAAE,SAAS,CAAC;CACjB,GACD;IACE,yCAAyC;IACzC,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,yCAAyC;IACzC,OAAO,EAAE,OAAO,CAAC;IACjB,wCAAwC;IACxC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,8CAA8C;IAC9C,IAAI,EAAE,QAAQ,CAAC;CAChB,CAAC;AAuBN;;GAEG;AACH,UAAU,gBAAgB;IACxB,wDAAwD;IACxD,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,4CAA4C;IAC5C,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IACpC,yDAAyD;IACzD,YAAY,CAAC,EAAE;QACb,4BAA4B;QAC5B,eAAe,EAAE,aAAa,CAAC;QAC/B,gCAAgC;QAChC,mBAAmB,EAAE,iBAAiB,CAAC;KACxC,CAAC;IACF,0BAA0B;IAC1B,IAAI,EAAE,MAAM,CAAC;CACd;AAyKD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,eAAO,MAAM,wBAAwB,GACnC,SAAS,MAAM,EACf,OAAO,KAAK,KACX,MAWF,CAAC;AAscF;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,uBAAuB,GAClC,OAAO,gBAAgB,EACvB,OAAO,KAAK,EACZ,OAAO,MAAM,EAAE,KACd,MAAM,EAuDR,CAAC;AAEF;;;;;;;;GAQG;AACH,eAAO,MAAM,aAAa,GAAI,OAAO,MAAM,KAAG,KAO7C,CAAC"}
@@ -0,0 +1,559 @@
1
+ /**
2
+ * Shell completion script generation for bargs CLIs.
3
+ *
4
+ * Provides dynamic shell completion support for bash, zsh, and fish shells. The
5
+ * generated scripts call back to the CLI to get completion candidates.
6
+ *
7
+ * @packageDocumentation
8
+ */
9
+ /**
10
+ * Sanitize a CLI name for use as a shell function name.
11
+ *
12
+ * Ensures the result is a valid POSIX identifier: starts with a letter or
13
+ * underscore, contains only alphanumeric characters and underscores.
14
+ *
15
+ * @function
16
+ * @param name - The CLI name to sanitize
17
+ * @returns A valid shell function name
18
+ */
19
+ const sanitizeFunctionName = (name) => {
20
+ // Replace any non-alphanumeric character with underscore
21
+ let sanitized = name.replace(/[^a-zA-Z0-9]/g, '_');
22
+ // Collapse multiple consecutive underscores
23
+ sanitized = sanitized.replace(/_+/g, '_');
24
+ // Remove leading/trailing underscores
25
+ sanitized = sanitized.replace(/^_+|_+$/g, '');
26
+ // Ensure it starts with a letter or underscore (not a digit)
27
+ if (/^[0-9]/.test(sanitized)) {
28
+ sanitized = `_${sanitized}`;
29
+ }
30
+ // Fallback for empty result
31
+ if (!sanitized) {
32
+ sanitized = 'cli';
33
+ }
34
+ return sanitized;
35
+ };
36
+ /**
37
+ * Generate bash completion script.
38
+ *
39
+ * @function
40
+ */
41
+ const generateBashScript = (cliName) => {
42
+ const funcName = `_${sanitizeFunctionName(cliName)}_completions`;
43
+ return `# bash completion for ${cliName}
44
+ # Add to ~/.bashrc or ~/.bash_profile:
45
+ # source <(${cliName} --completion-script bash)
46
+ # Or:
47
+ # ${cliName} --completion-script bash >> ~/.bashrc
48
+
49
+ ${funcName}() {
50
+ local IFS=$'\\n'
51
+ local cur="\${COMP_WORDS[COMP_CWORD]}"
52
+
53
+ # Call CLI to get completions
54
+ local completions
55
+ completions=($("${cliName}" --get-bargs-completions bash "\${COMP_WORDS[@]}"))
56
+
57
+ # Filter by current word prefix
58
+ COMPREPLY=($(compgen -W "\${completions[*]}" -- "\${cur}"))
59
+
60
+ # Fall back to file completion if no matches and not completing an option
61
+ if [[ \${#COMPREPLY[@]} -eq 0 && "\${cur}" != -* ]]; then
62
+ compopt -o default
63
+ fi
64
+ }
65
+
66
+ complete -o default -F ${funcName} ${cliName}
67
+ `;
68
+ };
69
+ /**
70
+ * Generate zsh completion script.
71
+ *
72
+ * @function
73
+ */
74
+ const generateZshScript = (cliName) => {
75
+ const funcName = `_${sanitizeFunctionName(cliName)}`;
76
+ return `#compdef ${cliName}
77
+ # zsh completion for ${cliName}
78
+ # Add to ~/.zshrc:
79
+ # source <(${cliName} --completion-script zsh)
80
+ # Or save to a file in your $fpath:
81
+ # ${cliName} --completion-script zsh > ~/.zsh/completions/_${cliName}
82
+
83
+ ${funcName}() {
84
+ local completions
85
+
86
+ # Call CLI to get completions with descriptions
87
+ completions=("\${(@f)$("${cliName}" --get-bargs-completions zsh "\${words[@]}")}")
88
+
89
+ if [[ \${#completions[@]} -gt 0 && -n "\${completions[1]}" ]]; then
90
+ # Check if completions have descriptions (format: "value:description")
91
+ if [[ "\${completions[1]}" == *":"* ]]; then
92
+ _describe 'completions' completions
93
+ else
94
+ compadd -a completions
95
+ fi
96
+ fi
97
+ }
98
+
99
+ compdef ${funcName} ${cliName}
100
+ `;
101
+ };
102
+ /**
103
+ * Generate fish completion script.
104
+ *
105
+ * @function
106
+ */
107
+ const generateFishScript = (cliName) => {
108
+ const funcName = `__fish_${sanitizeFunctionName(cliName)}_complete`;
109
+ return `# fish completion for ${cliName}
110
+ # Save to ~/.config/fish/completions/${cliName}.fish:
111
+ # ${cliName} --completion-script fish > ~/.config/fish/completions/${cliName}.fish
112
+
113
+ function ${funcName}
114
+ set -l tokens (commandline -opc)
115
+ ${cliName} --get-bargs-completions fish $tokens
116
+ end
117
+
118
+ # Disable file completions by default, let the CLI decide
119
+ complete -c ${cliName} -f -a '(${funcName})'
120
+ `;
121
+ };
122
+ /**
123
+ * Generate a shell completion script for the given CLI.
124
+ *
125
+ * The generated script calls back to the CLI with `--get-bargs-completions` to
126
+ * get completion candidates dynamically.
127
+ *
128
+ * @example
129
+ *
130
+ * ```typescript
131
+ * // Output script for bash
132
+ * console.log(generateCompletionScript('mytool', 'bash'));
133
+ * // Redirect to shell config: mytool --completion-script bash >> ~/.bashrc
134
+ * ```
135
+ *
136
+ * @function
137
+ * @param cliName - The name of the CLI executable
138
+ * @param shell - The target shell ('bash', 'zsh', or 'fish')
139
+ * @returns The completion script as a string
140
+ * @group Completion
141
+ */
142
+ export const generateCompletionScript = (cliName, shell) => {
143
+ switch (shell) {
144
+ case 'bash':
145
+ return generateBashScript(cliName);
146
+ case 'fish':
147
+ return generateFishScript(cliName);
148
+ case 'zsh':
149
+ return generateZshScript(cliName);
150
+ default:
151
+ throw new Error(`Unsupported shell: ${shell}`);
152
+ }
153
+ };
154
+ /**
155
+ * Extract completion metadata from internal CLI state.
156
+ *
157
+ * @function
158
+ */
159
+ const extractCompletionMetadata = (state) => {
160
+ const globalOptions = extractOptionsInfo(state.globalParser?.__optionsSchema ?? {});
161
+ const commands = new Map();
162
+ for (const [name, entry] of state.commands) {
163
+ // Skip the internal default command marker
164
+ if (name === '__default__') {
165
+ continue;
166
+ }
167
+ if (entry.type === 'command') {
168
+ commands.set(name, {
169
+ aliases: entry.aliases ?? [],
170
+ description: entry.description,
171
+ name,
172
+ options: extractOptionsInfo(entry.cmd.__optionsSchema),
173
+ positionals: extractPositionalsInfo(entry.cmd.__positionalsSchema),
174
+ });
175
+ }
176
+ else if (entry.type === 'nested') {
177
+ commands.set(name, {
178
+ aliases: entry.aliases ?? [],
179
+ description: entry.description,
180
+ name,
181
+ nestedBuilder: entry.builder,
182
+ options: [],
183
+ positionals: [],
184
+ });
185
+ }
186
+ }
187
+ return {
188
+ commands,
189
+ globalOptions,
190
+ name: state.name,
191
+ };
192
+ };
193
+ /**
194
+ * Check if a value is an internal builder with __getState method.
195
+ *
196
+ * @function
197
+ */
198
+ const isInternalBuilder = (value) => {
199
+ return (typeof value === 'object' &&
200
+ value !== null &&
201
+ '__getState' in value &&
202
+ typeof value.__getState === 'function');
203
+ };
204
+ /**
205
+ * Extract metadata from a nested builder.
206
+ *
207
+ * @function
208
+ */
209
+ const extractNestedMetadata = (nestedBuilder) => {
210
+ if (!isInternalBuilder(nestedBuilder)) {
211
+ return undefined;
212
+ }
213
+ return extractCompletionMetadata(nestedBuilder.__getState());
214
+ };
215
+ /**
216
+ * Extract option info from options schema.
217
+ *
218
+ * @function
219
+ */
220
+ const extractOptionsInfo = (schema) => {
221
+ const options = [];
222
+ for (const [name, def] of Object.entries(schema)) {
223
+ // Skip hidden options
224
+ if (def.hidden) {
225
+ continue;
226
+ }
227
+ const aliases = [];
228
+ if ('aliases' in def && Array.isArray(def.aliases)) {
229
+ for (const alias of def.aliases) {
230
+ if (alias.length === 1) {
231
+ aliases.push(`-${alias}`);
232
+ }
233
+ else {
234
+ aliases.push(`--${alias}`);
235
+ }
236
+ }
237
+ }
238
+ options.push({
239
+ aliases,
240
+ choices: getChoices(def),
241
+ description: def.description,
242
+ name: `--${name}`,
243
+ takesValue: def.type !== 'boolean' && def.type !== 'count',
244
+ type: def.type,
245
+ });
246
+ // Add --no-<name> for boolean options
247
+ if (def.type === 'boolean') {
248
+ options.push({
249
+ aliases: [],
250
+ description: def.description ? `Disable ${def.description}` : undefined,
251
+ name: `--no-${name}`,
252
+ takesValue: false,
253
+ type: 'boolean',
254
+ });
255
+ }
256
+ }
257
+ return options;
258
+ };
259
+ /**
260
+ * Extract positional info from positionals schema.
261
+ *
262
+ * @function
263
+ */
264
+ const extractPositionalsInfo = (schema) => schema.map((pos) => ({
265
+ choices: getChoices(pos),
266
+ description: pos.description,
267
+ name: pos.name ?? 'arg',
268
+ type: pos.type,
269
+ }));
270
+ /**
271
+ * Get choices from an option or positional definition.
272
+ *
273
+ * @function
274
+ */
275
+ const getChoices = (def) => {
276
+ if ('choices' in def && Array.isArray(def.choices)) {
277
+ return def.choices;
278
+ }
279
+ return undefined;
280
+ };
281
+ /**
282
+ * Find a command by name or alias in the metadata.
283
+ *
284
+ * @function
285
+ */
286
+ const findCommand = (metadata, name) => {
287
+ // Try direct match
288
+ const direct = metadata.commands.get(name);
289
+ if (direct) {
290
+ return direct;
291
+ }
292
+ // Try alias match
293
+ for (const [, cmd] of metadata.commands) {
294
+ if (cmd.aliases.includes(name)) {
295
+ return cmd;
296
+ }
297
+ }
298
+ return undefined;
299
+ };
300
+ /**
301
+ * Analyze command context from args, recursively handling nested commands.
302
+ *
303
+ * @function
304
+ */
305
+ const getCommandContext = (metadata, args, accumulatedOptions = []) => {
306
+ // Accumulate global options from this level
307
+ const options = [...accumulatedOptions, ...metadata.globalOptions];
308
+ if (metadata.commands.size === 0) {
309
+ return {
310
+ accumulatedOptions: options,
311
+ availableCommands: [],
312
+ needsCommand: false,
313
+ positionalIndex: 0,
314
+ };
315
+ }
316
+ // Find the first non-option argument (potential command)
317
+ // Note: The last arg is the current word being completed, so we don't count it
318
+ // as a completed positional
319
+ let commandName;
320
+ let commandArgIndex = -1;
321
+ // Process all args except the last one (which is being completed)
322
+ const completedArgs = args.slice(0, -1);
323
+ for (let i = 0; i < completedArgs.length; i++) {
324
+ const arg = completedArgs[i];
325
+ if (!arg.startsWith('-')) {
326
+ // First non-option is the command at this level
327
+ commandName = arg;
328
+ commandArgIndex = i;
329
+ break;
330
+ }
331
+ }
332
+ // Check if the command name matches a known command or alias
333
+ if (commandName) {
334
+ const cmd = findCommand(metadata, commandName);
335
+ if (cmd) {
336
+ // Check if this is a nested command - if so, recurse
337
+ if (cmd.nestedBuilder) {
338
+ const nestedMetadata = extractNestedMetadata(cmd.nestedBuilder);
339
+ if (nestedMetadata) {
340
+ // Get remaining args after this command
341
+ const remainingArgs = completedArgs.slice(commandArgIndex + 1);
342
+ // Add the current word being completed
343
+ if (args.length > 0) {
344
+ remainingArgs.push(args[args.length - 1]);
345
+ }
346
+ return getCommandContext(nestedMetadata, remainingArgs, options);
347
+ }
348
+ }
349
+ // It's a leaf command - calculate positional index
350
+ let positionalIndex = 0;
351
+ for (let i = commandArgIndex + 1; i < completedArgs.length; i++) {
352
+ const arg = completedArgs[i];
353
+ if (!arg.startsWith('-')) {
354
+ positionalIndex++;
355
+ }
356
+ }
357
+ return {
358
+ accumulatedOptions: options,
359
+ availableCommands: [],
360
+ currentCommand: cmd,
361
+ needsCommand: false,
362
+ positionalIndex,
363
+ };
364
+ }
365
+ }
366
+ // No valid command yet at this level - need to show available commands
367
+ return {
368
+ accumulatedOptions: options,
369
+ availableCommands: Array.from(metadata.commands.values()),
370
+ needsCommand: true,
371
+ positionalIndex: 0,
372
+ };
373
+ };
374
+ /**
375
+ * Get all options available in the current context.
376
+ *
377
+ * @function
378
+ */
379
+ const getAllOptionsForContext = (metadata, args) => {
380
+ // getCommandContext now accumulates global options from all parent levels
381
+ const commandContext = getCommandContext(metadata, args);
382
+ // Start with accumulated options (includes all global options from parent levels)
383
+ const options = [...commandContext.accumulatedOptions];
384
+ // Add command-specific options if we're in a leaf command
385
+ if (commandContext.currentCommand) {
386
+ options.push(...commandContext.currentCommand.options);
387
+ }
388
+ return options;
389
+ };
390
+ /**
391
+ * Format candidates for shell output.
392
+ *
393
+ * @function
394
+ */
395
+ const formatCandidates = (candidates, shell) => {
396
+ switch (shell) {
397
+ case 'fish':
398
+ // fish supports descriptions with tab separator
399
+ return candidates.map((c) => c.description ? `${c.value}\t${c.description}` : c.value);
400
+ case 'zsh':
401
+ // zsh supports descriptions in format "value:description"
402
+ return candidates.map((c) => c.description ? `${c.value}:${c.description}` : c.value);
403
+ case 'bash':
404
+ default:
405
+ // bash doesn't support descriptions in basic completion
406
+ return candidates.map((c) => c.value);
407
+ }
408
+ };
409
+ /**
410
+ * Get command candidates.
411
+ *
412
+ * @function
413
+ */
414
+ const getCommandCandidates = (commands, _currentWord, shell) => {
415
+ const candidates = [];
416
+ for (const cmd of commands) {
417
+ candidates.push({ description: cmd.description, value: cmd.name });
418
+ for (const alias of cmd.aliases) {
419
+ candidates.push({
420
+ description: cmd.description ? `(alias) ${cmd.description}` : '(alias)',
421
+ value: alias,
422
+ });
423
+ }
424
+ }
425
+ return formatCandidates(candidates, shell);
426
+ };
427
+ /**
428
+ * Get option candidates.
429
+ *
430
+ * @function
431
+ */
432
+ const getOptionCandidates = (metadata, args, shell) => {
433
+ const allOptions = getAllOptionsForContext(metadata, args);
434
+ const candidates = [];
435
+ for (const opt of allOptions) {
436
+ candidates.push({ description: opt.description, value: opt.name });
437
+ for (const alias of opt.aliases) {
438
+ candidates.push({ description: opt.description, value: alias });
439
+ }
440
+ }
441
+ return formatCandidates(candidates, shell);
442
+ };
443
+ /**
444
+ * Get candidates for option values (enum choices).
445
+ *
446
+ * @function
447
+ */
448
+ const getOptionValueCandidates = (metadata, prevWord, args, shell) => {
449
+ // Find the option definition
450
+ const allOptions = getAllOptionsForContext(metadata, args);
451
+ for (const opt of allOptions) {
452
+ if (opt.name === prevWord || opt.aliases.includes(prevWord)) {
453
+ if (opt.choices && opt.choices.length > 0) {
454
+ return {
455
+ candidates: formatCandidates(opt.choices.map((c) => ({ description: undefined, value: c })), shell),
456
+ found: true,
457
+ };
458
+ }
459
+ // Option takes a value but no specific choices - let shell do file completion
460
+ if (opt.takesValue) {
461
+ return { candidates: [], found: true };
462
+ }
463
+ // Boolean/count option - doesn't take a value, so prev word isn't an option expecting a value
464
+ return { candidates: [], found: false };
465
+ }
466
+ }
467
+ // Option not found
468
+ return { candidates: [], found: false };
469
+ };
470
+ /**
471
+ * Get positional candidates (for enum positionals).
472
+ *
473
+ * @function
474
+ */
475
+ const getPositionalCandidates = (command, positionalIndex, shell) => {
476
+ if (positionalIndex >= command.positionals.length) {
477
+ // Check for variadic last positional
478
+ const lastPos = command.positionals[command.positionals.length - 1];
479
+ if (lastPos?.type !== 'variadic') {
480
+ return [];
481
+ }
482
+ // Use the variadic positional's choices if any
483
+ if (lastPos.choices && lastPos.choices.length > 0) {
484
+ return formatCandidates(lastPos.choices.map((c) => ({ description: undefined, value: c })), shell);
485
+ }
486
+ return [];
487
+ }
488
+ const pos = command.positionals[positionalIndex];
489
+ if (!pos || !pos.choices || pos.choices.length === 0) {
490
+ return [];
491
+ }
492
+ return formatCandidates(pos.choices.map((c) => ({ description: undefined, value: c })), shell);
493
+ };
494
+ /**
495
+ * Get completion candidates for the current command line state.
496
+ *
497
+ * Analyzes the provided words to determine context and returns appropriate
498
+ * completion suggestions.
499
+ *
500
+ * @function
501
+ * @param state - Internal CLI state containing commands and options
502
+ * @param shell - The shell requesting completions (affects output format)
503
+ * @param words - The command line words (COMP_WORDS in bash)
504
+ * @returns Array of completion candidates (one per line when output)
505
+ * @group Completion
506
+ */
507
+ export const getCompletionCandidates = (state, shell, words) => {
508
+ const metadata = extractCompletionMetadata(state);
509
+ // Remove the CLI name from words if present
510
+ const args = words.length > 1 ? words.slice(1) : [];
511
+ const currentWord = args.length > 0 ? (args[args.length - 1] ?? '') : '';
512
+ const prevWord = args.length > 1 ? args[args.length - 2] : undefined;
513
+ // Check if we're completing an option value
514
+ if (prevWord?.startsWith('-')) {
515
+ const result = getOptionValueCandidates(metadata, prevWord, args, shell);
516
+ // If we found the option and it takes a value, return the result
517
+ // (which may be empty to allow file completion)
518
+ if (result.found) {
519
+ return result.candidates;
520
+ }
521
+ }
522
+ // Check if current word is an option
523
+ if (currentWord.startsWith('-')) {
524
+ return getOptionCandidates(metadata, args, shell);
525
+ }
526
+ // Check if we need to complete a command
527
+ const commandContext = getCommandContext(metadata, args);
528
+ if (commandContext.needsCommand) {
529
+ return getCommandCandidates(commandContext.availableCommands, currentWord, shell);
530
+ }
531
+ // Check if we're in a command and need positional completion
532
+ if (commandContext.currentCommand) {
533
+ const positionalCandidates = getPositionalCandidates(commandContext.currentCommand, commandContext.positionalIndex, shell);
534
+ if (positionalCandidates.length > 0) {
535
+ return positionalCandidates;
536
+ }
537
+ }
538
+ // Default: offer commands if we have them, or options
539
+ if (metadata.commands.size > 0) {
540
+ return getCommandCandidates(Array.from(metadata.commands.values()), currentWord, shell);
541
+ }
542
+ return getOptionCandidates(metadata, args, shell);
543
+ };
544
+ /**
545
+ * Validate that a shell name is supported.
546
+ *
547
+ * @function
548
+ * @param shell - The shell name to validate
549
+ * @returns The validated shell type
550
+ * @throws Error if the shell is not supported
551
+ * @group Completion
552
+ */
553
+ export const validateShell = (shell) => {
554
+ if (shell === 'bash' || shell === 'zsh' || shell === 'fish') {
555
+ return shell;
556
+ }
557
+ throw new Error(`Unsupported shell: "${shell}". Supported shells: bash, zsh, fish`);
558
+ };
559
+ //# sourceMappingURL=completion.js.map