feat(codex): mirror git bash preflight locally

Plan: plans/codex-windows-git-bash-profile.md
This commit is contained in:
YeonGyu-Kim
2026-05-31 06:03:52 +09:00
parent 2996b5775e
commit 37346a86f7
4 changed files with 247 additions and 1 deletions
@@ -0,0 +1,80 @@
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 function resolveGitBash({ platform, env, exists, where }) {
if (platform !== "win32") return { found: true, path: null, source: "not-required" };
const checkedPaths = [];
const envPath = nonEmptyEnvValue(env, GIT_BASH_ENV_KEY);
if (envPath !== undefined) {
checkedPaths.push(envPath);
if (isBashExePath(envPath) && 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" },
]) {
checkedPaths.push(candidate.path);
if (exists(candidate.path)) return { found: true, path: candidate.path, source: candidate.source };
}
for (const pathCandidate of where("bash")) {
const candidate = pathCandidate.trim();
if (candidate.length === 0) continue;
checkedPaths.push(candidate);
if (isBashExePath(candidate) && exists(candidate)) return { found: true, path: candidate, source: "path" };
}
return missingGitBash(checkedPaths);
}
export function resolveGitBashForCurrentProcess(options = {}) {
return resolveGitBash({
platform: options.platform ?? process.platform,
env: options.env ?? process.env,
exists: existsSync,
where: whereCommand,
});
}
function missingGitBash(checkedPaths) {
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, key) {
const value = env[key];
if (typeof value !== "string") return undefined;
const trimmed = value.trim();
return trimmed.length === 0 ? undefined : trimmed;
}
function isBashExePath(path) {
return path.toLowerCase().endsWith("bash.exe");
}
function whereCommand(command) {
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;
}
}
@@ -0,0 +1,70 @@
import assert from "node:assert/strict";
import test from "node:test";
import { resolveGitBash } from "./git-bash.mjs";
const programFilesGitBash = "C:\\Program Files\\Git\\bin\\bash.exe";
const programFilesX86GitBash = "C:\\Program Files (x86)\\Git\\bin\\bash.exe";
test("#given non-Windows platform #when resolving Git Bash #then no preflight is required", () => {
const result = resolveGitBash({
platform: "linux",
env: {},
exists: () => false,
where: () => [],
});
assert.deepEqual(result, { found: true, path: null, source: "not-required" });
});
test("#given Windows env override to bash.exe #when the file exists #then env path wins", () => {
const overridePath = "D:\\Tools\\Git\\bin\\bash.exe";
const result = resolveGitBash({
platform: "win32",
env: { OMO_CODEX_GIT_BASH_PATH: overridePath },
exists: (path) => path === overridePath,
where: () => [programFilesGitBash],
});
assert.deepEqual(result, { found: true, path: overridePath, source: "env" });
});
test("#given Windows standard paths are absent and PATH contains bash #when resolving #then uses where bash candidate", () => {
const pathCandidate = "E:\\Git\\bin\\bash.exe";
const result = resolveGitBash({
platform: "win32",
env: {},
exists: (path) => path === pathCandidate,
where: () => ["C:\\Windows\\System32\\bash.exe", pathCandidate],
});
assert.deepEqual(result, { found: true, path: pathCandidate, source: "path" });
});
test("#given Windows invalid env override #when resolving #then returns guidance without falling through", () => {
const overridePath = "D:\\Tools\\Git\\bin\\git.exe";
const result = resolveGitBash({
platform: "win32",
env: { OMO_CODEX_GIT_BASH_PATH: overridePath },
exists: () => true,
where: () => [programFilesGitBash],
});
assert.equal(result.found, false);
assert.deepEqual(result.checkedPaths, [overridePath]);
assert.match(result.installHint, /OMO_CODEX_GIT_BASH_PATH=C:\\path\\to\\bash\.exe/);
});
test("#given Windows without Git Bash #when resolving #then returns install guidance", () => {
const result = resolveGitBash({
platform: "win32",
env: {},
exists: () => false,
where: () => [],
});
assert.equal(result.found, false);
assert.deepEqual(result.checkedPaths, [programFilesGitBash, programFilesX86GitBash]);
assert.match(result.installHint, /winget install --id Git\.Git -e --source winget/);
assert.match(result.installHint, /rerun `bunx omo install --platform=codex`/);
});