فهرست منبع

feat(core): add debug workspace server (#23590)

James Long 1 روز پیش
والد
کامیت
debcff2b6b
2فایلهای تغییر یافته به همراه181 افزوده شده و 0 حذف شده
  1. 108 0
      packages/opencode/script/run-workspace-server
  2. 73 0
      packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts

+ 108 - 0
packages/opencode/script/run-workspace-server

@@ -0,0 +1,108 @@
+#!/usr/bin/env bun
+
+// This script runs a separate OpenCode server to be used as a remote
+// workspace, simulating a remote environment but all local to make
+// debugger easier
+//
+// *Important*: make sure you add the debug workspace plugin first.
+// In `.opencode/opencode.jsonc` in the root of this project add:
+//
+// "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"]
+//
+// Afterwards, run `./packages/opencode/script/run-workspace-server`
+
+import { stat } from "node:fs/promises"
+import { setTimeout as sleep } from "node:timers/promises"
+
+const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
+const RESTART_POLL_INTERVAL = 250
+
+async function readData() {
+  return await Bun.file(DEV_DATA_FILE).json()
+}
+
+async function readDataMtime() {
+  return await stat(DEV_DATA_FILE)
+    .then((info) => info.mtimeMs)
+    .catch((error) => {
+      if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
+        return undefined
+      }
+
+      throw error
+    })
+}
+
+async function readSnapshot() {
+  while (true) {
+    try {
+      const before = await readDataMtime()
+      if (before === undefined) {
+        await sleep(RESTART_POLL_INTERVAL)
+        continue
+      }
+
+      const data = await readData()
+      const after = await readDataMtime()
+
+      if (before === after) {
+        return { data, mtime: after }
+      }
+    } catch (error) {
+      if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
+        await sleep(RESTART_POLL_INTERVAL)
+        continue
+      }
+
+      throw error
+    }
+  }
+}
+
+function startDevServer(data: any) {
+  const env = Object.fromEntries(
+    Object.entries(data.env ?? {}).filter(([, value]) => value !== undefined),
+  )
+
+  return Bun.spawn(["bun", "run", "dev", "serve", "--port", String(data.port), "--print-logs"], {
+    env: {
+      ...process.env,
+      ...env,
+      XDG_DATA_HOME: "/tmp/data",
+    },
+    stdin: "inherit",
+    stdout: "inherit",
+    stderr: "inherit",
+  })
+}
+
+async function waitForRestartSignal(mtime: number, signal: AbortSignal) {
+  while (!signal.aborted) {
+    await sleep(RESTART_POLL_INTERVAL)
+    if (signal.aborted) return false
+    if ((await readDataMtime()) !== mtime) return true
+  }
+
+  return false
+}
+
+while (true) {
+  const { data, mtime } = await readSnapshot()
+  const proc = startDevServer(data)
+  const restartAbort = new AbortController()
+
+  const result = await Promise.race([
+    proc.exited.then((code) => ({ type: "exit" as const, code })),
+    waitForRestartSignal(mtime, restartAbort.signal).then((restart) => ({ type: "restart" as const, restart })),
+  ])
+
+  restartAbort.abort()
+
+  if (result.type === "restart" && result.restart) {
+    proc.kill()
+    await proc.exited
+    continue
+  }
+
+  process.exit(result.code)
+}

+ 73 - 0
packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts

@@ -0,0 +1,73 @@
+import type { Plugin } from "@opencode-ai/plugin"
+import { rename, writeFile } from "node:fs/promises"
+import { randomInt } from "node:crypto"
+import { setTimeout as sleep } from "node:timers/promises"
+
+const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
+const DEV_DATA_TEMP_FILE = `${DEV_DATA_FILE}.tmp`
+
+async function waitForHealth(port: number) {
+  const url = `http://127.0.0.1:${port}/global/health`
+  const started = Date.now()
+
+  while (Date.now() - started < 30_000) {
+    try {
+      const response = await fetch(url)
+      if (response.ok) {
+        return
+      }
+    } catch {}
+
+    await sleep(250)
+  }
+
+  throw new Error(`Timed out waiting for debug server health check at ${url}`)
+}
+
+let PORT: number | undefined
+
+async function writeDebugData(port: number, id: string, env: Record<string, string | undefined>) {
+  await writeFile(
+    DEV_DATA_TEMP_FILE,
+    JSON.stringify(
+      {
+        port,
+        id,
+        env,
+      },
+      null,
+      2,
+    ),
+  )
+
+  await rename(DEV_DATA_TEMP_FILE, DEV_DATA_FILE)
+}
+
+export const DebugWorkspacePlugin: Plugin = async ({ experimental_workspace }) => {
+  experimental_workspace.register("debug", {
+    name: "Debug",
+    description: "Create a debugging server",
+    configure(config) {
+      return config
+    },
+    async create(config, env) {
+      const port = randomInt(5000, 9001)
+      PORT = port
+
+      await writeDebugData(port, config.id, env)
+
+      await waitForHealth(port)
+    },
+    async remove(_config) {},
+    target(_config) {
+      return {
+        type: "remote",
+        url: `http://localhost:${PORT!}/`,
+      }
+    },
+  })
+
+  return {}
+}
+
+export default DebugWorkspacePlugin