From 2efe61c5ff3ef842828ec39d1e148fe63e7f4b80 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Thu, 9 Apr 2026 14:54:31 -0400 Subject: [PATCH] core: add Bun runtime support for Hono server Enables running the agent on Bun runtime in addition to Node.js by abstracting server initialization into runtime-specific adapters. The server will automatically use Bun's native APIs when available. --- packages/opencode/package.json | 5 ++ packages/opencode/src/config/config.ts | 1 - packages/opencode/src/server/adapter.bun.ts | 40 ++++++++++++ packages/opencode/src/server/adapter.node.ts | 66 ++++++++++++++++++++ packages/opencode/src/server/adapter.ts | 21 +++++++ packages/opencode/src/server/server.ts | 64 ++++--------------- 6 files changed, 144 insertions(+), 53 deletions(-) create mode 100644 packages/opencode/src/server/adapter.bun.ts create mode 100644 packages/opencode/src/server/adapter.node.ts create mode 100644 packages/opencode/src/server/adapter.ts diff --git a/packages/opencode/package.json b/packages/opencode/package.json index 7cce131908..d78ea68b5a 100644 --- a/packages/opencode/package.json +++ b/packages/opencode/package.json @@ -39,6 +39,11 @@ "bun": "./src/pty/pty.bun.ts", "node": "./src/pty/pty.node.ts", "default": "./src/pty/pty.bun.ts" + }, + "#hono": { + "bun": "./src/server/adapter.bun.ts", + "node": "./src/server/adapter.node.ts", + "default": "./src/server/adapter.bun.ts" } }, "devDependencies": { diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index ff79b739fe..0b17dfecd7 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -4,7 +4,6 @@ import { pathToFileURL } from "url" import os from "os" import { Process } from "../util/process" import z from "zod" -import { ModelsDev } from "../provider/models" import { mergeDeep, pipe, unique } from "remeda" import { Global } from "../global" import fsNode from "fs/promises" diff --git a/packages/opencode/src/server/adapter.bun.ts b/packages/opencode/src/server/adapter.bun.ts new file mode 100644 index 0000000000..3e70b97e8a --- /dev/null +++ b/packages/opencode/src/server/adapter.bun.ts @@ -0,0 +1,40 @@ +import type { Hono } from "hono" +import { createBunWebSocket } from "hono/bun" +import type { Adapter } from "./adapter" + +export const adapter: Adapter = { + create(app: Hono) { + const ws = createBunWebSocket() + return { + upgradeWebSocket: ws.upgradeWebSocket, + async listen(opts) { + const args = { + fetch: app.fetch, + hostname: opts.hostname, + idleTimeout: 0, + websocket: ws.websocket, + } as const + const start = (port: number) => { + try { + return Bun.serve({ ...args, port }) + } catch { + return + } + } + const server = opts.port === 0 ? (start(4096) ?? start(0)) : start(opts.port) + if (!server) { + throw new Error(`Failed to start server on port ${opts.port}`) + } + if (!server.port) { + throw new Error(`Failed to resolve server address for port ${opts.port}`) + } + return { + port: server.port, + stop(close?: boolean) { + return Promise.resolve(server.stop(close)) + }, + } + }, + } + }, +} diff --git a/packages/opencode/src/server/adapter.node.ts b/packages/opencode/src/server/adapter.node.ts new file mode 100644 index 0000000000..9c2a41cce2 --- /dev/null +++ b/packages/opencode/src/server/adapter.node.ts @@ -0,0 +1,66 @@ +import { createAdaptorServer, type ServerType } from "@hono/node-server" +import { createNodeWebSocket } from "@hono/node-ws" +import type { Hono } from "hono" +import type { Adapter } from "./adapter" + +export const adapter: Adapter = { + create(app: Hono) { + const ws = createNodeWebSocket({ app }) + return { + upgradeWebSocket: ws.upgradeWebSocket, + async listen(opts) { + const start = (port: number) => + new Promise((resolve, reject) => { + const server = createAdaptorServer({ fetch: app.fetch }) + ws.injectWebSocket(server) + const fail = (err: Error) => { + cleanup() + reject(err) + } + const ready = () => { + cleanup() + resolve(server) + } + const cleanup = () => { + server.off("error", fail) + server.off("listening", ready) + } + server.once("error", fail) + server.once("listening", ready) + server.listen(port, opts.hostname) + }) + + const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port) + const addr = server.address() + if (!addr || typeof addr === "string") { + throw new Error(`Failed to resolve server address for port ${opts.port}`) + } + + let closing: Promise | undefined + return { + port: addr.port, + stop(close?: boolean) { + closing ??= new Promise((resolve, reject) => { + server.close((err) => { + if (err) { + reject(err) + return + } + resolve() + }) + if (close) { + if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") { + server.closeAllConnections() + } + if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") { + server.closeIdleConnections() + } + } + }) + return closing + }, + } + }, + } + }, +} diff --git a/packages/opencode/src/server/adapter.ts b/packages/opencode/src/server/adapter.ts new file mode 100644 index 0000000000..272521d7d3 --- /dev/null +++ b/packages/opencode/src/server/adapter.ts @@ -0,0 +1,21 @@ +import type { Hono } from "hono" +import type { UpgradeWebSocket } from "hono/ws" + +export type Opts = { + port: number + hostname: string +} + +export type Listener = { + port: number + stop: (close?: boolean) => Promise +} + +export interface Runtime { + upgradeWebSocket: UpgradeWebSocket + listen(opts: Opts): Promise +} + +export interface Adapter { + create(app: Hono): Runtime +} diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts index 8ed661a8a5..02ec7356ec 100644 --- a/packages/opencode/src/server/server.ts +++ b/packages/opencode/src/server/server.ts @@ -1,12 +1,11 @@ import { generateSpecs } from "hono-openapi" import { Hono } from "hono" -import { createNodeWebSocket } from "@hono/node-ws" +import { adapter } from "#hono" import { MDNS } from "./mdns" import { lazy } from "@/util/lazy" import { AuthMiddleware, CompressionMiddleware, CorsMiddleware, ErrorMiddleware, LoggerMiddleware } from "./middleware" import { InstanceRoutes } from "./instance" import { initProjectors } from "./projectors" -import { createAdaptorServer, type ServerType } from "@hono/node-server" import { Log } from "@/util/log" import { ControlPlaneRoutes } from "./control" import { UIRoutes } from "./ui" @@ -30,7 +29,7 @@ export namespace Server { function create(opts: { cors?: string[] }) { const app = new Hono() - const ws = createNodeWebSocket({ app }) + const runtime = adapter.create(app) return { app: app .onError(ErrorMiddleware) @@ -39,9 +38,9 @@ export namespace Server { .use(CompressionMiddleware) .use(CorsMiddleware(opts)) .route("/", ControlPlaneRoutes()) - .route("/", InstanceRoutes(ws.upgradeWebSocket)) + .route("/", InstanceRoutes(runtime.upgradeWebSocket)) .route("/", UIRoutes()), - ws, + runtime, } } @@ -74,46 +73,21 @@ export namespace Server { cors?: string[] }): Promise { const built = create(opts) - const start = (port: number) => - new Promise((resolve, reject) => { - const server = createAdaptorServer({ fetch: built.app.fetch }) - built.ws.injectWebSocket(server) - const fail = (err: Error) => { - cleanup() - reject(err) - } - const ready = () => { - cleanup() - resolve(server) - } - const cleanup = () => { - server.off("error", fail) - server.off("listening", ready) - } - server.once("error", fail) - server.once("listening", ready) - server.listen(port, opts.hostname) - }) - - const server = opts.port === 0 ? await start(4096).catch(() => start(0)) : await start(opts.port) - const addr = server.address() - if (!addr || typeof addr === "string") { - throw new Error(`Failed to resolve server address for port ${opts.port}`) - } + const server = await built.runtime.listen(opts) const next = new URL("http://localhost") next.hostname = opts.hostname - next.port = String(addr.port) + next.port = String(server.port) url = next const mdns = opts.mdns && - addr.port && + server.port && opts.hostname !== "127.0.0.1" && opts.hostname !== "localhost" && opts.hostname !== "::1" if (mdns) { - MDNS.publish(addr.port, opts.mdnsDomain) + MDNS.publish(server.port, opts.mdnsDomain) } else if (opts.mdns) { log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") } @@ -121,27 +95,13 @@ export namespace Server { let closing: Promise | undefined return { hostname: opts.hostname, - port: addr.port, + port: server.port, url: next, stop(close?: boolean) { - closing ??= new Promise((resolve, reject) => { + closing ??= (async () => { if (mdns) MDNS.unpublish() - server.close((err) => { - if (err) { - reject(err) - return - } - resolve() - }) - if (close) { - if ("closeAllConnections" in server && typeof server.closeAllConnections === "function") { - server.closeAllConnections() - } - if ("closeIdleConnections" in server && typeof server.closeIdleConnections === "function") { - server.closeIdleConnections() - } - } - }) + await server.stop(close) + })() return closing }, }