run-workspace-server 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106
  1. #!/usr/bin/env bun
  2. // This script runs a separate OpenCode server to be used as a remote
  3. // workspace, simulating a remote environment but all local to make
  4. // debugger easier
  5. //
  6. // *Important*: make sure you add the debug workspace plugin first.
  7. // In `.opencode/opencode.jsonc` in the root of this project add:
  8. //
  9. // "plugin": ["../packages/opencode/src/control-plane/dev/debug-workspace-plugin.ts"]
  10. //
  11. // Afterwards, run `./packages/opencode/script/run-workspace-server`
  12. import { stat } from "node:fs/promises"
  13. import { setTimeout as sleep } from "node:timers/promises"
  14. const DEV_DATA_FILE = "/tmp/opencode-workspace-dev-data.json"
  15. const RESTART_POLL_INTERVAL = 250
  16. async function readData() {
  17. return await Bun.file(DEV_DATA_FILE).json()
  18. }
  19. async function readDataMtime() {
  20. return await stat(DEV_DATA_FILE)
  21. .then((info) => info.mtimeMs)
  22. .catch((error) => {
  23. if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
  24. return undefined
  25. }
  26. throw error
  27. })
  28. }
  29. async function readSnapshot() {
  30. while (true) {
  31. try {
  32. const before = await readDataMtime()
  33. if (before === undefined) {
  34. await sleep(RESTART_POLL_INTERVAL)
  35. continue
  36. }
  37. const data = await readData()
  38. const after = await readDataMtime()
  39. if (before === after) {
  40. return { data, mtime: after }
  41. }
  42. } catch (error) {
  43. if (typeof error === "object" && error && "code" in error && error.code === "ENOENT") {
  44. await sleep(RESTART_POLL_INTERVAL)
  45. continue
  46. }
  47. throw error
  48. }
  49. }
  50. }
  51. function startDevServer(data: any) {
  52. const env = Object.fromEntries(Object.entries(data.env ?? {}).filter(([, value]) => value !== undefined))
  53. return Bun.spawn(["bun", "run", "dev", "serve", "--port", String(data.port), "--print-logs"], {
  54. env: {
  55. ...process.env,
  56. ...env,
  57. XDG_DATA_HOME: "/tmp/data",
  58. },
  59. stdin: "inherit",
  60. stdout: "inherit",
  61. stderr: "inherit",
  62. })
  63. }
  64. async function waitForRestartSignal(mtime: number, signal: AbortSignal) {
  65. while (!signal.aborted) {
  66. await sleep(RESTART_POLL_INTERVAL)
  67. if (signal.aborted) return false
  68. if ((await readDataMtime()) !== mtime) return true
  69. }
  70. return false
  71. }
  72. while (true) {
  73. const { data, mtime } = await readSnapshot()
  74. const proc = startDevServer(data)
  75. const restartAbort = new AbortController()
  76. const result = await Promise.race([
  77. proc.exited.then((code) => ({ type: "exit" as const, code })),
  78. waitForRestartSignal(mtime, restartAbort.signal).then((restart) => ({ type: "restart" as const, restart })),
  79. ])
  80. restartAbort.abort()
  81. if (result.type === "restart" && result.restart) {
  82. proc.kill()
  83. await proc.exited
  84. continue
  85. }
  86. process.exit(result.code)
  87. }