فهرست منبع

effectify Worktree service with Effect layer and facades

Migrate Worktree from direct Instance ALS reads to the Effect service
pattern: define Interface, Service, layer (yielding InstanceContext),
and promise facades using runPromiseInstance. Wire into instances.ts.
Kit Langton 1 ماه پیش
والد
کامیت
b3023f0fa0
3فایلهای تغییر یافته به همراه392 افزوده شده و 306 حذف شده
  1. 1 1
      packages/opencode/specs/effect-migration.md
  2. 3 0
      packages/opencode/src/effect/instances.ts
  3. 388 305
      packages/opencode/src/worktree/index.ts

+ 1 - 1
packages/opencode/specs/effect-migration.md

@@ -129,7 +129,7 @@ Still open and likely worth migrating:
 - [ ] `Plugin`
 - [ ] `ToolRegistry`
 - [ ] `Pty`
-- [ ] `Worktree`
+- [x] `Worktree`
 - [ ] `Installation`
 - [ ] `Bus`
 - [ ] `Command`

+ 3 - 0
packages/opencode/src/effect/instances.ts

@@ -10,6 +10,7 @@ import { ProviderAuth } from "@/provider/auth"
 import { Question } from "@/question"
 import { Skill } from "@/skill/skill"
 import { Snapshot } from "@/snapshot"
+import { Worktree } from "@/worktree"
 import { InstanceContext } from "./instance-context"
 import { registerDisposer } from "./instance-registry"
 
@@ -26,6 +27,7 @@ export type InstanceServices =
   | File.Service
   | Skill.Service
   | Snapshot.Service
+  | Worktree.Service
 
 // NOTE: LayerMap only passes the key (directory string) to lookup, but we need
 // the full instance context (directory, worktree, project). We read from the
@@ -46,6 +48,7 @@ function lookup(_key: string) {
     Layer.fresh(File.layer),
     Layer.fresh(Skill.defaultLayer),
     Layer.fresh(Snapshot.defaultLayer),
+    Layer.fresh(Worktree.layer),
   ).pipe(Layer.provide(ctx))
 }
 

+ 388 - 305
packages/opencode/src/worktree/index.ts

@@ -9,12 +9,14 @@ import { Project } from "../project/project"
 import { Database, eq } from "../storage/db"
 import { ProjectTable } from "../project/project.sql"
 import type { ProjectID } from "../project/schema"
-import { fn } from "../util/fn"
 import { Log } from "../util/log"
 import { Process } from "../util/process"
 import { git } from "../util/git"
 import { BusEvent } from "@/bus/bus-event"
 import { GlobalBus } from "@/bus/global"
