Something went wrong. Try again.
opencode v2 mirror
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358export * as Worktree from "./worktree.js"
import { Context, Effect, Layer, Schema } from "effect"import { and, asc, desc, eq, isNull, sql } from "drizzle-orm"import path from "path"import { AbsolutePath } from "./schema.js"import { FSUtil } from "@opencode/util/fs-util"import { Git } from "./git.js"import { Node } from "@opencode/util/effect/app-node"import { LayerNode } from "@opencode/util/effect/layer-node"import { Slug } from "./util/slug.js"import { Bus } from "./bus.js"import { Database } from "./database/database.js"import { Location } from "./location.js"import { LocationServiceMap } from "./location-service-map.js"import { Project } from "./project.js"import { Worktree } from "@opencode/schema/worktree"import { WorktreeTable } from "./worktree/sql.js"import { canonical, DirectoryUnavailableError } from "./worktree/directory.js"import { WorktreeGit } from "./worktree/git.js"import type { EffectDrizzleSqlite } from "./database/drizzle.js"import { ProjectTable } from "./project/sql.js"import { AppProcess } from "@opencode/util/process"import { ChildProcess } from "effect/unstable/process"import { WorktreeStrategies } from "./worktree/strategies.js"
export type { Strategy, Editor } from "./worktree/strategies.js"
export { DirectoryUnavailableError } from "./worktree/directory.js"export { OperationError } from "@opencode/schema/worktree"
export const StrategyID = Worktree.StrategyIDexport type StrategyID = typeof StrategyID.Type
export const CreateInput = Worktree.CreateInputexport type CreateInput = typeof CreateInput.Type
export const RemoveInput = Worktree.RemoveInputexport type RemoveInput = typeof RemoveInput.Type
export const Info = Worktree.Infoexport type Info = typeof Info.Type
export const List = Worktree.Listexport type List = typeof List.Type
export const ListEntry = Worktree.ListEntryexport type ListEntry = typeof ListEntry.Type
export class SourceDirectoryNotFoundError extends Schema.TaggedError<SourceDirectoryNotFoundError>()( "Worktree.SourceDirectoryNotFoundError", { projectID: Project.ID, directory: Schema.optional(AbsolutePath) },) {}
export class DestinationExistsError extends Schema.TaggedError<DestinationExistsError>()( "Worktree.DestinationExistsError", { directory: AbsolutePath },) {}
export class InvalidDirectoryError extends Schema.TaggedError<InvalidDirectoryError>()( "Worktree.InvalidDirectoryError", { directory: AbsolutePath },) {}
export class StrategyUnavailableError extends Schema.TaggedError<StrategyUnavailableError>()( "Worktree.StrategyUnavailableError", { strategy: StrategyID },) {}
export type Error = | Project.NotFoundError | SourceDirectoryNotFoundError | DestinationExistsError | DirectoryUnavailableError | InvalidDirectoryError | StrategyUnavailableError | Worktree.OperationError | AppProcess.AppProcessError | Git.WorktreeError
export const Event = Worktree.Event
interface StoredInput { readonly directory: AbsolutePath readonly strategy?: string readonly replace?: boolean}
type DatabaseClient = EffectDrizzleSqlite.EffectSQLiteDatabasetype Transaction = Parameters<Parameters<DatabaseClient["transaction"]>[0]>[0]
export interface Interface { readonly list: (input: { projectID: Project.ID }) => Effect.Effect<List, Project.NotFoundError> // The plugin bridge supplies its registry so canonical-project setup can use registrations made so far. readonly create: (input: CreateInput, current?: WorktreeStrategies.Interface) => Effect.Effect<Info, Error> readonly remove: (input: RemoveInput, current?: WorktreeStrategies.Interface) => Effect.Effect<void, Error> readonly refresh: ( input: { projectID: Project.ID }, current?: WorktreeStrategies.Interface, ) => Effect.Effect<void, Error>}
export class Service extends Context.Service<Service, Interface>()("@opencode/Worktree") {}
const layer = Layer.effect( Service, Effect.gen(function* () { const fs = yield* FSUtil.Service const database = yield* Database.Service const db = database.db const bus = yield* Bus.Service const processService = yield* AppProcess.Service const locations = yield* LocationServiceMap.Service const gitStrategy = yield* WorktreeGit.make const project = Effect.fnUntraced(function* (projectID: Project.ID) { const row = yield* db.select().from(ProjectTable).where(eq(ProjectTable.id, projectID)).get().pipe(Effect.orDie) if (!row) return yield* new Project.NotFoundError({ projectID }) return row })
const load = Effect.fnUntraced(function* (directory: AbsolutePath, current?: WorktreeStrategies.Interface) { if (!(yield* fs.isDir(directory))) return yield* new DirectoryUnavailableError({ directory }) if (current?.directory === directory) return current.get() const { Plugin } = yield* Effect.promise(() => import("./plugin.js")) const context = yield* locations.contextEffect(Location.Ref.make({ directory })) yield* Context.get(context, Plugin.Service).awaitActivation return Context.get(context, WorktreeStrategies.Service).get() })
const changed = Effect.fnUntraced(function* (projectID: Project.ID, update: boolean) { if (update) yield* bus.publish(Event.Updated, { projectID }) })
const ops = { list: Effect.fnUntraced(function* (projectID: Project.ID) { const rows = yield* db .select({ directory: WorktreeTable.directory, strategy: WorktreeTable.strategy }) .from(WorktreeTable) .where(eq(WorktreeTable.project_id, projectID)) .orderBy(desc(WorktreeTable.time_created), asc(WorktreeTable.directory)) .all() .pipe(Effect.orDie) return rows.map((row) => ({ directory: row.directory, strategy: row.strategy ?? undefined })) }), find: Effect.fnUntraced(function* (projectID: Project.ID, directory: AbsolutePath) { const row = yield* db .select({ directory: WorktreeTable.directory, strategy: WorktreeTable.strategy }) .from(WorktreeTable) .where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory))) .get() .pipe(Effect.orDie) return row ? { directory: row.directory, strategy: row.strategy ?? undefined } : undefined }), create: (projectID: Project.ID, input: StoredInput, tx?: Transaction) => (tx ?? db) .insert(WorktreeTable) .values({ project_id: projectID, directory: input.directory, strategy: input.strategy, }) .onConflictDoUpdate({ target: [WorktreeTable.project_id, WorktreeTable.directory], set: { strategy: input.strategy ?? null, }, // Discovery may claim an unowned row, but never replace another strategy's ownership. setWhere: input.replace ? undefined : input.strategy ? isNull(WorktreeTable.strategy) : sql`false`, }) .returning({ directory: WorktreeTable.directory }) .get() .pipe( Effect.orDie, Effect.map((row) => row !== undefined), ), remove: (projectID: Project.ID, directory: AbsolutePath, tx?: Transaction) => (tx ?? db) .delete(WorktreeTable) .where(and(eq(WorktreeTable.project_id, projectID), eq(WorktreeTable.directory, directory))) .returning({ directory: WorktreeTable.directory }) .get() .pipe( Effect.orDie, Effect.map((row) => row !== undefined), ), }
const source = Effect.fnUntraced(function* (projectID: Project.ID, sourceDirectory: AbsolutePath) { const resolved = yield* canonical(fs, sourceDirectory) if ((yield* ops.find(projectID, resolved)) === undefined) return yield* new SourceDirectoryNotFoundError({ projectID, directory: resolved }) return resolved })
const getStrategy = Effect.fnUntraced(function* ( id: StrategyID, strategies: ReadonlyMap<StrategyID, WorktreeStrategies.Strategy>, ) { const found = strategies.get(id) if (!found) return yield* new StrategyUnavailableError({ strategy: id }) return found })
const create = Effect.fn("Worktree.create")(function* (input: CreateInput, current?: WorktreeStrategies.Interface) { const row = yield* project(input.projectID) const settings = yield* load(row.worktree, current) const selected = yield* getStrategy(settings.selected, settings.strategies) const directory = input.directory ?? settings.directory const sourceDirectory = yield* source(input.projectID, input.from ?? row.worktree) yield* fs.makeDirectory(directory, { recursive: true }).pipe(Effect.orDie) const name = input.name ?? Slug.create() let suffix = 1 let worktreeDirectory = AbsolutePath.make(path.join(directory, name)) while (yield* fs.existsSafe(worktreeDirectory)) { suffix++ if (suffix > 10) return yield* new DestinationExistsError({ directory: worktreeDirectory }) worktreeDirectory = AbsolutePath.make(path.join(directory, `${name}-${suffix}`)) }
const created = yield* selected .create({ directory: worktreeDirectory, sourceDirectory, branch: input.branch, }) .pipe(Effect.mapError((error) => operationError(selected.id, "create", error))) const result = { directory: yield* canonical(fs, created.directory) } yield* changed( input.projectID, yield* ops.create(input.projectID, { directory: result.directory, strategy: selected.id, replace: true, }), ) const command = row.commands?.start?.trim() if (command) { const windows = process.platform === "win32" yield* processService .run( ChildProcess.make(windows ? command : "bash", windows ? [] : ["-lc", command], { cwd: result.directory, env: { OPENCODE_WORKTREE_BASE: sourceDirectory, OPENCODE_WORKTREE_PATH: result.directory, }, extendEnv: true, stdin: "ignore", shell: windows, }), ) .pipe(Effect.flatMap(AppProcess.requireSuccess)) } return result }, Effect.scoped)
const remove = Effect.fn("Worktree.remove")(function* (input: RemoveInput, current?: WorktreeStrategies.Interface) { const row = yield* project(input.projectID) const worktreeDirectory = yield* canonical(fs, input.directory) const stored = yield* ops.find(input.projectID, worktreeDirectory) if (!stored?.strategy) return yield* new InvalidDirectoryError({ directory: worktreeDirectory }) const settings = yield* load(row.worktree, current) const strategy = yield* getStrategy(StrategyID.make(stored.strategy), settings.strategies) yield* strategy .remove({ directory: worktreeDirectory, force: input.force, }) .pipe(Effect.mapError((error) => operationError(strategy.id, "remove", error))) yield* changed(input.projectID, yield* ops.remove(input.projectID, worktreeDirectory)) }, Effect.scoped)
const refresh = Effect.fn("Worktree.refresh")(function* ( input: { projectID: Project.ID }, current?: WorktreeStrategies.Interface, ) { const row = yield* project(input.projectID) const settings = yield* load(row.worktree, current) const stored = yield* ops.list(input.projectID) const checked = yield* Effect.forEach( stored, (item) => fs.isDir(item.directory).pipe(Effect.map((exists) => ({ ...item, exists }))), { concurrency: "unbounded" }, ) const strategies = Array.from(settings.strategies.values()).toReversed() const discovered = new Map<AbsolutePath, StoredInput>() // Unowned rows are checkout/discovery roots. Managed children are enumerated by their backend. const roots = new Set([ row.worktree, ...checked.filter((item) => item.exists && !item.strategy).map((item) => item.directory), ]) for (const directory of roots) { if (!(yield* fs.isDir(directory))) continue for (const strategy of strategies) { const entries = yield* strategy.list(directory).pipe( Effect.mapError((error) => operationError(strategy.id, "list", error)), Effect.catch((error) => Effect.logWarning("worktree discovery failed", { directory, strategy: strategy.id, error }).pipe( Effect.as([]), ), ), ) for (const entry of entries) { const directory = yield* canonical(fs, entry.directory).pipe( Effect.catchTag("Worktree.DirectoryUnavailableError", () => Effect.undefined), ) if (!directory || discovered.has(directory)) continue discovered.set(directory, { directory, strategy: entry.type === "worktree" ? strategy.id : undefined, }) } } } const removed = checked.filter((item) => !item.exists).map((item) => item.directory) const changes = yield* db .transaction((tx) => Effect.all({ updated: Effect.filter(Array.from(discovered.values()), (item) => ops.create(input.projectID, item, tx), ).pipe(Effect.map((items) => items.map((item) => item.directory))), removed: Effect.filter(removed, (directory) => ops.remove(input.projectID, directory, tx)), }), ) .pipe(Effect.orDie) yield* changed(input.projectID, changes.updated.length > 0 || changes.removed.length > 0) }, Effect.scoped)
return Service.of({ list: Effect.fn("Worktree.list")(function* (input) { yield* project(input.projectID) return yield* ops.list(input.projectID) }), create, remove, refresh, }) }),)
export const node: LayerNode.Provider<Service, never, typeof Node.tags.values.global> = Node.makeGlobalNode({ service: Service, layer, deps: [FSUtil.node, Git.node, Bus.node, Database.node, AppProcess.node, LocationServiceMap.node],})
function operationError(strategy: StrategyID, operation: string, error: unknown) { if ( error instanceof Git.WorktreeError || error instanceof DirectoryUnavailableError || error instanceof Worktree.OperationError ) return error return new Worktree.OperationError({ message: `Worktree strategy ${strategy} failed to ${operation}: ${error instanceof globalThis.Error ? error.message : String(error)}`, })}