Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/fix-windows-git-bash-path-detection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@moonshot-ai/kimi-code": patch
Comment thread
OPBR marked this conversation as resolved.
"@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.
22 changes: 14 additions & 8 deletions packages/kaos/src/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,25 @@ async function locateWindowsGitBash(deps: EnvironmentDeps): Promise<string> {
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);
Expand All @@ -123,17 +128,18 @@ async function locateWindowsGitBash(deps: EnvironmentDeps): Promise<string> {
}

// Most Git for Windows installs put `git.exe` in `<root>\cmd\git.exe`,
// with bash at `<root>\bin\bash.exe`. Portable installs sometimes put
// both in `<root>\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 `<root>\bin\bash.exe` (a wrapper) or `<root>\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;
Expand Down
34 changes: 34 additions & 0 deletions packages/kaos/test/environment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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({
Expand All @@ -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({
Expand Down Expand Up @@ -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 ─────────────────────────────────────
Expand Down
Loading