diff --git a/packages/core/src/workspace.ts b/packages/core/src/workspace.ts index 4cf18c32ff..e94f8c7e12 100644 --- a/packages/core/src/workspace.ts +++ b/packages/core/src/workspace.ts @@ -1,6 +1,75 @@ export * as Workspace from "./workspace" +import { eq } from "drizzle-orm" +import { Context, Effect, Layer, Schema } from "effect" import { Workspace } from "@opencode-ai/schema/workspace" +import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" +import { Database } from "./database/database" +import { WorkspaceDriver } from "./workspace/driver" +import { WorkspaceTable } from "./workspace/sql" export const ID = Workspace.ID export type ID = typeof ID.Type + +export class NotFoundError extends Schema.TaggedErrorClass()("Workspace.NotFoundError", { + id: ID, +}) {} + +export class Info extends Schema.Class("Workspace.Info")({ + id: ID, + provider: Schema.String, + root: Schema.String, +}) {} + +export interface Interface { + /** Allocate through the named driver and persist. Resolves when the Workspace is usable. */ + readonly create: (input: { + readonly provider: string + }) => Effect.Effect + /** Metadata read; never contacts a driver. */ + readonly get: (id: ID) => Effect.Effect + /** Provider key and opaque binding for WorkspaceDriver.connect. Hosted Location construction only. */ + readonly binding: (id: ID) => Effect.Effect< + { readonly provider: string; readonly binding: WorkspaceDriver.Binding }, + NotFoundError + > +} + +export class Service extends Context.Service()("@opencode/Workspace") {} + +const layer = Layer.effect( + Service, + Effect.gen(function* () { + const { db } = yield* Database.Service + const registry = yield* WorkspaceDriver.RegistryService + + const require = Effect.fn("Workspace.require")(function* (id: ID) { + const row = yield* db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, id)).get().pipe(Effect.orDie) + if (!row) return yield* new NotFoundError({ id }) + return row + }) + + return Service.of({ + create: Effect.fn("Workspace.create")(function* (input) { + const driver = yield* registry.get(input.provider) + const id = ID.create() + const created = yield* driver.create({ workspaceID: id }) + yield* db + .insert(WorkspaceTable) + .values({ id, provider: input.provider, binding: created.binding, root: created.root }) + .pipe(Effect.orDie) + return new Info({ id, provider: input.provider, root: created.root }) + }), + get: Effect.fn("Workspace.get")(function* (id) { + const row = yield* require(id) + return new Info({ id: row.id, provider: row.provider, root: row.root }) + }), + binding: Effect.fn("Workspace.binding")(function* (id) { + const row = yield* require(id) + return { provider: row.provider, binding: row.binding } + }), + }) + }), +) + +export const node = makeGlobalNode({ service: Service, layer, deps: [Database.node, WorkspaceDriver.registryNode] }) diff --git a/packages/core/src/workspace/driver.ts b/packages/core/src/workspace/driver.ts index 7205b3277e..87908470a8 100644 --- a/packages/core/src/workspace/driver.ts +++ b/packages/core/src/workspace/driver.ts @@ -2,6 +2,8 @@ export * as WorkspaceDriver from "./driver" import { Context, Effect, Schema, Scope } from "effect" import { Workspace } from "@opencode-ai/schema/workspace" +import { tags } from "@opencode-ai/util/effect/app-node" +import { LayerNode } from "@opencode-ai/util/effect/layer-node" import type { WorkspaceEnvironment } from "./environment" /** @@ -49,6 +51,9 @@ export interface Registry { export class RegistryService extends Context.Service()("@opencode/WorkspaceDriverRegistry") {} +/** Bound by Server composition (or tests); core never constructs drivers. */ +export const registryNode = LayerNode.unbound(RegistryService, tags.values.global) + /** Immutable registry fixed at composition time. */ export const registry = (drivers: Readonly>): Registry => ({ get: (provider) => {