feat(codex): require git bash for windows install
Plan: plans/codex-windows-git-bash-profile.md
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
/// <reference path="../../../bun-test.d.ts" />
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { mkdtemp, readFile } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { updateCodexConfig } from "./codex-config-toml"
|
||||
|
||||
describe("codex-config shell settings", () => {
|
||||
test("#given Codex config update #when Git Bash is required by installer #then no fake shell config is written", async () => {
|
||||
// given
|
||||
const root = await mkdtemp(join(tmpdir(), "omo-codex-config-shell-"))
|
||||
const configPath = join(root, "config.toml")
|
||||
|
||||
// when
|
||||
await updateCodexConfig({
|
||||
configPath,
|
||||
repoRoot: "/repo/packages/omo-codex",
|
||||
marketplaceName: "debug",
|
||||
marketplaceSource: { sourceType: "local", source: "/repo/packages/omo-codex" },
|
||||
pluginNames: ["omo"],
|
||||
})
|
||||
|
||||
// then
|
||||
const content = await readFile(configPath, "utf8")
|
||||
expect(content).not.toContain("default_shell")
|
||||
expect(content).not.toContain("shell_path")
|
||||
expect(content).not.toContain("git_bash")
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,110 @@
|
||||
/// <reference path="../../../bun-test.d.ts" />
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { mkdtemp, readFile, stat, writeFile } from "node:fs/promises"
|
||||
import { tmpdir } from "node:os"
|
||||
import { join } from "node:path"
|
||||
import { runCodexInstaller } from "./install-codex"
|
||||
import type { CommandRunOptions } from "./types"
|
||||
|
||||
const WINDOWS_GIT_BASH_PATH = "C:\\Program Files\\Git\\bin\\bash.exe"
|
||||
|
||||
describe("install-codex Git Bash preflight", () => {
|
||||
test("#given Windows without Git Bash #when installing Codex profile #then rejects before marketplace or config mutation", async () => {
|
||||
// given
|
||||
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-missing-home-"))
|
||||
const repoRoot = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-missing-repo-"))
|
||||
const commands: string[] = []
|
||||
|
||||
// when
|
||||
const install = runCodexInstaller({
|
||||
codexHome,
|
||||
repoRoot,
|
||||
platform: "win32",
|
||||
gitBashResolver: () => ({
|
||||
found: false,
|
||||
checkedPaths: [WINDOWS_GIT_BASH_PATH],
|
||||
installHint: [
|
||||
"Git Bash is required.",
|
||||
"winget install --id Git.Git -e --source winget",
|
||||
"OMO_CODEX_GIT_BASH_PATH=C:\\path\\to\\bash.exe",
|
||||
"rerun `bunx omo install --platform=codex`",
|
||||
].join("\n"),
|
||||
}),
|
||||
runCommand: async (command: string, args: readonly string[], options: CommandRunOptions) => {
|
||||
commands.push([command, ...args, options.cwd].join(" "))
|
||||
},
|
||||
})
|
||||
|
||||
// then
|
||||
await expect(install).rejects.toThrow("winget install --id Git.Git -e --source winget")
|
||||
expect(commands).toEqual([])
|
||||
await expect(stat(join(codexHome, "config.toml"))).rejects.toThrow()
|
||||
})
|
||||
|
||||
test("#given Windows with Git Bash #when installing Codex profile #then proceeds and reports detected path", async () => {
|
||||
// given
|
||||
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-present-home-"))
|
||||
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-present-bin-"))
|
||||
|
||||
// when
|
||||
const result = await runCodexInstaller({
|
||||
codexHome,
|
||||
binDir,
|
||||
repoRoot: process.cwd(),
|
||||
platform: "win32",
|
||||
gitBashResolver: () => ({ found: true, path: WINDOWS_GIT_BASH_PATH, source: "program-files" }),
|
||||
runCommand: async () => undefined,
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result.gitBashPath).toBe(WINDOWS_GIT_BASH_PATH)
|
||||
expect(await readFile(join(codexHome, "config.toml"), "utf8")).toContain("[marketplaces.sisyphuslabs]")
|
||||
})
|
||||
|
||||
test("#given Windows env override in installer options #when no custom resolver is provided #then default resolver uses it", async () => {
|
||||
// given
|
||||
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-env-home-"))
|
||||
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-env-bin-"))
|
||||
const gitBashPath = join(await mkdtemp(join(tmpdir(), "omo-codex-git-bash-env-")), "bash.exe")
|
||||
await writeFile(gitBashPath, "")
|
||||
|
||||
// when
|
||||
const result = await runCodexInstaller({
|
||||
codexHome,
|
||||
binDir,
|
||||
repoRoot: process.cwd(),
|
||||
platform: "win32",
|
||||
env: { OMO_CODEX_GIT_BASH_PATH: gitBashPath },
|
||||
runCommand: async () => undefined,
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result.gitBashPath).toBe(gitBashPath)
|
||||
})
|
||||
|
||||
test("#given non-Windows install #when Git Bash resolver would fail #then installer keeps existing behavior", async () => {
|
||||
// given
|
||||
const codexHome = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-linux-home-"))
|
||||
const binDir = await mkdtemp(join(tmpdir(), "omo-codex-git-bash-linux-bin-"))
|
||||
|
||||
// when
|
||||
const result = await runCodexInstaller({
|
||||
codexHome,
|
||||
binDir,
|
||||
repoRoot: process.cwd(),
|
||||
platform: "linux",
|
||||
gitBashResolver: () => ({
|
||||
found: false,
|
||||
checkedPaths: [WINDOWS_GIT_BASH_PATH],
|
||||
installHint: "should not be used",
|
||||
}),
|
||||
runCommand: async () => undefined,
|
||||
})
|
||||
|
||||
// then
|
||||
expect(result.gitBashPath).toBeNull()
|
||||
expect(await readFile(join(codexHome, "config.toml"), "utf8")).toContain("[marketplaces.sisyphuslabs]")
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { mkdir, writeFile } from "node:fs/promises"
|
||||
import { installCachedPlugin, linkCachedPluginBins, pruneMarketplaceCache, pruneMarketplacePluginCaches } from "./codex-cache"
|
||||
import { updateCodexConfig } from "./codex-config-toml"
|
||||
import { trustedHookStatesForPlugin } from "./codex-hook-trust"
|
||||
import { resolveGitBashForCurrentProcess } from "./git-bash"
|
||||
import { linkCachedPluginAgents } from "./link-cached-plugin-agents"
|
||||
import { readMarketplace, readPluginManifest, resolvePluginSource, validatePathSegment } from "./codex-marketplace"
|
||||
import { writeInstalledMarketplaceSnapshot, type MarketplaceSnapshotPluginSource } from "./codex-marketplace-snapshot"
|
||||
@@ -14,12 +15,21 @@ import type { CodexInstallOptions, CodexInstallResult, CodexMarketplaceSource, I
|
||||
const SISYPHUS_LEGACY_CACHE_MARKETPLACES = ["lazycodex", "code-yeongyu-codex-plugins"] as const
|
||||
|
||||
export async function runCodexInstaller(options: CodexInstallOptions = {}): Promise<CodexInstallResult> {
|
||||
const repoRoot = resolve(options.repoRoot ?? findRepoRoot({ importerDir: import.meta.dir, env: process.env }))
|
||||
const codexHome = resolve(options.codexHome ?? process.env.CODEX_HOME ?? join(homedir(), ".codex"))
|
||||
const binDir = resolveCodexInstallerBinDir({ binDir: options.binDir, codexHome, env: process.env })
|
||||
const env = options.env ?? process.env
|
||||
const platform = options.platform ?? process.platform
|
||||
const repoRoot = resolve(options.repoRoot ?? findRepoRoot({ importerDir: import.meta.dir, env }))
|
||||
const codexHome = resolve(options.codexHome ?? env.CODEX_HOME ?? join(homedir(), ".codex"))
|
||||
const binDir = resolveCodexInstallerBinDir({ binDir: options.binDir, codexHome, env })
|
||||
const runCommand = options.runCommand ?? defaultRunCommand
|
||||
const log = options.log ?? (() => undefined)
|
||||
|
||||
const gitBashResolution = platform === "win32"
|
||||
? (options.gitBashResolver ?? (() => resolveGitBashForCurrentProcess({ platform, env })))()
|
||||
: { found: true, path: null, source: "not-required" } as const
|
||||
if (!gitBashResolution.found) {
|
||||
throw new Error(gitBashResolution.installHint)
|
||||
}
|
||||
|
||||
const codexPackageRoot = join(repoRoot, "packages", "omo-codex")
|
||||
const marketplace = await readMarketplace(repoRoot, {
|
||||
marketplacePath: join(codexPackageRoot, "marketplace.json"),
|
||||
@@ -125,6 +135,7 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom
|
||||
installed,
|
||||
configPath,
|
||||
codexHome,
|
||||
gitBashPath: gitBashResolution.path,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,10 +56,29 @@ export type RunCommand = (
|
||||
options: CommandRunOptions,
|
||||
) => Promise<void>
|
||||
|
||||
export type CodexInstallPlatform = "aix" | "android" | "darwin" | "freebsd" | "haiku" | "linux" | "openbsd" | "sunos" | "win32" | "cygwin" | "netbsd"
|
||||
|
||||
export type GitBashResolution =
|
||||
| {
|
||||
readonly found: true
|
||||
readonly path: string | null
|
||||
readonly source: "not-required" | "env" | "program-files" | "program-files-x86" | "path"
|
||||
}
|
||||
| {
|
||||
readonly found: false
|
||||
readonly checkedPaths: readonly string[]
|
||||
readonly installHint: string
|
||||
}
|
||||
|
||||
export type GitBashResolver = () => GitBashResolution
|
||||
|
||||
export interface CodexInstallOptions {
|
||||
readonly codexHome?: string
|
||||
readonly binDir?: string
|
||||
readonly repoRoot?: string
|
||||
readonly platform?: CodexInstallPlatform
|
||||
readonly env?: { readonly [key: string]: string | undefined }
|
||||
readonly gitBashResolver?: GitBashResolver
|
||||
readonly autonomousPermissions?: boolean
|
||||
readonly runCommand?: RunCommand
|
||||
readonly log?: (message: string) => void
|
||||
@@ -70,4 +89,5 @@ export interface CodexInstallResult {
|
||||
readonly installed: readonly InstalledPlugin[]
|
||||
readonly configPath: string
|
||||
readonly codexHome: string
|
||||
readonly gitBashPath: string | null
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user