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

gh matrixes

AmirSa12 (Aug 17, 2025, 8:36 PM +0330) 8ad79549 7c0ad271

+226 -163
+96 -3
.github/workflows/ci.yml
··· 10 10 11 11 jobs: 12 12 test: 13 - name: Tests 13 + name: Tests (${{ matrix.os }}) 14 14 strategy: 15 15 matrix: 16 16 os: [ubuntu-latest, macos-latest, windows-latest] ··· 33 33 - name: Install deps 34 34 run: pnpm install 35 35 36 - - name: Run tests 37 - run: pnpm test 36 + - name: Run core tests (excluding shell integration) 37 + run: pnpm test --exclude="**/shell-integration.test.ts" 38 + 39 + shell-tests: 40 + name: Shell Tests (${{ matrix.shell }} on ${{ matrix.os }}) 41 + strategy: 42 + matrix: 43 + shell: [bash, zsh, fish, powershell] 44 + os: [ubuntu-latest, macos-latest] 45 + exclude: 46 + # PowerShell installation can be flaky on macOS in CI 47 + - shell: powershell 48 + os: macos-latest 49 + runs-on: ${{ matrix.os }} 50 + 51 + steps: 52 + - name: Checkout 53 + uses: actions/checkout@v4 54 + 55 + - name: Install shell dependencies (Ubuntu) 56 + if: matrix.os == 'ubuntu-latest' 57 + run: | 58 + sudo apt-get update 59 + case "${{ matrix.shell }}" in 60 + bash) 61 + sudo apt-get install -y bash-completion 62 + ;; 63 + zsh) 64 + sudo apt-get install -y zsh 65 + ;; 66 + fish) 67 + sudo apt-get install -y fish 68 + ;; 69 + powershell) 70 + wget -q https://packages.microsoft.com/config/ubuntu/20.04/packages-microsoft-prod.deb 71 + sudo dpkg -i packages-microsoft-prod.deb 72 + sudo apt-get update 73 + sudo apt-get install -y powershell 74 + ;; 75 + esac 76 + 77 + - name: Install shell dependencies (macOS) 78 + if: matrix.os == 'macos-latest' 79 + run: | 80 + case "${{ matrix.shell }}" in 81 + bash) 82 + brew install bash-completion@2 83 + ;; 84 + zsh) 85 + # zsh is already installed on macOS 86 + echo "zsh already available" 87 + ;; 88 + fish) 89 + brew install fish 90 + ;; 91 + powershell) 92 + # Skip PowerShell on macOS for now due to CI flakiness 93 + echo "PowerShell skipped on macOS" 94 + ;; 95 + esac 96 + 97 + - name: Verify shell installation 98 + run: | 99 + case "${{ matrix.shell }}" in 100 + bash) 101 + bash --version 102 + ;; 103 + zsh) 104 + zsh --version 105 + ;; 106 + fish) 107 + fish --version 108 + ;; 109 + powershell) 110 + pwsh --version 111 + ;; 112 + esac 113 + 114 + - name: Install pnpm 115 + uses: pnpm/action-setup@v4.0.0 116 + 117 + - name: Set node version to 20 118 + uses: actions/setup-node@v4 119 + with: 120 + node-version: 20 121 + registry-url: https://registry.npmjs.org/ 122 + cache: 'pnpm' 123 + 124 + - name: Install deps 125 + run: pnpm install 126 + 127 + - name: Run shell-specific tests 128 + run: pnpm test tests/shell-integration.test.ts 129 + env: 130 + TEST_SHELL: ${{ matrix.shell }} 38 131 39 132 typecheck: 40 133 name: Lint and Type Check
+130 -160
tests/shell-integration.test.ts
··· 7 7 const exec = promisify(execCb); 8 8 9 9 describe('shell integration tests', () => { 10 - const shells = ['zsh', 'bash', 'fish', 'powershell']; 10 + // Support matrix testing - if TEST_SHELL is set, only test that shell 11 + const testShell = process.env.TEST_SHELL; 12 + const shells = testShell 13 + ? [testShell] 14 + : ['zsh', 'bash', 'fish', 'powershell']; 11 15 const cliTool = 'cac'; 12 16 13 17 describe('shell script generation', () => { ··· 64 68 }); 65 69 66 70 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); 71 + // Only run syntax validation for the shells we're testing 72 + const syntaxTestShells = shells.filter( 73 + (shell) => 74 + shell === 'bash' || 75 + shell === 'zsh' || 76 + shell === 'fish' || 77 + shell === 'powershell' 78 + ); 79 + 80 + syntaxTestShells.forEach((shell) => { 81 + it( 82 + `should generate syntactically valid ${shell} script`, 83 + async () => { 84 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete ${shell}`; 85 + const { stdout } = await exec(command); 86 + 87 + // Write script to temp file 88 + const scriptPath = join( 89 + process.cwd(), 90 + `temp-${shell}-completion.${shell === 'powershell' ? 'ps1' : shell === 'fish' ? 'fish' : 'sh'}` 91 + ); 92 + await writeFile(scriptPath, stdout); 93 + 94 + try { 95 + // Test syntax based on shell type 96 + switch (shell) { 97 + case 'bash': 98 + await exec(`bash -n ${scriptPath}`); 99 + break; 100 + case 'zsh': 101 + await exec(`zsh -n ${scriptPath}`); 102 + break; 103 + case 'fish': 104 + await exec(`fish -n ${scriptPath}`); 105 + break; 106 + case 'powershell': 107 + // Test PowerShell syntax with timeout to prevent hanging in CI 108 + const testPromise = exec( 109 + `pwsh -NoProfile -Command "& { . '${scriptPath}'; exit 0 }"` 110 + ); 70 111 71 - // Write script to temp file 72 - const scriptPath = join(process.cwd(), 'temp-bash-completion.sh'); 73 - await writeFile(scriptPath, stdout); 112 + const timeoutPromise = new Promise((_, reject) => 113 + setTimeout( 114 + () => reject(new Error('PowerShell test timed out')), 115 + 10000 116 + ) 117 + ); 74 118 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 - const errorMessage = 82 - error instanceof Error ? error.message : String(error); 119 + await Promise.race([testPromise, timeoutPromise]); 120 + break; 121 + } 122 + // If we get here, syntax is valid 123 + expect(true).toBe(true); 124 + } catch (error) { 125 + const errorMessage = 126 + error instanceof Error ? error.message : String(error); 83 127 84 - // Provide helpful error message about bash-completion dependency 85 - const helpMessage = ` 128 + // Provide helpful error message for bash-completion dependency 129 + if (shell === 'bash' && errorMessage.includes('syntax')) { 130 + const helpMessage = ` 86 131 Bash script has syntax errors: ${errorMessage} 87 132 88 133 This might be due to missing bash-completion dependency. ··· 93 138 Then source the completion in your shell profile: 94 139 echo 'source $(brew --prefix)/share/bash-completion/bash_completion' >> ~/.bashrc 95 140 `; 96 - throw new Error(helpMessage); 97 - } finally { 98 - // Clean up 99 - await unlink(scriptPath).catch(() => {}); 100 - } 101 - }); 102 - 103 - it('should generate syntactically valid zsh script', async () => { 104 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete zsh`; 105 - const { stdout } = await exec(command); 106 - 107 - // Write script to temp file 108 - const scriptPath = join(process.cwd(), 'temp-zsh-completion.zsh'); 109 - await writeFile(scriptPath, stdout); 110 - 111 - try { 112 - // Test zsh syntax with -n flag (syntax check only) 113 - await exec(`zsh -n ${scriptPath}`); 114 - // If we get here, syntax is valid 115 - expect(true).toBe(true); 116 - } catch (error) { 117 - throw new Error(`Zsh script has syntax errors: ${error}`); 118 - } finally { 119 - // Clean up 120 - await unlink(scriptPath).catch(() => {}); 121 - } 122 - }); 123 - 124 - it('should generate syntactically valid fish script', async () => { 125 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete fish`; 126 - const { stdout } = await exec(command); 127 - 128 - // Write script to temp file 129 - const scriptPath = join(process.cwd(), 'temp-fish-completion.fish'); 130 - await writeFile(scriptPath, stdout); 131 - 132 - try { 133 - // Test fish syntax with -n flag (syntax check only) 134 - await exec(`fish -n ${scriptPath}`); 135 - // If we get here, syntax is valid 136 - expect(true).toBe(true); 137 - } catch (error) { 138 - // Fish might not be available, so make this a softer check 139 - const errorMessage = 140 - error instanceof Error ? error.message : String(error); 141 - if ( 142 - errorMessage.includes('command not found') || 143 - errorMessage.includes('not recognized') 144 - ) { 145 - console.warn( 146 - 'Fish shell not available for syntax testing, skipping...' 147 - ); 148 - expect(true).toBe(true); 149 - } else { 150 - throw new Error(`Fish script has syntax errors: ${errorMessage}`); 151 - } 152 - } finally { 153 - // Clean up 154 - await unlink(scriptPath).catch(() => {}); 155 - } 156 - }); 157 - 158 - it('should generate syntactically valid powershell script', async () => { 159 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete powershell`; 160 - const { stdout } = await exec(command); 161 - 162 - // Write script to temp file 163 - const scriptPath = join(process.cwd(), 'temp-powershell-completion.ps1'); 164 - await writeFile(scriptPath, stdout); 141 + throw new Error(helpMessage); 142 + } 165 143 166 - try { 167 - // Test PowerShell syntax 168 - await exec( 169 - `pwsh -NoProfile -Command "& { . '${scriptPath}'; exit 0 }"` 170 - ); 171 - // If we get here, syntax is valid 172 - expect(true).toBe(true); 173 - } catch (error) { 174 - // PowerShell might not be available, so make this a softer check 175 - const errorMessage = 176 - error instanceof Error ? error.message : String(error); 177 - if ( 178 - errorMessage.includes('command not found') || 179 - errorMessage.includes('not recognized') 180 - ) { 181 - console.warn( 182 - 'PowerShell not available for syntax testing, skipping...' 183 - ); 184 - expect(true).toBe(true); 185 - } else { 186 - throw new Error( 187 - `PowerShell script has syntax errors: ${errorMessage}` 188 - ); 189 - } 190 - } finally { 191 - // Clean up 192 - await unlink(scriptPath).catch(() => {}); 193 - } 144 + throw new Error(`${shell} script has syntax errors: ${error}`); 145 + } finally { 146 + // Clean up 147 + await unlink(scriptPath).catch(() => {}); 148 + } 149 + }, 150 + shell === 'powershell' ? 15000 : 5000 151 + ); // Longer timeout for PowerShell 194 152 }); 195 153 }); 196 154 197 155 // Test shell-specific features 198 156 describe('shell-specific functionality', () => { 199 - it('bash completion should include proper function definitions', async () => { 200 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 201 - const { stdout } = await exec(command); 157 + shells.forEach((shell) => { 158 + switch (shell) { 159 + case 'bash': 160 + it('bash completion should include proper function definitions', async () => { 161 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 162 + const { stdout } = await exec(command); 202 163 203 - // Should contain bash completion function 204 - expect(stdout).toMatch(/__\w+_complete\(\)/); 164 + // Should contain bash completion function 165 + expect(stdout).toMatch(/__\w+_complete\(\)/); 205 166 206 - // Should contain complete command registration 207 - expect(stdout).toMatch(/complete -F __\w+_complete/); 167 + // Should contain complete command registration 168 + expect(stdout).toMatch(/complete -F __\w+_complete/); 208 169 209 - // Should handle bash completion variables (using _get_comp_words_by_ref) 210 - expect(stdout).toContain('_get_comp_words_by_ref'); 211 - expect(stdout).toContain('cur prev words cword'); 212 - }); 170 + // Should handle bash completion variables (using _get_comp_words_by_ref) 171 + expect(stdout).toContain('_get_comp_words_by_ref'); 172 + expect(stdout).toContain('cur prev words cword'); 173 + }); 174 + break; 213 175 214 - it('zsh completion should include proper compdef', async () => { 215 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete zsh`; 216 - const { stdout } = await exec(command); 176 + case 'zsh': 177 + it('zsh completion should include proper compdef', async () => { 178 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete zsh`; 179 + const { stdout } = await exec(command); 217 180 218 - // Should contain compdef directive 219 - expect(stdout).toMatch(/#compdef \w+/); 181 + // Should contain compdef directive 182 + expect(stdout).toMatch(/#compdef \w+/); 220 183 221 - // Should contain completion function 222 - expect(stdout).toMatch(/_\w+\(\)/); 184 + // Should contain completion function 185 + expect(stdout).toMatch(/_\w+\(\)/); 223 186 224 - // Should register the completion 225 - expect(stdout).toMatch(/compdef _\w+ \w+/); 226 - }); 187 + // Should register the completion 188 + expect(stdout).toMatch(/compdef _\w+ \w+/); 189 + }); 190 + break; 227 191 228 - it('fish completion should include proper complete commands', async () => { 229 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete fish`; 230 - const { stdout } = await exec(command); 192 + case 'fish': 193 + it('fish completion should include proper complete commands', async () => { 194 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete fish`; 195 + const { stdout } = await exec(command); 231 196 232 - // Should contain fish complete commands 233 - expect(stdout).toContain('complete -c'); 197 + // Should contain fish complete commands 198 + expect(stdout).toContain('complete -c'); 234 199 235 - // Should handle command completion 236 - expect(stdout).toMatch(/complete -c \w+ -f/); 200 + // Should handle command completion 201 + expect(stdout).toMatch(/complete -c \w+ -f/); 202 + }); 203 + break; 204 + } 237 205 }); 238 206 }); 239 207 240 - // Test for potential bash issues (related to the user's problem) 241 - describe('bash-specific issue detection', () => { 242 - it('should generate bash script with proper syntax (requires bash-completion@2)', async () => { 243 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 244 - const { stdout } = await exec(command); 208 + // Test for potential bash issues (only run for bash) 209 + if (shells.includes('bash')) { 210 + describe('bash-specific issue detection', () => { 211 + it('should generate bash script with proper syntax (requires bash-completion@2)', async () => { 212 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 213 + const { stdout } = await exec(command); 245 214 246 - // Check that it uses the correct ${words[@]:1} syntax 247 - expect(stdout).toContain('${words[@]:1}'); // Should use ${words[@]:1} (requires bash-completion@2) 248 - expect(stdout).toContain('requestComp='); // Should have proper variable assignment 249 - expect(stdout).toContain('complete -F'); // Should register completion properly 250 - expect(stdout).toContain('_get_comp_words_by_ref'); // Should use bash-completion functions 251 - }); 215 + // Check that it uses the correct ${words[@]:1} syntax 216 + expect(stdout).toContain('${words[@]:1}'); // Should use ${words[@]:1} (requires bash-completion@2) 217 + expect(stdout).toContain('requestComp='); // Should have proper variable assignment 218 + expect(stdout).toContain('complete -F'); // Should register completion properly 219 + expect(stdout).toContain('_get_comp_words_by_ref'); // Should use bash-completion functions 220 + }); 252 221 253 - it('should generate bash script that handles empty parameters correctly', async () => { 254 - const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 255 - const { stdout } = await exec(command); 222 + it('should generate bash script that handles empty parameters correctly', async () => { 223 + const command = `pnpm tsx examples/demo.${cliTool}.ts complete bash`; 224 + const { stdout } = await exec(command); 256 225 257 - // Should handle empty parameters 258 - expect(stdout).toContain(`''`); // Should add empty parameter handling 259 - expect(stdout).toContain('requestComp='); // Should build command properly 226 + // Should handle empty parameters 227 + expect(stdout).toContain(`''`); // Should add empty parameter handling 228 + expect(stdout).toContain('requestComp='); // Should build command properly 229 + }); 260 230 }); 261 - }); 231 + } 262 232 });