pi-worktrees/.pi/extensions/pi-worktree-remove.ts
David Kong 774e6005a3 feat: implement git_worktree_remove tool (Issue #5)
Add validation and error handling for removing linked worktrees:
- validateRemoveWorktreeInput() — validates path is non-empty
- isMainRepository() — refuses removal of main repo root
- hasUncommittedChanges() — checks git status --porcelain output
- buildWorktreeRemoveCommand() — builds git worktree remove args

Wire up createRemoveWorktreeTool with full validation pipeline:
1. Validate input parameters (path)
2. Check if target is the main repo — refuse with prune suggestion
3. Check for uncommitted changes — refuse with stash/commit message
4. Run git worktree remove <path>
5. Surface git errors with helpful suggestions
2026-07-25 07:23:30 +10:00

101 lines
3.4 KiB
TypeScript

/**
* Git worktree remove — pure validation and command-building functions.
* No pi SDK dependencies for testability.
*/
import path from "path";
// ─── Type Definitions ──────────────────────────────────────────────
/** Error returned when a worktree operation fails. */
export interface WorktreeRemoveError {
/** Human-readable error message. */
message: string;
/** Suggested fix for the user. */
suggestion?: string;
}
/** Options for removing a worktree. */
export interface RemoveWorktreeOptions {
/** Force removal even if the worktree has uncommitted changes. Defaults to false. */
force?: boolean;
}
// ─── Input Validation ──────────────────────────────────────────────
/**
* Validate input parameters for removing a worktree.
*
* Checks that path is non-empty and well-formed.
*
* @param worktreePath - Path of the worktree to remove
* @returns null if valid, or a WorktreeRemoveError describing the problem
*/
export function validateRemoveWorktreeInput(
worktreePath: string,
): WorktreeRemoveError | null {
if (!worktreePath || !worktreePath.trim()) {
return {
message: "Path cannot be empty.",
suggestion: "Provide a valid worktree path (e.g., '../worktrees/feature-x').",
};
}
return null;
}
// ─── Main Repository Check ────────────────────────────────────────
/**
* Check if the target path is the main repository root.
*
* Compares resolved paths to handle trailing slashes and relative paths.
* Removing the main repo is not allowed — use `git worktree prune` instead.
*
* @param targetPath - Path of the worktree to remove
* @param mainRepoRoot - Root path of the main repository
* @returns true if target is the main repo (removal should be refused)
*/
export function isMainRepository(targetPath: string, mainRepoRoot: string): boolean {
return path.resolve(targetPath) === path.resolve(mainRepoRoot);
}
// ─── Uncommitted Changes Check ──────────────────────────────────────
/**
* Check if the worktree has uncommitted changes based on `git status --porcelain` output.
*
* Any non-empty porcelain output indicates changes (staged, unstaged, or untracked).
*
* @param statusOutput - Raw stdout from `git status --porcelain` in the target worktree
* @returns true if there are uncommitted changes
*/
export function hasUncommittedChanges(statusOutput: string): boolean {
return statusOutput.trim().length > 0;
}
// ─── Command Building ──────────────────────────────────────────────
/**
* Build the `git worktree remove` command arguments.
*
* Uses `--force` flag when force option is true to bypass uncommitted changes check.
*
* @param worktreePath - Path of the worktree to remove
* @param options - Optional removal configuration
* @returns Array of command arguments (excluding 'git' itself)
*/
export function buildWorktreeRemoveCommand(
worktreePath: string,
options?: RemoveWorktreeOptions,
): string[] {
const args = ["worktree", "remove"];
if (options?.force) {
args.push("--force");
}
args.push(worktreePath);
return args;
}