diff --git a/packages/opencode/src/control-plane/adaptors/index.ts b/packages/opencode/src/control-plane/adaptors/index.ts index b1627bab7d..aaa477db28 100644 --- a/packages/opencode/src/control-plane/adaptors/index.ts +++ b/packages/opencode/src/control-plane/adaptors/index.ts @@ -1,23 +1,16 @@ import { lazy } from "@/util/lazy" import { Schema } from "effect" -import z from "zod" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" import type { ProjectID } from "@/project/schema" import type { WorkspaceAdaptor } from "../types" -const WorkspaceAdaptorEntryZod = z.object({ - type: z.string(), - name: z.string(), - description: z.string(), -}) - -const _WorkspaceAdaptorEntry = Schema.Struct({ +export const WorkspaceAdaptorEntry = Schema.Struct({ type: Schema.String, name: Schema.String, description: Schema.String, -}) - -export const WorkspaceAdaptorEntry = Object.assign(_WorkspaceAdaptorEntry, { zod: WorkspaceAdaptorEntryZod }) -export type WorkspaceAdaptorEntry = Schema.Schema.Type +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type WorkspaceAdaptorEntry = Schema.Schema.Type const BUILTIN: Record Promise> = { worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor), diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index 121fd9213b..d85e09c107 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/packages/opencode/src/control-plane/adaptors/worktree.ts @@ -4,9 +4,9 @@ import { Worktree } from "@/worktree" import { type WorkspaceAdaptor, WorkspaceInfo } from "../types" const WorktreeConfig = z.object({ - name: WorkspaceInfo.zod.shape.name, - branch: WorkspaceInfo.zod.shape.branch.unwrap(), - directory: WorkspaceInfo.zod.shape.directory.unwrap(), + name: z.string(), + branch: z.string(), + directory: z.string(), }) export const WorktreeAdaptor: WorkspaceAdaptor = { diff --git a/packages/opencode/src/control-plane/types.ts b/packages/opencode/src/control-plane/types.ts index 082f31089e..39c2d45f18 100644 --- a/packages/opencode/src/control-plane/types.ts +++ b/packages/opencode/src/control-plane/types.ts @@ -2,22 +2,10 @@ import z from "zod" import { Schema } from "effect" import { ProjectID } from "@/project/schema" import { WorkspaceID } from "./schema" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" -const WorkspaceInfoZod = z - .object({ - id: WorkspaceID.zod, - type: z.string(), - name: z.string(), - branch: z.string().nullable(), - directory: z.string().nullable(), - extra: z.unknown().nullable(), - projectID: ProjectID.zod, - }) - .meta({ - ref: "Workspace", - }) - -const _WorkspaceInfo = Schema.Struct({ +export const WorkspaceInfo = Schema.Struct({ id: WorkspaceID, type: Schema.String, name: Schema.String, @@ -25,10 +13,10 @@ const _WorkspaceInfo = Schema.Struct({ directory: Schema.NullOr(Schema.String), extra: Schema.NullOr(Schema.Unknown), projectID: ProjectID, -}).annotate({ identifier: "Workspace" }) - -export const WorkspaceInfo = Object.assign(_WorkspaceInfo, { zod: WorkspaceInfoZod }) -export type WorkspaceInfo = Schema.Schema.Type +}) + .annotate({ identifier: "Workspace" }) + .pipe(withStatics((s) => ({ zod: zod(s) }))) +export type WorkspaceInfo = Schema.Schema.Type export type Target = | { diff --git a/packages/opencode/src/control-plane/workspace.ts b/packages/opencode/src/control-plane/workspace.ts index db41fd5d59..d4044e56a0 100644 --- a/packages/opencode/src/control-plane/workspace.ts +++ b/packages/opencode/src/control-plane/workspace.ts @@ -26,24 +26,18 @@ import { AppRuntime } from "@/effect/app-runtime" import { EventSequenceTable } from "@/sync/event.sql" import { waitEvent } from "./util" import { Schema } from "effect" +import { zod } from "@/util/effect-zod" +import { withStatics } from "@/util/schema" export const Info = WorkspaceInfo export type Info = WorkspaceInfo -const ConnectionStatusZod = z.object({ - workspaceID: WorkspaceID.zod, - status: z.enum(["connected", "connecting", "disconnected", "error"]), - error: z.string().optional(), -}) - -const _ConnectionStatus = Schema.Struct({ +export const ConnectionStatus = Schema.Struct({ workspaceID: WorkspaceID, status: Schema.Literals(["connected", "connecting", "disconnected", "error"]), error: Schema.optional(Schema.String), -}) - -export const ConnectionStatus = Object.assign(_ConnectionStatus, { zod: ConnectionStatusZod }) -export type ConnectionStatus = Schema.Schema.Type +}).pipe(withStatics((s) => ({ zod: zod(s) }))) +export type ConnectionStatus = Schema.Schema.Type const Restore = z.object({ workspaceID: WorkspaceID.zod, @@ -83,29 +77,12 @@ function fromRow(row: typeof WorkspaceTable.$inferSelect): Info { const CreateInput = z.object({ id: WorkspaceID.zod.optional(), - type: WorkspaceInfo.zod.shape.type, - branch: WorkspaceInfo.zod.shape.branch, + type: zod(Schema.String), + branch: zod(Schema.NullOr(Schema.String)), projectID: ProjectID.zod, - extra: WorkspaceInfo.zod.shape.extra, + extra: zod(Schema.NullOr(Schema.Unknown)), }) -const CreateBodyZod = z.object({ - id: WorkspaceID.zod.optional(), - type: WorkspaceInfo.zod.shape.type, - branch: WorkspaceInfo.zod.shape.branch, - extra: WorkspaceInfo.zod.shape.extra, -}) - -const _CreateBody = Schema.Struct({ - id: Schema.optional(WorkspaceID), - type: Schema.String, - branch: Schema.NullOr(Schema.String), - extra: Schema.NullOr(Schema.Unknown), -}) - -export const CreateBody = Object.assign(_CreateBody, { zod: CreateBodyZod }) -export type CreateBody = Schema.Schema.Type - export const create = fn(CreateInput, async (input) => { const id = WorkspaceID.ascending(input.id) const adaptor = await getAdaptor(input.projectID, input.type) @@ -164,28 +141,6 @@ const SessionRestoreInput = z.object({ sessionID: SessionID.zod, }) -const SessionRestoreBodyZod = z.object({ - sessionID: SessionID.zod, -}) - -const _SessionRestoreBody = Schema.Struct({ - sessionID: SessionID, -}) - -export const SessionRestoreBody = Object.assign(_SessionRestoreBody, { zod: SessionRestoreBodyZod }) -export type SessionRestoreBody = Schema.Schema.Type - -const SessionRestoreResultZod = z.object({ - total: z.number().int().min(0), -}) - -const _SessionRestoreResult = Schema.Struct({ - total: Schema.Number, -}) - -export const SessionRestoreResult = Object.assign(_SessionRestoreResult, { zod: SessionRestoreResultZod }) -export type SessionRestoreResult = Schema.Schema.Type - export const sessionRestore = fn(SessionRestoreInput, async (input) => { log.info("session restore requested", { workspaceID: input.workspaceID, diff --git a/packages/opencode/src/server/instance/httpapi/workspace.ts b/packages/opencode/src/server/instance/httpapi/workspace.ts index 84c3feea2b..bd78efef9a 100644 --- a/packages/opencode/src/server/instance/httpapi/workspace.ts +++ b/packages/opencode/src/server/instance/httpapi/workspace.ts @@ -1,9 +1,8 @@ import { listAdaptors, WorkspaceAdaptorEntry } from "@/control-plane/adaptors" import { Workspace } from "@/control-plane/workspace" -import { WorkspaceID } from "@/control-plane/schema" import { Instance } from "@/project/instance" import { Effect, Schema } from "effect" -import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" const root = "/experimental/workspace" @@ -38,37 +37,6 @@ export const WorkspaceApi = HttpApi.make("workspace") description: "Get connection status for workspaces in the current project.", }), ), - HttpApiEndpoint.post("create", root, { - payload: Workspace.CreateBody, - success: Workspace.Info, - }).annotateMerge( - OpenApi.annotations({ - identifier: "experimental.workspace.create", - summary: "Create workspace", - description: "Create a workspace for the current project.", - }), - ), - HttpApiEndpoint.delete("remove", `${root}/:id`, { - params: { id: WorkspaceID }, - success: Schema.optional(Workspace.Info), - }).annotateMerge( - OpenApi.annotations({ - identifier: "experimental.workspace.remove", - summary: "Remove workspace", - description: "Remove an existing workspace.", - }), - ), - HttpApiEndpoint.post("sessionRestore", `${root}/:id/session-restore`, { - params: { id: WorkspaceID }, - payload: Workspace.SessionRestoreBody, - success: Workspace.SessionRestoreResult, - }).annotateMerge( - OpenApi.annotations({ - identifier: "experimental.workspace.sessionRestore", - summary: "Restore session into workspace", - description: "Replay a session's sync events into the target workspace in batches.", - }), - ), ) .annotateMerge( OpenApi.annotations({ @@ -98,39 +66,6 @@ const status = Effect.fn("WorkspaceHttpApi.status")(function* () { return Workspace.status().filter((item) => ids.has(item.workspaceID)) }) -const create = Effect.fn("WorkspaceHttpApi.create")(function* (ctx: { payload: Workspace.CreateBody }) { - return yield* Effect.promise(() => - Workspace.create({ - projectID: Instance.project.id, - ...ctx.payload, - }), - ).pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({})))) -}) - -const remove = Effect.fn("WorkspaceHttpApi.remove")(function* (ctx: { params: { id: WorkspaceID } }) { - return yield* Effect.promise(() => Workspace.remove(ctx.params.id)).pipe( - Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({}))), - ) -}) - -const sessionRestore = Effect.fn("WorkspaceHttpApi.sessionRestore")(function* (ctx: { - params: { id: WorkspaceID } - payload: Workspace.SessionRestoreBody -}) { - return yield* Effect.promise(() => - Workspace.sessionRestore({ - workspaceID: ctx.params.id, - sessionID: ctx.payload.sessionID, - }), - ).pipe(Effect.catch(() => Effect.fail(new HttpApiError.BadRequest({})))) -}) - export const workspaceHandlers = HttpApiBuilder.group(WorkspaceApi, "workspace", (handlers) => - handlers - .handle("adaptors", adaptors) - .handle("list", list) - .handle("status", status) - .handle("create", create) - .handle("remove", remove) - .handle("sessionRestore", sessionRestore), + handlers.handle("adaptors", adaptors).handle("list", list).handle("status", status), ) diff --git a/packages/opencode/src/server/instance/index.ts b/packages/opencode/src/server/instance/index.ts index e1ef3a0928..b37d53871a 100644 --- a/packages/opencode/src/server/instance/index.ts +++ b/packages/opencode/src/server/instance/index.ts @@ -42,15 +42,15 @@ export const InstanceRoutes = (upgrade: UpgradeWebSocket): Hono => { if (Flag.OPENCODE_EXPERIMENTAL_HTTPAPI) { const handler = ExperimentalHttpApiServer.webHandler().handler const context = Context.empty() as Context.Context - app.all("/question", (c) => handler(c.req.raw, context)) - app.all("/question/*", (c) => handler(c.req.raw, context)) - app.all("/permission", (c) => handler(c.req.raw, context)) - app.all("/permission/*", (c) => handler(c.req.raw, context)) - app.all("/experimental/workspace", (c) => handler(c.req.raw, context)) - app.all("/experimental/workspace/*", (c) => handler(c.req.raw, context)) - app.all("/experimental/workspace/adaptor", (c) => handler(c.req.raw, context)) - app.all("/experimental/workspace/status", (c) => handler(c.req.raw, context)) - app.all("/provider/auth", (c) => handler(c.req.raw, context)) + app.get("/question", (c) => handler(c.req.raw, context)) + app.post("/question/:requestID/reply", (c) => handler(c.req.raw, context)) + app.post("/question/:requestID/reject", (c) => handler(c.req.raw, context)) + app.get("/permission", (c) => handler(c.req.raw, context)) + app.post("/permission/:requestID/reply", (c) => handler(c.req.raw, context)) + app.get("/experimental/workspace", (c) => handler(c.req.raw, context)) + app.get("/experimental/workspace/adaptor", (c) => handler(c.req.raw, context)) + app.get("/experimental/workspace/status", (c) => handler(c.req.raw, context)) + app.get("/provider/auth", (c) => handler(c.req.raw, context)) } return app diff --git a/packages/opencode/src/server/instance/workspace.ts b/packages/opencode/src/server/instance/workspace.ts index 6c406c79e5..a23c8b6517 100644 --- a/packages/opencode/src/server/instance/workspace.ts +++ b/packages/opencode/src/server/instance/workspace.ts @@ -2,9 +2,9 @@ import { Hono } from "hono" import { describeRoute, resolver, validator } from "hono-openapi" import z from "zod" import { listAdaptors } from "../../control-plane/adaptors" +import { WorkspaceID } from "../../control-plane/schema" import { Workspace } from "../../control-plane/workspace" import { Instance } from "../../project/instance" -import { WorkspaceID } from "../../control-plane/schema" import { WorkspaceAdaptorEntry } from "../../control-plane/adaptors" import { errors } from "../error" import { lazy } from "../../util/lazy" @@ -54,7 +54,12 @@ export const WorkspaceRoutes = lazy(() => ...errors(400), }, }), - validator("json", Workspace.CreateBody.zod), + validator( + "json", + Workspace.create.schema.omit({ + projectID: true, + }), + ), async (c) => { const body = c.req.valid("json") const workspace = await Workspace.create({ @@ -147,7 +152,11 @@ export const WorkspaceRoutes = lazy(() => description: "Session replay started", content: { "application/json": { - schema: resolver(Workspace.SessionRestoreResult.zod), + schema: resolver( + z.object({ + total: z.number().int().min(0), + }), + ), }, }, }, @@ -155,7 +164,7 @@ export const WorkspaceRoutes = lazy(() => }, }), validator("param", z.object({ id: WorkspaceID.zod })), - validator("json", Workspace.SessionRestoreBody.zod), + validator("json", Workspace.sessionRestore.schema.omit({ workspaceID: true })), async (c) => { const { id } = c.req.valid("param") const body = c.req.valid("json")