diff --git a/.changeset/fix-windows-git-bash-path-detection.md b/.changeset/fix-windows-git-bash-path-detection.md new file mode 100644 index 00000000..8f1ae647 --- /dev/null +++ b/.changeset/fix-windows-git-bash-path-detection.md @@ -0,0 +1,6 @@ +--- +"@moonshot-ai/kimi-code": patch +"@moonshot-ai/kimi-code-sdk": patch +--- + +Fix Git Bash path detection on Windows by also searching `usr\bin\bash.exe` locations, which is where bash lives in many Git for Windows installations where `bin\bash.exe` does not exist. \ No newline at end of file diff --git a/packages/kaos/src/environment.ts b/packages/kaos/src/environment.ts index ff00a405..84cb3998 100644 --- a/packages/kaos/src/environment.ts +++ b/packages/kaos/src/environment.ts @@ -95,20 +95,25 @@ async function locateWindowsGitBash(deps: EnvironmentDeps): Promise { if (gitExe !== undefined) { const inferred = inferGitBashFromGitExe(gitExe); if (inferred !== undefined) { - checked.push(inferred); - if (await deps.isFile(inferred)) { - return inferred; + for (const path of inferred) { + checked.push(path); + if (await deps.isFile(path)) { + return path; + } } } } const candidates: string[] = [ 'C:\\Program Files\\Git\\bin\\bash.exe', + 'C:\\Program Files\\Git\\usr\\bin\\bash.exe', 'C:\\Program Files (x86)\\Git\\bin\\bash.exe', + 'C:\\Program Files (x86)\\Git\\usr\\bin\\bash.exe', ]; const localAppData = deps.env['LOCALAPPDATA']?.trim(); if (localAppData !== undefined && localAppData.length > 0) { candidates.push(`${localAppData}\\Programs\\Git\\bin\\bash.exe`); + candidates.push(`${localAppData}\\Programs\\Git\\usr\\bin\\bash.exe`); } for (const candidate of candidates) { checked.push(candidate); @@ -123,17 +128,18 @@ async function locateWindowsGitBash(deps: EnvironmentDeps): Promise { } // Most Git for Windows installs put `git.exe` in `\cmd\git.exe`, -// with bash at `\bin\bash.exe`. Portable installs sometimes put -// both in `\bin\`. Walk back to the parent of `cmd` / `bin` and -// re-anchor under `bin\bash.exe`. -function inferGitBashFromGitExe(gitExe: string): string | undefined { +// with bash at `\bin\bash.exe` (a wrapper) or `\usr\bin\bash.exe` +// (the real MSYS2 shell). Walk back to the parent of `cmd` / `bin` and +// return both candidates so the caller can try them in preference order. +function inferGitBashFromGitExe(gitExe: string): string[] | undefined { const sep = gitExe.includes('\\') ? '\\' : '/'; const parts = gitExe.split(sep); for (let i = parts.length - 2; i >= 0; i -= 1) { const segment = parts[i]; if (segment === 'cmd' || segment === 'bin') { const root = parts.slice(0, i).join(sep); - return root.length === 0 ? `bin${sep}bash.exe` : `${root}${sep}bin${sep}bash.exe`; + const prefix = root.length === 0 ? '' : `${root}${sep}`; + return [`${prefix}bin${sep}bash.exe`, `${prefix}usr${sep}bin${sep}bash.exe`]; } } return undefined; diff --git a/packages/kaos/test/environment.test.ts b/packages/kaos/test/environment.test.ts index 1d0e01d2..2f020ee5 100644 --- a/packages/kaos/test/environment.test.ts +++ b/packages/kaos/test/environment.test.ts @@ -157,6 +157,18 @@ describe('detectEnvironment', () => { expect(env.shellPath).toBe('C:\\Program Files\\Git\\bin\\bash.exe'); }); + it('infers Git Bash from usr/bin when bin/bash.exe is missing', async () => { + const env = await detectEnvironment( + stubDeps({ + platform: 'win32', + executables: { 'git.exe': 'D:\\Program Files\\Git\\cmd\\git.exe' }, + existingPaths: ['D:\\Program Files\\Git\\usr\\bin\\bash.exe'], + }), + ); + expect(env.shellName).toBe('bash'); + expect(env.shellPath).toBe('D:\\Program Files\\Git\\usr\\bin\\bash.exe'); + }); + it('falls back to the well-known Program Files install location', async () => { const env = await detectEnvironment( stubDeps({ @@ -167,6 +179,16 @@ describe('detectEnvironment', () => { expect(env.shellPath).toBe('C:\\Program Files\\Git\\bin\\bash.exe'); }); + it('falls back to usr/bin under Program Files when bin/bash.exe is missing', async () => { + const env = await detectEnvironment( + stubDeps({ + platform: 'win32', + existingPaths: ['C:\\Program Files\\Git\\usr\\bin\\bash.exe'], + }), + ); + expect(env.shellPath).toBe('C:\\Program Files\\Git\\usr\\bin\\bash.exe'); + }); + it('falls back to LOCALAPPDATA install when present', async () => { const env = await detectEnvironment( stubDeps({ @@ -178,6 +200,17 @@ describe('detectEnvironment', () => { expect(env.shellPath).toBe('C:\\Users\\me\\AppData\\Local\\Programs\\Git\\bin\\bash.exe'); }); +it('falls back to usr/bin under LOCALAPPDATA when bin/bash.exe is missing', async () => { + const env = await detectEnvironment( + stubDeps({ + platform: 'win32', + env: { LOCALAPPDATA: 'C:\\Users\\me\\AppData\\Local' }, + existingPaths: ['C:\\Users\\me\\AppData\\Local\\Programs\\Git\\usr\\bin\\bash.exe'], + }), + ); + expect(env.shellPath).toBe('C:\\Users\\me\\AppData\\Local\\Programs\\Git\\usr\\bin\\bash.exe'); + }); + it('throws KaosShellNotFoundError when no Git Bash candidate is found', async () => { const error = await detectEnvironment( stubDeps({ @@ -209,6 +242,7 @@ describe('detectEnvironment', () => { ); expect(error.message).toContain('D:\\custom\\bash.exe'); expect(error.message).toContain('C:\\Program Files\\Git\\bin\\bash.exe'); + expect(error.message).toContain('C:\\Program Files\\Git\\usr\\bin\\bash.exe'); }); // ── arch / version passthrough ─────────────────────────────────────