mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 13:55:50 +00:00
* feat: 适配 zed acp 协议 * docs: 完善 acp 文档 * feat: integrate feature branches + daemon/job 命令层级化 + 跨平台后台引擎 Cherry-picked from origin/lint/preview (637c908), excluding lint-only changes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct detectMimeFromBase64 to decode raw bytes from base64 Cherry-picked from origin/lint/preview (ee36954). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: daemon 子进程 spawn 跨平台修复 + CliLaunchSpec 集中化重构 Cherry-picked from origin/lint/preview (c5f52cd), excluding lint-only formatting changes. - 新建 src/utils/cliLaunch.ts: 集中化 CLI 子进程启动层 - 修复 --daemon-worker=kind 等号格式解析 - 修复 daemon/bg fast path 缺少 setShellIfWindows() - 修复 checkPathExists 用 existsSync 替代 execSync('dir') - 7 个 spawn 站点迁移到 CliLaunchSpec Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: merge tsconfig.base.json into tsconfig.json with full compiler options The cherry-pick from637c908dropped jsx/strict/etc settings when removing tsconfig.base.json. This commit restores them in a single tsconfig.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: merge tsconfig.base.json into tsconfig.json with full compiler options The cherry-pick from637c908dropped jsx/strict/etc settings when removing tsconfig.base.json. This commit restores them in a single tsconfig.json. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
173 lines
6.0 KiB
TypeScript
173 lines
6.0 KiB
TypeScript
import { existsSync } from 'fs'
|
|
import memoize from 'lodash-es/memoize.js'
|
|
import * as path from 'path'
|
|
import * as pathWin32 from 'path/win32'
|
|
import { getCwd } from './cwd.js'
|
|
import { logForDebugging } from './debug.js'
|
|
import { execSync_DEPRECATED } from './execSyncWrapper.js'
|
|
import { memoizeWithLRU } from './memoize.js'
|
|
import { getPlatform } from './platform.js'
|
|
|
|
/**
|
|
* Check if a file or directory exists on Windows.
|
|
* Uses fs.existsSync instead of `dir` shell command to avoid spawning
|
|
* cmd.exe — which can cause brief console window flashes in detached
|
|
* or windowsHide child processes.
|
|
*/
|
|
function checkPathExists(filePath: string): boolean {
|
|
return existsSync(filePath)
|
|
}
|
|
|
|
/**
|
|
* Find an executable using where.exe on Windows
|
|
* @param executable - The name of the executable to find
|
|
* @returns The path to the executable or null if not found
|
|
*/
|
|
function findExecutable(executable: string): string | null {
|
|
// For git, check common installation locations first
|
|
if (executable === 'git') {
|
|
const defaultLocations = [
|
|
// check 64 bit before 32 bit
|
|
'C:\\Program Files\\Git\\cmd\\git.exe',
|
|
'C:\\Program Files (x86)\\Git\\cmd\\git.exe',
|
|
// intentionally don't look for C:\Program Files\Git\mingw64\bin\git.exe
|
|
// because that directory is the "raw" tools with no environment setup
|
|
]
|
|
|
|
for (const location of defaultLocations) {
|
|
if (checkPathExists(location)) {
|
|
return location
|
|
}
|
|
}
|
|
}
|
|
|
|
// Fall back to where.exe
|
|
try {
|
|
const result = execSync_DEPRECATED(`where.exe ${executable}`, {
|
|
stdio: 'pipe',
|
|
encoding: 'utf8',
|
|
}).trim()
|
|
|
|
// SECURITY: Filter out any results from the current directory
|
|
// to prevent executing malicious git.bat/cmd/exe files
|
|
const paths = result.split('\r\n').filter(Boolean)
|
|
const cwd = getCwd().toLowerCase()
|
|
|
|
for (const candidatePath of paths) {
|
|
// Normalize and compare paths to ensure we're not in current directory
|
|
const normalizedPath = path.resolve(candidatePath).toLowerCase()
|
|
const pathDir = path.dirname(normalizedPath).toLowerCase()
|
|
|
|
// Skip if the executable is in the current working directory
|
|
if (pathDir === cwd || normalizedPath.startsWith(cwd + path.sep)) {
|
|
logForDebugging(
|
|
`Skipping potentially malicious executable in current directory: ${candidatePath}`,
|
|
)
|
|
continue
|
|
}
|
|
|
|
// Return the first valid path that's not in the current directory
|
|
return candidatePath
|
|
}
|
|
|
|
return null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
/**
|
|
* If Windows, set the SHELL environment variable to git-bash path.
|
|
* This is used by BashTool and Shell.ts for user shell commands.
|
|
* COMSPEC is left unchanged for system process execution.
|
|
*/
|
|
export function setShellIfWindows(): void {
|
|
if (getPlatform() === 'windows') {
|
|
const gitBashPath = findGitBashPath()
|
|
process.env.SHELL = gitBashPath
|
|
// Propagate to child processes so they skip filesystem probing
|
|
process.env.CLAUDE_CODE_GIT_BASH_PATH = gitBashPath
|
|
logForDebugging(`Using bash path: "${gitBashPath}"`)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find the path where `bash.exe` included with git-bash exists, exiting the process if not found.
|
|
*/
|
|
export const findGitBashPath = memoize((): string => {
|
|
if (process.env.CLAUDE_CODE_GIT_BASH_PATH) {
|
|
if (checkPathExists(process.env.CLAUDE_CODE_GIT_BASH_PATH)) {
|
|
return process.env.CLAUDE_CODE_GIT_BASH_PATH
|
|
}
|
|
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
|
console.error(
|
|
`Claude Code was unable to find CLAUDE_CODE_GIT_BASH_PATH path "${process.env.CLAUDE_CODE_GIT_BASH_PATH}"`,
|
|
)
|
|
// eslint-disable-next-line custom-rules/no-process-exit
|
|
process.exit(1)
|
|
}
|
|
|
|
const gitPath = findExecutable('git')
|
|
if (gitPath) {
|
|
const bashPath = pathWin32.join(gitPath, '..', '..', 'bin', 'bash.exe')
|
|
if (checkPathExists(bashPath)) {
|
|
return bashPath
|
|
}
|
|
}
|
|
|
|
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
|
console.error(
|
|
'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',
|
|
)
|
|
// eslint-disable-next-line custom-rules/no-process-exit
|
|
process.exit(1)
|
|
})
|
|
|
|
/** Convert a Windows path to a POSIX path using pure JS. */
|
|
export const windowsPathToPosixPath = memoizeWithLRU(
|
|
(windowsPath: string): string => {
|
|
// Handle UNC paths: \\server\share -> //server/share
|
|
if (windowsPath.startsWith('\\\\')) {
|
|
return windowsPath.replace(/\\/g, '/')
|
|
}
|
|
// Handle drive letter paths: C:\Users\foo -> /c/Users/foo
|
|
const match = windowsPath.match(/^([A-Za-z]):[/\\]/)
|
|
if (match) {
|
|
const driveLetter = match[1]!.toLowerCase()
|
|
return '/' + driveLetter + windowsPath.slice(2).replace(/\\/g, '/')
|
|
}
|
|
// Already POSIX or relative — just flip slashes
|
|
return windowsPath.replace(/\\/g, '/')
|
|
},
|
|
(p: string) => p,
|
|
500,
|
|
)
|
|
|
|
/** Convert a POSIX path to a Windows path using pure JS. */
|
|
export const posixPathToWindowsPath = memoizeWithLRU(
|
|
(posixPath: string): string => {
|
|
// Handle UNC paths: //server/share -> \\server\share
|
|
if (posixPath.startsWith('//')) {
|
|
return posixPath.replace(/\//g, '\\')
|
|
}
|
|
// Handle /cygdrive/c/... format
|
|
const cygdriveMatch = posixPath.match(/^\/cygdrive\/([A-Za-z])(\/|$)/)
|
|
if (cygdriveMatch) {
|
|
const driveLetter = cygdriveMatch[1]!.toUpperCase()
|
|
const rest = posixPath.slice(('/cygdrive/' + cygdriveMatch[1]).length)
|
|
return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\')
|
|
}
|
|
// Handle /c/... format (MSYS2/Git Bash)
|
|
const driveMatch = posixPath.match(/^\/([A-Za-z])(\/|$)/)
|
|
if (driveMatch) {
|
|
const driveLetter = driveMatch[1]!.toUpperCase()
|
|
const rest = posixPath.slice(2)
|
|
return driveLetter + ':' + (rest || '\\').replace(/\//g, '\\')
|
|
}
|
|
// Already Windows or relative — just flip slashes
|
|
return posixPath.replace(/\//g, '\\')
|
|
},
|
|
(p: string) => p,
|
|
500,
|
|
)
|