Files
claude-engineering-plugin/plugins/compounding-engineering/skills/git-worktree/scripts/worktree-manager.sh
Ben Fisher 44a0acb5c3 fix(git-worktree): auto-copy .env files when creating worktrees
Problem:
When Claude creates worktrees, it sometimes calls `git worktree add` directly
instead of using the worktree-manager.sh script. This means .env files don't
get copied to the new worktree, causing the app to fail on startup.

Solution:
1. Add copy_env_files() function to worktree-manager.sh that copies all .env*
   files (except .env.example) from main repo to new worktree
2. Call copy_env_files() automatically during worktree creation
3. Add new 'copy-env' command to manually copy env files to existing worktrees
4. Update SKILL.md with CRITICAL section instructing Claude to NEVER call
   git worktree add directly - always use the manager script
5. Update all code examples to use ${CLAUDE_PLUGIN_ROOT} for portability
6. Add troubleshooting section for missing .env files

Features:
- Automatically copies .env, .env.local, .env.test, etc.
- Skips .env.example (should be in git)
- Creates .backup if destination already exists
- New 'copy-env' command for manual copying to existing worktrees
2025-11-27 22:26:10 -08:00

346 lines
8.7 KiB
Bash
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 "$@"