feat(codex): inject windows git bash guidance
Plan: plans/codex-windows-git-bash-profile.md
This commit is contained in:
@@ -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.
|
||||
@@ -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([
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user