diff --git a/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.mjs b/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.mjs new file mode 100644 index 000000000..d93efc598 --- /dev/null +++ b/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.mjs @@ -0,0 +1,42 @@ +#!/usr/bin/env node +// Build the repository-level lsp-tools-mcp package used by codex-lsp. +import { execSync } from "node:child_process"; +import { existsSync, statSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const lspToolsDir = join(__dirname, "..", "..", "..", "..", "..", "lsp-tools-mcp"); +const packageJson = join(lspToolsDir, "package.json"); +const requiredOutputs = [ + join(lspToolsDir, "dist", "cli.js"), + join(lspToolsDir, "dist", "tools.js"), + join(lspToolsDir, "dist", "lsp", "manager.js"), +]; +const force = process.argv.includes("--force"); + +if (!force && isBuildFresh(packageJson, requiredOutputs)) { + process.exit(0); +} + +if (!existsSync(packageJson)) { + console.error( + `lsp-tools-mcp package metadata is missing at ${packageJson}; build packages/lsp-tools-mcp before codex-lsp`, + ); + process.exit(1); +} + +console.log("Installing repository lsp-tools-mcp dependencies..."); +execSync("npm ci", { cwd: lspToolsDir, stdio: "inherit" }); + +console.log("Building repository lsp-tools-mcp..."); +execSync("npm run build", { cwd: lspToolsDir, stdio: "inherit" }); + +console.log("Done."); + +function isBuildFresh(inputPath, outputPaths) { + if (!existsSync(inputPath)) return false; + if (outputPaths.some((path) => !existsSync(path))) return false; + const inputMtime = statSync(inputPath).mtimeMs; + return outputPaths.every((path) => statSync(path).mtimeMs >= inputMtime); +} diff --git a/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs b/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs new file mode 100644 index 000000000..6b24df49e --- /dev/null +++ b/packages/omo-codex/plugin/components/lsp/scripts/build-lsp-tools.test.mjs @@ -0,0 +1,86 @@ +import assert from "node:assert/strict"; +import { chmod, copyFile, mkdir, mkdtemp, readFile, utimes, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; +import test from "node:test"; + +async function makeFixture() { + const root = await mkdtemp(join(tmpdir(), "codex-lsp-build-")); + await mkdir(join(root, "packages", "omo-codex", "plugin", "components", "lsp", "scripts"), { recursive: true }); + await mkdir(join(root, "packages", "lsp-tools-mcp", "dist"), { recursive: true }); + await copyFile( + new URL("./build-lsp-tools.mjs", import.meta.url), + join(root, "packages", "omo-codex", "plugin", "components", "lsp", "scripts", "build-lsp-tools.mjs"), + ); + await writeFile(join(root, "packages", "lsp-tools-mcp", "package.json"), "{}\n"); + await writeFile(join(root, "packages", "lsp-tools-mcp", "dist", "cli.js"), "cli\n"); + const fakeBin = join(root, "bin"); + await mkdir(fakeBin, { recursive: true }); + const npmLog = join(root, "npm.log"); + await writeFile( + join(fakeBin, "npm"), + `#!/usr/bin/env node\nconst { appendFileSync } = require("node:fs");\nappendFileSync(${JSON.stringify(npmLog)}, process.argv.slice(2).join(" ") + "\\n");\n`, + ); + await chmod(join(fakeBin, "npm"), 0o755); + return { root, npmLog, script: join(root, "packages", "omo-codex", "plugin", "components", "lsp", "scripts", "build-lsp-tools.mjs"), fakeBin }; +} + +function runScript(script, fakeBin, args = []) { + return spawnSync(process.execPath, [script, ...args], { + encoding: "utf8", + env: { ...process.env, PATH: `${fakeBin}:${process.env.PATH ?? ""}` }, + }); +} + +test("#given dist cli exists but required hook outputs are missing #when bootstrapping #then it rebuilds", async () => { + // given + const fixture = await makeFixture(); + + // when + const result = runScript(fixture.script, fixture.fakeBin); + + // then + assert.equal(result.status, 0); + assert.match(await readFile(fixture.npmLog, "utf8"), /ci\nrun build\n/u); +}); + +test("#given package metadata is newer than required outputs #when bootstrapping #then it rebuilds", async () => { + // given + const fixture = await makeFixture(); + await mkdir(join(fixture.root, "packages", "lsp-tools-mcp", "dist", "lsp"), { recursive: true }); + await writeFile(join(fixture.root, "packages", "lsp-tools-mcp", "dist", "lsp", "manager.js"), "manager\n"); + await writeFile(join(fixture.root, "packages", "lsp-tools-mcp", "dist", "tools.js"), "tools\n"); + const older = new Date("2026-01-01T00:00:00.000Z"); + const newer = new Date("2026-01-02T00:00:00.000Z"); + for (const path of [ + join(fixture.root, "packages", "lsp-tools-mcp", "dist", "cli.js"), + join(fixture.root, "packages", "lsp-tools-mcp", "dist", "tools.js"), + join(fixture.root, "packages", "lsp-tools-mcp", "dist", "lsp", "manager.js"), + ]) { + await utimes(path, older, older); + } + await utimes(join(fixture.root, "packages", "lsp-tools-mcp", "package.json"), newer, newer); + + // when + const result = runScript(fixture.script, fixture.fakeBin); + + // then + assert.equal(result.status, 0); + assert.match(await readFile(fixture.npmLog, "utf8"), /ci\nrun build\n/u); +}); + +test("#given force flag #when bootstrapping #then it rebuilds even when dist exists", async () => { + // given + const fixture = await makeFixture(); + await mkdir(join(fixture.root, "packages", "lsp-tools-mcp", "dist", "lsp"), { recursive: true }); + await writeFile(join(fixture.root, "packages", "lsp-tools-mcp", "dist", "lsp", "manager.js"), "manager\n"); + await writeFile(join(fixture.root, "packages", "lsp-tools-mcp", "dist", "tools.js"), "tools\n"); + + // when + const result = runScript(fixture.script, fixture.fakeBin, ["--force"]); + + // then + assert.equal(result.status, 0); + assert.match(await readFile(fixture.npmLog, "utf8"), /ci\nrun build\n/u); +}); diff --git a/packages/omo-codex/plugin/components/lsp/scripts/clean-dist.mjs b/packages/omo-codex/plugin/components/lsp/scripts/clean-dist.mjs new file mode 100644 index 000000000..630de3ab9 --- /dev/null +++ b/packages/omo-codex/plugin/components/lsp/scripts/clean-dist.mjs @@ -0,0 +1,5 @@ +#!/usr/bin/env node +import { rm } from "node:fs/promises"; +import { fileURLToPath } from "node:url"; + +await rm(fileURLToPath(new URL("../dist/", import.meta.url)), { recursive: true, force: true }); diff --git a/packages/omo-codex/plugin/components/lsp/scripts/test.mjs b/packages/omo-codex/plugin/components/lsp/scripts/test.mjs new file mode 100644 index 000000000..3956f62ec --- /dev/null +++ b/packages/omo-codex/plugin/components/lsp/scripts/test.mjs @@ -0,0 +1,8 @@ +#!/usr/bin/env node +import { spawnSync } from "node:child_process"; + +const vitest = spawnSync("vitest", ["--run", ...process.argv.slice(2)], { stdio: "inherit" }); +if (vitest.status !== 0) process.exit(vitest.status ?? 1); + +const nodeTest = spawnSync(process.execPath, ["--test", "scripts/*.test.mjs"], { stdio: "inherit" }); +process.exit(nodeTest.status ?? 1);