mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-29 16:03:34 +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'
|
} from './sessionEnvironment.js'
|
||||||
import { subprocessEnv } from './subprocessEnv.js'
|
import { subprocessEnv } from './subprocessEnv.js'
|
||||||
import { getPlatform } from './platform.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 { getCachedPowerShellPath } from './shell/powershellDetection.js'
|
||||||
import { DEFAULT_HOOK_SHELL } from './shell/shellProvider.js'
|
import { DEFAULT_HOOK_SHELL } from './shell/shellProvider.js'
|
||||||
import { buildPowerShellArgs } from './shell/powershellProvider.js'
|
import { buildPowerShellArgs } from './shell/powershellProvider.js'
|
||||||
@ -787,9 +790,20 @@ async function execCommandHook(
|
|||||||
// PowerShell path deliberately skips the Windows-specific bash
|
// PowerShell path deliberately skips the Windows-specific bash
|
||||||
// accommodations (cygpath conversion, .sh auto-prepend, POSIX-quoted
|
// accommodations (cygpath conversion, .sh auto-prepend, POSIX-quoted
|
||||||
// SHELL_PREFIX).
|
// 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.
|
// 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
|
// PowerShell expects Windows paths on Windows (and native paths on
|
||||||
// Unix where pwsh is also available).
|
// Unix where pwsh is also available).
|
||||||
const toHookPath =
|
const toHookPath =
|
||||||
isWindows && !isPowerShell
|
isWindows && !usesPowerShell
|
||||||
? (p: string) => windowsPathToPosixPath(p)
|
? (p: string) => windowsPathToPosixPath(p)
|
||||||
: (p: string) => p
|
: (p: string) => p
|
||||||
|
|
||||||
@ -859,7 +873,7 @@ async function execCommandHook(
|
|||||||
// On Windows (bash only), auto-prepend `bash` for .sh scripts so they
|
// On Windows (bash only), auto-prepend `bash` for .sh scripts so they
|
||||||
// execute instead of opening in the default file handler. PowerShell
|
// execute instead of opening in the default file handler. PowerShell
|
||||||
// runs .ps1 files natively — no prepend needed.
|
// 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 ')) {
|
if (!command.trim().startsWith('bash ')) {
|
||||||
command = `bash ${command}`
|
command = `bash ${command}`
|
||||||
}
|
}
|
||||||
@ -870,7 +884,7 @@ async function execCommandHook(
|
|||||||
// PowerShell — see design §8.1. For now PS hooks ignore the prefix;
|
// 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.
|
// a CLAUDE_CODE_PS_SHELL_PREFIX (or shell-aware prefix) is a follow-up.
|
||||||
const finalCommand =
|
const finalCommand =
|
||||||
!isPowerShell && process.env.CLAUDE_CODE_SHELL_PREFIX
|
!usesPowerShell && process.env.CLAUDE_CODE_SHELL_PREFIX
|
||||||
? formatShellPrefixCommand(process.env.CLAUDE_CODE_SHELL_PREFIX, command)
|
? formatShellPrefixCommand(process.env.CLAUDE_CODE_SHELL_PREFIX, command)
|
||||||
: command
|
: command
|
||||||
|
|
||||||
@ -915,7 +929,7 @@ async function execCommandHook(
|
|||||||
// Skip for PS — consistent with how .sh prepend and SHELL_PREFIX are
|
// Skip for PS — consistent with how .sh prepend and SHELL_PREFIX are
|
||||||
// already bash-only above.
|
// already bash-only above.
|
||||||
if (
|
if (
|
||||||
!isPowerShell &&
|
!usesPowerShell &&
|
||||||
(hookEvent === 'SessionStart' ||
|
(hookEvent === 'SessionStart' ||
|
||||||
hookEvent === 'Setup' ||
|
hookEvent === 'Setup' ||
|
||||||
hookEvent === 'CwdChanged' ||
|
hookEvent === 'CwdChanged' ||
|
||||||
@ -948,12 +962,9 @@ async function execCommandHook(
|
|||||||
// skips user profile scripts (faster, deterministic).
|
// skips user profile scripts (faster, deterministic).
|
||||||
// -NonInteractive fails fast instead of prompting.
|
// -NonInteractive fails fast instead of prompting.
|
||||||
//
|
//
|
||||||
// The Git Bash hard-exit in findGitBashPath() is still in place for
|
// On Windows, default-shell hooks now degrade to PowerShell when Git Bash
|
||||||
// bash hooks. PowerShell hooks never call it, so a Windows user with
|
// is unavailable. Explicit bash hooks still surface an actionable error,
|
||||||
// only pwsh and shell: 'powershell' on every hook could in theory run
|
// but they no longer hard-exit the entire process.
|
||||||
// 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).
|
|
||||||
let child: ChildProcessWithoutNullStreams
|
let child: ChildProcessWithoutNullStreams
|
||||||
if (shellType === 'powershell') {
|
if (shellType === 'powershell') {
|
||||||
const pwshPath = await getCachedPowerShellPath()
|
const pwshPath = await getCachedPowerShellPath()
|
||||||
@ -973,7 +984,14 @@ async function execCommandHook(
|
|||||||
} else {
|
} else {
|
||||||
// On Windows, use Git Bash explicitly (cmd.exe can't run bash syntax).
|
// On Windows, use Git Bash explicitly (cmd.exe can't run bash syntax).
|
||||||
// On other platforms, shell: true uses /bin/sh.
|
// 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, [], {
|
child = spawn(finalCommand, [], {
|
||||||
env: envVars,
|
env: envVars,
|
||||||
cwd: safeCwd,
|
cwd: safeCwd,
|
||||||
|
|||||||
@ -86,26 +86,31 @@ function findExecutable(executable: string): string | null {
|
|||||||
*/
|
*/
|
||||||
export function setShellIfWindows(): void {
|
export function setShellIfWindows(): void {
|
||||||
if (getPlatform() === 'windows') {
|
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
|
process.env.SHELL = gitBashPath
|
||||||
logForDebugging(`Using 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.
|
* 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 (process.env.CLAUDE_CODE_GIT_BASH_PATH) {
|
||||||
if (checkPathExists(process.env.CLAUDE_CODE_GIT_BASH_PATH)) {
|
if (checkPathExists(process.env.CLAUDE_CODE_GIT_BASH_PATH)) {
|
||||||
return process.env.CLAUDE_CODE_GIT_BASH_PATH
|
return process.env.CLAUDE_CODE_GIT_BASH_PATH
|
||||||
}
|
}
|
||||||
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
return null
|
||||||
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')
|
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
|
// biome-ignore lint/suspicious/noConsole:: intentional console output
|
||||||
console.error(
|
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',
|
'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