+import { InstanceContext } from "@/effect/instance-context"
+import { runPromiseInstance } from "@/effect/runtime"
+import { Effect, Layer, ServiceMap } from "effect"
 
 export namespace Worktree {
   const log = Log.create({ service: "worktree" })
@@ -267,7 +269,7 @@ export namespace Worktree {
     return process.platform === "win32" ? normalized.toLowerCase() : normalized
   }
 
-  async function candidate(root: string, base?: string) {
+  async function candidateName(worktreeDir: string, root: string, base?: string) {
     for (const attempt of Array.from({ length: 26 }, (_, i) => i)) {
       const name = base ? (attempt === 0 ? base : `${base}-${randomName()}`) : randomName()
       const branch = `opencode/${name}`
@@ -277,7 +279,7 @@ export namespace Worktree {
 
       const ref = `refs/heads/${branch}`
       const branchCheck = await git(["show-ref", "--verify", "--quiet", ref], {
-        cwd: Instance.worktree,
+        cwd: worktreeDir,
       })
       if (branchCheck.exitCode === 0) continue
 
@@ -335,338 +337,419 @@ export namespace Worktree {
     }, 0)
   }
 
-  export async function makeWorktreeInfo(name?: string): Promise<Info> {
-    if (Instance.project.vcs !== "git") {
-      throw new NotGitError({ message: "Worktrees are only supported for git projects" })
-    }
-
-    const root = path.join(Global.Path.data, "worktree", Instance.project.id)
-    await fs.mkdir(root, { recursive: true })
+  // ---------------------------------------------------------------------------
+  // Effect service
+  // ---------------------------------------------------------------------------
 
-    const base = name ? slug(name) : ""
-    return candidate(root, base || undefined)
+  export interface Interface {
+    readonly makeWorktreeInfo: (name?: string) => Effect.Effect<Info>
+    readonly createFromInfo: (info: Info, startCommand?: string) => Effect.Effect<() => Promise<void>>
+    readonly create: (input?: CreateInput) => Effect.Effect<Info>
+    readonly remove: (input: RemoveInput) => Effect.Effect<boolean>
+    readonly reset: (input: ResetInput) => Effect.Effect<boolean>
   }
 
-  export async function createFromInfo(info: Info, startCommand?: string) {
-    const created = await git(["worktree", "add", "--no-checkout", "-b", info.branch, info.directory], {
-      cwd: Instance.worktree,
-    })
-    if (created.exitCode !== 0) {
-      throw new CreateFailedError({ message: errorText(created) || "Failed to create git worktree" })
-    }
+  export class Service extends ServiceMap.Service<Service, Interface>()("@opencode/Worktree") {}
 
-    await Project.addSandbox(Instance.project.id, info.directory).catch(() => undefined)
+  export const layer = Layer.effect(
+    Service,
+    Effect.gen(function* () {
+      const instance = yield* InstanceContext
 
-    const projectID = Instance.project.id
-    const extra = startCommand?.trim()
+      const makeWorktreeInfoEffect = Effect.fn("Worktree.makeWorktreeInfo")(function* (name?: string) {
+        return yield* Effect.promise(async () => {
+          if (instance.project.vcs !== "git") {
+            throw new NotGitError({ message: "Worktrees are only supported for git projects" })
+          }
 
-    return () => {
-      const start = async () => {
-        const populated = await git(["reset", "--hard"], { cwd: info.directory })
-        if (populated.exitCode !== 0) {
-          const message = errorText(populated) || "Failed to populate worktree"
-          log.error("worktree checkout failed", { directory: info.directory, message })
-          GlobalBus.emit("event", {
-            directory: info.directory,
-            payload: {
-              type: Event.Failed.type,
-              properties: {
-                message,
-              },
-            },
-          })
-          return
-        }
+          const root = path.join(Global.Path.data, "worktree", instance.project.id)
+          await fs.mkdir(root, { recursive: true })
 
-        const booted = await Instance.provide({
-          directory: info.directory,
-          init: InstanceBootstrap,
-          fn: () => undefined,
-        })
-          .then(() => true)
-          .catch((error) => {
-            const message = error instanceof Error ? error.message : String(error)
-            log.error("worktree bootstrap failed", { directory: info.directory, message })
-            GlobalBus.emit("event", {
-              directory: info.directory,
-              payload: {
-                type: Event.Failed.type,
-                properties: {
-                  message,
-                },
-              },
-            })
-            return false
-          })
-        if (!booted) return
-
-        GlobalBus.emit("event", {
-          directory: info.directory,
-          payload: {
-            type: Event.Ready.type,
-            properties: {
-              name: info.name,
-              branch: info.branch,
-            },
-          },
+          const base = name ? slug(name) : ""
+          return candidateName(instance.worktree, root, base || undefined)
         })
-
-        await runStartScripts(info.directory, { projectID, extra })
-      }
-
-      return start().catch((error) => {
-        log.error("worktree start task failed", { directory: info.directory, error })
       })
-    }
-  }
-
-  export const create = fn(CreateInput.optional(), async (input) => {
-    const info = await makeWorktreeInfo(input?.name)
-    const bootstrap = await createFromInfo(info, input?.startCommand)
-    // This is needed due to how worktrees currently work in the
-    // desktop app
-    setTimeout(() => {
-      bootstrap()
-    }, 0)
-    return info
-  })
 
-  export const remove = fn(RemoveInput, async (input) => {
-    if (Instance.project.vcs !== "git") {
-      throw new NotGitError({ message: "Worktrees are only supported for git projects" })
-    }
+      const createFromInfoEffect = Effect.fn("Worktree.createFromInfo")(function* (
+        info: Info,
+        startCommand?: string,
+      ) {
+        return yield* Effect.promise(async (): Promise<() => Promise<void>> => {
+          const created = await git(["worktree", "add", "--no-checkout", "-b", info.branch, info.directory], {
+            cwd: instance.worktree,
+          })
+          if (created.exitCode !== 0) {
+            throw new CreateFailedError({ message: errorText(created) || "Failed to create git worktree" })
+          }
+
+          await Project.addSandbox(instance.project.id, info.directory).catch(() => undefined)
+
+          const projectID = instance.project.id
+          const extra = startCommand?.trim()
+
+          return () => {
+            const start = async () => {
+              const populated = await git(["reset", "--hard"], { cwd: info.directory })
+              if (populated.exitCode !== 0) {
+                const message = errorText(populated) || "Failed to populate worktree"
+                log.error("worktree checkout failed", { directory: info.directory, message })
+                GlobalBus.emit("event", {
+                  directory: info.directory,
+                  payload: {
+                    type: Event.Failed.type,
+                    properties: {
+                      message,
+                    },
+                  },
+                })
+                return
+              }
+
+              const booted = await Instance.provide({
+                directory: info.directory,
+                init: InstanceBootstrap,
+                fn: () => undefined,
+              })
+                .then(() => true)
+                .catch((error) => {
+                  const message = error instanceof Error ? error.message : String(error)
+                  log.error("worktree bootstrap failed", { directory: info.directory, message })
+                  GlobalBus.emit("event", {
+                    directory: info.directory,
+                    payload: {
+                      type: Event.Failed.type,
+                      properties: {
+                        message,
+                      },
+                    },
+                  })
+                  return false
+                })
+              if (!booted) return
+
+              GlobalBus.emit("event", {
+                directory: info.directory,
+                payload: {
+                  type: Event.Ready.type,
+                  properties: {
+                    name: info.name,
+                    branch: info.branch,
+                  },
+                },
+              })
 
-    const directory = await canonical(input.directory)
-    const locate = async (stdout: Uint8Array | undefined) => {
-      const lines = outputText(stdout)
-        .split("\n")
-        .map((line) => line.trim())
-      const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
-        if (!line) return acc
-        if (line.startsWith("worktree ")) {
-          acc.push({ path: line.slice("worktree ".length).trim() })
-          return acc
-        }
-        const current = acc[acc.length - 1]
-        if (!current) return acc
-        if (line.startsWith("branch ")) {
-          current.branch = line.slice("branch ".length).trim()
-        }
-        return acc
-      }, [])
-
-      return (async () => {
-        for (const item of entries) {
-          if (!item.path) continue
-          const key = await canonical(item.path)
-          if (key === directory) return item
-        }
-      })()
-    }
+              await runStartScripts(info.directory, { projectID, extra })
+            }
 
-    const clean = (target: string) =>
-      fs
-        .rm(target, {
-          recursive: true,
-          force: true,
-          maxRetries: 5,
-          retryDelay: 100,
-        })
-        .catch((error) => {
-          const message = error instanceof Error ? error.message : String(error)
-          throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
+            return start().catch((error) => {
+              log.error("worktree start task failed", { directory: info.directory, error })
+            })
+          }
         })
+      })
 
-    const stop = async (target: string) => {
-      if (!(await exists(target))) return
-      await git(["fsmonitor--daemon", "stop"], { cwd: target })
-    }
-
-    const list = await git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree })
-    if (list.exitCode !== 0) {
-      throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" })
-    }
-
-    const entry = await locate(list.stdout)
-
-    if (!entry?.path) {
-      const directoryExists = await exists(directory)
-      if (directoryExists) {
-        await stop(directory)
-        await clean(directory)
-      }
-      return true
-    }
+      const createEffect = Effect.fn("Worktree.create")(function* (input?: CreateInput) {
+        const parsed = input ? CreateInput.optional().parse(input) : undefined
+        const info = yield* makeWorktreeInfoEffect(parsed?.name)
+        const bootstrap = yield* createFromInfoEffect(info, parsed?.startCommand)
+        // This is needed due to how worktrees currently work in the
+        // desktop app
+        setTimeout(() => {
+          bootstrap()
+        }, 0)
+        return info
+      })
 
-    await stop(entry.path)
-    const removed = await git(["worktree", "remove", "--force", entry.path], {
-      cwd: Instance.worktree,
-    })
-    if (removed.exitCode !== 0) {
-      const next = await git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree })
-      if (next.exitCode !== 0) {
-        throw new RemoveFailedError({
-          message: errorText(removed) || errorText(next) || "Failed to remove git worktree",
+      const removeEffect = Effect.fn("Worktree.remove")(function* (input: RemoveInput) {
+        return yield* Effect.promise(async () => {
+          const parsed = RemoveInput.parse(input)
+          if (instance.project.vcs !== "git") {
+            throw new NotGitError({ message: "Worktrees are only supported for git projects" })
+          }
+
+          const directory = await canonical(parsed.directory)
+          const locate = async (stdout: Uint8Array | undefined) => {
+            const lines = outputText(stdout)
+              .split("\n")
+              .map((line) => line.trim())
+            const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
+              if (!line) return acc
+              if (line.startsWith("worktree ")) {
+                acc.push({ path: line.slice("worktree ".length).trim() })
+                return acc
+              }
+              const current = acc[acc.length - 1]
+              if (!current) return acc
+              if (line.startsWith("branch ")) {
+                current.branch = line.slice("branch ".length).trim()
+              }
+              return acc
+            }, [])
+
+            return (async () => {
+              for (const item of entries) {
+                if (!item.path) continue
+                const key = await canonical(item.path)
+                if (key === directory) return item
+              }
+            })()
+          }
+
+          const clean = (target: string) =>
+            fs
+              .rm(target, {
+                recursive: true,
+                force: true,
+                maxRetries: 5,
+                retryDelay: 100,
+              })
+              .catch((error) => {
+                const message = error instanceof Error ? error.message : String(error)
+                throw new RemoveFailedError({ message: message || "Failed to remove git worktree directory" })
+              })
+
+          const stop = async (target: string) => {
+            if (!(await exists(target))) return
+            await git(["fsmonitor--daemon", "stop"], { cwd: target })
+          }
+
+          const list = await git(["worktree", "list", "--porcelain"], { cwd: instance.worktree })
+          if (list.exitCode !== 0) {
+            throw new RemoveFailedError({ message: errorText(list) || "Failed to read git worktrees" })
+          }
+
+          const entry = await locate(list.stdout)
+
+          if (!entry?.path) {
+            const directoryExists = await exists(directory)
+            if (directoryExists) {
+              await stop(directory)
+              await clean(directory)
+            }
+            return true
+          }
+
+          await stop(entry.path)
+          const removed = await git(["worktree", "remove", "--force", entry.path], {
+            cwd: instance.worktree,
+          })
+          if (removed.exitCode !== 0) {
+            const next = await git(["worktree", "list", "--porcelain"], { cwd: instance.worktree })
+            if (next.exitCode !== 0) {
+              throw new RemoveFailedError({
+                message: errorText(removed) || errorText(next) || "Failed to remove git worktree",
+              })
+            }
+
+            const stale = await locate(next.stdout)
+            if (stale?.path) {
+              throw new RemoveFailedError({ message: errorText(removed) || "Failed to remove git worktree" })
+            }
+          }
+
+          await clean(entry.path)
+
+          const branch = entry.branch?.replace(/^refs\/heads\//, "")
+          if (branch) {
+            const deleted = await git(["branch", "-D", branch], { cwd: instance.worktree })
+            if (deleted.exitCode !== 0) {
+              throw new RemoveFailedError({ message: errorText(deleted) || "Failed to delete worktree branch" })
+            }
+          }
+
+          return true
         })
-      }
-
-      const stale = await locate(next.stdout)
-      if (stale?.path) {
-        throw new RemoveFailedError({ message: errorText(removed) || "Failed to remove git worktree" })
-      }
-    }
-
-    await clean(entry.path)
-
-    const branch = entry.branch?.replace(/^refs\/heads\//, "")
-    if (branch) {
-      const deleted = await git(["branch", "-D", branch], { cwd: Instance.worktree })
-      if (deleted.exitCode !== 0) {
-        throw new RemoveFailedError({ message: errorText(deleted) || "Failed to delete worktree branch" })
-      }
-    }
-
-    return true
-  })
-
-  export const reset = fn(ResetInput, async (input) => {
-    if (Instance.project.vcs !== "git") {
-      throw new NotGitError({ message: "Worktrees are only supported for git projects" })
-    }
-
-    const directory = await canonical(input.directory)
-    const primary = await canonical(Instance.worktree)
-    if (directory === primary) {
-      throw new ResetFailedError({ message: "Cannot reset the primary workspace" })
-    }
+      })
 
-    const list = await git(["worktree", "list", "--porcelain"], { cwd: Instance.worktree })
-    if (list.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(list) || "Failed to read git worktrees" })
-    }
+      const resetEffect = Effect.fn("Worktree.reset")(function* (input: ResetInput) {
+        return yield* Effect.promise(async () => {
+          const parsed = ResetInput.parse(input)
+          if (instance.project.vcs !== "git") {
+            throw new NotGitError({ message: "Worktrees are only supported for git projects" })
+          }
+
+          const directory = await canonical(parsed.directory)
+          const primary = await canonical(instance.worktree)
+          if (directory === primary) {
+            throw new ResetFailedError({ message: "Cannot reset the primary workspace" })
+          }
+
+          const list = await git(["worktree", "list", "--porcelain"], { cwd: instance.worktree })
+          if (list.exitCode !== 0) {
+            throw new ResetFailedError({ message: errorText(list) || "Failed to read git worktrees" })
+          }
+
+          const lines = outputText(list.stdout)
+            .split("\n")
+            .map((line) => line.trim())
+          const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
+            if (!line) return acc
+            if (line.startsWith("worktree ")) {
+              acc.push({ path: line.slice("worktree ".length).trim() })
+              return acc
+            }
+            const current = acc[acc.length - 1]
+            if (!current) return acc
+            if (line.startsWith("branch ")) {
+              current.branch = line.slice("branch ".length).trim()
+            }
+            return acc
+          }, [])
+
+          const entry = await (async () => {
+            for (const item of entries) {
+              if (!item.path) continue
+              const key = await canonical(item.path)
+              if (key === directory) return item
+            }
+          })()
+          if (!entry?.path) {
+            throw new ResetFailedError({ message: "Worktree not found" })
+          }
+
+          const remoteList = await git(["remote"], { cwd: instance.worktree })
+          if (remoteList.exitCode !== 0) {
+            throw new ResetFailedError({ message: errorText(remoteList) || "Failed to list git remotes" })
+          }
+
+          const remotes = outputText(remoteList.stdout)
+            .split("\n")
+            .map((line) => line.trim())
+            .filter(Boolean)
+
+          const remote = remotes.includes("origin")
+            ? "origin"
+            : remotes.length === 1
+              ? remotes[0]
+              : remotes.includes("upstream")
+                ? "upstream"
+                : ""
+
+          const remoteHead = remote
+            ? await git(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd: instance.worktree })
+            : { exitCode: 1, stdout: undefined, stderr: undefined }
+
+          const remoteRef = remoteHead.exitCode === 0 ? outputText(remoteHead.stdout) : ""
+          const remoteTarget = remoteRef ? remoteRef.replace(/^refs\/remotes\//, "") : ""
+          const remoteBranch =
+            remote && remoteTarget.startsWith(`${remote}/`) ? remoteTarget.slice(`${remote}/`.length) : ""
+
+          const mainCheck = await git(["show-ref", "--verify", "--quiet", "refs/heads/main"], {
+            cwd: instance.worktree,
+          })
+          const masterCheck = await git(["show-ref", "--verify", "--quiet", "refs/heads/master"], {
+            cwd: instance.worktree,
+          })
+          const localBranch = mainCheck.exitCode === 0 ? "main" : masterCheck.exitCode === 0 ? "master" : ""
+
+          const target = remoteBranch ? `${remote}/${remoteBranch}` : localBranch
+          if (!target) {
+            throw new ResetFailedError({ message: "Default branch not found" })
+          }
+
+          if (remoteBranch) {
+            const fetch = await git(["fetch", remote, remoteBranch], { cwd: instance.worktree })
+            if (fetch.exitCode !== 0) {
+              throw new ResetFailedError({ message: errorText(fetch) || `Failed to fetch ${target}` })
+            }
+          }
+
+          if (!entry.path) {
+            throw new ResetFailedError({ message: "Worktree path not found" })
+          }
+
+          const worktreePath = entry.path
+
+          const resetToTarget = await git(["reset", "--hard", target], { cwd: worktreePath })
+          if (resetToTarget.exitCode !== 0) {
+            throw new ResetFailedError({
+              message: errorText(resetToTarget) || "Failed to reset worktree to target",
+            })
+          }
 
-    const lines = outputText(list.stdout)
-      .split("\n")
-      .map((line) => line.trim())
-    const entries = lines.reduce<{ path?: string; branch?: string }[]>((acc, line) => {
-      if (!line) return acc
-      if (line.startsWith("worktree ")) {
-        acc.push({ path: line.slice("worktree ".length).trim() })
-        return acc
-      }
-      const current = acc[acc.length - 1]
-      if (!current) return acc
-      if (line.startsWith("branch ")) {
-        current.branch = line.slice("branch ".length).trim()
-      }
-      return acc
-    }, [])
-
-    const entry = await (async () => {
-      for (const item of entries) {
-        if (!item.path) continue
-        const key = await canonical(item.path)
-        if (key === directory) return item
-      }
-    })()
-    if (!entry?.path) {
-      throw new ResetFailedError({ message: "Worktree not found" })
-    }
+          const cleanResult = await sweep(worktreePath)
+          if (cleanResult.exitCode !== 0) {
+            throw new ResetFailedError({ message: errorText(cleanResult) || "Failed to clean worktree" })
+          }
 
-    const remoteList = await git(["remote"], { cwd: Instance.worktree })
-    if (remoteList.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(remoteList) || "Failed to list git remotes" })
-    }
+          const update = await git(["submodule", "update", "--init", "--recursive", "--force"], {
+            cwd: worktreePath,
+          })
+          if (update.exitCode !== 0) {
+            throw new ResetFailedError({ message: errorText(update) || "Failed to update submodules" })
+          }
 
-    const remotes = outputText(remoteList.stdout)
-      .split("\n")
-      .map((line) => line.trim())
-      .filter(Boolean)
-
-    const remote = remotes.includes("origin")
-      ? "origin"
-      : remotes.length === 1
-        ? remotes[0]
-        : remotes.includes("upstream")
-          ? "upstream"
-          : ""
-
-    const remoteHead = remote
-      ? await git(["symbolic-ref", `refs/remotes/${remote}/HEAD`], { cwd: Instance.worktree })
-      : { exitCode: 1, stdout: undefined, stderr: undefined }
-
-    const remoteRef = remoteHead.exitCode === 0 ? outputText(remoteHead.stdout) : ""
-    const remoteTarget = remoteRef ? remoteRef.replace(/^refs\/remotes\//, "") : ""
-    const remoteBranch = remote && remoteTarget.startsWith(`${remote}/`) ? remoteTarget.slice(`${remote}/`.length) : ""
-
-    const mainCheck = await git(["show-ref", "--verify", "--quiet", "refs/heads/main"], {
-      cwd: Instance.worktree,
-    })
-    const masterCheck = await git(["show-ref", "--verify", "--quiet", "refs/heads/master"], {
-      cwd: Instance.worktree,
-    })
-    const localBranch = mainCheck.exitCode === 0 ? "main" : masterCheck.exitCode === 0 ? "master" : ""
+          const subReset = await git(["submodule", "foreach", "--recursive", "git", "reset", "--hard"], {
+            cwd: worktreePath,
+          })
+          if (subReset.exitCode !== 0) {
+            throw new ResetFailedError({ message: errorText(subReset) || "Failed to reset submodules" })
+          }
 
-    const target = remoteBranch ? `${remote}/${remoteBranch}` : localBranch
-    if (!target) {
-      throw new ResetFailedError({ message: "Default branch not found" })
-    }
+          const subClean = await git(["submodule", "foreach", "--recursive", "git", "clean", "-fdx"], {
+            cwd: worktreePath,
+          })
+          if (subClean.exitCode !== 0) {
+            throw new ResetFailedError({ message: errorText(subClean) || "Failed to clean submodules" })
+          }
 
-    if (remoteBranch) {
-      const fetch = await git(["fetch", remote, remoteBranch], { cwd: Instance.worktree })
-      if (fetch.exitCode !== 0) {
-        throw new ResetFailedError({ message: errorText(fetch) || `Failed to fetch ${target}` })
-      }
-    }
+          const status = await git(["-c", "core.fsmonitor=false", "status", "--porcelain=v1"], { cwd: worktreePath })
+          if (status.exitCode !== 0) {
+            throw new ResetFailedError({ message: errorText(status) || "Failed to read git status" })
+          }
 
-    if (!entry.path) {
-      throw new ResetFailedError({ message: "Worktree path not found" })
-    }
+          const dirty = outputText(status.stdout)
+          if (dirty) {
+            throw new ResetFailedError({ message: `Worktree reset left local changes:\n${dirty}` })
+          }
 
-    const worktreePath = entry.path
+          const projectID = instance.project.id
+          queueStartScripts(worktreePath, { projectID })
 
-    const resetToTarget = await git(["reset", "--hard", target], { cwd: worktreePath })
-    if (resetToTarget.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(resetToTarget) || "Failed to reset worktree to target" })
-    }
-
-    const clean = await sweep(worktreePath)
-    if (clean.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(clean) || "Failed to clean worktree" })
-    }
+          return true
+        })
+      })
 
-    const update = await git(["submodule", "update", "--init", "--recursive", "--force"], { cwd: worktreePath })
-    if (update.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(update) || "Failed to update submodules" })
-    }
+      return Service.of({
+        makeWorktreeInfo: makeWorktreeInfoEffect,
+        createFromInfo: createFromInfoEffect,
+        create: createEffect,
+        remove: removeEffect,
+        reset: resetEffect,
+      })
+    }),
+  )
 
