@boneskull/bargs 4.0.0 → 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.
- package/README.md +66 -5
- package/dist/bargs.cjs +79 -2
- package/dist/bargs.cjs.map +1 -1
- package/dist/bargs.d.cts.map +1 -1
- package/dist/bargs.d.ts.map +1 -1
- package/dist/bargs.js +79 -2
- package/dist/bargs.js.map +1 -1
- package/dist/completion.cjs +565 -0
- package/dist/completion.cjs.map +1 -0
- package/dist/completion.d.cts +107 -0
- package/dist/completion.d.cts.map +1 -0
- package/dist/completion.d.ts +107 -0
- package/dist/completion.d.ts.map +1 -0
- package/dist/completion.js +559 -0
- package/dist/completion.js.map +1 -0
- package/dist/help.cjs +57 -18
- package/dist/help.cjs.map +1 -1
- package/dist/help.d.cts.map +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +57 -18
- package/dist/help.js.map +1 -1
- package/dist/theme.cjs +10 -10
- package/dist/theme.cjs.map +1 -1
- package/dist/theme.js +10 -10
- package/dist/theme.js.map +1 -1
- package/dist/types.d.cts +21 -0
- package/dist/types.d.cts.map +1 -1
- package/dist/types.d.ts +21 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +17 -2
|
@@ -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
|