refactor(core): move more responsibility to workspace routing (#19455)

pull/19457/head
James Long 2026-03-27 16:33:56 -04:00 committed by GitHub
parent e5f0e813b6
commit 4b9660b211
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 102 additions and 97 deletions

View File

@ -32,15 +32,7 @@ export const WorktreeAdaptor: Adaptor = {
const config = Config.parse(info) const config = Config.parse(info)
await Worktree.remove({ directory: config.directory }) await Worktree.remove({ directory: config.directory })
}, },
async fetch(info, input: RequestInfo | URL, init?: RequestInit) { async fetch(_info, _input: RequestInfo | URL, _init?: RequestInit) {
const { Server } = await import("../../server/server") throw new Error("fetch not implemented")
const config = Config.parse(info)
const url = input instanceof Request || input instanceof URL ? input : new URL(input, "http://opencode.internal")
const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : undefined))
headers.set("x-opencode-directory", config.directory)
const request = new Request(url, { ...init, headers })
return Server.Default().fetch(request)
}, },
} }

View File

@ -1,64 +0,0 @@
import type { MiddlewareHandler } from "hono"
import { Flag } from "../flag/flag"
import { getAdaptor } from "./adaptors"
import { WorkspaceID } from "./schema"
import { Workspace } from "./workspace"
import { InstanceRoutes } from "../server/instance"
import { lazy } from "../util/lazy"
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
const RULES: Array<Rule> = [
{ path: "/session/status", action: "forward" },
{ method: "GET", path: "/session", action: "local" },
]
function local(method: string, path: string) {
for (const rule of RULES) {
if (rule.method && rule.method !== method) continue
const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/")
if (match) return rule.action === "local"
}
return false
}
const routes = lazy(() => InstanceRoutes())
export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
return routes().fetch(c.req.raw, c.env)
}
const url = new URL(c.req.url)
const raw = url.searchParams.get("workspace")
if (!raw) {
return routes().fetch(c.req.raw, c.env)
}
if (local(c.req.method, url.pathname)) {
return routes().fetch(c.req.raw, c.env)
}
const workspaceID = WorkspaceID.make(raw)
const workspace = await Workspace.get(workspaceID)
if (!workspace) {
return new Response(`Workspace not found: ${workspaceID}`, {
status: 500,
headers: {
"content-type": "text/plain; charset=utf-8",
},
})
}
const adaptor = await getAdaptor(workspace.type)
const headers = new Headers(c.req.raw.headers)
headers.delete("x-opencode-workspace")
return adaptor.fetch(workspace, `${url.pathname}${url.search}`, {
method: c.req.method,
body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(),
signal: c.req.raw.signal,
headers,
})
}

View File

@ -14,7 +14,6 @@ import { Global } from "../global"
import { LSP } from "../lsp" import { LSP } from "../lsp"
import { Command } from "../command" import { Command } from "../command"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { Filesystem } from "@/util/filesystem"
import { QuestionRoutes } from "./routes/question" import { QuestionRoutes } from "./routes/question"
import { PermissionRoutes } from "./routes/permission" import { PermissionRoutes } from "./routes/permission"
import { ProjectRoutes } from "./routes/project" import { ProjectRoutes } from "./routes/project"
@ -26,7 +25,6 @@ import { ConfigRoutes } from "./routes/config"
import { ExperimentalRoutes } from "./routes/experimental" import { ExperimentalRoutes } from "./routes/experimental"
import { ProviderRoutes } from "./routes/provider" import { ProviderRoutes } from "./routes/provider"
import { EventRoutes } from "./routes/event" import { EventRoutes } from "./routes/event"
import { InstanceBootstrap } from "../project/bootstrap"
import { errorHandler } from "./middleware" import { errorHandler } from "./middleware"
const log = Log.create({ service: "server" }) const log = Log.create({ service: "server" })
@ -45,26 +43,6 @@ const csp = (hash = "") =>
export const InstanceRoutes = (app?: Hono) => export const InstanceRoutes = (app?: Hono) =>
(app ?? new Hono()) (app ?? new Hono())
.onError(errorHandler(log)) .onError(errorHandler(log))
.use(async (c, next) => {
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
const directory = Filesystem.resolve(
(() => {
try {
return decodeURIComponent(raw)
} catch {
return raw
}
})(),
)
return Instance.provide({
directory,
init: InstanceBootstrap,
async fn() {
return next()
},
})
})
.route("/project", ProjectRoutes()) .route("/project", ProjectRoutes())
.route("/pty", PtyRoutes()) .route("/pty", PtyRoutes())
.route("/config", ConfigRoutes()) .route("/config", ConfigRoutes())

