refactor(core): move more responsibility to workspace routing (#19455)
parent
e5f0e813b6
commit
4b9660b211
|
|
@ -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)
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue