Quellcode durchsuchen

core: enable running in non-Bun environments by using standard Node.js APIs for OAuth servers and retry logic

Dax Raad vor 1 Monat
Ursprung
Commit
bca723e8fe

+ 2 - 1
packages/opencode/src/cli/cmd/tui/component/dialog-workspace-list.tsx

@@ -9,6 +9,7 @@ import { useToast } from "../ui/toast"
 import { useKeybind } from "../context/keybind"
 import { useKeybind } from "../context/keybind"
 import { DialogSessionList } from "./workspace/dialog-session-list"
 import { DialogSessionList } from "./workspace/dialog-session-list"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2"
 import { createOpencodeClient } from "@opencode-ai/sdk/v2"
+import { setTimeout as sleep } from "node:timers/promises"
 
 
 async function openWorkspace(input: {
 async function openWorkspace(input: {
   dialog: ReturnType<typeof useDialog>
   dialog: ReturnType<typeof useDialog>
@@ -56,7 +57,7 @@ async function openWorkspace(input: {
       return
       return
     }
     }
     if (result.response.status >= 500 && result.response.status < 600) {
     if (result.response.status >= 500 && result.response.status < 600) {
-      await Bun.sleep(1000)
+      await sleep(1000)
       continue
       continue
     }
     }
     if (!result.data) {
     if (!result.data) {

+ 3 - 2
packages/opencode/src/control-plane/workspace.ts

@@ -1,4 +1,5 @@
 import z from "zod"
 import z from "zod"
+import { setTimeout as sleep } from "node:timers/promises"
 import { Identifier } from "@/id/id"
 import { Identifier } from "@/id/id"
 import { fn } from "@/util/fn"
 import { fn } from "@/util/fn"
 import { Database, eq } from "@/storage/db"
 import { Database, eq } from "@/storage/db"
@@ -116,7 +117,7 @@ export namespace Workspace {
       const adaptor = await getAdaptor(space.type)
       const adaptor = await getAdaptor(space.type)
       const res = await adaptor.fetch(space, "/event", { method: "GET", signal: stop }).catch(() => undefined)
       const res = await adaptor.fetch(space, "/event", { method: "GET", signal: stop }).catch(() => undefined)
       if (!res || !res.ok || !res.body) {
       if (!res || !res.ok || !res.body) {
-        await Bun.sleep(1000)
+        await sleep(1000)
         continue
         continue
       }
       }
       await parseSSE(res.body, stop, (event) => {
       await parseSSE(res.body, stop, (event) => {
@@ -126,7 +127,7 @@ export namespace Workspace {
         })
         })
       })
       })
       // Wait 250ms and retry if SSE connection fails
       // Wait 250ms and retry if SSE connection fails
-      await Bun.sleep(250)
+      await sleep(250)
     }
     }
   }
   }
 
 

+ 4 - 7
packages/opencode/src/mcp/index.ts

@@ -11,6 +11,7 @@ import {
 } from "@modelcontextprotocol/sdk/types.js"
 } from "@modelcontextprotocol/sdk/types.js"
 import { Config } from "../config/config"
 import { Config } from "../config/config"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
+import { Process } from "../util/process"
 import { NamedError } from "@opencode-ai/util/error"
 import { NamedError } from "@opencode-ai/util/error"
 import z from "zod/v4"
 import z from "zod/v4"
 import { Instance } from "../project/instance"
 import { Instance } from "../project/instance"
