source dump of claude code
at main 173 lines 6.0 kB view raw
1import memoize from 'lodash-es/memoize.js' 2import * as path from 'path' 3import * as pathWin32 from 'path/win32' 4import { getCwd } from './cwd.js' 5import { logForDebugging } from './debug.js' 6import { execSync_DEPRECATED } from './execSyncWrapper.js' 7import { memoizeWithLRU } from './memoize.js' 8import { getPlatform } from './platform.js' 9 10/** 11 * Check if a file or directory exists on Windows using the dir command 12 * @param path - The path to check 13 * @returns true if the path exists, false otherwise 14 */ 15function checkPathExists(path: string): boolean { 16 try { 17 execSync_DEPRECATED(`dir "${path}"`, { stdio: 'pipe' }) 18 return true 19 } catch { 20 return false 21 } 22} 23 24/** 25 * Find an executable using where.exe on Windows 26 * @param executable - The name of the executable to find 27 * @returns The path to the executable or null if not found 28 */ 29function findExecutable(executable: string): string | null { 30 // For git, check common installation locations first 31 if (executable === 'git') { 32 const defaultLocations = [ 33 // check 64 bit before 32 bit 34 'C:\\Program Files\\Git\\cmd\\git.exe', 35 'C:\\Program Files (x86)\\Git\\cmd\\git.exe', 36 // intentionally don't look for C:\Program Files\Git\mingw64\bin\git.exe 37 // because that directory is the "raw" tools with no environment setup 38 ] 39 40 for (const location of defaultLocations) { 41 if (checkPathExists(location)) { 42 return location 43 } 44 } 45 } 46 47 // Fall back to where.exe 48 try { 49 const result = execSync_DEPRECATED(`where.exe ${executable}`, { 50 stdio: 'pipe', 51 encoding: 'utf8', 52 }).trim() 53 54 // SECURITY: Filter out any results from the current directory 55 // to prevent executing malicious git.bat/cmd/exe files 56 const paths = result.split('\r\n').filter(Boolean) 57 const cwd = getCwd().toLowerCase() 58 59 for (const candidatePath of paths) { 60 // Normalize and compare paths to ensure we're not in current directory 61 const normalizedPath = path.resolve(candidatePath).toLowerCase() 62 const pathDir = path.dirname(normalizedPath).toLowerCase() 63 64 // Skip if the executable is in the current working directory 65 if (pathDir === cwd || normalizedPath.startsWith(cwd + path.sep)) { 66 logForDebugging( 67 `Skipping potentially malicious executable in current directory: ${candidatePath}`, 68 ) 69 continue 70 } 71 72 // Return the first valid path that's not in the current directory 73 return candidatePath 74 } 75 76 return null 77 } catch { 78 return null 79 } 80} 81 82/** 83 * If Windows, set the SHELL environment variable to git-bash path. 84 * This is used by BashTool and Shell.ts for user shell commands. 85 * COMSPEC is left unchanged for system process execution. 86 */ 87export function setShellIfWindows(): void { 88 if (getPlatform() === 'windows') { 89 const gitBashPath = findGitBashPath() 90 process.env.SHELL = gitBashPath 91 logForDebugging(`Using bash path: "${gitBashPath}"`) 92 } 93} 94 95/** 96 * Find the path where `bash.exe` included with git-bash exists, exiting the process if not found. 97 */ 98export const findGitBashPath = memoize((): string => { 99 if (process.env.CLAUDE_CODE_GIT_BASH_PATH) { 100 if (checkPathExists(process.env.CLAUDE_CODE_GIT_BASH_PATH)) { 101 return process.env.CLAUDE_CODE_GIT_BASH_PATH 102 } 103 // biome-ignore lint/suspicious/noConsole:: intentional console output 104 console.error( 105 `Claude Code was unable to find CLAUDE_CODE_GIT_BASH_PATH path "${process.env.CLAUDE_CODE_GIT_BASH_PATH}"`, 106 ) 107 // eslint-disable-next-line custom-rules/no-process-exit 108 process.exit(1) 109 } 110 111 const gitPath = findExecutable('git') 112 if (gitPath) { 113 const bashPath = pathWin32.join(gitPath, '..', '..', 'bin', 'bash.exe') 114 if (checkPathExists(bashPath)) { 115 return bashPath 116 } 117 } 118 119 // biome-ignore lint/suspicious/noConsole:: intentional console output 120 console.error( 121 'Claude Code on Windows requires git-bash (https://git-scm.com/downloads/win). If installed but not in PATH, set environment variable pointing to your bash.exe, similar to: CLAUDE_CODE_GIT_BASH_PATH=C:\\Program Files\\Git\\bin\\bash.exe', 122 ) 123 // eslint-disable-next-line custom-rules/no-process-exit 124 process.exit(1) 125}) 126 127/** Convert a Windows path to a POSIX path using pure JS. */ 128export const windowsPathToPosixPath = memoizeWithLRU( 129 (windowsPath: string): string => { 130 // Handle UNC paths: \\server\share -> //server/share 131 if (windowsPath.startsWith('\\\\')) { 132 return windowsPath.replace(/\\/g, '/') 133 } 134 // Handle drive letter paths: C:\Users\foo -> /c/Users/foo 135 const match = windowsPath.match(/^([A-Za-z]):[/\\]/) 136 if (match) { 137 const driveLetter = match[1]!.toLowerCase() 138 return '/' + driveLetter + windowsPath.slice(2).replace(/\\/g, '/') 139 } 140 // Already POSIX or relative — just flip slashes 141 return windowsPath.replace(/\\/g, '/') 142 }, 143 (p: string) => p, 144 500, 145) 146 147/** Convert a POSIX path to a Windows path using pure JS. */ 148export const posixPathToWindowsPath = memoizeWithLRU( 149 (posixPath: string): string => { 150 // Handle UNC paths: //server/share -> \\server\share 151 if (posixPath.startsWith('//')) { 152 return posixPath.replace(/\//g, '\\') 153 } 154 // Handle /cygdrive/c/... format 155 const cygdriveMatch = posixPath.match(/^\/cygdrive\/([A-Za-z])(\/|$)/) 156 if (cygdriveMatch) { 157 const driveLetter = cygdriveMatch[1]!.toUpperCase() 158 const rest = posixPath.slice(('/cygdrive/' + cygdriveMatch[1]).length) 159 return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\') 160 } 161 // Handle /c/... format (MSYS2/Git Bash) 162 const driveMatch = posixPath.match(/^\/([A-Za-z])(\/|$)/) 163 if (driveMatch) { 164 const driveLetter = driveMatch[1]!.toUpperCase() 165 const rest = posixPath.slice(2) 166 return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\') 167 } 168 // Already Windows or relative — just flip slashes 169 return posixPath.replace(/\//g, '\\') 170 }, 171 (p: string) => p, 172 500, 173)