Commit 5cd0a634b01c
Changed files (5)
dots
pi
agent
extensions
dots/pi/agent/extensions/git/index.ts
@@ -0,0 +1,336 @@
+// =============================================================================
+// Git Extension for Pi Coding Agent
+// =============================================================================
+
+import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
+import {
+ listWorktrees,
+ createWorktree,
+ removeWorktree,
+ pruneWorktrees,
+ getCurrentWorktreeInfo,
+} from "./worktree.js";
+import { findRepoRoot, hasUncommittedChanges } from "./utils.js";
+
+export default function (pi: ExtensionAPI) {
+ // =========================================================================
+ // Commands: /worktree (alias: /wt)
+ // =========================================================================
+
+ pi.registerCommand("worktree", {
+ description: "Manage git worktrees (list, create, remove, prune)",
+ handler: async (args, ctx) => {
+ const parts = args.trim().split(/\s+/).filter(Boolean);
+ const subcommand = parts[0] || "list";
+ const remainingArgs = parts.slice(1);
+
+ try {
+ switch (subcommand) {
+ case "list":
+ case "ls":
+ await handleList(ctx);
+ break;
+
+ case "create":
+ case "new":
+ case "add":
+ await handleCreate(remainingArgs, ctx);
+ break;
+
+ case "remove":
+ case "rm":
+ case "delete":
+ await handleRemove(remainingArgs, ctx);
+ break;
+
+ case "prune":
+ await handlePrune(ctx);
+ break;
+
+ default:
+ ctx.ui.notify(`Unknown worktree command: ${subcommand}`, "error");
+ ctx.ui.notify(
+ "Available: list, create <branch>, remove <branch>, prune",
+ "info"
+ );
+ }
+ } catch (error: any) {
+ ctx.ui.notify(`Error: ${error.message}`, "error");
+ }
+ },
+ });
+
+ // Register /wt alias
+ pi.registerCommand("wt", {
+ description: "Alias for /worktree",
+ handler: async (args, ctx) => {
+ const parts = args.trim().split(/\s+/).filter(Boolean);
+ const subcommand = parts[0] || "list";
+ const remainingArgs = parts.slice(1);
+
+ try {
+ switch (subcommand) {
+ case "list":
+ case "ls":
+ await handleList(ctx);
+ break;
+
+ case "create":
+ case "new":
+ case "add":
+ await handleCreate(remainingArgs, ctx);
+ break;
+
+ case "remove":
+ case "rm":
+ case "delete":
+ await handleRemove(remainingArgs, ctx);
+ break;
+
+ case "prune":
+ await handlePrune(ctx);
+ break;
+
+ default:
+ ctx.ui.notify(`Unknown worktree command: ${subcommand}`, "error");
+ ctx.ui.notify(
+ "Available: list, create <branch>, remove <branch>, prune",
+ "info"
+ );
+ }
+ } catch (error: any) {
+ ctx.ui.notify(`Error: ${error.message}`, "error");
+ }
+ },
+ });
+
+ // =========================================================================
+ // Command Handlers
+ // =========================================================================
+
+ async function handleList(ctx: any) {
+ const repoRoot = findRepoRoot(ctx.cwd);
+ if (!repoRoot) {
+ ctx.ui.notify("Not in a git repository", "error");
+ return;
+ }
+
+ const worktrees = listWorktrees(ctx.cwd);
+ const currentInfo = getCurrentWorktreeInfo(ctx.cwd);
+
+ if (worktrees.length === 0) {
+ ctx.ui.notify("No worktrees found", "info");
+ return;
+ }
+
+ // Build output
+ const lines: string[] = [];
+ lines.push("Git Worktrees:");
+ lines.push("");
+
+ for (const wt of worktrees) {
+ const isCurrent = currentInfo && wt.path === currentInfo.path;
+ const marker = isCurrent ? "โ" : " ";
+ const dirty = hasUncommittedChanges(wt.path) ? "*" : "";
+
+ lines.push(`${marker} ${wt.branch}${dirty}`);
+ lines.push(` Path: ${wt.path}`);
+ lines.push(` Commit: ${wt.commit.substring(0, 8)}`);
+
+ if (wt.locked) lines.push(" ๐ Locked");
+ if (wt.prunable) lines.push(" โป๏ธ Prunable");
+
+ lines.push("");
+ }
+
+ ctx.ui.notify(lines.join("\n"), "info");
+ }
+
+ async function handleCreate(args: string[], ctx: any) {
+ const branch = args[0];
+ if (!branch) {
+ ctx.ui.notify("Usage: /worktree create <branch> [path]", "error");
+ return;
+ }
+
+ const customPath = args[1];
+
+ try {
+ const worktreePath = createWorktree(
+ {
+ branch,
+ path: customPath,
+ fromRemote: true,
+ },
+ ctx.cwd
+ );
+
+ ctx.ui.notify(`โ Worktree created: ${worktreePath}`, "success");
+
+ // Show how to switch
+ ctx.ui.notify(`To switch: cd ${worktreePath}`, "info");
+ } catch (error: any) {
+ ctx.ui.notify(`Failed to create worktree: ${error.message}`, "error");
+ }
+ }
+
+ async function handleRemove(args: string[], ctx: any) {
+ const branch = args[0];
+ if (!branch) {
+ ctx.ui.notify("Usage: /worktree remove <branch>", "error");
+ return;
+ }
+
+ // Check for uncommitted changes
+ const worktrees = listWorktrees(ctx.cwd);
+ const worktree = worktrees.find((wt) => wt.branch === branch);
+
+ if (!worktree) {
+ ctx.ui.notify(`Worktree for branch '${branch}' not found`, "error");
+ return;
+ }
+
+ const hasChanges = hasUncommittedChanges(worktree.path);
+
+ if (hasChanges) {
+ const confirmed = await ctx.ui.confirm(
+ `Worktree has uncommitted changes. Remove anyway?`
+ );
+
+ if (!confirmed) {
+ ctx.ui.notify("Cancelled", "info");
+ return;
+ }
+ }
+
+ try {
+ removeWorktree(
+ {
+ branch,
+ force: hasChanges,
+ },
+ ctx.cwd
+ );
+
+ ctx.ui.notify(`โ Worktree removed: ${branch}`, "success");
+ } catch (error: any) {
+ ctx.ui.notify(`Failed to remove worktree: ${error.message}`, "error");
+ }
+ }
+
+ async function handlePrune(ctx: any) {
+ try {
+ pruneWorktrees(ctx.cwd);
+ ctx.ui.notify("โ Pruned stale worktree references", "success");
+ } catch (error: any) {
+ ctx.ui.notify(`Failed to prune worktrees: ${error.message}`, "error");
+ }
+ }
+
+ // =========================================================================
+ // Tool: git-worktree (for AI to use)
+ // TODO: Implement with TypeBox schema
+ // =========================================================================
+
+ /* TODO: Add TypeBox dependency and implement tool
+ pi.registerTool({
+ name: "git-worktree",
+ description:
+ "Manage git worktrees. Create isolated working directories for different branches without switching in the main repository.",
+ input_schema: {
+ type: "object",
+ properties: {
+ action: {
+ type: "string",
+ enum: ["list", "create", "remove"],
+ description: "The worktree action to perform",
+ },
+ branch: {
+ type: "string",
+ description: "Branch name (required for create/remove)",
+ },
+ path: {
+ type: "string",
+ description: "Custom path for worktree (optional for create)",
+ },
+ force: {
+ type: "boolean",
+ description: "Force removal even with uncommitted changes",
+ },
+ },
+ required: ["action"],
+ },
+ async execute(input, ctx) {
+ const { action, branch, path: customPath, force } = input;
+
+ try {
+ switch (action) {
+ case "list": {
+ const worktrees = listWorktrees(ctx.cwd);
+ return {
+ worktrees: worktrees.map((wt) => ({
+ path: wt.path,
+ branch: wt.branch,
+ commit: wt.commit.substring(0, 8),
+ dirty: hasUncommittedChanges(wt.path),
+ locked: wt.locked,
+ prunable: wt.prunable,
+ })),
+ };
+ }
+
+ case "create": {
+ if (!branch) {
+ throw new Error("Branch name required for create action");
+ }
+
+ const worktreePath = createWorktree(
+ {
+ branch,
+ path: customPath,
+ fromRemote: true,
+ },
+ ctx.cwd
+ );
+
+ return {
+ success: true,
+ path: worktreePath,
+ branch,
+ message: `Worktree created at ${worktreePath}`,
+ };
+ }
+
+ case "remove": {
+ if (!branch) {
+ throw new Error("Branch name required for remove action");
+ }
+
+ removeWorktree(
+ {
+ branch,
+ force: force ?? false,
+ },
+ ctx.cwd
+ );
+
+ return {
+ success: true,
+ branch,
+ message: `Worktree removed for branch ${branch}`,
+ };
+ }
+
+ default:
+ throw new Error(`Unknown action: ${action}`);
+ }
+ } catch (error: any) {
+ return {
+ success: false,
+ error: error.message,
+ };
+ }
+ },
+ });
+ */
+}
dots/pi/agent/extensions/git/README.md
@@ -0,0 +1,301 @@
+# Git Extension for Pi Coding Agent
+
+Comprehensive git workflow automation for pi, focusing on worktree management, smart rebase/fixup workflows, and other common git tasks.
+
+## Features
+
+### Phase 1: Worktree Management โ
+
+Manage git worktrees easily without cluttering your repository with multiple checkouts.
+
+**Commands:**
+- `/worktree list` or `/wt ls` - List all worktrees with status
+- `/worktree create <branch>` or `/wt new <branch>` - Create worktree for branch
+- `/worktree remove <branch>` or `/wt rm <branch>` - Remove worktree
+- `/worktree prune` - Clean up stale worktree references
+
+**Tool:**
+- `git-worktree` - AI can manage worktrees programmatically
+
+### Upcoming Features
+
+- **Phase 2:** Smart rebase/fixup workflows
+- **Phase 3:** Commit helpers and validation
+- **Phase 4:** Branch management
+- **Phase 5:** Integration and polish
+
+## Installation
+
+The extension is automatically loaded from `~/.config/pi/agent/extensions/git/`.
+
+To enable:
+```bash
+cd ~/src/home/dots
+make pi-agent
+```
+
+## Usage
+
+### List Worktrees
+
+```
+/worktree list
+/wt ls
+```
+
+Shows all worktrees with:
+- Branch name (with dirty indicator `*`)
+- Path
+- Current commit
+- Status (locked, prunable)
+- Current worktree indicator `โ`
+
+### Create Worktree
+
+```
+/worktree create feature-branch
+/wt new bugfix-123
+```
+
+Creates a worktree in `.worktrees/feature-branch` relative to repository root.
+
+**With custom path:**
+```
+/worktree create feature-x /tmp/feature-x
+```
+
+**Behavior:**
+- If branch exists locally: Creates worktree for that branch
+- If branch exists on origin: Creates local branch tracking origin
+- If branch doesn't exist: Creates new branch
+
+### Remove Worktree
+
+```
+/worktree remove feature-branch
+/wt rm bugfix-123
+```
+
+**Safety:**
+- Checks for uncommitted changes
+- Prompts for confirmation if changes exist
+- Use force flag in tool for bypassing
+
+### Prune Worktrees
+
+```
+/worktree prune
+```
+
+Cleans up stale worktree administrative files.
+
+## Tool Usage (for AI)
+
+The extension registers a `git-worktree` tool that AI agents can use:
+
+```typescript
+{
+ name: "git-worktree",
+ action: "list" | "create" | "remove",
+ branch: "branch-name", // For create/remove
+ path: "/custom/path", // Optional for create
+ force: true // Optional for remove
+}
+```
+
+**Examples:**
+
+List worktrees:
+```json
+{
+ "action": "list"
+}
+```
+
+Create worktree:
+```json
+{
+ "action": "create",
+ "branch": "feature-x"
+}
+```
+
+Remove worktree:
+```json
+{
+ "action": "remove",
+ "branch": "feature-x",
+ "force": false
+}
+```
+
+## Worktree Directory Structure
+
+Worktrees are created in `.worktrees/` subdirectory:
+
+```
+~/src/home/
+โโโ .git/
+โโโ .worktrees/
+โ โโโ feature-a/ # Worktree for feature-a branch
+โ โโโ bugfix-123/ # Worktree for bugfix-123 branch
+โ โโโ experiment/ # Worktree for experiment branch
+โโโ systems/
+โโโ home/
+โโโ ...
+```
+
+**Why `.worktrees/`?**
+- Keeps worktrees organized in one place
+- Easy to gitignore (add `.worktrees/` to `.gitignore`)
+- Clear separation from main repository
+- All worktrees easy to find and manage
+
+## Best Practices
+
+### When to Use Worktrees
+
+**Good for:**
+- Working on multiple features simultaneously
+- Keeping long-running branches checked out
+- Testing changes without switching branches
+- Code review without disrupting current work
+
+**Not good for:**
+- Quick branch switches (just use `git switch`)
+- Temporary experiments (use `git stash` instead)
+
+### Worktree Workflow
+
+1. **Create worktree for feature:**
+ ```
+ /wt new feature-x
+ cd .worktrees/feature-x
+ ```
+
+2. **Work in worktree:**
+ ```
+ # Make changes, commit, push
+ git add .
+ git commit -m "feat: add feature x"
+ git push origin feature-x:feature-x
+ ```
+
+3. **Switch back to main:**
+ ```
+ cd ~/src/home
+ # Main worktree is unaffected
+ ```
+
+4. **Clean up when done:**
+ ```
+ /wt rm feature-x
+ ```
+
+### Multiple Worktrees Pattern
+
+Working on frontend and backend simultaneously:
+
+```
+/wt new frontend-feature
+/wt new backend-feature
+
+# Terminal 1
+cd .worktrees/frontend-feature
+# Work on frontend
+
+# Terminal 2
+cd .worktrees/backend-feature
+# Work on backend
+
+# Main terminal
+cd ~/src/home
+# Work on main branch or review
+```
+
+## Configuration
+
+No configuration needed for Phase 1. Future phases will add configuration options in `.pi/config.json`.
+
+## Troubleshooting
+
+### Error: "Not in a git repository"
+
+Make sure you're running commands from within a git repository.
+
+### Error: "Worktree path already exists"
+
+The worktree directory already exists. Either:
+- Remove it manually: `rm -rf .worktrees/branch-name`
+- Use a different path: `/wt new branch-name /tmp/branch-name`
+
+### Error: "Worktree has uncommitted changes"
+
+The worktree has uncommitted changes. Either:
+- Commit the changes
+- Use force removal (will be prompted)
+- Manually handle: `cd .worktrees/branch-name && git stash`
+
+### Stale Worktree References
+
+If you manually deleted a worktree directory, git may still have references:
+
+```
+/worktree prune
+```
+
+## Technical Details
+
+### Implementation
+
+- **Language:** TypeScript
+- **Location:** `dots/pi/agent/extensions/git/`
+- **Modules:**
+ - `index.ts` - Main extension entry, command/tool registration
+ - `worktree.ts` - Worktree management logic
+ - `utils.ts` - Git utility functions
+ - `types.ts` - TypeScript type definitions
+
+### Dependencies
+
+- Git >= 2.30 (for improved worktree support)
+- Node.js/Bun for TypeScript execution
+
+### Error Handling
+
+All operations include:
+- Input validation
+- Git repository detection
+- Uncommitted changes detection
+- Clear error messages
+- Safe defaults
+
+## Roadmap
+
+### Phase 2: Smart Rebase/Fixup (Next)
+- `/fixup <commit>` - Create fixup commit
+- `/rebase-fixups` - Auto-squash fixups
+- Pre-push fixup detection
+
+### Phase 3: Commit Helpers
+- `/commit` - Interactive conventional commit
+- Commit message validation
+- AI-assisted commit messages
+
+### Phase 4: Branch Management
+- `/branch list` - List branches with info
+- `/branch cleanup` - Remove merged branches
+
+### Phase 5: Polish
+- Comprehensive error handling
+- Configuration system
+- Full documentation
+
+## Related
+
+- Plan: `~/.local/share/ai/plans/pi-git-extension.md`
+- Research: `~/.local/share/ai/research/2026-02/2026-02-06-git-rebase-fixup-best-practices.md`
+
+## License
+
+Part of vdemeester/home repository.
dots/pi/agent/extensions/git/types.ts
@@ -0,0 +1,37 @@
+// =============================================================================
+// Git Extension Types
+// =============================================================================
+
+export interface WorktreeInfo {
+ path: string;
+ branch: string;
+ commit: string;
+ bare: boolean;
+ detached: boolean;
+ locked: boolean;
+ prunable: boolean;
+}
+
+export interface GitStatus {
+ branch: string;
+ commit: string;
+ dirty: boolean;
+ staged: number;
+ unstaged: number;
+ untracked: number;
+ ahead: number;
+ behind: number;
+}
+
+export interface WorktreeCreateOptions {
+ branch: string;
+ path?: string;
+ createBranch?: boolean;
+ fromRemote?: boolean;
+ checkout?: boolean;
+}
+
+export interface WorktreeRemoveOptions {
+ branch: string;
+ force?: boolean;
+}
dots/pi/agent/extensions/git/utils.ts
@@ -0,0 +1,106 @@
+// =============================================================================
+// Git Utilities
+// =============================================================================
+
+import { execSync } from "child_process";
+import * as path from "path";
+import * as fs from "fs";
+
+export function findRepoRoot(cwd: string = process.cwd()): string | null {
+ try {
+ return execSync("git rev-parse --show-toplevel", {
+ cwd,
+ encoding: "utf-8",
+ }).trim();
+ } catch {
+ return null;
+ }
+}
+
+export function getCurrentBranch(cwd: string = process.cwd()): string | null {
+ try {
+ return execSync("git rev-parse --abbrev-ref HEAD", {
+ cwd,
+ encoding: "utf-8",
+ }).trim();
+ } catch {
+ return null;
+ }
+}
+
+export function getCommitHash(cwd: string = process.cwd()): string | null {
+ try {
+ return execSync("git rev-parse HEAD", {
+ cwd,
+ encoding: "utf-8",
+ }).trim();
+ } catch {
+ return null;
+ }
+}
+
+export function isWorktree(cwd: string = process.cwd()): boolean {
+ try {
+ const gitDir = execSync("git rev-parse --git-dir", {
+ cwd,
+ encoding: "utf-8",
+ }).trim();
+ return gitDir.includes("/worktrees/");
+ } catch {
+ return false;
+ }
+}
+
+export function hasUncommittedChanges(cwd: string = process.cwd()): boolean {
+ try {
+ const status = execSync("git status --porcelain", {
+ cwd,
+ encoding: "utf-8",
+ }).trim();
+ return status.length > 0;
+ } catch {
+ return false;
+ }
+}
+
+export function sanitizeBranchName(branch: string): string {
+ // Remove remote prefix if present (origin/feature -> feature)
+ const withoutRemote = branch.replace(/^[^/]+\//, "");
+ // Replace invalid characters with hyphens
+ return withoutRemote.replace(/[^a-zA-Z0-9-_]/g, "-");
+}
+
+export function branchExists(branch: string, cwd: string = process.cwd()): boolean {
+ try {
+ execSync(`git show-ref --verify refs/heads/${branch}`, {
+ cwd,
+ encoding: "utf-8",
+ });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function remoteBranchExists(remote: string, branch: string, cwd: string = process.cwd()): boolean {
+ try {
+ execSync(`git show-ref --verify refs/remotes/${remote}/${branch}`, {
+ cwd,
+ encoding: "utf-8",
+ });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+export function getWorktreeDir(repoRoot: string): string {
+ return path.join(repoRoot, ".worktrees");
+}
+
+export function ensureWorktreeDir(repoRoot: string): void {
+ const worktreeDir = getWorktreeDir(repoRoot);
+ if (!fs.existsSync(worktreeDir)) {
+ fs.mkdirSync(worktreeDir, { recursive: true });
+ }
+}
dots/pi/agent/extensions/git/worktree.ts
@@ -0,0 +1,234 @@
+// =============================================================================
+// Git Worktree Management
+// =============================================================================
+
+import { execSync } from "child_process";
+import * as path from "path";
+import * as fs from "fs";
+import type { WorktreeInfo, WorktreeCreateOptions, WorktreeRemoveOptions } from "./types.js";
+import {
+ findRepoRoot,
+ getCurrentBranch,
+ getCommitHash,
+ sanitizeBranchName,
+ branchExists,
+ remoteBranchExists,
+ hasUncommittedChanges,
+ getWorktreeDir,
+ ensureWorktreeDir,
+} from "./utils.js";
+
+// =============================================================================
+// List Worktrees
+// =============================================================================
+
+export function listWorktrees(cwd: string = process.cwd()): WorktreeInfo[] {
+ const repoRoot = findRepoRoot(cwd);
+ if (!repoRoot) {
+ throw new Error("Not in a git repository");
+ }
+
+ try {
+ const output = execSync("git worktree list --porcelain", {
+ cwd: repoRoot,
+ encoding: "utf-8",
+ });
+
+ return parseWorktreeList(output);
+ } catch (error) {
+ throw new Error(`Failed to list worktrees: ${error}`);
+ }
+}
+
+function parseWorktreeList(output: string): WorktreeInfo[] {
+ const worktrees: WorktreeInfo[] = [];
+ const lines = output.trim().split("\n");
+
+ let current: Partial<WorktreeInfo> = {};
+
+ for (const line of lines) {
+ if (line === "") {
+ if (current.path) {
+ worktrees.push(current as WorktreeInfo);
+ current = {};
+ }
+ continue;
+ }
+
+ const [key, ...valueParts] = line.split(" ");
+ const value = valueParts.join(" ");
+
+ switch (key) {
+ case "worktree":
+ current.path = value;
+ break;
+ case "HEAD":
+ current.commit = value;
+ break;
+ case "branch":
+ // Remove refs/heads/ prefix
+ current.branch = value.replace("refs/heads/", "");
+ break;
+ case "bare":
+ current.bare = true;
+ break;
+ case "detached":
+ current.detached = true;
+ break;
+ case "locked":
+ current.locked = true;
+ break;
+ case "prunable":
+ current.prunable = true;
+ break;
+ }
+ }
+
+ // Add last worktree if exists
+ if (current.path) {
+ worktrees.push(current as WorktreeInfo);
+ }
+
+ // Set defaults
+ return worktrees.map((wt) => ({
+ ...wt,
+ bare: wt.bare ?? false,
+ detached: wt.detached ?? false,
+ locked: wt.locked ?? false,
+ prunable: wt.prunable ?? false,
+ branch: wt.branch ?? "(detached)",
+ }));
+}
+
+// =============================================================================
+// Create Worktree
+// =============================================================================
+
+export function createWorktree(options: WorktreeCreateOptions, cwd: string = process.cwd()): string {
+ const repoRoot = findRepoRoot(cwd);
+ if (!repoRoot) {
+ throw new Error("Not in a git repository");
+ }
+
+ // Ensure .worktrees directory exists
+ ensureWorktreeDir(repoRoot);
+
+ // Determine worktree path
+ const worktreePath =
+ options.path ?? path.join(getWorktreeDir(repoRoot), sanitizeBranchName(options.branch));
+
+ // Check if path already exists
+ if (fs.existsSync(worktreePath)) {
+ throw new Error(`Worktree path already exists: ${worktreePath}`);
+ }
+
+ // Build command
+ let cmd = `git worktree add`;
+
+ // Check if we should create a new branch
+ const localExists = branchExists(options.branch, repoRoot);
+ const remoteExists = remoteBranchExists("origin", options.branch, repoRoot);
+
+ if (options.createBranch || (!localExists && !remoteExists)) {
+ // Create new branch
+ cmd += ` -b ${options.branch}`;
+ }
+
+ cmd += ` "${worktreePath}"`;
+
+ if (localExists) {
+ // Use existing local branch
+ cmd += ` ${options.branch}`;
+ } else if (remoteExists && options.fromRemote) {
+ // Create from remote branch
+ cmd += ` origin/${options.branch}`;
+ }
+
+ // Execute command
+ try {
+ execSync(cmd, {
+ cwd: repoRoot,
+ encoding: "utf-8",
+ stdio: "inherit",
+ });
+
+ return worktreePath;
+ } catch (error) {
+ throw new Error(`Failed to create worktree: ${error}`);
+ }
+}
+
+// =============================================================================
+// Remove Worktree
+// =============================================================================
+
+export function removeWorktree(options: WorktreeRemoveOptions, cwd: string = process.cwd()): void {
+ const repoRoot = findRepoRoot(cwd);
+ if (!repoRoot) {
+ throw new Error("Not in a git repository");
+ }
+
+ // Find worktree by branch name
+ const worktrees = listWorktrees(repoRoot);
+ const worktree = worktrees.find((wt) => wt.branch === options.branch);
+
+ if (!worktree) {
+ throw new Error(`Worktree for branch '${options.branch}' not found`);
+ }
+
+ // Check for uncommitted changes (unless force)
+ if (!options.force && hasUncommittedChanges(worktree.path)) {
+ throw new Error(
+ `Worktree has uncommitted changes. Use force option to remove anyway.`
+ );
+ }
+
+ // Build command
+ let cmd = `git worktree remove "${worktree.path}"`;
+ if (options.force) {
+ cmd += " --force";
+ }
+
+ // Execute command
+ try {
+ execSync(cmd, {
+ cwd: repoRoot,
+ encoding: "utf-8",
+ stdio: "inherit",
+ });
+ } catch (error) {
+ throw new Error(`Failed to remove worktree: ${error}`);
+ }
+}
+
+// =============================================================================
+// Prune Worktrees
+// =============================================================================
+
+export function pruneWorktrees(cwd: string = process.cwd()): void {
+ const repoRoot = findRepoRoot(cwd);
+ if (!repoRoot) {
+ throw new Error("Not in a git repository");
+ }
+
+ try {
+ execSync("git worktree prune", {
+ cwd: repoRoot,
+ encoding: "utf-8",
+ stdio: "inherit",
+ });
+ } catch (error) {
+ throw new Error(`Failed to prune worktrees: ${error}`);
+ }
+}
+
+// =============================================================================
+// Get Current Worktree Info
+// =============================================================================
+
+export function getCurrentWorktreeInfo(cwd: string = process.cwd()): WorktreeInfo | null {
+ const worktrees = listWorktrees(cwd);
+ const currentPath = path.resolve(cwd);
+
+ return worktrees.find((wt) => path.resolve(wt.path) === currentPath) ?? null;
+}