Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions packages/opencode/script/run-workspace-server
Original file line number Diff line number Diff line change
@@ -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 changes: 73 additions & 0 deletions packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts
Original file line number Diff line number Diff line change
@@ -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
Loading