diff --git a/headroom/providers/opencode/_dist/entry.opencode.js b/headroom/providers/opencode/_dist/entry.opencode.js index 56f9873e8..f2fb1fcbb 100644 --- a/headroom/providers/opencode/_dist/entry.opencode.js +++ b/headroom/providers/opencode/_dist/entry.opencode.js @@ -12484,6 +12484,7 @@ var http = nodeRequire("node:http"); var https = nodeRequire("node:https"); var http2 = nodeRequire("node:http2"); var childProcess = nodeRequire("node:child_process"); +var fs = nodeRequire("node:fs"); var BASE_URL_HEADER = "x-headroom-base-url"; var ORIGINAL_PATH_HEADER = "x-headroom-original-path"; var PROXY_ENV = "HEADROOM_OPENCODE_TRANSPORT_PROXY_URL"; @@ -12495,7 +12496,8 @@ function setState(state) { globalThis[STATE_KEY] = state; } function shimImportSpecifier() { - return new URL("../hook-shim/handler.js", import.meta.url).href; + const shim = new URL("../hook-shim/handler.js", import.meta.url); + return fs.existsSync(shim) ? shim.href : void 0; } function withNodeImportOption(existing, shim) { const parts = existing?.trim() ? existing.trim().split(/\s+/) : []; @@ -12510,12 +12512,18 @@ function withNodeImportOption(existing, shim) { function withShimEnv(env, proxyUrl) { const nextEnv = { ...env ?? process.env }; nextEnv[PROXY_ENV] = proxyUrl; - nextEnv.NODE_OPTIONS = withNodeImportOption(nextEnv.NODE_OPTIONS, shimImportSpecifier()); + const shim = shimImportSpecifier(); + if (shim) { + nextEnv.NODE_OPTIONS = withNodeImportOption(nextEnv.NODE_OPTIONS, shim); + } return nextEnv; } function installProcessEnv(proxyUrl) { process.env[PROXY_ENV] = proxyUrl; - process.env.NODE_OPTIONS = withNodeImportOption(process.env.NODE_OPTIONS, shimImportSpecifier()); + const shim = shimImportSpecifier(); + if (shim) { + process.env.NODE_OPTIONS = withNodeImportOption(process.env.NODE_OPTIONS, shim); + } } function isOptions(value) { return Boolean(value) && typeof value === "object" && !Array.isArray(value) && !(value instanceof URL); diff --git a/plugins/opencode/src/transport.test.ts b/plugins/opencode/src/transport.test.ts index 2e97e1fe5..39edd98de 100644 --- a/plugins/opencode/src/transport.test.ts +++ b/plugins/opencode/src/transport.test.ts @@ -1,4 +1,5 @@ import childProcess from "node:child_process"; +import fs from "node:fs"; import http from "node:http"; import http2 from "node:http2"; import https from "node:https"; @@ -328,6 +329,35 @@ describe("Headroom OpenCode transport", () => { } }); + it("skips the shim preload when the bundle ships without it (#2798)", () => { + const originalNodeOptions = process.env.NODE_OPTIONS; + const originalSpawn = childProcess.spawn; + const spawnMock = vi.fn(() => ({ on: vi.fn(), kill: vi.fn(), pid: 123 })); + childProcess.spawn = spawnMock as unknown as typeof childProcess.spawn; + vi.spyOn(fs, "existsSync").mockReturnValue(false); + + try { + process.env.NODE_OPTIONS = "--trace-warnings"; + installHeadroomTransport({ proxyUrl: "http://127.0.0.1:8787/v1" }); + + // A missing --import target aborts the child before it speaks JSON-RPC, + // which OpenCode reports as `MCP error -32000: Connection closed`. + expect(process.env.NODE_OPTIONS).toBe("--trace-warnings"); + + childProcess.spawn("npx", ["-y", "firecrawl-mcp"]); + const options = (spawnMock.mock.calls[0] as unknown[])[2] as { env: NodeJS.ProcessEnv }; + expect(options.env.NODE_OPTIONS).not.toContain("--import"); + } finally { + if (originalNodeOptions === undefined) { + delete process.env.NODE_OPTIONS; + } else { + process.env.NODE_OPTIONS = originalNodeOptions; + } + childProcess.spawn = originalSpawn; + uninstallHeadroomTransport(); + } + }); + it("injects the Headroom shim into child processes with custom env", () => { const originalSpawn = childProcess.spawn; const spawnMock = vi.fn(() => ({ diff --git a/plugins/opencode/src/transport.ts b/plugins/opencode/src/transport.ts index b7ced7912..b15d5f67d 100644 --- a/plugins/opencode/src/transport.ts +++ b/plugins/opencode/src/transport.ts @@ -5,6 +5,7 @@ const http = nodeRequire("node:http") as typeof import("node:http"); const https = nodeRequire("node:https") as typeof import("node:https"); const http2 = nodeRequire("node:http2") as typeof import("node:http2"); const childProcess = nodeRequire("node:child_process") as typeof import("node:child_process"); +const fs = nodeRequire("node:fs") as typeof import("node:fs"); const BASE_URL_HEADER = "x-headroom-base-url"; const ORIGINAL_PATH_HEADER = "x-headroom-original-path"; @@ -61,8 +62,15 @@ function setState(state: TransportState | undefined): void { (globalThis as GlobalWithHeadroomTransport)[STATE_KEY] = state; } -function shimImportSpecifier(): string { - return new URL("../hook-shim/handler.js", import.meta.url).href; +// ponytail: the shim only exists next to the checkout build +// (plugins/opencode/dist/). The wheel ships entry.opencode.js alone, so +// `--import=` killed every Node child at startup — including +// OpenCode's stdio MCP servers (issue #2798). No shim on disk, no injection: +// children go direct instead of dying. Upgrade path is bundling the shim into +// _dist/ so wheel installs get child-process routing back. +function shimImportSpecifier(): string | undefined { + const shim = new URL("../hook-shim/handler.js", import.meta.url); + return fs.existsSync(shim) ? shim.href : undefined; } function withNodeImportOption(existing: string | undefined, shim: string): string { @@ -79,13 +87,19 @@ function withNodeImportOption(existing: string | undefined, shim: string): strin function withShimEnv(env: NodeJS.ProcessEnv | Record | undefined, proxyUrl: string): NodeJS.ProcessEnv { const nextEnv = { ...(env ?? process.env) } as NodeJS.ProcessEnv; nextEnv[PROXY_ENV] = proxyUrl; - nextEnv.NODE_OPTIONS = withNodeImportOption(nextEnv.NODE_OPTIONS, shimImportSpecifier()); + const shim = shimImportSpecifier(); + if (shim) { + nextEnv.NODE_OPTIONS = withNodeImportOption(nextEnv.NODE_OPTIONS, shim); + } return nextEnv; } function installProcessEnv(proxyUrl: string): void { process.env[PROXY_ENV] = proxyUrl; - process.env.NODE_OPTIONS = withNodeImportOption(process.env.NODE_OPTIONS, shimImportSpecifier()); + const shim = shimImportSpecifier(); + if (shim) { + process.env.NODE_OPTIONS = withNodeImportOption(process.env.NODE_OPTIONS, shim); + } } function isOptions(value: unknown): value is Record {