Просмотр исходного кода

refactor(control-plane): migrate workspace DTO schemas (#24056)

Kit Langton 4 месяцев назад
Родитель
Сommit
31d01d404a

+ 3 - 3
packages/opencode/specs/effect/schema.md

@@ -354,9 +354,9 @@ piecewise.
 - [ ] `src/cli/cmd/tui/event.ts`
 - [ ] `src/cli/ui.ts`
 - [ ] `src/command/index.ts`
-- [ ] `src/control-plane/adaptors/worktree.ts`
-- [ ] `src/control-plane/types.ts`
-- [ ] `src/control-plane/workspace.ts`
+- [x] `src/control-plane/adaptors/worktree.ts`
+- [x] `src/control-plane/types.ts`
+- [x] `src/control-plane/workspace.ts`
 - [ ] `src/file/index.ts`
 - [ ] `src/file/ripgrep.ts`
 - [ ] `src/file/watcher.ts`

+ 1 - 7
packages/opencode/src/control-plane/adaptors/index.ts

@@ -1,12 +1,6 @@
 import { lazy } from "@/util/lazy"
 import type { ProjectID } from "@/project/schema"
-import type { WorkspaceAdaptor } from "../types"
-
-export type WorkspaceAdaptorEntry = {
-  type: string
-  name: string
-  description: string
-}
+import type { WorkspaceAdaptor, WorkspaceAdaptorEntry } from "../types"
 
 const BUILTIN: Record<string, () => Promise<WorkspaceAdaptor>> = {
   worktree: lazy(async () => (await import("./worktree")).WorktreeAdaptor),

+ 11 - 9
packages/opencode/src/control-plane/adaptors/worktree.ts

@@ -1,13 +1,15 @@
-import z from "zod"
+import { Schema } from "effect"
 import { AppRuntime } from "@/effect/app-runtime"
 import { Worktree } from "@/worktree"
 import { type WorkspaceAdaptor, WorkspaceInfo } from "../types"
+import { zod } from "@/util/effect-zod"
+import { withStatics } from "@/util/schema"
 
-const WorktreeConfig = z.object({
-  name: WorkspaceInfo.shape.name,
-  branch: WorkspaceInfo.shape.branch.unwrap(),
-  directory: WorkspaceInfo.shape.directory.unwrap(),
-})
+const WorktreeConfig = Schema.Struct({
+  name: WorkspaceInfo.fields.name,
+  branch: Schema.String,
+  directory: Schema.String,
+}).pipe(withStatics((s) => ({ zod: zod(s) })))
 
 export const WorktreeAdaptor: WorkspaceAdaptor = {
   name: "Worktree",
@@ -22,7 +24,7 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
     }
   },
   async create(info) {
-    const config = WorktreeConfig.parse(info)
+    const config = WorktreeConfig.zod.parse(info)
     await AppRuntime.runPromise(
       Worktree.Service.use((svc) =>
         svc.createFromInfo({
@@ -34,11 +36,11 @@ export const WorktreeAdaptor: WorkspaceAdaptor = {
     )
   },
   async remove(info) {
-    const config = WorktreeConfig.parse(info)
+    const config = WorktreeConfig.zod.parse(info)
     await AppRuntime.runPromise(Worktree.Service.use((svc) => svc.remove({ directory: config.directory })))
   },
   target(info) {
-    const config = WorktreeConfig.parse(info)
+    const config = WorktreeConfig.zod.parse(info)
     return {
       type: "local",
       directory: config.directory,

+ 21 - 10
packages/opencode/src/control-plane/types.ts

@@ -1,17 +1,28 @@
-import z from "zod"
+import { Schema } from "effect"
 import { ProjectID } from "@/project/schema"
 import { WorkspaceID } from "./schema"
+import { zod } from "@/util/effect-zod"
+import { type DeepMutable, withStatics } from "@/util/schema"
 
-export const WorkspaceInfo = 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,
+export const WorkspaceInfo = Schema.Struct({
+  id: WorkspaceID,
+  type: Schema.String,
+  name: Schema.String,
+  branch: Schema.NullOr(Schema.String),
+  directory: Schema.NullOr(Schema.String),
+  extra: Schema.NullOr(Schema.Unknown),
+  projectID: ProjectID,
 })
-export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>
+  .annotate({ identifier: "Workspace" })
+  .pipe(withStatics((s) => ({ zod: zod(s) })))
+export type WorkspaceInfo = DeepMutable<Schema.Schema.Type<typeof WorkspaceInfo>>
+
+export const WorkspaceAdaptorEntry = Schema.Struct({
+  type: Schema.String,
+  name: Schema.String,
+  description: Schema.String,
+}).pipe(withStatics((s) => ({ zod: zod(s) })))
+export type WorkspaceAdaptorEntry = Schema.Schema.Type<typeof WorkspaceAdaptorEntry>
 
 export type Target =
   | {

+ 20 - 20
packages/opencode/src/control-plane/workspace.ts

@@ -1,4 +1,3 @@
-import z from "zod"
 import { Schema } from "effect"
 import { setTimeout as sleep } from "node:timers/promises"
 import { fn } from "@/util/fn"
@@ -16,7 +15,7 @@ import { ProjectID } from "@/project/schema"
 import { Slug } from "@opencode-ai/shared/util/slug"
 import { WorkspaceTable } from "./workspace.sql"
 import { getAdaptor } from "./adaptors"
-import { WorkspaceInfo } from "./types"
+import { type WorkspaceInfo, WorkspaceInfo as WorkspaceInfoSchema } from "./types"
 import { WorkspaceID } from "./schema"
 import { parseSSE } from "./sse"
 import { Session } from "@/session"
@@ -26,12 +25,11 @@ import { errorData } from "@/util/error"
 import { AppRuntime } from "@/effect/app-runtime"
 import { waitEvent } from "./util"
 import { WorkspaceContext } from "./workspace-context"
-import { NonNegativeInt } from "@/util/schema"
+import { NonNegativeInt, withStatics } from "@/util/schema"
+import { zod as effectZod, zodObject } from "@/util/effect-zod"
 
-export const Info = WorkspaceInfo.meta({
-  ref: "Workspace",
-})
-export type Info = z.infer<typeof Info>
+export const Info = WorkspaceInfoSchema
+export type Info = WorkspaceInfo
 
 export const ConnectionStatus = Schema.Struct({
   workspaceID: WorkspaceID,
@@ -75,15 +73,16 @@ function fromRow(row: typeof WorkspaceTable.$inferSelect): Info {
   }
 }
 
-const CreateInput = z.object({
-  id: WorkspaceID.zod.optional(),
-  type: Info.shape.type,
-  branch: Info.shape.branch,
-  projectID: ProjectID.zod,
-  extra: Info.shape.extra,
-})
+export const CreateInput = Schema.Struct({
+  id: Schema.optional(WorkspaceID),
+  type: Info.fields.type,
+  branch: Info.fields.branch,
+  projectID: ProjectID,
+  extra: Info.fields.extra,
+}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
+export type CreateInput = Schema.Schema.Type<typeof CreateInput>
 
-export const create = fn(CreateInput, async (input) => {
+export const create = fn(CreateInput.zod, async (input) => {
   const id = WorkspaceID.ascending(input.id)
   const adaptor = await getAdaptor(input.projectID, input.type)
 
@@ -139,12 +138,13 @@ export const create = fn(CreateInput, async (input) => {
   return info
 })
 
-const SessionRestoreInput = z.object({
-  workspaceID: WorkspaceID.zod,
-  sessionID: SessionID.zod,
-})
+export const SessionRestoreInput = Schema.Struct({
+  workspaceID: WorkspaceID,
+  sessionID: SessionID,
+}).pipe(withStatics((s) => ({ zod: effectZod(s), zodObject: zodObject(s) })))
+export type SessionRestoreInput = Schema.Schema.Type<typeof SessionRestoreInput>
 
-export const sessionRestore = fn(SessionRestoreInput, async (input) => {
+export const sessionRestore = fn(SessionRestoreInput.zod, async (input) => {
   log.info("session restore requested", {
     workspaceID: input.workspaceID,
     sessionID: input.sessionID,

+ 11 - 16
packages/opencode/src/server/routes/control/workspace.ts

@@ -3,6 +3,7 @@ import { describeRoute, resolver, validator } from "hono-openapi"
 import z from "zod"
 import { listAdaptors } from "@/control-plane/adaptors"
 import { Workspace } from "@/control-plane/workspace"
+import { WorkspaceAdaptorEntry } from "@/control-plane/types"
 import { zodObject } from "@/util/effect-zod"
 import { Instance } from "@/project/instance"
 import { errors } from "../../error"
@@ -26,13 +27,7 @@ export const WorkspaceRoutes = lazy(() =>
             content: {
               "application/json": {
                 schema: resolver(
-                  z.array(
-                    z.object({
-                      type: z.string(),
-                      name: z.string(),
-                      description: z.string(),
-                    }),
-                  ),
+                  z.array(zodObject(WorkspaceAdaptorEntry)),
                 ),
               },
             },
@@ -54,7 +49,7 @@ export const WorkspaceRoutes = lazy(() =>
             description: "Workspace created",
             content: {
               "application/json": {
-                schema: resolver(Workspace.Info),
+                schema: resolver(Workspace.Info.zod),
               },
             },
           },
@@ -63,12 +58,12 @@ export const WorkspaceRoutes = lazy(() =>
       }),
       validator(
         "json",
-        Workspace.create.schema.omit({
+        Workspace.CreateInput.zodObject.omit({
           projectID: true,
         }),
       ),
       async (c) => {
-        const body = c.req.valid("json")
+        const body = c.req.valid("json") as Omit<Workspace.CreateInput, "projectID">
         const workspace = await Workspace.create({
           projectID: Instance.project.id,
           ...body,
@@ -87,7 +82,7 @@ export const WorkspaceRoutes = lazy(() =>
             description: "Workspaces",
             content: {
               "application/json": {
-                schema: resolver(z.array(Workspace.Info)),
+                schema: resolver(z.array(Workspace.Info.zod)),
               },
             },
           },
@@ -130,7 +125,7 @@ export const WorkspaceRoutes = lazy(() =>
             description: "Workspace removed",
             content: {
               "application/json": {
-                schema: resolver(Workspace.Info.optional()),
+                schema: resolver(Workspace.Info.zod.optional()),
               },
             },
           },
@@ -140,7 +135,7 @@ export const WorkspaceRoutes = lazy(() =>
       validator(
         "param",
         z.object({
-          id: Workspace.Info.shape.id,
+          id: zodObject(Workspace.Info).shape.id,
         }),
       ),
       async (c) => {
@@ -170,11 +165,11 @@ export const WorkspaceRoutes = lazy(() =>
           ...errors(400),
         },
       }),
-      validator("param", z.object({ id: Workspace.Info.shape.id })),
-      validator("json", Workspace.sessionRestore.schema.omit({ workspaceID: true })),
+      validator("param", z.object({ id: zodObject(Workspace.Info).shape.id })),
+      validator("json", Workspace.SessionRestoreInput.zodObject.omit({ workspaceID: true })),
       async (c) => {
         const { id } = c.req.valid("param")
-        const body = c.req.valid("json")
+        const body = c.req.valid("json") as Omit<Workspace.SessionRestoreInput, "workspaceID">
         log.info("session restore route requested", {
           workspaceID: id,
           sessionID: body.sessionID,