diff --git a/plugins/developer/skills/worktree/SKILL.md b/plugins/developer/skills/worktree/SKILL.md new file mode 100644 index 0000000..dc61165 --- /dev/null +++ b/plugins/developer/skills/worktree/SKILL.md @@ -0,0 +1,87 @@ +--- +name: worktree +description: Create and manage git worktrees for parallel development. Use when user asks to create a worktree, work in parallel on multiple branches, set up isolated development environments, or clean up stale worktrees. Triggers on phrases like "create worktree", "new worktree", "worktree for branch", "/worktree", "clean worktrees", "stale worktrees". +--- + +# Git Worktree Management + +Create worktrees in `/worktrees//`. + +`SKILL_DIR` = directory containing this SKILL.md + +## Creating a Worktree + +```bash +"$SKILL_DIR/scripts/create-worktree.sh" [base-branch] +``` + +- Creates worktree for existing branch, or auto-creates from `origin/main` if branch doesn't exist +- Runs `bin/worktree-init.sh` if present (see below) + +## Custom Initialization + +Create `bin/worktree-init.sh` in your repo to customize worktree setup: + +```bash +#!/usr/bin/env bash +# bin/worktree-init.sh - runs in new worktree, $1 = source repo root +SOURCE_REPO="$1" + +# Example: copy env files +cp "$SOURCE_REPO/.env" ./.env + +# Example: install dependencies +yarn install +``` + +The script runs inside the new worktree directory with `$1` set to the source repo root. + +## After Creating a Worktree + +Always check for stale worktrees and offer to clean them up: + +```bash +"$SKILL_DIR/scripts/detect-stale-worktrees.sh" +``` + +If stale worktrees are found, ask user if they want to remove them. For each one they confirm: + +```bash +"$SKILL_DIR/scripts/purge-worktree.sh" "" +``` + +## Scripts + +### create-worktree.sh + +```bash +"$SKILL_DIR/scripts/create-worktree.sh" feature/my-branch # Existing or new branch from origin/main +"$SKILL_DIR/scripts/create-worktree.sh" feature/new-feature develop # New branch from specific base +``` + +### detect-stale-worktrees.sh + +Finds worktrees whose branches no longer exist on remote (merged/deleted PRs). + +```bash +"$SKILL_DIR/scripts/detect-stale-worktrees.sh" # Human-readable output +"$SKILL_DIR/scripts/detect-stale-worktrees.sh" --quiet # Just paths (for scripting) +``` + +Exit code 0 = stale worktrees found, 1 = none found. + +### purge-worktree.sh + +Removes worktree and deletes local branch. + +```bash +"$SKILL_DIR/scripts/purge-worktree.sh" "/path/to/worktree" +"$SKILL_DIR/scripts/purge-worktree.sh" "/path/to/worktree" --keep-branch # Keep local branch +``` + +## Other Commands + +```bash +git worktree list # List all worktrees +git worktree prune # Clean up stale worktree refs (different from stale branches) +``` diff --git a/plugins/developer/skills/worktree/scripts/create-worktree.sh b/plugins/developer/skills/worktree/scripts/create-worktree.sh new file mode 100755 index 0000000..b56236b --- /dev/null +++ b/plugins/developer/skills/worktree/scripts/create-worktree.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Configuration - set these before running +BRANCH_NAME="${1:?Usage: $0 [base-branch]}" +BASE_BRANCH="${2:-}" # Optional: base branch for creating new branches + +# Compute paths +REPO_ROOT=$(git rev-parse --show-toplevel) +REPO_NAME=$(basename "$REPO_ROOT") +REPO_PARENT=$(dirname "$REPO_ROOT") +WORKTREE_BASE="$REPO_PARENT/worktrees/$REPO_NAME" +WORKTREE_PATH="$WORKTREE_BASE/$BRANCH_NAME" + +# Check if worktree already exists +if [ -d "$WORKTREE_PATH" ]; then + echo "Error: Worktree already exists at $WORKTREE_PATH" + exit 1 +fi + +# Create worktree base directory +mkdir -p "$WORKTREE_BASE" + +# Check if branch exists (locally or remotely) +branch_exists() { + git show-ref --verify --quiet "refs/heads/$1" 2>/dev/null || \ + git show-ref --verify --quiet "refs/remotes/origin/$1" 2>/dev/null +} + +# Create the worktree +if [ -n "$BASE_BRANCH" ]; then + echo "Creating new branch '$BRANCH_NAME' from '$BASE_BRANCH'..." + git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" "$BASE_BRANCH" +elif branch_exists "$BRANCH_NAME"; then + echo "Creating worktree for existing branch '$BRANCH_NAME'..." + git worktree add "$WORKTREE_PATH" "$BRANCH_NAME" +else + echo "Branch '$BRANCH_NAME' not found. Fetching origin/main and creating new branch..." + git fetch origin main + git worktree add -b "$BRANCH_NAME" "$WORKTREE_PATH" origin/main +fi + +# Run repo-specific initialization if available +INIT_SCRIPT="$REPO_ROOT/bin/worktree-init.sh" +if [ -x "$INIT_SCRIPT" ]; then + echo "Running worktree initialization script..." + cd "$WORKTREE_PATH" && "$INIT_SCRIPT" "$REPO_ROOT" +else + echo "" + echo "Tip: Create bin/worktree-init.sh (chmod +x) to customize worktree setup." + echo " It runs in the new worktree with \$1=source_repo_root" +fi + +echo "" +echo "Worktree created successfully at: $WORKTREE_PATH" +echo "To start working: cd $WORKTREE_PATH" diff --git a/plugins/developer/skills/worktree/scripts/detect-stale-worktrees.sh b/plugins/developer/skills/worktree/scripts/detect-stale-worktrees.sh new file mode 100755 index 0000000..329a5da --- /dev/null +++ b/plugins/developer/skills/worktree/scripts/detect-stale-worktrees.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Detect worktrees without corresponding remote branches +# Usage: ./detect-stale-worktrees.sh [--quiet] +# +# Returns exit code 0 if stale worktrees found, 1 if none found +# With --quiet, only outputs paths of stale worktrees (for scripting) + +QUIET="${1:-}" +REPO_ROOT=$(git rev-parse --show-toplevel) + +# Fetch latest remote refs +git fetch --prune origin 2>/dev/null || true + +stale_count=0 +stale_worktrees=() + +# Parse worktree list +while IFS= read -r line; do + # Extract path and branch info + path=$(echo "$line" | awk '{print $1}') + branch_info=$(echo "$line" | grep -oP '\[.*\]' || echo "") + + # Skip if no branch (detached HEAD) + if [[ -z "$branch_info" || "$branch_info" == *"detached"* ]]; then + continue + fi + + # Extract branch name from [branch] + branch=$(echo "$branch_info" | sed 's/\[\(.*\)\]/\1/') + + # Skip main repo directory + if [[ "$path" == "$REPO_ROOT" ]]; then + continue + fi + + # Check if remote branch exists + if ! git ls-remote --exit-code --heads origin "$branch" >/dev/null 2>&1; then + stale_worktrees+=("$path|$branch") + stale_count=$((stale_count + 1)) + fi +done < <(git worktree list) + +# Output results +if [[ $stale_count -eq 0 ]]; then + [[ "$QUIET" != "--quiet" ]] && echo "No stale worktrees found." + exit 1 +fi + +if [[ "$QUIET" == "--quiet" ]]; then + for entry in "${stale_worktrees[@]}"; do + echo "${entry%%|*}" + done +else + echo "Found $stale_count stale worktree(s) without remote branches:" + echo "" + for entry in "${stale_worktrees[@]}"; do + path="${entry%%|*}" + branch="${entry##*|}" + echo " Path: $path" + echo " Branch: $branch" + echo "" + done +fi + +exit 0 diff --git a/plugins/developer/skills/worktree/scripts/purge-worktree.sh b/plugins/developer/skills/worktree/scripts/purge-worktree.sh new file mode 100755 index 0000000..2bb4063 --- /dev/null +++ b/plugins/developer/skills/worktree/scripts/purge-worktree.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Purge a worktree and optionally its local branch +# Usage: ./purge-worktree.sh [--keep-branch] +# +# Options: +# --keep-branch Keep the local branch after removing the worktree + +WORKTREE_PATH="${1:?Usage: $0 [--keep-branch]}" +KEEP_BRANCH="${2:-}" + +# Validate worktree exists +if ! git worktree list | grep -q "^$WORKTREE_PATH "; then + echo "Error: '$WORKTREE_PATH' is not a valid worktree" + exit 1 +fi + +# Get branch name from worktree +BRANCH_INFO=$(git worktree list | grep "^$WORKTREE_PATH " | grep -oP '\[.*\]' || echo "") +if [[ -z "$BRANCH_INFO" || "$BRANCH_INFO" == *"detached"* ]]; then + BRANCH="" +else + BRANCH=$(echo "$BRANCH_INFO" | sed 's/\[\(.*\)\]/\1/') +fi + +echo "Removing worktree: $WORKTREE_PATH" +if [[ -n "$BRANCH" ]]; then + echo "Associated branch: $BRANCH" +fi + +# Remove the worktree +git worktree remove "$WORKTREE_PATH" --force + +# Delete the local branch if requested and it exists +if [[ -n "$BRANCH" && "$KEEP_BRANCH" != "--keep-branch" ]]; then + if git show-ref --verify --quiet "refs/heads/$BRANCH" 2>/dev/null; then + echo "Deleting local branch: $BRANCH" + git branch -D "$BRANCH" + fi +fi + +echo "" +echo "Worktree purged successfully."