/** * Git worktree create — pure validation and command-building functions. * No pi SDK dependencies for testability. */ // ─── Type Definitions ────────────────────────────────────────────── /** Error returned when a worktree operation fails. */ export interface WorktreeError { /** Human-readable error message from git or validation. */ message: string; /** Suggested fix for the user. */ suggestion?: string; } /** Parameters for creating a new worktree. */ export interface CreateWorktreeParams { /** Path for the new worktree (relative or absolute). */ path: string; /** Branch name to check out. */ branch: string; /** Whether to create the branch if it doesn't exist. Defaults to true. */ createBranch?: boolean; } // ─── Validation ──────────────────────────────────────────────────── /** * Validate input parameters for creating a worktree. * * Checks that path and branch name are non-empty and well-formed. * Branch names follow git conventions: no spaces, no leading/trailing whitespace, * no special characters like ~, ^, . at start or ?, *, [, ], ", ' in the middle (except / for nesting). * * @param worktreePath - Path for the new worktree * @param branchName - Branch name to check out * @returns null if valid, or a WorktreeError describing the problem */ export function validateCreateWorktreeInput( worktreePath: string, branchName: string, ): WorktreeError | null { // Validate path is non-empty if (!worktreePath || !worktreePath.trim()) { return { message: "Path cannot be empty.", suggestion: "Provide a valid path for the new worktree (e.g., '../worktrees/feature-x').", }; } // Validate branch name is non-empty if (!branchName || !branchName.trim()) { return { message: "Branch name cannot be empty.", suggestion: "Provide a valid branch name (e.g., 'feature-x' or 'bugfix/issue-42').", }; } // Validate branch name doesn't contain spaces if (branchName.includes(" ")) { return { message: `Branch name '${branchName}' contains invalid characters (spaces).`, suggestion: "Use hyphens or underscores instead of spaces (e.g., 'feature-x' not 'my feature').", }; } // Validate branch name doesn't start/end with special chars if (/^[\/\-~^\.]/.test(branchName) || /[\/\-\.\s]$/.test(branchName)) { return { message: `Branch name '${branchName}' starts or ends with an invalid character.`, suggestion: "Branch names should start with a letter and not end with '/', '-', '.', or whitespace.", }; } // Validate branch name doesn't contain dangerous characters if (/[?*\[\]"'\\]/.test(branchName)) { return { message: `Branch name '${branchName}' contains invalid characters.`, suggestion: "Avoid characters like ?, *, [, ], \", ' in branch names.", }; } return null; } // ─── Path Validation ────────────────────────────────────────────── /** * Check if the target path already exists and return an error if so. * * @param worktreePath - Path to check * @param checkExists - Function that checks if a path exists (injected for testability) * @returns null if path is free, or a WorktreeError with the exact message format */ export function validatePathDoesNotExist( worktreePath: string, checkExists: (path: string) => boolean, ): WorktreeError | null { if (checkExists(worktreePath)) { return { message: `Path '${worktreePath}' already exists.`, suggestion: "Choose a different location.", }; } return null; } // ─── Branch Validation ────────────────────────────────────────────── /** * Check if the target branch is the same as the current branch. * * @param targetBranch - The branch we want to create the worktree on * @param currentBranch - The currently checked out branch * @returns true if they match (worktree creation should be refused) */ export function isCurrentBranch(targetBranch: string, currentBranch: string): boolean { return targetBranch === currentBranch; } // ─── Command Building ────────────────────────────────────────────── /** * Build the `git worktree add` command arguments. * * Uses `-b` flag when createBranch is true to create a new branch. * Without `-b`, checks out an existing branch. * * @param params - Worktree creation parameters * @returns Array of command arguments (excluding 'git' itself) */ export function buildWorktreeAddCommand(params: CreateWorktreeParams): string[] { const { path: worktreePath, branch, createBranch = true } = params; const args = ["worktree", "add"]; if (createBranch) { args.push("-b", branch); } else { args.push(branch); } args.push(worktreePath); return args; }