From 644be5df7a3ecbfbbf5bdef0e1ce9933e1f70427 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sun, 31 May 2026 06:03:59 +0900 Subject: [PATCH] feat(codex): inject windows git bash guidance Plan: plans/codex-windows-git-bash-profile.md --- .../rules/bundled-rules/windows-git-bash.md | 10 ++ .../rules/test/package-smoke.test.ts | 4 +- .../windows-git-bash-bundled-rule.test.ts | 97 +++++++++++++++++++ 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md create mode 100644 packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts diff --git a/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md b/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md new file mode 100644 index 000000000..71b2f2635 --- /dev/null +++ b/packages/omo-codex/plugin/components/rules/bundled-rules/windows-git-bash.md @@ -0,0 +1,10 @@ +--- +description: Windows Git Bash guidance for Codex +alwaysApply: true +--- + +On Windows native Codex sessions, prefer Git Bash for shell commands. + +Use `shell: "bash"` when `bash.exe` is on PATH. Otherwise use the absolute Git Bash path from `OMO_CODEX_GIT_BASH_PATH` or `C:\Program Files\Git\bin\bash.exe`. + +Use PowerShell only for Windows-native operations that need PowerShell. diff --git a/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts b/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts index bd0b96351..afa67eecd 100644 --- a/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts +++ b/packages/omo-codex/plugin/components/rules/test/package-smoke.test.ts @@ -1,4 +1,4 @@ -import { readFileSync } from "node:fs"; +import { readdirSync, readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; type PackageJson = { @@ -51,6 +51,7 @@ describe("plugin package metadata", () => { const pluginJson = readPluginJson(".codex-plugin/plugin.json"); const hooksJson = readHooksJson("hooks/hooks.json"); const cliSource = readFileSync("src/cli.ts", "utf8"); + const bundledRules = readdirSync("bundled-rules").sort(); // when const hookConfig = hooksJson.hooks; @@ -69,6 +70,7 @@ describe("plugin package metadata", () => { expect(packageJson.dependencies ?? {}).toEqual({ picomatch: "^4.0.3" }); expect(packageJson.bin["omo-rules"]).toBe("./dist/cli.js"); expect(packageJson.files).toContain("bundled-rules"); + expect(bundledRules).toContain("windows-git-bash.md"); expect(pluginJson.hooks).toBe("./hooks/hooks.json"); expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true); expect(commands).toEqual([ diff --git a/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts b/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts new file mode 100644 index 000000000..4b43305e2 --- /dev/null +++ b/packages/omo-codex/plugin/components/rules/test/windows-git-bash-bundled-rule.test.ts @@ -0,0 +1,97 @@ +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; + +import { runSessionStartHook, type CodexSessionStartInput } from "../src/codex-hook.js"; +import { findPluginBundledCandidates } from "../src/rules/finder.js"; + +const WINDOWS_RULE_DESCRIPTION = "Windows Git Bash guidance for Codex"; +const WINDOWS_RULE_PATH = "bundled-rules/windows-git-bash.md"; +const WINDOWS_GUIDANCE = "On Windows native Codex sessions, prefer Git Bash for shell commands."; +const BUNDLED_ONLY_ENV = { + CODEX_RULES_ENABLED_SOURCES: "plugin-bundled", +}; +const PROJECT_AND_BUNDLED_ENV = { + CODEX_RULES_ENABLED_SOURCES: ".omo/rules,plugin-bundled", +}; +const tempDirectories: string[] = []; +let originalPluginRoot: string | undefined; + +afterEach(() => { + restoreEnv("PLUGIN_ROOT", originalPluginRoot); + for (const directory of tempDirectories.splice(0)) { + rmSync(directory, { recursive: true, force: true }); + } +}); + +function makeProject(): { readonly root: string; readonly pluginData: string } { + originalPluginRoot = process.env["PLUGIN_ROOT"]; + process.env["PLUGIN_ROOT"] = process.cwd(); + const root = mkdtempSync(join(tmpdir(), "codex-rules-windows-git-bash-project-")); + const pluginData = mkdtempSync(join(tmpdir(), "codex-rules-windows-git-bash-data-")); + tempDirectories.push(root, pluginData); + writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture" })); + return { root, pluginData }; +} + +function sessionStartInput(root: string): CodexSessionStartInput { + return { + session_id: "session-1", + transcript_path: null, + cwd: root, + hook_event_name: "SessionStart", + model: "gpt-5.5", + permission_mode: "default", + source: "startup", + }; +} + +function restoreEnv(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + return; + } + process.env[name] = value; +} + +function occurrenceCount(value: string, search: string): number { + return value.split(search).length - 1; +} + +describe("Windows Git Bash bundled rule", () => { + it("#given packaged bundled rules #when discovering plugin-bundled candidates #then Windows Git Bash rule is included", () => { + const candidates = findPluginBundledCandidates({ pluginRoot: process.cwd() }); + + expect(candidates.map((candidate) => candidate.relativePath)).toContain(WINDOWS_RULE_PATH); + }); + + it("#given bundled rules enabled #when SessionStart runs #then Windows Git Bash guidance is injected once", async () => { + const { root, pluginData } = makeProject(); + + const output = await runSessionStartHook(sessionStartInput(root), { + pluginDataRoot: pluginData, + env: BUNDLED_ONLY_ENV, + }); + + expect(occurrenceCount(output, WINDOWS_GUIDANCE)).toBe(1); + }); + + it("#given project rule with same description #when static rules load #then project guidance overrides bundled guidance", async () => { + const { root, pluginData } = makeProject(); + const projectGuidance = "Project-specific Windows shell policy."; + mkdirSync(join(root, ".omo", "rules"), { recursive: true }); + writeFileSync( + join(root, ".omo", "rules", "windows-git-bash.md"), + ["---", `description: ${WINDOWS_RULE_DESCRIPTION}`, "alwaysApply: true", "---", "", projectGuidance].join("\n"), + ); + + const output = await runSessionStartHook(sessionStartInput(root), { + pluginDataRoot: pluginData, + env: PROJECT_AND_BUNDLED_ENV, + }); + + expect(output).toContain(projectGuidance); + expect(output).not.toContain(WINDOWS_GUIDANCE); + }); +});