mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-15 12:53:31 +08:00
Stop treating Git Bash as a hard startup dependency on Windows desktop sessions
Windows desktop sessions were failing during early hook execution because the
runtime assumed Git Bash was always installed. That made a missing Git
installation look like a fatal CLI startup regression even though PowerShell is
usually available and sufficient for default hook execution.
This change turns Git Bash lookup into a best-effort probe, leaves SHELL unset
when Git Bash is missing, and falls back to PowerShell for default Windows hook
execution. Explicit bash hooks still fail with a direct installation hint, but
they no longer hard-exit the whole process.
Constraint: Windows users may have PowerShell but no Git for Windows installed
Rejected: Keep hard-exiting on missing Git Bash | makes first-run desktop chat fail for a recoverable missing dependency
Rejected: Bundle Git for Windows into the app | larger distribution and higher maintenance burden than a runtime fallback
Confidence: medium
Scope-risk: moderate
Reversibility: clean
Directive: Treat Git Bash as an optional Windows capability unless a code path explicitly requires bash semantics
Tested: bun test src/server/__tests__/conversation-service.test.ts
Tested: bun -e "await import('./src/utils/windowsPaths.ts'); await import('./src/utils/hooks.ts'); console.log('module-import-ok')"
Not-tested: End-to-end desktop chat startup on a clean Windows Server host without Git installed
Not-tested: Explicit bash hook execution on Windows after the fallback path
Related: GitHub issue #62
This commit is contained in:
parent
b7396e4f65
commit
28059157ff
@ -17,7 +17,10 @@ import {
|
||||
} from './sessionEnvironment.js'
|
||||
import { subprocessEnv } from './subprocessEnv.js'
|
||||
import { getPlatform } from './platform.js'
|
||||
import { findGitBashPath, windowsPathToPosixPath } from './windowsPaths.js'
|
||||
import {
|
||||
tryFindGitBashPath,
|
||||
windowsPathToPosixPath,
|
||||
} from './windowsPaths.js'
|
||||
import { getCachedPowerShellPath } from './shell/powershellDetection.js'
|
||||
import { DEFAULT_HOOK_SHELL } from './shell/shellProvider.js'
|
||||
import { buildPowerShellArgs } from './shell/powershellProvider.js'
|
||||
@ -787,9 +790,20 @@ async function execCommandHook(
|
||||
// PowerShell path deliberately skips the Windows-specific bash
|
||||
// accommodations (cygpath conversion, .sh auto-prepend, POSIX-quoted
|
||||
// SHELL_PREFIX).
|
||||
const shellType = hook.shell ?? DEFAULT_HOOK_SHELL
|
||||
let shellType = hook.shell ?? DEFAULT_HOOK_SHELL
|
||||
|
||||
const isPowerShell = shellType === 'powershell'
|
||||
if (isWindows && !hook.shell && shellType === 'bash' && !tryFindGitBashPath()) {
|
||||
const pwshPath = await getCachedPowerShellPath()
|
||||
if (pwshPath) {
|
||||
shellType = 'powershell'
|
||||
logForDebugging(
|
||||
`Hooks: Git Bash unavailable on Windows, falling back to PowerShell for default shell on hook "${hook.command}"`,
|
||||
{ level: 'warn' },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const usesPowerShell = shellType === 'powershell'
|
||||
|
||||
// --
|
||||
// Windows bash path: hooks run via Git Bash (Cygwin), NOT cmd.exe.
|
||||
@ -806,7 +820,7 @@ async function execCommandHook(
|
||||
// PowerShell expects Windows paths on Windows (and native paths on
|
||||
// Unix where pwsh is also available).
|
||||
const toHookPath =
|
||||
isWindows && !isPowerShell
|
||||
isWindows && !usesPowerShell
|
||||
? (p: string) => windowsPathToPosixPath(p)
|
||||
: (p: string) => p
|
||||
|
||||
@ -859,7 +873,7 @@ async function execCommandHook(
|
||||
// On Windows (bash only), auto-prepend `bash` for .sh scripts so they
|
||||
// execute instead of opening in the default file handler. PowerShell
|
||||
// runs .ps1 files natively — no prepend needed.
|
||||
if (isWindows && !isPowerShell && command.trim().match(/\.sh(\s|$|")/)) {
|
||||
if (isWindows && !usesPowerShell && command.trim().match(/\.sh(\s|$|")/)) {
|
||||
if (!command.trim().startsWith('bash ')) {
|
||||
command = `bash ${command}`
|
||||
}
|
||||
@ -870,7 +884,7 @@ async function execCommandHook(
|
||||
// PowerShell — see design §8.1. For now PS hooks ignore the prefix;
|
||||
// a CLAUDE_CODE_PS_SHELL_PREFIX (or shell-aware prefix) is a follow-up.
|
||||
const finalCommand =
|
||||
!isPowerShell && process.env.CLAUDE_CODE_SHELL_PREFIX
|
||||
!usesPowerShell && process.env.CLAUDE_CODE_SHELL_PREFIX
|
||||
? formatShellPrefixCommand(process.env.CLAUDE_CODE_SHELL_PREFIX, command)
|
||||
: command
|
||||
|
||||
@ -915,7 +929,7 @@ async function execCommandHook(
|
||||
// Skip for PS — consistent with how .sh prepend and SHELL_PREFIX are
|
||||
// already bash-only above.
|
||||
if (
|
||||
!isPowerShell &&
|
||||
!usesPowerShell &&
|
||||
(hookEvent === 'SessionStart' ||
|
||||
hookEvent === 'Setup' ||
|
||||
hookEvent === 'CwdChanged' ||
|
||||
@ -948,12 +962,9 @@ async function execCommandHook(
|
||||
// skips user profile scripts (faster, deterministic).
|
||||
// -NonInteractive fails fast instead of prompting.
|
||||
//
|
||||
// The Git Bash hard-exit in findGitBashPath() is still in place for
|
||||
// bash hooks. PowerShell hooks never call it, so a Windows user with
|
||||
// only pwsh and shell: 'powershell' on every hook could in theory run
|
||||
// without Git Bash — but init.ts still calls setShellIfWindows() on
|
||||
// startup, which will exit first. Relaxing that is phase 1 of the
|
||||
// design's implementation order (separate PR).
|
||||
// On Windows, default-shell hooks now degrade to PowerShell when Git Bash
|
||||
// is unavailable. Explicit bash hooks still surface an actionable error,
|
||||
// but they no longer hard-exit the entire process.
|
||||
let child: ChildProcessWithoutNullStreams
|
||||
if (shellType === 'powershell') {
|
||||
const pwshPath = await getCachedPowerShellPath()
|
||||
@ -973,7 +984,14 @@ async function execCommandHook(
|
||||
} else {
|
||||
// On Windows, use Git Bash explicitly (cmd.exe can't run bash syntax).
|
||||
// On other platforms, shell: true uses /bin/sh.
|
||||
const shell = isWindows ? findGitBashPath() : true
|
||||
const shell = isWindows
|
||||
? tryFindGitBashPath() ??
|
||||
(() => {
|
||||
throw new Error(
|
||||
'Git Bash is required to run bash hooks on Windows. Install Git for Windows or configure the hook with shell: "powershell".',
|
||||
)
|
||||
})()
|
||||
: true
|
||||
child = spawn(finalCommand, [], {
|
||||
env: envVars,
|
||||
cwd: safeCwd,
|
||||
|
||||
@ -86,26 +86,31 @@ function findExecutable(executable: string): string | null {
|
||||
*/
|
||||
export function setShellIfWindows(): void {
|
||||
if (getPlatform() === 'windows') {
|
||||
const gitBashPath = findGitBashPath()
|
||||
const gitBashPath = tryFindGitBashPath()
|
||||
if (!gitBashPath) {
|
||||
logForDebugging(
|
||||
'Git Bash not found on Windows; leaving SHELL unset for lazy fallback handling',
|
||||
{ level: 'warn' },
|
||||
)
|
||||
return
|
||||
}
|
||||
process.env.SHELL = gitBashPath
|
||||
logForDebugging(`Using bash path: "${gitBashPath}"`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the path where `bash.exe` included with git-bash exists, exiting the process if not found.
|
||||
* Best-effort Git Bash resolution on Windows.
|
||||
*
|
||||
* Returns null when Git Bash is unavailable so callers can decide whether to
|
||||
* fall back (for example, to PowerShell) instead of hard-exiting the process.
|
||||
*/
|
||||
export const findGitBashPath = memoize((): string => {
|
||||
export const tryFindGitBashPath = memoize((): string | null => {
|
||||
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)
|
||||
return null
|
||||
}
|
||||
|
||||
const gitPath = findExecutable('git')
|
||||
@ -116,6 +121,27 @@ export const findGitBashPath = memoize((): string => {
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
})
|
||||
|
||||
/**
|
||||
* Find the path where `bash.exe` included with git-bash exists, exiting the process if not found.
|
||||
*/
|
||||
export const findGitBashPath = memoize((): string => {
|
||||
const gitBashPath = tryFindGitBashPath()
|
||||
if (gitBashPath) {
|
||||
return gitBashPath
|
||||
}
|
||||
|
||||
if (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)
|
||||
}
|
||||
|
||||
// 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',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user