fix(codex): harden windows light install
This commit is contained in:
@@ -5,6 +5,11 @@
|
||||
"args": ["../../ast-grep-mcp/dist/cli.js", "mcp"],
|
||||
"cwd": "."
|
||||
},
|
||||
"git_bash": {
|
||||
"command": "node",
|
||||
"args": ["../../git-bash-mcp/dist/cli.js", "mcp"],
|
||||
"cwd": "."
|
||||
},
|
||||
"lsp": {
|
||||
"command": "node",
|
||||
"args": ["../../lsp-tools-mcp/dist/cli.js", "mcp"],
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "^Bash$",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook pre-tool-use",
|
||||
"timeout": 5,
|
||||
"statusMessage": "LazyCodex(0.1.0): Recommending Git Bash Mcp"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"PostCompact": [
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook post-compact",
|
||||
"timeout": 5,
|
||||
"statusMessage": "LazyCodex(0.1.0): Resetting Git Bash Mcp Reminder"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "@sisyphuslabs/codex-git-bash-hook",
|
||||
"version": "0.1.0",
|
||||
"description": "Codex hook component that reminds Windows sessions to prefer the OMO git_bash MCP.",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"bin": {
|
||||
"omo-git-bash-hook": "./dist/cli.js"
|
||||
},
|
||||
"files": ["dist", "hooks"],
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"test": "bun test test/*.test.ts",
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.7.0",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
import { runGitBashHookCli } from "./codex-hook.js";
|
||||
|
||||
const TOP_LEVEL_HELP =
|
||||
"Usage:\n omo-git-bash-hook hook pre-tool-use\n omo-git-bash-hook hook post-compact\n omo-git-bash-hook help | --help | -h\n";
|
||||
|
||||
async function main(): Promise<number> {
|
||||
const argv = process.argv.slice(2);
|
||||
const command = argv[0];
|
||||
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
||||
process.stdout.write(TOP_LEVEL_HELP);
|
||||
return 0;
|
||||
}
|
||||
if (command === "hook" && argv[1] === "pre-tool-use") {
|
||||
await runGitBashHookCli(process.stdin, process.stdout, "pre-tool-use");
|
||||
return 0;
|
||||
}
|
||||
if (command === "hook" && argv[1] === "post-compact") {
|
||||
await runGitBashHookCli(process.stdin, process.stdout, "post-compact");
|
||||
return 0;
|
||||
}
|
||||
process.stderr.write(`[omo-git-bash-hook] unknown command: ${argv.join(" ")}\n${TOP_LEVEL_HELP}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
main()
|
||||
.then((code) => {
|
||||
process.exit(code);
|
||||
})
|
||||
.catch((error: unknown) => {
|
||||
process.stderr.write(`[omo-git-bash-hook] ${error instanceof Error ? error.message : String(error)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,180 @@
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
|
||||
export interface PreToolUsePayload {
|
||||
readonly cwd: string;
|
||||
readonly hook_event_name: "PreToolUse";
|
||||
readonly model: string;
|
||||
readonly permission_mode: string;
|
||||
readonly session_id: string;
|
||||
readonly tool_input: unknown;
|
||||
readonly tool_name: string;
|
||||
readonly tool_use_id: string;
|
||||
readonly transcript_path: string | null;
|
||||
readonly turn_id: string;
|
||||
}
|
||||
|
||||
export interface GitBashHookOptions {
|
||||
readonly env?: NodeJS.ProcessEnv;
|
||||
readonly platform?: NodeJS.Platform | string;
|
||||
readonly pluginDataRoot?: string;
|
||||
}
|
||||
|
||||
export interface PostCompactPayload {
|
||||
readonly hook_event_name: "PostCompact";
|
||||
readonly session_id: string;
|
||||
readonly transcript_path?: string | null;
|
||||
readonly trigger?: string;
|
||||
}
|
||||
|
||||
interface PreToolUseHookOutput {
|
||||
readonly hookSpecificOutput: {
|
||||
readonly hookEventName: "PreToolUse";
|
||||
readonly additionalContext: string;
|
||||
};
|
||||
}
|
||||
|
||||
const BASH_TOOL_NAME = "Bash";
|
||||
const REMINDER =
|
||||
"On Windows, prefer the OMO git_bash MCP for shell commands before using built-in exec_command. Use exec_command only when git_bash is unavailable or for non-shell operations.";
|
||||
|
||||
export function parsePreToolUsePayload(raw: string): PreToolUsePayload | null {
|
||||
if (raw.trim().length === 0) return null;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
return isPreToolUsePayload(parsed) ? parsed : null;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) return null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function parsePostCompactPayload(raw: string): PostCompactPayload | null {
|
||||
if (raw.trim().length === 0) return null;
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(raw);
|
||||
return isPostCompactPayload(parsed) ? parsed : null;
|
||||
} catch (error) {
|
||||
if (error instanceof SyntaxError) return null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyGitBashPreToolUseReminder(payload: PreToolUsePayload, options: GitBashHookOptions = {}): string {
|
||||
if (payload.hook_event_name !== "PreToolUse") return "";
|
||||
if (payload.tool_name !== BASH_TOOL_NAME) return "";
|
||||
if (!isWindowsHost(options)) return "";
|
||||
|
||||
const markerPath = reminderMarkerPath(payload.session_id, options.pluginDataRoot);
|
||||
if (hasReminderMarker(markerPath)) return "";
|
||||
mkdirSync(dirname(markerPath), { recursive: true });
|
||||
writeFileSync(markerPath, `${new Date().toISOString()}\n`);
|
||||
|
||||
const output: PreToolUseHookOutput = {
|
||||
hookSpecificOutput: {
|
||||
hookEventName: "PreToolUse",
|
||||
additionalContext: REMINDER,
|
||||
},
|
||||
};
|
||||
return `${JSON.stringify(output)}\n`;
|
||||
}
|
||||
|
||||
export function applyGitBashPostCompactReset(payload: PostCompactPayload, options: GitBashHookOptions = {}): string {
|
||||
if (payload.hook_event_name !== "PostCompact") return "";
|
||||
rmSync(reminderMarkerPath(payload.session_id, options.pluginDataRoot), { force: true });
|
||||
return "";
|
||||
}
|
||||
|
||||
export async function runGitBashHookCli(
|
||||
stdin: NodeJS.ReadableStream,
|
||||
stdout: NodeJS.WritableStream,
|
||||
eventName: "pre-tool-use" | "post-compact" = "pre-tool-use",
|
||||
options: GitBashHookOptions = {},
|
||||
): Promise<void> {
|
||||
try {
|
||||
const raw = await readAll(stdin);
|
||||
const output =
|
||||
eventName === "post-compact" ? postCompactOutput(raw, options) : preToolUseOutput(raw, options);
|
||||
if (output.length > 0) stdout.write(output);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function preToolUseOutput(raw: string, options: GitBashHookOptions): string {
|
||||
const payload = parsePreToolUsePayload(raw);
|
||||
if (payload === null) return "";
|
||||
return applyGitBashPreToolUseReminder(payload, options);
|
||||
}
|
||||
|
||||
function postCompactOutput(raw: string, options: GitBashHookOptions): string {
|
||||
const payload = parsePostCompactPayload(raw);
|
||||
if (payload === null) return "";
|
||||
return applyGitBashPostCompactReset(payload, options);
|
||||
}
|
||||
|
||||
function isWindowsHost(options: GitBashHookOptions): boolean {
|
||||
const platform = options.platform ?? process.platform;
|
||||
if (platform === "win32") return true;
|
||||
const env = options.env ?? process.env;
|
||||
return env["OS"] === "Windows_NT" || env["ComSpec"] !== undefined || env["SystemRoot"] !== undefined;
|
||||
}
|
||||
|
||||
function hasReminderMarker(path: string): boolean {
|
||||
return existsSync(path);
|
||||
}
|
||||
|
||||
function reminderMarkerPath(sessionId: string, pluginDataRoot?: string): string {
|
||||
const root = pluginDataRoot ?? process.env["PLUGIN_DATA"] ?? join(homedir(), ".codex", "omo-git-bash");
|
||||
return join(root, "git-bash-reminder", `${safePathSegment(sessionId)}.seen`);
|
||||
}
|
||||
|
||||
function safePathSegment(value: string): string {
|
||||
return value.replace(/[^A-Za-z0-9._-]/g, "_");
|
||||
}
|
||||
|
||||
function isPreToolUsePayload(value: unknown): value is PreToolUsePayload {
|
||||
if (!isRecord(value)) return false;
|
||||
return (
|
||||
value["hook_event_name"] === "PreToolUse" &&
|
||||
typeof value["cwd"] === "string" &&
|
||||
typeof value["model"] === "string" &&
|
||||
typeof value["permission_mode"] === "string" &&
|
||||
typeof value["session_id"] === "string" &&
|
||||
typeof value["tool_name"] === "string" &&
|
||||
typeof value["tool_use_id"] === "string" &&
|
||||
(value["transcript_path"] === null || typeof value["transcript_path"] === "string") &&
|
||||
typeof value["turn_id"] === "string" &&
|
||||
Object.hasOwn(value, "tool_input")
|
||||
);
|
||||
}
|
||||
|
||||
function isPostCompactPayload(value: unknown): value is PostCompactPayload {
|
||||
if (!isRecord(value)) return false;
|
||||
return (
|
||||
value["hook_event_name"] === "PostCompact" &&
|
||||
typeof value["session_id"] === "string" &&
|
||||
(value["transcript_path"] === undefined ||
|
||||
value["transcript_path"] === null ||
|
||||
typeof value["transcript_path"] === "string") &&
|
||||
(value["trigger"] === undefined || typeof value["trigger"] === "string")
|
||||
);
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function readAll(stdin: NodeJS.ReadableStream): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = "";
|
||||
stdin.setEncoding("utf8");
|
||||
stdin.on("data", (chunk: unknown) => {
|
||||
data += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
||||
});
|
||||
stdin.once("error", reject);
|
||||
stdin.once("end", () => resolve(data));
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export {
|
||||
applyGitBashPostCompactReset,
|
||||
applyGitBashPreToolUseReminder,
|
||||
parsePostCompactPayload,
|
||||
parsePreToolUsePayload,
|
||||
runGitBashHookCli,
|
||||
type GitBashHookOptions,
|
||||
type PostCompactPayload,
|
||||
type PreToolUsePayload,
|
||||
} from "./codex-hook.js";
|
||||
@@ -0,0 +1,195 @@
|
||||
import { afterEach, describe, expect, it } from "bun:test";
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
|
||||
import {
|
||||
applyGitBashPostCompactReset,
|
||||
applyGitBashPreToolUseReminder,
|
||||
runGitBashHookCli,
|
||||
type PostCompactPayload,
|
||||
type PreToolUsePayload,
|
||||
} from "../src/codex-hook.js";
|
||||
|
||||
const temporaryDirectories: string[] = [];
|
||||
|
||||
function createTemporaryDirectory(prefix: string): string {
|
||||
const directory = mkdtempSync(join(tmpdir(), prefix));
|
||||
temporaryDirectories.push(directory);
|
||||
return directory;
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
for (const directory of temporaryDirectories.splice(0)) {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
function preToolPayload(toolName: string, sessionId = "session-1"): PreToolUsePayload {
|
||||
return {
|
||||
cwd: "/repo",
|
||||
hook_event_name: "PreToolUse",
|
||||
model: "gpt-5.5",
|
||||
permission_mode: "default",
|
||||
session_id: sessionId,
|
||||
tool_input: { command: "pwd" },
|
||||
tool_name: toolName,
|
||||
tool_use_id: "call-1",
|
||||
transcript_path: null,
|
||||
turn_id: "turn-1",
|
||||
};
|
||||
}
|
||||
|
||||
function postCompactPayload(sessionId = "session-1"): PostCompactPayload {
|
||||
return {
|
||||
hook_event_name: "PostCompact",
|
||||
session_id: sessionId,
|
||||
transcript_path: null,
|
||||
trigger: "manual",
|
||||
};
|
||||
}
|
||||
|
||||
function windowsEnv(): NodeJS.ProcessEnv {
|
||||
return { OS: "Windows_NT", ComSpec: "C:\\Windows\\System32\\cmd.exe" };
|
||||
}
|
||||
|
||||
function captureStdout(): { readonly stdout: Writable; readonly read: () => string } {
|
||||
let captured = "";
|
||||
const stdout = new Writable({
|
||||
write(chunk: unknown, _encoding: BufferEncoding, callback: (error?: Error | null) => void): void {
|
||||
captured += chunk instanceof Buffer ? chunk.toString() : String(chunk);
|
||||
callback();
|
||||
},
|
||||
});
|
||||
return { stdout, read: () => captured };
|
||||
}
|
||||
|
||||
describe("applyGitBashPreToolUseReminder", () => {
|
||||
it("#given first Windows Bash call #when hook runs #then emits non-blocking git_bash guidance", () => {
|
||||
// given
|
||||
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
||||
|
||||
// when
|
||||
const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
|
||||
env: windowsEnv(),
|
||||
platform: "linux",
|
||||
pluginDataRoot,
|
||||
});
|
||||
|
||||
// then
|
||||
const parsed = JSON.parse(output);
|
||||
expect(parsed.hookSpecificOutput).toEqual({
|
||||
hookEventName: "PreToolUse",
|
||||
additionalContext:
|
||||
"On Windows, prefer the OMO git_bash MCP for shell commands before using built-in exec_command. Use exec_command only when git_bash is unavailable or for non-shell operations.",
|
||||
});
|
||||
});
|
||||
|
||||
it("#given second Windows Bash call in same session #when hook runs #then it stays silent", () => {
|
||||
// given
|
||||
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
||||
const payload = preToolPayload("Bash");
|
||||
|
||||
// when
|
||||
const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
||||
const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
||||
|
||||
// then
|
||||
expect(first).toContain("git_bash");
|
||||
expect(second).toBe("");
|
||||
});
|
||||
|
||||
it("#given non-Windows Bash call #when hook runs #then it stays silent", () => {
|
||||
// given
|
||||
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
||||
|
||||
// when
|
||||
const output = applyGitBashPreToolUseReminder(preToolPayload("Bash"), {
|
||||
env: {},
|
||||
platform: "darwin",
|
||||
pluginDataRoot,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(output).toBe("");
|
||||
});
|
||||
|
||||
it("#given non-Bash tool call #when hook runs #then it stays silent", () => {
|
||||
// given
|
||||
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
||||
|
||||
// when
|
||||
const output = applyGitBashPreToolUseReminder(preToolPayload("exec_command"), {
|
||||
env: windowsEnv(),
|
||||
platform: "linux",
|
||||
pluginDataRoot,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(output).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("applyGitBashPostCompactReset", () => {
|
||||
it("#given reminder already emitted #when PostCompact runs #then next Windows Bash call emits reminder again", () => {
|
||||
// given
|
||||
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
||||
const payload = preToolPayload("Bash");
|
||||
const first = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
||||
const second = applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
||||
|
||||
// when
|
||||
applyGitBashPostCompactReset(postCompactPayload(), { pluginDataRoot });
|
||||
const afterCompact = applyGitBashPreToolUseReminder(payload, {
|
||||
env: windowsEnv(),
|
||||
platform: "linux",
|
||||
pluginDataRoot,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(first).toContain("git_bash");
|
||||
expect(second).toBe("");
|
||||
expect(afterCompact).toContain("git_bash");
|
||||
});
|
||||
});
|
||||
|
||||
describe("runGitBashHookCli", () => {
|
||||
it("#given Codex PreToolUse stdin on Windows #when CLI hook runs #then it writes reminder JSON", async () => {
|
||||
// given
|
||||
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
||||
const stdin = Readable.from([JSON.stringify(preToolPayload("Bash"))]);
|
||||
const capture = captureStdout();
|
||||
|
||||
// when
|
||||
await runGitBashHookCli(stdin, capture.stdout, "pre-tool-use", {
|
||||
env: windowsEnv(),
|
||||
platform: "linux",
|
||||
pluginDataRoot,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(capture.read()).toContain("git_bash MCP");
|
||||
});
|
||||
|
||||
it("#given PostCompact stdin #when CLI hook runs #then it resets the one-shot reminder", async () => {
|
||||
// given
|
||||
const pluginDataRoot = createTemporaryDirectory("omo-git-bash-hook-");
|
||||
const payload = preToolPayload("Bash");
|
||||
applyGitBashPreToolUseReminder(payload, { env: windowsEnv(), platform: "linux", pluginDataRoot });
|
||||
const resetStdin = Readable.from([JSON.stringify(postCompactPayload())]);
|
||||
const capture = captureStdout();
|
||||
|
||||
// when
|
||||
await runGitBashHookCli(resetStdin, capture.stdout, "post-compact", { pluginDataRoot });
|
||||
const afterCompact = applyGitBashPreToolUseReminder(payload, {
|
||||
env: windowsEnv(),
|
||||
platform: "linux",
|
||||
pluginDataRoot,
|
||||
});
|
||||
|
||||
// then
|
||||
expect(capture.read()).toBe("");
|
||||
expect(afterCompact).toContain("git_bash");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": false,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src",
|
||||
"noEmit": false,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["test/**/*"]
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "Node16",
|
||||
"moduleResolution": "Node16",
|
||||
"lib": ["ES2022"],
|
||||
"strict": true,
|
||||
"exactOptionalPropertyTypes": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noImplicitOverride": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"esModuleInterop": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node", "bun-types"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["src/**/*", "test/**/*"]
|
||||
}
|
||||
@@ -55,6 +55,17 @@
|
||||
}
|
||||
],
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "^Bash$",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${PLUGIN_ROOT}/components/git-bash/dist/cli.js\" hook pre-tool-use",
|
||||
"timeout": 5,
|
||||
"statusMessage": "LazyCodex(0.1.0): Recommending Git Bash Mcp"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "^create_goal$",
|
||||
"hooks": [
|
||||
@@ -98,6 +109,17 @@
|
||||
}
|
||||
],
|
||||
"PostCompact": [
|
||||
{
|
||||
"matcher": "manual|auto",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "node \"${PLUGIN_ROOT}/components/git-bash/dist/cli.js\" hook post-compact",
|
||||
"timeout": 5,
|
||||
"statusMessage": "LazyCodex(0.1.0): Resetting Git Bash Mcp Reminder"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"matcher": "manual|auto",
|
||||
"hooks": [
|
||||
|
||||
+19
@@ -9,6 +9,7 @@
|
||||
"version": "0.1.0",
|
||||
"workspaces": [
|
||||
"components/comment-checker",
|
||||
"components/git-bash",
|
||||
"components/rules",
|
||||
"components/lsp",
|
||||
"components/telemetry",
|
||||
@@ -61,6 +62,20 @@
|
||||
"@code-yeongyu/comment-checker": "^0.8.0"
|
||||
}
|
||||
},
|
||||
"components/git-bash": {
|
||||
"name": "@sisyphuslabs/codex-git-bash-hook",
|
||||
"version": "0.1.0",
|
||||
"bin": {
|
||||
"omo-git-bash-hook": "dist/cli.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^25.7.0",
|
||||
"typescript": "^6.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"components/lsp": {
|
||||
"name": "@code-yeongyu/codex-lsp",
|
||||
"version": "0.2.0",
|
||||
@@ -765,6 +780,10 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@sisyphuslabs/codex-git-bash-hook": {
|
||||
"resolved": "components/git-bash",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@standard-schema/spec": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"private": true,
|
||||
"workspaces": [
|
||||
"components/comment-checker",
|
||||
"components/git-bash",
|
||||
"components/rules",
|
||||
"components/lsp",
|
||||
"components/telemetry",
|
||||
|
||||
@@ -18,6 +18,11 @@ const runtimes = [
|
||||
packageRoot: join(repoPackagesRoot, "ast-grep-mcp"),
|
||||
requiredOutputs: ["dist/cli.js"],
|
||||
},
|
||||
{
|
||||
label: "git-bash-mcp",
|
||||
packageRoot: join(repoPackagesRoot, "git-bash-mcp"),
|
||||
requiredOutputs: ["dist/cli.js"],
|
||||
},
|
||||
];
|
||||
|
||||
for (const runtime of runtimes) {
|
||||
@@ -25,13 +30,18 @@ for (const runtime of runtimes) {
|
||||
}
|
||||
|
||||
function buildRuntime(runtime) {
|
||||
if (hasBundledDist(runtime)) {
|
||||
console.log(`Using bundled ${runtime.label} dist`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!existsSync(join(runtime.packageRoot, "package.json"))) {
|
||||
assertBundledDist(runtime);
|
||||
console.log(`Using bundled ${runtime.label} dist`);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = spawnSync("bun", ["run", "build"], {
|
||||
const result = spawnSync("npm", ["run", "build"], {
|
||||
cwd: runtime.packageRoot,
|
||||
stdio: "inherit",
|
||||
});
|
||||
@@ -39,6 +49,10 @@ function buildRuntime(runtime) {
|
||||
if (result.status !== 0) process.exit(result.status ?? 1);
|
||||
}
|
||||
|
||||
function hasBundledDist(runtime) {
|
||||
return runtime.requiredOutputs.every((output) => existsSync(join(runtime.packageRoot, output)));
|
||||
}
|
||||
|
||||
function assertBundledDist(runtime) {
|
||||
const missingOutputs = runtime.requiredOutputs.filter((output) => !existsSync(join(runtime.packageRoot, output)));
|
||||
if (missingOutputs.length === 0) return;
|
||||
|
||||
@@ -14,7 +14,7 @@ for (const workspace of workspaces) {
|
||||
if (typeof workspacePackageJson.scripts?.build !== "string") continue;
|
||||
|
||||
console.log(`Building ${workspace}`);
|
||||
const result = spawnSync("bun", ["run", "--cwd", workspace, "build"], {
|
||||
const result = spawnSync("npm", ["run", "--workspace", workspace, "build"], {
|
||||
cwd: root,
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
@@ -154,7 +154,7 @@ test("#given hook status messages #when inspected #then labels describe OMO resp
|
||||
assert.deepEqual(genericStatusMessages, []);
|
||||
});
|
||||
|
||||
test("#given aggregate OMO plugin is enabled #when hooks are inspected #then ulw-loop guards budgeted create_goal calls", async () => {
|
||||
test("#given aggregate OMO plugin is enabled #when hooks are inspected #then shell guidance and ulw-loop guard are registered", async () => {
|
||||
// given
|
||||
const hooks = await readJson("hooks/hooks.json");
|
||||
const text = JSON.stringify(hooks);
|
||||
@@ -163,9 +163,13 @@ test("#given aggregate OMO plugin is enabled #when hooks are inspected #then ulw
|
||||
const preToolUseGroups = hooks.hooks.PreToolUse;
|
||||
|
||||
// then
|
||||
assert.match(text, /components\/git-bash\/dist\/cli\.js/);
|
||||
assert.match(text, /Recommending Git Bash Mcp/);
|
||||
assert.match(text, /hook post-compact/);
|
||||
assert.match(text, /Resetting Git Bash Mcp Reminder/);
|
||||
assert.match(text, /components\/ulw-loop\/dist\/cli\.js/);
|
||||
assert.match(text, /hook pre-tool-use/);
|
||||
assert.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^create_goal$"]);
|
||||
assert.deepEqual(preToolUseGroups.map((group) => group.matcher), ["^Bash$", "^create_goal$"]);
|
||||
});
|
||||
|
||||
test("#given aggregate MCP config #when inspected #then code MCPs reference package runtimes without package names", async () => {
|
||||
@@ -178,17 +182,19 @@ test("#given aggregate MCP config #when inspected #then code MCPs reference pack
|
||||
// when
|
||||
const lspServer = mcp.mcpServers.lsp;
|
||||
const astGrepServer = mcp.mcpServers.ast_grep;
|
||||
const gitBashServer = mcp.mcpServers.git_bash;
|
||||
const codeMcpNames = Object.keys(mcp.mcpServers)
|
||||
.filter((name) => name === "lsp" || name === "ast_grep")
|
||||
.filter((name) => name === "lsp" || name === "ast_grep" || name === "git_bash")
|
||||
.sort();
|
||||
const componentLocalMcpSources = lspSources.filter((name) => name.startsWith("lazy-mcp") || name === "lazy-lsp-mcp.ts");
|
||||
|
||||
// then
|
||||
assert.deepEqual(codeMcpNames, ["ast_grep", "lsp"]);
|
||||
assert.deepEqual(codeMcpNames, ["ast_grep", "git_bash", "lsp"]);
|
||||
assert.equal(packageJson.workspaces.includes("components/lsp/packages/lsp-tools-mcp"), false);
|
||||
assert.equal(packageJson.workspaces.includes("components/ast-grep/packages/ast-grep-mcp"), false);
|
||||
assert.deepEqual(packageJson.dependencies, { "@oh-my-opencode/shared-skills": "file:../../shared-skills" });
|
||||
assert.match(bundledMcpBuildScript, /ast-grep-mcp/);
|
||||
assert.match(bundledMcpBuildScript, /git-bash-mcp/);
|
||||
assert.doesNotMatch(packageJson.scripts.build, /--workspaces/);
|
||||
assert.equal(lspServer.command, "node");
|
||||
assert.deepEqual(lspServer.args, ["../../lsp-tools-mcp/dist/cli.js", "mcp"]);
|
||||
@@ -196,6 +202,9 @@ test("#given aggregate MCP config #when inspected #then code MCPs reference pack
|
||||
assert.equal(astGrepServer.command, "node");
|
||||
assert.deepEqual(astGrepServer.args, ["../../ast-grep-mcp/dist/cli.js", "mcp"]);
|
||||
assert.equal(astGrepServer.cwd, ".");
|
||||
assert.equal(gitBashServer.command, "node");
|
||||
assert.deepEqual(gitBashServer.args, ["../../git-bash-mcp/dist/cli.js", "mcp"]);
|
||||
assert.equal(gitBashServer.cwd, ".");
|
||||
assert.deepEqual(componentLocalMcpSources, []);
|
||||
});
|
||||
|
||||
@@ -203,12 +212,17 @@ test("#given package-level MCP CLIs #when package metadata is inspected #then bi
|
||||
// given
|
||||
const lspPackageJson = await readJson("../../lsp-tools-mcp/package.json");
|
||||
const astGrepPackageJson = await readJson("../../ast-grep-mcp/package.json");
|
||||
const gitBashPackageJson = await readJson("../../git-bash-mcp/package.json");
|
||||
|
||||
// when
|
||||
const binNames = [...Object.keys(lspPackageJson.bin ?? {}), ...Object.keys(astGrepPackageJson.bin ?? {})].sort();
|
||||
const binNames = [
|
||||
...Object.keys(lspPackageJson.bin ?? {}),
|
||||
...Object.keys(astGrepPackageJson.bin ?? {}),
|
||||
...Object.keys(gitBashPackageJson.bin ?? {}),
|
||||
].sort();
|
||||
|
||||
// then
|
||||
assert.deepEqual(binNames, ["omo-ast-grep", "omo-lsp"]);
|
||||
assert.deepEqual(binNames, ["omo-ast-grep", "omo-git-bash", "omo-lsp"]);
|
||||
for (const name of binNames) {
|
||||
assert.match(name, /^omo-/);
|
||||
}
|
||||
@@ -252,6 +266,7 @@ test("#given component directories #when scanned #then only intentional resource
|
||||
// then
|
||||
assert.deepEqual(componentNames, [
|
||||
"comment-checker",
|
||||
"git-bash",
|
||||
"lsp",
|
||||
"rules",
|
||||
"start-work-continuation",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { dirname, join } from "node:path";
|
||||
import test from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
test("#given aggregate build scripts #when inspected #then install-time build does not invoke Bun", async () => {
|
||||
// given
|
||||
const buildComponentsScript = await readFile(join(root, "scripts", "build-components.mjs"), "utf8");
|
||||
const buildBundledMcpRuntimesScript = await readFile(join(root, "scripts", "build-bundled-mcp-runtimes.mjs"), "utf8");
|
||||
|
||||
// when
|
||||
const installTimeBuildScripts = [buildComponentsScript, buildBundledMcpRuntimesScript].join("\n");
|
||||
|
||||
// then
|
||||
assert.doesNotMatch(installTimeBuildScripts, /spawnSync\("bun"/);
|
||||
assert.doesNotMatch(installTimeBuildScripts, /\bbun\s+run\b/);
|
||||
});
|
||||
Reference in New Issue
Block a user