-    const subReset = await git(["submodule", "foreach", "--recursive", "git", "reset", "--hard"], {
-      cwd: worktreePath,
-    })
-    if (subReset.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(subReset) || "Failed to reset submodules" })
-    }
+  // ---------------------------------------------------------------------------
+  // Promise facades
+  // ---------------------------------------------------------------------------
 
-    const subClean = await git(["submodule", "foreach", "--recursive", "git", "clean", "-fdx"], {
-      cwd: worktreePath,
-    })
-    if (subClean.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(subClean) || "Failed to clean submodules" })
-    }
+  export async function makeWorktreeInfo(name?: string): Promise<Info> {
+    return runPromiseInstance(Service.use((svc) => svc.makeWorktreeInfo(name)))
+  }
 
-    const status = await git(["-c", "core.fsmonitor=false", "status", "--porcelain=v1"], { cwd: worktreePath })
-    if (status.exitCode !== 0) {
-      throw new ResetFailedError({ message: errorText(status) || "Failed to read git status" })
-    }
+  export async function createFromInfo(info: Info, startCommand?: string) {
+    return runPromiseInstance(Service.use((svc) => svc.createFromInfo(info, startCommand)))
+  }
 
-    const dirty = outputText(status.stdout)
-    if (dirty) {
-      throw new ResetFailedError({ message: `Worktree reset left local changes:\n${dirty}` })
-    }
+  export const create = Object.assign(
+    async (input?: CreateInput) => {
+      return runPromiseInstance(Service.use((svc) => svc.create(input)))
+    },
+    { schema: CreateInput.optional() },
+  )
 
-    const projectID = Instance.project.id
-    queueStartScripts(worktreePath, { projectID })
+  export const remove = Object.assign(
+    async (input: RemoveInput) => {
+      return runPromiseInstance(Service.use((svc) => svc.remove(input)))
+    },
+    { schema: RemoveInput },
+  )
 
-    return true
-  })
+  export const reset = Object.assign(
+    async (input: ResetInput) => {
+      return runPromiseInstance(Service.use((svc) => svc.reset(input)))
+    },
+    { schema: ResetInput },
+  )
 }