View File

@ -0,0 +1,99 @@
import type { MiddlewareHandler } from "hono"
import { getAdaptor } from "@/control-plane/adaptors"
import { WorkspaceID } from "@/control-plane/schema"
import { Workspace } from "@/control-plane/workspace"
import { lazy } from "@/util/lazy"
import { Filesystem } from "@/util/filesystem"
import { Instance } from "@/project/instance"
import { InstanceBootstrap } from "@/project/bootstrap"
import { InstanceRoutes } from "./instance"
type Rule = { method?: string; path: string; exact?: boolean; action: "local" | "forward" }
const RULES: Array<Rule> = [
{ path: "/session/status", action: "forward" },
{ method: "GET", path: "/session", action: "local" },
]
function local(method: string, path: string) {
for (const rule of RULES) {
if (rule.method && rule.method !== method) continue
const match = rule.exact ? path === rule.path : path === rule.path || path.startsWith(rule.path + "/")
if (match) return rule.action === "local"
}
return false
}
const routes = lazy(() => InstanceRoutes())
export const WorkspaceRouterMiddleware: MiddlewareHandler = async (c) => {
const raw = c.req.query("directory") || c.req.header("x-opencode-directory") || process.cwd()
const directory = Filesystem.resolve(
(() => {
try {
return decodeURIComponent(raw)
} catch {
return raw
}
})(),
)
const url = new URL(c.req.url)
const workspaceParam = url.searchParams.get("workspace")
// TODO: If session is being routed, force it to lookup the
// project/workspace
// If no workspace is provided we use the "project" workspace
if (!workspaceParam) {
return Instance.provide({
directory,
init: InstanceBootstrap,
async fn() {
return routes().fetch(c.req.raw, c.env)
},
})
}
const workspaceID = WorkspaceID.make(workspaceParam)
const workspace = await Workspace.get(workspaceID)
if (!workspace) {
return new Response(`Workspace not found: ${workspaceID}`, {
status: 500,
headers: {
"content-type": "text/plain; charset=utf-8",
},
})
}
// Handle local workspaces directly so we can pass env to `fetch`,
// necessary for websocket upgrades
if (workspace.type === "worktree") {
return Instance.provide({
directory: workspace.directory!,
init: InstanceBootstrap,
async fn() {
return routes().fetch(c.req.raw, c.env)
},
})
}
// Remote workspaces
if (local(c.req.method, url.pathname)) {
// No instance provided because we are serving cached data; there
// is no instance to work with
return routes().fetch(c.req.raw, c.env)
}
const adaptor = await getAdaptor(workspace.type)
const headers = new Headers(c.req.raw.headers)
headers.delete("x-opencode-workspace")
return adaptor.fetch(workspace, `${url.pathname}${url.search}`, {
method: c.req.method,
body: c.req.method === "GET" || c.req.method === "HEAD" ? undefined : await c.req.raw.arrayBuffer(),
signal: c.req.raw.signal,
headers,
})
}

View File

@ -8,7 +8,7 @@ import z from "zod"
import { Auth } from "../auth" import { Auth } from "../auth"
import { Flag } from "../flag/flag" import { Flag } from "../flag/flag"
import { ProviderID } from "../provider/schema" import { ProviderID } from "../provider/schema"
import { WorkspaceRouterMiddleware } from "../control-plane/workspace-router-middleware" import { WorkspaceRouterMiddleware } from "./router"
import { websocket } from "hono/bun" import { websocket } from "hono/bun"
import { errors } from "./error" import { errors } from "./error"
import { GlobalRoutes } from "./routes/global" import { GlobalRoutes } from "./routes/global"