diff --git a/packages/cli/script/build.ts b/packages/cli/script/build.ts index 80a67fe75589..e07c27ed87e7 100755 --- a/packages/cli/script/build.ts +++ b/packages/cli/script/build.ts @@ -114,6 +114,18 @@ const pluginRuntimePlugin: BunPlugin = { })) }, } +// Bun drops `export * from "ws"` in this sideEffects-free module, leaving NodeWS.WebSocketServer undefined and +// failing every WebSocket upgrade in the compiled binary. Bun's built-in ws has no PerMessageDeflate. +const wsReexportPlugin: BunPlugin = { + name: "ws-named-reexports", + setup(build) { + build.onLoad({ filter: /platform-node-shared[/\\]dist[/\\]NodeWS\.js$/ }, () => ({ + contents: + 'export { createWebSocketStream, extension, Receiver, Sender, subprotocol, WebSocket, WebSocketServer } from "ws"', + loader: "js", + })) + }, +} for (const item of targets) { const opencodePty = await resolveOpencodePty({ @@ -166,6 +178,7 @@ export default { path: file, version: ${JSON.stringify(opencodePty.version)}, sh parcelWatcherPlugin, opencodePtyPlugin, pluginRuntimePlugin, + wsReexportPlugin, simulationGraphPlugin, modelsSnapshot, ], diff --git a/packages/cli/script/service-smoke.ts b/packages/cli/script/service-smoke.ts index cf2d11c3a350..8b3d0a7fcd39 100644 --- a/packages/cli/script/service-smoke.ts +++ b/packages/cli/script/service-smoke.ts @@ -2,6 +2,7 @@ import { NodeFileSystem } from "@effect/platform-node" import { Service } from "@opencode/client/effect/service" +import { MCP_CLIENT_PATH } from "@opencode/protocol/groups/mcp" import { ServerInfo } from "@opencode/protocol/groups/server" import { Effect, Schema } from "effect" import fs from "node:fs/promises" @@ -44,6 +45,7 @@ try { const token = encodeURIComponent(credential) const serverInfo = await waitForReady(info.url, headers) if (serverInfo.pid !== info.pid) throw new Error("Server info does not match registration") + await verifyClientMcpSocket(info.url, token) const tokenInfo = await fetch(new URL(`/api/info?auth_token=${token}`, info.url), { signal: AbortSignal.timeout(5_000), }) @@ -136,6 +138,25 @@ async function verifyWebUi(url: string) { throw new Error(`Compiled service did not serve ${script}`) } +// Bun once bundled the WebSocket server away, so every upgrade failed only in the compiled binary. +async function verifyClientMcpSocket(url: string, token: string) { + const endpoint = new URL(`${MCP_CLIENT_PATH}?name=smoke&auth_token=${token}`, url) + endpoint.protocol = endpoint.protocol === "https:" ? "wss:" : "ws:" + const socket = new WebSocket(endpoint) + const message = await new Promise((resolve, reject) => { + const timeout = setTimeout(() => reject(new Error("Compiled service sent no client MCP initialize")), 5_000) + socket.onmessage = (event) => { + clearTimeout(timeout) + resolve(String(event.data)) + } + socket.onerror = () => { + clearTimeout(timeout) + reject(new Error("Compiled service rejected the client MCP socket")) + } + }).finally(() => socket.close()) + if (!message.includes('"initialize"')) throw new Error("Compiled service did not initialize the client MCP socket") +} + async function waitForReady(url: string, headers: HeadersInit) { const deadline = Date.now() + 20_000 while (Date.now() < deadline) {