251 lines · 9.2 KB · Text
RawDownload
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   } }