diff --git a/packages/opencode/src/control-plane/adaptors/worktree.ts b/packages/opencode/src/control-plane/adaptors/worktree.ts index f848909501..ff2d92e199 100644 --- a/packages/opencode/src/control-plane/adaptors/worktree.ts +++ b/packages/opencode/src/control-plane/adaptors/worktree.ts @@ -22,12 +22,11 @@ export const WorktreeAdaptor: Adaptor = { }, async create(info) { const config = Config.parse(info) - const bootstrap = await Worktree.createFromInfo({ + await Worktree.createFromInfo({ name: config.name, directory: config.directory, branch: config.branch, }) - return bootstrap() }, async remove(info) { const config = Config.parse(info) diff --git a/packages/opencode/src/worktree/index.ts b/packages/opencode/src/worktree/index.ts index 3599197122..718b9a76f5 100644 --- a/packages/opencode/src/worktree/index.ts +++ b/packages/opencode/src/worktree/index.ts @@ -14,7 +14,7 @@ import { Process } from "../util/process" import { git } from "../util/git" import { BusEvent } from "@/bus/bus-event" import { GlobalBus } from "@/bus/global" -import { Effect, FileSystem, Layer, Path, ServiceMap, Stream } from "effect" +import { Effect, Fiber, FileSystem, Layer, Path, Scope, ServiceMap, Stream } from "effect" import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process" import { NodeChildProcessSpawner, NodeFileSystem, NodePath } from "@effect/platform-node" import { makeRunPromise } from "@/effect/run-service" @@ -344,7 +344,7 @@ export namespace Worktree { export interface Interface { readonly makeWorktreeInfo: (name?: string) => Effect.Effect - readonly createFromInfo: (info: Info, startCommand?: string) => Effect.Effect<() => Promise> + readonly createFromInfo: (info: Info, startCommand?: string) => Effect.Effect readonly create: (input?: CreateInput) => Effect.Effect readonly remove: (input: RemoveInput) => Effect.Effect readonly reset: (input: ResetInput) => Effect.Effect @@ -361,6 +361,7 @@ export namespace Worktree { > = Layer.effect( Service, Effect.gen(function* () { + const scope = yield* Scope.Scope const fsys = yield* FileSystem.FileSystem const pathSvc = yield* Path.Path const spawner = yield* ChildProcessSpawner.ChildProcessSpawner @@ -412,83 +413,67 @@ export namespace Worktree { info: Info, startCommand?: string, ) { - return yield* Effect.promise(async () => { - const created = await git(["worktree", "add", "--no-checkout", "-b", info.branch, info.directory], { - cwd: Instance.worktree, + const created = yield* gitRun( + ["worktree", "add", "--no-checkout", "-b", info.branch, info.directory], + { cwd: Instance.worktree }, + ) + if (created.code !== 0) { + throw new CreateFailedError({ message: (created.stderr || created.text) || "Failed to create git worktree" }) + } + + yield* Effect.promise(() => Project.addSandbox(Instance.project.id, info.directory).catch(() => undefined)) + + const projectID = Instance.project.id + const extra = startCommand?.trim() + + // Populate the worktree, boot the instance, run start scripts + const populated = yield* gitRun(["reset", "--hard"], { cwd: info.directory }) + if (populated.code !== 0) { + const message = (populated.stderr || populated.text) || "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 } }, }) - 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 + return + } + yield* Effect.promise(async () => { + 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.Ready.type, - properties: { - name: info.name, - branch: info.branch, - }, - }, + payload: { type: Event.Failed.type, properties: { message } }, }) - - await runStartScripts(info.directory, { projectID, extra }) - } - - return start().catch((error) => { - log.error("worktree start task failed", { directory: info.directory, error }) + return false }) - } + if (!booted) return + + GlobalBus.emit("event", { + directory: info.directory, + payload: { + type: Event.Ready.type, + properties: { name: info.name, branch: info.branch }, + }, + }) + + await runStartScripts(info.directory, { projectID, extra }) }) }) const create = Effect.fn("Worktree.create")(function* (input?: CreateInput) { const info = yield* makeWorktreeInfo(input?.name) - const bootstrap = yield* createFromInfo(info, input?.startCommand) - // This is needed due to how worktrees currently work in the desktop app - setTimeout(() => { - bootstrap() - }, 0) + yield* createFromInfo(info, input?.startCommand).pipe( + Effect.catchCause((cause) => Effect.sync(() => log.error("worktree bootstrap failed", { cause }))), + Effect.forkIn(scope), + ) return info }) diff --git a/packages/opencode/test/project/worktree.test.ts b/packages/opencode/test/project/worktree.test.ts index 5b6e58a0e8..1d3e857a05 100644 --- a/packages/opencode/test/project/worktree.test.ts +++ b/packages/opencode/test/project/worktree.test.ts @@ -90,13 +90,11 @@ describe("Worktree", () => { }) describe("createFromInfo", () => { - test("creates git worktree and returns bootstrap function", async () => { + test("creates and bootstraps git worktree", async () => { await using tmp = await tmpdir({ git: true }) const info = await withInstance(tmp.path, () => Worktree.makeWorktreeInfo("from-info-test")) - const bootstrap = await withInstance(tmp.path, () => Worktree.createFromInfo(info)) - - expect(typeof bootstrap).toBe("function") + await withInstance(tmp.path, () => Worktree.createFromInfo(info)) // Worktree should exist in git const list = await $`git worktree list --porcelain`.cwd(tmp.path).quiet().text()