pi-worktrees/.pi/extensions/pi-worktree-remove.ts
David Kong 8761c3af5f feat: implement git_worktree_remove tool (Issue #5) (#18)
## Summary
Implement `git_worktree_remove` tool with proper validation and error handling.

## Changes
- Add `pi-worktree-remove.ts` — pure validation functions for remove operation
  - `validateRemoveWorktreeInput()` — validates path is non-empty
  - `isMainRepository()` — refuses removal of main repo root (suggests `git worktree prune`)
  - `hasUncommittedChanges()` — checks git status --porcelain output
  - `buildWorktreeRemoveCommand()` — builds `git worktree remove` args with optional `--force`
- Add `pi-worktree-remove.test.ts` — 17 unit tests covering all validation paths
- Wire up `createRemoveWorktreeTool()` in main extension with full validation pipeline

## Testing
- [x] All 72 tests pass (55 existing + 17 new)
- [x] No regressions in existing test suite
- [ ] Manual verification — remove a worktree, try removing main repo, try removing worktree with changes

## Checklist
- [x] Self-reviewed the diff
- [x] Code follows project conventions (pure functions, fail-fast validation)
- [x] Descriptive naming (no single-character variables)

Co-authored-by: David Kong <davkon@gmail.com>
Reviewed-on: #18
2026-07-25 02:21:03 +00:00

101 lines
3.5 KiB
TypeScript

/**
* Git worktree remove — pure validation and command-building functions.
* No pi SDK dependencies for testability.
*/
import path from "path";
import type { WorktreeError } from "./pi-worktree-create";
// ─── Type Definitions ──────────────────────────────────────────────
// WorktreeError is imported from pi-worktree-create (shared error shape)
/** 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 WorktreeError describing the problem
*/
export function validateRemoveWorktreeInput(
worktreePath: string,
): WorktreeError | 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
* @param baseDir - Base directory to resolve relative paths against (defaults to process.cwd())
* @returns true if target is the main repo (removal should be refused)
*/
export function isMainRepository(
targetPath: string,
mainRepoRoot: string,
baseDir: string = process.cwd(),
): boolean {
return path.resolve(baseDir, 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;
}