Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions sdk/src/__tests__/run-terminal-command.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expect, test } from 'bun:test'

import { findWindowsBash } from '../tools/run-terminal-command'

function existsOnly(paths: string[]) {
const existingPaths = new Set(paths.map((path) => path.toLowerCase()))
return (path: string) => existingPaths.has(path.toLowerCase())
}

describe('findWindowsBash', () => {
test('finds Git Bash installed by Scoop under USERPROFILE', () => {
const scoopBash = String.raw`C:\Users\dev\scoop\apps\git\current\bin\bash.exe`

expect(
findWindowsBash(
{ USERPROFILE: String.raw`C:\Users\dev`, PATH: '' },
existsOnly([scoopBash]),
),
).toBe(scoopBash)
})

test('finds Git Bash installed by Scoop under SCOOP_GLOBAL', () => {
const scoopBash = String.raw`C:\ProgramData\scoop\apps\git\current\usr\bin\bash.exe`

expect(
findWindowsBash(
{
SCOOP_GLOBAL: String.raw`C:\ProgramData\scoop`,
USERPROFILE: String.raw`C:\Users\dev`,
PATH: '',
},
existsOnly([scoopBash]),
),
).toBe(scoopBash)
})

test('prefers non-WSL bash in PATH over WSL bash', () => {
const gitBash = String.raw`C:\Tools\Git\bin\bash.exe`
const wslBash = String.raw`C:\Windows\System32\bash.exe`

expect(
findWindowsBash(
{
PATH: String.raw`C:\Windows\System32;C:\Tools\Git\bin`,
},
existsOnly([gitBash, wslBash]),
),
).toBe(gitBash)
})
})
58 changes: 41 additions & 17 deletions sdk/src/tools/run-terminal-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,23 @@ const GIT_BASH_COMMON_PATHS = [

// WSL bash paths that are often unreliable (VM may not be running, quote escaping issues)
// These are checked last as a fallback only
const WSL_BASH_PATH_PATTERNS = [
'system32',
'windowsapps',
]
const WSL_BASH_PATH_PATTERNS = ['system32', 'windowsapps']

function getScoopGitBashPaths(env: NodeJS.ProcessEnv): string[] {
const scoopBases = [
env.SCOOP,
env.USERPROFILE ? path.win32.join(env.USERPROFILE, 'scoop') : undefined,
env.SCOOP_GLOBAL,
].filter((base): base is string => Boolean(base))

return [...new Set(scoopBases)].flatMap((scoopBase) => {
const gitDir = path.win32.join(scoopBase, 'apps', 'git', 'current')
return [
path.win32.join(gitDir, 'bin', 'bash.exe'),
path.win32.join(gitDir, 'usr', 'bin', 'bash.exe'),
]
})
}

/**
* Find bash executable on Windows.
Expand All @@ -34,37 +47,48 @@ const WSL_BASH_PATH_PATTERNS = [
* 2. Common Git Bash installation locations (most reliable)
* 3. Non-WSL bash in PATH (e.g., Git Bash added to PATH)
* 4. WSL bash in PATH (last resort - System32, WindowsApps)
*
*
* WSL bash is deprioritized because it can fail with cryptic errors when:
* - The WSL VM is not running
* - Quote/argument escaping issues between Windows and Linux
* - UTF-16 encoding mismatches
*/
function findWindowsBash(env: NodeJS.ProcessEnv): string | null {
export function findWindowsBash(
env: NodeJS.ProcessEnv,
existsSync: (path: string) => boolean = fs.existsSync,
): string | null {
// Check for user-specified path via environment variable
const customPath = env.CODEBUFF_GIT_BASH_PATH
if (customPath && fs.existsSync(customPath)) {
if (customPath && existsSync(customPath)) {
return customPath
}

// Check common Git Bash installation locations first (most reliable)
for (const commonPath of GIT_BASH_COMMON_PATHS) {
if (fs.existsSync(commonPath)) {
if (existsSync(commonPath)) {
return commonPath
}
}

for (const scoopPath of getScoopGitBashPaths(env)) {
if (existsSync(scoopPath)) {
return scoopPath
}
}

// Fall back to bash.exe in PATH, but skip WSL paths initially
const pathEnv = env.PATH || env.Path || ''
const pathDirs = pathEnv.split(path.delimiter)
const pathDirs = pathEnv.split(';')
const wslFallbackPaths: string[] = []

for (const dir of pathDirs) {
const dirLower = dir.toLowerCase()
const isWslPath = WSL_BASH_PATH_PATTERNS.some(pattern => dirLower.includes(pattern))

const bashPath = path.join(dir, 'bash.exe')
if (fs.existsSync(bashPath)) {
const isWslPath = WSL_BASH_PATH_PATTERNS.some((pattern) =>
dirLower.includes(pattern),
)

const bashPath = path.win32.join(dir, 'bash.exe')
if (existsSync(bashPath)) {
if (isWslPath) {
// Save WSL paths for last resort
wslFallbackPaths.push(bashPath)
Expand All @@ -73,10 +97,10 @@ function findWindowsBash(env: NodeJS.ProcessEnv): string | null {
return bashPath
}
}

// Also check for just 'bash' (without .exe)
const bashPathNoExt = path.join(dir, 'bash')
if (fs.existsSync(bashPathNoExt)) {
const bashPathNoExt = path.win32.join(dir, 'bash')
if (existsSync(bashPathNoExt)) {
if (isWslPath) {
wslFallbackPaths.push(bashPathNoExt)
} else {
Expand Down