|
@@ -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)
|
|
|
|
|
+}
|