summaryrefslogtreecommitdiffhomepage
path: root/packages
diff options
context:
space:
mode:
authorJames Long <[email protected]>2026-04-20 14:27:58 -0400
committerGitHub <[email protected]>2026-04-20 18:27:58 +0000
commitdebcff2b6b10bd191d68a427f19bcd3d0d6eaf59 (patch)
tree7fe3188a0fb053c7d16e3a844613d74192c78dbc /packages
parent8b3323708d14811fa42d5e7626470e0ec041325e (diff)
downloadopencode-debcff2b6b10bd191d68a427f19bcd3d0d6eaf59.tar.gz
opencode-debcff2b6b10bd191d68a427f19bcd3d0d6eaf59.zip
feat(core): add debug workspace server (#23590)
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/opencode/script/run-workspace-server108
-rw-r--r--packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts73
2 files changed, 181 insertions, 0 deletions
diff --git a/packages/opencode/script/run-workspace-server b/packages/opencode/script/run-workspace-server
new file mode 100755
index 000000000..0087fba10
--- /dev/null
+++ b/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)
+}
diff --git a/packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts b/packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts
new file mode 100644
index 000000000..efc9d0c65
--- /dev/null
+++ b/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