import { tool } from "@opencode-ai/plugin" import { $ } from "bun" import { join, dirname } from "node:path" async function getWorktreeLocation() { const repoRoot = await $`git rev-parse --show-toplevel`.text() const worktreesDir = join(dirname(repoRoot), "worktrees") return { repoRoot, worktreesDir } } export const worktreeCreate = tool({ description: "Create a new git worktree with specified name and branch", args: { name: tool.schema.string().describe("Worktree directory name (e.g., 'issue-42')"), branch: tool.schema.string().describe("Git branch to attach (must exist or will be created from base)"), base: tool.schema.string().optional().describe("Base branch for new branches (default: origin/main)"), }, async execute(args, context) { try { const { worktreesDir } = await getWorktreeLocation() const worktreePath = join(worktreesDir, args.name) await $`mkdir -p ${worktreesDir}`.nothrow() const existingBranch = await $`git branch --list ${args.branch}`.text() const base = args.base || "origin/main" if (existingBranch.trim()) { await $`git worktree add ${worktreePath} ${args.branch}` } else { await $`git worktree add ${worktreePath} -b ${args.branch} ${base}` } return { success: true, path: worktreePath, } } catch (error: any) { return { success: false, error: "Failed to create worktree", message: error.message, stderr: error.stderr?.toString(), } } }, }) export const worktreeList = tool({ description: "List all worktrees with their paths and branches", args: { path: tool.schema.string().optional().describe("Specific worktree path to list (lists all if omitted)"), }, async execute(args, context) { try { const { repoRoot } = await getWorktreeLocation() let cmd if (args.path) { cmd = $`git worktree list --porcelain` } else { cmd = $`git worktree list --porcelain` } const output = await cmd.text() const worktrees = output .split("\n") .filter((line) => line.startsWith("worktree ")) .map((line) => { const path = line.replace("worktree ", "").trim() return { path } }) return { success: true, worktrees, } } catch (error: any) { return { success: false, error: "Failed to list worktrees", message: error.message, stderr: error.stderr?.toString(), } } }, }) export const worktreeCheckout = tool({ description: "Checkout an existing worktree by name", args: { name: tool.schema.string().describe("Worktree name to checkout"), }, async execute(args, context) { try { const { worktreesDir } = await getWorktreeLocation() const worktreePath = join(worktreesDir, args.name) if (!(await $`test -d ${worktreePath}`.nothrow().text())) { return { success: false, error: "Worktree does not exist", message: `Worktree '${args.name}' not found at ${worktreePath}`, } } return { success: true, path: worktreePath, } } catch (error: any) { return { success: false, error: "Failed to checkout worktree", message: error.message, stderr: error.stderr?.toString(), } } }, }) export const worktreeRemove = tool({ description: "Remove a worktree with dirty-check safety", args: { name: tool.schema.string().describe("Worktree name to remove"), force: tool.schema.boolean().optional().describe("Force remove even if dirty"), }, async execute(args, context) { try { const { worktreesDir } = await getWorktreeLocation() const worktreePath = join(worktreesDir, args.name) if (!(await $`test -d ${worktreePath}`.nothrow().text())) { return { success: false, error: "Worktree does not exist", message: `Worktree '${args.name}' not found at ${worktreePath}`, } } if (!args.force) { const status = await $`git -C ${worktreePath} status --porcelain`.nothrow().text() if (status.trim()) { return { success: false, error: "Worktree has uncommitted changes", message: "Use --force to remove anyway", } } } await $`git worktree remove ${worktreePath} ${args.force ? "--force" : ""}` return { success: true, message: `Worktree '${args.name}' removed`, } } catch (error: any) { return { success: false, error: "Failed to remove worktree", message: error.message, stderr: error.stderr?.toString(), } } }, }) export const worktreeCleanup = tool({ description: "Remove stale worktrees (manually invoked)", args: { olderThan: tool.schema.number().optional().describe("Remove worktrees older than N days (default: 7)"), force: tool.schema.boolean().optional().describe("Force cleanup without confirmation"), }, async execute(args, context) { try { const { worktreesDir } = await getWorktreeLocation() const olderThan = args.olderThan || 7 const output = await $`git worktree list --porcelain`.text() const worktrees = output .split("\n") .filter((line) => line.startsWith("worktree ")) .map((line) => { const path = line.replace("worktree ", "").trim() return path }) const now = new Date() const staleWorktrees = [] for (const path of worktrees) { if (!(await $`test -d ${path}`.nothrow().text())) { continue } const stat = await $`stat -f %m ${path}`.nothrow().text() const mtime = parseInt(stat.trim()) * 1000 const ageDays = (now.getTime() - mtime) / (1000 * 60 * 60 * 24) if (ageDays > olderThan) { staleWorktrees.push({ path, age: Math.round(ageDays) }) } } if (staleWorktrees.length === 0) { return { success: true, message: "No stale worktrees found", } } if (!args.force) { return { success: true, staleWorktrees, message: `Found ${staleWorktrees.length} stale worktree(s). Use --force to remove.`, } } for (const { path } of staleWorktrees) { await $`git worktree remove ${path} --force`.nothrow() } return { success: true, removed: staleWorktrees, message: `Removed ${staleWorktrees.length} stale worktree(s)`, } } catch (error: any) { return { success: false, error: "Failed to cleanup worktrees", message: error.message, stderr: error.stderr?.toString(), } } }, })