feat(codex): detect git bash on windows

Plan: plans/codex-windows-git-bash-profile.md
This commit is contained in:
YeonGyu-Kim
2026-05-31 06:03:36 +09:00
parent c4db598834
commit 5b9ca5fa26
2 changed files with 222 additions and 0 deletions
+118
View File
@@ -0,0 +1,118 @@
/// <reference path="../../../bun-test.d.ts" />
/// <reference types="bun-types" />
import { describe, expect, test } from "bun:test"
import { resolveGitBash } from "./git-bash"
const PROGRAM_FILES_GIT_BASH = "C:\\Program Files\\Git\\bin\\bash.exe"
const PROGRAM_FILES_X86_GIT_BASH = "C:\\Program Files (x86)\\Git\\bin\\bash.exe"
describe("git-bash", () => {
test("#given non-Windows platform #when resolving Git Bash #then no preflight is required", () => {
// given / when
const result = resolveGitBash({
platform: "darwin",
env: {},
exists: () => false,
where: () => [],
})
// then
expect(result).toEqual({ found: true, path: null, source: "not-required" })
})
test("#given Windows env override to bash.exe #when the file exists #then env path wins", () => {
// given
const overridePath = "D:\\Tools\\Git\\bin\\bash.exe"
// when
const result = resolveGitBash({
platform: "win32",
env: { OMO_CODEX_GIT_BASH_PATH: overridePath },
exists: (path: string) => path === overridePath,
where: () => [PROGRAM_FILES_GIT_BASH],
})
// then
expect(result).toEqual({ found: true, path: overridePath, source: "env" })
})
test("#given Windows env override not pointing to bash.exe #when resolving #then reports invalid override and stops", () => {
// given
const overridePath = "D:\\Tools\\Git\\bin\\git.exe"
// when
const result = resolveGitBash({
platform: "win32",
env: { OMO_CODEX_GIT_BASH_PATH: overridePath },
exists: () => true,
where: () => [PROGRAM_FILES_GIT_BASH],
})
// then
expect(result.found).toBe(false)
if (result.found) return
expect(result.checkedPaths).toContain(overridePath)
expect(result.installHint).toContain("OMO_CODEX_GIT_BASH_PATH=C:\\path\\to\\bash.exe")
})
test("#given Windows standard 64-bit Git Bash exists #when resolving #then uses Program Files path", () => {
// given / when
const result = resolveGitBash({
platform: "win32",
env: {},
exists: (path: string) => path === PROGRAM_FILES_GIT_BASH,
where: () => [],
})
// then
expect(result).toEqual({ found: true, path: PROGRAM_FILES_GIT_BASH, source: "program-files" })
})
test("#given Windows standard 32-bit Git Bash exists #when resolving #then uses Program Files x86 path", () => {
// given / when
const result = resolveGitBash({
platform: "win32",
env: {},
exists: (path: string) => path === PROGRAM_FILES_X86_GIT_BASH,
where: () => [],
})
// then
expect(result).toEqual({ found: true, path: PROGRAM_FILES_X86_GIT_BASH, source: "program-files-x86" })
})
test("#given Windows bash on PATH #when standard paths are missing #then uses where bash candidate", () => {
// given
const pathCandidate = "E:\\Git\\bin\\bash.exe"
// when
const result = resolveGitBash({
platform: "win32",
env: {},
exists: (path: string) => path === pathCandidate,
where: () => ["C:\\Windows\\System32\\bash.exe", pathCandidate],
})
// then
expect(result).toEqual({ found: true, path: pathCandidate, source: "path" })
})
test("#given Windows without Git Bash #when resolving #then returns install guidance", () => {
// given / when
const result = resolveGitBash({
platform: "win32",
env: {},
exists: () => false,
where: () => [],
})
// then
expect(result.found).toBe(false)
if (result.found) return
expect(result.checkedPaths).toEqual([PROGRAM_FILES_GIT_BASH, PROGRAM_FILES_X86_GIT_BASH])
expect(result.installHint).toContain("winget install --id Git.Git -e --source winget")
expect(result.installHint).toContain("OMO_CODEX_GIT_BASH_PATH=C:\\path\\to\\bash.exe")
expect(result.installHint).toContain("rerun `bunx omo install --platform=codex`")
})
})
+104
View File
@@ -0,0 +1,104 @@
import { execFileSync } from "node:child_process"
import { existsSync } from "node:fs"
const GIT_BASH_ENV_KEY = "OMO_CODEX_GIT_BASH_PATH"
const PROGRAM_FILES_GIT_BASH = "C:\\Program Files\\Git\\bin\\bash.exe"
const PROGRAM_FILES_X86_GIT_BASH = "C:\\Program Files (x86)\\Git\\bin\\bash.exe"
export type GitBashSource = "not-required" | "env" | "program-files" | "program-files-x86" | "path"
export type GitBashResolution =
| {
readonly found: true
readonly path: string | null
readonly source: GitBashSource
}
| {
readonly found: false
readonly checkedPaths: readonly string[]
readonly installHint: string
}
export interface GitBashResolverInput {
readonly platform: string
readonly env: { readonly [key: string]: string | undefined }
readonly exists: (path: string) => boolean
readonly where: (command: "bash") => readonly string[]
}
export function resolveGitBash(input: GitBashResolverInput): GitBashResolution {
if (input.platform !== "win32") return { found: true, path: null, source: "not-required" }
const checkedPaths: string[] = []
const envPath = nonEmptyEnvValue(input.env, GIT_BASH_ENV_KEY)
if (envPath !== undefined) {
checkedPaths.push(envPath)
if (isBashExePath(envPath) && input.exists(envPath)) return { found: true, path: envPath, source: "env" }
return missingGitBash(checkedPaths)
}
for (const candidate of [
{ path: PROGRAM_FILES_GIT_BASH, source: "program-files" },
{ path: PROGRAM_FILES_X86_GIT_BASH, source: "program-files-x86" },
] as const) {
checkedPaths.push(candidate.path)
if (input.exists(candidate.path)) return { found: true, path: candidate.path, source: candidate.source }
}
for (const pathCandidate of input.where("bash")) {
const candidate = pathCandidate.trim()
if (candidate.length === 0) continue
checkedPaths.push(candidate)
if (isBashExePath(candidate) && input.exists(candidate)) return { found: true, path: candidate, source: "path" }
}
return missingGitBash(checkedPaths)
}
export function resolveGitBashForCurrentProcess(input: {
readonly platform?: string
readonly env?: { readonly [key: string]: string | undefined }
} = {}): GitBashResolution {
return resolveGitBash({
platform: input.platform ?? process.platform,
env: input.env ?? process.env,
exists: existsSync,
where: whereCommand,
})
}
function missingGitBash(checkedPaths: readonly string[]): GitBashResolution {
return {
found: false,
checkedPaths,
installHint: [
"Git Bash is required for native Windows Codex profile installs.",
"Install it with: winget install --id Git.Git -e --source winget",
`For a custom install, set ${GIT_BASH_ENV_KEY}=C:\\path\\to\\bash.exe`,
"Then rerun `bunx omo install --platform=codex`.",
].join("\n"),
}
}
function nonEmptyEnvValue(env: { readonly [key: string]: string | undefined }, key: string): string | undefined {
const value = env[key]
if (value === undefined) return undefined
const trimmed = value.trim()
return trimmed.length === 0 ? undefined : trimmed
}
function isBashExePath(path: string): boolean {
return path.toLowerCase().endsWith("bash.exe")
}
function whereCommand(command: "bash"): readonly string[] {
try {
return execFileSync("where", [command], { encoding: "utf8" })
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)
} catch (error) {
if (error instanceof Error) return []
throw error
}
}