From f96e4e67b4a3ab834030f9d73c9bed53a2913d01 Mon Sep 17 00:00:00 2001 From: zhangy Date: Thu, 28 May 2026 21:03:39 +0800 Subject: [PATCH] fix(kaos): search usr\bin\bash.exe paths for Git Bash on Windows Some Git for Windows installations have bash.exe only at \usr\bin\bash.exe while \bin\bash.exe does not exist. Add usr\bin\bash.exe candidates alongside each bin\bash.exe entry in both the hardcoded fallback list and the git.exe inference logic. Also add @moonshot-ai/kimi-code-sdk to the changeset since the SDK bundles agent-core code and calls detectEnvironmentFromNode() on session creation, so Windows SDK users also need this fix. --- .../fix-windows-git-bash-path-detection.md | 6 ++++ packages/kaos/src/environment.ts | 22 +++++++----- packages/kaos/test/environment.test.ts | 34 +++++++++++++++++++ 3 files changed, 54 insertions(+), 8 deletions(-) create mode 100644 .changeset/fix-windows-git-bash-path-detection.md 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 ─────────────────────────────────────