BREAKING: Plugin renamed from compounding-engineering to compound-engineering. Users will need to reinstall with the new name: claude /plugin install compound-engineering Changes: - Renamed plugin directory and all references - Updated documentation counts (24 agents, 19 commands) - Added julik-frontend-races-reviewer to docs 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
346 lines
8.7 KiB
Bash
Executable File
346 lines
8.7 KiB
Bash
Executable File
#!/bin/bash
|
||
|
||
# Git Worktree Manager
|
||
# Handles creating, listing, switching, and cleaning up Git worktrees
|
||
# KISS principle: Simple, interactive, opinionated
|
||
|
||
set -e
|
||
|
||
# Colors for output
|
||
RED='\033[0;31m'
|
||
GREEN='\033[0;32m'
|
||
YELLOW='\033[1;33m'
|
||
BLUE='\033[0;34m'
|
||
NC='\033[0m' # No Color
|
||
|
||
# Get repo root
|
||
GIT_ROOT=$(git rev-parse --show-toplevel)
|
||
WORKTREE_DIR="$GIT_ROOT/.worktrees"
|
||
|
||
# Ensure .worktrees is in .gitignore
|
||
ensure_gitignore() {
|
||
if ! grep -q "^\.worktrees$" "$GIT_ROOT/.gitignore" 2>/dev/null; then
|
||
echo ".worktrees" >> "$GIT_ROOT/.gitignore"
|
||
fi
|
||
}
|
||
|
||
# Copy .env files from main repo to worktree
|
||
copy_env_files() {
|
||
local worktree_path="$1"
|
||
|
||
echo -e "${BLUE}Copying environment files...${NC}"
|
||
|
||
# Find all .env* files in root (excluding .env.example which should be in git)
|
||
local env_files=()
|
||
for f in "$GIT_ROOT"/.env*; do
|
||
if [[ -f "$f" ]]; then
|
||
local basename=$(basename "$f")
|
||
# Skip .env.example (that's typically committed to git)
|
||
if [[ "$basename" != ".env.example" ]]; then
|
||
env_files+=("$basename")
|
||
fi
|
||
fi
|
||
done
|
||
|
||
if [[ ${#env_files[@]} -eq 0 ]]; then
|
||
echo -e " ${YELLOW}ℹ️ No .env files found in main repository${NC}"
|
||
return
|
||
fi
|
||
|
||
local copied=0
|
||
for env_file in "${env_files[@]}"; do
|
||
local source="$GIT_ROOT/$env_file"
|
||
local dest="$worktree_path/$env_file"
|
||
|
||
if [[ -f "$dest" ]]; then
|
||
echo -e " ${YELLOW}⚠️ $env_file already exists, backing up to ${env_file}.backup${NC}"
|
||
cp "$dest" "${dest}.backup"
|
||
fi
|
||
|
||
cp "$source" "$dest"
|
||
echo -e " ${GREEN}✓ Copied $env_file${NC}"
|
||
copied=$((copied + 1))
|
||
done
|
||
|
||
echo -e " ${GREEN}✓ Copied $copied environment file(s)${NC}"
|
||
}
|
||
|
||
# Create a new worktree
|
||
create_worktree() {
|
||
local branch_name="$1"
|
||
local from_branch="${2:-main}"
|
||
|
||
if [[ -z "$branch_name" ]]; then
|
||
echo -e "${RED}Error: Branch name required${NC}"
|
||
exit 1
|
||
fi
|
||
|
||
local worktree_path="$WORKTREE_DIR/$branch_name"
|
||
|
||
# Check if worktree already exists
|
||
if [[ -d "$worktree_path" ]]; then
|
||
echo -e "${YELLOW}Worktree already exists at: $worktree_path${NC}"
|
||
echo -e "Switch to it instead? (y/n)"
|
||
read -r response
|
||
if [[ "$response" == "y" ]]; then
|
||
switch_worktree "$branch_name"
|
||
fi
|
||
return
|
||
fi
|
||
|
||
echo -e "${BLUE}Creating worktree: $branch_name${NC}"
|
||
echo " From: $from_branch"
|
||
echo " Path: $worktree_path"
|
||
echo ""
|
||
echo "Proceed? (y/n)"
|
||
read -r response
|
||
|
||
if [[ "$response" != "y" ]]; then
|
||
echo -e "${YELLOW}Cancelled${NC}"
|
||
return
|
||
fi
|
||
|
||
# Update main branch
|
||
echo -e "${BLUE}Updating $from_branch...${NC}"
|
||
git checkout "$from_branch"
|
||
git pull origin "$from_branch" || true
|
||
|
||
# Create worktree
|
||
mkdir -p "$WORKTREE_DIR"
|
||
ensure_gitignore
|
||
|
||
echo -e "${BLUE}Creating worktree...${NC}"
|
||
git worktree add -b "$branch_name" "$worktree_path" "$from_branch"
|
||
|
||
# Copy environment files
|
||
copy_env_files "$worktree_path"
|
||
|
||
echo -e "${GREEN}✓ Worktree created successfully!${NC}"
|
||
echo ""
|
||
echo "To switch to this worktree:"
|
||
echo -e "${BLUE}cd $worktree_path${NC}"
|
||
echo ""
|
||
}
|
||
|
||
# List all worktrees
|
||
list_worktrees() {
|
||
echo -e "${BLUE}Available worktrees:${NC}"
|
||
echo ""
|
||
|
||
if [[ ! -d "$WORKTREE_DIR" ]]; then
|
||
echo -e "${YELLOW}No worktrees found${NC}"
|
||
return
|
||
fi
|
||
|
||
local count=0
|
||
for worktree_path in "$WORKTREE_DIR"/*; do
|
||
if [[ -d "$worktree_path" && -d "$worktree_path/.git" ]]; then
|
||
count=$((count + 1))
|
||
local worktree_name=$(basename "$worktree_path")
|
||
local branch=$(git -C "$worktree_path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||
|
||
if [[ "$PWD" == "$worktree_path" ]]; then
|
||
echo -e "${GREEN}✓ $worktree_name${NC} (current) → branch: $branch"
|
||
else
|
||
echo -e " $worktree_name → branch: $branch"
|
||
fi
|
||
fi
|
||
done
|
||
|
||
if [[ $count -eq 0 ]]; then
|
||
echo -e "${YELLOW}No worktrees found${NC}"
|
||
else
|
||
echo ""
|
||
echo -e "${BLUE}Total: $count worktree(s)${NC}"
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "${BLUE}Main repository:${NC}"
|
||
local main_branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown")
|
||
echo " Branch: $main_branch"
|
||
echo " Path: $GIT_ROOT"
|
||
}
|
||
|
||
# Switch to a worktree
|
||
switch_worktree() {
|
||
local worktree_name="$1"
|
||
|
||
if [[ -z "$worktree_name" ]]; then
|
||
list_worktrees
|
||
echo -e "${BLUE}Switch to which worktree? (enter name)${NC}"
|
||
read -r worktree_name
|
||
fi
|
||
|
||
local worktree_path="$WORKTREE_DIR/$worktree_name"
|
||
|
||
if [[ ! -d "$worktree_path" ]]; then
|
||
echo -e "${RED}Error: Worktree not found: $worktree_name${NC}"
|
||
echo ""
|
||
list_worktrees
|
||
exit 1
|
||
fi
|
||
|
||
echo -e "${GREEN}Switching to worktree: $worktree_name${NC}"
|
||
cd "$worktree_path"
|
||
echo -e "${BLUE}Now in: $(pwd)${NC}"
|
||
}
|
||
|
||
# Copy env files to an existing worktree (or current directory if in a worktree)
|
||
copy_env_to_worktree() {
|
||
local worktree_name="$1"
|
||
local worktree_path
|
||
|
||
if [[ -z "$worktree_name" ]]; then
|
||
# Check if we're currently in a worktree
|
||
local current_dir=$(pwd)
|
||
if [[ "$current_dir" == "$WORKTREE_DIR"/* ]]; then
|
||
worktree_path="$current_dir"
|
||
worktree_name=$(basename "$worktree_path")
|
||
echo -e "${BLUE}Detected current worktree: $worktree_name${NC}"
|
||
else
|
||
echo -e "${YELLOW}Usage: worktree-manager.sh copy-env [worktree-name]${NC}"
|
||
echo "Or run from within a worktree to copy to current directory"
|
||
list_worktrees
|
||
return 1
|
||
fi
|
||
else
|
||
worktree_path="$WORKTREE_DIR/$worktree_name"
|
||
|
||
if [[ ! -d "$worktree_path" ]]; then
|
||
echo -e "${RED}Error: Worktree not found: $worktree_name${NC}"
|
||
list_worktrees
|
||
return 1
|
||
fi
|
||
fi
|
||
|
||
copy_env_files "$worktree_path"
|
||
echo ""
|
||
}
|
||
|
||
# Clean up completed worktrees
|
||
cleanup_worktrees() {
|
||
if [[ ! -d "$WORKTREE_DIR" ]]; then
|
||
echo -e "${YELLOW}No worktrees to clean up${NC}"
|
||
return
|
||
fi
|
||
|
||
echo -e "${BLUE}Checking for completed worktrees...${NC}"
|
||
echo ""
|
||
|
||
local found=0
|
||
local to_remove=()
|
||
|
||
for worktree_path in "$WORKTREE_DIR"/*; do
|
||
if [[ -d "$worktree_path" && -d "$worktree_path/.git" ]]; then
|
||
local worktree_name=$(basename "$worktree_path")
|
||
|
||
# Skip if current worktree
|
||
if [[ "$PWD" == "$worktree_path" ]]; then
|
||
echo -e "${YELLOW}(skip) $worktree_name - currently active${NC}"
|
||
continue
|
||
fi
|
||
|
||
found=$((found + 1))
|
||
to_remove+=("$worktree_path")
|
||
echo -e "${YELLOW}• $worktree_name${NC}"
|
||
fi
|
||
done
|
||
|
||
if [[ $found -eq 0 ]]; then
|
||
echo -e "${GREEN}No inactive worktrees to clean up${NC}"
|
||
return
|
||
fi
|
||
|
||
echo ""
|
||
echo -e "Remove $found worktree(s)? (y/n)"
|
||
read -r response
|
||
|
||
if [[ "$response" != "y" ]]; then
|
||
echo -e "${YELLOW}Cleanup cancelled${NC}"
|
||
return
|
||
fi
|
||
|
||
echo -e "${BLUE}Cleaning up worktrees...${NC}"
|
||
for worktree_path in "${to_remove[@]}"; do
|
||
local worktree_name=$(basename "$worktree_path")
|
||
git worktree remove "$worktree_path" --force 2>/dev/null || true
|
||
echo -e "${GREEN}✓ Removed: $worktree_name${NC}"
|
||
done
|
||
|
||
# Clean up empty directory if nothing left
|
||
if [[ -z "$(ls -A "$WORKTREE_DIR" 2>/dev/null)" ]]; then
|
||
rmdir "$WORKTREE_DIR" 2>/dev/null || true
|
||
fi
|
||
|
||
echo -e "${GREEN}Cleanup complete!${NC}"
|
||
}
|
||
|
||
# Main command handler
|
||
main() {
|
||
local command="${1:-list}"
|
||
|
||
case "$command" in
|
||
create)
|
||
create_worktree "$2" "$3"
|
||
;;
|
||
list|ls)
|
||
list_worktrees
|
||
;;
|
||
switch|go)
|
||
switch_worktree "$2"
|
||
;;
|
||
copy-env|env)
|
||
copy_env_to_worktree "$2"
|
||
;;
|
||
cleanup|clean)
|
||
cleanup_worktrees
|
||
;;
|
||
help)
|
||
show_help
|
||
;;
|
||
*)
|
||
echo -e "${RED}Unknown command: $command${NC}"
|
||
echo ""
|
||
show_help
|
||
exit 1
|
||
;;
|
||
esac
|
||
}
|
||
|
||
show_help() {
|
||
cat << EOF
|
||
Git Worktree Manager
|
||
|
||
Usage: worktree-manager.sh <command> [options]
|
||
|
||
Commands:
|
||
create <branch-name> [from-branch] Create new worktree (copies .env files automatically)
|
||
(from-branch defaults to main)
|
||
list | ls List all worktrees
|
||
switch | go [name] Switch to worktree
|
||
copy-env | env [name] Copy .env files from main repo to worktree
|
||
(if name omitted, uses current worktree)
|
||
cleanup | clean Clean up inactive worktrees
|
||
help Show this help message
|
||
|
||
Environment Files:
|
||
- Automatically copies .env, .env.local, .env.test, etc. on create
|
||
- Skips .env.example (should be in git)
|
||
- Creates .backup files if destination already exists
|
||
- Use 'copy-env' to refresh env files after main repo changes
|
||
|
||
Examples:
|
||
worktree-manager.sh create feature-login
|
||
worktree-manager.sh create feature-auth develop
|
||
worktree-manager.sh switch feature-login
|
||
worktree-manager.sh copy-env feature-login
|
||
worktree-manager.sh copy-env # copies to current worktree
|
||
worktree-manager.sh cleanup
|
||
worktree-manager.sh list
|
||
|
||
EOF
|
||
}
|
||
|
||
# Run
|
||
main "$@"
|