router.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102
  1. import type { MiddlewareHandler } from "hono"
  2. import type { UpgradeWebSocket } from "hono/ws"
  3. import { getAdaptor } from "@/control-plane/adaptors"
  4. import { WorkspaceID } from "@/control-plane/schema"
  5. import { Workspace } from "@/control-plane/workspace"
  6. import { lazy } from "@/util/lazy"
  7. import { Filesystem } from "@/util/filesystem"
  8. import { Instance } from "@/project/instance"
  9. import { InstanceBootstrap } from "@/project/bootstrap"
  10. import { InstanceRoutes } from "./instance"
  11. type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
  12. const RULES: Array<Rule> = [
  13. { path: "/session/status", action: "forward" },
  14. { method: "GET", path: "/session", action: "local" },
  15. ]
  16. function local(method: string, path: string) {
  17. for (const rule of RULES) {
  18. if (rule.method && rule.method !== method) continue
  19. const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/")
  20. if (match) return rule.action === "local"
  21. }
  22. return false
  23. }
  24. export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
  25. const routes = lazy(() => InstanceRoutes(upgrade))
  26. return async (c) => {
  27. const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
  28. const directory = Filesystem.resolve(
  29. (() => {
  30. try {
  31. return decodeURIComponent(raw)
  32. } catch {
  33. return raw
  34. }
  35. })(),
  36. )
  37. const url = new URL(c.req.url)
  38. const workspaceParam = url.searchParams.get("workspace")
  39. // TODO: If session is being routed, force it to lookup the
  40. // project/workspace
  41. // If no workspace is provided we use the "project" workspace
  42. if (!workspaceParam) {
  43. return Instance.provide({
  44. directory,
  45. init: InstanceBootstrap,
  46. async fn() {
  47. return routes().fetch(c.req.raw, c.env)
  48. },
  49. })
  50. }
  51. const workspaceID = WorkspaceID.make(workspaceParam)
  52. const workspace = await Workspace.get(workspaceID)
  53. if (!workspace) {
  54. return new Response(`Workspace not found: ${workspaceID}`, {
  55. status: 500,
  56. headers: {
  57. "content-type": "text/plain; charset=utf-8",
  58. },
  59. })
  60. }
  61. // Handle local workspaces directly so we can pass env to `fetch`,
  62. // necessary for websocket upgrades
  63. if (workspace.type === "worktree") {
  64. return Instance.provide({
  65. directory: workspace.directory!,
  66. init: InstanceBootstrap,
  67. async fn() {
  68. return routes().fetch(c.req.raw, c.env)
  69. },
  70. })
  71. }
  72. // Remote workspaces
  73. if (local(c.req.method, url.pathname)) {
  74. // No instance provided because we are serving cached data; there
  75. // is no instance to work with
  76. return routes().fetch(c.req.raw, c.env)
  77. }
  78. const adaptor = await getAdaptor(workspace.type)
  79. const headers = new Headers(c.req.raw.headers)
  80. headers.delete("x-opencode-workspace")
  81. return adaptor.fetch(workspace, `${url.pathname}${url.search}`, {
  82. method: c.req.method,
  83. body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(),
  84. signal: c.req.raw.signal,
  85. headers,
  86. })
  87. }
  88. }