|
@@ -8,10 +8,11 @@ import * as Fence from "@/server/shared/fence"
|
|
|
import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing"
|
|
import { getWorkspaceRouteSessionID, isLocalWorkspaceRoute, workspaceProxyURL } from "@/server/shared/workspace-routing"
|
|
|
import { NotFoundError } from "@/storage/storage"
|
|
import { NotFoundError } from "@/storage/storage"
|
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
import { Flag } from "@opencode-ai/core/flag/flag"
|
|
|
-import { Context, Data, Effect, Layer, Schema } from "effect"
|
|
|
|
|
|
|
+import { Context, Data, Effect, Layer, Option, Schema } from "effect"
|
|
|
import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
|
import { HttpClient, HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
|
|
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
|
import { HttpApiMiddleware } from "effect/unstable/httpapi"
|
|
|
import * as Socket from "effect/unstable/socket/Socket"
|
|
import * as Socket from "effect/unstable/socket/Socket"
|
|
|
|
|
+import { InvalidRequestError } from "../errors"
|
|
|
|
|
|
|
|
// Query fields this middleware reads from the URL. Spread into every
|
|
// Query fields this middleware reads from the URL. Spread into every
|
|
|
// endpoint query schema in groups that apply WorkspaceRoutingMiddleware,
|
|
// endpoint query schema in groups that apply WorkspaceRoutingMiddleware,
|
|
@@ -28,6 +29,7 @@ export const WorkspaceRoutingQuery = Schema.Struct(WorkspaceRoutingQueryFields)
|
|
|
type RemoteTarget = Extract<Target, { type: "remote" }>
|
|
type RemoteTarget = Extract<Target, { type: "remote" }>
|
|
|
|
|
|
|
|
type RequestPlan = Data.TaggedEnum<{
|
|
type RequestPlan = Data.TaggedEnum<{
|
|
|
|
|
+ InvalidWorkspace: {}
|
|
|
MissingWorkspace: { readonly workspaceID: WorkspaceID }
|
|
MissingWorkspace: { readonly workspaceID: WorkspaceID }
|
|
|
Local: { readonly directory: string; readonly workspaceID?: WorkspaceID }
|
|
Local: { readonly directory: string; readonly workspaceID?: WorkspaceID }
|
|
|
Remote: {
|
|
Remote: {
|
|
@@ -38,6 +40,7 @@ type RequestPlan = Data.TaggedEnum<{
|
|
|
}
|
|
}
|
|
|
}>
|
|
}>
|
|
|
const RequestPlan = Data.taggedEnum<RequestPlan>()
|
|
const RequestPlan = Data.taggedEnum<RequestPlan>()
|
|
|
|
|
+const InvalidWorkspaceID = Symbol("InvalidWorkspaceID")
|
|
|
|
|
|
|
|
export class WorkspaceRouteContext extends Context.Service<
|
|
export class WorkspaceRouteContext extends Context.Service<
|
|
|
WorkspaceRouteContext,
|
|
WorkspaceRouteContext,
|
|
@@ -68,6 +71,15 @@ function selectedWorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): Worksp
|
|
|
return sessionWorkspaceID ?? (workspaceParam ? WorkspaceID.make(workspaceParam) : undefined)
|
|
return sessionWorkspaceID ?? (workspaceParam ? WorkspaceID.make(workspaceParam) : undefined)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+function selectedV2WorkspaceID(url: URL, sessionWorkspaceID?: WorkspaceID): WorkspaceID | typeof InvalidWorkspaceID | undefined {
|
|
|
|
|
+ if (sessionWorkspaceID) return sessionWorkspaceID
|
|
|
|
|
+ const workspaceParam = url.searchParams.get("workspace")
|
|
|
|
|
+ if (!workspaceParam) return undefined
|
|
|
|
|
+ const workspaceID = Schema.decodeUnknownOption(WorkspaceID)(workspaceParam)
|
|
|
|
|
+ if (Option.isNone(workspaceID)) return InvalidWorkspaceID
|
|
|
|
|
+ return workspaceID.value
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
|
|
function defaultDirectory(request: HttpServerRequest.HttpServerRequest, url: URL): string {
|
|
|
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || process.cwd()
|
|
return url.searchParams.get("directory") || request.headers["x-opencode-directory"] || process.cwd()
|
|
|
}
|
|
}
|
|
@@ -149,7 +161,10 @@ function planRequest(
|
|
|
return Effect.gen(function* () {
|
|
return Effect.gen(function* () {
|
|
|
const url = requestURL(request)
|
|
const url = requestURL(request)
|
|
|
const envWorkspaceID = configuredWorkspaceID()
|
|
const envWorkspaceID = configuredWorkspaceID()
|
|
|
- const workspaceID = selectedWorkspaceID(url, sessionWorkspaceID)
|
|
|
|
|
|
|
+ const workspaceID = url.pathname.startsWith("/api/")
|
|
|
|
|
+ ? selectedV2WorkspaceID(url, sessionWorkspaceID)
|
|
|
|
|
+ : selectedWorkspaceID(url, sessionWorkspaceID)
|
|
|
|
|
+ if (workspaceID === InvalidWorkspaceID) return RequestPlan.InvalidWorkspace()
|
|
|
const workspace = yield* resolveWorkspace(workspaceID, envWorkspaceID)
|
|
const workspace = yield* resolveWorkspace(workspaceID, envWorkspaceID)
|
|
|
|
|
|
|
|
if (workspaceID && workspace === undefined && !envWorkspaceID) {
|
|
if (workspaceID && workspace === undefined && !envWorkspaceID) {
|
|
@@ -170,6 +185,17 @@ function routeWorkspace<E>(
|
|
|
plan: RequestPlan,
|
|
plan: RequestPlan,
|
|
|
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, Socket.WebSocketConstructor | Workspace.Service> {
|
|
): Effect.Effect<HttpServerResponse.HttpServerResponse, E, Socket.WebSocketConstructor | Workspace.Service> {
|
|
|
return RequestPlan.$match(plan, {
|
|
return RequestPlan.$match(plan, {
|
|
|
|
|
+ InvalidWorkspace: () =>
|
|
|
|
|
+ Effect.succeed(
|
|
|
|
|
+ HttpServerResponse.jsonUnsafe(
|
|
|
|
|
+ new InvalidRequestError({
|
|
|
|
|
+ message: "Invalid workspace query parameter",
|
|
|
|
|
+ kind: "Query",
|
|
|
|
|
+ field: "workspace",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 400 },
|
|
|
|
|
+ ),
|
|
|
|
|
+ ),
|
|
|
MissingWorkspace: ({ workspaceID }) => Effect.succeed(missingWorkspaceResponse(workspaceID)),
|
|
MissingWorkspace: ({ workspaceID }) => Effect.succeed(missingWorkspaceResponse(workspaceID)),
|
|
|
Remote: ({ request, workspace, target, url }) => proxyRemote(client, request, workspace, target, url),
|
|
Remote: ({ request, workspace, target, url }) => proxyRemote(client, request, workspace, target, url),
|
|
|
Local: ({ directory, workspaceID }) =>
|
|
Local: ({ directory, workspaceID }) =>
|