diff --git a/README.md b/README.md index 7fdb921..ac54e39 100644 --- a/README.md +++ b/README.md @@ -137,17 +137,40 @@ wt remove [pathOrBranch] [options] **Interactive Selection**: Run `wt remove` without arguments to select a worktree to remove. +**Cleanup Scripts**: If `cleanup-worktree` is defined in `worktrees.json`, those commands will be executed before removal (with user confirmation). + Options: - `-f, --force`: Force removal without confirmation +- `-s, --skip-cleanup`: Skip cleanup scripts defined in worktrees.json +- `-t, --trust`: Trust and run cleanup commands without confirmation (for CI environments) Example: ```bash -wt remove # Interactive selection -wt remove feature/login # Remove by branch name -wt remove ./path/to/worktree # Remove by path -wt remove feature/old -f # Force remove +wt remove # Interactive selection +wt remove feature/login # Remove by branch name +wt remove ./path/to/worktree # Remove by path +wt remove feature/old -f # Force remove +wt remove feature/old -s # Skip cleanup scripts +wt remove feature/old -t # Run cleanup without confirmation ``` +### Cleanup Worktree Configuration + +You can define cleanup commands that run automatically when removing a worktree: + +```json +{ + "setup-worktree": ["npm install"], + "cleanup-worktree": ["docker-compose down", "echo 'Cleanup complete'"] +} +``` + +Cleanup behavior: +- **Automatic execution**: Runs before `wt remove` (after confirmation) +- **Skip option**: Use `--skip-cleanup` or `-s` to bypass +- **Trust mode**: Use `--trust` to run without confirmation (CI environments) +- **Environment variables**: `$ROOT_WORKTREE_PATH` is available + ### Purge multiple worktrees ```bash diff --git a/build/commands/remove.js b/build/commands/remove.js index b6f0142..7ce2da6 100644 --- a/build/commands/remove.js +++ b/build/commands/remove.js @@ -4,6 +4,7 @@ import { stat, rm } from "node:fs/promises"; import { findWorktreeByBranch, findWorktreeByPath } from "../utils/git.js"; import { selectWorktree, confirm } from "../utils/tui.js"; import { withSpinner } from "../utils/spinner.js"; +import { runCleanupScriptsSecure } from "../utils/setup.js"; export async function removeWorktreeHandler(pathOrBranch = "", options) { try { await execa("git", ["rev-parse", "--is-inside-work-tree"]); @@ -70,6 +71,10 @@ export async function removeWorktreeHandler(pathOrBranch = "", options) { process.exit(0); } } + // Execute cleanup scripts if not skipped + if (!options.skipCleanup) { + await runCleanupScriptsSecure(targetPath, { trust: options.trust }); + } // Remove the worktree try { await withSpinner(`Removing worktree: ${targetPath}`, async () => { diff --git a/build/index.js b/build/index.js index 587bff7..bb4da95 100755 --- a/build/index.js +++ b/build/index.js @@ -44,7 +44,9 @@ program .alias("rm") .argument("[pathOrBranch]", "Path of the worktree or branch to remove.") .option("-f, --force", "Force removal of worktree and deletion of the folder", false) - .description("Remove a specified worktree. Cleans up the .git/worktrees references.") + .option("-s, --skip-cleanup", "Skip cleanup scripts defined in worktrees.json", false) + .option("-t, --trust", "Trust and run cleanup commands without confirmation (for CI environments)", false) + .description("Remove a specified worktree. Runs cleanup-worktree scripts from worktrees.json before removal.") .action(removeWorktreeHandler); program .command("merge") diff --git a/build/utils/setup.js b/build/utils/setup.js index ea63f7b..ae92821 100644 --- a/build/utils/setup.js +++ b/build/utils/setup.js @@ -195,3 +195,78 @@ export async function runSetupScripts(worktreePath) { return false; } } +/** + * Load and parse cleanup commands from worktrees.json + */ +async function loadCleanupCommands(repoRoot) { + const paths = [ + join(repoRoot, ".cursor", "worktrees.json"), + join(repoRoot, "worktrees.json"), + ]; + for (const configPath of paths) { + try { + await stat(configPath); + const content = await readFile(configPath, "utf-8"); + const data = JSON.parse(content); + if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray(data["cleanup-worktree"])) { + const commands = data["cleanup-worktree"]; + if (commands.length > 0) { + return { commands, filePath: configPath }; + } + } + } + catch { + // Not found, try next + } + } + return null; +} +/** + * Execute cleanup commands before worktree removal (SECURE) + * + * This function loads cleanup-worktree commands from worktrees.json and executes them + * with user confirmation (unless --trust flag is set). + * + * @param worktreePath - Path to the worktree where commands should be executed + * @param options - Execution options (trust flag bypasses confirmation) + * @returns true if cleanup commands were found and executed, false if no cleanup commands exist + */ +export async function runCleanupScriptsSecure(worktreePath, options = {}) { + const repoRoot = await getRepoRoot(); + if (!repoRoot) { + return false; + } + const cleanupResult = await loadCleanupCommands(repoRoot); + if (!cleanupResult) { + return false; + } + console.log(chalk.blue(`Found cleanup config: ${cleanupResult.filePath}`)); + // Show commands and ask for confirmation (unless --trust flag is set) + const shouldRun = await confirmCommands(cleanupResult.commands, { + title: "The following cleanup commands will be executed:", + trust: options.trust, + }); + if (!shouldRun) { + console.log(chalk.yellow("Cleanup commands skipped.")); + return false; + } + // Execute commands + const env = { ...process.env, ROOT_WORKTREE_PATH: repoRoot }; + for (const command of cleanupResult.commands) { + console.log(chalk.gray(`Executing: ${command}`)); + try { + await execa(command, { shell: true, cwd: worktreePath, env, stdio: "inherit" }); + } + catch (cmdError) { + if (cmdError instanceof Error) { + console.error(chalk.red(`Cleanup command failed: ${command}`), cmdError.message); + } + else { + console.error(chalk.red(`Cleanup command failed: ${command}`), cmdError); + } + // Continue with other commands + } + } + console.log(chalk.green("Cleanup commands completed.")); + return true; +} diff --git a/src/commands/remove.ts b/src/commands/remove.ts index fbcc296..12063bf 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -5,10 +5,11 @@ import { resolve } from "node:path"; import { getWorktrees, findWorktreeByBranch, findWorktreeByPath, WorktreeInfo } from "../utils/git.js"; import { selectWorktree, confirm } from "../utils/tui.js"; import { withSpinner } from "../utils/spinner.js"; +import { runCleanupScriptsSecure } from "../utils/setup.js"; export async function removeWorktreeHandler( pathOrBranch: string = "", - options: { force?: boolean } + options: { force?: boolean; skipCleanup?: boolean; trust?: boolean } ) { try { await execa("git", ["rev-parse", "--is-inside-work-tree"]); @@ -85,6 +86,11 @@ export async function removeWorktreeHandler( } } + // Execute cleanup scripts if not skipped + if (!options.skipCleanup) { + await runCleanupScriptsSecure(targetPath, { trust: options.trust }); + } + // Remove the worktree try { await withSpinner( diff --git a/src/index.ts b/src/index.ts index beaa0e9..1e63c53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -84,8 +84,18 @@ program "Force removal of worktree and deletion of the folder", false ) + .option( + "-s, --skip-cleanup", + "Skip cleanup scripts defined in worktrees.json", + false + ) + .option( + "-t, --trust", + "Trust and run cleanup commands without confirmation (for CI environments)", + false + ) .description( - "Remove a specified worktree. Cleans up the .git/worktrees references." + "Remove a specified worktree. Runs cleanup-worktree scripts from worktrees.json before removal." ) .action(removeWorktreeHandler); diff --git a/src/utils/setup.ts b/src/utils/setup.ts index 31dd7ad..a684cff 100644 --- a/src/utils/setup.ts +++ b/src/utils/setup.ts @@ -9,6 +9,7 @@ import { confirmCommands } from "./tui.js"; interface WorktreeSetupData { "setup-worktree"?: string[]; + "cleanup-worktree"?: string[]; [key: string]: unknown; } @@ -212,3 +213,89 @@ export async function runSetupScripts(worktreePath: string): Promise { return false; } } + +/** + * Load and parse cleanup commands from worktrees.json + */ +async function loadCleanupCommands(repoRoot: string): Promise<{ commands: string[]; filePath: string } | null> { + const paths = [ + join(repoRoot, ".cursor", "worktrees.json"), + join(repoRoot, "worktrees.json"), + ]; + + for (const configPath of paths) { + try { + await stat(configPath); + const content = await readFile(configPath, "utf-8"); + const data = JSON.parse(content) as WorktreeSetupData; + + if (data && typeof data === 'object' && !Array.isArray(data) && Array.isArray(data["cleanup-worktree"])) { + const commands = data["cleanup-worktree"]; + if (commands.length > 0) { + return { commands, filePath: configPath }; + } + } + } catch { + // Not found, try next + } + } + + return null; +} + +/** + * Execute cleanup commands before worktree removal (SECURE) + * + * This function loads cleanup-worktree commands from worktrees.json and executes them + * with user confirmation (unless --trust flag is set). + * + * @param worktreePath - Path to the worktree where commands should be executed + * @param options - Execution options (trust flag bypasses confirmation) + * @returns true if cleanup commands were found and executed, false if no cleanup commands exist + */ +export async function runCleanupScriptsSecure( + worktreePath: string, + options: { trust?: boolean } = {} +): Promise { + const repoRoot = await getRepoRoot(); + if (!repoRoot) { + return false; + } + + const cleanupResult = await loadCleanupCommands(repoRoot); + + if (!cleanupResult) { + return false; + } + + console.log(chalk.blue(`Found cleanup config: ${cleanupResult.filePath}`)); + + // Show commands and ask for confirmation (unless --trust flag is set) + const shouldRun = await confirmCommands(cleanupResult.commands, { + title: "The following cleanup commands will be executed:", + trust: options.trust, + }); + + if (!shouldRun) { + console.log(chalk.yellow("Cleanup commands skipped.")); + return false; + } + + // Execute commands + const env = { ...process.env, ROOT_WORKTREE_PATH: repoRoot }; + for (const command of cleanupResult.commands) { + console.log(chalk.gray(`Executing: ${command}`)); + try { + await execa(command, { shell: true, cwd: worktreePath, env, stdio: "inherit" }); + } catch (cmdError: unknown) { + if (cmdError instanceof Error) { + console.error(chalk.red(`Cleanup command failed: ${command}`), cmdError.message); + } else { + console.error(chalk.red(`Cleanup command failed: ${command}`), cmdError); + } + // Continue with other commands + } + } + console.log(chalk.green("Cleanup commands completed.")); + return true; +}