[READ-ONLY] Mirror of https://github.com/bombshell-dev/tab. shell autocompletions for javascript CLIs
4

Configure Feed

Select the types of activity you want to include in your feed.

feat: add fish.ts & bash.ts (#19)

* feat: add fish.ts & bash.ts

* fix: format

* cleanup: remove unused type definition

* apply suggestions from @43081j review

* fix: format

* moar cleanup

* fix: add missing serve command to demo.cac.ts to correct CLI test snapshots

* fix: add serve command and use snapshot testing for shells

- Add missing serve command to demo.cac.ts to correct CLI test snapshots

- Implement suggestion to use toMatchSnapshot() for fish shell tests

- Update snapshots to include proper completions

authored by

Paul Valladares and committed by
GitHub
(Apr 12, 2025, 8:51 PM +0330) d31d3d14 820a3d65

+1120 -217
+65 -5
README.md
··· 6 6 7 7 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. 8 8 9 + ## Examples 10 + 11 + Check out the [examples directory](./examples) for complete examples of using Tab with different command-line frameworks: 12 + 13 + - [CAC](./examples/demo.cac.ts) 14 + - [Citty](./examples/demo.citty.ts) 15 + - [Commander.js](./examples/demo.commander.ts) 16 + 17 + ## Usage 18 + 9 19 ```ts 10 20 import { Completion, script } from '@bombsh/tab'; 11 21 ··· 146 156 cli(); 147 157 ``` 148 158 159 + ### `@bombsh/tab/commander` 160 + 161 + ```ts 162 + import { Command } from 'commander'; 163 + import tab from '@bombsh/tab/commander'; 164 + 165 + const program = new Command('my-cli'); 166 + program.version('1.0.0'); 167 + 168 + // Add commands 169 + program 170 + .command('serve') 171 + .description('Start the server') 172 + .option('-p, --port <number>', 'port to use', '3000') 173 + .option('-H, --host <host>', 'host to use', 'localhost') 174 + .action((options) => { 175 + console.log('Starting server...'); 176 + }); 177 + 178 + // Initialize tab completion 179 + const completion = tab(program); 180 + 181 + // Configure custom completions 182 + for (const command of completion.commands.values()) { 183 + if (command.name === 'serve') { 184 + for (const [option, config] of command.options.entries()) { 185 + if (option === '--port') { 186 + config.handler = () => { 187 + return [ 188 + { value: '3000', description: 'Default port' }, 189 + { value: '8080', description: 'Alternative port' }, 190 + ]; 191 + }; 192 + } 193 + } 194 + } 195 + } 196 + 197 + program.parse(); 198 + ``` 199 + 149 200 ## Recipe 150 201 151 202 `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. ··· 157 208 echo 'source ~/completion-for-my-cli.zsh' >> ~/.zshrc 158 209 ``` 159 210 211 + For other shells: 212 + 213 + ```bash 214 + # Bash 215 + my-cli complete bash > ~/.bash_completion.d/my-cli 216 + echo 'source ~/.bash_completion.d/my-cli' >> ~/.bashrc 217 + 218 + # Fish 219 + my-cli complete fish > ~/.config/fish/completions/my-cli.fish 220 + 221 + # PowerShell 222 + my-cli complete powershell > $PROFILE.CurrentUserAllHosts 223 + ``` 224 + 160 225 ## Autocompletion Server 161 226 162 227 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. ··· 181 246 182 247 - git 183 248 - [cobra](https://github.com/spf13/cobra/blob/main/shell_completions.go), without cobra, tab would have took 10x longer to build 184 - 185 - ## TODO 186 - 187 - - [] fish 188 - - [] bash
+7 -1
demo.cac.ts examples/demo.cac.ts
··· 1 1 import cac from 'cac'; 2 - import tab from './src/cac'; 2 + import tab from '../src/cac'; 3 3 4 4 const cli = cac('vite'); 5 5 ··· 10 10 11 11 cli 12 12 .command('dev', 'Start dev server') 13 + .option('-H, --host [host]', `Specify hostname`) 14 + .option('-p, --port <port>', `Specify port`) 15 + .action((options) => {}); 16 + 17 + cli 18 + .command('serve', 'Start the server') 13 19 .option('-H, --host [host]', `Specify hostname`) 14 20 .option('-p, --port <port>', `Specify port`) 15 21 .action((options) => {});
-128
demo.citty.ts
··· 1 - import { defineCommand, createMain, CommandDef, ArgsDef } from 'citty'; 2 - import tab from './src/citty'; 3 - 4 - const main = defineCommand({ 5 - meta: { 6 - name: 'vite', 7 - description: 'Vite CLI tool', 8 - }, 9 - args: { 10 - config: { 11 - type: 'string', 12 - description: 'Use specified config file', 13 - alias: 'c', 14 - }, 15 - mode: { type: 'string', description: 'Set env mode', alias: 'm' }, 16 - logLevel: { 17 - type: 'string', 18 - description: 'info | warn | error | silent', 19 - alias: 'l', 20 - }, 21 - }, 22 - run(_ctx) {}, 23 - }); 24 - 25 - const devCommand = defineCommand({ 26 - meta: { 27 - name: 'dev', 28 - description: 'Start dev server', 29 - }, 30 - args: { 31 - host: { type: 'string', description: 'Specify hostname' }, 32 - port: { type: 'string', description: 'Specify port' }, 33 - }, 34 - run(ctx) {}, 35 - }); 36 - 37 - devCommand.subCommands = { 38 - build: defineCommand({ 39 - meta: { 40 - name: 'build', 41 - description: 'Build project', 42 - }, 43 - run({ args }) {}, 44 - }), 45 - }; 46 - 47 - const lintCommand = defineCommand({ 48 - meta: { 49 - name: 'lint', 50 - description: 'Lint project', 51 - }, 52 - args: { 53 - files: { type: 'positional', description: 'Files to lint' }, 54 - }, 55 - run(ctx) {}, 56 - }); 57 - 58 - main.subCommands = { 59 - dev: devCommand, 60 - lint: lintCommand, 61 - } as Record<string, CommandDef<ArgsDef>>; 62 - 63 - const completion = await tab(main, { 64 - subCommands: { 65 - lint: { 66 - handler() { 67 - return [ 68 - { value: 'main.ts', description: 'Main file' }, 69 - { value: 'index.ts', description: 'Index file' }, 70 - ]; 71 - }, 72 - }, 73 - dev: { 74 - options: { 75 - port: { 76 - handler() { 77 - return [ 78 - { value: '3000', description: 'Development server port' }, 79 - { value: '8080', description: 'Alternative port' }, 80 - ]; 81 - }, 82 - }, 83 - host: { 84 - handler() { 85 - return [ 86 - { value: 'localhost', description: 'Localhost' }, 87 - { value: '0.0.0.0', description: 'All interfaces' }, 88 - ]; 89 - }, 90 - }, 91 - }, 92 - }, 93 - }, 94 - options: { 95 - config: { 96 - handler() { 97 - return [ 98 - { value: 'vite.config.ts', description: 'Vite config file' }, 99 - { value: 'vite.config.js', description: 'Vite config file' }, 100 - ]; 101 - }, 102 - }, 103 - mode: { 104 - handler() { 105 - return [ 106 - { value: 'development', description: 'Development mode' }, 107 - { value: 'production', description: 'Production mode' }, 108 - ]; 109 - }, 110 - }, 111 - logLevel: { 112 - handler() { 113 - return [ 114 - { value: 'info', description: 'Info level' }, 115 - { value: 'warn', description: 'Warn level' }, 116 - { value: 'error', description: 'Error level' }, 117 - { value: 'silent', description: 'Silent level' }, 118 - ]; 119 - }, 120 - }, 121 - }, 122 - }); 123 - 124 - void completion; 125 - 126 - const cli = createMain(main); 127 - 128 - cli();
+4 -12
demo.commander.ts examples/demo.commander.ts
··· 1 1 import { Command } from 'commander'; 2 - import tab from './src/commander'; 2 + import tab from '../src/commander'; 3 3 4 4 // Create a new Commander program 5 5 const program = new Command('myapp'); ··· 111 111 if (process.argv[2] === 'test-completion') { 112 112 const args = process.argv.slice(3); 113 113 console.log('Testing completion with args:', args); 114 - 115 - // Special case for deploy command with a space at the end 116 - if (args.length === 1 && args[0] === 'deploy ') { 117 - console.log('staging Deploy to staging environment'); 118 - console.log('production Deploy to production environment'); 119 - console.log(':2'); 120 - } else { 121 - completion.parse(args).then(() => { 122 - // Done 123 - }); 124 - } 114 + completion.parse(args).then(() => { 115 + // Done 116 + }); 125 117 } else { 126 118 // Parse command line arguments 127 119 program.parse();
+132
examples/demo.citty.ts
··· 1 + import { defineCommand, createMain, CommandDef, ArgsDef } from 'citty'; 2 + import tab from '../src/citty'; 3 + 4 + const main = defineCommand({ 5 + meta: { 6 + name: 'vite', 7 + version: '0.0.0', 8 + description: 'Vite CLI', 9 + }, 10 + args: { 11 + config: { 12 + type: 'string', 13 + description: 'Use specified config file', 14 + alias: 'c', 15 + }, 16 + mode: { 17 + type: 'string', 18 + description: 'Set env mode', 19 + alias: 'm', 20 + }, 21 + logLevel: { 22 + type: 'string', 23 + description: 'info | warn | error | silent', 24 + alias: 'l', 25 + }, 26 + }, 27 + run: (_ctx) => {}, 28 + }); 29 + 30 + const devCommand = defineCommand({ 31 + meta: { 32 + name: 'dev', 33 + description: 'Start dev server', 34 + }, 35 + args: { 36 + host: { 37 + type: 'string', 38 + description: 'Specify hostname', 39 + alias: 'H', 40 + }, 41 + port: { 42 + type: 'string', 43 + description: 'Specify port', 44 + alias: 'p', 45 + }, 46 + }, 47 + run: () => {}, 48 + }); 49 + 50 + const buildCommand = defineCommand({ 51 + meta: { 52 + name: 'build', 53 + description: 'Build project', 54 + }, 55 + run: () => {}, 56 + }); 57 + 58 + const lintCommand = defineCommand({ 59 + meta: { 60 + name: 'lint', 61 + description: 'Lint project', 62 + }, 63 + args: { 64 + files: { 65 + type: 'positional', 66 + description: 'Files to lint', 67 + required: false, 68 + }, 69 + }, 70 + run: () => {}, 71 + }); 72 + 73 + main.subCommands = { 74 + dev: devCommand, 75 + build: buildCommand, 76 + lint: lintCommand, 77 + } as Record<string, CommandDef<ArgsDef>>; 78 + 79 + const completion = await tab(main, { 80 + options: { 81 + config: { 82 + handler: () => [ 83 + { value: 'vite.config.ts', description: 'Vite config file' }, 84 + { value: 'vite.config.js', description: 'Vite config file' }, 85 + ], 86 + }, 87 + mode: { 88 + handler: () => [ 89 + { value: 'development', description: 'Development mode' }, 90 + { value: 'production', description: 'Production mode' }, 91 + ], 92 + }, 93 + logLevel: { 94 + handler: () => [ 95 + { value: 'info', description: 'Info level' }, 96 + { value: 'warn', description: 'Warn level' }, 97 + { value: 'error', description: 'Error level' }, 98 + { value: 'silent', description: 'Silent level' }, 99 + ], 100 + }, 101 + }, 102 + 103 + subCommands: { 104 + lint: { 105 + handler: () => [ 106 + { value: 'main.ts', description: 'Main file' }, 107 + { value: 'index.ts', description: 'Index file' }, 108 + ], 109 + }, 110 + dev: { 111 + options: { 112 + port: { 113 + handler: () => [ 114 + { value: '3000', description: 'Development server port' }, 115 + { value: '8080', description: 'Alternative port' }, 116 + ], 117 + }, 118 + host: { 119 + handler: () => [ 120 + { value: 'localhost', description: 'Localhost' }, 121 + { value: '0.0.0.0', description: 'All interfaces' }, 122 + ], 123 + }, 124 + }, 125 + }, 126 + }, 127 + }); 128 + 129 + void completion; 130 + 131 + const cli = createMain(main); 132 + cli();
+122 -1
src/bash.ts
··· 1 - export function generate(name: string, exec: string) {} 1 + import { ShellCompDirective } from './'; 2 + 3 + export function generate(name: string, exec: string): string { 4 + // Replace '-' and ':' with '_' for variable names 5 + const nameForVar = name.replace(/[-:]/g, '_'); 6 + 7 + // Shell completion directives 8 + const ShellCompDirectiveError = ShellCompDirective.ShellCompDirectiveError; 9 + const ShellCompDirectiveNoSpace = 10 + ShellCompDirective.ShellCompDirectiveNoSpace; 11 + const ShellCompDirectiveNoFileComp = 12 + ShellCompDirective.ShellCompDirectiveNoFileComp; 13 + const ShellCompDirectiveFilterFileExt = 14 + ShellCompDirective.ShellCompDirectiveFilterFileExt; 15 + const ShellCompDirectiveFilterDirs = 16 + ShellCompDirective.ShellCompDirectiveFilterDirs; 17 + const ShellCompDirectiveKeepOrder = 18 + ShellCompDirective.ShellCompDirectiveKeepOrder; 19 + 20 + return `# bash completion for ${name} 21 + 22 + # Define shell completion directives 23 + readonly ShellCompDirectiveError=${ShellCompDirectiveError} 24 + readonly ShellCompDirectiveNoSpace=${ShellCompDirectiveNoSpace} 25 + readonly ShellCompDirectiveNoFileComp=${ShellCompDirectiveNoFileComp} 26 + readonly ShellCompDirectiveFilterFileExt=${ShellCompDirectiveFilterFileExt} 27 + readonly ShellCompDirectiveFilterDirs=${ShellCompDirectiveFilterDirs} 28 + readonly ShellCompDirectiveKeepOrder=${ShellCompDirectiveKeepOrder} 29 + 30 + # Function to debug completion 31 + __${nameForVar}_debug() { 32 + if [[ -n \${BASH_COMP_DEBUG_FILE:-} ]]; then 33 + echo "$*" >> "\${BASH_COMP_DEBUG_FILE}" 34 + fi 35 + } 36 + 37 + # Function to handle completions 38 + __${nameForVar}_complete() { 39 + local cur prev words cword 40 + _get_comp_words_by_ref -n "=:" cur prev words cword 41 + 42 + local requestComp out directive 43 + 44 + # Build the command to get completions 45 + requestComp="${exec} complete -- \${words[@]:1}" 46 + 47 + # Add an empty parameter if the last parameter is complete 48 + if [[ -z "$cur" ]]; then 49 + requestComp="$requestComp ''" 50 + fi 51 + 52 + # Get completions from the program 53 + out=$(eval "$requestComp" 2>/dev/null) 54 + 55 + # Extract directive if present 56 + directive=0 57 + if [[ "$out" == *:* ]]; then 58 + directive=\${out##*:} 59 + out=\${out%:*} 60 + fi 61 + 62 + # Process completions based on directive 63 + if [[ $((directive & $ShellCompDirectiveError)) -ne 0 ]]; then 64 + # Error, no completion 65 + return 66 + fi 67 + 68 + # Apply directives 69 + if [[ $((directive & $ShellCompDirectiveNoSpace)) -ne 0 ]]; then 70 + compopt -o nospace 71 + fi 72 + if [[ $((directive & $ShellCompDirectiveKeepOrder)) -ne 0 ]]; then 73 + compopt -o nosort 74 + fi 75 + if [[ $((directive & $ShellCompDirectiveNoFileComp)) -ne 0 ]]; then 76 + compopt +o default 77 + fi 78 + 79 + # Handle file extension filtering 80 + if [[ $((directive & $ShellCompDirectiveFilterFileExt)) -ne 0 ]]; then 81 + local filter="" 82 + for ext in $out; do 83 + filter="$filter|$ext" 84 + done 85 + filter="\\.($filter)" 86 + compopt -o filenames 87 + COMPREPLY=( $(compgen -f -X "!$filter" -- "$cur") ) 88 + return 89 + fi 90 + 91 + # Handle directory filtering 92 + if [[ $((directive & $ShellCompDirectiveFilterDirs)) -ne 0 ]]; then 93 + compopt -o dirnames 94 + COMPREPLY=( $(compgen -d -- "$cur") ) 95 + return 96 + fi 97 + 98 + # Process completions 99 + local IFS=$'\\n' 100 + local tab=$(printf '\\t') 101 + 102 + # Parse completions with descriptions 103 + local completions=() 104 + while read -r comp; do 105 + if [[ "$comp" == *$tab* ]]; then 106 + # Split completion and description 107 + local value=\${comp%%$tab*} 108 + local desc=\${comp#*$tab} 109 + completions+=("$value") 110 + else 111 + completions+=("$comp") 112 + fi 113 + done <<< "$out" 114 + 115 + # Return completions 116 + COMPREPLY=( $(compgen -W "\${completions[*]}" -- "$cur") ) 117 + } 118 + 119 + # Register completion function 120 + complete -F __${nameForVar}_complete ${name} 121 + `; 122 + }
+170 -1
src/fish.ts
··· 1 - export function generate(name: string, exec: string) {} 1 + import { ShellCompDirective } from './'; 2 + 3 + export function generate(name: string, exec: string): string { 4 + // Replace '-' and ':' with '_' for variable names 5 + const nameForVar = name.replace(/[-:]/g, '_'); 6 + 7 + // Shell completion directives 8 + const ShellCompDirectiveError = ShellCompDirective.ShellCompDirectiveError; 9 + const ShellCompDirectiveNoSpace = 10 + ShellCompDirective.ShellCompDirectiveNoSpace; 11 + const ShellCompDirectiveNoFileComp = 12 + ShellCompDirective.ShellCompDirectiveNoFileComp; 13 + const ShellCompDirectiveFilterFileExt = 14 + ShellCompDirective.ShellCompDirectiveFilterFileExt; 15 + const ShellCompDirectiveFilterDirs = 16 + ShellCompDirective.ShellCompDirectiveFilterDirs; 17 + const ShellCompDirectiveKeepOrder = 18 + ShellCompDirective.ShellCompDirectiveKeepOrder; 19 + 20 + return `# fish completion for ${name} -*- shell-script -*- 21 + 22 + # Define shell completion directives 23 + set -l ShellCompDirectiveError ${ShellCompDirectiveError} 24 + set -l ShellCompDirectiveNoSpace ${ShellCompDirectiveNoSpace} 25 + set -l ShellCompDirectiveNoFileComp ${ShellCompDirectiveNoFileComp} 26 + set -l ShellCompDirectiveFilterFileExt ${ShellCompDirectiveFilterFileExt} 27 + set -l ShellCompDirectiveFilterDirs ${ShellCompDirectiveFilterDirs} 28 + set -l ShellCompDirectiveKeepOrder ${ShellCompDirectiveKeepOrder} 29 + 30 + function __${nameForVar}_debug 31 + set -l file "$BASH_COMP_DEBUG_FILE" 32 + if test -n "$file" 33 + echo "$argv" >> $file 34 + end 35 + end 36 + 37 + function __${nameForVar}_perform_completion 38 + __${nameForVar}_debug "Starting __${nameForVar}_perform_completion" 39 + 40 + # Extract all args except the completion flag 41 + set -l args (string match -v -- "--completion=" (commandline -opc)) 42 + 43 + # Extract the current token being completed 44 + set -l current_token (commandline -ct) 45 + 46 + # Check if current token starts with a dash 47 + set -l flag_prefix "" 48 + if string match -q -- "-*" $current_token 49 + set flag_prefix "--flag=" 50 + end 51 + 52 + __${nameForVar}_debug "Current token: $current_token" 53 + __${nameForVar}_debug "All args: $args" 54 + 55 + # Call the completion program and get the results 56 + set -l requestComp "${exec} complete -- $args" 57 + __${nameForVar}_debug "Calling $requestComp" 58 + set -l results (eval $requestComp 2> /dev/null) 59 + 60 + # Some programs may output extra empty lines after the directive. 61 + # Let's ignore them or else it will break completion. 62 + # Ref: https://github.com/spf13/cobra/issues/1279 63 + for line in $results[-1..1] 64 + if test (string sub -s 1 -l 1 -- $line) = ":" 65 + # The directive 66 + set -l directive (string sub -s 2 -- $line) 67 + set -l directive_num (math $directive) 68 + break 69 + end 70 + end 71 + 72 + # No directive specified, use default 73 + if not set -q directive_num 74 + set directive_num 0 75 + end 76 + 77 + __${nameForVar}_debug "Directive: $directive_num" 78 + 79 + # Process completions based on directive 80 + if test $directive_num -eq $ShellCompDirectiveError 81 + # Error code. No completion. 82 + __${nameForVar}_debug "Received error directive: aborting." 83 + return 1 84 + end 85 + 86 + # Filter out the directive (last line) 87 + if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":" 88 + set results $results[1..-2] 89 + end 90 + 91 + # No completions, let fish handle file completions unless forbidden 92 + if test (count $results) -eq 0 93 + if test $directive_num -ne $ShellCompDirectiveNoFileComp 94 + __${nameForVar}_debug "No completions, performing file completion" 95 + return 1 96 + end 97 + __${nameForVar}_debug "No completions, but file completion forbidden" 98 + return 0 99 + end 100 + 101 + # Filter file extensions 102 + if test $directive_num -eq $ShellCompDirectiveFilterFileExt 103 + __${nameForVar}_debug "File extension filtering" 104 + set -l file_extensions 105 + for item in $results 106 + if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-" 107 + set -a file_extensions "*$item" 108 + end 109 + end 110 + __${nameForVar}_debug "File extensions: $file_extensions" 111 + 112 + # Use the file extensions as completions 113 + set -l completions 114 + for ext in $file_extensions 115 + # Get all files matching the extension 116 + set -a completions (string replace -r '^.*/' '' -- $ext) 117 + end 118 + 119 + for item in $completions 120 + echo -e "$item\t" 121 + end 122 + return 0 123 + end 124 + 125 + # Filter directories 126 + if test $directive_num -eq $ShellCompDirectiveFilterDirs 127 + __${nameForVar}_debug "Directory filtering" 128 + set -l dirs 129 + for item in $results 130 + if test -d "$item" 131 + set -a dirs "$item/" 132 + end 133 + end 134 + 135 + for item in $dirs 136 + echo -e "$item\t" 137 + end 138 + return 0 139 + end 140 + 141 + # Process remaining completions 142 + for item in $results 143 + if test -n "$item" 144 + # Check if the item has a description 145 + if string match -q "*\t*" -- "$item" 146 + set -l completion_parts (string split \t -- "$item") 147 + set -l comp $completion_parts[1] 148 + set -l desc $completion_parts[2] 149 + 150 + # Add the completion and description 151 + echo -e "$comp\t$desc" 152 + else 153 + # Add just the completion 154 + echo -e "$item\t" 155 + end 156 + end 157 + end 158 + 159 + # If directive contains NoSpace, tell fish not to add a space after completion 160 + if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0 161 + return 2 162 + end 163 + 164 + return 0 165 + end 166 + 167 + # Set up the completion for the ${name} command 168 + complete -c ${name} -f -a "(eval __${nameForVar}_perform_completion)" 169 + `; 170 + }
+25 -3
tests/__snapshots__/cli.test.ts.snap
··· 109 109 110 110 exports[`cli completion tests for cac > should complete cli options 1`] = ` 111 111 "dev Start dev server 112 + serve Start the server 112 113 lint Lint project 113 114 :4 114 115 " ··· 121 122 `; 122 123 123 124 exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '-H', expected: '-H' }' 1`] = ` 124 - ":4 125 + "-H Specify hostname 126 + :4 125 127 " 126 128 `; 127 129 128 130 exports[`cli completion tests for citty > cli option completion tests > should complete option for partial input '{ partial: '-p', expected: '-p' }' 1`] = ` 129 - ":4 131 + "-p Specify port 132 + :4 130 133 " 131 134 `; 132 135 ··· 184 187 `; 185 188 186 189 exports[`cli completion tests for citty > short flag handling > should handle short flag value completion 1`] = ` 187 - ":4 190 + "-p Specify port 191 + :4 188 192 " 189 193 `; 190 194 ··· 203 207 204 208 exports[`cli completion tests for citty > should complete cli options 1`] = ` 205 209 "dev Start dev server 210 + build Build project 206 211 lint Lint project 207 212 :4 208 213 " 209 214 `; 215 + 216 + exports[`cli completion tests for commander > cli option value handling > should handle unknown options with no completions 1`] = `":4"`; 217 + 218 + exports[`cli completion tests for commander > short flag handling > should handle global short flags 1`] = ` 219 + "-c specify config file 220 + :4 221 + " 222 + `; 223 + 224 + exports[`cli completion tests for commander > should complete cli options 1`] = ` 225 + "serve Start the server 226 + build Build the project 227 + deploy Deploy the application 228 + lint Lint source files 229 + :4 230 + " 231 + `;
+307
tests/__snapshots__/shell.test.ts.snap
··· 1 + // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 + 3 + exports[`shell completion generators > fish shell completion > should generate a valid fish completion script 1`] = ` 4 + "# fish completion for testcli -*- shell-script -*- 5 + 6 + # Define shell completion directives 7 + set -l ShellCompDirectiveError 1 8 + set -l ShellCompDirectiveNoSpace 2 9 + set -l ShellCompDirectiveNoFileComp 4 10 + set -l ShellCompDirectiveFilterFileExt 8 11 + set -l ShellCompDirectiveFilterDirs 16 12 + set -l ShellCompDirectiveKeepOrder 32 13 + 14 + function __testcli_debug 15 + set -l file "$BASH_COMP_DEBUG_FILE" 16 + if test -n "$file" 17 + echo "$argv" >> $file 18 + end 19 + end 20 + 21 + function __testcli_perform_completion 22 + __testcli_debug "Starting __testcli_perform_completion" 23 + 24 + # Extract all args except the completion flag 25 + set -l args (string match -v -- "--completion=" (commandline -opc)) 26 + 27 + # Extract the current token being completed 28 + set -l current_token (commandline -ct) 29 + 30 + # Check if current token starts with a dash 31 + set -l flag_prefix "" 32 + if string match -q -- "-*" $current_token 33 + set flag_prefix "--flag=" 34 + end 35 + 36 + __testcli_debug "Current token: $current_token" 37 + __testcli_debug "All args: $args" 38 + 39 + # Call the completion program and get the results 40 + set -l requestComp "/usr/bin/node /path/to/testcli complete -- $args" 41 + __testcli_debug "Calling $requestComp" 42 + set -l results (eval $requestComp 2> /dev/null) 43 + 44 + # Some programs may output extra empty lines after the directive. 45 + # Let's ignore them or else it will break completion. 46 + # Ref: https://github.com/spf13/cobra/issues/1279 47 + for line in $results[-1..1] 48 + if test (string sub -s 1 -l 1 -- $line) = ":" 49 + # The directive 50 + set -l directive (string sub -s 2 -- $line) 51 + set -l directive_num (math $directive) 52 + break 53 + end 54 + end 55 + 56 + # No directive specified, use default 57 + if not set -q directive_num 58 + set directive_num 0 59 + end 60 + 61 + __testcli_debug "Directive: $directive_num" 62 + 63 + # Process completions based on directive 64 + if test $directive_num -eq $ShellCompDirectiveError 65 + # Error code. No completion. 66 + __testcli_debug "Received error directive: aborting." 67 + return 1 68 + end 69 + 70 + # Filter out the directive (last line) 71 + if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":" 72 + set results $results[1..-2] 73 + end 74 + 75 + # No completions, let fish handle file completions unless forbidden 76 + if test (count $results) -eq 0 77 + if test $directive_num -ne $ShellCompDirectiveNoFileComp 78 + __testcli_debug "No completions, performing file completion" 79 + return 1 80 + end 81 + __testcli_debug "No completions, but file completion forbidden" 82 + return 0 83 + end 84 + 85 + # Filter file extensions 86 + if test $directive_num -eq $ShellCompDirectiveFilterFileExt 87 + __testcli_debug "File extension filtering" 88 + set -l file_extensions 89 + for item in $results 90 + if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-" 91 + set -a file_extensions "*$item" 92 + end 93 + end 94 + __testcli_debug "File extensions: $file_extensions" 95 + 96 + # Use the file extensions as completions 97 + set -l completions 98 + for ext in $file_extensions 99 + # Get all files matching the extension 100 + set -a completions (string replace -r '^.*/' '' -- $ext) 101 + end 102 + 103 + for item in $completions 104 + echo -e "$item " 105 + end 106 + return 0 107 + end 108 + 109 + # Filter directories 110 + if test $directive_num -eq $ShellCompDirectiveFilterDirs 111 + __testcli_debug "Directory filtering" 112 + set -l dirs 113 + for item in $results 114 + if test -d "$item" 115 + set -a dirs "$item/" 116 + end 117 + end 118 + 119 + for item in $dirs 120 + echo -e "$item " 121 + end 122 + return 0 123 + end 124 + 125 + # Process remaining completions 126 + for item in $results 127 + if test -n "$item" 128 + # Check if the item has a description 129 + if string match -q "* *" -- "$item" 130 + set -l completion_parts (string split -- "$item") 131 + set -l comp $completion_parts[1] 132 + set -l desc $completion_parts[2] 133 + 134 + # Add the completion and description 135 + echo -e "$comp $desc" 136 + else 137 + # Add just the completion 138 + echo -e "$item " 139 + end 140 + end 141 + end 142 + 143 + # If directive contains NoSpace, tell fish not to add a space after completion 144 + if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0 145 + return 2 146 + end 147 + 148 + return 0 149 + end 150 + 151 + # Set up the completion for the testcli command 152 + complete -c testcli -f -a "(eval __testcli_perform_completion)" 153 + " 154 + `; 155 + 156 + exports[`shell completion generators > fish shell completion > should handle special characters in the name 1`] = ` 157 + "# fish completion for test-cli:app -*- shell-script -*- 158 + 159 + # Define shell completion directives 160 + set -l ShellCompDirectiveError 1 161 + set -l ShellCompDirectiveNoSpace 2 162 + set -l ShellCompDirectiveNoFileComp 4 163 + set -l ShellCompDirectiveFilterFileExt 8 164 + set -l ShellCompDirectiveFilterDirs 16 165 + set -l ShellCompDirectiveKeepOrder 32 166 + 167 + function __test_cli_app_debug 168 + set -l file "$BASH_COMP_DEBUG_FILE" 169 + if test -n "$file" 170 + echo "$argv" >> $file 171 + end 172 + end 173 + 174 + function __test_cli_app_perform_completion 175 + __test_cli_app_debug "Starting __test_cli_app_perform_completion" 176 + 177 + # Extract all args except the completion flag 178 + set -l args (string match -v -- "--completion=" (commandline -opc)) 179 + 180 + # Extract the current token being completed 181 + set -l current_token (commandline -ct) 182 + 183 + # Check if current token starts with a dash 184 + set -l flag_prefix "" 185 + if string match -q -- "-*" $current_token 186 + set flag_prefix "--flag=" 187 + end 188 + 189 + __test_cli_app_debug "Current token: $current_token" 190 + __test_cli_app_debug "All args: $args" 191 + 192 + # Call the completion program and get the results 193 + set -l requestComp "/usr/bin/node /path/to/testcli complete -- $args" 194 + __test_cli_app_debug "Calling $requestComp" 195 + set -l results (eval $requestComp 2> /dev/null) 196 + 197 + # Some programs may output extra empty lines after the directive. 198 + # Let's ignore them or else it will break completion. 199 + # Ref: https://github.com/spf13/cobra/issues/1279 200 + for line in $results[-1..1] 201 + if test (string sub -s 1 -l 1 -- $line) = ":" 202 + # The directive 203 + set -l directive (string sub -s 2 -- $line) 204 + set -l directive_num (math $directive) 205 + break 206 + end 207 + end 208 + 209 + # No directive specified, use default 210 + if not set -q directive_num 211 + set directive_num 0 212 + end 213 + 214 + __test_cli_app_debug "Directive: $directive_num" 215 + 216 + # Process completions based on directive 217 + if test $directive_num -eq $ShellCompDirectiveError 218 + # Error code. No completion. 219 + __test_cli_app_debug "Received error directive: aborting." 220 + return 1 221 + end 222 + 223 + # Filter out the directive (last line) 224 + if test (count $results) -gt 0 -a (string sub -s 1 -l 1 -- $results[-1]) = ":" 225 + set results $results[1..-2] 226 + end 227 + 228 + # No completions, let fish handle file completions unless forbidden 229 + if test (count $results) -eq 0 230 + if test $directive_num -ne $ShellCompDirectiveNoFileComp 231 + __test_cli_app_debug "No completions, performing file completion" 232 + return 1 233 + end 234 + __test_cli_app_debug "No completions, but file completion forbidden" 235 + return 0 236 + end 237 + 238 + # Filter file extensions 239 + if test $directive_num -eq $ShellCompDirectiveFilterFileExt 240 + __test_cli_app_debug "File extension filtering" 241 + set -l file_extensions 242 + for item in $results 243 + if test -n "$item" -a (string sub -s 1 -l 1 -- $item) != "-" 244 + set -a file_extensions "*$item" 245 + end 246 + end 247 + __test_cli_app_debug "File extensions: $file_extensions" 248 + 249 + # Use the file extensions as completions 250 + set -l completions 251 + for ext in $file_extensions 252 + # Get all files matching the extension 253 + set -a completions (string replace -r '^.*/' '' -- $ext) 254 + end 255 + 256 + for item in $completions 257 + echo -e "$item " 258 + end 259 + return 0 260 + end 261 + 262 + # Filter directories 263 + if test $directive_num -eq $ShellCompDirectiveFilterDirs 264 + __test_cli_app_debug "Directory filtering" 265 + set -l dirs 266 + for item in $results 267 + if test -d "$item" 268 + set -a dirs "$item/" 269 + end 270 + end 271 + 272 + for item in $dirs 273 + echo -e "$item " 274 + end 275 + return 0 276 + end 277 + 278 + # Process remaining completions 279 + for item in $results 280 + if test -n "$item" 281 + # Check if the item has a description 282 + if string match -q "* *" -- "$item" 283 + set -l completion_parts (string split -- "$item") 284 + set -l comp $completion_parts[1] 285 + set -l desc $completion_parts[2] 286 + 287 + # Add the completion and description 288 + echo -e "$comp $desc" 289 + else 290 + # Add just the completion 291 + echo -e "$item " 292 + end 293 + end 294 + end 295 + 296 + # If directive contains NoSpace, tell fish not to add a space after completion 297 + if test (math "$directive_num & $ShellCompDirectiveNoSpace") -ne 0 298 + return 2 299 + end 300 + 301 + return 0 302 + end 303 + 304 + # Set up the completion for the test-cli:app command 305 + complete -c test-cli:app -f -a "(eval __test_cli_app_perform_completion)" 306 + " 307 + `;
+90 -66
tests/cli.test.ts
··· 13 13 }); 14 14 } 15 15 16 - const cliTools = ['citty', 'cac']; 17 - // const cliTools = ['citty', 'cac']; 16 + const cliTools = ['citty', 'cac', 'commander']; 18 17 19 18 describe.each(cliTools)('cli completion tests for %s', (cliTool) => { 20 - const commandPrefix = `pnpm tsx demo.${cliTool}.ts complete --`; 19 + // For Commander, we need to skip most of the tests since it handles completion differently 20 + const shouldSkipTest = cliTool === 'commander'; 21 21 22 - it('should complete cli options', async () => { 22 + // Commander uses a different command structure for completion 23 + const commandPrefix = 24 + cliTool === 'commander' 25 + ? `pnpm tsx examples/demo.${cliTool}.ts complete` 26 + : `pnpm tsx examples/demo.${cliTool}.ts complete --`; 27 + 28 + // Use 'dev' for citty and 'serve' for other tools 29 + const commandName = cliTool === 'citty' ? 'dev' : 'serve'; 30 + 31 + it.runIf(!shouldSkipTest)('should complete cli options', async () => { 23 32 const output = await runCommand(`${commandPrefix}`); 24 33 expect(output).toMatchSnapshot(); 25 34 }); 26 35 27 - describe('cli option completion tests', () => { 36 + describe.runIf(!shouldSkipTest)('cli option completion tests', () => { 28 37 const optionTests = [ 29 38 { partial: '--p', expected: '--port' }, 30 39 { partial: '-p', expected: '-p' }, // Test short flag completion ··· 34 43 test.each(optionTests)( 35 44 "should complete option for partial input '%s'", 36 45 async ({ partial }) => { 37 - const command = `${commandPrefix} dev ${partial}`; 46 + const command = `${commandPrefix} ${commandName} ${partial}`; 38 47 const output = await runCommand(command); 39 48 expect(output).toMatchSnapshot(); 40 49 } 41 50 ); 42 51 }); 43 52 44 - describe('cli option exclusion tests', () => { 53 + describe.runIf(!shouldSkipTest)('cli option exclusion tests', () => { 45 54 const alreadySpecifiedTests = [ 46 55 { specified: '--config', shouldNotContain: '--config' }, 47 56 ]; 48 57 49 58 test.each(alreadySpecifiedTests)( 50 59 "should not suggest already specified option '%s'", 51 - async ({ specified }) => { 60 + async ({ specified, shouldNotContain }) => { 52 61 const command = `${commandPrefix} ${specified} --`; 53 62 const output = await runCommand(command); 54 - console.log(output); 55 63 expect(output).toMatchSnapshot(); 56 64 } 57 65 ); 58 66 }); 59 67 60 - describe('cli option value handling', () => { 68 + describe.runIf(!shouldSkipTest)('cli option value handling', () => { 61 69 it('should resolve port value correctly', async () => { 62 - const command = `${commandPrefix} dev --port=3`; 70 + const command = `${commandPrefix} ${commandName} --port=3`; 63 71 const output = await runCommand(command); 64 72 expect(output).toMatchSnapshot(); 65 73 }); ··· 83 91 }); 84 92 }); 85 93 86 - describe('edge case completions for end with space', () => { 87 - //TOOD: remove this 88 - it('should suggest port values if user ends with space after `--port`', async () => { 89 - const command = `${commandPrefix} dev --port ""`; 90 - const output = await runCommand(command); 91 - expect(output).toMatchSnapshot(); 92 - }); 94 + describe.runIf(!shouldSkipTest)( 95 + 'edge case completions for end with space', 96 + () => { 97 + it('should suggest port values if user ends with space after `--port`', async () => { 98 + const command = `${commandPrefix} ${commandName} --port ""`; 99 + const output = await runCommand(command); 100 + expect(output).toMatchSnapshot(); 101 + }); 93 102 94 - it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { 95 - const command = `${commandPrefix} dev --po`; 96 - const output = await runCommand(command); 97 - expect(output).toMatchSnapshot(); 98 - }); 103 + it("should keep suggesting the --port option if user typed partial but didn't end with space", async () => { 104 + const command = `${commandPrefix} ${commandName} --po`; 105 + const output = await runCommand(command); 106 + expect(output).toMatchSnapshot(); 107 + }); 99 108 100 - it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { 101 - const command = `${commandPrefix} dev --port=`; 102 - const output = await runCommand(command); 103 - expect(output).toMatchSnapshot(); 104 - }); 105 - }); 109 + it("should suggest port values if user typed `--port=` and hasn't typed a space or value yet", async () => { 110 + const command = `${commandPrefix} ${commandName} --port=`; 111 + const output = await runCommand(command); 112 + expect(output).toMatchSnapshot(); 113 + }); 114 + } 115 + ); 106 116 107 - describe('short flag handling', () => { 117 + describe.runIf(!shouldSkipTest)('short flag handling', () => { 108 118 it('should handle short flag value completion', async () => { 109 - const command = `${commandPrefix} dev -p `; 119 + const command = `${commandPrefix} ${commandName} -p `; 110 120 const output = await runCommand(command); 111 121 expect(output).toMatchSnapshot(); 112 122 }); 113 123 114 124 it('should handle short flag with equals sign', async () => { 115 - const command = `${commandPrefix} dev -p=3`; 125 + const command = `${commandPrefix} ${commandName} -p=3`; 116 126 const output = await runCommand(command); 117 127 expect(output).toMatchSnapshot(); 118 128 }); ··· 130 140 }); 131 141 }); 132 142 133 - // single positional command: `lint [file]` 134 - // vite "" 135 - // -> src/ 136 - // -> ./ 137 - 138 - // vite src/ "" 139 - // -> nothing 140 - // should not suggest anything 141 - 142 - // multiple postiionals command `lint [...files]` 143 - // vite "" 144 - // -> src/ 145 - // -> ./ 146 - 147 - // vite src/ "" 148 - // -> src/ 149 - // -> ./ 150 - 151 - describe('positional argument completions', () => { 152 - it.runIf(cliTool !== 'citty')( 153 - 'should complete multiple positional arguments when ending with space', 154 - async () => { 143 + describe.runIf(!shouldSkipTest && cliTool !== 'citty')( 144 + 'positional argument completions', 145 + () => { 146 + it('should complete multiple positional arguments when ending with space', async () => { 155 147 const command = `${commandPrefix} lint ""`; 156 148 const output = await runCommand(command); 157 149 expect(output).toMatchSnapshot(); 158 - } 159 - ); 150 + }); 160 151 161 - it.runIf(cliTool !== 'citty')( 162 - 'should complete multiple positional arguments when ending with part of the value', 163 - async () => { 152 + it('should complete multiple positional arguments when ending with part of the value', async () => { 164 153 const command = `${commandPrefix} lint ind`; 165 154 const output = await runCommand(command); 166 155 expect(output).toMatchSnapshot(); 167 - } 168 - ); 156 + }); 169 157 170 - it.runIf(cliTool !== 'citty')( 171 - 'should complete single positional argument when ending with space', 172 - async () => { 158 + it('should complete single positional argument when ending with space', async () => { 173 159 const command = `${commandPrefix} lint main.ts ""`; 174 160 const output = await runCommand(command); 175 161 expect(output).toMatchSnapshot(); 176 - } 177 - ); 162 + }); 163 + } 164 + ); 165 + }); 166 + 167 + // Add specific tests for Commander 168 + describe('commander specific tests', () => { 169 + it('should complete commands', async () => { 170 + const command = `pnpm tsx examples/demo.commander.ts complete -- `; 171 + const output = await runCommand(command); 172 + expect(output).toContain('serve'); 173 + expect(output).toContain('build'); 174 + expect(output).toContain('deploy'); 175 + }); 176 + 177 + it('should handle subcommands', async () => { 178 + // First, we need to check if deploy is recognized as a command 179 + const command1 = `pnpm tsx examples/demo.commander.ts complete -- deploy`; 180 + const output1 = await runCommand(command1); 181 + expect(output1).toContain('deploy'); 182 + expect(output1).toContain('Deploy the application'); 183 + 184 + // Then we need to check if the deploy command has subcommands 185 + // We can check this by running the deploy command with --help 186 + const command2 = `pnpm tsx examples/demo.commander.ts deploy --help`; 187 + const output2 = await runCommand(command2); 188 + expect(output2).toContain('staging'); 189 + expect(output2).toContain('production'); 190 + }); 191 + }); 192 + 193 + describe('shell completion script generation', () => { 194 + const shells = ['zsh', 'bash', 'fish', 'powershell']; 195 + const cliTool = 'commander'; // Use commander for shell script generation tests 196 + 197 + test.each(shells)('should generate %s completion script', async (shell) => { 198 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete ${shell}`; 199 + const output = await runCommand(command); 200 + expect(output).toContain(`# ${shell} completion for`); 201 + expect(output.length).toBeGreaterThan(100); // Ensure we got a substantial script 178 202 }); 179 203 });
+198
tests/shell.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import * as fish from '../src/fish'; 3 + import * as bash from '../src/bash'; 4 + import * as zsh from '../src/zsh'; 5 + import * as powershell from '../src/powershell'; 6 + import { ShellCompDirective } from '../src'; 7 + 8 + describe('shell completion generators', () => { 9 + const name = 'testcli'; 10 + const exec = '/usr/bin/node /path/to/testcli'; 11 + const specialName = 'test-cli:app'; 12 + const escapedName = specialName.replace(/[-:]/g, '_'); 13 + 14 + describe('fish shell completion', () => { 15 + it('should generate a valid fish completion script', () => { 16 + const script = fish.generate(name, exec); 17 + 18 + // Use snapshot testing instead of individual assertions 19 + expect(script).toMatchSnapshot(); 20 + }); 21 + 22 + it('should handle special characters in the name', () => { 23 + const script = fish.generate(specialName, exec); 24 + 25 + // Use snapshot testing instead of individual assertions 26 + expect(script).toMatchSnapshot(); 27 + }); 28 + }); 29 + 30 + describe('bash shell completion', () => { 31 + it('should generate a valid bash completion script', () => { 32 + const script = bash.generate(name, exec); 33 + 34 + // Check that the script contains the shell name 35 + expect(script).toContain(`# bash completion for ${name}`); 36 + 37 + // Check that the script defines the directives 38 + expect(script).toContain( 39 + `readonly ShellCompDirectiveError=${ShellCompDirective.ShellCompDirectiveError}` 40 + ); 41 + expect(script).toContain( 42 + `readonly ShellCompDirectiveNoSpace=${ShellCompDirective.ShellCompDirectiveNoSpace}` 43 + ); 44 + expect(script).toContain( 45 + `readonly ShellCompDirectiveNoFileComp=${ShellCompDirective.ShellCompDirectiveNoFileComp}` 46 + ); 47 + expect(script).toContain( 48 + `readonly ShellCompDirectiveFilterFileExt=${ShellCompDirective.ShellCompDirectiveFilterFileExt}` 49 + ); 50 + expect(script).toContain( 51 + `readonly ShellCompDirectiveFilterDirs=${ShellCompDirective.ShellCompDirectiveFilterDirs}` 52 + ); 53 + expect(script).toContain( 54 + `readonly ShellCompDirectiveKeepOrder=${ShellCompDirective.ShellCompDirectiveKeepOrder}` 55 + ); 56 + 57 + // Check that the script contains the debug function 58 + expect(script).toContain(`__${name}_debug()`); 59 + 60 + // Check that the script contains the completion function 61 + expect(script).toContain(`__${name}_complete()`); 62 + 63 + // Check that the script contains the completion registration 64 + expect(script).toContain(`complete -F __${name}_complete ${name}`); 65 + 66 + // Check that the script uses the provided exec path 67 + expect(script).toContain(`requestComp="${exec} complete --`); 68 + 69 + // Check that the script handles directives correctly 70 + expect(script).toContain( 71 + `if [[ $((directive & $ShellCompDirectiveError)) -ne 0 ]]` 72 + ); 73 + expect(script).toContain( 74 + `if [[ $((directive & $ShellCompDirectiveNoSpace)) -ne 0 ]]` 75 + ); 76 + expect(script).toContain( 77 + `if [[ $((directive & $ShellCompDirectiveKeepOrder)) -ne 0 ]]` 78 + ); 79 + expect(script).toContain( 80 + `if [[ $((directive & $ShellCompDirectiveNoFileComp)) -ne 0 ]]` 81 + ); 82 + expect(script).toContain( 83 + `if [[ $((directive & $ShellCompDirectiveFilterFileExt)) -ne 0 ]]` 84 + ); 85 + expect(script).toContain( 86 + `if [[ $((directive & $ShellCompDirectiveFilterDirs)) -ne 0 ]]` 87 + ); 88 + }); 89 + 90 + it('should handle special characters in the name', () => { 91 + const script = bash.generate(specialName, exec); 92 + 93 + // Check that the script properly escapes the name 94 + expect(script).toContain(`__${escapedName}_debug()`); 95 + expect(script).toContain(`__${escapedName}_complete()`); 96 + expect(script).toContain( 97 + `complete -F __${escapedName}_complete ${specialName}` 98 + ); 99 + }); 100 + }); 101 + 102 + describe('zsh shell completion', () => { 103 + it('should generate a valid zsh completion script', () => { 104 + const script = zsh.generate(name, exec); 105 + 106 + // Check that the script contains the shell name 107 + expect(script).toContain(`#compdef ${name}`); 108 + expect(script).toContain(`compdef _${name} ${name}`); 109 + 110 + // Check that the script contains the debug function 111 + expect(script).toContain(`__${name}_debug()`); 112 + 113 + // Check that the script contains the completion function 114 + expect(script).toContain(`_${name}()`); 115 + 116 + // Check that the script uses the provided exec path 117 + expect(script).toContain(`requestComp="${exec} complete --`); 118 + 119 + // Check that the script handles directives 120 + expect(script).toContain( 121 + `shellCompDirectiveError=${ShellCompDirective.ShellCompDirectiveError}` 122 + ); 123 + expect(script).toContain( 124 + `shellCompDirectiveNoSpace=${ShellCompDirective.ShellCompDirectiveNoSpace}` 125 + ); 126 + expect(script).toContain( 127 + `shellCompDirectiveNoFileComp=${ShellCompDirective.ShellCompDirectiveNoFileComp}` 128 + ); 129 + expect(script).toContain( 130 + `shellCompDirectiveFilterFileExt=${ShellCompDirective.ShellCompDirectiveFilterFileExt}` 131 + ); 132 + expect(script).toContain( 133 + `shellCompDirectiveFilterDirs=${ShellCompDirective.ShellCompDirectiveFilterDirs}` 134 + ); 135 + expect(script).toContain( 136 + `shellCompDirectiveKeepOrder=${ShellCompDirective.ShellCompDirectiveKeepOrder}` 137 + ); 138 + }); 139 + 140 + it('should handle special characters in the name', () => { 141 + const script = zsh.generate(specialName, exec); 142 + 143 + // Check that the script properly escapes the name 144 + expect(script).toContain(`#compdef ${specialName}`); 145 + // In zsh, special characters are not escaped in the function name 146 + expect(script).toContain(`__${specialName}_debug()`); 147 + expect(script).toContain(`_${specialName}()`); 148 + }); 149 + }); 150 + 151 + describe('powershell completion', () => { 152 + it('should generate a valid powershell completion script', () => { 153 + const script = powershell.generate(name, exec); 154 + 155 + // Check that the script contains the shell name 156 + expect(script).toContain(`# powershell completion for ${name}`); 157 + 158 + // Check that the script contains the debug function 159 + expect(script).toContain(`function __${name}_debug`); 160 + 161 + // Check that the script contains the completion block 162 + expect(script).toContain(`[scriptblock]$__${name}CompleterBlock =`); 163 + 164 + // Check that the script uses the provided exec path 165 + expect(script).toContain(`$RequestComp = "& ${exec} complete --`); 166 + 167 + // Check that the script handles directives 168 + expect(script).toContain( 169 + `$ShellCompDirectiveError=${ShellCompDirective.ShellCompDirectiveError}` 170 + ); 171 + expect(script).toContain( 172 + `$ShellCompDirectiveNoSpace=${ShellCompDirective.ShellCompDirectiveNoSpace}` 173 + ); 174 + expect(script).toContain( 175 + `$ShellCompDirectiveNoFileComp=${ShellCompDirective.ShellCompDirectiveNoFileComp}` 176 + ); 177 + expect(script).toContain( 178 + `$ShellCompDirectiveFilterFileExt=${ShellCompDirective.ShellCompDirectiveFilterFileExt}` 179 + ); 180 + expect(script).toContain( 181 + `$ShellCompDirectiveFilterDirs=${ShellCompDirective.ShellCompDirectiveFilterDirs}` 182 + ); 183 + expect(script).toContain( 184 + `$ShellCompDirectiveKeepOrder=${ShellCompDirective.ShellCompDirectiveKeepOrder}` 185 + ); 186 + }); 187 + 188 + it('should handle special characters in the name', () => { 189 + const script = powershell.generate(specialName, exec); 190 + 191 + // Check that the script properly escapes the name 192 + // In PowerShell, special characters are not escaped in the function name 193 + expect(script).toContain(`function __${specialName}_debug`); 194 + // The CompleterBlock name uses underscores instead of colons 195 + expect(script).toContain(`$__test_cli_appCompleterBlock`); 196 + }); 197 + }); 198 + });