refactor: rename workspace adapters (#25272)

This commit is contained in:
Kit Langton
2026-05-01 07:36:52 -04:00
committed by GitHub
parent 97ed9ba624
commit 8c79c58c4d
25 changed files with 225 additions and 225 deletions
+2 -2
View File
@@ -198,7 +198,7 @@ Use raw Effect HTTP routes where `HttpApi` does not fit. The goal is deleting Ho
| `project` | `bridged` | list, current, git init, update | | `project` | `bridged` | list, current, git init, update |
| `file` | `bridged` partial | find text/file/symbol, list/content/status | | `file` | `bridged` partial | find text/file/symbol, list/content/status |
| `mcp` | `bridged` | status, add, OAuth, connect/disconnect | | `mcp` | `bridged` | status, add, OAuth, connect/disconnect |
| `workspace` | `bridged` | adaptor/list/status/create/remove/session-restore | | `workspace` | `bridged` | adapter/list/status/create/remove/session-restore |
| top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose | | top-level instance routes | `bridged` | path, vcs, command, agent, skill, lsp, formatter, dispose |
| experimental JSON routes | `bridged` | console, tool, worktree list/mutations, global session list, resource list | | experimental JSON routes | `bridged` | console, tool, worktree list/mutations, global session list, resource list |
| `session` | `bridged` | read, lifecycle, prompt, message/part mutations, revert, permission reply | | `session` | `bridged` | read, lifecycle, prompt, message/part mutations, revert, permission reply |
@@ -290,7 +290,7 @@ This checklist tracks bridge parity only. Checked routes are available through t
### Workspace Routes ### Workspace Routes
- [x] `GET /experimental/workspace/adaptor` - list workspace adaptors. - [x] `GET /experimental/workspace/adapter` - list workspace adapters.
- [x] `POST /experimental/workspace` - create workspace. - [x] `POST /experimental/workspace` - create workspace.
- [x] `GET /experimental/workspace` - list workspaces. - [x] `GET /experimental/workspace` - list workspaces.
- [x] `GET /experimental/workspace/status` - workspace status. - [x] `GET /experimental/workspace/status` - workspace status.
+1 -1
View File
@@ -353,7 +353,7 @@ piecewise.
- [ ] `src/cli/cmd/tui/event.ts` - [ ] `src/cli/cmd/tui/event.ts`
- [ ] `src/cli/ui.ts` - [ ] `src/cli/ui.ts`
- [ ] `src/command/index.ts` - [ ] `src/command/index.ts`
- [x] `src/control-plane/adaptors/worktree.ts` - [x] `src/control-plane/adapters/worktree.ts`
- [x] `src/control-plane/types.ts` - [x] `src/control-plane/types.ts`
- [x] `src/control-plane/workspace.ts` - [x] `src/control-plane/workspace.ts`
- [ ] `src/file/index.ts` - [ ] `src/file/index.ts`
@@ -10,7 +10,7 @@ import { errorMessage } from "@/util/error"
import { useSDK } from "../context/sdk" import { useSDK } from "../context/sdk"
import { useToast } from "../ui/toast" import { useToast } from "../ui/toast"
type Adaptor = { type Adapter = {
type: string type: string
name: string name: string
description: string description: string
@@ -108,26 +108,26 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
const sdk = useSDK() const sdk = useSDK()
const toast = useToast() const toast = useToast()
const [creating, setCreating] = createSignal<string>() const [creating, setCreating] = createSignal<string>()
const [adaptors, setAdaptors] = createSignal<Adaptor[]>() const [adapters, setAdapters] = createSignal<Adapter[]>()
onMount(() => { onMount(() => {
dialog.setSize("medium") dialog.setSize("medium")
void (async () => { void (async () => {
const dir = sync.path.directory || sdk.directory const dir = sync.path.directory || sdk.directory
const url = new URL("/experimental/workspace/adaptor", sdk.url) const url = new URL("/experimental/workspace/adapter", sdk.url)
if (dir) url.searchParams.set("directory", dir) if (dir) url.searchParams.set("directory", dir)
const res = await sdk const res = await sdk
.fetch(url) .fetch(url)
.then((x) => x.json() as Promise<Adaptor[]>) .then((x) => x.json() as Promise<Adapter[]>)
.catch(() => undefined) .catch(() => undefined)
if (!res) { if (!res) {
toast.show({ toast.show({
message: "Failed to load workspace adaptors", message: "Failed to load workspace adapters",
variant: "error", variant: "error",
}) })
return return
} }
setAdaptors(res) setAdapters(res)
})() })()
}) })
@@ -142,13 +142,13 @@ export function DialogWorkspaceCreate(props: { onSelect: (workspaceID: string) =
}, },
] ]
} }
const list = adaptors() const list = adapters()
if (!list) { if (!list) {
return [ return [
{ {
title: "Loading workspaces...", title: "Loading workspaces...",
value: "loading" as const, value: "loading" as const,
description: "Fetching available workspace adaptors", description: "Fetching available workspace adapters",
}, },
] ]
} }
@@ -0,0 +1,45 @@
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdapter, WorkspaceAdapterEntry } from "../types"
import { WorktreeAdapter } from "./worktree"
const BUILTIN: Record<string, WorkspaceAdapter> = {
worktree: WorktreeAdapter,
}
const state = new Map<ProjectID, Map<string, WorkspaceAdapter>>()
export function getAdapter(projectID: ProjectID, type: string): WorkspaceAdapter {
const custom = state.get(projectID)?.get(type)
if (custom) return custom
const builtin = BUILTIN[type]
if (builtin) return builtin
throw new Error(`Unknown workspace adapter: ${type}`)
}
export async function listAdapters(projectID: ProjectID): Promise<WorkspaceAdapterEntry[]> {
const builtin = await Promise.all(
Object.entries(BUILTIN).map(async ([type, adapter]) => {
return {
type,
name: adapter.name,
description: adapter.description,
}
}),
)
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adapter]) => ({
type,
name: adapter.name,
description: adapter.description,
}))
return [...builtin, ...custom]
}
// Plugins can be loaded per-project so we need to scope them. If you
// want to install a global one pass `ProjectID.global`
export function registerAdapter(projectID: ProjectID, type: string, adapter: WorkspaceAdapter) {
const adapters = state.get(projectID) ?? new Map<string, WorkspaceAdapter>()
adapters.set(type, adapter)
state.set(projectID, adapters)
}
@@ -1,5 +1,5 @@
import { Schema } from "effect" import { Schema } from "effect"
import { type WorkspaceAdaptor, WorkspaceInfo } from "../types" import { type WorkspaceAdapter, WorkspaceInfo } from "../types"
const WorktreeConfig = Schema.Struct({ const WorktreeConfig = Schema.Struct({
name: WorkspaceInfo.fields.name, name: WorkspaceInfo.fields.name,
@@ -13,7 +13,7 @@ async function loadWorktree() {
return { AppRuntime, Worktree } return { AppRuntime, Worktree }
} }
export const WorktreeAdaptor: WorkspaceAdaptor = { export const WorktreeAdapter: WorkspaceAdapter = {
name: "Worktree", name: "Worktree",
description: "Create a git worktree", description: "Create a git worktree",
async configure(info) { async configure(info) {
@@ -1,45 +0,0 @@
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
import { WorktreeAdaptor } from "./worktree"
const BUILTIN: Record<string, WorkspaceAdaptor> = {
worktree: WorktreeAdaptor,
}
const state = new Map<ProjectID, Map<string, WorkspaceAdaptor>>()
export function getAdaptor(projectID: ProjectID, type: string): WorkspaceAdaptor {
const custom = state.get(projectID)?.get(type)
if (custom) return custom
const builtin = BUILTIN[type]
if (builtin) return builtin
throw new Error(`Unknown workspace adaptor: ${type}`)
}
export async function listAdaptors(projectID: ProjectID): Promise<WorkspaceAdaptorEntry[]> {
const builtin = await Promise.all(
Object.entries(BUILTIN).map(async ([type, adaptor]) => {
return {
type,
name: adaptor.name,
description: adaptor.description,
}
}),
)
const custom = [...(state.get(projectID)?.entries() ?? [])].map(([type, adaptor]) => ({
type,
name: adaptor.name,
description: adaptor.description,
}))
return [...builtin, ...custom]
}
// Plugins can be loaded per-project so we need to scope them. If you
// want to install a global one pass `ProjectID.global`
export function registerAdaptor(projectID: ProjectID, type: string, adaptor: WorkspaceAdaptor) {
const adaptors = state.get(projectID) ?? new Map<string, WorkspaceAdaptor>()
adaptors.set(type, adaptor)
state.set(projectID, adaptors)
}
+3 -3
View File
@@ -17,12 +17,12 @@ export const WorkspaceInfo = Schema.Struct({
.pipe(withStatics((s) => ({ zod: zod(s) }))) .pipe(withStatics((s) => ({ zod: zod(s) })))
export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>> export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
export const WorkspaceAdaptorEntry = Schema.Struct({ export const WorkspaceAdapterEntry = Schema.Struct({
type: Schema.String, type: Schema.String,
name: Schema.String, name: Schema.String,
description: Schema.String, description: Schema.String,
}).pipe(withStatics((s) => ({ zod: zod(s) }))) }).pipe(withStatics((s) => ({ zod: zod(s) })))
export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry> export type WorkspaceAdapterEntry = Schema.Schema.Type<typeof WorkspaceAdapterEntry>
export type Target = export type Target =
| { | {
@@ -35,7 +35,7 @@ export type Target =
headers?: HeadersInit headers?: HeadersInit
} }
export type WorkspaceAdaptor = { export type WorkspaceAdapter = {
name: string name: string
description: string description: string
configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo> configure(info: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
@@ -16,7 +16,7 @@ import { Filesystem } from "@/util/filesystem"
import { ProjectID } from "@/project/schema" import { ProjectID } from "@/project/schema"
import { Slug } from "@opencode-ai/core/util/slug" import { Slug } from "@opencode-ai/core/util/slug"
import { WorkspaceTable } from "./workspace.sql" import { WorkspaceTable } from "./workspace.sql"
import { getAdaptor } from "./adaptors" import { getAdapter } from "./adapters"
import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types" import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
import { WorkspaceID } from "./schema" import { WorkspaceID } from "./schema"
import { Session } from "@/session/session" import { Session } from "@/session/session"
@@ -335,8 +335,8 @@ export const layer = Layer.effect(
}) })
const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) { const syncWorkspaceLoop = Effect.fn("Workspace.syncWorkspaceLoop")(function* (space: Info) {
const adaptor = getAdaptor(space.projectID, space.type) const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space))) const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
if (target.type === "local") return if (target.type === "local") return
@@ -419,8 +419,8 @@ export const layer = Layer.effect(
const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) { const startSync = Effect.fn("Workspace.startSync")(function* (space: Info) {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
const adaptor = getAdaptor(space.projectID, space.type) const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space))) const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
if (target.type === "local") { if (target.type === "local") {
setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error") setStatus(space.id, (yield* Effect.promise(() => Filesystem.exists(target.directory))) ? "connected" : "error")
@@ -458,9 +458,9 @@ export const layer = Layer.effect(
const create = Effect.fn("Workspace.create")(function* (input: CreateInput) { const create = Effect.fn("Workspace.create")(function* (input: CreateInput) {
const id = WorkspaceID.ascending(input.id) const id = WorkspaceID.ascending(input.id)
const adaptor = getAdaptor(input.projectID, input.type) const adapter = getAdapter(input.projectID, input.type)
const config = yield* Effect.promise(() => const config = yield* Effect.promise(() =>
Promise.resolve(adaptor.configure({ ...input, id, name: Slug.create(), directory: null })), Promise.resolve(adapter.configure({ ...input, id, name: Slug.create(), directory: null })),
) )
const info: Info = { const info: Info = {
@@ -496,7 +496,7 @@ export const layer = Layer.effect(
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES, OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
} }
yield* Effect.promise(() => adaptor.create(config, env)) yield* Effect.promise(() => adapter.create(config, env))
yield* Effect.all( yield* Effect.all(
[ [
waitEvent({ waitEvent({
@@ -531,8 +531,8 @@ export const layer = Layer.effect(
workspaceID: input.workspaceID, workspaceID: input.workspaceID,
}) })
const adaptor = getAdaptor(space.projectID, space.type) const adapter = getAdapter(space.projectID, space.type)
const target = yield* Effect.promise(() => Promise.resolve(adaptor.target(space))) const target = yield* Effect.promise(() => Promise.resolve(adapter.target(space)))
yield* sync.run(Session.Event.Updated, { yield* sync.run(Session.Event.Updated, {
sessionID: input.sessionID, sessionID: input.sessionID,
@@ -726,12 +726,12 @@ export const layer = Layer.effect(
const info = fromRow(row) const info = fromRow(row)
yield* Effect.catch( yield* Effect.catch(
Effect.gen(function* () { Effect.gen(function* () {
const adaptor = getAdaptor(info.projectID, row.type) const adapter = getAdapter(info.projectID, row.type)
yield* Effect.tryPromise(() => Promise.resolve(adaptor.remove(info))) yield* Effect.tryPromise(() => Promise.resolve(adapter.remove(info)))
}), }),
() => () =>
Effect.sync(() => { Effect.sync(() => {
log.error("adaptor not available when removing workspace", { type: row.type }) log.error("adapter not available when removing workspace", { type: row.type })
}), }),
) )
+5 -5
View File
@@ -3,7 +3,7 @@ import type {
PluginInput, PluginInput,
Plugin as PluginInstance, Plugin as PluginInstance,
PluginModule, PluginModule,
WorkspaceAdaptor as PluginWorkspaceAdaptor, WorkspaceAdapter as PluginWorkspaceAdapter,
} from "@opencode-ai/plugin" } from "@opencode-ai/plugin"
import { Config } from "@/config/config" import { Config } from "@/config/config"
import { Bus } from "../bus" import { Bus } from "../bus"
@@ -24,8 +24,8 @@ import { InstanceState } from "@/effect/instance-state"
import { errorMessage } from "@/util/error" import { errorMessage } from "@/util/error"
import { PluginLoader } from "./loader" import { PluginLoader } from "./loader"
import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared" import { parsePluginSpecifier, readPluginId, readV1Plugin, resolvePluginId } from "./shared"
import { registerAdaptor } from "@/control-plane/adaptors" import { registerAdapter } from "@/control-plane/adapters"
import type { WorkspaceAdaptor } from "@/control-plane/types" import type { WorkspaceAdapter } from "@/control-plane/types"
const log = Log.create({ service: "plugin" }) const log = Log.create({ service: "plugin" })
@@ -138,8 +138,8 @@ export const layer = Layer.effect(
worktree: ctx.worktree, worktree: ctx.worktree,
directory: ctx.directory, directory: ctx.directory,
experimental_workspace: { experimental_workspace: {
register(type: string, adaptor: PluginWorkspaceAdaptor) { register(type: string, adapter: PluginWorkspaceAdapter) {
registerAdaptor(ctx.project.id, type, adaptor as WorkspaceAdaptor) registerAdapter(ctx.project.id, type, adapter as WorkspaceAdapter)
}, },
}, },
get serverUrl(): URL { get serverUrl(): URL {
@@ -2,10 +2,10 @@ import { Hono } from "hono"
import { describeRoute, resolver, validator } from "hono-openapi" import { describeRoute, resolver, validator } from "hono-openapi"
import z from "zod" import z from "zod"
import { Effect } from "effect" import { Effect } from "effect"
import { listAdaptors } from "@/control-plane/adaptors" import { listAdapters } from "@/control-plane/adapters"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { WorkspaceAdaptorEntry } from "@/control-plane/types" import { WorkspaceAdapterEntry } from "@/control-plane/types"
import { zodObject } from "@/util/effect-zod" import { zodObject } from "@/util/effect-zod"
import { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
import { errors } from "../../error" import { errors } from "../../error"
@@ -18,24 +18,24 @@ const log = Log.create({ service: "server.workspace" })
export const WorkspaceRoutes = lazy(() => export const WorkspaceRoutes = lazy(() =>
new Hono() new Hono()
.get( .get(
"/adaptor", "/adapter",
describeRoute({ describeRoute({
summary: "List workspace adaptors", summary: "List workspace adapters",
description: "List all available workspace adaptors for the current project.", description: "List all available workspace adapters for the current project.",
operationId: "experimental.workspace.adaptor.list", operationId: "experimental.workspace.adapter.list",
responses: { responses: {
200: { 200: {
description: "Workspace adaptors", description: "Workspace adapters",
content: { content: {
"application/json": { "application/json": {
schema: resolver(z.array(zodObject(WorkspaceAdaptorEntry))), schema: resolver(z.array(zodObject(WorkspaceAdapterEntry))),
}, },
}, },
}, },
}, },
}), }),
async (c) => { async (c) => {
return c.json(await listAdaptors(Instance.project.id)) return c.json(await listAdapters(Instance.project.id))
}, },
) )
.post( .post(
@@ -1,5 +1,5 @@
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
import { WorkspaceAdaptorEntry } from "@/control-plane/types" import { WorkspaceAdapterEntry } from "@/control-plane/types"
import { NonNegativeInt } from "@/util/schema" import { NonNegativeInt } from "@/util/schema"
import { Schema, Struct } from "effect" import { Schema, Struct } from "effect"
import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi"
@@ -16,7 +16,7 @@ export const SessionRestoreResponse = Schema.Struct({
}) })
export const WorkspacePaths = { export const WorkspacePaths = {
adaptors: `${root}/adaptor`, adapters: `${root}/adapter`,
list: root, list: root,
status: `${root}/status`, status: `${root}/status`,
remove: `${root}/:id`, remove: `${root}/:id`,
@@ -27,13 +27,13 @@ export const WorkspaceApi = HttpApi.make("workspace")
.add( .add(
HttpApiGroup.make("workspace") HttpApiGroup.make("workspace")
.add( .add(
HttpApiEndpoint.get("adaptors", WorkspacePaths.adaptors, { HttpApiEndpoint.get("adapters", WorkspacePaths.adapters, {
success: described(Schema.Array(WorkspaceAdaptorEntry), "Workspace adaptors"), success: described(Schema.Array(WorkspaceAdapterEntry), "Workspace adapters"),
}).annotateMerge( }).annotateMerge(
OpenApi.annotations({ OpenApi.annotations({
identifier: "experimental.workspace.adaptor.list", identifier: "experimental.workspace.adapter.list",
summary: "List workspace adaptors", summary: "List workspace adapters",
description: "List all available workspace adaptors for the current project.", description: "List all available workspace adapters for the current project.",
}), }),
), ),
HttpApiEndpoint.get("list", WorkspacePaths.list, { HttpApiEndpoint.get("list", WorkspacePaths.list, {
@@ -1,4 +1,4 @@
import { listAdaptors } from "@/control-plane/adaptors" import { listAdapters } from "@/control-plane/adapters"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
import * as InstanceState from "@/effect/instance-state" import * as InstanceState from "@/effect/instance-state"
import { Effect } from "effect" import { Effect } from "effect"
@@ -10,9 +10,9 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* Workspace.Service const workspace = yield* Workspace.Service
const adaptors = Effect.fn("WorkspaceHttpApi.adaptors")(function* () { const adapters = Effect.fn("WorkspaceHttpApi.adapters")(function* () {
const instance = yield* InstanceState.context const instance = yield* InstanceState.context
return yield* Effect.promise(() => listAdaptors(instance.project.id)) return yield* Effect.promise(() => listAdapters(instance.project.id))
}) })
const list = Effect.fn("WorkspaceHttpApi.list")(function* () { const list = Effect.fn("WorkspaceHttpApi.list")(function* () {
@@ -51,7 +51,7 @@ export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspac
}) })
return handlers return handlers
.handle("adaptors", adaptors) .handle("adapters", adapters)
.handle("list", list) .handle("list", list)
.handle("create", create) .handle("create", create)
.handle("status", status) .handle("status", status)
@@ -1,4 +1,4 @@
import { getAdaptor } from "@/control-plane/adaptors" import { getAdapter } from "@/control-plane/adapters"
import { WorkspaceID } from "@/control-plane/schema" import { WorkspaceID } from "@/control-plane/schema"
import type { Target } from "@/control-plane/types" import type { Target } from "@/control-plane/types"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
@@ -89,8 +89,8 @@ function missingWorkspaceResponse(id: WorkspaceID): HttpServerResponse.HttpServe
function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> { function resolveTarget(workspace: Workspace.Info): Effect.Effect<Target> {
return Effect.gen(function* () { return Effect.gen(function* () {
const adaptor = yield* Effect.sync(() => getAdaptor(workspace.projectID, workspace.type)) const adapter = yield* Effect.sync(() => getAdapter(workspace.projectID, workspace.type))
return yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace))) return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
}) })
} }
+3 -3
View File
@@ -1,6 +1,6 @@
import type { MiddlewareHandler } from "hono" import type { MiddlewareHandler } from "hono"
import type { UpgradeWebSocket } from "hono/ws" import type { UpgradeWebSocket } from "hono/ws"
import { getAdaptor } from "@/control-plane/adaptors" import { getAdapter } from "@/control-plane/adapters"
import { WorkspaceID } from "@/control-plane/schema" import { WorkspaceID } from "@/control-plane/schema"
import { WorkspaceContext } from "@/control-plane/workspace-context" import { WorkspaceContext } from "@/control-plane/workspace-context"
import { Workspace } from "@/control-plane/workspace" import { Workspace } from "@/control-plane/workspace"
@@ -91,8 +91,8 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
return next() return next()
} }
const adaptor = getAdaptor(workspace.projectID, workspace.type) const adapter = getAdapter(workspace.projectID, workspace.type)
const target = await adaptor.target(workspace) const target = await adapter.target(workspace)
if (target.type === "local") { if (target.type === "local") {
return WorkspaceContext.provide({ return WorkspaceContext.provide({
@@ -1,5 +1,5 @@
import { describe, expect, test } from "bun:test" import { describe, expect, test } from "bun:test"
import { getAdaptor, registerAdaptor } from "../../src/control-plane/adaptors" import { getAdapter, registerAdapter } from "../../src/control-plane/adapters"
import { ProjectID } from "../../src/project/schema" import { ProjectID } from "../../src/project/schema"
import type { WorkspaceInfo } from "../../src/control-plane/types" import type { WorkspaceInfo } from "../../src/control-plane/types"
@@ -15,7 +15,7 @@ function info(projectID: WorkspaceInfo["projectID"], type: string): WorkspaceInf
} }
} }
function adaptor(dir: string) { function adapter(dir: string) {
return { return {
name: dir, name: dir,
description: dir, description: dir,
@@ -33,19 +33,19 @@ function adaptor(dir: string) {
} }
} }
describe("control-plane/adaptors", () => { describe("control-plane/adapters", () => {
test("isolates custom adaptors by project", async () => { test("isolates custom adapters by project", async () => {
const type = `demo-${Math.random().toString(36).slice(2)}` const type = `demo-${Math.random().toString(36).slice(2)}`
const one = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`) const one = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
const two = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`) const two = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
registerAdaptor(one, type, adaptor("/one")) registerAdapter(one, type, adapter("/one"))
registerAdaptor(two, type, adaptor("/two")) registerAdapter(two, type, adapter("/two"))
expect(await (await getAdaptor(one, type)).target(info(one, type))).toEqual({ expect(await (await getAdapter(one, type)).target(info(one, type))).toEqual({
type: "local", type: "local",
directory: "/one", directory: "/one",
}) })
expect(await (await getAdaptor(two, type)).target(info(two, type))).toEqual({ expect(await (await getAdapter(two, type)).target(info(two, type))).toEqual({
type: "local", type: "local",
directory: "/two", directory: "/two",
}) })
@@ -54,16 +54,16 @@ describe("control-plane/adaptors", () => {
test("latest install wins within a project", async () => { test("latest install wins within a project", async () => {
const type = `demo-${Math.random().toString(36).slice(2)}` const type = `demo-${Math.random().toString(36).slice(2)}`
const id = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`) const id = ProjectID.make(`project-${Math.random().toString(36).slice(2)}`)
registerAdaptor(id, type, adaptor("/one")) registerAdapter(id, type, adapter("/one"))
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({ expect(await (await getAdapter(id, type)).target(info(id, type))).toEqual({
type: "local", type: "local",
directory: "/one", directory: "/one",
}) })
registerAdaptor(id, type, adaptor("/two")) registerAdapter(id, type, adapter("/two"))
expect(await (await getAdaptor(id, type)).target(info(id, type))).toEqual({ expect(await (await getAdapter(id, type)).target(info(id, type))).toEqual({
type: "local", type: "local",
directory: "/two", directory: "/two",
}) })
@@ -23,10 +23,10 @@ import { EventSequenceTable, EventTable } from "@/sync/event.sql"
import { resetDatabase } from "../fixture/db" import { resetDatabase } from "../fixture/db"
import { provideTmpdirInstance, tmpdir } from "../fixture/fixture" import { provideTmpdirInstance, tmpdir } from "../fixture/fixture"
import { testEffect } from "../lib/effect" import { testEffect } from "../lib/effect"
import { registerAdaptor } from "../../src/control-plane/adaptors" import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceID } from "../../src/control-plane/schema" import { WorkspaceID } from "../../src/control-plane/schema"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql" import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import type { Target, WorkspaceAdaptor, WorkspaceInfo } from "../../src/control-plane/types" import type { Target, WorkspaceAdapter, WorkspaceInfo } from "../../src/control-plane/types"
import * as WorkspaceOld from "../../src/control-plane/workspace" import * as WorkspaceOld from "../../src/control-plane/workspace"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
@@ -53,8 +53,8 @@ type RecordedCreate = {
from?: WorkspaceInfo from?: WorkspaceInfo
} }
type RecordedAdaptor = { type RecordedAdapter = {
adaptor: WorkspaceAdaptor adapter: WorkspaceAdapter
calls: { calls: {
configure: WorkspaceInfo[] configure: WorkspaceInfo[]
create: RecordedCreate[] create: RecordedCreate[]
@@ -165,13 +165,13 @@ function eventuallyEffect(effect: Effect.Effect<void>, timeout = 1500) {
}) })
} }
function recordedAdaptor(input: { function recordedAdapter(input: {
target: (info: WorkspaceInfo) => Target | Promise<Target> target: (info: WorkspaceInfo) => Target | Promise<Target>
configure?: (info: WorkspaceInfo) => WorkspaceInfo | Promise<WorkspaceInfo> configure?: (info: WorkspaceInfo) => WorkspaceInfo | Promise<WorkspaceInfo>
create?: (info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo) => Promise<void> create?: (info: WorkspaceInfo, env: Record<string, string | undefined>, from?: WorkspaceInfo) => Promise<void>
remove?: (info: WorkspaceInfo) => Promise<void> remove?: (info: WorkspaceInfo) => Promise<void>
}): RecordedAdaptor { }): RecordedAdapter {
const calls: RecordedAdaptor["calls"] = { const calls: RecordedAdapter["calls"] = {
configure: [], configure: [],
create: [], create: [],
remove: [], remove: [],
@@ -180,7 +180,7 @@ function recordedAdaptor(input: {
return { return {
calls, calls,
adaptor: { adapter: {
name: "recorded", name: "recorded",
description: "recorded", description: "recorded",
configure(info) { configure(info) {
@@ -207,8 +207,8 @@ function recordedAdaptor(input: {
} }
} }
function localAdaptor(dir: string, input?: { createDir?: boolean; remove?: (info: WorkspaceInfo) => Promise<void> }) { function localAdapter(dir: string, input?: { createDir?: boolean; remove?: (info: WorkspaceInfo) => Promise<void> }) {
return recordedAdaptor({ return recordedAdapter({
configure(info) { configure(info) {
return { ...info, directory: dir } return { ...info, directory: dir }
}, },
@@ -223,8 +223,8 @@ function localAdaptor(dir: string, input?: { createDir?: boolean; remove?: (info
}) })
} }
function remoteAdaptor(url: string, input?: { directory?: string | null; headers?: HeadersInit }) { function remoteAdapter(url: string, input?: { directory?: string | null; headers?: HeadersInit }) {
return recordedAdaptor({ return recordedAdapter({
configure(info) { configure(info) {
return { ...info, directory: input?.directory ?? info.directory } return { ...info, directory: input?.directory ?? info.directory }
}, },
@@ -429,7 +429,7 @@ describe("workspace-old CRUD", () => {
const workspaceID = WorkspaceID.ascending("wrk_create_local") const workspaceID = WorkspaceID.ascending("wrk_create_local")
const type = unique("create-local") const type = unique("create-local")
const targetDir = path.join(dir, "created-local") const targetDir = path.join(dir, "created-local")
const recorded = recordedAdaptor({ const recorded = recordedAdapter({
configure(info) { configure(info) {
return { return {
...info, ...info,
@@ -446,7 +446,7 @@ describe("workspace-old CRUD", () => {
return { type: "local", directory: targetDir } return { type: "local", directory: targetDir }
}, },
}) })
registerAdaptor(Instance.project.id, type, recorded.adaptor) registerAdapter(Instance.project.id, type, recorded.adapter)
const info = await createWorkspace({ const info = await createWorkspace({
id: workspaceID, id: workspaceID,
@@ -489,17 +489,17 @@ describe("workspace-old CRUD", () => {
test("create propagates configure failures and does not insert a workspace", async () => { test("create propagates configure failures and does not insert a workspace", async () => {
await withInstance(async () => { await withInstance(async () => {
const type = unique("configure-failure") const type = unique("configure-failure")
registerAdaptor( registerAdapter(
Instance.project.id, Instance.project.id,
type, type,
recordedAdaptor({ recordedAdapter({
configure() { configure() {
throw new Error("configure exploded") throw new Error("configure exploded")
}, },
target() { target() {
return { type: "local", directory: "/unused" } return { type: "local", directory: "/unused" }
}, },
}).adaptor, }).adapter,
) )
await expect( await expect(
@@ -509,10 +509,10 @@ describe("workspace-old CRUD", () => {
}) })
}) })
test("create leaves the inserted row when adaptor create fails", async () => { test("create leaves the inserted row when adapter create fails", async () => {
await withInstance(async () => { await withInstance(async () => {
const type = unique("create-failure") const type = unique("create-failure")
const recorded = recordedAdaptor({ const recorded = recordedAdapter({
async create() { async create() {
throw new Error("create exploded") throw new Error("create exploded")
}, },
@@ -520,7 +520,7 @@ describe("workspace-old CRUD", () => {
return { type: "local", directory: "/unused" } return { type: "local", directory: "/unused" }
}, },
}) })
registerAdaptor(Instance.project.id, type, recorded.adaptor) registerAdapter(Instance.project.id, type, recorded.adapter)
await expect( await expect(
createWorkspace({ type, branch: "branch", projectID: Instance.project.id, extra: { x: 1 } }), createWorkspace({ type, branch: "branch", projectID: Instance.project.id, extra: { x: 1 } }),
@@ -538,8 +538,8 @@ describe("workspace-old CRUD", () => {
await withInstance(async (dir) => { await withInstance(async (dir) => {
const type = unique("local-error") const type = unique("local-error")
const missing = path.join(dir, "missing-local-target") const missing = path.join(dir, "missing-local-target")
const recorded = localAdaptor(missing, { createDir: false }) const recorded = localAdapter(missing, { createDir: false })
registerAdaptor(Instance.project.id, type, recorded.adaptor) registerAdapter(Instance.project.id, type, recorded.adapter)
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null }) const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
@@ -576,8 +576,8 @@ describe("workspace-old CRUD", () => {
Effect.gen(function* () { Effect.gen(function* () {
const workspace = yield* WorkspaceOld.Service const workspace = yield* WorkspaceOld.Service
const type = unique("remote-create") const type = unique("remote-create")
const recorded = remoteAdaptor(`${url}/base/?ignored=1#hash`, { directory: dir }) const recorded = remoteAdapter(`${url}/base/?ignored=1#hash`, { directory: dir })
registerAdaptor(Instance.project.id, type, recorded.adaptor) registerAdapter(Instance.project.id, type, recorded.adapter)
const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null }) const info = yield* workspace.create({ type, branch: null, projectID: Instance.project.id, extra: null })
@@ -603,11 +603,11 @@ describe("workspace-old CRUD", () => {
}) })
}) })
test("remove deletes the workspace, associated sessions, adaptor resources, and status", async () => { test("remove deletes the workspace, associated sessions, adapter resources, and status", async () => {
await withInstance(async (dir) => { await withInstance(async (dir) => {
const type = unique("remove-local") const type = unique("remove-local")
const recorded = localAdaptor(path.join(dir, "remove-local")) const recorded = localAdapter(path.join(dir, "remove-local"))
registerAdaptor(Instance.project.id, type, recorded.adaptor) registerAdapter(Instance.project.id, type, recorded.adapter)
const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null }) const info = await createWorkspace({ type, branch: null, projectID: Instance.project.id, extra: null })
const one = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) const one = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
const two = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) const two = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
@@ -628,21 +628,21 @@ describe("workspace-old CRUD", () => {
}) })
}) })
test("remove still deletes the row when the adaptor cannot remove resources", async () => { test("remove still deletes the row when the adapter cannot remove resources", async () => {
await withInstance(async () => { await withInstance(async () => {
const type = unique("remove-throws") const type = unique("remove-throws")
const info = workspaceInfo(Instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") }) const info = workspaceInfo(Instance.project.id, type, { id: WorkspaceID.ascending("wrk_remove_throws") })
registerAdaptor( registerAdapter(
Instance.project.id, Instance.project.id,
type, type,
recordedAdaptor({ recordedAdapter({
async remove() { async remove() {
throw new Error("remove exploded") throw new Error("remove exploded")
}, },
target() { target() {
return { type: "local", directory: "/unused" } return { type: "local", directory: "/unused" }
}, },
}).adaptor, }).adapter,
) )
insertWorkspace(info) insertWorkspace(info)
@@ -661,7 +661,7 @@ describe("workspace-old sync state", () => {
const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({}))) const session = await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))
attachSessionToWorkspace(session.id, info.id) attachSessionToWorkspace(session.id, info.id)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, localAdaptor(path.join(dir, "flag-disabled")).adaptor) registerAdapter(Instance.project.id, type, localAdapter(path.join(dir, "flag-disabled")).adapter)
startWorkspaceSyncing(Instance.project.id) startWorkspaceSyncing(Instance.project.id)
await delay(25) await delay(25)
@@ -682,8 +682,8 @@ describe("workspace-old sync state", () => {
await fs.mkdir(withoutSessionDir, { recursive: true }) await fs.mkdir(withoutSessionDir, { recursive: true })
insertWorkspace(withSession) insertWorkspace(withSession)
insertWorkspace(withoutSession) insertWorkspace(withoutSession)
registerAdaptor(Instance.project.id, withSessionType, localAdaptor(withSessionDir).adaptor) registerAdapter(Instance.project.id, withSessionType, localAdapter(withSessionDir).adapter)
registerAdaptor(Instance.project.id, withoutSessionType, localAdaptor(withoutSessionDir).adaptor) registerAdapter(Instance.project.id, withoutSessionType, localAdapter(withoutSessionDir).adapter)
attachSessionToWorkspace( attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, (await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
withSession.id, withSession.id,
@@ -707,10 +707,10 @@ describe("workspace-old sync state", () => {
const type = unique("missing-local") const type = unique("missing-local")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor( registerAdapter(
Instance.project.id, Instance.project.id,
type, type,
localAdaptor(path.join(dir, "missing-target"), { createDir: false }).adaptor, localAdapter(path.join(dir, "missing-target"), { createDir: false }).adapter,
) )
attachSessionToWorkspace( attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, (await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
@@ -738,7 +738,7 @@ describe("workspace-old sync state", () => {
const target = path.join(dir, "dedupe-local") const target = path.join(dir, "dedupe-local")
await fs.mkdir(target, { recursive: true }) await fs.mkdir(target, { recursive: true })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, localAdaptor(target).adaptor) registerAdapter(Instance.project.id, type, localAdapter(target).adapter)
attachSessionToWorkspace( attachSessionToWorkspace(
(await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id, (await AppRuntime.runPromise(SessionNs.Service.use((svc) => svc.create({})))).id,
info.id, info.id,
@@ -795,7 +795,7 @@ describe("workspace-old sync state", () => {
const type = unique("remote-start") const type = unique("remote-start")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sync`).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sync`).adapter)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id) attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -850,7 +850,7 @@ describe("workspace-old sync state", () => {
const type = unique("remote-connect-fail") const type = unique("remote-connect-fail")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/failed`).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/failed`).adapter)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id) attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -890,7 +890,7 @@ describe("workspace-old sync state", () => {
const type = unique("remote-history-fail") const type = unique("remote-history-fail")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history-failed`).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history-failed`).adapter)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id) attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -947,7 +947,7 @@ describe("workspace-old sync state", () => {
const type = unique("history-replay") const type = unique("history-replay")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/history`).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/history`).adapter)
const session = yield* sessionSvc.create({ title: "before history" }) const session = yield* sessionSvc.create({ title: "before history" })
attachSessionToWorkspace(session.id, info.id) attachSessionToWorkspace(session.id, info.id)
historySessionID = session.id historySessionID = session.id
@@ -1014,7 +1014,7 @@ describe("workspace-old sync state", () => {
const type = unique("sse-forward") const type = unique("sse-forward")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-forward`).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-forward`).adapter)
attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id) attachSessionToWorkspace((yield* sessionSvc.create({})).id, info.id)
yield* workspace.startWorkspaceSyncing(Instance.project.id) yield* workspace.startWorkspaceSyncing(Instance.project.id)
@@ -1095,7 +1095,7 @@ describe("workspace-old sync state", () => {
const type = unique("sse-sync") const type = unique("sse-sync")
const info = workspaceInfo(Instance.project.id, type) const info = workspaceInfo(Instance.project.id, type)
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/sse-sync`).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/sse-sync`).adapter)
const session = yield* sessionSvc.create({ title: "before sse" }) const session = yield* sessionSvc.create({ title: "before sse" })
attachSessionToWorkspace(session.id, info.id) attachSessionToWorkspace(session.id, info.id)
sseSessionID = session.id sseSessionID = session.id
@@ -1232,7 +1232,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-missing-session") const type = unique("restore-missing-session")
const info = workspaceInfo(Instance.project.id, type, { directory: dir }) const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor) registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
await expect( await expect(
restoreWorkspaceSession({ workspaceID: info.id, sessionID: SessionID.descending("ses_missing_restore") }), restoreWorkspaceSession({ workspaceID: info.id, sessionID: SessionID.descending("ses_missing_restore") }),
@@ -1273,13 +1273,13 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-remote") const type = unique("restore-remote")
const info = workspaceInfo(Instance.project.id, type, { directory: dir }) const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor( registerAdapter(
Instance.project.id, Instance.project.id,
type, type,
remoteAdaptor(`${url}/restore/?ignored=1#hash`, { remoteAdapter(`${url}/restore/?ignored=1#hash`, {
directory: dir, directory: dir,
headers: { authorization: "Bearer restore" }, headers: { authorization: "Bearer restore" },
}).adaptor, }).adapter,
) )
const session = yield* sessionSvc.create({ title: "restore remote" }) const session = yield* sessionSvc.create({ title: "restore remote" })
replaceSessionEvents(session.id, 24) replaceSessionEvents(session.id, 24)
@@ -1353,7 +1353,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-null-dir") const type = unique("restore-null-dir")
const info = workspaceInfo(Instance.project.id, type, { directory: null }) const info = workspaceInfo(Instance.project.id, type, { directory: null })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/null-dir`, { directory: null }).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/null-dir`, { directory: null }).adapter)
const session = yield* sessionSvc.create({ title: "null dir" }) const session = yield* sessionSvc.create({ title: "null dir" })
replaceSessionEvents(session.id, 0) replaceSessionEvents(session.id, 0)
@@ -1397,7 +1397,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-remote-fail") const type = unique("restore-remote-fail")
const info = workspaceInfo(Instance.project.id, type, { directory: dir }) const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/fail`, { directory: dir }).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/fail`, { directory: dir }).adapter)
const session = yield* sessionSvc.create({ title: "restore fail" }) const session = yield* sessionSvc.create({ title: "restore fail" })
replaceSessionEvents(session.id, 11) replaceSessionEvents(session.id, 11)
@@ -1437,7 +1437,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-local") const type = unique("restore-local")
const info = workspaceInfo(Instance.project.id, type, { directory: dir }) const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, localAdaptor(dir).adaptor) registerAdapter(Instance.project.id, type, localAdapter(dir).adapter)
const session = yield* sessionSvc.create({ title: "restore local" }) const session = yield* sessionSvc.create({ title: "restore local" })
replaceSessionEvents(session.id, 20) replaceSessionEvents(session.id, 20)
@@ -1488,7 +1488,7 @@ describe("workspace-old sessionRestore", () => {
const type = unique("restore-real-events") const type = unique("restore-real-events")
const info = workspaceInfo(Instance.project.id, type, { directory: dir }) const info = workspaceInfo(Instance.project.id, type, { directory: dir })
insertWorkspace(info) insertWorkspace(info)
registerAdaptor(Instance.project.id, type, remoteAdaptor(`${url}/real`, { directory: dir }).adaptor) registerAdapter(Instance.project.id, type, remoteAdapter(`${url}/real`, { directory: dir }).adapter)
const session = yield* sessionSvc.create({ title: "real events" }) const session = yield* sessionSvc.create({ title: "real events" })
for (let i = 0; i < 3; i++) { for (let i = 0; i < 3; i++) {
const msg = yield* sessionSvc.updateMessage({ const msg = yield* sessionSvc.updateMessage({
@@ -34,7 +34,7 @@ afterAll(() => {
}) })
describe("plugin.workspace", () => { describe("plugin.workspace", () => {
it.live("plugin can install a workspace adaptor", () => it.live("plugin can install a workspace adapter", () =>
provideTmpdirInstance((dir) => provideTmpdirInstance((dir) =>
Effect.gen(function* () { Effect.gen(function* () {
const type = `plug-${Math.random().toString(36).slice(2)}` const type = `plug-${Math.random().toString(36).slice(2)}`
@@ -48,7 +48,7 @@ describe("plugin.workspace", () => {
"export default async ({ experimental_workspace }) => {", "export default async ({ experimental_workspace }) => {",
` experimental_workspace.register(${JSON.stringify(type)}, {`, ` experimental_workspace.register(${JSON.stringify(type)}, {`,
' name: "plug",', ' name: "plug",',
' description: "plugin workspace adaptor",', ' description: "plugin workspace adapter",',
" configure(input) {", " configure(input) {",
` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`, ` return { ...input, name: "plug", branch: "plug/main", directory: ${JSON.stringify(space)} }`,
" },", " },",
@@ -7,8 +7,8 @@ import { HttpClient, HttpClientRequest, HttpRouter, HttpServerResponse } from "e
import * as Socket from "effect/unstable/socket/Socket" import * as Socket from "effect/unstable/socket/Socket"
import { mkdir } from "node:fs/promises" import { mkdir } from "node:fs/promises"
import path from "node:path" import path from "node:path"
import { registerAdaptor } from "../../src/control-plane/adaptors" import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdaptor } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace" import { Workspace } from "../../src/control-plane/workspace"
import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref" import { InstanceRef, WorkspaceRef } from "../../src/effect/instance-ref"
import { Instance } from "../../src/project/instance" import { Instance } from "../../src/project/instance"
@@ -49,7 +49,7 @@ const instanceContextTestLayer = instanceRouterMiddleware
.combine(workspaceRouterMiddleware) .combine(workspaceRouterMiddleware)
.layer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal)) .layer.pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))
const localAdaptor = (directory: string): WorkspaceAdaptor => ({ const localAdapter = (directory: string): WorkspaceAdapter => ({
name: "Local Test", name: "Local Test",
description: "Create a local test workspace", description: "Create a local test workspace",
configure: (info) => ({ ...info, name: "local-test", directory }), configure: (info) => ({ ...info, name: "local-test", directory }),
@@ -63,7 +63,7 @@ const localAdaptor = (directory: string): WorkspaceAdaptor => ({
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) => const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
Effect.acquireRelease( Effect.acquireRelease(
Effect.gen(function* () { Effect.gen(function* () {
registerAdaptor(input.projectID, input.type, localAdaptor(input.directory)) registerAdapter(input.projectID, input.type, localAdapter(input.directory))
const workspace = yield* Workspace.Service const workspace = yield* Workspace.Service
return yield* workspace.create({ return yield* workspace.create({
type: input.type, type: input.type,
@@ -3,8 +3,8 @@ import { mkdir } from "node:fs/promises"
import path from "node:path" import path from "node:path"
import { Effect } from "effect" import { Effect } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { registerAdaptor } from "../../src/control-plane/adaptors" import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdaptor } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace" import { Workspace } from "../../src/control-plane/workspace"
import { PermissionID } from "../../src/permission/schema" import { PermissionID } from "../../src/permission/schema"
import { ModelID, ProviderID } from "../../src/provider/schema" import { ModelID, ProviderID } from "../../src/provider/schema"
@@ -82,7 +82,7 @@ function createTextMessage(directory: string, sessionID: SessionID, text: string
) )
} }
const localAdaptor = (directory: string): WorkspaceAdaptor => ({ const localAdapter = (directory: string): WorkspaceAdapter => ({
name: "Local Test", name: "Local Test",
description: "Create a local test workspace", description: "Create a local test workspace",
configure: (info) => ({ ...info, name: "local-test", directory }), configure: (info) => ({ ...info, name: "local-test", directory }),
@@ -95,7 +95,7 @@ const localAdaptor = (directory: string): WorkspaceAdaptor => ({
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) => const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
Effect.gen(function* () { Effect.gen(function* () {
registerAdaptor(input.projectID, input.type, localAdaptor(input.directory)) registerAdapter(input.projectID, input.type, localAdapter(input.directory))
return yield* Workspace.Service.use((svc) => return yield* Workspace.Service.use((svc) =>
svc.create({ svc.create({
type: input.type, type: input.type,
@@ -15,9 +15,9 @@ import * as Socket from "effect/unstable/socket/Socket"
import Http from "node:http" import Http from "node:http"
import { mkdir } from "node:fs/promises" import { mkdir } from "node:fs/promises"
import path from "node:path" import path from "node:path"
import { registerAdaptor } from "../../src/control-plane/adaptors" import { registerAdapter } from "../../src/control-plane/adapters"
import { WorkspaceID } from "../../src/control-plane/schema" import { WorkspaceID } from "../../src/control-plane/schema"
import type { WorkspaceAdaptor } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace" import { Workspace } from "../../src/control-plane/workspace"
import { WorkspaceTable } from "../../src/control-plane/workspace.sql" import { WorkspaceTable } from "../../src/control-plane/workspace.sql"
import { Project } from "../../src/project/project" import { Project } from "../../src/project/project"
@@ -82,7 +82,7 @@ const listenAdditionalServer = <E, R>(handler: TestHandler<E, R>) =>
return HttpServer.formatAddress(server.address) return HttpServer.formatAddress(server.address)
}) })
const localAdaptor = (directory: string): WorkspaceAdaptor => ({ const localAdapter = (directory: string): WorkspaceAdapter => ({
name: "Local Test", name: "Local Test",
description: "Create a local test workspace", description: "Create a local test workspace",
configure: (info) => ({ ...info, name: "local-test", directory }), configure: (info) => ({ ...info, name: "local-test", directory }),
@@ -93,7 +93,7 @@ const localAdaptor = (directory: string): WorkspaceAdaptor => ({
target: () => ({ type: "local" as const, directory }), target: () => ({ type: "local" as const, directory }),
}) })
const remoteAdaptor = (directory: string, url: string, headers?: HeadersInit): WorkspaceAdaptor => ({ const remoteAdapter = (directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter => ({
name: "Remote Test", name: "Remote Test",
description: "Create a remote test workspace", description: "Create a remote test workspace",
configure: (info) => ({ ...info, name: "remote-test", directory }), configure: (info) => ({ ...info, name: "remote-test", directory }),
@@ -116,10 +116,10 @@ const syncResponse = (request: HttpServerRequest.HttpServerRequest) => {
return undefined return undefined
} }
const createWorkspace = (input: { projectID: Project.Info["id"]; type: string; adaptor: WorkspaceAdaptor }) => const createWorkspace = (input: { projectID: Project.Info["id"]; type: string; adapter: WorkspaceAdapter }) =>
Effect.acquireRelease( Effect.acquireRelease(
Effect.gen(function* () { Effect.gen(function* () {
registerAdaptor(input.projectID, input.type, input.adaptor) registerAdapter(input.projectID, input.type, input.adapter)
const workspace = yield* Workspace.Service const workspace = yield* Workspace.Service
return yield* workspace.create({ return yield* workspace.create({
type: input.type, type: input.type,
@@ -144,14 +144,14 @@ const createRemoteWorkspace = (input: {
createWorkspace({ createWorkspace({
projectID: input.projectID, projectID: input.projectID,
type: input.type, type: input.type,
adaptor: remoteAdaptor(path.join(input.dir, `.${input.type}`), input.url, input.headers), adapter: remoteAdapter(path.join(input.dir, `.${input.type}`), input.url, input.headers),
}) })
const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) => const createLocalWorkspace = (input: { projectID: Project.Info["id"]; type: string; directory: string }) =>
createWorkspace({ createWorkspace({
projectID: input.projectID, projectID: input.projectID,
type: input.type, type: input.type,
adaptor: localAdaptor(input.directory), adapter: localAdapter(input.directory),
}) })
const insertRemoteWorkspaceWithoutSync = (input: { const insertRemoteWorkspaceWithoutSync = (input: {
@@ -162,7 +162,7 @@ const insertRemoteWorkspaceWithoutSync = (input: {
}) => }) =>
Effect.sync(() => { Effect.sync(() => {
const id = WorkspaceID.ascending() const id = WorkspaceID.ascending()
registerAdaptor(input.projectID, input.type, remoteAdaptor(path.join(input.dir, `.${input.type}`), input.url)) registerAdapter(input.projectID, input.type, remoteAdapter(path.join(input.dir, `.${input.type}`), input.url))
Database.use((db) => db.insert(WorkspaceTable).values({ id, type: input.type, project_id: input.projectID }).run()) Database.use((db) => db.insert(WorkspaceTable).values({ id, type: input.type, project_id: input.projectID }).run())
return id return id
}) })
@@ -237,7 +237,7 @@ describe("HttpApi workspace routing middleware", () => {
{ status: 201, headers: { "x-remote": "yes" } }, { status: 201, headers: { "x-remote": "yes" } },
) )
}) })
// The adaptor target tells the middleware where to proxy selected remote // The adapter target tells the middleware where to proxy selected remote
// workspace requests. Appending /probe to this base should produce // workspace requests. Appending /probe to this base should produce
// `${remoteUrl}/base/probe` on the fake remote server above. // `${remoteUrl}/base/probe` on the fake remote server above.
const workspace = yield* createRemoteWorkspace({ const workspace = yield* createRemoteWorkspace({
@@ -4,8 +4,8 @@ import { mkdir } from "node:fs/promises"
import path from "node:path" import path from "node:path"
import { Effect, Layer } from "effect" import { Effect, Layer } from "effect"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
import { registerAdaptor } from "../../src/control-plane/adaptors" import { registerAdapter } from "../../src/control-plane/adapters"
import type { WorkspaceAdaptor } from "../../src/control-plane/types" import type { WorkspaceAdapter } from "../../src/control-plane/types"
import { Workspace } from "../../src/control-plane/workspace" import { Workspace } from "../../src/control-plane/workspace"
import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace" import { WorkspacePaths } from "../../src/server/routes/instance/httpapi/groups/workspace"
import { Session } from "@/session/session" import { Session } from "@/session/session"
@@ -36,7 +36,7 @@ function request(path: string, directory: string, init: RequestInit = {}) {
}) })
} }
function localAdaptor(directory: string): WorkspaceAdaptor { function localAdapter(directory: string): WorkspaceAdapter {
return { return {
name: "Local Test", name: "Local Test",
description: "Create a local test workspace", description: "Create a local test workspace",
@@ -60,7 +60,7 @@ function localAdaptor(directory: string): WorkspaceAdaptor {
} }
} }
function remoteAdaptor(directory: string, url: string, headers?: HeadersInit): WorkspaceAdaptor { function remoteAdapter(directory: string, url: string, headers?: HeadersInit): WorkspaceAdapter {
return { return {
name: "Remote Test", name: "Remote Test",
description: "Create a remote test workspace", description: "Create a remote test workspace",
@@ -137,14 +137,14 @@ describe("workspace HttpApi", () => {
Effect.gen(function* () { Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true }) const dir = yield* tmpdirScoped({ git: true })
const [adaptors, workspaces, status] = yield* Effect.all([ const [adapters, workspaces, status] = yield* Effect.all([
request(WorkspacePaths.adaptors, dir), request(WorkspacePaths.adapters, dir),
request(WorkspacePaths.list, dir), request(WorkspacePaths.list, dir),
request(WorkspacePaths.status, dir), request(WorkspacePaths.status, dir),
]) ])
expect(adaptors.status).toBe(200) expect(adapters.status).toBe(200)
expect(yield* Effect.promise(() => adaptors.json())).toContainEqual({ expect(yield* Effect.promise(() => adapters.json())).toContainEqual({
type: "worktree", type: "worktree",
name: "Worktree", name: "Worktree",
description: "Create a git worktree", description: "Create a git worktree",
@@ -163,7 +163,7 @@ describe("workspace HttpApi", () => {
Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true Flag.OPENCODE_EXPERIMENTAL_WORKSPACES = true
const dir = yield* tmpdirScoped({ git: true }) const dir = yield* tmpdirScoped({ git: true })
const project = yield* Project.use.fromDirectory(dir) const project = yield* Project.use.fromDirectory(dir)
registerAdaptor(project.project.id, "local-test", localAdaptor(path.join(dir, ".workspace"))) registerAdapter(project.project.id, "local-test", localAdapter(path.join(dir, ".workspace")))
const created = yield* request(WorkspacePaths.list, dir, { const created = yield* request(WorkspacePaths.list, dir, {
method: "POST", method: "POST",
@@ -201,7 +201,7 @@ describe("workspace HttpApi", () => {
const dir = yield* tmpdirScoped({ git: true }) const dir = yield* tmpdirScoped({ git: true })
const workspaceDir = path.join(dir, ".workspace-local") const workspaceDir = path.join(dir, ".workspace-local")
const project = yield* Project.use.fromDirectory(dir) const project = yield* Project.use.fromDirectory(dir)
registerAdaptor(project.project.id, "local-target", localAdaptor(workspaceDir)) registerAdapter(project.project.id, "local-target", localAdapter(workspaceDir))
const created = yield* request(WorkspacePaths.list, dir, { const created = yield* request(WorkspacePaths.list, dir, {
method: "POST", method: "POST",
headers: { "content-type": "application/json" }, headers: { "content-type": "application/json" },
@@ -250,10 +250,10 @@ describe("workspace HttpApi", () => {
}) })
const project = yield* Project.use.fromDirectory(dir) const project = yield* Project.use.fromDirectory(dir)
registerAdaptor( registerAdapter(
project.project.id, project.project.id,
"remote-target", "remote-target",
remoteAdaptor(path.join(dir, ".remote"), `http://127.0.0.1:${remote.port}/base`, { remoteAdapter(path.join(dir, ".remote"), `http://127.0.0.1:${remote.port}/base`, {
"x-target-auth": "secret", "x-target-auth": "secret",
}), }),
) )
@@ -319,10 +319,10 @@ describe("workspace HttpApi", () => {
}) })
const project = yield* Project.use.fromDirectory(dir) const project = yield* Project.use.fromDirectory(dir)
registerAdaptor( registerAdapter(
project.project.id, project.project.id,
"remote-session-target", "remote-session-target",
remoteAdaptor(path.join(dir, ".remote-session"), `http://127.0.0.1:${remote.port}/base`), remoteAdapter(path.join(dir, ".remote-session"), `http://127.0.0.1:${remote.port}/base`),
) )
const created = yield* request(WorkspacePaths.list, dir, { const created = yield* request(WorkspacePaths.list, dir, {
method: "POST", method: "POST",
+2 -2
View File
@@ -45,7 +45,7 @@ export type WorkspaceTarget =
headers?: HeadersInit headers?: HeadersInit
} }
export type WorkspaceAdaptor = { export type WorkspaceAdapter = {
name: string name: string
description: string description: string
configure(config: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo> configure(config: WorkspaceInfo): WorkspaceInfo | Promise<WorkspaceInfo>
@@ -60,7 +60,7 @@ export type PluginInput = {
directory: string directory: string
worktree: string worktree: string
experimental_workspace: { experimental_workspace: {
register(type: string, adaptor: WorkspaceAdaptor): void register(type: string, adapter: WorkspaceAdapter): void
} }
serverUrl: URL serverUrl: URL
$: BunShell $: BunShell
+9 -9
View File
@@ -29,7 +29,7 @@ import type {
ExperimentalConsoleSwitchOrgResponses, ExperimentalConsoleSwitchOrgResponses,
ExperimentalResourceListResponses, ExperimentalResourceListResponses,
ExperimentalSessionListResponses, ExperimentalSessionListResponses,
ExperimentalWorkspaceAdaptorListResponses, ExperimentalWorkspaceAdapterListResponses,
ExperimentalWorkspaceCreateErrors, ExperimentalWorkspaceCreateErrors,
ExperimentalWorkspaceCreateResponses, ExperimentalWorkspaceCreateResponses,
ExperimentalWorkspaceListResponses, ExperimentalWorkspaceListResponses,
@@ -512,11 +512,11 @@ export class App extends HeyApiClient {
} }
} }
export class Adaptor extends HeyApiClient { export class Adapter extends HeyApiClient {
/** /**
* List workspace adaptors * List workspace adapters
* *
* List all available workspace adaptors for the current project. * List all available workspace adapters for the current project.
*/ */
public list<ThrowOnError extends boolean = false>( public list<ThrowOnError extends boolean = false>(
parameters?: { parameters?: {
@@ -536,8 +536,8 @@ export class Adaptor extends HeyApiClient {
}, },
], ],
) )
return (options?.client ?? this.client).get<ExperimentalWorkspaceAdaptorListResponses, unknown, ThrowOnError>({ return (options?.client ?? this.client).get<ExperimentalWorkspaceAdapterListResponses, unknown, ThrowOnError>({
url: "/experimental/workspace/adaptor", url: "/experimental/workspace/adapter",
...options, ...options,
...params, ...params,
}) })
@@ -731,9 +731,9 @@ export class Workspace extends HeyApiClient {
}) })
} }
private _adaptor?: Adaptor private _adapter?: Adapter
get adaptor(): Adaptor { get adapter(): Adapter {
return (this._adaptor ??= new Adaptor({ client: this.client })) return (this._adapter ??= new Adapter({ client: this.client }))
} }
} }
+6 -6
View File
@@ -2430,19 +2430,19 @@ export type AppLogResponses = {
export type AppLogResponse = AppLogResponses[keyof AppLogResponses] export type AppLogResponse = AppLogResponses[keyof AppLogResponses]
export type ExperimentalWorkspaceAdaptorListData = { export type ExperimentalWorkspaceAdapterListData = {
body?: never body?: never
path?: never path?: never
query?: { query?: {
directory?: string directory?: string
workspace?: string workspace?: string
} }
url: "/experimental/workspace/adaptor" url: "/experimental/workspace/adapter"
} }
export type ExperimentalWorkspaceAdaptorListResponses = { export type ExperimentalWorkspaceAdapterListResponses = {
/** /**
* Workspace adaptors * Workspace adapters
*/ */
200: Array<{ 200: Array<{
type: string type: string
@@ -2451,8 +2451,8 @@ export type ExperimentalWorkspaceAdaptorListResponses = {
}> }>
} }
export type ExperimentalWorkspaceAdaptorListResponse = export type ExperimentalWorkspaceAdapterListResponse =
ExperimentalWorkspaceAdaptorListResponses[keyof ExperimentalWorkspaceAdaptorListResponses] ExperimentalWorkspaceAdapterListResponses[keyof ExperimentalWorkspaceAdapterListResponses]
export type ExperimentalWorkspaceListData = { export type ExperimentalWorkspaceListData = {
body?: never body?: never
+6 -6
View File
@@ -415,9 +415,9 @@
] ]
} }
}, },
"/experimental/workspace/adaptor": { "/experimental/workspace/adapter": {
"get": { "get": {
"operationId": "experimental.workspace.adaptor.list", "operationId": "experimental.workspace.adapter.list",
"parameters": [ "parameters": [
{ {
"in": "query", "in": "query",
@@ -434,11 +434,11 @@
} }
} }
], ],
"summary": "List workspace adaptors", "summary": "List workspace adapters",
"description": "List all available workspace adaptors for the current project.", "description": "List all available workspace adapters for the current project.",
"responses": { "responses": {
"200": { "200": {
"description": "Workspace adaptors", "description": "Workspace adapters",
"content": { "content": {
"application/json": { "application/json": {
"schema": { "schema": {
@@ -466,7 +466,7 @@
"x-codeSamples": [ "x-codeSamples": [
{ {
"lang": "js", "lang": "js",
"source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adaptor.list({\n ...\n})" "source": "import { createOpencodeClient } from \"@opencode-ai/sdk\n\nconst client = createOpencodeClient()\nawait client.experimental.workspace.adapter.list({\n ...\n})"
} }
] ]
} }