diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.tsindex 99a9a81ab..2fb9038b0 100644--- a/packages/opencode/src/cli/cmd/acp.ts+++ b/packages/opencode/src/cli/cmd/acp.ts@@ -23,7 +23,7 @@ export const AcpCommand = cmd({ process.env.OPENCODE_CLIENT = "acp" await bootstrap(process.cwd(), async () => { const opts = await resolveNetworkOptions(args)- const server = Server.listen(opts)+ const server = await Server.listen(opts) const sdk = createOpencodeClient({ baseUrl: `http://${server.hostname}:${server.port}`,diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.tsindex f86d8d32a..fdb4dc107 100644--- a/packages/opencode/src/config/config.ts+++ b/packages/opencode/src/config/config.ts@@ -90,6 +90,11 @@ export namespace Config { } export async function installDependencies(dir: string, input?: InstallInput) {+ if (process.env.OPENCODE_DISABLE_CONFIG_DEP_INSTALL === "1") {+ log.info("skipping dependency install", { dir, reason: "disabled_by_env" })+ return+ }+ if (!(await needsInstall(dir))) return await using _ = await Flock.acquire(`config-install:${Filesystem.resolve(dir)}`, {diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.tsindex b05dd8625..f2afc5979 100644--- a/packages/opencode/src/plugin/index.ts+++ b/packages/opencode/src/plugin/index.ts@@ -50,7 +50,10 @@ export namespace Plugin { export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Plugin") {} // Built-in plugins that are directly imported (not installed from npm)- const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin, PoeAuthPlugin]+ const INTERNAL_PLUGINS: PluginInstance[] =+ process.env.OPENCODE_ENABLE_INTERNAL_AUTH_PLUGINS === "1"+ ? [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin, PoeAuthPlugin]+ : [] function isServerPlugin(value: unknown): value is PluginInstance { return typeof value === "function"@@ -128,7 +131,9 @@ export namespace Plugin { get serverUrl(): URL { return Server.url ?? new URL("http://localhost:4096") },- $: Bun.$,+ $: ((..._args: unknown[]) => {+ throw new Error("Bun shell is unavailable in the Node ACP build")+ }) as PluginInput["$"], } for (const plugin of INTERNAL_PLUGINS) {diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.tsindex 4bb6efaf9..fc634f5c2 100644--- a/packages/opencode/src/server/instance.ts+++ b/packages/opencode/src/server/instance.ts@@ -18,7 +18,6 @@ import { QuestionRoutes } from "./routes/question" import { PermissionRoutes } from "./routes/permission" import { ProjectRoutes } from "./routes/project" import { SessionRoutes } from "./routes/session"-import { PtyRoutes } from "./routes/pty" import { McpRoutes } from "./routes/mcp" import { FileRoutes } from "./routes/file" import { ConfigRoutes } from "./routes/config"@@ -44,7 +43,6 @@ export const InstanceRoutes = (app?: Hono) => (app ?? new Hono()) .onError(errorHandler(log)) .route("/project", ProjectRoutes())- .route("/pty", PtyRoutes()) .route("/config", ConfigRoutes()) .route("/experimental", ExperimentalRoutes()) .route("/session", SessionRoutes())diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.tsindex ec245ed59..8df8fe679 100644--- a/packages/opencode/src/server/server.ts+++ b/packages/opencode/src/server/server.ts@@ -9,7 +9,6 @@ import { Auth } from "../auth" import { Flag } from "../flag/flag" import { ProviderID } from "../provider/schema" import { WorkspaceRouterMiddleware } from "./router"-import { websocket } from "hono/bun" import { errors } from "./error" import { GlobalRoutes } from "./routes/global" import { MDNS } from "./mdns"@@ -17,6 +16,9 @@ import { lazy } from "@/util/lazy" import { errorHandler } from "./middleware" import { InstanceRoutes } from "./instance" import { initProjectors } from "./projectors"+import { createServer, type IncomingMessage, type ServerResponse } from "node:http"+import type { AddressInfo } from "node:net"+import { Readable } from "node:stream" // @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85 globalThis.AI_SDK_LOG_WARNINGS = false@@ -264,7 +266,106 @@ export namespace Server { /** @deprecated do not use this dumb shit */ export let url: URL - export function listen(opts: {+ type NodeServeHandle = {+ hostname: string+ port: number+ stop: (closeActiveConnections?: boolean) => Promise<void>+ }++ function toRequestUrl(req: IncomingMessage, fallbackHost: string, fallbackPort: number) {+ const host = req.headers.host ?? `${fallbackHost}:${fallbackPort}`+ return new URL(req.url ?? "/", `http://${host}`)+ }++ function toHeaders(req: IncomingMessage) {+ const headers = new Headers()+ for (const [key, value] of Object.entries(req.headers)) {+ if (Array.isArray(value)) {+ for (const item of value) headers.append(key, item)+ continue+ }+ if (typeof value === "string") headers.set(key, value)+ }+ return headers+ }++ async function writeResponse(res: ServerResponse, response: Response) {+ res.statusCode = response.status+ res.statusMessage = response.statusText+ response.headers.forEach((value, key) => {+ res.setHeader(key, value)+ })++ if (!response.body) {+ res.end()+ return+ }++ await new Promise<void>((resolve, reject) => {+ const body = Readable.fromWeb(response.body as globalThis.ReadableStream<Uint8Array>)+ body.on("error", reject)+ res.on("error", reject)+ res.on("close", resolve)+ body.pipe(res)+ })+ }++ async function serveNode(opts: {+ hostname: string+ port: number+ fetch: (request: Request) => Response | Promise<Response>+ }): Promise<NodeServeHandle> {+ const server = createServer(async (req, res) => {+ try {+ const request = new Request(toRequestUrl(req, opts.hostname, opts.port), {+ method: req.method,+ headers: toHeaders(req),+ body:+ req.method && req.method !== "GET" && req.method !== "HEAD"+ ? (Readable.toWeb(req) as globalThis.ReadableStream<Uint8Array>)+ : undefined,+ duplex: "half",+ })+ const response = await opts.fetch(request)+ await writeResponse(res, response)+ } catch (error) {+ log.error("node server request failed", { error })+ if (!res.headersSent) {+ res.statusCode = 500+ res.setHeader("content-type", "text/plain; charset=utf-8")+ }+ res.end("Internal Server Error")+ }+ })++ await new Promise<void>((resolve, reject) => {+ server.once("error", reject)+ server.listen(opts.port, opts.hostname, () => {+ server.off("error", reject)+ resolve()+ })+ })++ const address = server.address()+ if (!address || typeof address === "string") {+ throw new Error("Failed to resolve server address")+ }++ return {+ hostname: opts.hostname,+ port: (address as AddressInfo).port,+ stop() {+ return new Promise<void>((resolve, reject) => {+ server.close((error) => {+ if (error) reject(error)+ else resolve()+ })+ })+ },+ }+ }++ export async function listen(opts: { port: number hostname: string mdns?: boolean@@ -273,20 +374,18 @@ export namespace Server { }) { url = new URL(`http://${opts.hostname}:${opts.port}`) const app = ControlPlaneRoutes({ cors: opts.cors })- const args = {- hostname: opts.hostname,- idleTimeout: 0,- fetch: app.fetch,- websocket: websocket,- } as const- const tryServe = (port: number) => {+ const tryServe = async (port: number) => { try {- return Bun.serve({ ...args, port })+ return await serveNode({+ hostname: opts.hostname,+ port,+ fetch: app.fetch,+ }) } catch { return undefined } }- const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)+ const server = opts.port === 0 ? ((await tryServe(4096)) ?? (await tryServe(0))) : await tryServe(opts.port) if (!server) throw new Error(`Failed to start server on port ${opts.port}`) const shouldPublishMDNS =@@ -296,7 +395,7 @@ export namespace Server { opts.hostname !== "localhost" && opts.hostname !== "::1" if (shouldPublishMDNS) {- MDNS.publish(server.port!, opts.mdnsDomain)+ MDNS.publish(server.port, opts.mdnsDomain) } else if (opts.mdns) { log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish") }@@ -307,6 +406,7 @@ export namespace Server { return originalStop(closeActiveConnections) } + url = new URL(`http://${opts.hostname}:${server.port}`) return server } }
diff --git a/packages/opencode/src/cli/cmd/acp.ts b/packages/opencode/src/cli/cmd/acp.ts
index 99a9a81ab..2fb9038b0 100644
--- a/packages/opencode/src/cli/cmd/acp.ts
+++ b/packages/opencode/src/cli/cmd/acp.ts
@@ -23,7 +23,7 @@ export const AcpCommand = cmd({
process.env.OPENCODE_CLIENT = "acp"
await bootstrap(process.cwd(), async () => {
const opts = await resolveNetworkOptions(args)
- const server = Server.listen(opts)
+ const server = await Server.listen(opts)
const sdk = createOpencodeClient({
baseUrl: `http://${server.hostname}:${server.port}`,
diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts
index f86d8d32a..fdb4dc107 100644
--- a/packages/opencode/src/config/config.ts
+++ b/packages/opencode/src/config/config.ts
@@ -90,6 +90,11 @@ export namespace Config {
}
export async function installDependencies(dir: string, input?: InstallInput) {
+ if (process.env.OPENCODE_DISABLE_CONFIG_DEP_INSTALL === "1") {
+ log.info("skipping dependency install", { dir, reason: "disabled_by_env" })
+ return
+ }
+
if (!(await needsInstall(dir))) return
await using _ = await Flock.acquire(`config-install:${Filesystem.resolve(dir)}`, {
diff --git a/packages/opencode/src/plugin/index.ts b/packages/opencode/src/plugin/index.ts
index b05dd8625..f2afc5979 100644
--- a/packages/opencode/src/plugin/index.ts
+++ b/packages/opencode/src/plugin/index.ts
@@ -50,7 +50,10 @@ export namespace Plugin {
export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Plugin") {}
// Built-in plugins that are directly imported (not installed from npm)
- const INTERNAL_PLUGINS: PluginInstance[] = [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin, PoeAuthPlugin]
+ const INTERNAL_PLUGINS: PluginInstance[] =
+ process.env.OPENCODE_ENABLE_INTERNAL_AUTH_PLUGINS === "1"
+ ? [CodexAuthPlugin, CopilotAuthPlugin, GitlabAuthPlugin, PoeAuthPlugin]
+ : []
function isServerPlugin(value: unknown): value is PluginInstance {
return typeof value === "function"
@@ -128,7 +131,9 @@ export namespace Plugin {
get serverUrl(): URL {
return Server.url ?? new URL("http://localhost:4096")
},
- $: Bun.$,
+ $: ((..._args: unknown[]) => {
+ throw new Error("Bun shell is unavailable in the Node ACP build")
+ }) as PluginInput["$"],
}
for (const plugin of INTERNAL_PLUGINS) {
diff --git a/packages/opencode/src/server/instance.ts b/packages/opencode/src/server/instance.ts
index 4bb6efaf9..fc634f5c2 100644
--- a/packages/opencode/src/server/instance.ts
+++ b/packages/opencode/src/server/instance.ts
@@ -18,7 +18,6 @@ import { QuestionRoutes } from "./routes/question"
import { PermissionRoutes } from "./routes/permission"
import { ProjectRoutes } from "./routes/project"
import { SessionRoutes } from "./routes/session"
-import { PtyRoutes } from "./routes/pty"
import { McpRoutes } from "./routes/mcp"
import { FileRoutes } from "./routes/file"
import { ConfigRoutes } from "./routes/config"
@@ -44,7 +43,6 @@ export const InstanceRoutes = (app?: Hono) =>
(app ?? new Hono())
.onError(errorHandler(log))
.route("/project", ProjectRoutes())
- .route("/pty", PtyRoutes())
.route("/config", ConfigRoutes())
.route("/experimental", ExperimentalRoutes())
.route("/session", SessionRoutes())
diff --git a/packages/opencode/src/server/server.ts b/packages/opencode/src/server/server.ts
index ec245ed59..8df8fe679 100644
--- a/packages/opencode/src/server/server.ts
+++ b/packages/opencode/src/server/server.ts
@@ -9,7 +9,6 @@ import { Auth } from "../auth"
import { Flag } from "../flag/flag"
import { ProviderID } from "../provider/schema"
import { WorkspaceRouterMiddleware } from "./router"
-import { websocket } from "hono/bun"
import { errors } from "./error"
import { GlobalRoutes } from "./routes/global"
import { MDNS } from "./mdns"
@@ -17,6 +16,9 @@ import { lazy } from "@/util/lazy"
import { errorHandler } from "./middleware"
import { InstanceRoutes } from "./instance"
import { initProjectors } from "./projectors"
+import { createServer, type IncomingMessage, type ServerResponse } from "node:http"
+import type { AddressInfo } from "node:net"
+import { Readable } from "node:stream"
// @ts-ignore This global is needed to prevent ai-sdk from logging warnings to stdout https://github.com/vercel/ai/blob/2dc67e0ef538307f21368db32d5a12345d98831b/packages/ai/src/logger/log-warnings.ts#L85
globalThis.AI_SDK_LOG_WARNINGS = false
@@ -264,7 +266,106 @@ export namespace Server {
/** @deprecated do not use this dumb shit */
export let url: URL
- export function listen(opts: {
+ type NodeServeHandle = {
+ hostname: string
+ port: number
+ stop: (closeActiveConnections?: boolean) => Promise<void>
+ }
+
+ function toRequestUrl(req: IncomingMessage, fallbackHost: string, fallbackPort: number) {
+ const host = req.headers.host ?? `${fallbackHost}:${fallbackPort}`
+ return new URL(req.url ?? "/", `http://${host}`)
+ }
+
+ function toHeaders(req: IncomingMessage) {
+ const headers = new Headers()
+ for (const [key, value] of Object.entries(req.headers)) {
+ if (Array.isArray(value)) {
+ for (const item of value) headers.append(key, item)
+ continue
+ }
+ if (typeof value === "string") headers.set(key, value)
+ }
+ return headers
+ }
+
+ async function writeResponse(res: ServerResponse, response: Response) {
+ res.statusCode = response.status
+ res.statusMessage = response.statusText
+ response.headers.forEach((value, key) => {
+ res.setHeader(key, value)
+ })
+
+ if (!response.body) {
+ res.end()
+ return
+ }
+
+ await new Promise<void>((resolve, reject) => {
+ const body = Readable.fromWeb(response.body as globalThis.ReadableStream<Uint8Array>)
+ body.on("error", reject)
+ res.on("error", reject)
+ res.on("close", resolve)
+ body.pipe(res)
+ })
+ }
+
+ async function serveNode(opts: {
+ hostname: string
+ port: number
+ fetch: (request: Request) => Response | Promise<Response>
+ }): Promise<NodeServeHandle> {
+ const server = createServer(async (req, res) => {
+ try {
+ const request = new Request(toRequestUrl(req, opts.hostname, opts.port), {
+ method: req.method,
+ headers: toHeaders(req),
+ body:
+ req.method && req.method !== "GET" && req.method !== "HEAD"
+ ? (Readable.toWeb(req) as globalThis.ReadableStream<Uint8Array>)
+ : undefined,
+ duplex: "half",
+ })
+ const response = await opts.fetch(request)
+ await writeResponse(res, response)
+ } catch (error) {
+ log.error("node server request failed", { error })
+ if (!res.headersSent) {
+ res.statusCode = 500
+ res.setHeader("content-type", "text/plain; charset=utf-8")
+ }
+ res.end("Internal Server Error")
+ }
+ })
+
+ await new Promise<void>((resolve, reject) => {
+ server.once("error", reject)
+ server.listen(opts.port, opts.hostname, () => {
+ server.off("error", reject)
+ resolve()
+ })
+ })
+
+ const address = server.address()
+ if (!address || typeof address === "string") {
+ throw new Error("Failed to resolve server address")
+ }
+
+ return {
+ hostname: opts.hostname,
+ port: (address as AddressInfo).port,
+ stop() {
+ return new Promise<void>((resolve, reject) => {
+ server.close((error) => {
+ if (error) reject(error)
+ else resolve()
+ })
+ })
+ },
+ }
+ }
+
+ export async function listen(opts: {
port: number
hostname: string
mdns?: boolean
@@ -273,20 +374,18 @@ export namespace Server {
}) {
url = new URL(`http://${opts.hostname}:${opts.port}`)
const app = ControlPlaneRoutes({ cors: opts.cors })
- const args = {
- hostname: opts.hostname,
- idleTimeout: 0,
- fetch: app.fetch,
- websocket: websocket,
- } as const
- const tryServe = (port: number) => {
+ const tryServe = async (port: number) => {
try {
- return Bun.serve({ ...args, port })
+ return await serveNode({
+ hostname: opts.hostname,
+ port,
+ fetch: app.fetch,
+ })
} catch {
return undefined
}
}
- const server = opts.port === 0 ? (tryServe(4096) ?? tryServe(0)) : tryServe(opts.port)
+ const server = opts.port === 0 ? ((await tryServe(4096)) ?? (await tryServe(0))) : await tryServe(opts.port)
if (!server) throw new Error(`Failed to start server on port ${opts.port}`)
const shouldPublishMDNS =
@@ -296,7 +395,7 @@ export namespace Server {
opts.hostname !== "localhost" &&
opts.hostname !== "::1"
if (shouldPublishMDNS) {
- MDNS.publish(server.port!, opts.mdnsDomain)
+ MDNS.publish(server.port, opts.mdnsDomain)
} else if (opts.mdns) {
log.warn("mDNS enabled but hostname is loopback; skipping mDNS publish")
}
@@ -307,6 +406,7 @@ export namespace Server {
return originalStop(closeActiveConnections)
}
+ url = new URL(`http://${opts.hostname}:${server.port}`)
return server
}
}