Compare commits

...
Sign in to create a new pull request.

1 commit

Author SHA1 Message Date
Kit Langton
76e2ff4522 refactor: rename workspace adapters 2026-04-29 20:27:20 -04:00
22 changed files with 163 additions and 163 deletions

View file

@ -187,7 +187,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 |
@ -279,7 +279,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.

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`

View file

@ -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",
}, },
] ]
} }

View file

@ -0,0 +1,46 @@
import { lazy } from "@/util/lazy"
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdapter, WorkspaceAdapterEntry } from "../types"
const BUILTIN: Record<string, () => Promise<WorkspaceAdapter>> = {
worktree: lazy(async () => (await import("./worktree")).WorktreeAdapter),
}
const state = new Map<ProjectID, Map<string, WorkspaceAdapter>>()
export async function getAdapter(projectID: ProjectID, type: string): Promise<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, init]) => {
const adapter = await init()
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)
}

View file

@ -1,7 +1,7 @@
import { Schema } from "effect" import { Schema } from "effect"
import { AppRuntime } from "@/effect/app-runtime" import { AppRuntime } from "@/effect/app-runtime"
import { Worktree } from "@/worktree" import { Worktree } from "@/worktree"
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,
@ -10,7 +10,7 @@ const WorktreeConfig = Schema.Struct({
}) })
const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig) const decodeWorktreeConfig = Schema.decodeUnknownSync(WorktreeConfig)
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) {

View file

@ -1,46 +0,0 @@
import { lazy } from "@/util/lazy"
import type { ProjectID } from "@/project/schema"
import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
const BUILTIN: Record<string, () => Promise<WorkspaceAdaptor>> = {
worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),
}
const state = new Map<ProjectID, Map<string, WorkspaceAdaptor>>()
export async function getAdaptor(projectID: ProjectID, type: string): Promise<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, init]) => {
const adaptor = await init()
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)
}

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>

View file

@ -17,7 +17,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 { parseSSE } from "./sse" import { parseSSE } from "./sse"
@ -87,9 +87,9 @@ export type CreateInput = Schema.Schema.Type<typeof CreateInput>
export const create = fn(CreateInput.zod, async (input) => { export const create = fn(CreateInput.zod, async (input) => {
const id = WorkspaceID.ascending(input.id) const id = WorkspaceID.ascending(input.id)
const adaptor = await getAdaptor(input.projectID, input.type) const adapter = await getAdapter(input.projectID, input.type)
const config = await adaptor.configure({ ...input, id, name: Slug.create(), directory: null }) const config = await adapter.configure({ ...input, id, name: Slug.create(), directory: null })
const info: Info = { const info: Info = {
id, id,
@ -123,7 +123,7 @@ export const create = fn(CreateInput.zod, async (input) => {
OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT, OTEL_EXPORTER_OTLP_ENDPOINT: process.env.OTEL_EXPORTER_OTLP_ENDPOINT,
OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES, OTEL_RESOURCE_ATTRIBUTES: process.env.OTEL_RESOURCE_ATTRIBUTES,
} }
await adaptor.create(config, env) await adapter.create(config, env)
startSync(info) startSync(info)
@ -156,8 +156,8 @@ export const sessionRestore = fn(SessionRestoreInput.zod, async (input) => {
const space = await get(input.workspaceID) const space = await get(input.workspaceID)
if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`) if (!space) throw new Error(`Workspace not found: ${input.workspaceID}`)
const adaptor = await getAdaptor(space.projectID, space.type) const adapter = await getAdapter(space.projectID, space.type)
const target = await adaptor.target(space) const target = await adapter.target(space)
// Need to switch the workspace of the session // Need to switch the workspace of the session
SyncEvent.run(Session.Event.Updated, { SyncEvent.run(Session.Event.Updated, {
@ -329,10 +329,10 @@ export const remove = fn(WorkspaceID.zod, async (id) => {
const info = fromRow(row) const info = fromRow(row)
try { try {
const adaptor = await getAdaptor(info.projectID, row.type) const adapter = await getAdapter(info.projectID, row.type)
await adaptor.remove(info) await adapter.remove(info)
} catch { } catch {
log.error("adaptor not available when removing workspace", { type: row.type }) log.error("adapter not available when removing workspace", { type: row.type })
} }
Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run()) Database.use((db) => db.delete(WorkspaceTable).where(eq(WorkspaceTable.id, id)).run())
return info return info
@ -501,8 +501,8 @@ async function syncHistory(space: Info, url: URL | string, headers: HeadersInit
} }
async function syncWorkspaceLoop(space: Info, signal: AbortSignal) { async function syncWorkspaceLoop(space: Info, signal: AbortSignal) {
const adaptor = await getAdaptor(space.projectID, space.type) const adapter = await getAdapter(space.projectID, space.type)
const target = await adaptor.target(space) const target = await adapter.target(space)
if (target.type === "local") return null if (target.type === "local") return null
@ -568,8 +568,8 @@ async function syncWorkspaceLoop(space: Info, signal: AbortSignal) {
async function startSync(space: Info) { async function startSync(space: Info) {
if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return if (!Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) return
const adaptor = await getAdaptor(space.projectID, space.type) const adapter = await getAdapter(space.projectID, space.type)
const target = await adaptor.target(space) const target = await adapter.target(space)
if (target.type === "local") { if (target.type === "local") {
void Filesystem.exists(target.directory).then((exists) => { void Filesystem.exists(target.directory).then((exists) => {

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"
@ -23,8 +23,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" })
@ -136,8 +136,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 {

View file

@ -1,9 +1,9 @@
import { Hono } from "hono" 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 { listAdaptors } from "@/control-plane/adaptors" import { listAdapters } from "@/control-plane/adapters"
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 { 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"
@ -16,24 +16,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(

View file

@ -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, {

View file

@ -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 { Instance } from "@/project/instance" import { Instance } from "@/project/instance"
@ -9,9 +9,9 @@ import { CreatePayload, SessionRestorePayload } from "../groups/workspace"
export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) => export const workspaceHandlers = HttpApiBuilder.group(InstanceHttpApi, "workspace", (handlers) =>
Effect.gen(function* () { Effect.gen(function* () {
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* () {
@ -56,7 +56,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)

View file

@ -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.promise(() => getAdaptor(workspace.projectID, workspace.type)) const adapter = yield* Effect.promise(() => getAdapter(workspace.projectID, workspace.type))
return yield* Effect.promise(() => Promise.resolve(adaptor.target(workspace))) return yield* Effect.promise(() => Promise.resolve(adapter.target(workspace)))
}) })
} }

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"
@ -88,8 +88,8 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
return next() return next()
} }
const adaptor = await getAdaptor(workspace.projectID, workspace.type) const adapter = await 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({

View file

@ -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",
}) })

View file

@ -31,7 +31,7 @@ afterAll(() => {
}) })
describe("plugin.workspace", () => { describe("plugin.workspace", () => {
test("plugin can install a workspace adaptor", async () => { test("plugin can install a workspace adapter", async () => {
await using tmp = await tmpdir({ await using tmp = await tmpdir({
init: async (dir) => { init: async (dir) => {
const type = `plug-${Math.random().toString(36).slice(2)}` const type = `plug-${Math.random().toString(36).slice(2)}`
@ -44,7 +44,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)} }`,
" },", " },",

View file

@ -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 { 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"
@ -37,7 +37,7 @@ function runSession<A, E>(fx: Effect.Effect<A, E, Session.Service>, workspaceID?
) )
} }
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",
@ -61,7 +61,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",
@ -139,14 +139,14 @@ describe("workspace HttpApi", () => {
test("serves read endpoints", async () => { test("serves read endpoints", async () => {
await using tmp = await tmpdir({ git: true }) await using tmp = await tmpdir({ git: true })
const [adaptors, workspaces, status] = await Promise.all([ const [adapters, workspaces, status] = await Promise.all([
request(WorkspacePaths.adaptors, tmp.path), request(WorkspacePaths.adapters, tmp.path),
request(WorkspacePaths.list, tmp.path), request(WorkspacePaths.list, tmp.path),
request(WorkspacePaths.status, tmp.path), request(WorkspacePaths.status, tmp.path),
]) ])
expect(adaptors.status).toBe(200) expect(adapters.status).toBe(200)
expect(await adaptors.json()).toEqual([ expect(await adapters.json()).toEqual([
{ {
type: "worktree", type: "worktree",
name: "Worktree", name: "Worktree",
@ -167,7 +167,7 @@ describe("workspace HttpApi", () => {
await Instance.provide({ await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => fn: async () =>
registerAdaptor(Instance.project.id, "local-test", localAdaptor(path.join(tmp.path, ".workspace"))), registerAdapter(Instance.project.id, "local-test", localAdapter(path.join(tmp.path, ".workspace"))),
}) })
const created = await request(WorkspacePaths.list, tmp.path, { const created = await request(WorkspacePaths.list, tmp.path, {
@ -207,7 +207,7 @@ describe("workspace HttpApi", () => {
const workspace = await Instance.provide({ const workspace = await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
registerAdaptor(Instance.project.id, "local-target", localAdaptor(workspaceDir)) registerAdapter(Instance.project.id, "local-target", localAdapter(workspaceDir))
return Workspace.create({ return Workspace.create({
type: "local-target", type: "local-target",
branch: null, branch: null,
@ -261,10 +261,10 @@ describe("workspace HttpApi", () => {
const workspace = await Instance.provide({ const workspace = await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
registerAdaptor( registerAdapter(
Instance.project.id, Instance.project.id,
"remote-target", "remote-target",
remoteAdaptor(path.join(tmp.path, ".remote"), `http://127.0.0.1:${remote.port}/base`, { remoteAdapter(path.join(tmp.path, ".remote"), `http://127.0.0.1:${remote.port}/base`, {
"x-target-auth": "secret", "x-target-auth": "secret",
}), }),
) )
@ -332,10 +332,10 @@ describe("workspace HttpApi", () => {
const workspace = await Instance.provide({ const workspace = await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
registerAdaptor( registerAdapter(
Instance.project.id, Instance.project.id,
"remote-session-target", "remote-session-target",
remoteAdaptor(path.join(tmp.path, ".remote-session"), `http://127.0.0.1:${remote.port}/base`), remoteAdapter(path.join(tmp.path, ".remote-session"), `http://127.0.0.1:${remote.port}/base`),
) )
return Workspace.create({ return Workspace.create({
type: "remote-session-target", type: "remote-session-target",

View file

@ -2,8 +2,8 @@ import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from "bun:
import fs from "node:fs/promises" import fs from "node:fs/promises"
import path from "node:path" import path from "node:path"
import { GlobalBus } from "../../src/bus/global" import { GlobalBus } from "../../src/bus/global"
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 { AppRuntime } from "../../src/effect/app-runtime" import { AppRuntime } from "../../src/effect/app-runtime"
import { Flag } from "@opencode-ai/core/flag/flag" import { Flag } from "@opencode-ai/core/flag/flag"
@ -71,7 +71,7 @@ async function user(sessionID: SessionID, text: string) {
}) })
} }
function remote(dir: string, url: string): WorkspaceAdaptor { function remote(dir: string, url: string): WorkspaceAdapter {
return { return {
name: "remote", name: "remote",
description: "remote", description: "remote",
@ -94,7 +94,7 @@ function remote(dir: string, url: string): WorkspaceAdaptor {
} }
} }
function local(dir: string): WorkspaceAdaptor { function local(dir: string): WorkspaceAdapter {
return { return {
name: "local", name: "local",
description: "local", description: "local",
@ -166,7 +166,7 @@ describe("Workspace.sessionRestore", () => {
const setup = await Instance.provide({ const setup = await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
registerAdaptor(Instance.project.id, "worktree", remote(dir, "https://workspace.test/base")) registerAdapter(Instance.project.id, "worktree", remote(dir, "https://workspace.test/base"))
const space = await Workspace.create({ const space = await Workspace.create({
type: "worktree", type: "worktree",
branch: null, branch: null,
@ -247,7 +247,7 @@ describe("Workspace.sessionRestore", () => {
const setup = await Instance.provide({ const setup = await Instance.provide({
directory: tmp.path, directory: tmp.path,
fn: async () => { fn: async () => {
registerAdaptor(Instance.project.id, "local-restore", local(dir)) registerAdapter(Instance.project.id, "local-restore", local(dir))
const space = await Workspace.create({ const space = await Workspace.create({
type: "local-restore", type: "local-restore",
branch: null, branch: null,

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

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 }))
} }
} }

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

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})"
} }
] ]
} }