fix(codex): harden windows light install

This commit is contained in:
YeonGyu-Kim
2026-05-31 13:23:24 +09:00
parent 4e31d7df5d
commit 92bad87d84
42 changed files with 1675 additions and 23 deletions
+5
View File
@@ -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
View File
@@ -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",
+1
View File
@@ -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/);
});