···2233# tab
4455-- [x] zsh test in git
55+Shell autocompletions are largely missing in the javascript cli ecosystem. This tool is an attempt to make autocompletions come out of the box for any cli tool.
6677-```zsh
88-source <(pnpm tsx demo.ts complete zsh)
77+Tools like git and their autocompletion experience inspired us to build this tool and make the same ability available for any javascript cli project. Developers love hitting the tab key, hence why they prefer tabs over spaces.
981010-vite # rest of the completions
99+```ts
1010+import { Completion, script } from '@bombsh/tab';
11111212-pnpm tsx demo.ts complete -- --po
1212+const name = 'my-cli';
1313+const completion = new Completion();
1414+1515+completion.addCommand(
1616+ 'start',
1717+ 'Start the application',
1818+ async (previousArgs, toComplete, endsWithSpace) => {
1919+ // suggestions
2020+ return [
2121+ { value: 'dev', description: 'Start in development mode' },
2222+ { value: 'prod', description: 'Start in production mode' },
2323+ ];
2424+ }
2525+);
2626+2727+completion.addOption(
2828+ 'start',
2929+ '--port',
3030+ 'Specify the port number',
3131+ async (previousArgs, toComplete, endsWithSpace) => {
3232+ return [
3333+ { value: '3000', description: 'Development port' },
3434+ { value: '8080', description: 'Production port' },
3535+ ];
3636+ }
3737+);
3838+3939+// a way of getting the executable path to pass to the shell autocompletion script
4040+function quoteIfNeeded(path: string) {
4141+ return path.includes(' ') ? `'${path}'` : path;
4242+}
4343+const execPath = process.execPath;
4444+const processArgs = process.argv.slice(1);
4545+const quotedExecPath = quoteIfNeeded(execPath);
4646+const quotedProcessArgs = processArgs.map(quoteIfNeeded);
4747+const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded);
4848+const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`;
4949+5050+if (process.argv[2] === '--') {
5151+ // autocompletion logic
5252+ await completion.parse(process.argv.slice(2), 'start'); // TODO: remove "start"
5353+} else {
5454+ // process.argv[2] can be "zsh", "bash", "fish", "powershell"
5555+ script(process.argv[2], name, x);
5656+}
1357```
14581515-- [x] tests vitest (this should mostly test the completions array, e.g. logs)
1616-- [x] powershell completions generation
1717-- [x] citty support `@bomsh/tab/citty`
1818-- [] `@bombsh/tab`
5959+Now your user can run `source <(my-cli complete zsh)` and they will get completions for the `my-cli` command using the [autocompletion server](#autocompletion-server).
19602020-- [] fish
2121-- [] bash
6161+## Adapters
6262+6363+Since we are heavy users of tools like `cac` and `citty`, we have created adapters for both of them. Ideally, tab would be integrated internally into these tools, but for now, this is a good compromise.
6464+6565+### `@bombsh/tab/cac`
22662367```ts
2424-const completion = new Completion()
2525-completion.addCommand()
2626-completion.addOption()
6868+import cac from 'cac';
6969+import tab from '@bombsh/tab/cac';
7070+7171+const cli = cac('my-cli');
7272+7373+cli.command('dev', 'Start dev server').option('--port <port>', 'Specify port');
7474+7575+const completion = tab(cli);
7676+7777+// Get the dev command completion handler
7878+const devCommandCompletion = completion.commands.get('dev');
7979+8080+// Get and configure the port option completion handler
8181+const portOptionCompletion = devCommandCompletion.options.get('--port');
8282+portOptionCompletion.handler = async (
8383+ previousArgs,
8484+ toComplete,
8585+ endsWithSpace
8686+) => {
8787+ return [
8888+ { value: '3000', description: 'Development port' },
8989+ { value: '8080', description: 'Production port' },
9090+ ];
9191+};
27922828-// better name
2929-completion.parse()
9393+cli.parse();
3094```
9595+9696+Now autocompletion will be available for any specified command and option in your cac instance. If your user writes `my-cli dev --po`, they will get suggestions for the `--port` option. Or if they write `my-cli d` they will get suggestions for the `dev` command.
9797+9898+Suggestions are missing in the adapters since yet cac or citty do not have a way to provide suggestions (tab just came out!), we'd have to provide them manually. Mutations do not hurt in this situation.
9999+100100+### `@bombsh/tab/citty`
101101+102102+```ts
103103+import citty, { defineCommand, createMain } from 'citty';
104104+import tab from '@bombsh/tab/citty';
105105+106106+const main = defineCommand({
107107+ meta: {
108108+ name: 'my-cli',
109109+ description: 'My CLI tool',
110110+ },
111111+});
112112+113113+const devCommand = defineCommand({
114114+ meta: {
115115+ name: 'dev',
116116+ description: 'Start dev server',
117117+ },
118118+ args: {
119119+ port: { type: 'string', description: 'Specify port' },
120120+ },
121121+});
122122+123123+main.subCommands = {
124124+ dev: devCommand,
125125+};
126126+127127+const completion = await tab(main);
128128+129129+// TODO: addHandler function to export
130130+const devCommandCompletion = completion.commands.get('dev');
131131+132132+const portOptionCompletion = devCommandCompletion.options.get('--port');
133133+134134+portOptionCompletion.handler = async (
135135+ previousArgs,
136136+ toComplete,
137137+ endsWithSpace
138138+) => {
139139+ return [
140140+ { value: '3000', description: 'Development port' },
141141+ { value: '8080', description: 'Production port' },
142142+ ];
143143+};
144144+145145+const cli = createMain(main);
146146+cli();
147147+```
148148+149149+## Recipe
150150+151151+`source <(my-cli complete zsh)` won't be enough since the user would have to run this command each time they spin up a new shell instance.
152152+153153+We suggest this approach for the end user that you as a maintainer might want to push.
154154+155155+```
156156+my-cli completion zsh > ~/completion-for-my-cli.zsh
157157+echo 'source ~/completion-for-my-cli.zsh' >> ~/.zshrc
158158+```
159159+160160+## Autocompletion Server
161161+162162+By integrating tab into your cli, your cli would have a new command called `complete`. This is where all the magic happens. And the shell would contact this command to get completions. That's why we call it the autocompletion server.
163163+164164+```zsh
165165+my-cli complete -- --po
166166+--port Specify the port number
167167+:0
168168+```
169169+170170+The autocompletion server can be a standard to identify whether a package provides autocompletions. Whether running `tool complete --` would result in an output that ends with `:{Number}` (matching the pattern `/:\d+$/`).
171171+172172+In situations like `my-cli dev --po` you'd have autocompletions! But in the case of `pnpm my-cli dev --po` which is what most of us use, tab does not inject autocompletions for a tool like pnpm.
173173+174174+Since pnpm already has its own autocompletion [script](https://pnpm.io/completion), this provides the opportunity to check whether a package provides autocompletions and use those autocompletions if available.
175175+176176+This would also have users avoid injecting autocompletions in their shell config for any tool that provides its own autocompletion script, since pnpm would already support proxying the autocompletions out of the box.
177177+178178+Other package managers like `npm` and `yarn` can decide whether to support this or not too for more universal support.
179179+180180+## Inspiration
181181+182182+- git
183183+- [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go), without cobra, tab would have took 10x longer to build
184184+185185+## TODO
186186+187187+- [] fish
188188+- [] bash
-2
bash.ts
···11-export function generate(name: string, exec: string) {
22-}
-304
cac.ts
···11-// @bombsh/tab/cac
22-import { CAC } from "cac";
33-import * as zsh from "./zsh";
44-import * as bash from "./bash";
55-import * as fish from "./fish";
66-import * as powershell from "./powershell";
77-import {
88- flagMap,
99- Positional,
1010- positionalMap,
1111- ShellCompDirective,
1212-} from "./shared";
1313-1414-function quoteIfNeeded(path: string): string {
1515- return path.includes(" ") ? `'${path}'` : path;
1616-}
1717-1818-const execPath = process.execPath;
1919-const processArgs = process.argv.slice(1);
2020-2121-// Apply the quoting function to each part of x
2222-// This ensures that paths like "Program Files" are quoted for PowerShell execution.
2323-const quotedExecPath = quoteIfNeeded(execPath);
2424-const quotedProcessArgs = processArgs.map(quoteIfNeeded);
2525-const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded);
2626-2727-const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${
2828- quotedProcessArgs[0]
2929-}`;
3030-3131-export default function tab(instance: CAC): void {
3232- instance.command("complete [shell]").action(async (shell, extra) => {
3333- switch (shell) {
3434- case "zsh": {
3535- const script = zsh.generate(instance.name, x);
3636- console.log(script);
3737- break;
3838- }
3939- case "bash": {
4040- const script = bash.generate(instance.name, x);
4141- console.log(script);
4242- break;
4343- }
4444- case "fish": {
4545- const script = fish.generate(instance.name, x);
4646- console.log(script);
4747- break;
4848- }
4949- case "powershell": {
5050- const script = powershell.generate(instance.name, x);
5151- console.log(script);
5252- break;
5353- }
5454- default: {
5555- const args: string[] = extra["--"];
5656-5757- instance.showHelpOnExit = false;
5858- let directive = ShellCompDirective.ShellCompDirectiveDefault;
5959-6060- const endsWithSpace = args[args.length - 1] === "";
6161- if (endsWithSpace) {
6262- args.pop();
6363- }
6464-6565- let toComplete = args[args.length - 1] || "";
6666- const previousArgs = args.slice(0, -1);
6767-6868- const completions: string[] = [];
6969-7070- instance.unsetMatchedCommand();
7171- instance.parse([execPath, processArgs[0], ...previousArgs], {
7272- run: false,
7373- });
7474-7575- const command = instance.matchedCommand ?? instance.globalCommand;
7676-7777- const options = [
7878- ...new Set([
7979- ...(command?.options ?? []),
8080- ...instance.globalCommand.options,
8181- ]),
8282- ];
8383-8484- let isCompletingFlagValue = false;
8585- let flagName = "";
8686- let option: (typeof options)[number] | null = null;
8787- const lastArg = previousArgs[previousArgs.length - 1];
8888-8989- function processOption() {
9090- const matchedOption = options.find((o) =>
9191- o.names.some((name) => name === flagName)
9292- );
9393-9494- if (matchedOption && !matchedOption.isBoolean) {
9595- isCompletingFlagValue = true;
9696- option = matchedOption;
9797- if (endsWithSpace) {
9898- toComplete = "";
9999- }
100100- } else {
101101- isCompletingFlagValue = false;
102102- option = null;
103103- }
104104- }
105105-106106- if (toComplete.startsWith("--")) {
107107- // Long option
108108- flagName = toComplete.slice(2);
109109- const equalsIndex = flagName.indexOf("=");
110110- if (equalsIndex !== -1 && !endsWithSpace) {
111111- // Option with '=', get the name before '='
112112- flagName = flagName.slice(0, equalsIndex);
113113- toComplete = toComplete.slice(toComplete.indexOf("=") + 1);
114114- processOption();
115115- } else if (!endsWithSpace) {
116116- // If not ending with space, still typing option name
117117- flagName = "";
118118- } else {
119119- // User pressed space after typing the option name
120120- processOption();
121121- toComplete = "";
122122- }
123123- } else if (toComplete.startsWith("-") && toComplete.length > 1) {
124124- // Short option
125125- flagName = toComplete.slice(1);
126126- if (!endsWithSpace) {
127127- // Still typing option name
128128- flagName = "";
129129- } else {
130130- processOption();
131131- toComplete = "";
132132- }
133133- } else if (lastArg?.startsWith("--") && !endsWithSpace) {
134134- flagName = lastArg.slice(2);
135135- processOption();
136136- } else if (
137137- lastArg?.startsWith("-") &&
138138- lastArg.length > 1 &&
139139- !endsWithSpace
140140- ) {
141141- flagName = lastArg.slice(2);
142142- processOption();
143143- }
144144-145145- if (isCompletingFlagValue) {
146146- const flagCompletionFn = flagMap.get(
147147- `${command.name} ${option?.name}`
148148- );
149149-150150- if (flagCompletionFn) {
151151- // Call custom completion function for the flag
152152- const comps = await flagCompletionFn(previousArgs, toComplete);
153153- completions.push(
154154- ...comps.map(
155155- (comp) => `${comp.action}\t${comp.description ?? ""}`
156156- )
157157- );
158158- directive = ShellCompDirective.ShellCompDirectiveNoFileComp;
159159- } else {
160160- // Default completion (e.g., file completion)
161161- directive = ShellCompDirective.ShellCompDirectiveDefault;
162162- }
163163- } else if (toComplete.startsWith("-") && !endsWithSpace) {
164164- const flag = toComplete.replace(/^-+/, ""); // Remove leading '-'
165165-166166- // Determine options to suggest
167167- let optionsToSuggest = options.filter((o) => {
168168- const equalToDefault =
169169- "default" in o.config &&
170170- instance.options[o.name] === o.config.default;
171171- return (
172172- o.names.some((name) => name.startsWith(flag)) &&
173173- !(instance.options[o.name] && !equalToDefault)
174174- );
175175- });
176176-177177- const requiredOptions = optionsToSuggest.filter((o) => o.required);
178178-179179- if (requiredOptions.length) {
180180- // Required options not yet specified
181181- optionsToSuggest = requiredOptions;
182182- }
183183-184184- if (optionsToSuggest.length > 0) {
185185- completions.push(
186186- ...optionsToSuggest.map(
187187- (o) => `--${o.name}\t${o.description ?? ""}`
188188- )
189189- );
190190- }
191191-192192- directive = ShellCompDirective.ShellCompDirectiveNoFileComp;
193193- } else {
194194- instance.parse(
195195- [execPath, processArgs[0], ...previousArgs, toComplete],
196196- {
197197- run: false,
198198- }
199199- );
200200- const fullCommandName = args
201201- .filter((arg) => !arg.startsWith("-"))
202202- .join(" ");
203203-204204- for (const c of instance.commands) {
205205- if (c.name === "complete") {
206206- // avoid showing completions for the completion server
207207- continue;
208208- }
209209- const fullCommandParts = fullCommandName.split(" ");
210210- const commandParts: { type: "command"; value: string }[] = c.name
211211- .split(" ")
212212- .map((part) => ({ type: "command", value: part }));
213213- const args: {
214214- type: "positional";
215215- position: number;
216216- value: Positional;
217217- }[] =
218218- positionalMap.get(c.name)?.map((arg, i) => ({
219219- type: "positional",
220220- position: i,
221221- value: arg,
222222- })) ?? [];
223223- const parts = [...commandParts, ...args];
224224-225225- for (let i = 0; i < parts.length; i++) {
226226- const fullCommandPart = fullCommandParts[i];
227227- const part = parts[i];
228228-229229- if (part.type === "command") {
230230- // Command part matching
231231- if (part.value === fullCommandPart) {
232232- // Command part matches user input, continue to next part
233233- continue;
234234- } else if (
235235- !fullCommandPart ||
236236- part.value.startsWith(fullCommandPart)
237237- ) {
238238- // User is typing this command part, provide completion
239239- completions.push(`${part.value}\t${c.description ?? ""}`);
240240- }
241241- // Command part does not match, break
242242- break;
243243- } else if (part.type === "positional") {
244244- const positional = part.value;
245245- // Positional argument handling
246246- if (part.value.variadic) {
247247- const comps = await positional.completion(
248248- previousArgs,
249249- toComplete
250250- );
251251- completions.push(
252252- ...comps.map(
253253- (comp) => `${comp.action}\t${comp.description ?? ""}`
254254- )
255255- );
256256- break;
257257- }
258258- if (typeof fullCommandPart !== "undefined") {
259259- // User has provided input for this positional argument
260260- if (i === fullCommandParts.length - 1 && !endsWithSpace) {
261261- // User is still typing this positional argument, provide completions
262262- const comps = await positional.completion(
263263- previousArgs,
264264- toComplete
265265- );
266266- completions.push(
267267- ...comps.map(
268268- (comp) => `${comp.action}\t${comp.description ?? ""}`
269269- )
270270- );
271271- break;
272272- } else {
273273- // Positional argument is already provided, move to next
274274- previousArgs.push(fullCommandPart);
275275- continue;
276276- }
277277- } else {
278278- // User has not provided input for this positional argument
279279- const comps = await positional.completion(
280280- previousArgs,
281281- toComplete
282282- );
283283- completions.push(
284284- ...comps.map(
285285- (comp) => `${comp.action}\t${comp.description ?? ""}`
286286- )
287287- );
288288- break;
289289- }
290290- }
291291- }
292292- }
293293- }
294294-295295- // Output completions
296296- for (const comp of completions) {
297297- console.log(comp.split("\n")[0].trim());
298298- }
299299- console.log(`:${directive}`);
300300- console.error(`Completion ended with directive: ${directive}`);
301301- }
302302- }
303303- });
304304-}
···11-import { ShellCompDirective } from "./shared";
22-33-// TODO: issue with -- -- completions
44-55-export function generate(
66- name: string,
77- exec: string,
88- includeDesc = false
99-): string {
1010- // Replace '-' and ':' with '_' for variable names
1111- const nameForVar = name.replace(/[-:]/g, "_");
1212-1313- // Determine the completion command
1414- // const compCmd = includeDesc ? "complete" : "complete";
1515-1616- // Shell completion directives
1717- const ShellCompDirectiveError = ShellCompDirective.ShellCompDirectiveError;
1818- const ShellCompDirectiveNoSpace =
1919- ShellCompDirective.ShellCompDirectiveNoSpace;
2020- const ShellCompDirectiveNoFileComp =
2121- ShellCompDirective.ShellCompDirectiveNoFileComp;
2222- const ShellCompDirectiveFilterFileExt =
2323- ShellCompDirective.ShellCompDirectiveFilterFileExt;
2424- const ShellCompDirectiveFilterDirs =
2525- ShellCompDirective.ShellCompDirectiveFilterDirs;
2626- const ShellCompDirectiveKeepOrder =
2727- ShellCompDirective.ShellCompDirectiveKeepOrder;
2828-2929- return `# powershell completion for ${name} -*- shell-script -*-
3030-3131- [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
3232- function __${name}_debug {
3333- if ($env:BASH_COMP_DEBUG_FILE) {
3434- "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE"
3535- }
3636- }
3737-3838- filter __${name}_escapeStringWithSpecialChars {
3939- $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|"|\\||<|>|&','\`$&'
4040- }
4141-4242-[scriptblock]$__${nameForVar}CompleterBlock = {
4343- param(
4444- $WordToComplete,
4545- $CommandAst,
4646- $CursorPosition
4747- )
4848-4949- # Get the current command line and convert into a string
5050- $Command = $CommandAst.CommandElements
5151- $Command = "$Command"
5252-5353- __${name}_debug ""
5454- __${name}_debug "========= starting completion logic =========="
5555- __${name}_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition"
5656-5757- # The user could have moved the cursor backwards on the command-line.
5858- # We need to trigger completion from the $CursorPosition location, so we need
5959- # to truncate the command-line ($Command) up to the $CursorPosition location.
6060- # Make sure the $Command is longer then the $CursorPosition before we truncate.
6161- # This happens because the $Command does not include the last space.
6262- if ($Command.Length -gt $CursorPosition) {
6363- $Command = $Command.Substring(0, $CursorPosition)
6464- }
6565- __${name}_debug "Truncated command: $Command"
6666-6767- $ShellCompDirectiveError=${ShellCompDirectiveError}
6868- $ShellCompDirectiveNoSpace=${ShellCompDirectiveNoSpace}
6969- $ShellCompDirectiveNoFileComp=${ShellCompDirectiveNoFileComp}
7070- $ShellCompDirectiveFilterFileExt=${ShellCompDirectiveFilterFileExt}
7171- $ShellCompDirectiveFilterDirs=${ShellCompDirectiveFilterDirs}
7272- $ShellCompDirectiveKeepOrder=${ShellCompDirectiveKeepOrder}
7373-7474- # Prepare the command to request completions for the program.
7575- # Split the command at the first space to separate the program and arguments.
7676- $Program, $Arguments = $Command.Split(" ", 2)
7777-7878- $RequestComp = "& ${exec} complete -- $Arguments"
7979- __${name}_debug "RequestComp: $RequestComp"
8080-8181- # we cannot use $WordToComplete because it
8282- # has the wrong values if the cursor was moved
8383- # so use the last argument
8484- if ($WordToComplete -ne "" ) {
8585- $WordToComplete = $Arguments.Split(" ")[-1]
8686- }
8787- __${name}_debug "New WordToComplete: $WordToComplete"
8888-8989-9090- # Check for flag with equal sign
9191- $IsEqualFlag = ($WordToComplete -Like "--*=*" )
9292- if ( $IsEqualFlag ) {
9393- __${name}_debug "Completing equal sign flag"
9494- # Remove the flag part
9595- $Flag, $WordToComplete = $WordToComplete.Split("=", 2)
9696- }
9797-9898- if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) {
9999- # If the last parameter is complete (there is a space following it)
100100- # We add an extra empty parameter so we can indicate this to the go method.
101101- __${name}_debug "Adding extra empty parameter"
102102- # PowerShell 7.2+ changed the way how the arguments are passed to executables,
103103- # so for pre-7.2 or when Legacy argument passing is enabled we need to use
104104- if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or
105105- ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or
106106- (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and
107107- $PSNativeCommandArgumentPassing -eq 'Legacy')) {
108108- $RequestComp="$RequestComp" + ' \`"\`"'
109109- } else {
110110- $RequestComp = "$RequestComp" + ' ""'
111111- }
112112- }
113113-114114- __${name}_debug "Calling $RequestComp"
115115- # First disable ActiveHelp which is not supported for Powershell
116116- $env:ActiveHelp = 0
117117-118118- # call the command store the output in $out and redirect stderr and stdout to null
119119- # $Out is an array contains each line per element
120120- Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null
121121-122122- # get directive from last line
123123- [int]$Directive = $Out[-1].TrimStart(':')
124124- if ($Directive -eq "") {
125125- # There is no directive specified
126126- $Directive = 0
127127- }
128128- __${name}_debug "The completion directive is: $Directive"
129129-130130- # remove directive (last element) from out
131131- $Out = $Out | Where-Object { $_ -ne $Out[-1] }
132132- __${name}_debug "The completions are: $Out"
133133-134134- if (($Directive -band $ShellCompDirectiveError) -ne 0 ) {
135135- # Error code. No completion.
136136- __${name}_debug "Received error from custom completion go code"
137137- return
138138- }
139139-140140- $Longest = 0
141141- [Array]$Values = $Out | ForEach-Object {
142142- # Split the output in name and description
143143- $Name, $Description = $_.Split("\`t", 2)
144144- __${name}_debug "Name: $Name Description: $Description"
145145-146146- # Look for the longest completion so that we can format things nicely
147147- if ($Longest -lt $Name.Length) {
148148- $Longest = $Name.Length
149149- }
150150-151151- # Set the description to a one space string if there is none set.
152152- # This is needed because the CompletionResult does not accept an empty string as argument
153153- if (-Not $Description) {
154154- $Description = " "
155155- }
156156- @{ Name = "$Name"; Description = "$Description" }
157157- }
158158-159159-160160- $Space = " "
161161- if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) {
162162- # remove the space here
163163- __${name}_debug "ShellCompDirectiveNoSpace is called"
164164- $Space = ""
165165- }
166166-167167- if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or
168168- (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) {
169169- __${name}_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported"
170170-171171- # return here to prevent the completion of the extensions
172172- return
173173- }
174174-175175- $Values = $Values | Where-Object {
176176- # filter the result
177177- $_.Name -like "$WordToComplete*"
178178-179179- # Join the flag back if we have an equal sign flag
180180- if ( $IsEqualFlag ) {
181181- __${name}_debug "Join the equal sign flag back to the completion value"
182182- $_.Name = $Flag + "=" + $_.Name
183183- }
184184- }
185185-186186- # we sort the values in ascending order by name if keep order isn't passed
187187- if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) {
188188- $Values = $Values | Sort-Object -Property Name
189189- }
190190-191191- if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) {
192192- __${name}_debug "ShellCompDirectiveNoFileComp is called"
193193-194194- if ($Values.Length -eq 0) {
195195- # Just print an empty string here so the
196196- # shell does not start to complete paths.
197197- # We cannot use CompletionResult here because
198198- # it does not accept an empty string as argument.
199199- ""
200200- return
201201- }
202202- }
203203-204204- # Get the current mode
205205- $Mode = (Get-PSReadLineKeyHandler | Where-Object { $_.Key -eq "Tab" }).Function
206206- __${name}_debug "Mode: $Mode"
207207-208208- $Values | ForEach-Object {
209209-210210- # store temporary because switch will overwrite $_
211211- $comp = $_
212212-213213- # PowerShell supports three different completion modes
214214- # - TabCompleteNext (default windows style - on each key press the next option is displayed)
215215- # - Complete (works like bash)
216216- # - MenuComplete (works like zsh)
217217- # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode>
218218-219219- # CompletionResult Arguments:
220220- # 1) CompletionText text to be used as the auto completion result
221221- # 2) ListItemText text to be displayed in the suggestion list
222222- # 3) ResultType type of completion result
223223- # 4) ToolTip text for the tooltip with details about the object
224224-225225- switch ($Mode) {
226226-227227- # bash like
228228- "Complete" {
229229-230230- if ($Values.Length -eq 1) {
231231- __${name}_debug "Only one completion left"
232232-233233- # insert space after value
234234- [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
235235-236236- } else {
237237- # Add the proper number of spaces to align the descriptions
238238- while($comp.Name.Length -lt $Longest) {
239239- $comp.Name = $comp.Name + " "
240240- }
241241-242242- # Check for empty description and only add parentheses if needed
243243- if ($($comp.Description) -eq " " ) {
244244- $Description = ""
245245- } else {
246246- $Description = " ($($comp.Description))"
247247- }
248248-249249- [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)")
250250- }
251251- }
252252-253253- # zsh like
254254- "MenuComplete" {
255255- # insert space after value
256256- # MenuComplete will automatically show the ToolTip of
257257- # the highlighted value at the bottom of the suggestions.
258258- [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
259259- }
260260-261261- # TabCompleteNext and in case we get something unknown
262262- Default {
263263- # Like MenuComplete but we don't want to add a space here because
264264- # the user need to press space anyway to get the completion.
265265- # Description will not be shown because that's not possible with TabCompleteNext
266266- [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
267267- }
268268- }
269269-270270- }
271271-}
272272-273273-Register-ArgumentCompleter -CommandName '${name}' -ScriptBlock $__${nameForVar}CompleterBlock
274274-`;
275275-}
11+import { ShellCompDirective } from './';
22+33+// TODO: issue with -- -- completions
44+55+export function generate(
66+ name: string,
77+ exec: string,
88+ includeDesc = false
99+): string {
1010+ // Replace '-' and ':' with '_' for variable names
1111+ const nameForVar = name.replace(/[-:]/g, '_');
1212+1313+ // Determine the completion command
1414+ // const compCmd = includeDesc ? "complete" : "complete";
1515+1616+ // Shell completion directives
1717+ const ShellCompDirectiveError = ShellCompDirective.ShellCompDirectiveError;
1818+ const ShellCompDirectiveNoSpace =
1919+ ShellCompDirective.ShellCompDirectiveNoSpace;
2020+ const ShellCompDirectiveNoFileComp =
2121+ ShellCompDirective.ShellCompDirectiveNoFileComp;
2222+ const ShellCompDirectiveFilterFileExt =
2323+ ShellCompDirective.ShellCompDirectiveFilterFileExt;
2424+ const ShellCompDirectiveFilterDirs =
2525+ ShellCompDirective.ShellCompDirectiveFilterDirs;
2626+ const ShellCompDirectiveKeepOrder =
2727+ ShellCompDirective.ShellCompDirectiveKeepOrder;
2828+2929+ return `# powershell completion for ${name} -*- shell-script -*-
3030+3131+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
3232+ function __${name}_debug {
3333+ if ($env:BASH_COMP_DEBUG_FILE) {
3434+ "$args" | Out-File -Append -FilePath "$env:BASH_COMP_DEBUG_FILE"
3535+ }
3636+ }
3737+3838+ filter __${name}_escapeStringWithSpecialChars {
3939+ $_ -replace '\\s|#|@|\\$|;|,|''|\\{|\\}|\\(|\\)|"|\\||<|>|&','\`$&'
4040+ }
4141+4242+[scriptblock]$__${nameForVar}CompleterBlock = {
4343+ param(
4444+ $WordToComplete,
4545+ $CommandAst,
4646+ $CursorPosition
4747+ )
4848+4949+ # Get the current command line and convert into a string
5050+ $Command = $CommandAst.CommandElements
5151+ $Command = "$Command"
5252+5353+ __${name}_debug ""
5454+ __${name}_debug "========= starting completion logic =========="
5555+ __${name}_debug "WordToComplete: $WordToComplete Command: $Command CursorPosition: $CursorPosition"
5656+5757+ # The user could have moved the cursor backwards on the command-line.
5858+ # We need to trigger completion from the $CursorPosition location, so we need
5959+ # to truncate the command-line ($Command) up to the $CursorPosition location.
6060+ # Make sure the $Command is longer then the $CursorPosition before we truncate.
6161+ # This happens because the $Command does not include the last space.
6262+ if ($Command.Length -gt $CursorPosition) {
6363+ $Command = $Command.Substring(0, $CursorPosition)
6464+ }
6565+ __${name}_debug "Truncated command: $Command"
6666+6767+ $ShellCompDirectiveError=${ShellCompDirectiveError}
6868+ $ShellCompDirectiveNoSpace=${ShellCompDirectiveNoSpace}
6969+ $ShellCompDirectiveNoFileComp=${ShellCompDirectiveNoFileComp}
7070+ $ShellCompDirectiveFilterFileExt=${ShellCompDirectiveFilterFileExt}
7171+ $ShellCompDirectiveFilterDirs=${ShellCompDirectiveFilterDirs}
7272+ $ShellCompDirectiveKeepOrder=${ShellCompDirectiveKeepOrder}
7373+7474+ # Prepare the command to request completions for the program.
7575+ # Split the command at the first space to separate the program and arguments.
7676+ $Program, $Arguments = $Command.Split(" ", 2)
7777+7878+ $RequestComp = "& ${exec} complete -- $Arguments"
7979+ __${name}_debug "RequestComp: $RequestComp"
8080+8181+ # we cannot use $WordToComplete because it
8282+ # has the wrong values if the cursor was moved
8383+ # so use the last argument
8484+ if ($WordToComplete -ne "" ) {
8585+ $WordToComplete = $Arguments.Split(" ")[-1]
8686+ }
8787+ __${name}_debug "New WordToComplete: $WordToComplete"
8888+8989+9090+ # Check for flag with equal sign
9191+ $IsEqualFlag = ($WordToComplete -Like "--*=*" )
9292+ if ( $IsEqualFlag ) {
9393+ __${name}_debug "Completing equal sign flag"
9494+ # Remove the flag part
9595+ $Flag, $WordToComplete = $WordToComplete.Split("=", 2)
9696+ }
9797+9898+ if ( $WordToComplete -eq "" -And ( -Not $IsEqualFlag )) {
9999+ # If the last parameter is complete (there is a space following it)
100100+ # We add an extra empty parameter so we can indicate this to the go method.
101101+ __${name}_debug "Adding extra empty parameter"
102102+ # PowerShell 7.2+ changed the way how the arguments are passed to executables,
103103+ # so for pre-7.2 or when Legacy argument passing is enabled we need to use
104104+ if ($PSVersionTable.PsVersion -lt [version]'7.2.0' -or
105105+ ($PSVersionTable.PsVersion -lt [version]'7.3.0' -and -not [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -or
106106+ (($PSVersionTable.PsVersion -ge [version]'7.3.0' -or [ExperimentalFeature]::IsEnabled("PSNativeCommandArgumentPassing")) -and
107107+ $PSNativeCommandArgumentPassing -eq 'Legacy')) {
108108+ $RequestComp="$RequestComp" + ' \`"\`"'
109109+ } else {
110110+ $RequestComp = "$RequestComp" + ' ""'
111111+ }
112112+ }
113113+114114+ __${name}_debug "Calling $RequestComp"
115115+ # First disable ActiveHelp which is not supported for Powershell
116116+ $env:ActiveHelp = 0
117117+118118+ # call the command store the output in $out and redirect stderr and stdout to null
119119+ # $Out is an array contains each line per element
120120+ Invoke-Expression -OutVariable out "$RequestComp" 2>&1 | Out-Null
121121+122122+ # get directive from last line
123123+ [int]$Directive = $Out[-1].TrimStart(':')
124124+ if ($Directive -eq "") {
125125+ # There is no directive specified
126126+ $Directive = 0
127127+ }
128128+ __${name}_debug "The completion directive is: $Directive"
129129+130130+ # remove directive (last element) from out
131131+ $Out = $Out | Where-Object { $_ -ne $Out[-1] }
132132+ __${name}_debug "The completions are: $Out"
133133+134134+ if (($Directive -band $ShellCompDirectiveError) -ne 0 ) {
135135+ # Error code. No completion.
136136+ __${name}_debug "Received error from custom completion go code"
137137+ return
138138+ }
139139+140140+ $Longest = 0
141141+ [Array]$Values = $Out | ForEach-Object {
142142+ # Split the output in name and description
143143+ $Name, $Description = $_.Split("\`t", 2)
144144+ __${name}_debug "Name: $Name Description: $Description"
145145+146146+ # Look for the longest completion so that we can format things nicely
147147+ if ($Longest -lt $Name.Length) {
148148+ $Longest = $Name.Length
149149+ }
150150+151151+ # Set the description to a one space string if there is none set.
152152+ # This is needed because the CompletionResult does not accept an empty string as argument
153153+ if (-Not $Description) {
154154+ $Description = " "
155155+ }
156156+ @{ Name = "$Name"; Description = "$Description" }
157157+ }
158158+159159+160160+ $Space = " "
161161+ if (($Directive -band $ShellCompDirectiveNoSpace) -ne 0 ) {
162162+ # remove the space here
163163+ __${name}_debug "ShellCompDirectiveNoSpace is called"
164164+ $Space = ""
165165+ }
166166+167167+ if ((($Directive -band $ShellCompDirectiveFilterFileExt) -ne 0 ) -or
168168+ (($Directive -band $ShellCompDirectiveFilterDirs) -ne 0 )) {
169169+ __${name}_debug "ShellCompDirectiveFilterFileExt ShellCompDirectiveFilterDirs are not supported"
170170+171171+ # return here to prevent the completion of the extensions
172172+ return
173173+ }
174174+175175+ $Values = $Values | Where-Object {
176176+ # filter the result
177177+ $_.Name -like "$WordToComplete*"
178178+179179+ # Join the flag back if we have an equal sign flag
180180+ if ( $IsEqualFlag ) {
181181+ __${name}_debug "Join the equal sign flag back to the completion value"
182182+ $_.Name = $Flag + "=" + $_.Name
183183+ }
184184+ }
185185+186186+ # we sort the values in ascending order by name if keep order isn't passed
187187+ if (($Directive -band $ShellCompDirectiveKeepOrder) -eq 0 ) {
188188+ $Values = $Values | Sort-Object -Property Name
189189+ }
190190+191191+ if (($Directive -band $ShellCompDirectiveNoFileComp) -ne 0 ) {
192192+ __${name}_debug "ShellCompDirectiveNoFileComp is called"
193193+194194+ if ($Values.Length -eq 0) {
195195+ # Just print an empty string here so the
196196+ # shell does not start to complete paths.
197197+ # We cannot use CompletionResult here because
198198+ # it does not accept an empty string as argument.
199199+ ""
200200+ return
201201+ }
202202+ }
203203+204204+ # Get the current mode
205205+ $Mode = (Get-PSReadLineKeyHandler | Where-Object { $_.Key -eq "Tab" }).Function
206206+ __${name}_debug "Mode: $Mode"
207207+208208+ $Values | ForEach-Object {
209209+210210+ # store temporary because switch will overwrite $_
211211+ $comp = $_
212212+213213+ # PowerShell supports three different completion modes
214214+ # - TabCompleteNext (default windows style - on each key press the next option is displayed)
215215+ # - Complete (works like bash)
216216+ # - MenuComplete (works like zsh)
217217+ # You set the mode with Set-PSReadLineKeyHandler -Key Tab -Function <mode>
218218+219219+ # CompletionResult Arguments:
220220+ # 1) CompletionText text to be used as the auto completion result
221221+ # 2) ListItemText text to be displayed in the suggestion list
222222+ # 3) ResultType type of completion result
223223+ # 4) ToolTip text for the tooltip with details about the object
224224+225225+ switch ($Mode) {
226226+227227+ # bash like
228228+ "Complete" {
229229+230230+ if ($Values.Length -eq 1) {
231231+ __${name}_debug "Only one completion left"
232232+233233+ # insert space after value
234234+ [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
235235+236236+ } else {
237237+ # Add the proper number of spaces to align the descriptions
238238+ while($comp.Name.Length -lt $Longest) {
239239+ $comp.Name = $comp.Name + " "
240240+ }
241241+242242+ # Check for empty description and only add parentheses if needed
243243+ if ($($comp.Description) -eq " " ) {
244244+ $Description = ""
245245+ } else {
246246+ $Description = " ($($comp.Description))"
247247+ }
248248+249249+ [System.Management.Automation.CompletionResult]::new("$($comp.Name)$Description", "$($comp.Name)$Description", 'ParameterValue', "$($comp.Description)")
250250+ }
251251+ }
252252+253253+ # zsh like
254254+ "MenuComplete" {
255255+ # insert space after value
256256+ # MenuComplete will automatically show the ToolTip of
257257+ # the highlighted value at the bottom of the suggestions.
258258+ [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars) + $Space, "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
259259+ }
260260+261261+ # TabCompleteNext and in case we get something unknown
262262+ Default {
263263+ # Like MenuComplete but we don't want to add a space here because
264264+ # the user need to press space anyway to get the completion.
265265+ # Description will not be shown because that's not possible with TabCompleteNext
266266+ [System.Management.Automation.CompletionResult]::new($($comp.Name | __${name}_escapeStringWithSpecialChars), "$($comp.Name)", 'ParameterValue', "$($comp.Description)")
267267+ }
268268+ }
269269+270270+ }
271271+}
272272+273273+Register-ArgumentCompleter -CommandName '${name}' -ScriptBlock $__${nameForVar}CompleterBlock
274274+`;
275275+}
+91-92
shadcn.cac.ts
···11-import fs from "fs/promises";
22-import cac from "cac";
33-import {
44- Callback,
55- Completion,
66- flagMap,
77- Positional,
88- positionalMap,
99-} from "./shared";
1010-import path from "path";
1111-import tab from "./cac";
11+// import fs from "fs/promises";
22+// import cac from "cac";
33+// import {
44+// Callback,
55+// Completion,
66+// flagMap,
77+// Positional,
88+// positionalMap,
99+// } from "./shared";
1010+// import path from "path";
1111+// import tab from "./cac";
12121313-const cli = cac("shadcn"); // Using 'shadcn' as the CLI tool name
1313+// const cli = cac("shadcn"); // Using 'shadcn' as the CLI tool name
14141515-// Global options
1616-cli
1717- .option("-c, --cwd [cwd]", `[string] the working directory. defaults to the current directory.`)
1818- .option("-h, --help", `display help for command`);
1515+// // Global options
1616+// cli
1717+// .option("-c, --cwd [cwd]", `[string] the working directory. defaults to the current directory.`)
1818+// .option("-h, --help", `display help for command`);
19192020-// Init command
2121-cli
2222- .command("init", "initialize your project and install dependencies")
2323- .option("-d, --defaults", `[boolean] use default values i.e new-york, zinc, and css variables`, { default: false })
2424- .option("-f, --force", `[boolean] force overwrite of existing components.json`, { default: false })
2525- .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false })
2626- .action((options) => {
2727- console.log(`Initializing project with options:`, options);
2828- });
2020+// // Init command
2121+// cli
2222+// .command("init", "initialize your project and install dependencies")
2323+// .option("-d, --defaults", `[boolean] use default values i.e new-york, zinc, and css variables`, { default: false })
2424+// .option("-f, --force", `[boolean] force overwrite of existing components.json`, { default: false })
2525+// .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false })
2626+// .action((options) => {
2727+// console.log(`Initializing project with options:`, options);
2828+// });
29293030-// Add command
3131-cli
3232- .command("add [...components]", "add a component to your project")
3333- .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false })
3434- .option("-o, --overwrite", `[boolean] overwrite existing files`, { default: false })
3535- .option("-a, --all", `[boolean] add all available components`, { default: false })
3636- .option("-p, --path [path]", `[string] the path to add the component to`)
3737- .action((components, options) => {
3838- console.log(`Adding components:`, components, `with options:`, options);
3939- });
3030+// // Add command
3131+// cli
3232+// .command("add [...components]", "add a component to your project")
3333+// .option("-y, --yes", `[boolean] skip confirmation prompt`, { default: false })
3434+// .option("-o, --overwrite", `[boolean] overwrite existing files`, { default: false })
3535+// .option("-a, --all", `[boolean] add all available components`, { default: false })
3636+// .option("-p, --path [path]", `[string] the path to add the component to`)
3737+// .action((components, options) => {
3838+// console.log(`Adding components:`, components, `with options:`, options);
3939+// });
40404141-// Build positional completions for each command using command.args
4242-for (const c of [cli.globalCommand, ...cli.commands]) {
4343- // Handle options
4444- for (const o of [...cli.globalCommand.options, ...c.options]) {
4545- const optionKey = `${c.name} ${o.name}`;
4141+// // Build positional completions for each command using command.args
4242+// for (const c of [cli.globalCommand, ...cli.commands]) {
4343+// // Handle options
4444+// for (const o of [...cli.globalCommand.options, ...c.options]) {
4545+// const optionKey = `${c.name} ${o.name}`;
46464747- if (o.rawName.includes("--cwd <cwd>")) {
4848- // Completion for --cwd (common working directories)
4949- flagMap.set(optionKey, async (previousArgs, toComplete) => {
5050- return [
5151- { action: "./apps/www", description: "Default app directory" },
5252- { action: "./apps/admin", description: "Admin app directory" },
5353- ].filter((comp) => comp.action.startsWith(toComplete));
5454- });
5555- }
4747+// if (o.rawName.includes("--cwd <cwd>")) {
4848+// // Completion for --cwd (common working directories)
4949+// flagMap.set(optionKey, async (previousArgs, toComplete) => {
5050+// return [
5151+// { action: "./apps/www", description: "Default app directory" },
5252+// { action: "./apps/admin", description: "Admin app directory" },
5353+// ].filter((comp) => comp.action.startsWith(toComplete));
5454+// });
5555+// }
56565757- if (o.rawName.includes("--defaults")) {
5858- // Completion for --defaults (show info for default setup)
5959- flagMap.set(optionKey, async (previousArgs, toComplete) => {
6060- return [{ action: "true", description: "Use default values for setup" }];
6161- });
6262- }
5757+// if (o.rawName.includes("--defaults")) {
5858+// // Completion for --defaults (show info for default setup)
5959+// flagMap.set(optionKey, async (previousArgs, toComplete) => {
6060+// return [{ action: "true", description: "Use default values for setup" }];
6161+// });
6262+// }
63636464- if (o.rawName.includes("--path <path>")) {
6565- // Completion for --path (common component paths)
6666- flagMap.set(optionKey, async (previousArgs, toComplete) => {
6767- return [
6868- { action: "src/components", description: "Main components directory" },
6969- { action: "src/ui", description: "UI components directory" },
7070- ].filter((comp) => comp.action.startsWith(toComplete));
7171- });
7272- }
7373- }
6464+// if (o.rawName.includes("--path <path>")) {
6565+// // Completion for --path (common component paths)
6666+// flagMap.set(optionKey, async (previousArgs, toComplete) => {
6767+// return [
6868+// { action: "src/components", description: "Main components directory" },
6969+// { action: "src/ui", description: "UI components directory" },
7070+// ].filter((comp) => comp.action.startsWith(toComplete));
7171+// });
7272+// }
7373+// }
74747575- // Handle positional arguments
7676- if (c.name === "add" && c.args && c.args.length > 0) {
7777- const componentChoices = [
7878- "accordion", "alert", "alert-dialog", "aspect-ratio", "avatar",
7979- "badge", "button", "calendar", "card", "checkbox"
8080- ];
8181- const positionals = c.args.map((arg) => ({
8282- required: arg.required,
8383- variadic: arg.variadic,
8484- value: arg.value,
8585- completion: async (previousArgs, toComplete) => {
8686- // if (arg.value === "root") {
8787- return componentChoices
8888- // TODO: a bug here that toComplete is equal to "add" which then makes filter not work, we should omit toComplete and add it to previous args if the endsWithSpace is true
8989- // .filter((comp) => comp.startsWith(toComplete))
9090- .map((comp) => ({ action: comp, description: `Add ${comp} component` }));
9191- // }
9292- // return [];
9393- },
9494- }));
7575+// // Handle positional arguments
7676+// if (c.name === "add" && c.args && c.args.length > 0) {
7777+// const componentChoices = [
7878+// "accordion", "alert", "alert-dialog", "aspect-ratio", "avatar",
7979+// "badge", "button", "calendar", "card", "checkbox"
8080+// ];
8181+// const positionals = c.args.map((arg) => ({
8282+// required: arg.required,
8383+// variadic: arg.variadic,
8484+// value: arg.value,
8585+// completion: async (previousArgs, toComplete) => {
8686+// // if (arg.value === "root") {
8787+// return componentChoices
8888+// // TODO: a bug here that toComplete is equal to "add" which then makes filter not work, we should omit toComplete and add it to previous args if the endsWithSpace is true
8989+// // .filter((comp) => comp.startsWith(toComplete))
9090+// .map((comp) => ({ action: comp, description: `Add ${comp} component` }));
9191+// // }
9292+// // return [];
9393+// },
9494+// }));
95959696- positionalMap.set(c.name, positionals);
9797- }
9898-}
9696+// positionalMap.set(c.name, positionals);
9797+// }
9898+// }
9999100100-// Initialize tab completion
101101-tab(cli);
100100+// // Initialize tab completion
101101+// tab(cli);
102102103103-cli.parse();
104104-103103+// cli.parse();
-75
shared.ts
···11-22-// ShellCompRequestCmd is the name of the hidden command that is used to request
33-// completion results from the program. It is used by the shell completion scripts.
44-export const ShellCompRequestCmd: string = "__complete";
55-66-// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
77-// completion results without their description. It is used by the shell completion scripts.
88-export const ShellCompNoDescRequestCmd: string = "__completeNoDesc";
99-1010-// ShellCompDirective is a bit map representing the different behaviors the shell
1111-// can be instructed to have once completions have been provided.
1212-export const ShellCompDirective = {
1313- // ShellCompDirectiveError indicates an error occurred and completions should be ignored.
1414- ShellCompDirectiveError: 1 << 0,
1515-1616- // ShellCompDirectiveNoSpace indicates that the shell should not add a space
1717- // after the completion even if there is a single completion provided.
1818- ShellCompDirectiveNoSpace: 1 << 1,
1919-2020- // ShellCompDirectiveNoFileComp indicates that the shell should not provide
2121- // file completion even when no completion is provided.
2222- ShellCompDirectiveNoFileComp: 1 << 2,
2323-2424- // ShellCompDirectiveFilterFileExt indicates that the provided completions
2525- // should be used as file extension filters.
2626- // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
2727- // is a shortcut to using this directive explicitly. The BashCompFilenameExt
2828- // annotation can also be used to obtain the same behavior for flags.
2929- ShellCompDirectiveFilterFileExt: 1 << 3,
3030-3131- // ShellCompDirectiveFilterDirs indicates that only directory names should
3232- // be provided in file completion. To request directory names within another
3333- // directory, the returned completions should specify the directory within
3434- // which to search. The BashCompSubdirsInDir annotation can be used to
3535- // obtain the same behavior but only for flags.
3636- ShellCompDirectiveFilterDirs: 1 << 4,
3737-3838- // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order
3939- // in which the completions are provided.
4040- ShellCompDirectiveKeepOrder: 1 << 5,
4141-4242- // ===========================================================================
4343-4444- // All directives using iota (or equivalent in Go) should be above this one.
4545- // For internal use.
4646- shellCompDirectiveMaxValue: 1 << 6,
4747-4848- // ShellCompDirectiveDefault indicates to let the shell perform its default
4949- // behavior after completions have been provided.
5050- // This one must be last to avoid messing up the iota count.
5151- ShellCompDirectiveDefault: 0,
5252-};
5353-5454-5555-5656-export type Completion = {
5757- action: string;
5858- description?: string;
5959-};
6060-6161-export type Callback = (
6262- previousArgs: string[],
6363- toComplete: string,
6464-) => Completion[] | Promise<Completion[]>;
6565-6666-export type Positional = {
6767- required: boolean;
6868- variadic: boolean;
6969- completion: Callback;
7070-};
7171-7272-7373-export const positionalMap = new Map<string, Positional[]>();
7474-7575-export const flagMap = new Map<string, Callback>();
+1
src/bash.ts
···11+export function generate(name: string, exec: string) {}
+366
src/cac.ts
···11+import * as zsh from "./zsh";
22+import * as bash from "./bash";
33+import * as fish from "./fish";
44+import * as powershell from "./powershell";
55+import type { CAC } from "cac";
66+import { Completion } from "./";
77+88+const execPath = process.execPath;
99+const processArgs = process.argv.slice(1);
1010+const quotedExecPath = quoteIfNeeded(execPath);
1111+const quotedProcessArgs = processArgs.map(quoteIfNeeded);
1212+const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded);
1313+1414+const x = `${quotedExecPath} ${quotedProcessExecArgs.join(" ")} ${quotedProcessArgs[0]}`;
1515+1616+function quoteIfNeeded(path: string): string {
1717+ return path.includes(" ") ? `'${path}'` : path;
1818+}
1919+2020+// export default function tab(instance: CAC): void {
2121+// instance.command("complete [shell]").action(async (shell, extra) => {
2222+// switch (shell) {
2323+// case "zsh": {
2424+// const script = zsh.generate(instance.name, x);
2525+// console.log(script);
2626+// break;
2727+// }
2828+// case "bash": {
2929+// const script = bash.generate(instance.name, x);
3030+// console.log(script);
3131+// break;
3232+// }
3333+// case "fish": {
3434+// const script = fish.generate(instance.name, x);
3535+// console.log(script);
3636+// break;
3737+// }
3838+// case "powershell": {
3939+// const script = powershell.generate(instance.name, x);
4040+// console.log(script);
4141+// break;
4242+// }
4343+// default: {
4444+// const args: string[] = extra["--"];
4545+4646+// instance.showHelpOnExit = false;
4747+// let directive = ShellCompDirective.ShellCompDirectiveDefault;
4848+4949+// const endsWithSpace = args[args.length - 1] === "";
5050+// if (endsWithSpace) {
5151+// args.pop();
5252+// }
5353+5454+// let toComplete = args[args.length - 1] || "";
5555+// const previousArgs = args.slice(0, -1);
5656+5757+// const completions: string[] = [];
5858+5959+// instance.unsetMatchedCommand();
6060+// instance.parse([execPath, processArgs[0], ...previousArgs], {
6161+// run: false,
6262+// });
6363+6464+// const command = instance.matchedCommand ?? instance.globalCommand;
6565+6666+// const options = [
6767+// ...new Set([
6868+// ...(command?.options ?? []),
6969+// ...instance.globalCommand.options,
7070+// ]),
7171+// ];
7272+7373+// let isCompletingFlagValue = false;
7474+// let flagName = "";
7575+// let option: (typeof options)[number] | null = null;
7676+// const lastArg = previousArgs[previousArgs.length - 1];
7777+7878+// function processOption() {
7979+// const matchedOption = options.find((o) =>
8080+// o.names.some((name) => name === flagName)
8181+// );
8282+8383+// if (matchedOption && !matchedOption.isBoolean) {
8484+// isCompletingFlagValue = true;
8585+// option = matchedOption;
8686+// if (endsWithSpace) {
8787+// toComplete = "";
8888+// }
8989+// } else {
9090+// isCompletingFlagValue = false;
9191+// option = null;
9292+// }
9393+// }
9494+9595+// if (toComplete.startsWith("--")) {
9696+// // Long option
9797+// flagName = toComplete.slice(2);
9898+// const equalsIndex = flagName.indexOf("=");
9999+// if (equalsIndex !== -1 && !endsWithSpace) {
100100+// // Option with '=', get the name before '='
101101+// flagName = flagName.slice(0, equalsIndex);
102102+// toComplete = toComplete.slice(toComplete.indexOf("=") + 1);
103103+// processOption();
104104+// } else if (!endsWithSpace) {
105105+// // If not ending with space, still typing option name
106106+// flagName = "";
107107+// } else {
108108+// // User pressed space after typing the option name
109109+// processOption();
110110+// toComplete = "";
111111+// }
112112+// } else if (toComplete.startsWith("-") && toComplete.length > 1) {
113113+// // Short option
114114+// flagName = toComplete.slice(1);
115115+// if (!endsWithSpace) {
116116+// // Still typing option name
117117+// flagName = "";
118118+// } else {
119119+// processOption();
120120+// toComplete = "";
121121+// }
122122+// } else if (lastArg?.startsWith("--") && !endsWithSpace) {
123123+// flagName = lastArg.slice(2);
124124+// processOption();
125125+// } else if (
126126+// lastArg?.startsWith("-") &&
127127+// lastArg.length > 1 &&
128128+// !endsWithSpace
129129+// ) {
130130+// flagName = lastArg.slice(2);
131131+// processOption();
132132+// }
133133+134134+// if (isCompletingFlagValue) {
135135+// const flagCompletionFn = flagMap.get(
136136+// `${command.name} ${option?.name}`
137137+// );
138138+139139+// if (flagCompletionFn) {
140140+// // Call custom completion function for the flag
141141+// const comps = await flagCompletionFn(previousArgs, toComplete);
142142+// completions.push(
143143+// ...comps.map(
144144+// (comp) => `${comp.action}\t${comp.description ?? ""}`
145145+// )
146146+// );
147147+// directive = ShellCompDirective.ShellCompDirectiveNoFileComp;
148148+// } else {
149149+// // Default completion (e.g., file completion)
150150+// directive = ShellCompDirective.ShellCompDirectiveDefault;
151151+// }
152152+// } else if (toComplete.startsWith("-") && !endsWithSpace) {
153153+// const flag = toComplete.replace(/^-+/, ""); // Remove leading '-'
154154+155155+// // Determine options to suggest
156156+// let optionsToSuggest = options.filter((o) => {
157157+// const equalToDefault =
158158+// "default" in o.config &&
159159+// instance.options[o.name] === o.config.default;
160160+// return (
161161+// o.names.some((name) => name.startsWith(flag)) &&
162162+// !(instance.options[o.name] && !equalToDefault)
163163+// );
164164+// });
165165+166166+// const requiredOptions = optionsToSuggest.filter((o) => o.required);
167167+168168+// if (requiredOptions.length) {
169169+// // Required options not yet specified
170170+// optionsToSuggest = requiredOptions;
171171+// }
172172+173173+// if (optionsToSuggest.length > 0) {
174174+// completions.push(
175175+// ...optionsToSuggest.map(
176176+// (o) => `--${o.name}\t${o.description ?? ""}`
177177+// )
178178+// );
179179+// }
180180+181181+// directive = ShellCompDirective.ShellCompDirectiveNoFileComp;
182182+// } else {
183183+// instance.parse(
184184+// [execPath, processArgs[0], ...previousArgs, toComplete],
185185+// {
186186+// run: false,
187187+// }
188188+// );
189189+// const fullCommandName = args
190190+// .filter((arg) => !arg.startsWith("-"))
191191+// .join(" ");
192192+193193+// for (const c of instance.commands) {
194194+// if (c.name === "complete") {
195195+// // avoid showing completions for the completion server
196196+// continue;
197197+// }
198198+// const fullCommandParts = fullCommandName.split(" ");
199199+// const commandParts: { type: "command"; value: string }[] = c.name
200200+// .split(" ")
201201+// .map((part) => ({ type: "command", value: part }));
202202+// const args: {
203203+// type: "positional";
204204+// position: number;
205205+// value: Positional;
206206+// }[] =
207207+// positionalMap.get(c.name)?.map((arg, i) => ({
208208+// type: "positional",
209209+// position: i,
210210+// value: arg,
211211+// })) ?? [];
212212+// const parts = [...commandParts, ...args];
213213+214214+// for (let i = 0; i < parts.length; i++) {
215215+// const fullCommandPart = fullCommandParts[i];
216216+// const part = parts[i];
217217+218218+// if (part.type === "command") {
219219+// // Command part matching
220220+// if (part.value === fullCommandPart) {
221221+// // Command part matches user input, continue to next part
222222+// continue;
223223+// } else if (
224224+// !fullCommandPart ||
225225+// part.value.startsWith(fullCommandPart)
226226+// ) {
227227+// // User is typing this command part, provide completion
228228+// completions.push(`${part.value}\t${c.description ?? ""}`);
229229+// }
230230+// // Command part does not match, break
231231+// break;
232232+// } else if (part.type === "positional") {
233233+// const positional = part.value;
234234+// // Positional argument handling
235235+// if (part.value.variadic) {
236236+// const comps = await positional.completion(
237237+// previousArgs,
238238+// toComplete
239239+// );
240240+// completions.push(
241241+// ...comps.map(
242242+// (comp) => `${comp.action}\t${comp.description ?? ""}`
243243+// )
244244+// );
245245+// break;
246246+// }
247247+// if (typeof fullCommandPart !== "undefined") {
248248+// // User has provided input for this positional argument
249249+// if (i === fullCommandParts.length - 1 && !endsWithSpace) {
250250+// // User is still typing this positional argument, provide completions
251251+// const comps = await positional.completion(
252252+// previousArgs,
253253+// toComplete
254254+// );
255255+// completions.push(
256256+// ...comps.map(
257257+// (comp) => `${comp.action}\t${comp.description ?? ""}`
258258+// )
259259+// );
260260+// break;
261261+// } else {
262262+// // Positional argument is already provided, move to next
263263+// previousArgs.push(fullCommandPart);
264264+// continue;
265265+// }
266266+// } else {
267267+// // User has not provided input for this positional argument
268268+// const comps = await positional.completion(
269269+// previousArgs,
270270+// toComplete
271271+// );
272272+// completions.push(
273273+// ...comps.map(
274274+// (comp) => `${comp.action}\t${comp.description ?? ""}`
275275+// )
276276+// );
277277+// break;
278278+// }
279279+// }
280280+// }
281281+// }
282282+// }
283283+284284+// // Output completions
285285+// for (const comp of completions) {
286286+// console.log(comp.split("\n")[0].trim());
287287+// }
288288+// console.log(`:${directive}`);
289289+// console.error(`Completion ended with directive: ${directive}`);
290290+// }
291291+// }
292292+// });
293293+// }
294294+295295+export default function tab(instance: CAC): Completion {
296296+ const completion = new Completion();
297297+298298+ // Add all commands and their options
299299+ for (const cmd of [instance.globalCommand, ...instance.commands]) {
300300+ if (cmd.name === 'complete') continue; // Skip completion command
301301+302302+ // Get positional args info from command usage
303303+ const args = (cmd.rawName.match(/\[.*?\]|\<.*?\>/g) || [])
304304+ .map(arg => arg.startsWith('[')); // true if optional (wrapped in [])
305305+306306+ // Add command to completion
307307+ const commandName = completion.addCommand(
308308+ cmd.name === '@@global@@' ? '' : cmd.name,
309309+ cmd.description || '',
310310+ args,
311311+ async () => []
312312+ );
313313+314314+ // Add command options
315315+ for (const option of [...instance.globalCommand.options, ...cmd.options]) {
316316+ completion.addOption(
317317+ commandName,
318318+ `--${option.name}`,
319319+ option.description || '',
320320+ async () => []
321321+ );
322322+ }
323323+ }
324324+325325+ instance.command("complete [shell]").action(async (shell, extra) => {
326326+ switch (shell) {
327327+ case "zsh": {
328328+ const script = zsh.generate(instance.name, x);
329329+ console.log(script);
330330+ break;
331331+ }
332332+ case "bash": {
333333+ const script = bash.generate(instance.name, x);
334334+ console.log(script);
335335+ break;
336336+ }
337337+ case "fish": {
338338+ const script = fish.generate(instance.name, x);
339339+ console.log(script);
340340+ break;
341341+ }
342342+ case "powershell": {
343343+ const script = powershell.generate(instance.name, x);
344344+ console.log(script);
345345+ break;
346346+ }
347347+ default: {
348348+ const args: string[] = extra["--"];
349349+ instance.showHelpOnExit = false;
350350+351351+ // Parse current command context
352352+ instance.unsetMatchedCommand();
353353+ instance.parse([execPath, processArgs[0], ...args], {
354354+ run: false,
355355+ });
356356+357357+ // const matchedCommand = instance.matchedCommand?.name || '';
358358+ // const potentialCommand = args.join(' ')
359359+ // console.log(potentialCommand)
360360+ return completion.parse(args);
361361+ }
362362+ }
363363+ });
364364+365365+ return completion;
366366+}
+185
src/citty.ts
···11+import { ArgDef, defineCommand, parseArgs } from 'citty';
22+import * as zsh from './zsh';
33+import * as bash from './bash';
44+import * as fish from './fish';
55+import * as powershell from './powershell';
66+import { Completion } from '.';
77+import type {
88+ ArgsDef,
99+ CommandDef,
1010+ PositionalArgDef,
1111+ SubCommandsDef,
1212+} from 'citty';
1313+1414+function quoteIfNeeded(path: string) {
1515+ return path.includes(' ') ? `'${path}'` : path;
1616+}
1717+1818+const execPath = process.execPath;
1919+const processArgs = process.argv.slice(1);
2020+const quotedExecPath = quoteIfNeeded(execPath);
2121+const quotedProcessArgs = processArgs.map(quoteIfNeeded);
2222+const quotedProcessExecArgs = process.execArgv.map(quoteIfNeeded);
2323+const x = `${quotedExecPath} ${quotedProcessExecArgs.join(' ')} ${quotedProcessArgs[0]}`;
2424+2525+function isConfigPositional<T extends ArgsDef>(config: CommandDef<T>) {
2626+ return (
2727+ config.args &&
2828+ Object.values(config.args).some((arg) => arg.type === 'positional')
2929+ );
3030+}
3131+3232+async function handleSubCommands<T extends ArgsDef = ArgsDef>(
3333+ completion: Completion,
3434+ subCommands: SubCommandsDef,
3535+ parentCmd?: string
3636+) {
3737+ for (const [cmd, resolvableConfig] of Object.entries(subCommands)) {
3838+ const config = await resolve(resolvableConfig);
3939+ const meta = await resolve(config.meta);
4040+ const subCommands = await resolve(config.subCommands);
4141+4242+ if (!meta || typeof meta?.description !== 'string') {
4343+ throw new Error('Invalid meta or missing description.');
4444+ }
4545+ const isPositional = isConfigPositional(config);
4646+ const name = completion.addCommand(
4747+ cmd,
4848+ meta.description,
4949+ isPositional ? [false] : [],
5050+ async (previousArgs, toComplete, endsWithSpace) => {
5151+ return [];
5252+ },
5353+ parentCmd
5454+ );
5555+5656+ // Handle nested subcommands recursively
5757+ if (subCommands) {
5858+ await handleSubCommands(completion, subCommands, name);
5959+ }
6060+6161+ // Handle arguments
6262+ if (config.args) {
6363+ for (const [argName, argConfig] of Object.entries(config.args)) {
6464+ const conf = argConfig as ArgDef;
6565+ if (conf.type === 'positional') {
6666+ continue;
6767+ }
6868+ completion.addOption(
6969+ name,
7070+ `--${argName}`,
7171+ conf.description ?? '',
7272+ async (previousArgs, toComplete, endsWithSpace) => {
7373+ return [];
7474+ }
7575+ );
7676+ }
7777+ }
7878+ }
7979+}
8080+8181+export default async function tab<T extends ArgsDef = ArgsDef>(
8282+ instance: CommandDef<T>
8383+) {
8484+ const completion = new Completion();
8585+8686+ const meta = await resolve(instance.meta);
8787+8888+ if (!meta) {
8989+ throw new Error('Invalid meta.');
9090+ }
9191+ const name = meta.name;
9292+ if (!name) {
9393+ throw new Error('Invalid meta or missing name.');
9494+ }
9595+9696+ const subCommands = await resolve(instance.subCommands);
9797+ if (!subCommands) {
9898+ throw new Error('Invalid or missing subCommands.');
9999+ }
100100+101101+ const root = '';
102102+ const isPositional = isConfigPositional(instance);
103103+ completion.addCommand(
104104+ root,
105105+ meta?.description ?? '',
106106+ isPositional ? [false] : [],
107107+ async (previousArgs, toComplete, endsWithSpace) => {
108108+ return [];
109109+ }
110110+ );
111111+112112+ await handleSubCommands(completion, subCommands);
113113+114114+ if (instance.args) {
115115+ for (const [argName, argConfig] of Object.entries(instance.args)) {
116116+ const conf = argConfig as PositionalArgDef;
117117+ completion.addOption(
118118+ root,
119119+ `--${argName}`,
120120+ conf.description ?? '',
121121+ async (previousArgs, toComplete, endsWithSpace) => {
122122+ return [];
123123+ }
124124+ );
125125+ }
126126+ }
127127+128128+ const completeCommand = defineCommand({
129129+ meta: {
130130+ name: 'complete',
131131+ description: 'Generate shell completion scripts',
132132+ },
133133+ async run(ctx) {
134134+ let shell: string | undefined = ctx.rawArgs[0];
135135+ const extra = ctx.rawArgs.slice(ctx.rawArgs.indexOf('--') + 1);
136136+137137+ if (shell === '--') {
138138+ shell = undefined;
139139+ }
140140+141141+ switch (shell) {
142142+ case 'zsh': {
143143+ const script = zsh.generate(name, x);
144144+ console.log(script);
145145+ break;
146146+ }
147147+ case 'bash': {
148148+ const script = bash.generate(name, x);
149149+ console.log(script);
150150+ break;
151151+ }
152152+ case 'fish': {
153153+ const script = fish.generate(name, x);
154154+ console.log(script);
155155+ break;
156156+ }
157157+ case 'powershell': {
158158+ const script = powershell.generate(name, x);
159159+ console.log(script);
160160+ break;
161161+ }
162162+ default: {
163163+ const args = (await resolve(instance.args))!;
164164+ const parsed = parseArgs(extra, args);
165165+ // TODO: this is not ideal at all
166166+ const matchedCommand = parsed._.join(' ').trim();
167167+ // TODO: `command lint i` does not work because `lint` and `i` are potential commands
168168+ // instead the potential command should only be `lint`
169169+ // and `i` is the to be completed part
170170+ return completion.parse(extra, matchedCommand);
171171+ }
172172+ }
173173+ },
174174+ });
175175+176176+ subCommands.complete = completeCommand;
177177+178178+ return completion;
179179+}
180180+181181+type Resolvable<T> = T | Promise<T> | (() => T) | (() => Promise<T>);
182182+183183+async function resolve<T>(resolvable: Resolvable<T>): Promise<T> {
184184+ return resolvable instanceof Function ? await resolvable() : await resolvable;
185185+}
+1
src/fish.ts
···11+export function generate(name: string, exec: string) {}
+361
src/index.ts
···11+import * as zsh from './zsh';
22+import * as bash from './bash';
33+import * as fish from './fish';
44+import * as powershell from './powershell';
55+66+// ShellCompRequestCmd is the name of the hidden command that is used to request
77+// completion results from the program. It is used by the shell completion scripts.
88+export const ShellCompRequestCmd: string = '__complete';
99+1010+// ShellCompNoDescRequestCmd is the name of the hidden command that is used to request
1111+// completion results without their description. It is used by the shell completion scripts.
1212+export const ShellCompNoDescRequestCmd: string = '__completeNoDesc';
1313+1414+// ShellCompDirective is a bit map representing the different behaviors the shell
1515+// can be instructed to have once completions have been provided.
1616+export const ShellCompDirective = {
1717+ // ShellCompDirectiveError indicates an error occurred and completions should be ignored.
1818+ ShellCompDirectiveError: 1 << 0,
1919+2020+ // ShellCompDirectiveNoSpace indicates that the shell should not add a space
2121+ // after the completion even if there is a single completion provided.
2222+ ShellCompDirectiveNoSpace: 1 << 1,
2323+2424+ // ShellCompDirectiveNoFileComp indicates that the shell should not provide
2525+ // file completion even when no completion is provided.
2626+ ShellCompDirectiveNoFileComp: 1 << 2,
2727+2828+ // ShellCompDirectiveFilterFileExt indicates that the provided completions
2929+ // should be used as file extension filters.
3030+ // For flags, using Command.MarkFlagFilename() and Command.MarkPersistentFlagFilename()
3131+ // is a shortcut to using this directive explicitly. The BashCompFilenameExt
3232+ // annotation can also be used to obtain the same behavior for flags.
3333+ ShellCompDirectiveFilterFileExt: 1 << 3,
3434+3535+ // ShellCompDirectiveFilterDirs indicates that only directory names should
3636+ // be provided in file completion. To request directory names within another
3737+ // directory, the returned completions should specify the directory within
3838+ // which to search. The BashCompSubdirsInDir annotation can be used to
3939+ // obtain the same behavior but only for flags.
4040+ ShellCompDirectiveFilterDirs: 1 << 4,
4141+4242+ // ShellCompDirectiveKeepOrder indicates that the shell should preserve the order
4343+ // in which the completions are provided.
4444+ ShellCompDirectiveKeepOrder: 1 << 5,
4545+4646+ // ===========================================================================
4747+4848+ // All directives using iota (or equivalent in Go) should be above this one.
4949+ // For internal use.
5050+ shellCompDirectiveMaxValue: 1 << 6,
5151+5252+ // ShellCompDirectiveDefault indicates to let the shell perform its default
5353+ // behavior after completions have been provided.
5454+ // This one must be last to avoid messing up the iota count.
5555+ ShellCompDirectiveDefault: 0,
5656+};
5757+5858+export type Positional = {
5959+ required: boolean;
6060+ variadic: boolean;
6161+ completion: Handler;
6262+};
6363+6464+type Item = {
6565+ description: string;
6666+ value: string;
6767+};
6868+6969+type Handler = (
7070+ previousArgs: string[],
7171+ toComplete: string,
7272+ endsWithSpace: boolean
7373+) => Item[] | Promise<Item[]>;
7474+7575+type Option = {
7676+ description: string;
7777+ handler: Handler;
7878+};
7979+8080+type Command = {
8181+ name: string;
8282+ description: string;
8383+ args: boolean[]
8484+ handler: Handler;
8585+ options: Map<string, Option>;
8686+ parent?: Command;
8787+};
8888+8989+export class Completion {
9090+ commands = new Map<string, Command>();
9191+ completions: Item[] = []
9292+ directive = ShellCompDirective.ShellCompDirectiveDefault;
9393+9494+ // vite <entry> <another> [...files]
9595+ // args: [false, false, true], only the last argument can be variadic
9696+ addCommand(
9797+ name: string,
9898+ description: string,
9999+ args: boolean[],
100100+ handler: Handler,
101101+ parent?: string
102102+ ) {
103103+ const key = parent ? `${parent} ${name}` : name;
104104+ this.commands.set(key, {
105105+ name: key,
106106+ description,
107107+ args,
108108+ handler,
109109+ options: new Map(),
110110+ parent: parent ? this.commands.get(parent)! : undefined,
111111+ });
112112+ return key;
113113+ }
114114+115115+ // --port
116116+ addOption(
117117+ command: string,
118118+ option: string,
119119+ description: string,
120120+ handler: Handler
121121+ ) {
122122+ const cmd = this.commands.get(command);
123123+ if (!cmd) {
124124+ throw new Error(`Command ${command} not found.`);
125125+ }
126126+ cmd.options.set(option, { description, handler });
127127+ return option;
128128+ }
129129+130130+ // TODO: this should be aware of boolean args and stuff
131131+ private stripOptions(args: string[]): string[] {
132132+ const parts: string[] = []
133133+ let option = false;
134134+ for (const k of args) {
135135+ if (k.startsWith('-')) {
136136+ option = true;
137137+ continue
138138+ }
139139+ if (option) {
140140+ option = false;
141141+ continue
142142+ }
143143+ parts.push(k)
144144+ }
145145+ return parts
146146+ }
147147+148148+ private matchCommand(args: string[]): [Command, string[]] {
149149+ args = this.stripOptions(args);
150150+ let parts: string[] = [];
151151+ let remaining: string[] = [];
152152+ let matched: Command = this.commands.get('')!
153153+ for (let i = 0; i < args.length; i++) {
154154+ const k = args[i];
155155+ parts.push(k);
156156+ const potential = this.commands.get(parts.join(' '))
157157+158158+ if (potential) {
159159+ matched = potential;
160160+ } else {
161161+ remaining = args.slice(i, args.length);
162162+ break
163163+ }
164164+ }
165165+ return [matched, remaining];
166166+ }
167167+168168+ async parse(args: string[]) {
169169+ const endsWithSpace = args[args.length - 1] === '';
170170+171171+ if (endsWithSpace) {
172172+ args.pop();
173173+ }
174174+175175+ let toComplete = args[args.length - 1] || '';
176176+ const previousArgs = args.slice(0, -1);
177177+178178+ if (endsWithSpace) {
179179+ previousArgs.push(toComplete);
180180+ toComplete = '';
181181+ }
182182+183183+ const [matchedCommand, remaining] = this.matchCommand(previousArgs);
184184+185185+ const lastPrevArg = previousArgs[previousArgs.length - 1];
186186+187187+ // 1. Handle flag/option completion
188188+ if (this.shouldCompleteFlags(lastPrevArg, toComplete, endsWithSpace)) {
189189+ await this.handleFlagCompletion(
190190+ matchedCommand,
191191+ previousArgs,
192192+ toComplete,
193193+ endsWithSpace,
194194+ lastPrevArg,
195195+ );
196196+ }
197197+ else {
198198+ // 2. Handle command/subcommand completion
199199+ if (this.shouldCompleteCommands(toComplete, endsWithSpace)) {
200200+ await this.handleCommandCompletion(
201201+ previousArgs,
202202+ toComplete,
203203+ );
204204+ }
205205+ // 3. Handle positional arguments
206206+ if (matchedCommand && matchedCommand.args.length > 0) {
207207+ await this.handlePositionalCompletion(
208208+ matchedCommand,
209209+ previousArgs,
210210+ toComplete,
211211+ endsWithSpace,
212212+ );
213213+ }
214214+ }
215215+ this.complete(toComplete)
216216+ }
217217+218218+ private complete(toComplete: string) {
219219+ this.directive = ShellCompDirective.ShellCompDirectiveNoFileComp;
220220+221221+ const seen = new Set<string>();
222222+ this.completions
223223+ .filter((comp) => {
224224+ if (seen.has(comp.value)) return false;
225225+ seen.add(comp.value);
226226+ return true;
227227+ })
228228+ .filter((comp) => comp.value.startsWith(toComplete))
229229+ .forEach((comp) => console.log(`${comp.value}\t${comp.description ?? ''}`));
230230+ console.log(`:${this.directive}`);
231231+ }
232232+233233+ private shouldCompleteFlags(lastPrevArg: string | undefined, toComplete: string, endsWithSpace: boolean): boolean {
234234+ return (lastPrevArg?.startsWith('--')) || toComplete.startsWith('--');
235235+ }
236236+237237+ private shouldCompleteCommands(toComplete: string, endsWithSpace: boolean): boolean {
238238+ return !toComplete.startsWith('-');
239239+ }
240240+241241+ private async handleFlagCompletion(
242242+ command: Command,
243243+ previousArgs: string[],
244244+ toComplete: string,
245245+ endsWithSpace: boolean,
246246+ lastPrevArg: string | undefined,
247247+ ) {
248248+ // Handle flag value completion
249249+ let flagName: string | undefined;
250250+ let valueToComplete = toComplete;
251251+252252+ if (toComplete.includes('=')) {
253253+ // Handle --flag=value case
254254+ const parts = toComplete.split('=');
255255+ flagName = parts[0];
256256+ valueToComplete = parts[1] || '';
257257+ } else if (lastPrevArg?.startsWith('--')) {
258258+ // Handle --flag value case
259259+ flagName = lastPrevArg;
260260+ }
261261+262262+ if (flagName) {
263263+ const option = command.options.get(flagName);
264264+ if (option) {
265265+ const suggestions = await option.handler(previousArgs, valueToComplete, endsWithSpace);
266266+ if (toComplete.includes('=')) {
267267+ // Reconstruct the full flag=value format
268268+ this.completions = suggestions.map(suggestion => ({
269269+ value: `${flagName}=${suggestion.value}`,
270270+ description: suggestion.description
271271+ }));
272272+ } else {
273273+ this.completions.push(...suggestions);
274274+ }
275275+ }
276276+ return;
277277+ }
278278+279279+ // Handle flag name completion
280280+ if (toComplete.startsWith('--')) {
281281+ for (const [name, option] of command.options) {
282282+ if (name.startsWith(toComplete)) {
283283+ this.completions.push({
284284+ value: name,
285285+ description: option.description
286286+ });
287287+ }
288288+ }
289289+ }
290290+ }
291291+292292+ private async handleCommandCompletion(
293293+ previousArgs: string[],
294294+ toComplete: string,
295295+ ) {
296296+ const commandParts = [...previousArgs];
297297+298298+ for (const k of this.commands.keys()) {
299299+ if (k === '') continue;
300300+ const parts = k.split(' ');
301301+ let match = true;
302302+303303+ let i = 0;
304304+ while (i < commandParts.length) {
305305+ if (parts[i] !== commandParts[i]) {
306306+ match = false;
307307+ break
308308+ }
309309+ i++;
310310+ }
311311+ if (match && parts[i]?.startsWith(toComplete)) {
312312+ this.completions.push({
313313+ value: parts[i],
314314+ description: this.commands.get(k)!.description
315315+ });
316316+ }
317317+ }
318318+ }
319319+320320+ private async handlePositionalCompletion(
321321+ command: Command,
322322+ previousArgs: string[],
323323+ toComplete: string,
324324+ endsWithSpace: boolean,
325325+ ) {
326326+ const suggestions = await command.handler(previousArgs, toComplete, endsWithSpace);
327327+ this.completions.push(...suggestions);
328328+ }
329329+}
330330+331331+export function script(
332332+ shell: 'zsh' | 'bash' | 'fish' | 'powershell',
333333+ name: string,
334334+ x: string
335335+) {
336336+ switch (shell) {
337337+ case 'zsh': {
338338+ const script = zsh.generate(name, x);
339339+ console.log(script);
340340+ break;
341341+ }
342342+ case 'bash': {
343343+ const script = bash.generate(name, x);
344344+ console.log(script);
345345+ break;
346346+ }
347347+ case 'fish': {
348348+ const script = fish.generate(name, x);
349349+ console.log(script);
350350+ break;
351351+ }
352352+ case 'powershell': {
353353+ const script = powershell.generate(name, x);
354354+ console.log(script);
355355+ break;
356356+ }
357357+ default: {
358358+ throw new Error(`Unsupported shell: ${shell}`);
359359+ }
360360+ }
361361+}
src/shared.ts
This is a binary file and will not be displayed.
+143
tests/__snapshots__/cli.test.ts.snap
···11+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
22+33+exports[`cli completion tests for cac > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = `
44+"--port Specify port
55+:4
66+"
77+`;
88+99+exports[`cli completion tests for cac > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = `
1010+":4
1111+"
1212+`;
1313+1414+exports[`cli completion tests for cac > cli option value handling > should handle unknown options with no completions 1`] = `":4"`;
1515+1616+exports[`cli completion tests for cac > cli option value handling > should not show duplicate options 1`] = `
1717+"--config Use specified config file
1818+--mode Set env mode
1919+--logLevel info | warn | error | silent
2020+:4
2121+"
2222+`;
2323+2424+exports[`cli completion tests for cac > cli option value handling > should resolve config option values correctly 1`] = `
2525+"vite.config.ts Vite config file
2626+vite.config.js Vite config file
2727+:4
2828+"
2929+`;
3030+3131+exports[`cli completion tests for cac > cli option value handling > should resolve port value correctly 1`] = `
3232+"--port=3000 Development server port
3333+:4
3434+"
3535+`;
3636+3737+exports[`cli completion tests for cac > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = `
3838+"--port Specify port
3939+:4
4040+"
4141+`;
4242+4343+exports[`cli completion tests for cac > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = `
4444+"3000 Development server port
4545+8080 Alternative port
4646+:4
4747+"
4848+`;
4949+5050+exports[`cli completion tests for cac > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = `
5151+"--port=3000 Development server port
5252+--port=8080 Alternative port
5353+:4
5454+"
5555+`;
5656+5757+exports[`cli completion tests for cac > positional argument completions > should complete multiple positional arguments when ending with part of the value 1`] = `
5858+"index.ts Index file
5959+:4
6060+"
6161+`;
6262+6363+exports[`cli completion tests for cac > positional argument completions > should complete multiple positional arguments when ending with space 1`] = `
6464+"main.ts Main file
6565+index.ts Index file
6666+:4
6767+"
6868+`;
6969+7070+exports[`cli completion tests for cac > positional argument completions > should complete single positional argument when ending with space 1`] = `
7171+"main.ts Main file
7272+index.ts Index file
7373+:4
7474+"
7575+`;
7676+7777+exports[`cli completion tests for cac > should complete cli options 1`] = `
7878+"dev Start dev server
7979+lint Lint project
8080+:4
8181+"
8282+`;
8383+8484+exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '--p', expected: '--port' }' 1`] = `
8585+"--port Specify port
8686+:4
8787+"
8888+`;
8989+9090+exports[`cli completion tests for citty > cli option exclusion tests > should not suggest already specified option '{ specified: '--config', shouldNotContain: '--config' }' 1`] = `
9191+":4
9292+"
9393+`;
9494+9595+exports[`cli completion tests for citty > cli option value handling > should handle unknown options with no completions 1`] = `":4"`;
9696+9797+exports[`cli completion tests for citty > cli option value handling > should not show duplicate options 1`] = `
9898+"--config Use specified config file
9999+--mode Set env mode
100100+--logLevel info | warn | error | silent
101101+:4
102102+"
103103+`;
104104+105105+exports[`cli completion tests for citty > cli option value handling > should resolve config option values correctly 1`] = `
106106+"vite.config.ts Vite config file
107107+vite.config.js Vite config file
108108+:4
109109+"
110110+`;
111111+112112+exports[`cli completion tests for citty > cli option value handling > should resolve port value correctly 1`] = `
113113+"--port=3000 Development server port
114114+:4
115115+"
116116+`;
117117+118118+exports[`cli completion tests for citty > edge case completions for end with space > should keep suggesting the --port option if user typed partial but didn't end with space 1`] = `
119119+"--port Specify port
120120+:4
121121+"
122122+`;
123123+124124+exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user ends with space after \`--port\` 1`] = `
125125+"3000 Development server port
126126+8080 Alternative port
127127+:4
128128+"
129129+`;
130130+131131+exports[`cli completion tests for citty > edge case completions for end with space > should suggest port values if user typed \`--port=\` and hasn't typed a space or value yet 1`] = `
132132+"--port=3000 Development server port
133133+--port=8080 Alternative port
134134+:4
135135+"
136136+`;
137137+138138+exports[`cli completion tests for citty > should complete cli options 1`] = `
139139+"dev Start dev server
140140+lint Lint project
141141+:4
142142+"
143143+`;
···11+{
22+ "compilerOptions": {
33+ /* Visit https://aka.ms/tsconfig to read more about this file */
44+55+ /* Projects */
66+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
77+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
88+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
99+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
1010+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
1111+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
1212+1313+ /* Language and Environment */
1414+ "target": "es2022" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
1515+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
1616+ // "jsx": "preserve", /* Specify what JSX code is generated. */
1717+ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
1818+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
1919+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
2020+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
2121+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
2222+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
2323+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
2424+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
2525+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
2626+2727+ /* Modules */
2828+ "module": "es2022" /* Specify what module code is generated. */,
2929+ // "rootDir": "./", /* Specify the root folder within your source files. */
3030+ "moduleResolution": "node10" /* Specify how TypeScript looks up a file from a given module specifier. */,
3131+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
3232+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
3333+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
3434+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
3535+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
3636+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
3737+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
3838+ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
3939+ // "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
4040+ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
4141+ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
4242+ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
4343+ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
4444+ // "resolveJsonModule": true, /* Enable importing .json files. */
4545+ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
4646+ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
4747+4848+ /* JavaScript Support */
4949+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
5050+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
5151+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
5252+5353+ /* Emit */
5454+ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
5555+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
5656+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
5757+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
5858+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
5959+ // "noEmit": true, /* Disable emitting files from a compilation. */
6060+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
6161+ // "outDir": "./", /* Specify an output folder for all emitted files. */
6262+ // "removeComments": true, /* Disable emitting comments. */
6363+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
6464+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
6565+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
6666+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
6767+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
6868+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
6969+ // "newLine": "crlf", /* Set the newline character for emitting files. */
7070+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
7171+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
7272+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
7373+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
7474+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
7575+7676+ /* Interop Constraints */
7777+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
7878+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
7979+ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
8080+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
8181+ "esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
8282+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
8383+ "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
8484+8585+ /* Type Checking */
8686+ "strict": true /* Enable all strict type-checking options. */,
8787+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
8888+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
8989+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
9090+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
9191+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
9292+ // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
9393+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
9494+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
9595+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
9696+ "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */,
9797+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
9898+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
9999+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
100100+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
101101+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
102102+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
103103+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
104104+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
105105+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
106106+107107+ /* Completeness */
108108+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
109109+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
110110+ }
111111+}
+1-2
zsh.ts
src/zsh.ts
···11-import { ShellCompDirective } from "./shared";
11+import { ShellCompDirective } from './';
2233export function generate(name: string, exec: string) {
44 return `#compdef ${name}
···212212fi
213213`;
214214}
215215-