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 { installCachedPlugin, linkCachedPluginBins, pruneMarketplaceCache, pruneMarketplacePluginCaches } from "./codex-cache"
|
||||||
import { updateCodexConfig } from "./codex-config-toml"
|
import { updateCodexConfig } from "./codex-config-toml"
|
||||||
import { trustedHookStatesForPlugin } from "./codex-hook-trust"
|
import { trustedHookStatesForPlugin } from "./codex-hook-trust"
|
||||||
|
import { resolveGitBashForCurrentProcess } from "./git-bash"
|
||||||
import { linkCachedPluginAgents } from "./link-cached-plugin-agents"
|
import { linkCachedPluginAgents } from "./link-cached-plugin-agents"
|
||||||
import { readMarketplace, readPluginManifest, resolvePluginSource, validatePathSegment } from "./codex-marketplace"
|
import { readMarketplace, readPluginManifest, resolvePluginSource, validatePathSegment } from "./codex-marketplace"
|
||||||
import { writeInstalledMarketplaceSnapshot, type MarketplaceSnapshotPluginSource } from "./codex-marketplace-snapshot"
|
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
|
const SISYPHUS_LEGACY_CACHE_MARKETPLACES = ["lazycodex", "code-yeongyu-codex-plugins"] as const
|
||||||
|
|
||||||
export async function runCodexInstaller(options: CodexInstallOptions = {}): Promise<CodexInstallResult> {
|
export async function runCodexInstaller(options: CodexInstallOptions = {}): Promise<CodexInstallResult> {
|
||||||
const repoRoot = resolve(options.repoRoot ?? findRepoRoot({ importerDir: import.meta.dir, env: process.env }))
|
const env = options.env ?? process.env
|
||||||
const codexHome = resolve(options.codexHome ?? process.env.CODEX_HOME ?? join(homedir(), ".codex"))
|
const platform = options.platform ?? process.platform
|
||||||
const binDir = resolveCodexInstallerBinDir({ binDir: options.binDir, codexHome, env: process.env })
|
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 runCommand = options.runCommand ?? defaultRunCommand
|
||||||
const log = options.log ?? (() => undefined)
|
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 codexPackageRoot = join(repoRoot, "packages", "omo-codex")
|
||||||
const marketplace = await readMarketplace(repoRoot, {
|
const marketplace = await readMarketplace(repoRoot, {
|
||||||
marketplacePath: join(codexPackageRoot, "marketplace.json"),
|
marketplacePath: join(codexPackageRoot, "marketplace.json"),
|
||||||
@@ -125,6 +135,7 @@ export async function runCodexInstaller(options: CodexInstallOptions = {}): Prom
|
|||||||
installed,
|
installed,
|
||||||
configPath,
|
configPath,
|
||||||
codexHome,
|
codexHome,
|
||||||
|
gitBashPath: gitBashResolution.path,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,10 +56,29 @@ export type RunCommand = (
|
|||||||
options: CommandRunOptions,
|
options: CommandRunOptions,
|
||||||
) => Promise<void>
|
) => 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 {
|
export interface CodexInstallOptions {
|
||||||
readonly codexHome?: string
|
readonly codexHome?: string
|
||||||
readonly binDir?: string
|
readonly binDir?: string
|
||||||
readonly repoRoot?: string
|
readonly repoRoot?: string
|
||||||
|
readonly platform?: CodexInstallPlatform
|
||||||
|
readonly env?: { readonly [key: string]: string | undefined }
|
||||||
|
readonly gitBashResolver?: GitBashResolver
|
||||||
readonly autonomousPermissions?: boolean
|
readonly autonomousPermissions?: boolean
|
||||||
readonly runCommand?: RunCommand
|
readonly runCommand?: RunCommand
|
||||||
readonly log?: (message: string) => void
|
readonly log?: (message: string) => void
|
||||||
@@ -70,4 +89,5 @@ export interface CodexInstallResult {
|
|||||||
readonly installed: readonly InstalledPlugin[]
|
readonly installed: readonly InstalledPlugin[]
|
||||||
readonly configPath: string
|
readonly configPath: string
|
||||||
readonly codexHome: string
|
readonly codexHome: string
|
||||||
|
readonly gitBashPath: string | null
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user