@@ -166,14 +167,10 @@ export namespace MCP {
     const queue = [pid]
     const queue = [pid]
     while (queue.length > 0) {
     while (queue.length > 0) {
       const current = queue.shift()!
       const current = queue.shift()!
-      const proc = Bun.spawn(["pgrep", "-P", String(current)], { stdout: "pipe", stderr: "pipe" })
-      const [code, out] = await Promise.all([proc.exited, new Response(proc.stdout).text()]).catch(
-        () => [-1, ""] as const,
-      )
-      if (code !== 0) continue
-      for (const tok of out.trim().split(/\s+/)) {
+      const lines = await Process.lines(["pgrep", "-P", String(current)], { nothrow: true })
+      for (const tok of lines) {
         const cpid = parseInt(tok, 10)
         const cpid = parseInt(tok, 10)
-        if (!isNaN(cpid) && pids.indexOf(cpid) === -1) {
+        if (!isNaN(cpid) && !pids.includes(cpid)) {
           pids.push(cpid)
           pids.push(cpid)
           queue.push(cpid)
           queue.push(cpid)
         }
         }

+ 70 - 67
packages/opencode/src/mcp/oauth-callback.ts

@@ -1,4 +1,5 @@
 import { createConnection } from "net"
 import { createConnection } from "net"
+import { createServer } from "http"
 import { Log } from "../util/log"
 import { Log } from "../util/log"
 import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
 import { OAUTH_CALLBACK_PORT, OAUTH_CALLBACK_PATH } from "./oauth-provider"
 
 
@@ -52,89 +53,91 @@ interface PendingAuth {
 }
 }
 
 
 export namespace McpOAuthCallback {
 export namespace McpOAuthCallback {
-  let server: ReturnType<typeof Bun.serve> | undefined
+  let server: ReturnType<typeof createServer> | undefined
   const pendingAuths = new Map<string, PendingAuth>()
   const pendingAuths = new Map<string, PendingAuth>()
 
 
   const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
   const CALLBACK_TIMEOUT_MS = 5 * 60 * 1000 // 5 minutes
 
 
-  export async function ensureRunning(): Promise<void> {
-    if (server) return
+  function handleRequest(req: import("http").IncomingMessage, res: import("http").ServerResponse) {
+    const url = new URL(req.url || "/", `http://localhost:${OAUTH_CALLBACK_PORT}`)
 
 
-    const running = await isPortInUse()
-    if (running) {
-      log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
+    if (url.pathname !== OAUTH_CALLBACK_PATH) {
+      res.writeHead(404)
+      res.end("Not found")
       return
       return
     }
     }
 
 
-    server = Bun.serve({
-      port: OAUTH_CALLBACK_PORT,
-      fetch(req) {
-        const url = new URL(req.url)
+    const code = url.searchParams.get("code")
+    const state = url.searchParams.get("state")
+    const error = url.searchParams.get("error")
+    const errorDescription = url.searchParams.get("error_description")
 
 
-        if (url.pathname !== OAUTH_CALLBACK_PATH) {
-          return new Response("Not found", { status: 404 })
-        }
+    log.info("received oauth callback", { hasCode: !!code, state, error })
 
 
-        const code = url.searchParams.get("code")
-        const state = url.searchParams.get("state")
-        const error = url.searchParams.get("error")
-        const errorDescription = url.searchParams.get("error_description")
-
-        log.info("received oauth callback", { hasCode: !!code, state, error })
-
-        // Enforce state parameter presence
-        if (!state) {
-          const errorMsg = "Missing required state parameter - potential CSRF attack"
-          log.error("oauth callback missing state parameter", { url: url.toString() })
-          return new Response(HTML_ERROR(errorMsg), {
-            status: 400,
-            headers: { "Content-Type": "text/html" },
-          })
-        }
+    // Enforce state parameter presence
+    if (!state) {
+      const errorMsg = "Missing required state parameter - potential CSRF attack"
+      log.error("oauth callback missing state parameter", { url: url.toString() })
+      res.writeHead(400, { "Content-Type": "text/html" })
+      res.end(HTML_ERROR(errorMsg))
+      return
+    }
 
 
-        if (error) {
-          const errorMsg = errorDescription || error
-          if (pendingAuths.has(state)) {
-            const pending = pendingAuths.get(state)!
-            clearTimeout(pending.timeout)
-            pendingAuths.delete(state)
-            pending.reject(new Error(errorMsg))
-          }
-          return new Response(HTML_ERROR(errorMsg), {
-            headers: { "Content-Type": "text/html" },
-          })
-        }
+    if (error) {
+      const errorMsg = errorDescription || error
+      if (pendingAuths.has(state)) {
+        const pending = pendingAuths.get(state)!
+        clearTimeout(pending.timeout)
+        pendingAuths.delete(state)
+        pending.reject(new Error(errorMsg))
+      }
+      res.writeHead(200, { "Content-Type": "text/html" })
+      res.end(HTML_ERROR(errorMsg))
+      return
+    }
 
 
-        if (!code) {
-          return new Response(HTML_ERROR("No authorization code provided"), {
-            status: 400,
-            headers: { "Content-Type": "text/html" },
-          })
-        }
+    if (!code) {
+      res.writeHead(400, { "Content-Type": "text/html" })
+      res.end(HTML_ERROR("No authorization code provided"))
+      return
+    }
 
 
-        // Validate state parameter
-        if (!pendingAuths.has(state)) {
-          const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
-          log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
-          return new Response(HTML_ERROR(errorMsg), {
-            status: 400,
-            headers: { "Content-Type": "text/html" },
-          })
-        }
+    // Validate state parameter
+    if (!pendingAuths.has(state)) {
+      const errorMsg = "Invalid or expired state parameter - potential CSRF attack"
+      log.error("oauth callback with invalid state", { state, pendingStates: Array.from(pendingAuths.keys()) })
+      res.writeHead(400, { "Content-Type": "text/html" })
+      res.end(HTML_ERROR(errorMsg))
+      return
+    }
 
 
-        const pending = pendingAuths.get(state)!
+    const pending = pendingAuths.get(state)!
 
 
-        clearTimeout(pending.timeout)
-        pendingAuths.delete(state)
-        pending.resolve(code)
+    clearTimeout(pending.timeout)
+    pendingAuths.delete(state)
+    pending.resolve(code)
 
 
-        return new Response(HTML_SUCCESS, {
-          headers: { "Content-Type": "text/html" },
-        })
-      },
-    })
+    res.writeHead(200, { "Content-Type": "text/html" })
+    res.end(HTML_SUCCESS)
+  }
+
+  export async function ensureRunning(): Promise<void> {
+    if (server) return
 
 
-    log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
+    const running = await isPortInUse()
+    if (running) {
+      log.info("oauth callback server already running on another instance", { port: OAUTH_CALLBACK_PORT })
+      return
+    }
+
+    server = createServer(handleRequest)
+    await new Promise<void>((resolve, reject) => {
+      server!.listen(OAUTH_CALLBACK_PORT, () => {
+        log.info("oauth callback server started", { port: OAUTH_CALLBACK_PORT })
+        resolve()
+      })
+      server!.on("error", reject)
+    })
   }
   }
 
 
   export function waitForCallback(oauthState: string): Promise<string> {
   export function waitForCallback(oauthState: string): Promise<string> {
@@ -174,7 +177,7 @@ export namespace McpOAuthCallback {
 
 
   export async function stop(): Promise<void> {
   export async function stop(): Promise<void> {
     if (server) {
     if (server) {
-      server.stop()
+      await new Promise<void>((resolve) => server!.close(() => resolve()))
       server = undefined
       server = undefined
       log.info("oauth callback server stopped")
       log.info("oauth callback server stopped")
     }
     }

+ 62 - 55
packages/opencode/src/plugin/codex.ts

@@ -5,6 +5,7 @@ import { Auth, OAUTH_DUMMY_KEY } from "../auth"
 import os from "os"
 import os from "os"
 import { ProviderTransform } from "@/provider/transform"
 import { ProviderTransform } from "@/provider/transform"
 import { setTimeout as sleep } from "node:timers/promises"
 import { setTimeout as sleep } from "node:timers/promises"
+import { createServer } from "http"
 
 
 const log = Log.create({ service: "plugin.codex" })
 const log = Log.create({ service: "plugin.codex" })
 
 
@@ -240,7 +241,7 @@ interface PendingOAuth {
   reject: (error: Error) => void
   reject: (error: Error) => void
 }
 }
 
 
-let oauthServer: ReturnType<typeof Bun.serve> | undefined
+let oauthServer: ReturnType<typeof createServer> | undefined
 let pendingOAuth: PendingOAuth | undefined
 let pendingOAuth: PendingOAuth | undefined
 
 
 async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
 async function startOAuthServer(): Promise<{ port: number; redirectUri: string }> {
@@ -248,77 +249,83 @@ async function startOAuthServer(): Promise<{ port: number; redirectUri: string }
     return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
     return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
   }
   }
 
 
-  oauthServer = Bun.serve({
-    port: OAUTH_PORT,
-    fetch(req) {
-      const url = new URL(req.url)
+  oauthServer = createServer((req, res) => {
+    const url = new URL(req.url || "/", `http://localhost:${OAUTH_PORT}`)
 
 
-      if (url.pathname === "/auth/callback") {
-        const code = url.searchParams.get("code")
-        const state = url.searchParams.get("state")
-        const error = url.searchParams.get("error")
-        const errorDescription = url.searchParams.get("error_description")
+    if (url.pathname === "/auth/callback") {
+      const code = url.searchParams.get("code")
+      const state = url.searchParams.get("state")
+      const error = url.searchParams.get("error")
+      const errorDescription = url.searchParams.get("error_description")
 
 
-        if (error) {
-          const errorMsg = errorDescription || error
-          pendingOAuth?.reject(new Error(errorMsg))
-          pendingOAuth = undefined
-          return new Response(HTML_ERROR(errorMsg), {
-            headers: { "Content-Type": "text/html" },
-          })
-        }
-
-        if (!code) {
-          const errorMsg = "Missing authorization code"
-          pendingOAuth?.reject(new Error(errorMsg))
-          pendingOAuth = undefined
-          return new Response(HTML_ERROR(errorMsg), {
-            status: 400,
-            headers: { "Content-Type": "text/html" },
-          })
-        }
-
-        if (!pendingOAuth || state !== pendingOAuth.state) {
-          const errorMsg = "Invalid state - potential CSRF attack"
-          pendingOAuth?.reject(new Error(errorMsg))
-          pendingOAuth = undefined
-          return new Response(HTML_ERROR(errorMsg), {
-            status: 400,
-            headers: { "Content-Type": "text/html" },
-          })
-        }
-
-        const current = pendingOAuth
+      if (error) {
+        const errorMsg = errorDescription || error
+        pendingOAuth?.reject(new Error(errorMsg))
         pendingOAuth = undefined
         pendingOAuth = undefined
+        res.writeHead(200, { "Content-Type": "text/html" })
+        res.end(HTML_ERROR(errorMsg))
+        return
+      }
 
 
-        exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
-          .then((tokens) => current.resolve(tokens))
-          .catch((err) => current.reject(err))
-
-        return new Response(HTML_SUCCESS, {
-          headers: { "Content-Type": "text/html" },
-        })
+      if (!code) {
+        const errorMsg = "Missing authorization code"
+        pendingOAuth?.reject(new Error(errorMsg))
+        pendingOAuth = undefined
+        res.writeHead(400, { "Content-Type": "text/html" })
+        res.end(HTML_ERROR(errorMsg))
+        return
       }
       }
 
 
-      if (url.pathname === "/cancel") {
-        pendingOAuth?.reject(new Error("Login cancelled"))
+      if (!pendingOAuth || state !== pendingOAuth.state) {
+        const errorMsg = "Invalid state - potential CSRF attack"
+        pendingOAuth?.reject(new Error(errorMsg))
         pendingOAuth = undefined
         pendingOAuth = undefined
-        return new Response("Login cancelled", { status: 200 })
+        res.writeHead(400, { "Content-Type": "text/html" })
+        res.end(HTML_ERROR(errorMsg))
+        return
       }
       }
 
 
-      return new Response("Not found", { status: 404 })
-    },
+      const current = pendingOAuth
+      pendingOAuth = undefined
+
+      exchangeCodeForTokens(code, `http://localhost:${OAUTH_PORT}/auth/callback`, current.pkce)
+        .then((tokens) => current.resolve(tokens))
+        .catch((err) => current.reject(err))
+
+      res.writeHead(200, { "Content-Type": "text/html" })
+      res.end(HTML_SUCCESS)
+      return
+    }
+
+    if (url.pathname === "/cancel") {
+      pendingOAuth?.reject(new Error("Login cancelled"))
+      pendingOAuth = undefined
+      res.writeHead(200)
+      res.end("Login cancelled")
+      return
+    }
+
+    res.writeHead(404)
+    res.end("Not found")
+  })
+
+  await new Promise<void>((resolve, reject) => {
+    oauthServer!.listen(OAUTH_PORT, () => {
+      log.info("codex oauth server started", { port: OAUTH_PORT })
+      resolve()
+    })
+    oauthServer!.on("error", reject)
   })
   })
 
 
-  log.info("codex oauth server started", { port: OAUTH_PORT })
   return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
   return { port: OAUTH_PORT, redirectUri: `http://localhost:${OAUTH_PORT}/auth/callback` }
 }
 }
 
 
 function stopOAuthServer() {
 function stopOAuthServer() {
   if (oauthServer) {
   if (oauthServer) {
-    oauthServer.stop()
+    oauthServer.close(() => {
+      log.info("codex oauth server stopped")
+    })
     oauthServer = undefined
     oauthServer = undefined
-    log.info("codex oauth server stopped")
   }
   }
 }
 }