[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.

update

AmirSa12 (Aug 17, 2025, 12:40 PM +0330) dae6ae7e 11d2b2b2

+509 -1
+8 -1
src/bash.ts
··· 42 42 local requestComp out directive 43 43 44 44 # Build the command to get completions 45 - requestComp="${exec} complete -- \${words[@]:1}" 45 + # Use a more compatible approach instead of \${words[@]:1} 46 + local args="" 47 + for (( i=1; i<cword+1; i++ )); do 48 + if [[ -n "\${words[i]}" ]]; then 49 + args="$args \\"\${words[i]}\\"" 50 + fi 51 + done 52 + requestComp="${exec} complete --$args" 46 53 47 54 # Add an empty parameter if the last parameter is complete 48 55 if [[ -z "$cur" ]]; then
+255
tests/package-manager-integration.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { promisify } from 'node:util'; 3 + import { exec as execCb } from 'node:child_process'; 4 + import { writeFile, unlink } from 'node:fs/promises'; 5 + import { join } from 'node:path'; 6 + 7 + const exec = promisify(execCb); 8 + 9 + describe('package manager integration tests', () => { 10 + const packageManagers = ['npm', 'pnpm', 'yarn', 'bun']; 11 + const shells = ['zsh', 'bash', 'fish', 'powershell']; 12 + 13 + describe('package manager completion script generation', () => { 14 + packageManagers.forEach((pm) => { 15 + shells.forEach((shell) => { 16 + it(`should generate ${shell} completion script for ${pm}`, async () => { 17 + const command = `pnpm tsx bin/cli.ts ${pm} ${shell}`; 18 + 19 + try { 20 + const { stdout, stderr } = await exec(command); 21 + 22 + // Should have output 23 + expect(stdout).toBeTruthy(); 24 + expect(stdout.length).toBeGreaterThan(100); 25 + 26 + // Should contain shell-specific markers 27 + expect(stdout).toContain(`# ${shell} completion for`); 28 + 29 + // Should contain package manager name 30 + expect(stdout).toContain(pm); 31 + 32 + // Should not have errors 33 + expect(stderr).toBe(''); 34 + } catch (error) { 35 + throw new Error( 36 + `Failed to generate ${shell} script for ${pm}: ${error}` 37 + ); 38 + } 39 + }); 40 + }); 41 + }); 42 + }); 43 + 44 + // Test package manager completion functionality 45 + describe('package manager completion functionality', () => { 46 + packageManagers.forEach((pm) => { 47 + describe(`${pm} completion`, () => { 48 + it('should handle basic command completion', async () => { 49 + const command = `pnpm tsx bin/cli.ts ${pm} complete -- install`; 50 + 51 + try { 52 + const { stdout, stderr } = await exec(command); 53 + 54 + // Should have some output (completions or directive) 55 + expect(stdout).toBeTruthy(); 56 + 57 + // Should end with completion directive 58 + expect(stdout.trim()).toMatch(/:\d+$/); 59 + 60 + // Should not have errors 61 + expect(stderr).toBe(''); 62 + } catch (error) { 63 + throw new Error(`Failed ${pm} completion test: ${error}`); 64 + } 65 + }); 66 + 67 + it('should handle script completion', async () => { 68 + const command = `pnpm tsx bin/cli.ts ${pm} complete -- run`; 69 + 70 + try { 71 + const { stdout, stderr } = await exec(command); 72 + 73 + // Should have output 74 + expect(stdout).toBeTruthy(); 75 + expect(stdout.trim()).toMatch(/:\d+$/); 76 + expect(stderr).toBe(''); 77 + } catch (error) { 78 + throw new Error(`Failed ${pm} script completion: ${error}`); 79 + } 80 + }); 81 + 82 + it('should handle no match scenarios', async () => { 83 + const command = `pnpm tsx bin/cli.ts ${pm} complete -- nonexistentcommand`; 84 + 85 + try { 86 + const { stdout, stderr } = await exec(command); 87 + 88 + // Should have output (even if no matches) 89 + expect(stdout).toBeTruthy(); 90 + expect(stdout.trim()).toMatch(/:\d+$/); 91 + expect(stderr).toBe(''); 92 + } catch (error) { 93 + throw new Error(`Failed ${pm} no-match test: ${error}`); 94 + } 95 + }); 96 + }); 97 + }); 98 + }); 99 + 100 + // Test bash-specific package manager issues 101 + describe('bash package manager completion validation', () => { 102 + packageManagers.forEach((pm) => { 103 + it(`should generate syntactically valid bash completion for ${pm}`, async () => { 104 + const command = `pnpm tsx bin/cli.ts ${pm} bash`; 105 + const { stdout } = await exec(command); 106 + 107 + // Write script to temp file 108 + const scriptPath = join(process.cwd(), `temp-${pm}-bash-completion.sh`); 109 + await writeFile(scriptPath, stdout); 110 + 111 + try { 112 + // Test bash syntax with -n flag (syntax check only) 113 + await exec(`bash -n ${scriptPath}`); 114 + // If we get here, syntax is valid 115 + expect(true).toBe(true); 116 + } catch (error) { 117 + throw new Error(`${pm} bash script has syntax errors: ${error}`); 118 + } finally { 119 + // Clean up 120 + await unlink(scriptPath).catch(() => {}); 121 + } 122 + }); 123 + 124 + it(`should generate bash completion for ${pm} without problematic syntax`, async () => { 125 + const command = `pnpm tsx bin/cli.ts ${pm} bash`; 126 + const { stdout } = await exec(command); 127 + 128 + // Check for the actual problematic bash syntax in the requestComp assignment 129 + expect(stdout).not.toMatch(/requestComp="[^"]*\$\{words\[@\]:1\}/); // Should not use ${words[@]:1} in requestComp 130 + expect(stdout).toContain('complete -F'); // Should register completion properly 131 + 132 + // Should contain package manager specific function 133 + expect(stdout).toMatch( 134 + new RegExp(`__${pm.replace('-', '_')}_complete\\(\\)`) 135 + ); 136 + }); 137 + }); 138 + }); 139 + 140 + describe('package manager completion registration', () => { 141 + shells.forEach((shell) => { 142 + it(`should generate ${shell} completion with proper registration for all package managers`, async () => { 143 + for (const pm of packageManagers) { 144 + const command = `pnpm tsx bin/cli.ts ${pm} ${shell}`; 145 + const { stdout } = await exec(command); 146 + 147 + switch (shell) { 148 + case 'bash': 149 + expect(stdout).toMatch(/complete -F __\w+_complete/); 150 + expect(stdout).toContain(pm); // Should register for the specific package manager 151 + break; 152 + case 'zsh': 153 + expect(stdout).toMatch(/#compdef \w+/); 154 + expect(stdout).toMatch(/compdef _\w+ \w+/); 155 + break; 156 + case 'fish': 157 + expect(stdout).toContain('complete -c'); 158 + expect(stdout).toContain(pm); 159 + break; 160 + case 'powershell': 161 + expect(stdout).toContain('Register-ArgumentCompleter'); 162 + expect(stdout).toContain(pm); 163 + break; 164 + } 165 + } 166 + }); 167 + }); 168 + }); 169 + 170 + // Test error handling 171 + describe('package manager error handling', () => { 172 + it('should handle unsupported package manager', async () => { 173 + const command = `pnpm tsx bin/cli.ts unsupported complete -- test`; 174 + 175 + try { 176 + await exec(command); 177 + // Should not reach here 178 + expect(true).toBe(false); 179 + } catch (error) { 180 + const errorMessage = 181 + error instanceof Error ? error.message : String(error); 182 + expect(errorMessage).toContain('Unsupported package manager'); 183 + } 184 + }); 185 + 186 + it('should handle missing -- separator', async () => { 187 + const command = `pnpm tsx bin/cli.ts pnpm complete test`; 188 + 189 + try { 190 + await exec(command); 191 + // Should not reach here 192 + expect(true).toBe(false); 193 + } catch (error) { 194 + const errorMessage = 195 + error instanceof Error ? error.message : String(error); 196 + expect(errorMessage).toContain("Expected '--' followed by command"); 197 + } 198 + }); 199 + }); 200 + 201 + // Test specific package manager features 202 + describe('package manager specific features', () => { 203 + it('should handle pnpm-specific commands', async () => { 204 + const command = `pnpm tsx bin/cli.ts pnpm complete -- dlx`; 205 + 206 + try { 207 + const { stdout, stderr } = await exec(command); 208 + expect(stdout).toBeTruthy(); 209 + expect(stdout.trim()).toMatch(/:\d+$/); 210 + expect(stderr).toBe(''); 211 + } catch (error) { 212 + throw new Error(`Failed pnpm dlx completion: ${error}`); 213 + } 214 + }); 215 + 216 + it('should handle yarn-specific commands', async () => { 217 + const command = `pnpm tsx bin/cli.ts yarn complete -- workspace`; 218 + 219 + try { 220 + const { stdout, stderr } = await exec(command); 221 + expect(stdout).toBeTruthy(); 222 + expect(stdout.trim()).toMatch(/:\d+$/); 223 + expect(stderr).toBe(''); 224 + } catch (error) { 225 + throw new Error(`Failed yarn workspace completion: ${error}`); 226 + } 227 + }); 228 + 229 + it('should handle npm-specific commands', async () => { 230 + const command = `pnpm tsx bin/cli.ts npm complete -- audit`; 231 + 232 + try { 233 + const { stdout, stderr } = await exec(command); 234 + expect(stdout).toBeTruthy(); 235 + expect(stdout.trim()).toMatch(/:\d+$/); 236 + expect(stderr).toBe(''); 237 + } catch (error) { 238 + throw new Error(`Failed npm audit completion: ${error}`); 239 + } 240 + }); 241 + 242 + it('should handle bun-specific commands', async () => { 243 + const command = `pnpm tsx bin/cli.ts bun complete -- create`; 244 + 245 + try { 246 + const { stdout, stderr } = await exec(command); 247 + expect(stdout).toBeTruthy(); 248 + expect(stdout.trim()).toMatch(/:\d+$/); 249 + expect(stderr).toBe(''); 250 + } catch (error) { 251 + throw new Error(`Failed bun create completion: ${error}`); 252 + } 253 + }); 254 + }); 255 + });
+246
tests/shell-integration.test.ts
··· 1 + import { describe, it, expect } from 'vitest'; 2 + import { promisify } from 'node:util'; 3 + import { exec as execCb } from 'node:child_process'; 4 + import { writeFile, unlink } from 'node:fs/promises'; 5 + import { join } from 'node:path'; 6 + 7 + const exec = promisify(execCb); 8 + 9 + describe('shell integration tests', () => { 10 + const shells = ['zsh', 'bash', 'fish', 'powershell']; 11 + const cliTool = 'cac'; 12 + 13 + describe('shell script generation', () => { 14 + shells.forEach((shell) => { 15 + it(`should generate ${shell} completion script without errors`, async () => { 16 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete ${shell}`; 17 + 18 + try { 19 + const { stdout, stderr } = await exec(command); 20 + 21 + // Should have output 22 + expect(stdout).toBeTruthy(); 23 + expect(stdout.length).toBeGreaterThan(100); 24 + 25 + // Should contain shell-specific markers 26 + expect(stdout).toContain(`# ${shell} completion for`); 27 + 28 + // Should not have errors 29 + expect(stderr).toBe(''); 30 + } catch (error) { 31 + throw new Error(`Failed to generate ${shell} script: ${error}`); 32 + } 33 + }); 34 + }); 35 + }); 36 + 37 + describe('completion functionality', () => { 38 + const completionTests = [ 39 + { name: 'command completion', args: 'serve' }, 40 + { name: 'option completion', args: 'serve --p' }, 41 + { name: 'no match', args: 'nonexistent' }, 42 + ]; 43 + 44 + completionTests.forEach(({ name, args }) => { 45 + it(`should handle ${name}`, async () => { 46 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete -- ${args}`; 47 + 48 + try { 49 + const { stdout, stderr } = await exec(command); 50 + 51 + // Should have some output (completions or directive) 52 + expect(stdout).toBeTruthy(); 53 + 54 + // Should end with completion directive (e.g., :0, :1, etc.) 55 + expect(stdout.trim()).toMatch(/:\d+$/); 56 + 57 + // Should not have errors 58 + expect(stderr).toBe(''); 59 + } catch (error) { 60 + throw new Error(`Failed completion test '${name}': ${error}`); 61 + } 62 + }); 63 + }); 64 + }); 65 + 66 + describe('shell script syntax validation', () => { 67 + it('should generate syntactically valid bash script', async () => { 68 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 69 + const { stdout } = await exec(command); 70 + 71 + // Write script to temp file 72 + const scriptPath = join(process.cwd(), 'temp-bash-completion.sh'); 73 + await writeFile(scriptPath, stdout); 74 + 75 + try { 76 + // Test bash syntax with -n flag (syntax check only) 77 + await exec(`bash -n ${scriptPath}`); 78 + // If we get here, syntax is valid 79 + expect(true).toBe(true); 80 + } catch (error) { 81 + throw new Error(`Bash script has syntax errors: ${error}`); 82 + } finally { 83 + // Clean up 84 + await unlink(scriptPath).catch(() => {}); 85 + } 86 + }); 87 + 88 + it('should generate syntactically valid zsh script', async () => { 89 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete zsh`; 90 + const { stdout } = await exec(command); 91 + 92 + // Write script to temp file 93 + const scriptPath = join(process.cwd(), 'temp-zsh-completion.zsh'); 94 + await writeFile(scriptPath, stdout); 95 + 96 + try { 97 + // Test zsh syntax with -n flag (syntax check only) 98 + await exec(`zsh -n ${scriptPath}`); 99 + // If we get here, syntax is valid 100 + expect(true).toBe(true); 101 + } catch (error) { 102 + throw new Error(`Zsh script has syntax errors: ${error}`); 103 + } finally { 104 + // Clean up 105 + await unlink(scriptPath).catch(() => {}); 106 + } 107 + }); 108 + 109 + it('should generate syntactically valid fish script', async () => { 110 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete fish`; 111 + const { stdout } = await exec(command); 112 + 113 + // Write script to temp file 114 + const scriptPath = join(process.cwd(), 'temp-fish-completion.fish'); 115 + await writeFile(scriptPath, stdout); 116 + 117 + try { 118 + // Test fish syntax with -n flag (syntax check only) 119 + await exec(`fish -n ${scriptPath}`); 120 + // If we get here, syntax is valid 121 + expect(true).toBe(true); 122 + } catch (error) { 123 + // Fish might not be available, so make this a softer check 124 + const errorMessage = 125 + error instanceof Error ? error.message : String(error); 126 + if ( 127 + errorMessage.includes('command not found') || 128 + errorMessage.includes('not recognized') 129 + ) { 130 + console.warn( 131 + 'Fish shell not available for syntax testing, skipping...' 132 + ); 133 + expect(true).toBe(true); 134 + } else { 135 + throw new Error(`Fish script has syntax errors: ${errorMessage}`); 136 + } 137 + } finally { 138 + // Clean up 139 + await unlink(scriptPath).catch(() => {}); 140 + } 141 + }); 142 + 143 + it('should generate syntactically valid powershell script', async () => { 144 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete powershell`; 145 + const { stdout } = await exec(command); 146 + 147 + // Write script to temp file 148 + const scriptPath = join(process.cwd(), 'temp-powershell-completion.ps1'); 149 + await writeFile(scriptPath, stdout); 150 + 151 + try { 152 + // Test PowerShell syntax 153 + await exec( 154 + `pwsh -NoProfile -Command "& { . '${scriptPath}'; exit 0 }"` 155 + ); 156 + // If we get here, syntax is valid 157 + expect(true).toBe(true); 158 + } catch (error) { 159 + // PowerShell might not be available, so make this a softer check 160 + const errorMessage = 161 + error instanceof Error ? error.message : String(error); 162 + if ( 163 + errorMessage.includes('command not found') || 164 + errorMessage.includes('not recognized') 165 + ) { 166 + console.warn( 167 + 'PowerShell not available for syntax testing, skipping...' 168 + ); 169 + expect(true).toBe(true); 170 + } else { 171 + throw new Error( 172 + `PowerShell script has syntax errors: ${errorMessage}` 173 + ); 174 + } 175 + } finally { 176 + // Clean up 177 + await unlink(scriptPath).catch(() => {}); 178 + } 179 + }); 180 + }); 181 + 182 + // Test shell-specific features 183 + describe('shell-specific functionality', () => { 184 + it('bash completion should include proper function definitions', async () => { 185 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 186 + const { stdout } = await exec(command); 187 + 188 + // Should contain bash completion function 189 + expect(stdout).toMatch(/__\w+_complete\(\)/); 190 + 191 + // Should contain complete command registration 192 + expect(stdout).toMatch(/complete -F __\w+_complete/); 193 + 194 + // Should handle bash completion variables (using _get_comp_words_by_ref) 195 + expect(stdout).toContain('_get_comp_words_by_ref'); 196 + expect(stdout).toContain('cur prev words cword'); 197 + }); 198 + 199 + it('zsh completion should include proper compdef', async () => { 200 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete zsh`; 201 + const { stdout } = await exec(command); 202 + 203 + // Should contain compdef directive 204 + expect(stdout).toMatch(/#compdef \w+/); 205 + 206 + // Should contain completion function 207 + expect(stdout).toMatch(/_\w+\(\)/); 208 + 209 + // Should register the completion 210 + expect(stdout).toMatch(/compdef _\w+ \w+/); 211 + }); 212 + 213 + it('fish completion should include proper complete commands', async () => { 214 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete fish`; 215 + const { stdout } = await exec(command); 216 + 217 + // Should contain fish complete commands 218 + expect(stdout).toContain('complete -c'); 219 + 220 + // Should handle command completion 221 + expect(stdout).toMatch(/complete -c \w+ -f/); 222 + }); 223 + }); 224 + 225 + // Test for potential bash issues (related to the user's problem) 226 + describe('bash-specific issue detection', () => { 227 + it('should generate bash script with proper escaping', async () => { 228 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 229 + const { stdout } = await exec(command); 230 + 231 + // Check for the actual problematic bash syntax in the requestComp assignment 232 + expect(stdout).not.toMatch(/requestComp="[^"]*\$\{words\[@\]:1\}/); // Should not use ${words[@]:1} in requestComp 233 + expect(stdout).toContain('requestComp='); // Should have proper variable assignment 234 + expect(stdout).toContain('complete -F'); // Should register completion properly 235 + }); 236 + 237 + it('should generate bash script that handles empty parameters correctly', async () => { 238 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 239 + const { stdout } = await exec(command); 240 + 241 + // Should handle empty parameters 242 + expect(stdout).toContain(`''`); // Should add empty parameter handling 243 + expect(stdout).toContain('requestComp='); // Should build command properly 244 + }); 245 + }); 246 + });