#!/bin/bash
# pwt - Power Worktrees
# A generic tool for managing git worktrees across multiple projects
#
# Usage:
#   pwt create <branch> [base] [desc]   # Create worktree from branch
#   pwt list                            # List worktrees and status
#   pwt info [worktree]                 # Show worktree details
#   pwt remove <worktree>               # Remove worktree
#   pwt server                          # Start dev server
#   pwt gateway                         # Stable gateway to a worktree server
#   pwt servers                         # Show project server status
#   pwt fix-port [worktree]             # Fix port conflict
#   pwt auto-remove [target]            # Remove worktrees merged into target
#   pwt meta [action] [args]            # Manage worktree metadata
#   pwt project [action] [args]         # Manage project configs
#
# Project auto-detection:
#   pwt detects the current project from your working directory
#   or you can specify: pwt myproject create ...
#   or use flag: pwt --project myproject create ...
#
# First time setup:
#   pwt project init myproject
#   pwt project set myproject main_app ~/path/to/main/app
#   pwt project set myproject worktrees_dir ~/path/to/worktrees

set -euo pipefail

# PWT directory structure
# PWT_DIR can be overridden via environment for testing/sandbox
PWT_DIR="${PWT_DIR:-$HOME/.pwt}"
PWT_META_FILE="$PWT_DIR/meta.json"
PWT_PROJECTS_DIR="$PWT_DIR/projects"

# Module library path (relative to this script)
# Resolve symlinks to find actual script location (needed for npm installs)
_pwt_source="${BASH_SOURCE[0]}"
while [[ -L "$_pwt_source" ]]; do
    _pwt_dir="$(cd -P "$(dirname "$_pwt_source")" && pwd)"
    _pwt_source="$(readlink "$_pwt_source")"
    [[ "$_pwt_source" != /* ]] && _pwt_source="$_pwt_dir/$_pwt_source"
done
PWT_SCRIPT_DIR="$(cd -P "$(dirname "$_pwt_source")" && pwd)"
PWT_LIB="${PWT_LIB:-$PWT_SCRIPT_DIR/../lib/pwt}"

# Load a module from lib/pwt/
# Usage: load_module <module_name>
load_module() {
    local module="$1"
    local module_path="$PWT_LIB/${module}.sh"
    if [[ -f "$module_path" ]]; then
        source "$module_path"
    else
        pwt_error "Error: Module not found: $module_path"
        exit 1
    fi
}

# Legacy support - will be overridden by project config
METADATA_DIR="$PWT_DIR"
METADATA_FILE="$PWT_META_FILE"
PROJECTS_DIR="$PWT_PROJECTS_DIR"

# Current project context (set by detect_project or --project flag)
CURRENT_PROJECT=""
PROJECT_EXPLICIT=false  # true if project was specified via --project or first arg
MAIN_APP=""
WORKTREES_DIR=""
BRANCH_PREFIX=""
PROJECT_REMOTE=""
DEFAULT_BRANCH=""  # Detected from remote (master or main)
BASE_PORT=5000

# Version
PWT_VERSION="0.1.13"
PWT_VERSION_CHECK_URL="https://api.github.com/repos/jonasporto/pwt/releases/latest"
PWT_VERSION_CACHE="$PWT_DIR/version-check"
PWT_VERSION_CACHE_TTL=18000  # 5 hours

# Detect installation method based on binary path
_pwt_install_method() {
    # Use PWT_SCRIPT_DIR which is already symlink-resolved at startup
    case "$PWT_SCRIPT_DIR" in
        */Cellar/pwt/*|*/homebrew/*)
            echo "brew"
            ;;
        */node_modules/*|*/.npm/*|*/.nvm/*)
            echo "npm"
            ;;
        *)
            echo "curl"
            ;;
    esac
}

# Get upgrade command based on installation method
_pwt_upgrade_command() {
    case "$(_pwt_install_method)" in
        brew) echo "brew upgrade pwt" ;;
        npm)  echo "npm i -g @jonasporto/pwt@latest" ;;
        curl) echo "curl -fsSL https://raw.githubusercontent.com/jonasporto/pwt/main/install.sh | bash" ;;
    esac
}

# Check for new version (cached)
_pwt_check_update() {
    local cache_file="$PWT_VERSION_CACHE"
    local now latest_version cache_time

    mkdir -p "$(dirname "$cache_file")" 2>/dev/null || true
    now=$(date +%s)

    # Check cache
    if [[ -f "$cache_file" ]]; then
        cache_time=$(head -1 "$cache_file" 2>/dev/null || echo "0")
        if (( now - cache_time < PWT_VERSION_CACHE_TTL )); then
            tail -1 "$cache_file" 2>/dev/null
            return
        fi
    fi

    # Fetch latest version (background-safe, no blocking)
    latest_version=$(curl -sf --max-time 2 "$PWT_VERSION_CHECK_URL" 2>/dev/null | grep -o '"tag_name": *"v[^"]*"' | head -1 | sed 's/.*"v\([^"]*\)".*/\1/' || echo "")

    if [[ -n "$latest_version" ]]; then
        echo "$now" > "$cache_file"
        echo "$latest_version" >> "$cache_file"
        echo "$latest_version"
    fi
}

# Compare versions (returns 0 if $1 < $2)
_pwt_version_lt() {
    [[ "$1" != "$2" ]] && [[ "$(printf '%s\n%s' "$1" "$2" | sort -V | head -1)" == "$1" ]]
}

# Show version with update check
_pwt_show_version() {
    echo "pwt version $PWT_VERSION"

    local latest
    latest=$(_pwt_check_update)

    if [[ -n "$latest" ]] && _pwt_version_lt "$PWT_VERSION" "$latest"; then
        echo ""
        echo -e "${YELLOW}Update available: $latest${NC}"
        echo -e "Run: ${GREEN}$(_pwt_upgrade_command)${NC}"
    fi
}

# Exit codes (for scripting)
EXIT_SUCCESS=0
EXIT_ERROR=1        # General error
EXIT_USAGE=2        # Usage/argument error
EXIT_NOT_FOUND=3    # Resource not found (worktree, project, branch)
EXIT_CONFLICT=4     # Conflict (already exists, port in use)

# Output control (set via --quiet or --verbose flags)
PWT_QUIET=false
PWT_VERBOSE=false
PWT_BG=false        # --bg: daemonize Pwtfile execution
PWT_NO_INPUT=false  # --no-input: close stdin, set PWT_AGENT=1

# Performance: List cache and fetch state
LIST_CACHE_DIR="$PWT_DIR/cache"
LIST_CACHE_TTL=300  # 5 minutes
PREFETCH_DONE=false  # Track if we already fetched remote refs
LIST_QUICK_MODE=false  # Skip network operations
LIST_REFRESH_MODE=false  # Force refresh cache

# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
DIM='\033[2m'
NC='\033[0m' # No Color

# Cross-platform sed in-place edit (BSD/GNU compatible)
sed_inplace() {
    local expr="$1"
    local file="$2"
    if sed --version >/dev/null 2>&1; then
        sed -i "$expr" "$file"        # GNU sed
    else
        sed -i '' "$expr" "$file"     # BSD sed (macOS)
    fi
}

# Output helpers for --quiet/--verbose control
# Usage: pwt_debug "message"    - Debug output (only with --verbose)
# Usage: pwt_error "message"    - Error (always shown)
pwt_debug() {
    [ "$PWT_VERBOSE" = true ] && echo -e "${DIM}[debug] $*${NC}" >&2
    return 0
}

pwt_error() {
    echo -e "${RED}$*${NC}" >&2
}

# Strip --bg and --no-input from arguments, setting globals instead
# Uses string concatenation (not arrays) to avoid empty array issues with set -u
_strip_pwt_execution_flags() {
    local result=""
    local first=true
    for arg in "$@"; do
        case "$arg" in
            --bg)       PWT_BG=true ;;
            --no-input) PWT_NO_INPUT=true ;;
            *)
                if [ "$first" = true ]; then
                    result="$arg"
                    first=false
                else
                    result="$result $arg"
                fi
                ;;
        esac
    done
    printf '%s' "$result"
}

# Find similar string from a list (for "did you mean?" suggestions)
# Usage: find_similar "input" "candidate1 candidate2 ..."
# Returns the closest match if similarity > 50%, empty otherwise
find_similar() {
    local input="$1"
    local candidates="$2"
    local best_match=""
    local best_score=0
    local input_len=${#input}

    for candidate in $candidates; do
        local score=0
        local cand_len=${#candidate}

        # Skip if lengths are too different (more than 3 chars apart)
        local len_diff=$((input_len - cand_len))
        [ "$len_diff" -lt 0 ] && len_diff=$((-len_diff))
        [ "$len_diff" -gt 3 ] && continue

        # Check prefix match (strongest signal)
        local prefix_len=2
        [ "$input_len" -lt 2 ] && prefix_len="$input_len"
        if [[ "${candidate:0:$prefix_len}" == "${input:0:$prefix_len}" ]]; then
            score=$((score + 30))
        fi

        # Count common characters
        local i=0
        while [ $i -lt $input_len ]; do
            local char="${input:$i:1}"
            if [[ "$candidate" == *"$char"* ]]; then
                score=$((score + 5))
            fi
            i=$((i + 1))
        done

        # Bonus for similar length
        if [ "$len_diff" -eq 0 ]; then
            score=$((score + 10))
        elif [ "$len_diff" -eq 1 ]; then
            score=$((score + 5))
        fi

        # Update best match
        if [ "$score" -gt "$best_score" ]; then
            best_score="$score"
            best_match="$candidate"
        fi
    done

    # Only suggest if similarity is reasonable (score > 40)
    if [ "$best_score" -gt 40 ]; then
        echo "$best_match"
    fi
}

# Check required command exists
# Usage: require_cmd <cmd> [optional]
# If optional=true, just warn instead of exit
require_cmd() {
    local cmd="$1"
    local optional="${2:-false}"

    if ! command -v "$cmd" >/dev/null 2>&1; then
        if [ "$optional" = true ]; then
            return 1
        else
            pwt_error "Error: Required command not found: $cmd"
            # Provide install hints for common tools
            case "$cmd" in
                git)
                    echo "Install: https://git-scm.com/downloads" >&2
                    echo "  macOS:  xcode-select --install" >&2
                    echo "  Ubuntu: sudo apt install git" >&2
                    ;;
                jq)
                    echo "Install: https://jqlang.github.io/jq/download/" >&2
                    echo "  macOS:  brew install jq" >&2
                    echo "  Ubuntu: sudo apt install jq" >&2
                    ;;
                fzf)
                    echo "Install: https://github.com/junegunn/fzf" >&2
                    echo "  macOS:  brew install fzf" >&2
                    echo "  Ubuntu: sudo apt install fzf" >&2
                    ;;
                lsof)
                    echo "Install:" >&2
                    echo "  macOS:  (pre-installed)" >&2
                    echo "  Ubuntu: sudo apt install lsof" >&2
                    ;;
                *)
                    echo "Please install $cmd to use this feature." >&2
                    ;;
            esac
            exit 1
        fi
    fi
    return 0
}

# Check dependencies early
check_dependencies() {
    require_cmd git
    require_cmd jq
    # Optional: lsof (for port checking), fzf (for select)
    # These are checked where needed, not at startup
}

# Ask user for confirmation (returns 0 for yes, 1 for no)
confirm_action() {
    local prompt="${1:-Continue?}"
    local response
    echo -n "$prompt [y/N] "
    read -r response
    case "$response" in
        [yY]|[yY][eE][sS]) return 0 ;;
        *) return 1 ;;
    esac
}

# Detect submodules and warn user (worktrees + submodules can be problematic)
# Returns 0 if ok to proceed, 1 if user aborted
detect_submodules() {
    local repo_path="${1:-$MAIN_APP}"

    # Check if .gitmodules exists
    if [ ! -f "$repo_path/.gitmodules" ]; then
        return 0
    fi

    echo -e "${YELLOW}⚠  Submodules detected (.gitmodules found)${NC}"

    # Check if any submodule is initialized
    if git -C "$repo_path" submodule status --recursive 2>/dev/null | grep -q '^[ +-]'; then
        echo -e "${YELLOW}⚠  Submodules are initialized${NC}"
        echo ""
        echo "Worktrees + submodules may behave unexpectedly."
        echo "Consider using clone mode for this project."
        echo ""
        echo "Options:"
        echo "  - Continue with worktree (may have issues)"
        echo "  - Use clone mode: pwt create --clone ..."
        echo ""

        if ! confirm_action "Continue with worktree?"; then
            echo "Aborted."
            return 1
        fi
    fi

    return 0
}

# Check if lsof is available (cache result for performance)
_lsof_available=""
has_lsof() {
    if [ -z "$_lsof_available" ]; then
        if command -v lsof >/dev/null 2>&1; then
            _lsof_available="yes"
        else
            _lsof_available="no"
        fi
    fi
    [ "$_lsof_available" = "yes" ]
}

# Get PIDs using a port (returns empty if lsof unavailable)
# Usage: get_pids_on_port <port>
get_pids_on_port() {
    local port="$1"
    if has_lsof; then
        lsof -ti ":$port" 2>/dev/null || true
    fi
    # Without lsof, we can't detect - return empty (assume free)
}

# Check if a port is occupied (returns 1 if occupied or unknown, 0 if free)
# Usage: is_port_free <port>
is_port_free() {
    local port="$1"
    if ! has_lsof; then
        # Without lsof, assume port is free (best effort)
        return 0
    fi
    ! lsof -ti ":$port" > /dev/null 2>&1
}

# Initialize PWT directory
init_pwt() {
    # Create directory structure if it doesn't exist
    if [ ! -d "$PWT_DIR" ]; then
        mkdir -p "$PWT_DIR"
        mkdir -p "$PWT_PROJECTS_DIR"
    fi

    # Create empty metadata file if it doesn't exist
    if [ ! -f "$PWT_META_FILE" ]; then
        echo '{}' > "$PWT_META_FILE"
    fi
}

# Alias for compatibility
init_metadata() {
    init_pwt
}

# ============================================
# List Cache Functions (shared infrastructure)
# ============================================

# Initialize cache directory
init_cache_dir() {
    [ -d "$LIST_CACHE_DIR" ] || mkdir -p "$LIST_CACHE_DIR"
}

# Get cache file path for current project
get_list_cache_file() {
    local project="${CURRENT_PROJECT:-unknown}"
    echo "$LIST_CACHE_DIR/list-${project}.txt"
}

# Check if cache exists and is fresh (< TTL)
is_list_cache_valid() {
    local cache_file=$(get_list_cache_file)
    [ ! -f "$cache_file" ] && return 1

    # Get cache file age in seconds
    local now=$(date +%s)
    local cache_mtime
    if stat --version >/dev/null 2>&1; then
        cache_mtime=$(stat -c %Y "$cache_file" 2>/dev/null)  # GNU stat
    else
        cache_mtime=$(stat -f %m "$cache_file" 2>/dev/null)  # BSD stat (macOS)
    fi
    [ -z "$cache_mtime" ] && return 1

    local age=$((now - cache_mtime))
    [ "$age" -lt "$LIST_CACHE_TTL" ]
}

# Clear list cache for current project
clear_list_cache() {
    local cache_file=$(get_list_cache_file)
    [ -f "$cache_file" ] && rm -f "$cache_file" || true
}

# Read cached list with dirty-only filter
# Keeps headers (first 3 lines) and filters data lines for dirty status (! or ?)
read_list_cache_filtered() {
    local cache_file=$(get_list_cache_file)
    [ ! -f "$cache_file" ] && return 1
    head -3 "$cache_file"
    tail -n +4 "$cache_file" | grep -E '[!?]' || true
}

# Detect default branch from remote (master or main)
# Usage: detect_default_branch <repo_path>
# Sets DEFAULT_BRANCH global variable
detect_default_branch() {
    local repo="${1:-$MAIN_APP}"
    if [ -z "$repo" ] || [ ! -d "$repo" ]; then
        return
    fi

    # Try to get from remote HEAD
    local remote_head=$(git -C "$repo" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null)
    if [ -n "$remote_head" ]; then
        DEFAULT_BRANCH="${remote_head#refs/remotes/origin/}"
        return
    fi

    # Fallback: check if main or master exists
    if git -C "$repo" rev-parse --verify origin/main >/dev/null 2>&1; then
        DEFAULT_BRANCH="main"
    elif git -C "$repo" rev-parse --verify origin/master >/dev/null 2>&1; then
        DEFAULT_BRANCH="master"
    else
        # Last resort default
        DEFAULT_BRANCH="master"
    fi
}

# Detect project from current directory
# Sets CURRENT_PROJECT, MAIN_APP, WORKTREES_DIR, BRANCH_PREFIX
detect_project() {
    local current_dir="${1:-}"
    # Get current dir, resolve symlinks for consistent comparison
    [ -z "$current_dir" ] && current_dir=$(pwd)
    # Resolve symlinks (e.g., /var -> /private/var on macOS)
    current_dir=$(cd "$current_dir" 2>/dev/null && pwd -P)

    # Already set via --project flag
    if [ -n "$CURRENT_PROJECT" ]; then
        load_project_config "$CURRENT_PROJECT"
        return 0
    fi

    # Search through existing configured projects
    if [ -d "$PWT_PROJECTS_DIR" ]; then
        for project_dir in "$PWT_PROJECTS_DIR"/*/; do
            [ -d "$project_dir" ] || continue
            local project_name=$(basename "$project_dir")
            local config_file="$project_dir/config.json"
            [ -f "$config_file" ] || continue

            local main_app=$(jq -r '.path // .main_app // empty' "$config_file")
            local worktrees_dir=$(jq -r '.worktrees_dir // empty' "$config_file")

            # Check if current dir is main_app or inside worktrees_dir
            # Resolve symlinks in config paths for consistent comparison
            if [ -n "$main_app" ] && [ -d "$main_app" ]; then
                local resolved_main=$(cd "$main_app" 2>/dev/null && pwd -P)
                case "$current_dir" in
                    "$resolved_main"*|"$resolved_main")
                        CURRENT_PROJECT="$project_name"
                        load_project_config "$project_name"
                        return 0
                        ;;
                esac
            fi
            if [ -n "$worktrees_dir" ] && [ -d "$worktrees_dir" ]; then
                local resolved_wt=$(cd "$worktrees_dir" 2>/dev/null && pwd -P)
                case "$current_dir" in
                    "$resolved_wt"*|"$resolved_wt")
                        CURRENT_PROJECT="$project_name"
                        load_project_config "$project_name"
                        return 0
                        ;;
                esac
            fi

            # Adopted worktrees can live outside worktrees_dir; use metadata paths
            # so commands run from inside them still resolve the configured project.
            if [ -f "$METADATA_FILE" ]; then
                while IFS= read -r meta_path; do
                    [ -n "$meta_path" ] && [ -d "$meta_path" ] || continue
                    local resolved_meta=$(cd "$meta_path" 2>/dev/null && pwd -P) || continue
                    case "$current_dir" in
                        "$resolved_meta"|"$resolved_meta"/*)
                            CURRENT_PROJECT="$project_name"
                            load_project_config "$project_name"
                            return 0
                            ;;
                    esac
                done < <(jq -r --arg project "$project_name" '.[$project] // {} | to_entries[] | .value.path // empty' "$METADATA_FILE" 2>/dev/null)
            fi
        done
    fi

    # No configured project found - try auto-detect from git
    auto_detect_project "$current_dir" || true
    return 0
}

# Auto-detect project from git repository (zero-config)
auto_detect_project() {
    local current_dir="$1"

    # Find git root
    local git_root=$(git -C "$current_dir" rev-parse --show-toplevel 2>/dev/null)
    [ -z "$git_root" ] && return 1

    # Project name = directory name
    local project_name=$(basename "$git_root")

    # Check if we're in a worktree (git-common-dir points to main repo's .git)
    local git_dir=$(git -C "$current_dir" rev-parse --git-dir 2>/dev/null)
    local git_common=$(git -C "$current_dir" rev-parse --git-common-dir 2>/dev/null)

    # In a worktree: git_dir is .git file, git_common is path to main's .git
    # In main repo: git_dir == git_common == .git
    if [ -n "$git_common" ] && [ "$git_dir" != "$git_common" ]; then
        # We're in a worktree, find the main repo
        # git_common = /path/to/main/.git, so main = dirname
        local main_repo=$(dirname "$git_common")
        if [ -d "$main_repo" ]; then
            git_root="$main_repo"
            project_name=$(basename "$git_root")
        fi
    fi

    # Set globals using conventions
    CURRENT_PROJECT="$project_name"
    MAIN_APP="$git_root"
    WORKTREES_DIR="$(dirname "$git_root")/${project_name}-worktrees"
    BRANCH_PREFIX=""

    return 0
}

# Resolve project alias to real project name
# Scans all project configs for "aliases" array or "alias" string
resolve_project_alias() {
    local name="$1"

    # First check if it's a real project
    if [ -f "$PWT_PROJECTS_DIR/$name/config.json" ]; then
        # Check it's not just an alias pointer (legacy) - check both .path and .main_app
        local has_main=$(jq -r '.path // .main_app // empty' "$PWT_PROJECTS_DIR/$name/config.json")
        if [ -n "$has_main" ]; then
            echo "$name"
            return 0
        fi
    fi

    # Scan all projects for alias match
    for config in "$PWT_PROJECTS_DIR"/*/config.json; do
        [ -f "$config" ] || continue

        # Check "aliases" array first (preferred format)
        local found_in_aliases=$(jq -r --arg name "$name" '.aliases // [] | index($name) // empty' "$config" 2>/dev/null)
        if [ -n "$found_in_aliases" ]; then
            basename "$(dirname "$config")"
            return 0
        fi

        # Fall back to legacy "alias" string
        local proj_alias=$(jq -r '.alias // empty' "$config")
        if [ "$proj_alias" = "$name" ]; then
            basename "$(dirname "$config")"
            return 0
        fi
    done

    # Not found, return original (might be auto-detected)
    echo "$name"
}

# Load project configuration into global variables
# Falls back to auto-detect if config doesn't exist
# Supports alias: config can have "alias": "pc"
load_project_config() {
    local project="$1"

    # Resolve alias to real project name
    project=$(resolve_project_alias "$project")
    local config_file="$PWT_PROJECTS_DIR/$project/config.json"

    CURRENT_PROJECT="$project"

    if [ -f "$config_file" ]; then
        # Load from config file
        # path is preferred, main_app for backwards compatibility
        local cfg_path=$(jq -r '.path // .main_app // empty' "$config_file")
        local cfg_wt=$(jq -r '.worktrees_dir // empty' "$config_file")
        local cfg_prefix=$(jq -r '.branch_prefix // empty' "$config_file")
        local cfg_port=$(jq -r '.base_port // empty' "$config_file")
        local cfg_remote=$(jq -r '.remote // empty' "$config_file")

        [ -n "$cfg_path" ] && MAIN_APP="$cfg_path"
        [ -n "$cfg_wt" ] && WORKTREES_DIR="$cfg_wt"
        [ -n "$cfg_prefix" ] && BRANCH_PREFIX="$cfg_prefix"
        [ -n "$cfg_port" ] && BASE_PORT="$cfg_port"
        [ -n "$cfg_remote" ] && PROJECT_REMOTE="$cfg_remote"
    fi

    # Detect default branch after MAIN_APP is set
    detect_default_branch "$MAIN_APP"

    return 0
}

# Require project context for commands that need it
# Auto-creates worktrees directory if needed
# Usage: require_project [--clone] [--info-only]
#   --clone     Auto-clone from remote if not cloned
#   --info-only Skip clone check (for list/info commands)
require_project() {
    local auto_clone=false
    local info_only=false
    for arg in "$@"; do
        [ "$arg" = "--clone" ] && auto_clone=true
        [ "$arg" = "--info-only" ] && info_only=true
    done

    if [ -z "$CURRENT_PROJECT" ]; then
        # Check if we're in a git repo (potential new project)
        if git rev-parse --git-dir &>/dev/null; then
            local repo_name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)")
            echo -e "${YELLOW}Git repository detected: $repo_name${NC}" >&2
            echo "" >&2
            echo "This repo is not configured with pwt yet." >&2
            echo "" >&2
            echo "To set it up:" >&2
            echo "  pwt init                    # configure this repo" >&2
            echo "" >&2
            echo "Or specify a configured project:" >&2
            echo "  pwt <project> <command>     # e.g., pwt acme list" >&2
            echo "  pwt project list            # see configured projects" >&2
        else
            pwt_error "Error: No project detected"
            echo "" >&2
            echo "You're not inside a git repository." >&2
            echo "" >&2
            echo "Options:" >&2
            echo "  pwt <project> <command>     # specify project explicitly" >&2
            echo "  pwt project list            # see configured projects" >&2
            echo "  cd /path/to/repo && pwt init" >&2
        fi
        exit 1
    fi

    if [ -z "$MAIN_APP" ] || [ -z "$WORKTREES_DIR" ]; then
        pwt_error "Error: Could not determine project paths"
        echo "" >&2
        echo "Run from inside a git repository, or initialize a project:" >&2
        echo "  cd /path/to/repo && pwt init" >&2
        echo "  pwt init git@github.com:user/repo.git" >&2
        echo "" >&2
        echo "Or configure manually:" >&2
        echo "  pwt project set myproject path /path/to/main/app" >&2
        echo "  pwt project set myproject worktrees_dir /path/to/worktrees" >&2
        exit 1
    fi

    # For info-only mode, skip clone/exist checks
    [ "$info_only" = true ] && return 0

    # Check if main app exists
    if [ ! -d "$MAIN_APP" ]; then
        if [ -n "$PROJECT_REMOTE" ]; then
            if [ "$auto_clone" = true ]; then
                echo -e "${BLUE}Cloning from remote: $PROJECT_REMOTE${NC}"
                local parent_dir=$(dirname "$MAIN_APP")
                mkdir -p "$parent_dir"
                if git clone "$PROJECT_REMOTE" "$MAIN_APP"; then
                    echo -e "${GREEN}✓ Cloned successfully${NC}"
                else
                    pwt_error "Error: Failed to clone from $PROJECT_REMOTE"
                    exit 1
                fi
            else
                pwt_error "Error: Project not cloned"
                echo ""
                echo "Run: pwt clone $CURRENT_PROJECT"
                echo "Or:  pwt $CURRENT_PROJECT create <branch> <base> <desc>"
                exit 1
            fi
        else
            pwt_error "Error: Main app directory not found: $MAIN_APP"
            exit 1
        fi
    fi

    # Auto-create worktrees directory if it doesn't exist
    if [ ! -d "$WORKTREES_DIR" ]; then
        echo -e "${BLUE}Creating worktrees directory: $WORKTREES_DIR${NC}"
        mkdir -p "$WORKTREES_DIR"
    fi
}

# Initialize a project (clone from URL or configure current directory)
# Usage: cmd_init [url]
#   With url: clone and configure as pwt project
#   Without url: configure current git repo as pwt project
cmd_init() {
    local url="${1:-}"

    # If URL provided, clone and configure
    if [ -n "$url" ]; then
        # Extract project name from URL
        local project_name=$(basename "$url" .git)
        local target_dir="${PWD}/${project_name}"

        if [ -d "$target_dir" ]; then
            echo -e "${YELLOW}Already exists: $target_dir${NC}"
            echo "To configure: cd $target_dir && pwt init"
            return 0
        fi

        echo -e "${BLUE}Cloning: $url${NC}"
        if ! git clone "$url" "$target_dir"; then
            pwt_error "Error: Failed to clone"
            exit 1
        fi

        # Configure the project
        local project_dir="$PWT_PROJECTS_DIR/$project_name"
        mkdir -p "$project_dir"

        cat > "$project_dir/config.json" << EOF
{
  "path": "$target_dir",
  "remote": "$url",
  "worktrees_dir": "${target_dir}-worktrees"
}
EOF

        echo -e "${GREEN}✓ Cloned and configured: $project_name${NC}"
        echo ""
        echo "Usage:"
        echo -e "  ${GREEN}pwt $project_name list${NC}"
        echo -e "  ${GREEN}pwt $project_name create TICKET base \"description\"${NC}"
        return 0
    fi

    # No URL - configure current directory
    if ! git rev-parse --git-dir > /dev/null 2>&1; then
        pwt_error "Error: Not a git repository"
        echo ""
        echo "Usage:"
        echo "  pwt init <url>    # Clone and configure new project"
        echo "  cd <repo> && pwt init  # Configure existing repo"
        exit 1
    fi

    local repo_root=$(git rev-parse --show-toplevel)
    local project_name=$(basename "$repo_root")
    local remote_url=$(git remote get-url origin 2>/dev/null || echo "")

    # Check if already configured
    if [ -f "$PWT_PROJECTS_DIR/$project_name/config.json" ]; then
        echo -e "${YELLOW}Already configured: $project_name${NC}"
        echo ""
        cat "$PWT_PROJECTS_DIR/$project_name/config.json"
        return 0
    fi

    # Create config
    local project_dir="$PWT_PROJECTS_DIR/$project_name"
    mkdir -p "$project_dir"

    local config="{
  \"path\": \"$repo_root\",
  \"worktrees_dir\": \"${repo_root}-worktrees\""

    if [ -n "$remote_url" ]; then
        config="$config,
  \"remote\": \"$remote_url\""
    fi

    config="$config
}"

    echo "$config" > "$project_dir/config.json"

    echo -e "${GREEN}✓ Configured: $project_name${NC}"
    echo ""
    cat "$project_dir/config.json"
    echo ""
    echo "Usage:"
    echo -e "  ${GREEN}pwt $project_name list${NC}"
    echo -e "  ${GREEN}pwt $project_name create TICKET base \"description\"${NC}"
}

# Initialize a project with explicit name
# Usage: cmd_init_named <project_name> [url]
#   With url: clone to <project_name> directory and configure
#   Without url: configure current git repo as <project_name>
cmd_init_named() {
    local project_name="$1"
    local url="${2:-}"

    if [ -n "$url" ]; then
        # Clone mode: pwt myproj init <url>
        local target_dir="${PWD}/${project_name}"

        if [ -d "$target_dir" ]; then
            echo -e "${YELLOW}Already exists: $target_dir${NC}"
            echo "To configure: cd $target_dir && pwt init"
            return 0
        fi

        echo -e "${BLUE}Cloning: $url${NC}"
        if ! git clone "$url" "$target_dir"; then
            pwt_error "Error: Failed to clone"
            exit 1
        fi

        # Configure the project
        local project_dir="$PWT_PROJECTS_DIR/$project_name"
        mkdir -p "$project_dir"

        cat > "$project_dir/config.json" << EOF
{
  "path": "$target_dir",
  "remote": "$url",
  "worktrees_dir": "${target_dir}-worktrees"
}
EOF

        echo -e "${GREEN}✓ Cloned and configured: $project_name${NC}"
        echo ""
        echo "Usage:"
        echo -e "  ${GREEN}pwt $project_name list${NC}"
        echo -e "  ${GREEN}pwt $project_name create TICKET base \"description\"${NC}"
        return 0
    fi

    # No URL - configure current directory with given name
    if ! git rev-parse --git-dir > /dev/null 2>&1; then
        pwt_error "Error: Not a git repository"
        echo ""
        echo "Usage:"
        echo "  pwt <name> init <url>    # Clone and configure with name"
        echo "  cd <repo> && pwt <name> init  # Configure existing repo with name"
        exit 1
    fi

    local repo_root=$(git rev-parse --show-toplevel)
    local remote_url=$(git remote get-url origin 2>/dev/null || echo "")

    # Check if already configured
    if [ -f "$PWT_PROJECTS_DIR/$project_name/config.json" ]; then
        echo -e "${YELLOW}Already configured: $project_name${NC}"
        echo ""
        cat "$PWT_PROJECTS_DIR/$project_name/config.json"
        return 0
    fi

    # Create config
    local project_dir="$PWT_PROJECTS_DIR/$project_name"
    mkdir -p "$project_dir"

    local config="{
  \"path\": \"$repo_root\",
  \"worktrees_dir\": \"${repo_root}-worktrees\""

    if [ -n "$remote_url" ]; then
        config="$config,
  \"remote\": \"$remote_url\""
    fi

    config="$config
}"

    echo "$config" > "$project_dir/config.json"

    echo -e "${GREEN}✓ Configured as: $project_name${NC}"
    echo ""
    cat "$project_dir/config.json"
    echo ""
    echo "Usage:"
    echo -e "  ${GREEN}pwt $project_name list${NC}"
    echo -e "  ${GREEN}pwt $project_name create TICKET base \"description\"${NC}"
}

# Discover unconfigured git repositories in a directory
# Usage: cmd_discover <path> [--init] [--depth N] [--include-submodules]
cmd_discover() {
    local search_path="${1:-.}"
    local do_init=false
    local max_depth=5
    local include_submodules=false

    # Resolve to absolute path
    search_path=$(cd "$search_path" 2>/dev/null && pwd) || {
        pwt_error "Error: Directory not found: $1"
        exit 1
    }

    # Parse flags
    shift || true
    while [ $# -gt 0 ]; do
        case "$1" in
            --init)
                do_init=true
                shift
                ;;
            --depth)
                max_depth="$2"
                shift 2
                ;;
            --include-submodules)
                include_submodules=true
                shift
                ;;
            *)
                pwt_error "Unknown option: $1"
                exit 1
                ;;
        esac
    done

    echo -e "${BLUE}Scanning $search_path...${NC}"
    echo ""

    local found_repos=()
    local skipped_configured=()
    local skipped_worktrees=()
    local skipped_submodules=()
    local find_depth="$max_depth"
    if [[ "$find_depth" =~ ^[0-9]+$ ]]; then
        find_depth=$((find_depth + 1))
    fi

    # Get list of already configured project paths
    local configured_paths=()
    if [ -d "$PWT_PROJECTS_DIR" ]; then
        for project_config in "$PWT_PROJECTS_DIR"/*/config.json; do
            [ -f "$project_config" ] || continue
            local path=$(jq -r '.path // empty' "$project_config" 2>/dev/null)
            [ -n "$path" ] && configured_paths+=("$path")
        done
    fi

    # Find all .git directories/files
    while IFS= read -r git_path; do
        local repo_path=$(dirname "$git_path")
        local repo_name=$(basename "$repo_path")
        local relative_path="${repo_path#$search_path/}"

        # Skip *-worktrees directories (pwt convention)
        if [[ "$repo_path" == *-worktrees/* ]] || [[ "$repo_path" == *-worktrees ]]; then
            skipped_worktrees+=("$relative_path")
            continue
        fi

        # Check if .git is a file (worktree or submodule)
        if [ -f "$git_path" ]; then
            # It's a git worktree or submodule
            if grep -q "gitdir:" "$git_path" 2>/dev/null; then
                # Check if it's a submodule (parent has .gitmodules referencing it)
                local submodule_root=""
                local scan_dir
                scan_dir=$(dirname "$repo_path")
                while [ "$scan_dir" != "$search_path" ] && [ "$scan_dir" != "/" ] && [ -n "$scan_dir" ]; do
                    if [ -f "$scan_dir/.gitmodules" ]; then
                        local submodule_rel="${repo_path#$scan_dir/}"
                        if awk -F= -v path="$submodule_rel" '
                            $1 ~ /^[[:space:]]*path[[:space:]]*$/ {
                                value = $2
                                gsub(/^[[:space:]]+|[[:space:]]+$/, "", value)
                                if (value == path) found = 1
                            }
                            END { exit found ? 0 : 1 }
                        ' "$scan_dir/.gitmodules" 2>/dev/null; then
                            submodule_root="$scan_dir"
                            break
                        fi
                    fi
                    scan_dir=$(dirname "$scan_dir")
                done
                if [ -n "$submodule_root" ]; then
                    if [ "$include_submodules" = false ]; then
                        skipped_submodules+=("$relative_path (submodule)")
                        continue
                    fi
                else
                    # It's a worktree, skip
                    skipped_worktrees+=("$relative_path (worktree)")
                    continue
                fi
            fi
        fi

        # Check if already configured
        local is_configured=false
        if [ ${#configured_paths[@]} -gt 0 ]; then
            for configured_path in "${configured_paths[@]}"; do
                if [ "$repo_path" = "$configured_path" ]; then
                    is_configured=true
                    skipped_configured+=("$relative_path")
                    break
                fi
            done
        fi

        if [ "$is_configured" = false ]; then
            found_repos+=("$repo_path")
        fi

    done < <(find "$search_path" -maxdepth "$find_depth" \( -name ".git" -type d -o -name ".git" -type f \) 2>/dev/null | sort)

    # Display results
    if [ ${#found_repos[@]} -eq 0 ]; then
        echo "No unconfigured repositories found."
        echo ""
        if [ ${#skipped_configured[@]} -gt 0 ]; then
            echo -e "${GREEN}Already configured:${NC} ${skipped_configured[*]}"
        fi
        return 0
    fi

    echo -e "${GREEN}Found ${#found_repos[@]} unconfigured $([ ${#found_repos[@]} -eq 1 ] && echo "repository" || echo "repositories"):${NC}"
    echo ""

    for repo_path in "${found_repos[@]}"; do
        local relative_path="${repo_path#$search_path/}"
        local remote_url=$(git -C "$repo_path" remote get-url origin 2>/dev/null || echo "(no remote)")
        local branch=$(git -C "$repo_path" branch --show-current 2>/dev/null || echo "?")

        echo -e "  ${YELLOW}$relative_path${NC}"
        echo -e "  └─ $remote_url ($branch)"
        echo ""
    done

    # Show skipped
    if [ ${#skipped_configured[@]} -gt 0 ]; then
        echo -e "${BLUE}Skipped (configured):${NC} ${skipped_configured[*]}"
    fi
    if [ ${#skipped_worktrees[@]} -gt 0 ]; then
        echo -e "${BLUE}Skipped (worktrees):${NC} ${skipped_worktrees[*]}"
    fi
    if [ ${#skipped_submodules[@]} -gt 0 ]; then
        echo -e "${BLUE}Skipped (submodules):${NC} ${skipped_submodules[*]}"
    fi
    echo ""

    # If --init, configure all found repos
    if [ "$do_init" = true ]; then
        echo -e "${BLUE}Configuring...${NC}"
        echo ""
        for repo_path in "${found_repos[@]}"; do
            (cd "$repo_path" && cmd_init)
            echo ""
        done
    else
        echo "To configure:"
        for repo_path in "${found_repos[@]}"; do
            local project_name=$(basename "$repo_path")
            echo -e "  pwt init $repo_path"
        done
        echo ""
        echo -e "Or run: ${GREEN}pwt discover $search_path --init${NC}"
    fi
}

# Atomic write to metadata file with file locking
# Usage: atomic_metadata_write <jq_filter> [jq_args...]
atomic_metadata_write() {
    local jq_filter="$1"
    shift

    init_metadata

    local lock_dir="$METADATA_FILE.lock"
    local lock_pid_file="$lock_dir/pid"
    # Create tmp in same directory as metadata for atomic mv (same filesystem)
    local tmp_file
    tmp_file="$(mktemp "${METADATA_FILE}.tmp.XXXXXX")"
    local max_wait=50  # 5 seconds (50 * 0.1s)
    local stale_threshold=60  # seconds - lock older than this is stale
    local already_locked=false

    # Create tmp_file early and set trap to clean it up on any exit
    trap "rm -f '$tmp_file'" RETURN

    # Check if we already own the lock (re-entrant call)
    if [ -d "$lock_dir" ] && [ -f "$lock_pid_file" ]; then
        local current_owner=$(cat "$lock_pid_file" 2>/dev/null)
        if [ "$current_owner" = "$$" ]; then
            already_locked=true
        fi
    fi

    # Acquire lock using mkdir (atomic on all systems), unless we already own it
    local waited=0
    while [ "$already_locked" = false ] && ! mkdir "$lock_dir" 2>/dev/null; do
        # Check for stale lock
        if [ -f "$lock_pid_file" ]; then
            local lock_pid=$(cat "$lock_pid_file" 2>/dev/null)
            local lock_age=0
            # Get lock age in seconds (portable: stat -f %m on macOS, stat -c %Y on Linux)
            if stat --version >/dev/null 2>&1; then
                lock_age=$(( $(date +%s) - $(stat -c %Y "$lock_pid_file" 2>/dev/null || echo 0) ))
            else
                lock_age=$(( $(date +%s) - $(stat -f %m "$lock_pid_file" 2>/dev/null || echo 0) ))
            fi

            # Remove stale lock if process is dead or lock is too old
            if [ -n "$lock_pid" ]; then
                if ! kill -0 "$lock_pid" 2>/dev/null || [ "$lock_age" -gt "$stale_threshold" ]; then
                    rm -rf "$lock_dir" 2>/dev/null
                    continue  # Retry acquiring lock
                fi
            fi
        fi

        waited=$((waited + 1))
        if [ $waited -ge $max_wait ]; then
            pwt_error "Error: Could not acquire lock on metadata file"
            return 1
        fi
        sleep 0.1
    done

    # Write our PID to the lock (only if we acquired it)
    if [ "$already_locked" = false ]; then
        echo "$$" > "$lock_pid_file"
        # Update trap to also clean lock after acquiring it
        trap "rm -rf '$lock_dir' '$tmp_file'" RETURN
    fi
    # If already_locked, we keep the original trap that only cleans tmp_file

    # Perform atomic write
    if jq "$jq_filter" "$@" "$METADATA_FILE" > "$tmp_file"; then
        mv "$tmp_file" "$METADATA_FILE"
        return 0
    else
        return 1
    fi
    # trap handles cleanup on return
}

# Acquire metadata lock (for operations needing read-modify-write atomicity)
# Usage: acquire_metadata_lock
# Returns: 0 on success, 1 on failure
# IMPORTANT: Must call release_metadata_lock when done
acquire_metadata_lock() {
    init_metadata

    local lock_dir="$METADATA_FILE.lock"
    local lock_pid_file="$lock_dir/pid"
    local max_wait=50  # 5 seconds
    local stale_threshold=60

    local waited=0
    while ! mkdir "$lock_dir" 2>/dev/null; do
        # Check for stale lock
        if [ -f "$lock_pid_file" ]; then
            local lock_pid=$(cat "$lock_pid_file" 2>/dev/null)
            local lock_age=0
            if stat --version >/dev/null 2>&1; then
                lock_age=$(( $(date +%s) - $(stat -c %Y "$lock_pid_file" 2>/dev/null || echo 0) ))
            else
                lock_age=$(( $(date +%s) - $(stat -f %m "$lock_pid_file" 2>/dev/null || echo 0) ))
            fi
            if [ -n "$lock_pid" ]; then
                if ! kill -0 "$lock_pid" 2>/dev/null || [ "$lock_age" -gt "$stale_threshold" ]; then
                    rm -rf "$lock_dir" 2>/dev/null
                    continue
                fi
            fi
        fi

        waited=$((waited + 1))
        if [ $waited -ge $max_wait ]; then
            pwt_error "Error: Could not acquire metadata lock"
            return 1
        fi
        sleep 0.1
    done

    echo "$$" > "$lock_pid_file"
    return 0
}

# Release metadata lock
# Usage: release_metadata_lock
release_metadata_lock() {
    local lock_dir="$METADATA_FILE.lock"
    rm -rf "$lock_dir" 2>/dev/null
}

# Save worktree metadata (namespaced by project)
# Usage: save_metadata <name> <path> <branch> <base> <base_commit> <port> <description>
save_metadata() {
    local name="$1"
    local path="$2"
    local branch="$3"
    local base="$4"
    local base_commit="$5"
    local port="$6"
    local description="$7"
    local mode="${8:-worktree}"  # worktree or clone
    local created_at=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
    local project="${CURRENT_PROJECT:-unknown}"

    atomic_metadata_write \
       '.[$project][$name] = {
           path: $path,
           branch: $branch,
           base: $base,
           base_commit: $base_commit,
           port: $port,
           description: $description,
           mode: $mode,
           created_at: $created_at
       }' \
       --arg project "$project" \
       --arg name "$name" \
       --arg path "$path" \
       --arg branch "$branch" \
       --arg base "$base" \
       --arg base_commit "$base_commit" \
       --argjson port "$port" \
       --arg description "$description" \
       --arg mode "$mode" \
       --arg created_at "$created_at"
}

# Get worktree metadata field (namespaced by project)
# Usage: get_metadata <name> <field>
get_metadata() {
    local name="$1"
    local field="$2"
    local project="${CURRENT_PROJECT:-unknown}"

    init_metadata

    jq -r --arg project "$project" --arg name "$name" --arg field "$field" \
        '.[$project][$name][$field] // empty' "$METADATA_FILE" 2>/dev/null
}

# Get port for a worktree, with legacy fallback
# Tries metadata first, then falls back to extracting from directory name (-XXXX suffix)
# Usage: get_worktree_port <name>
get_worktree_port() {
    local name="$1"
    local port=$(get_metadata "$name" "port")
    if [ -z "$port" ] && [[ "$name" =~ -([0-9]{4})$ ]]; then
        port="${BASH_REMATCH[1]}"
    fi
    echo "$port"
}

# Get extra metadata fields as key=value string (for Meta column in list)
# Excludes fields already shown as columns: path, branch, base, base_commit, created_at, mode
# Shows: port, description, and any custom fields (only if non-empty)
# Usage: get_extra_metadata <name>
get_extra_metadata() {
    local name="$1"
    local project="${CURRENT_PROJECT:-unknown}"

    init_metadata

    jq -r --arg project "$project" --arg name "$name" '
        .[$project][$name] // {} |
        to_entries |
        map(select(.key | test("^(path|branch|base|base_commit|created_at|mode)$") | not)) |
        map(select(.value != null and .value != "")) |
        map("\(.key)=\(.value)") |
        join(" ")
    ' "$METADATA_FILE" 2>/dev/null
}

# Remove worktree metadata (namespaced by project)
# Usage: remove_metadata <name>
remove_metadata() {
    local name="$1"
    local project="${CURRENT_PROJECT:-unknown}"
    atomic_metadata_write 'del(.[$project][$name])' \
        --arg project "$project" --arg name "$name"
}

# Update worktree metadata field (namespaced by project)
# Usage: update_metadata <name> <field> <value>
# Note: For numeric fields (port), use update_metadata_json
update_metadata() {
    local name="$1"
    local field="$2"
    local value="$3"
    local project="${CURRENT_PROJECT:-unknown}"
    atomic_metadata_write \
       'if .[$project][$name] then .[$project][$name][$field] = $value else . end' \
       --arg project "$project" --arg name "$name" --arg field "$field" --arg value "$value"
}

# Update worktree metadata field with JSON value (for numbers, booleans, etc.)
# Usage: update_metadata_json <name> <field> <json_value>
update_metadata_json() {
    local name="$1"
    local field="$2"
    local json_value="$3"
    local project="${CURRENT_PROJECT:-unknown}"
    atomic_metadata_write \
       'if .[$project][$name] then .[$project][$name][$field] = $value else . end' \
       --arg project "$project" --arg name "$name" --arg field "$field" --argjson value "$json_value"
}

# Get project config value
# Usage: get_project_config <project_name> <key>
get_project_config() {
    local project="$1"
    local key="$2"
    local config_file="$PROJECTS_DIR/$project/config.json"

    if [ -f "$config_file" ]; then
        jq -r --arg key "$key" '.[$key] // empty' "$config_file" 2>/dev/null
    fi
}

# ============================================
# Global AI Tools Config Functions
# ============================================

# Resolve AI tool command from config or PATH
# Returns the command to execute for the given tool name
# Resolution order: config.json > PATH > empty
resolve_ai_tool() {
    local name="${1:-}"
    local config_file="$PWT_DIR/config.json"

    # If empty, use default
    if [ -z "$name" ]; then
        if [ -f "$config_file" ]; then
            name=$(jq -r '.ai.default // empty' "$config_file" 2>/dev/null)
        fi
        [ -z "$name" ] && name="claude"
    fi

    # Look up in config
    local cmd=""
    if [ -f "$config_file" ]; then
        cmd=$(jq -r --arg name "$name" '.ai.tools[$name] // empty' "$config_file" 2>/dev/null)
    fi

    # Fallback to PATH
    if [ -z "$cmd" ] && command -v "$name" &>/dev/null; then
        cmd="$name"
    fi

    echo "$cmd"
}

# Add an AI tool to global config
add_ai_tool() {
    local name="$1"
    local cmd="$2"
    local config_file="$PWT_DIR/config.json"

    [ ! -f "$config_file" ] && echo '{}' > "$config_file"

    local tmp_file
    tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
    jq --arg name "$name" --arg cmd "$cmd" '
        .ai //= {} |
        .ai.tools //= {} |
        .ai.tools[$name] = $cmd
    ' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
}

# Remove an AI tool from global config
remove_ai_tool() {
    local name="$1"
    local config_file="$PWT_DIR/config.json"

    [ ! -f "$config_file" ] && return 0

    local tmp_file
    tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
    jq --arg name "$name" 'del(.ai.tools[$name])' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
}

# Set default AI tool
set_default_ai_tool() {
    local name="$1"
    local config_file="$PWT_DIR/config.json"

    [ ! -f "$config_file" ] && echo '{}' > "$config_file"

    local tmp_file
    tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
    jq --arg name "$name" '.ai.default = $name' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
}

# Add an editor to global config
add_editor_tool() {
    local name="$1"
    local cmd="$2"
    local config_file="$PWT_DIR/config.json"

    [ ! -f "$config_file" ] && echo '{}' > "$config_file"

    local tmp_file
    tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
    jq --arg name "$name" --arg cmd "$cmd" '
        .editor //= {} |
        .editor.tools //= {} |
        .editor.tools[$name] = $cmd
    ' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
}

# Remove an editor from global config
remove_editor_tool() {
    local name="$1"
    local config_file="$PWT_DIR/config.json"

    [ ! -f "$config_file" ] && return 0

    local tmp_file
    tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
    jq --arg name "$name" 'del(.editor.tools[$name])' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
}

# Set default editor
set_default_editor_tool() {
    local name="$1"
    local config_file="$PWT_DIR/config.json"

    [ ! -f "$config_file" ] && echo '{}' > "$config_file"

    local tmp_file
    tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
    jq --arg name "$name" '.editor.default = $name' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
}

# Get editor command (from config, then $EDITOR)
get_editor_cmd() {
    local name="${1:-}"
    local config_file="$PWT_DIR/config.json"

    # If name specified, look it up
    if [ -n "$name" ]; then
        if [ -f "$config_file" ]; then
            local cmd
            cmd=$(jq -r --arg name "$name" '.editor.tools[$name] // empty' "$config_file" 2>/dev/null)
            if [ -n "$cmd" ]; then
                echo "$cmd"
                return 0
            fi
        fi
        # Check if it's in PATH
        if command -v "$name" &>/dev/null; then
            echo "$name"
            return 0
        fi
        return 1
    fi

    # No name: use default from config, then $EDITOR
    if [ -f "$config_file" ]; then
        local default_name
        default_name=$(jq -r '.editor.default // empty' "$config_file" 2>/dev/null)
        if [ -n "$default_name" ]; then
            local cmd
            cmd=$(jq -r --arg name "$default_name" '.editor.tools[$name] // empty' "$config_file" 2>/dev/null)
            if [ -n "$cmd" ]; then
                echo "$cmd"
                return 0
            fi
            # Default name might be a PATH command
            if command -v "$default_name" &>/dev/null; then
                echo "$default_name"
                return 0
            fi
        fi
    fi

    # Fall back to $EDITOR
    if [ -n "$EDITOR" ]; then
        echo "$EDITOR"
        return 0
    fi

    return 1
}

# Pwtfile helpers (available in Pwtfile)
# These are sourced when running a Pwtfile
pwtfile_env() {
    local key="$1"
    local value="$2"
    if [ -f .env ]; then
        if grep -q "^${key}=" .env; then
            sed_inplace "s|^${key}=.*|${key}=${value}|" .env
        else
            echo "${key}=${value}" >> .env
        fi
    fi
}

pwtfile_replace() {
    local file="$1"
    local from="$2"
    local to="$3"
    [ -f "$file" ] && sed_inplace "s|${from}|${to}|g" "$file"
}

# Safe literal string replacement (no regex interpretation)
# Cross-platform: uses perl's \Q...\E for literal matching
# Usage: pwtfile_replace_literal <file> <from> <to>
# Example: pwtfile_replace_literal "config.yml" "localhost:3000" "localhost:5007"
pwtfile_replace_literal() {
    local file="$1"
    local from="$2"
    local to="$3"
    [ -f "$file" ] || return 0

    if command -v perl >/dev/null 2>&1; then
        # \Q...\E quotes metacharacters, making it literal
        perl -i -pe 'BEGIN { $f = shift; $t = shift } s/\Q$f\E/$t/g' "$from" "$to" "$file"
    else
        echo "  ! perl not found, skipping literal replace in $file"
        return 1
    fi
}

# Regex replacement using perl (cross-platform, unlike sed -E)
# Usage: pwtfile_replace_re <file> <pattern> <replacement>
# Example: pwtfile_replace_re "config.yml" "port:\s*\d+" "port: 5007"
pwtfile_replace_re() {
    local file="$1"
    local pattern="$2"
    local replacement="$3"
    [ -f "$file" ] || return 0

    if command -v perl >/dev/null 2>&1; then
        perl -i -pe "s/$pattern/$replacement/g" "$file"
    else
        echo "  ! perl not found, skipping regex replace in $file"
        return 1
    fi
}

pwtfile_database() {
    local action="$1"
    local name="$2"
    case "$action" in
        create) createdb "$name" 2>/dev/null || true ;;
        drop) dropdb "$name" 2>/dev/null || true ;;
    esac
}

pwtfile_rake() {
    bundle exec rake "$@" 2>/dev/null || true
}

pwtfile_run() {
    "$@" 2>/dev/null || true
}

# Symlink from main app to current worktree
# Usage: pwtfile_symlink <path>
# Example: pwtfile_symlink "node_modules"
pwtfile_symlink() {
    local path="$1"
    local source="$MAIN_APP/$path"
    local target="$PWD/$path"

    if [ ! -e "$source" ]; then
        echo "  ! Source not found: $source"
        return 1
    fi

    if [ -e "$target" ] && [ ! -L "$target" ]; then
        echo "  ! Target exists (not a symlink): $target"
        return 1
    fi

    rm -f "$target" 2>/dev/null
    ln -sf "$source" "$target"
    echo "  ✓ Symlinked: $path"
}

# Extract a flag value from PWT_ARGS
# Usage (in Pwtfile): pwt_arg <flag>
# Returns: flag value, "true" for boolean flags, "" if not found (exit 1)
# Examples:
#   PWT_ARGS="-p 5002 --sidekiq -e staging"
#   pwt_arg -p        → "5002"
#   pwt_arg --sidekiq → "true"
#   pwt_arg --foo     → "" (exit 1)
#   pwt_arg --env=staging → "staging"
pwt_arg() {
    local flag="$1"
    local args="${PWT_ARGS:-}"
    local found=false

    for arg in $args; do
        if [ "$found" = true ]; then
            # Previous arg was our flag; if next starts with -, flag was boolean
            if [[ "$arg" == -* ]]; then
                echo "true"
                return 0
            fi
            echo "$arg"
            return 0
        fi
        # Handle --flag=value syntax
        if [[ "$arg" == "${flag}="* ]]; then
            echo "${arg#*=}"
            return 0
        fi
        if [ "$arg" = "$flag" ]; then
            found=true
        fi
    done

    # Flag was last arg (boolean)
    if [ "$found" = true ]; then
        echo "true"
        return 0
    fi

    # Not found
    return 1
}

# Copy from main app to current worktree
# Usage: pwtfile_copy <path>
# Example: pwtfile_copy ".env"
pwtfile_copy() {
    local path="$1"
    local source="$MAIN_APP/$path"
    local target="$PWD/$path"

    if [ ! -e "$source" ]; then
        echo "  ! Source not found: $source"
        return 1
    fi

    if [ -d "$source" ]; then
        cp -r "$source" "$target"
    else
        cp "$source" "$target"
    fi
    echo "  ✓ Copied: $path"
}

# ============================================================
# List display helpers
# ============================================================

# Format relative time from Unix timestamp or git date
# Usage: format_relative_time <timestamp_or_git_date>
# Output: "30m", "4h", "2d", "3w", "5M"
format_relative_time() {
    local input="$1"
    local now=$(date +%s)
    local timestamp

    # Handle git date format or Unix timestamp
    if [[ "$input" =~ ^[0-9]+$ ]]; then
        timestamp="$input"
    else
        timestamp=$(date -j -f "%Y-%m-%d %H:%M:%S %z" "$input" +%s 2>/dev/null || \
                   date -d "$input" +%s 2>/dev/null || \
                   echo "$now")
    fi

    local diff=$((now - timestamp))

    if [ $diff -lt 60 ]; then
        echo "now"
    elif [ $diff -lt 3600 ]; then
        echo "$((diff / 60))m"
    elif [ $diff -lt 86400 ]; then
        echo "$((diff / 3600))h"
    elif [ $diff -lt 604800 ]; then
        echo "$((diff / 86400))d"
    elif [ $diff -lt 2592000 ]; then
        echo "$((diff / 604800))w"
    else
        echo "$((diff / 2592000))M"
    fi
}

# Get git status symbols
# Usage: get_status_symbols <dir>
# Output: "+!?" format (+ staged, ! modified, ? untracked)
get_status_symbols() {
    local dir="$1"
    local symbols=""

    local staged=$(git -C "$dir" diff --cached --numstat 2>/dev/null | wc -l | tr -d ' ')
    local modified=$(git -C "$dir" diff --numstat 2>/dev/null | wc -l | tr -d ' ')
    local untracked=$(git -C "$dir" ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')

    [ "$staged" -gt 0 ] && symbols="${symbols}+"
    [ "$modified" -gt 0 ] && symbols="${symbols}!"
    [ "$untracked" -gt 0 ] && symbols="${symbols}?"

    echo "$symbols"
}

# Get divergence from a ref (ahead/behind)
# Usage: get_divergence <dir> <ref>
# Output: "↑3" or "↓2" or "↑3↓2" or ""
get_divergence() {
    local dir="$1"
    local ref="$2"
    local result=""

    local ahead=$(git -C "$dir" rev-list --count "HEAD...$ref" --left-only 2>/dev/null || echo "0")
    local behind=$(git -C "$dir" rev-list --count "HEAD...$ref" --right-only 2>/dev/null || echo "0")

    # Remove whitespace
    ahead=$(echo "$ahead" | tr -d ' ')
    behind=$(echo "$behind" | tr -d ' ')

    [ "$ahead" -gt 0 ] && result="↑${ahead}"
    [ "$behind" -gt 0 ] && result="${result}↓${behind}"

    echo "$result"
}

# Get remote divergence (vs upstream)
# Usage: get_remote_divergence <dir>
# Output: "⇡3" or "⇣2" or "⇡3⇣2" or ""
get_remote_divergence() {
    local dir="$1"
    local branch=$(git -C "$dir" branch --show-current 2>/dev/null)
    [ -z "$branch" ] && return

    local upstream=$(git -C "$dir" rev-parse --abbrev-ref "${branch}@{upstream}" 2>/dev/null)
    [ -z "$upstream" ] && return

    local result=""
    local ahead=$(git -C "$dir" rev-list --count "${upstream}..HEAD" 2>/dev/null || echo "0")
    local behind=$(git -C "$dir" rev-list --count "HEAD..${upstream}" 2>/dev/null || echo "0")

    # Remove whitespace
    ahead=$(echo "$ahead" | tr -d ' ')
    behind=$(echo "$behind" | tr -d ' ')

    [ "$ahead" -gt 0 ] && result="⇡${ahead}"
    [ "$behind" -gt 0 ] && result="${result}⇣${behind}"

    echo "$result"
}

# Check if path is the previous worktree
# Usage: is_previous_worktree <path>
# Returns: 0 if previous, 1 otherwise
is_previous_worktree() {
    local path="$1"
    [ -n "${PWT_PREVIOUS_PATH:-}" ] && [ "$PWT_PREVIOUS_PATH" = "$path" ]
}

# Calculate visual width of string (accounting for multi-byte Unicode)
# Usage: visual_width <string>
visual_width() {
    local str="$1"
    # Use wc -m to count characters (not bytes)
    # Preserve current locale or fallback to UTF-8 for proper Unicode handling
    local lc="${LC_ALL:-${LC_CTYPE:-${LANG:-C.UTF-8}}}"
    printf "%s" "$str" | LC_ALL="$lc" wc -m | tr -d ' '
}

# Pad string to fixed visual width
# Usage: pad_visual <string> <width>
pad_visual() {
    local str="$1"
    local width="$2"
    local visual=$(visual_width "$str")
    printf "%s" "$str"
    local i
    for ((i=visual; i<width; i++)); do
        printf " "
    done
}

# Get short commit hash
# Usage: get_short_hash <dir>
get_short_hash() {
    local dir="$1"
    git -C "$dir" rev-parse --short HEAD 2>/dev/null || echo "?"
}

# Get base branch from metadata or detect from reflog
# Usage: get_base_branch <worktree_name> <dir>
get_base_branch() {
    local name="$1"
    local dir="$2"

    # First try metadata
    local base=$(get_metadata "$name" "base")
    if [ -n "$base" ]; then
        echo "$base"
        return
    fi

    # Fallback to default branch
    echo "${DEFAULT_BRANCH:-master}"
}

# Hash-based port generation (deterministic)
# Usage: pwtfile_hash_port [name] [base]
# Output: port number
pwtfile_hash_port() {
    local name="${1:-${PWT_WORKTREE:-}}"
    local base="${2:-${PORT_BASE:-5000}}"

    # Hash the name and take first 4 hex chars for offset 0-65535
    # macOS uses 'md5 -q', Linux uses 'md5sum'
    local hash
    if command -v md5sum >/dev/null 2>&1; then
        hash=$(printf "%s" "$name" | md5sum | awk '{print $1}')
    else
        hash=$(printf "%s" "$name" | md5 -q)
    fi
    local offset=$((16#${hash:0:4} % 1000))

    echo $((base + offset))
}

# ============================================================
# End list display helpers
# ============================================================

# Read PORT_BASE from Pwtfile (if defined)
# Sets BASE_PORT global variable
read_port_base() {
    local pwtfile=""

    # Check project config for custom pwtfile path first
    local config_pwtfile=$(get_project_config "$CURRENT_PROJECT" "pwtfile")
    if [ -n "$config_pwtfile" ]; then
        config_pwtfile="${config_pwtfile/#\~/$HOME}"
        [[ "$config_pwtfile" != /* ]] && config_pwtfile="$MAIN_APP/$config_pwtfile"
        [ -f "$config_pwtfile" ] && pwtfile="$config_pwtfile"
    fi

    # Fallback to default location
    if [ -z "$pwtfile" ] && [ -n "$MAIN_APP" ] && [ -f "$MAIN_APP/Pwtfile" ]; then
        pwtfile="$MAIN_APP/Pwtfile"
    fi

    [ -z "$pwtfile" ] && return 0

    # Extract PORT_BASE from Pwtfile (bash only)
    local port_base=$(grep -E "^PORT_BASE=" "$pwtfile" 2>/dev/null | head -1 | cut -d= -f2)

    if [ -n "$port_base" ] && [[ "$port_base" =~ ^[0-9]+$ ]]; then
        # Pwtfile PORT_BASE means "first worktree port"
        # Internal BASE_PORT means "main app port" (worktrees start at BASE_PORT+1)
        # So if PORT_BASE=5001 (first worktree), BASE_PORT=5000 (main app)
        BASE_PORT=$((port_base - 1))
    fi
}

# Execute a single Pwtfile
# Arguments: pwtfile_path phase label
run_single_pwtfile() {
    local pwtfile="$1"
    local phase="$2"
    local label="$3"

    [ ! -f "$pwtfile" ] && return 0

    # Check if function exists before printing/running
    if ! grep -qE "^${phase}\s*\(\)|^function\s+${phase}\b" "$pwtfile" 2>/dev/null; then
        return 0  # Function doesn't exist, skip silently
    fi

    # --bg: delegate to background execution
    if [ "$PWT_BG" = true ]; then
        _run_pwtfile_bg "$pwtfile" "$phase" "$label"
        return $?
    fi

    echo -e "${BLUE}Running $label ($phase)...${NC}"
    if [ "$PWT_NO_INPUT" = true ]; then
        export PWT_AGENT=1
        (
            cd "$PWT_WORKTREE_PATH"
            # Make helpers available
            env() { pwtfile_env "$@"; }
            replace() { pwtfile_replace "$@"; }
            replace_literal() { pwtfile_replace_literal "$@"; }
            replace_re() { pwtfile_replace_re "$@"; }
            database() { pwtfile_database "$@"; }
            rake() { pwtfile_rake "$@"; }
            run() { pwtfile_run "$@"; }

            source "$pwtfile"

            # Call the phase function if it exists
            if type "$phase" &>/dev/null; then
                "$phase"
            fi
        ) </dev/null
    else
        (
            cd "$PWT_WORKTREE_PATH"
            # Make helpers available
            env() { pwtfile_env "$@"; }
            replace() { pwtfile_replace "$@"; }
            replace_literal() { pwtfile_replace_literal "$@"; }
            replace_re() { pwtfile_replace_re "$@"; }
            database() { pwtfile_database "$@"; }
            rake() { pwtfile_rake "$@"; }
            run() { pwtfile_run "$@"; }

            source "$pwtfile"

            # Call the phase function if it exists
            if type "$phase" &>/dev/null; then
                "$phase"
            fi
        )
    fi
    echo -e "  ${GREEN}✓${NC} $label ($phase) completed"
}

# Run a Pwtfile phase in the background as a daemon
# Uses perl double-fork + setsid to fully detach from parent
_run_pwtfile_bg() {
    local pwtfile="$1"
    local phase="$2"
    local label="$3"

    load_module jobs
    local worktree="${PWT_WORKTREE:-unknown}"
    local project="${PWT_PROJECT:-unknown}"

    # Check for duplicate
    local dup_id
    if dup_id=$(check_duplicate_job "$worktree" "$phase"); then
        pwt_error "Already running: $phase for $worktree (job: $dup_id)"
        pwt_error "Stop with: pwt jobs stop $dup_id"
        return $EXIT_CONFLICT
    fi

    local job_id=$(_generate_job_id "$worktree" "$phase")
    local log_file="$PWT_JOBS_DIR/${job_id}.log"
    local pid_file="$PWT_JOBS_DIR/${job_id}.pid"
    _init_jobs_dir

    # Build a self-contained bash script for background execution
    local bg_script="$PWT_JOBS_DIR/${job_id}.sh"
    cat > "$bg_script" << BGEOF
#!/usr/bin/env bash
export PWT_PORT='${PWT_PORT:-}'
export PWT_WORKTREE='${PWT_WORKTREE:-}'
export PWT_WORKTREE_PATH='${PWT_WORKTREE_PATH:-}'
export PWT_BRANCH='${PWT_BRANCH:-}'
export PWT_TICKET='${PWT_TICKET:-}'
export PWT_PROJECT='${PWT_PROJECT:-}'
export PWT_ARGS='${PWT_ARGS:-}'
export PWT_AGENT='${PWT_AGENT:-0}'
export MAIN_APP='${MAIN_APP:-}'
pwt_arg() {
    local flag="\$1"
    local args="\${PWT_ARGS:-}"
    local found=false
    for arg in \$args; do
        if [ "\$found" = true ]; then
            if [[ "\$arg" == -* ]]; then echo "true"; return 0; fi
            echo "\$arg"; return 0
        fi
        if [[ "\$arg" == "\${flag}="* ]]; then echo "\${arg#*=}"; return 0; fi
        if [ "\$arg" = "\$flag" ]; then found=true; fi
    done
    if [ "\$found" = true ]; then echo "true"; return 0; fi
    return 1
}
cd '${PWT_WORKTREE_PATH}' || exit 1
source '${pwtfile}'
type '${phase}' &>/dev/null && '${phase}'
BGEOF
    chmod +x "$bg_script"

    # Use perl double-fork daemon pattern
    # This creates a truly detached process that won't be waited on by parent
    perl -e '
        use POSIX qw(setsid);
        my $pid = fork();
        if ($pid == 0) {
            setsid();
            open(STDIN, "<", "/dev/null");
            open(STDOUT, ">", $ARGV[1]);
            open(STDERR, ">&STDOUT");
            exec("/bin/bash", $ARGV[0]) or die "exec failed: $!";
        }
        open(my $fh, ">", $ARGV[2]);
        print $fh "$pid\n";
        close($fh);
    ' "$bg_script" "$log_file" "$pid_file"

    # Read the PID from the file
    local bg_pid=""
    if [ -f "$pid_file" ]; then
        bg_pid=$(cat "$pid_file")
        rm -f "$pid_file"
    fi

    if [ -z "$bg_pid" ]; then
        pwt_error "Background job failed to start (no PID)"
        rm -f "$bg_script" 2>/dev/null
        return $EXIT_ERROR
    fi

    # Grace period: verify startup
    sleep 2
    if kill -0 "$bg_pid" 2>/dev/null; then
        local pgid
        pgid=$(ps -o pgid= -p "$bg_pid" 2>/dev/null | tr -d ' ')
        _save_job "$job_id" "$bg_pid" "${pgid:-$bg_pid}" "$phase" "$worktree" "$project" "$log_file"

        if [ "$PWT_QUIET" = true ]; then
            echo "{\"job_id\":\"$job_id\",\"pid\":$bg_pid,\"log\":\"$log_file\"}"
        else
            echo -e "${GREEN}✓${NC} Background job started"
            echo "{\"job_id\":\"$job_id\",\"pid\":$bg_pid,\"log\":\"$log_file\"}"
            echo ""
            echo "  pwt jobs logs $job_id       # view output"
            echo "  pwt jobs logs $job_id -f    # follow output"
            echo "  pwt jobs stop $job_id       # stop"
        fi
    else
        pwt_error "Background job failed to start"
        echo "Last output:" >&2
        tail -20 "$log_file" 2>/dev/null >&2 || echo "(no output)" >&2
        rm -f "$PWT_JOBS_DIR/${job_id}.json" 2>/dev/null
        return $EXIT_ERROR
    fi
}

# Check if a command exists in a specific file
# Usage: has_command_in_file <file> <command>
has_command_in_file() {
    local file="$1"
    local cmd="$2"
    [ -f "$file" ] || return 1
    # Match: "cmd() {" or "function cmd {"
    grep -qE "^${cmd}\s*\(\)|^function\s+${cmd}\b" "$file" 2>/dev/null
}

# Get project Pwtfile path
# Usage: get_project_pwtfile
get_project_pwtfile() {
    detect_project

    local project_pwtfile=""
    local config_pwtfile=$(get_project_config "$CURRENT_PROJECT" "pwtfile")

    if [ -n "$config_pwtfile" ]; then
        config_pwtfile="${config_pwtfile/#\~/$HOME}"
        [[ "$config_pwtfile" != /* ]] && config_pwtfile="$MAIN_APP/$config_pwtfile" || true
        [ -f "$config_pwtfile" ] && project_pwtfile="$config_pwtfile" || true
    fi

    if [ -z "$project_pwtfile" ]; then
        [ -f "$MAIN_APP/Pwtfile" ] && project_pwtfile="$MAIN_APP/Pwtfile" || true
    fi

    echo "$project_pwtfile"
}

# Check if a command exists in project OR global Pwtfile
# Usage: has_pwtfile_command <command>
has_pwtfile_command() {
    local cmd="$1"
    local project_pwtfile=$(get_project_pwtfile)
    local global_pwtfile="$PWT_DIR/Pwtfile"

    # Check project first, then global
    has_command_in_file "$project_pwtfile" "$cmd" && return 0
    has_command_in_file "$global_pwtfile" "$cmd" && return 0
    return 1
}

# Run a Pwtfile command in current worktree context
# Usage: run_pwtfile_command <command> [args...]
run_pwtfile_command() {
    local cmd="$1"
    shift

    # Set up worktree context from current directory
    # Resolve symlinks for consistent comparison (e.g., /var -> /private/var on macOS)
    local current_dir=$(pwd -P)
    local resolved_worktrees_dir=""
    local resolved_main_app=""

    if [ -n "$WORKTREES_DIR" ] && [ -d "$WORKTREES_DIR" ]; then
        resolved_worktrees_dir=$(cd "$WORKTREES_DIR" 2>/dev/null && pwd -P)
    fi
    if [ -n "$MAIN_APP" ] && [ -d "$MAIN_APP" ]; then
        resolved_main_app=$(cd "$MAIN_APP" 2>/dev/null && pwd -P)
    fi

    # Check if we're in a worktree or main app
    if [ -n "$resolved_worktrees_dir" ] && [[ "$current_dir" == "$resolved_worktrees_dir"/* ]]; then
        PWT_WORKTREE=$(basename "$current_dir")
        PWT_WORKTREE_PATH="$current_dir"
    elif [ -n "$resolved_main_app" ] && [[ "$current_dir" == "$resolved_main_app"* ]]; then
        PWT_WORKTREE="@"
        PWT_WORKTREE_PATH="$resolved_main_app"
    else
        # Default to main app
        PWT_WORKTREE="@"
        PWT_WORKTREE_PATH="${resolved_main_app:-$MAIN_APP}"
    fi

    # Get port and other metadata
    if [ "$PWT_WORKTREE" != "@" ]; then
        PWT_PORT=$(get_metadata "$PWT_WORKTREE" "port")
        PWT_BRANCH=$(get_metadata "$PWT_WORKTREE" "branch")
    else
        PWT_PORT=""
        PWT_BRANCH=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "master")
    fi

    # Pass arguments to the command via environment (strip --bg/--no-input)
    export PWT_ARGS="$(_strip_pwt_execution_flags "$@")"

    run_pwtfile "$cmd"
}

# Run Pwtfile commands with scope-aware behavior:
# - setup/teardown: run BOTH project AND global (additive)
# - server: run ONLY project (local only)
# - custom commands: run project if exists, else global (fallback)
run_pwtfile() {
    local phase="$1"

    # Export canonical PWT_* variables (always available)
    export PWT_PORT="${PWT_PORT:-}"
    export PWT_WORKTREE="${PWT_WORKTREE:-}"
    export PWT_WORKTREE_PATH="${PWT_WORKTREE_PATH:-}"
    export PWT_BRANCH="${PWT_BRANCH:-}"
    export PWT_TICKET="${PWT_TICKET:-}"
    export PWT_PROJECT="${PWT_PROJECT:-$CURRENT_PROJECT}"
    export PWT_ARGS="${PWT_ARGS:-}"
    export PWT_AGENT="${PWT_AGENT:-0}"
    export MAIN_APP="${MAIN_APP:-}"

    # Back-compat aliases (short names)
    export PORT="$PWT_PORT"
    export WORKTREE="$PWT_WORKTREE"
    export WORKTREE_PATH="$PWT_WORKTREE_PATH"
    export BRANCH="$PWT_BRANCH"
    export TICKET="$PWT_TICKET"
    export PROJECT="$PWT_PROJECT"

    # Find project Pwtfile
    local project_pwtfile=""
    local config_pwtfile=$(get_project_config "$CURRENT_PROJECT" "pwtfile")

    if [ -n "$config_pwtfile" ]; then
        config_pwtfile="${config_pwtfile/#\~/$HOME}"
        [[ "$config_pwtfile" != /* ]] && config_pwtfile="$MAIN_APP/$config_pwtfile" || true
        [ -f "$config_pwtfile" ] && project_pwtfile="$config_pwtfile" || true
    fi

    if [ -z "$project_pwtfile" ] && [ -f "$MAIN_APP/Pwtfile" ]; then
        project_pwtfile="$MAIN_APP/Pwtfile"
    fi

    local global_pwtfile="$PWT_DIR/Pwtfile"

    case "$phase" in
        setup|teardown)
            # Additive: run BOTH project and global
            [ -n "$project_pwtfile" ] && run_single_pwtfile "$project_pwtfile" "$phase" "Pwtfile" || true
            [ -f "$global_pwtfile" ] && run_single_pwtfile "$global_pwtfile" "$phase" "Global Pwtfile" || true
            ;;
        server)
            # Local only: server is project-specific
            [ -n "$project_pwtfile" ] && run_single_pwtfile "$project_pwtfile" "$phase" "Pwtfile" || true
            ;;
        *)
            # Custom commands: project > global (fallback)
            if [ -n "$project_pwtfile" ] && has_command_in_file "$project_pwtfile" "$phase"; then
                run_single_pwtfile "$project_pwtfile" "$phase" "Pwtfile"
            elif [ -f "$global_pwtfile" ] && has_command_in_file "$global_pwtfile" "$phase"; then
                run_single_pwtfile "$global_pwtfile" "$phase" "Global Pwtfile"
            fi
            ;;
    esac
}

# Run project hook if it exists
# Usage: run_hook <hook_name>
# Environment variables passed to hooks:
#   PWT_PROJECT      - Project name
#   PWT_WORKTREE     - Worktree name (directory name)
#   PWT_WORKTREE_PATH - Full path to worktree
#   PWT_BRANCH       - Git branch name
#   PWT_PORT         - Allocated port
#   PWT_TICKET       - Ticket/worktree name
#   PWT_BASE         - Base branch
#   PWT_DESC         - Description
run_hook() {
    local hook_name="$1"
    local hook_file="$PWT_PROJECTS_DIR/$CURRENT_PROJECT/hooks/$hook_name"

    [ -x "$hook_file" ] || return 0

    echo -e "${BLUE}Running $hook_name hook...${NC}"

    # Export all PWT_* variables for the hook
    export PWT_PROJECT="${PWT_PROJECT:-$CURRENT_PROJECT}"
    export PWT_WORKTREE="${PWT_WORKTREE:-}"
    export PWT_WORKTREE_PATH="${PWT_WORKTREE_PATH:-}"
    export PWT_BRANCH="${PWT_BRANCH:-}"
    export PWT_PORT="${PWT_PORT:-}"
    export PWT_TICKET="${PWT_TICKET:-}"
    export PWT_BASE="${PWT_BASE:-}"
    export PWT_DESC="${PWT_DESC:-}"

    if "$hook_file"; then
        echo -e "  ${GREEN}✓${NC} $hook_name hook completed"
        return 0
    else
        echo -e "  ${YELLOW}⚠${NC} $hook_name hook failed (exit $?)"
        return 1
    fi
}

# Initialize project config directory
# Usage: init_project <project_name>
init_project() {
    local project="$1"
    local project_dir="$PROJECTS_DIR/$project"

    if [ ! -d "$project_dir" ]; then
        mkdir -p "$project_dir/hooks"
        cat > "$project_dir/config.json" << 'EOF'
{
  "name": "PROJECT_NAME",
  "main_app": "",
  "worktrees_dir": "",
  "branch_prefix": "",
  "post_create_commands": [],
  "pre_remove_commands": []
}
EOF
        sed_inplace "s/PROJECT_NAME/$project/" "$project_dir/config.json"
        echo -e "${GREEN}✓ Created project config: $project_dir${NC}"
    fi
}

# Extract worktree name from branch
# Removes path prefix (feature/, user/, etc) and sanitizes for directory use
extract_worktree_name() {
    local branch="$1"
    echo "$branch" | sed -E 's|.*/||; s|[^A-Za-z0-9._-]|-|g'
}

# Alias for backwards compatibility (was pair, now single port)
# Projects needing multiple ports should derive them in Pwtfile (e.g., VITE_PORT=$((PWT_PORT+1)))
is_port_pair_free() {
    is_port_free "$1"
}

# Find next available port
# Strategy: try to reuse ports from removed worktrees before incrementing
# Note: BASE_PORT is reserved for main app, worktrees start at BASE_PORT+1
#       (Pwtfile PORT_BASE defines first worktree port, see read_port_base)
next_available_port() {
    # Collect all ports "allocated" by existing worktrees
    local -a allocated_ports=()
    local project="${CURRENT_PROJECT:-unknown}"

    # BASE_PORT is reserved for main app (worktrees use BASE_PORT+1, +2, etc.)
    allocated_ports+=("$BASE_PORT")

    # Read ports from metadata for current project (primary source)
    if [ -f "$METADATA_FILE" ]; then
        while IFS= read -r port; do
            if [[ "$port" =~ ^[0-9]+$ ]]; then
                allocated_ports+=("$port")
            fi
        done < <(jq -r --arg project "$project" '.[$project] // {} | .[].port // empty' "$METADATA_FILE" 2>/dev/null)
    fi

    # Also scan directory names (fallback for legacy worktrees without metadata)
    if [ -d "$WORKTREES_DIR" ]; then
        for dir in "$WORKTREES_DIR"/*/; do
            if [ -d "$dir" ]; then
                local dirname=$(basename "$dir")
                # Only extract port if it looks like old format (ends with -XXXX)
                if [[ "$dirname" =~ -([0-9]{4})$ ]]; then
                    local port="${BASH_REMATCH[1]}"
                    allocated_ports+=("$port")
                fi
            fi
        done
    fi

    # Find next free port, starting from BASE_PORT+1
    # Try to reuse "holes" from removed worktrees
    local candidate=$((BASE_PORT + 1))
    local max_attempts=100  # Avoid infinite loop

    for ((i=0; i<max_attempts; i++)); do
        local is_allocated=false

        # Check if port is allocated by existing worktree
        for allocated in "${allocated_ports[@]}"; do
            if [ "$candidate" -eq "$allocated" ]; then
                is_allocated=true
                break
            fi
        done

        if [ "$is_allocated" = false ]; then
            # Check if port (and Vite +1) are actually free on the system
            if is_port_pair_free "$candidate"; then
                echo "$candidate"
                return 0
            fi
        fi

        candidate=$((candidate + 1))
    done

    # Fallback: return next after all allocated
    local max_port=$BASE_PORT
    for allocated in "${allocated_ports[@]}"; do
        if [ "$allocated" -gt "$max_port" ]; then
            max_port=$allocated
        fi
    done
    echo $((max_port + 1))
}

# Check if server is running in a worktree (generic - checks port, not pidfile)
check_server_status() {
    local dir="$1"
    local port="${2:-}"

    # If port provided, check if it's in use
    if [ -n "$port" ] && [[ "$port" =~ ^[0-9]+$ ]]; then
        local pids=$(get_pids_on_port "$port")
        if [ -n "$pids" ]; then
            echo -e "${GREEN}[running]${NC} port $port"
            return
        fi
    fi
    echo -e "${YELLOW}[stopped]${NC}"
}


# Command: create
# Usage: pwt create <branch> [base-ref] [description] [options]
# Options:
#   --dry-run, -n     Show what would be created without creating
#   -e, --editor      Open editor after creating
#   -a, --ai          Start AI tool after creating
#   --from <ref>      Create from specific ref (tag, commit, branch)
#   --from-current    Create from current branch


# Command: server
# Start the server using Pwtfile's server() function
# Usage: cmd_server [worktree]
cmd_server() {
    # Parse server-specific flags (--bg, --no-input, --help)
    # Separate worktree arg from pwtfile args (like --sidekiq)
    local worktree_arg=""
    local pwtfile_args=""
    local pwtfile_first=true
    while [ $# -gt 0 ]; do
        case "$1" in
            -h|--help)
                echo "Usage: pwt server|s [worktree] [--bg] [--no-input]"
                echo ""
                echo "Start development server for a worktree."
                echo ""
                echo "Arguments:"
                echo "  worktree        Worktree name (default: current worktree or symlink)"
                echo ""
                echo "Options:"
                echo "  --bg            Run server in background (daemonize)"
                echo "  --no-input      Close stdin and set PWT_AGENT=1"
                echo "  -h, --help      Show this help"
                echo ""
                echo "Detection order:"
                echo "  1. Argument provided: pwt server ACME-1234-50XX"
                echo "  2. Inside worktree directory"
                echo "  3. Current symlink set via 'pwt use'"
                echo ""
                echo "Server runs on port from worktree metadata (usually 50XX)."
                return 0
                ;;
            --bg)       PWT_BG=true; shift ;;
            --no-input) PWT_NO_INPUT=true; shift ;;
            -*)
                # Flags like --sidekiq go to pwtfile args
                local flag="$1"
                shift
                if [ "$pwtfile_first" = true ]; then
                    pwtfile_args="$flag"
                    pwtfile_first=false
                else
                    pwtfile_args="$pwtfile_args $flag"
                fi
                # If flag has --key=value, value is already included
                # Otherwise, peek at next arg: if it's not a flag, it's the value
                if [[ "$flag" != *"="* ]] && [ $# -gt 0 ] && [[ "$1" != -* ]]; then
                    pwtfile_args="$pwtfile_args $1"
                    shift
                fi
                ;;
            *)
                if [ -z "$worktree_arg" ]; then
                    worktree_arg="$1"
                else
                    # Extra positional args go to pwtfile args
                    if [ "$pwtfile_first" = true ]; then
                        pwtfile_args="$1"
                        pwtfile_first=false
                    else
                        pwtfile_args="$pwtfile_args $1"
                    fi
                fi
                shift
                ;;
        esac
    done

    # Pass pwtfile args via PWT_ARGS
    if [ -n "$pwtfile_args" ]; then
        export PWT_ARGS="$pwtfile_args"
    fi

    local arg="$worktree_arg"
    local current_dir=$(pwd)
    local worktree_name=""
    local worktree_path=""
    local via_symlink=false
    local via_argument=false

    # 0. Check if worktree name provided as argument
    if [ -n "$arg" ]; then
        # Handle @ (main app)
        if [ "$arg" = "@" ]; then
            worktree_name="@"
            worktree_path="$MAIN_APP"
            via_argument=true
        else
            worktree_path="$WORKTREES_DIR/$arg"
            # Try partial match if not found
            if [ ! -d "$worktree_path" ]; then
                local match=$(cmd_cd "$arg" 2>/dev/null)
                if [ -n "$match" ] && [ -d "$match" ]; then
                    worktree_path="$match"
                fi
            fi
            if [ ! -d "$worktree_path" ]; then
                pwt_error "Error: Worktree not found: $arg"
                exit 1
            fi
            worktree_name=$(basename "$worktree_path")
            via_argument=true
        fi
    # 1. Check if inside a worktree directly
    elif [[ "$current_dir" == "$WORKTREES_DIR"/* ]]; then
        worktree_name=$(basename "$current_dir")
        worktree_path="$current_dir"
    # 2. Check if inside main app directory
    elif [[ "$current_dir" == "$MAIN_APP"* ]]; then
        worktree_name="@"
        worktree_path="$MAIN_APP"
    # 3. Check if current symlink is set
    elif worktree_name=$(get_current_from_symlink 2>/dev/null); then
        # @ = main app
        if [ "$worktree_name" = "@" ]; then
            worktree_path="$MAIN_APP"
        else
            worktree_path="$WORKTREES_DIR/$worktree_name"
            via_symlink=true
        fi
    else
        pwt_error "Error: Not inside a worktree and no current set"
        echo ""
        echo "Options:"
        echo "  pwt server <worktree>  # specify worktree"
        echo "  cd into a worktree directory"
        echo "  pwt use <worktree>  # set current first"
        exit 1
    fi

    local port=""
    if [ "$worktree_name" = "@" ]; then
        # Main app uses BASE_PORT
        port="${BASE_PORT:-5000}"
    else
        port=$(get_metadata "$worktree_name" "port")
        if [ -z "$port" ]; then
            echo -e "${YELLOW}Warning: Port not found in metadata${NC}"
            port="3000"
        fi
    fi

    # Warning if running via symlink
    if [ "$via_symlink" = true ]; then
        echo -e "${YELLOW}⚠️  Running server via symlink path${NC}"
        echo -e "${DIM}   → resolved to: $worktree_path${NC}"
        echo -e "${DIM}   Tip: Run from worktree path for better LSP/watcher support${NC}"
        echo ""
    fi

    # Set context for Pwtfile
    export PWT_WORKTREE="$worktree_name"
    export PWT_WORKTREE_PATH="$worktree_path"
    export PWT_PORT="$port"
    export PWT_PROJECT="$CURRENT_PROJECT"
    export MAIN_APP="$MAIN_APP"

    echo -e "${BLUE}Starting server on port $port...${NC}"

    # Pre-check for duplicate background jobs (run_pwtfile uses || true which swallows errors)
    if [ "$PWT_BG" = true ]; then
        load_module jobs
        local dup_id
        if dup_id=$(check_duplicate_job "$worktree_name" "server"); then
            pwt_error "Already running: server for $worktree_name (job: $dup_id)"
            pwt_error "Stop with: pwt jobs stop $dup_id"
            return $EXIT_CONFLICT
        fi
    fi

    # Actually cd to worktree path (for Pwtfile commands that use pwd)
    cd "$worktree_path" || exit 1

    # Run Pwtfile server phase
    run_pwtfile "server"
}

# Command: current
# Show current worktree info
# Works from ANYWHERE (uses symlink, not just pwd)
# Default output: SYMLINK path on stdout (pipe-friendly), context on stderr
# Use --resolved to get actual worktree path
# Usage: pwt current [--name|--port|--branch|--path|--resolved|--json]
cmd_current() {
    local format="default"
    local show_resolved=false

    # Parse arguments
    while [ $# -gt 0 ]; do
        case "$1" in
            -h|--help)
                echo "Usage: pwt current [options]"
                echo ""
                echo "Show the currently active worktree."
                echo ""
                echo "Options:"
                echo "  --name       Output only the worktree name"
                echo "  --port       Output only the port number"
                echo "  --branch     Output only the branch name"
                echo "  --path       Output only the worktree path"
                echo "  --json       Output full context as JSON"
                echo "  --resolved   Show resolved symlink path"
                echo ""
                echo "Detection order:"
                echo "  1. Current directory (if inside a worktree)"
                echo "  2. 'current' symlink (set via 'pwt use')"
                echo ""
                echo "Examples:"
                echo "  pwt current              # show current worktree"
                echo "  pwt current --port       # get port for scripts"
                echo "  pwt current --json       # full context as JSON"
                return 0
                ;;
            --name) format="name"; shift ;;
            --port) format="port"; shift ;;
            --branch) format="branch"; shift ;;
            --path) format="path"; shift ;;
            --resolved) show_resolved=true; shift ;;
            --json) format="json"; shift ;;
            *) shift ;;
        esac
    done

    # Detect current worktree (priority: symlink > pwd)
    local current_dir=$(pwd)
    local name=""
    local is_main=false
    local from_symlink=false

    # 1. Try symlink first (works from anywhere)
    if name=$(get_current_from_symlink 2>/dev/null); then
        from_symlink=true
        # @ means symlink points to main
        [ "$name" = "@" ] && is_main=true
    # 2. Fall back to pwd detection
    elif [[ "$current_dir" == "$WORKTREES_DIR"/* ]]; then
        name=$(basename "$current_dir")
    elif name=$(find_worktree_name_by_path "$current_dir" 2>/dev/null); then
        :
    elif [[ "$current_dir" == "$MAIN_APP"* ]]; then
        is_main=true
        name="@"
    # 3. If project was explicitly specified, default to main
    elif [ "$PROJECT_EXPLICIT" = true ] && [ -n "$MAIN_APP" ]; then
        is_main=true
        name="@"
    else
        # No symlink and not in a worktree
        if [ "$format" = "json" ]; then
            echo '{"in_worktree":false}'
        else
            echo -e "${YELLOW}Not in a worktree (and no current set)${NC}" >&2
            echo "Run 'pwt use <worktree>' to set current" >&2
        fi
        return 1
    fi

    # Get info
    local port=""
    local branch=""
    local resolved_path=""
    local symlink_path=$(get_current_symlink_path)
    local marker=""
    local desc=""

    if [ "$is_main" = true ]; then
        resolved_path="$MAIN_APP"
        branch=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "main")
    else
        resolved_path=$(get_worktree_path "$name" 2>/dev/null || echo "$WORKTREES_DIR/$name")
        port=$(get_metadata "$name" "port")
        marker=$(get_metadata "$name" "marker")
        desc=$(get_metadata "$name" "description")
        branch=$(git -C "$resolved_path" branch --show-current 2>/dev/null || echo "?")
    fi

    # Determine which path to output (symlink by default, resolved with --resolved)
    local output_path="$symlink_path"
    [ "$show_resolved" = true ] && output_path="$resolved_path"

    # Output based on format
    case "$format" in
        name)
            echo "$name"
            ;;
        port)
            echo "${port:-}"
            ;;
        branch)
            echo "$branch"
            ;;
        path)
            # --path respects --resolved flag
            echo "$output_path"
            ;;
        json)
            jq -n -c \
                --arg name "$name" \
                --arg branch "$branch" \
                --arg symlink_path "$symlink_path" \
                --arg resolved_path "$resolved_path" \
                --arg port "$port" \
                --arg marker "$marker" \
                --arg description "$desc" \
                --argjson from_symlink "$from_symlink" '
                {
                    in_worktree: true,
                    name: $name,
                    branch: $branch,
                    symlink_path: $symlink_path,
                    resolved_path: $resolved_path
                }
                + (if $port != "" then {port: ($port | tonumber)} else {} end)
                + (if $marker != "" then {marker: $marker} else {} end)
                + (if $description != "" then {description: $description} else {} end)
                + (if $from_symlink then {from_symlink: true} else {} end)
                '
            ;;
        default)
            # stdout=symlink path (stable), stderr=context (Unix-style)
            # Use --resolved to get actual worktree path
            echo "$output_path"
            if [ "$is_main" = true ]; then
                echo -e "current@${BLUE}@${NC} (main) on ${GREEN}$branch${NC}" >&2
            else
                local status=$(get_status_symbols "$resolved_path")
                local port_str=""
                [ -n "$port" ] && port_str=":${port}"
                local status_str=""
                [ -n "$status" ] && status_str=" [${status}]"
                echo -e "current@${BLUE}$name${NC} $port_str$status_str on ${GREEN}$branch${NC}" >&2
            fi
            ;;
    esac
}

# Save last-used worktree for a project
save_last_used() {
    local name="$1"
    local project="${CURRENT_PROJECT:-unknown}"
    local last_file="$PWT_DIR/projects/$project/last"
    mkdir -p "$(dirname "$last_file")"
    echo "$name" > "$last_file"
}

# Get last-used worktree for a project
get_last_used() {
    local project="${CURRENT_PROJECT:-unknown}"
    local last_file="$PWT_DIR/projects/$project/last"
    if [ -f "$last_file" ]; then
        cat "$last_file"
    fi
}

# Save previous worktree for "pwt -" navigation (like cd -)
save_previous() {
    local name="$1"
    local project="${CURRENT_PROJECT:-unknown}"
    local prev_file="$PWT_DIR/projects/$project/previous"
    mkdir -p "$(dirname "$prev_file")"
    echo "$name" > "$prev_file"
}

# Get previous worktree for "pwt -" navigation
get_previous() {
    local project="${CURRENT_PROJECT:-unknown}"
    local prev_file="$PWT_DIR/projects/$project/previous"
    if [ -f "$prev_file" ]; then
        cat "$prev_file"
    fi
}

# ============================================
# Current Symlink Functions (Capistrano-style)
# ============================================

# Get symlink path for current project
get_current_symlink_path() {
    local project="${CURRENT_PROJECT:-unknown}"
    echo "$PROJECTS_DIR/$project/current"
}

# Read current worktree from symlink
# Returns "@" if pointing to MAIN_APP, otherwise worktree name
get_current_from_symlink() {
    local symlink_path=$(get_current_symlink_path)
    [ -L "$symlink_path" ] || return 1
    local target=$(readlink "$symlink_path" 2>/dev/null) || return 1
    [ -d "$target" ] || return 1

    # Check if pointing to main app
    if [ "$target" = "$MAIN_APP" ]; then
        echo "@"
    else
        basename "$target"
    fi
}

get_worktree_path() {
    local name="$1"
    local meta_path=""

    [ -n "$name" ] || return 1
    if [ "$name" = "@" ]; then
        echo "$MAIN_APP"
        return 0
    fi

    meta_path=$(get_metadata "$name" "path" 2>/dev/null || true)
    if [ -n "$meta_path" ] && [ -d "$meta_path" ]; then
        echo "$meta_path"
        return 0
    fi

    if [ -d "$WORKTREES_DIR/$name" ]; then
        echo "$WORKTREES_DIR/$name"
        return 0
    fi

    return 1
}

find_worktree_name_by_path() {
    local search_path="$1"
    local project="${CURRENT_PROJECT:-unknown}"
    local resolved_search=""

    [ -n "$search_path" ] || return 1
    resolved_search=$(cd "$search_path" 2>/dev/null && pwd -P) || return 1

    if [ -d "$WORKTREES_DIR" ] && [[ "$resolved_search" == "$WORKTREES_DIR"/* ]]; then
        basename "$resolved_search"
        return 0
    fi

    init_metadata
    while IFS=$'\t' read -r name path; do
        [ -n "$name" ] && [ -n "$path" ] || continue
        [ -d "$path" ] || continue
        local resolved_path
        resolved_path=$(cd "$path" 2>/dev/null && pwd -P) || continue
        if [ "$resolved_search" = "$resolved_path" ] || [[ "$resolved_search" == "$resolved_path"/* ]]; then
            echo "$name"
            return 0
        fi
    done < <(jq -r --arg project "$project" '.[$project] // {} | to_entries[] | [.key, (.value.path // "")] | @tsv' "$METADATA_FILE" 2>/dev/null)

    return 1
}

list_known_worktree_entries() {
    local project="${CURRENT_PROJECT:-unknown}"
    local seen=""
    local dir name path

    if [ -d "$WORKTREES_DIR" ]; then
        for dir in "$WORKTREES_DIR"/*/; do
            [ -d "$dir" ] || continue
            name=$(basename "$dir")
            path="${dir%/}"
            printf '%s\t%s\n' "$name" "$path"
            seen="${seen}|${name}|"
        done
    fi

    init_metadata
    while IFS=$'\t' read -r name path; do
        [ -n "$name" ] && [ -n "$path" ] && [ -d "$path" ] || continue
        [[ "$seen" == *"|$name|"* ]] && continue
        printf '%s\t%s\n' "$name" "$path"
        seen="${seen}|${name}|"
    done < <(jq -r --arg project "$project" '.[$project] // {} | to_entries[] | [.key, (.value.path // "")] | @tsv' "$METADATA_FILE" 2>/dev/null)
}

# Update symlink atomically
set_current_worktree() {
    local name="$1"
    local symlink_path=$(get_current_symlink_path)
    local target_path
    target_path=$(get_worktree_path "$name") || {
        echo "Worktree not found: $name" >&2
        return 1
    }

    mkdir -p "$(dirname "$symlink_path")"
    ln -sfn "$target_path" "$symlink_path"
    save_last_used "$name"
}

# Clear current symlink
clear_current_symlink() {
    local symlink_path=$(get_current_symlink_path)
    rm -f "$symlink_path"
}

# ============================================
# Command: ps1
# ============================================
# Fast prompt helper - NO git, NO directory scanning
# Designed for shell prompts, status bars, tmux
# Usage: pwt ps1
#   Output: pwt@WORKTREE-NAME (or empty if no current)
#   Adds ! suffix if pwd differs from current (mismatch warning)
cmd_ps1() {
    # Fast path: read symlink directly
    local symlink_path="$PROJECTS_DIR/${CURRENT_PROJECT:-unknown}/current"

    # No symlink = no output
    [ -L "$symlink_path" ] || return 0

    local target=$(readlink "$symlink_path" 2>/dev/null) || return 0
    [ -d "$target" ] || return 0

    local name=$(basename "$target")

    # Check for mismatch: pwd is in a different worktree than current
    local mismatch=""
    local current_dir=$(pwd)
    local current_path
    current_path=$(get_worktree_path "$name" 2>/dev/null || echo "")
    if [ -n "$current_path" ] && { [ "$current_dir" = "$current_path" ] || [[ "$current_dir" == "$current_path"/* ]]; }; then
        :
    elif [ -d "$WORKTREES_DIR" ] && [[ "$current_dir" == "$WORKTREES_DIR"/* ]]; then
        local pwd_name=$(basename "$current_dir")
        if [ "$pwd_name" != "$name" ]; then
            mismatch="!"
        fi
    fi

    echo "pwt@${name}${mismatch}"
}

# Helper for pwt use --select (interactive picker)
# Usage: _use_interactive_select [query]
_use_interactive_select() {
    local initial_query="${1:-}"
    # Check if fzf is installed
    if ! command -v fzf &>/dev/null; then
        pwt_error "Error: fzf is required for pwt use --select"
        echo "Install with: brew install fzf"
        exit 1
    fi

    # Build enriched list of worktrees
    local items=()

    # Add main app
    if [ -d "$MAIN_APP" ]; then
        local main_branch=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "main")
        local main_status=$(get_status_symbols "$MAIN_APP")
        items+=("@|$main_branch|·|${main_status:-·}|·|main app")
    fi

    # Add worktrees with metadata
    while IFS=$'\t' read -r name dir; do
        [ -n "$name" ] && [ -d "$dir" ] || continue
        local branch=$(git -C "$dir" branch --show-current 2>/dev/null || echo "?")
        local port=$(get_metadata "$name" "port")
        local marker=$(get_metadata "$name" "marker")
        local desc=$(get_metadata "$name" "description")
        local status=$(get_status_symbols "$dir")

        [ -n "$desc" ] && desc="${desc:0:30}"
        items+=("$name|$branch|:${port:-·}|${status:-·}|${marker:-·}|${desc:-·}")
    done < <(list_known_worktree_entries)

    if [ ${#items[@]} -eq 0 ]; then
        echo -e "${YELLOW}No worktrees found${NC}"
        exit 0
    fi

    local formatted
    formatted=$(printf '%s\n' "${items[@]}" | column -t -s'|')

    # Build preview command
    local preview_cmd="name=\$(echo {} | awk '{print \$1}'); "
    preview_cmd+="if [ \"\$name\" = \"@\" ]; then "
    preview_cmd+="  echo '=== Main App ===' && git -C \"$MAIN_APP\" status -sb; "
    preview_cmd+="else "
    preview_cmd+="  pwt info \"\$name\" 2>/dev/null || echo \"Worktree: \$name\"; "
    preview_cmd+="fi"

    # Run fzf
    local selected
    selected=$(echo "$formatted" | fzf \
        --height=60% \
        --reverse \
        --ansi \
        --header=$'Select worktree to set as current\n↵:use  Esc:cancel' \
        --preview="$preview_cmd" \
        --preview-window=right:45%:wrap \
        ${initial_query:+--query="$initial_query"})

    if [ -z "$selected" ]; then
        exit 0
    fi

    # Extract worktree name and set as current
    local target=$(echo "$selected" | awk '{print $1}')
    cmd_use "$target"
}

# ============================================
# Command: use
# ============================================
# Switch the current symlink to a different worktree
# Usage: pwt use <worktree>
#        pwt use --select    # interactive fzf picker
#   Atomically swaps the symlink, does NOT:
#   - Open editor
#   - Kill processes
#   - Change shell directory
cmd_use() {
    local target=""
    local use_select=false

    # Parse arguments
    while [ $# -gt 0 ]; do
        case "$1" in
            -h|--help)
                echo "Usage: pwt use <worktree> [options]"
                echo ""
                echo "Switch the current worktree symlink."
                echo ""
                echo "Arguments:"
                echo "  worktree     Target worktree name (supports partial match)"
                echo "  @            Switch to main app"
                echo ""
                echo "Options:"
                echo "  -s, --select   Interactive picker (fzf)"
                echo ""
                echo "Examples:"
                echo "  pwt use TICKET-123     # switch by name"
                echo "  pwt use 123            # partial match"
                echo "  pwt use @              # switch to main app"
                echo "  pwt use --select       # interactive picker"
                return 0
                ;;
            -s|--select)
                use_select=true
                shift
                ;;
            -*)
                echo -e "${RED}Unknown option: $1${NC}" >&2
                exit 1
                ;;
            *)
                target="$1"
                shift
                ;;
        esac
    done

    # Interactive selection mode
    if [ "$use_select" = true ]; then
        _use_interactive_select "$target"
        return $?
    fi

    if [ -z "$target" ]; then
        echo -e "${RED}Usage: pwt use <worktree>${NC}" >&2
        echo "       pwt use --select    # interactive picker (fzf)" >&2
        echo "Switches the current symlink to point to the specified worktree" >&2
        exit 1
    fi

    # Normalize: strip trailing slash (from shell completion)
    target="${target%/}"

    # @ = main worktree (special case)
    if [ "$target" = "@" ]; then
        # Point symlink to main app (always have a current)
        local symlink_path=$(get_current_symlink_path)
        mkdir -p "$(dirname "$symlink_path")"
        ln -sfn "$MAIN_APP" "$symlink_path"
        save_last_used "@"
        local branch=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "?")
        echo -e "current → ${BLUE}@ (main)${NC} (branch ${GREEN}$branch${NC})"
        return 0
    fi

    # Check if it's a worktree name (exact match)
    local worktree_path
    worktree_path=$(get_worktree_path "$target" 2>/dev/null || echo "$WORKTREES_DIR/$target")
    local resolved_name="$target"

    if [ ! -d "$worktree_path" ]; then
        # Try partial match by directory name
        local matches=()
        if [ -d "$WORKTREES_DIR" ]; then
            for dir in "$WORKTREES_DIR"/*/; do
                local name=$(basename "$dir")
                if [[ "$name" == *"$target"* ]]; then
                    matches+=("$name")
                fi
            done
        fi

        # If no directory match, try matching by branch name
        if [ ${#matches[@]} -eq 0 ] && [ -d "$WORKTREES_DIR" ]; then
            local normalized_target="${target//\//-}"  # feature/foo → feature-foo
            for dir in "$WORKTREES_DIR"/*/; do
                local name=$(basename "$dir")
                local branch=$(git -C "$dir" branch --show-current 2>/dev/null)
                if [[ -n "$branch" ]] && [[ "$branch" == *"$target"* || "$branch" == *"$normalized_target"* ]]; then
                    matches+=("$name")
                fi
            done
        fi

        if [ ${#matches[@]} -eq 1 ]; then
            resolved_name="${matches[0]}"
            worktree_path=$(get_worktree_path "$resolved_name" 2>/dev/null || echo "$WORKTREES_DIR/$resolved_name")
        elif [ ${#matches[@]} -gt 1 ]; then
            echo -e "${RED}Multiple matches for '$target':${NC}" >&2
            for m in "${matches[@]}"; do
                echo "  $m" >&2
            done
            exit 1
        else
            # No worktree match - check if target matches main app's branch
            local main_branch=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null)
            if [[ -n "$main_branch" && "$main_branch" == *"$target"* ]]; then
                # Target matches main app branch - use @
                local symlink_path=$(get_current_symlink_path)
                mkdir -p "$(dirname "$symlink_path")"
                ln -sfn "$MAIN_APP" "$symlink_path"
                save_last_used "@"
                echo -e "current → ${BLUE}@ (main)${NC} (branch ${GREEN}$main_branch${NC})"
                return 0
            fi
            echo -e "${RED}Worktree not found: $target${NC}" >&2
            exit 1
        fi
    fi

    # Swap symlink
    set_current_worktree "$resolved_name" || exit 1

    # Output context
    local port=$(get_metadata "$resolved_name" "port")
    local branch=$(git -C "$worktree_path" branch --show-current 2>/dev/null || echo "?")
    echo -e "current → ${BLUE}$resolved_name${NC} (branch ${GREEN}$branch${NC})"
    [ -n "$port" ] && echo -e "         port ${CYAN}:$port${NC}"
    return 0
}

# Command: info
cmd_info() {
    local name="$1"

    # Handle --help
    if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
        echo "Usage: pwt info [worktree]"
        echo ""
        echo "Show detailed information about a worktree."
        echo ""
        echo "Arguments:"
        echo "  worktree  Name of the worktree (optional if inside one)"
        echo "  @         Main app"
        echo ""
        echo "Information shown:"
        echo "  - Branch and tracking information"
        echo "  - Assigned port number"
        echo "  - Server status"
        echo "  - Directory, mode, description, and created timestamp from metadata"
        echo "  - Creation metadata"
        echo "  - Git status (dirty files)"
        echo ""
        echo "Examples:"
        echo "  pwt info              # info for current worktree"
        echo "  pwt info TICKET-123   # info for specific worktree"
        echo "  pwt info @            # info for main app"
        return 0
    fi

    # If no name specified, try to detect from current directory
    if [ -z "$name" ]; then
        local current_dir=$(pwd)
        if [[ "$current_dir" == "$WORKTREES_DIR"/* ]]; then
            name=$(basename "$current_dir")
        elif [[ "$current_dir" == "$MAIN_APP" || "$current_dir" == "$MAIN_APP"/* ]]; then
            name="@"
        else
            name=$(find_worktree_name_by_path "$current_dir" 2>/dev/null || echo "")
            if [ -z "$name" ]; then
                pwt_error "Error: Worktree not specified"
                echo "Usage: pwt info <worktree>"
                echo "Or run from inside a worktree or main app"
                exit 1
            fi
        fi
    fi

    # Normalize: strip trailing slash (from shell completion)
    name="${name%/}"

    # Handle @ as main app
    local worktree_dir
    if [ "$name" = "@" ]; then
        worktree_dir="$MAIN_APP"
    else
        worktree_dir=$(get_worktree_path "$name" 2>/dev/null || echo "$WORKTREES_DIR/$name")
    fi

    if [ ! -d "$worktree_dir" ]; then
        pwt_error "Error: Worktree not found: $name"
        exit 1
    fi

    # Extract information
    local port=$(get_worktree_port "$name")
    [ -z "$port" ] && port="-"
    local ticket="$name"
    local branch=$(git -C "$worktree_dir" branch --show-current 2>/dev/null || echo "detached")
    local upstream=$(git -C "$worktree_dir" rev-parse --abbrev-ref "${branch}@{upstream}" 2>/dev/null || echo "-")
    local commit=$(git -C "$worktree_dir" rev-parse --short HEAD 2>/dev/null || echo "-")
    local commit_msg=$(git -C "$worktree_dir" log -1 --format='%s' 2>/dev/null | head -c 50)
    local mode=$(get_metadata "$name" "mode")
    local desc=$(get_metadata "$name" "description")
    local created=$(get_metadata "$name" "created_at")

    # Server status (check port, not pidfile)
    local server_status="${YELLOW}stopped${NC}"
    if [ -n "$port" ] && [[ "$port" =~ ^[0-9]+$ ]]; then
        local pids=$(get_pids_on_port "$port")
        if [ -n "$pids" ]; then
            local first_pid=$(echo "$pids" | head -1)
            server_status="${GREEN}running${NC} (PID $first_pid)"
        fi
    fi

    # Commits ahead/behind
    local ahead_behind=""
    if [ "$upstream" != "-" ]; then
        local ahead=$(git -C "$worktree_dir" rev-list --count "${upstream}..${branch}" 2>/dev/null || echo "0")
        local behind=$(git -C "$worktree_dir" rev-list --count "${branch}..${upstream}" 2>/dev/null || echo "0")
        if [ "$ahead" -gt 0 ] || [ "$behind" -gt 0 ]; then
            ahead_behind=" (↑${ahead} ↓${behind})"
        fi
    fi

    # Output
    echo ""
    echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
    echo -e "${YELLOW}$name${NC}"
    echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
    echo ""
    echo -e "  ${BLUE}Ticket:${NC}    $ticket"
    echo -e "  ${BLUE}Branch:${NC}    $branch"
    echo -e "  ${BLUE}Based on:${NC}  $upstream$ahead_behind"
    echo -e "  ${BLUE}Commit:${NC}    $commit - $commit_msg"
    echo ""
    echo -e "  ${BLUE}Port:${NC}      $port"
    echo -e "  ${BLUE}Server:${NC}    $server_status"
    echo -e "  ${BLUE}Directory:${NC} $worktree_dir"
    [ -n "$mode" ] && echo -e "  ${BLUE}Mode:${NC}      $mode"
    [ -n "$created" ] && echo -e "  ${BLUE}Created:${NC}   $created"
    [ -n "$desc" ] && echo -e "  ${BLUE}Desc:${NC}      $desc"
    echo ""

    # Show modified files if any
    local modified=$(git -C "$worktree_dir" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
    if [ "$modified" -gt 0 ]; then
        echo -e "  ${YELLOW}Modified files:${NC} $modified"
        git -C "$worktree_dir" status --porcelain 2>/dev/null | head -5 | sed 's/^/    /'
        if [ "$modified" -gt 5 ]; then
            echo "    ... and $((modified - 5)) more"
        fi
        echo ""
    fi
}

# Command: fix-port
# Reallocate a worktree to a new free port
cmd_fix_port() {
    local name="$1"

    if [[ "$name" == "-h" || "$name" == "--help" ]]; then
        echo "Usage: pwt fix-port [worktree]"
        echo ""
        echo "Resolve port conflicts for a worktree."
        echo ""
        echo "Arguments:"
        echo "  worktree   Target worktree (optional if inside one)"
        echo ""
        echo "When a port conflict is detected, offers:"
        echo "  - Kill the process using the port"
        echo "  - Reallocate to a new port"
        echo ""
        echo "Examples:"
        echo "  pwt fix-port              # fix port for current worktree"
        echo "  pwt fix-port TICKET-123   # fix port for specific worktree"
        return 0
    fi

    # Normalize: strip trailing slash (from shell completion)
    name="${name%/}"

    # If no name specified, try to detect from current directory
    if [ -z "$name" ]; then
        local current_dir=$(pwd)
        if [[ "$current_dir" == "$WORKTREES_DIR"/* ]]; then
            name=$(basename "$current_dir")
        else
            pwt_error "Error: Worktree name not specified"
            echo "Usage: pwt fix-port <worktree>"
            echo "Or run from inside a worktree"
            exit 1
        fi
    fi

    local worktree_dir="$WORKTREES_DIR/$name"

    if [ ! -d "$worktree_dir" ]; then
        pwt_error "Error: Worktree not found: $name"
        exit 1
    fi

    # Get current port from metadata, fallback to directory name
    local old_port=$(get_worktree_port "$name")

    if [ -z "$old_port" ] || ! [[ "$old_port" =~ ^[0-9]+$ ]]; then
        pwt_error "Error: Could not find port for worktree: $name"
        echo "Check metadata with: pwt meta show $name"
        exit 1
    fi

    # Check if current port is actually occupied
    if is_port_pair_free "$old_port"; then
        echo -e "${GREEN}Port $old_port is already free!${NC}"
        echo "No changes needed."
        exit 0
    fi

    # Analyze what's occupying the port
    local pids=$(get_pids_on_port "$old_port")
    local procs_info=""

    if has_lsof && [ -n "$pids" ]; then
        for pid in $pids; do
            local proc=$(ps -p "$pid" -o comm= 2>/dev/null || echo "?")
            procs_info="${procs_info}  PID $pid: $proc\n"
        done
    fi

    echo -e "${YELLOW}Port $old_port is occupied:${NC}"
    if [ -n "$procs_info" ]; then
        echo -e "$procs_info"
    else
        echo "  (could not identify processes)"
    fi
    echo ""

    echo -e "${BLUE}Options:${NC}"
    echo "  1) Kill processes and keep port $old_port"
    echo "  2) Reallocate to new port"
    echo "  3) Cancel"
    echo ""
    read -p "Choose (1/2/3): " -n 1 -r choice
    echo ""

    case "$choice" in
        1)
            if [ -n "$pids" ]; then
                echo ""
                echo -e "${YELLOW}Killing processes...${NC}"
                for pid in $pids; do
                    kill -9 "$pid" 2>/dev/null && echo "  ✓ PID $pid killed"
                done
                sleep 1

                # Check if freed
                if is_port_free "$old_port"; then
                    echo ""
                    echo -e "${GREEN}✓ Port $old_port freed!${NC}"
                    echo ""
                    echo "Now you can start the server:"
                    echo -e "  ${BLUE}pwt server${NC}"
                    exit 0
                else
                    echo -e "${RED}Port still occupied. Reallocating...${NC}"
                fi
            else
                echo "No processes found to kill."
            fi
            ;;
        2)
            echo ""
            echo "Reallocating to new port..."
            ;;
        *)
            echo "Cancelled."
            exit 0
            ;;
    esac

    # Find new port
    local new_port=$(next_available_port)

    echo -e "${BLUE}Reallocating to port $new_port...${NC}"
    echo "  Port: $old_port → $new_port"
    echo ""

    # Update metadata (use JSON for numeric port)
    update_metadata_json "$name" "port" "$new_port"
    echo -e "  ${GREEN}✓${NC} Metadata updated"

    # Set context for hook (project can update configs via hook)
    export PWT_OLD_PORT="$old_port"
    export PWT_PORT="$new_port"
    export PWT_WORKTREE="$name"
    export PWT_WORKTREE_PATH="$worktree_dir"
    export PWT_PROJECT="$CURRENT_PROJECT"
    export MAIN_APP="$MAIN_APP"

    # Run project hook (for config updates like .env, databases, etc.)
    run_hook "post-fix-port"

    echo ""
    echo -e "${GREEN}✓ Port reallocated successfully!${NC}"
    echo ""
    echo "To start server:"
    echo -e "  ${BLUE}pwt server${NC}  # Starts on port $new_port"
}

# Command: meta
# View or edit worktree metadata
cmd_meta() {
    local action="${1:-}"
    local name="${2:-}"
    local field="${3:-}"
    local value="${4:-}"

    init_metadata

    local project="${CURRENT_PROJECT:-unknown}"

    # Shortcut: pwt meta <key> [value] for current worktree
    # Also: pwt meta "text with spaces" → sets description (same heuristic as create)
    if [[ -n "$action" && ! "$action" =~ ^(list|show|set|get|delete|import|export|help|--help|-h)$ ]]; then
        local key="$action"
        local shortcut_value="$name"
        local current_dir=$(pwd)
        local current_name=""

        # Heuristic: text with spaces = description value
        # (no metadata key has spaces, same logic as pwt create)
        if [[ "$action" == *" "* ]]; then
            key="description"
            shortcut_value="$action"
        fi

        # Detect current worktree from pwd or symlink
        if current_name=$(get_current_from_symlink 2>/dev/null); then
            : # got it from symlink
        elif [[ "$current_dir" == "$WORKTREES_DIR"/* ]]; then
            current_name=$(basename "$current_dir")
        fi

        if [ -z "$current_name" ] || [ "$current_name" = "@" ]; then
            pwt_error "Not in a worktree"
            return 1
        fi

        if [ -z "$shortcut_value" ]; then
            # GET: pwt meta description
            local result=$(get_metadata "$current_name" "$key")
            if [ -n "$result" ]; then
                echo "$result"
            fi
        else
            # SET: pwt meta description "some text"
            update_metadata "$current_name" "$key" "$shortcut_value"
            echo -e "${GREEN}✓ $current_name.$key = $shortcut_value${NC}"
            clear_list_cache
        fi
        return $?
    fi

    case "$action" in
        ""|list)
            # List all metadata for current project (key=value format)
            echo -e "${BLUE}Worktree Metadata ($project):${NC}"
            echo ""
            jq -r --arg project "$project" '
              .[$project] // {} | to_entries[] |
              "\(.key):",
              "  path=\(.value.path // "")",
              "  branch=\(.value.branch // "")",
              "  base=\(.value.base // "")@\(.value.base_commit // "")",
              "  port=\(.value.port // "")",
              "  description=\(.value.description // "")",
              "  created=\(.value.created_at // "")",
              ""
            ' "$METADATA_FILE"
            ;;
        show)
            if [ -z "$name" ]; then
                pwt_error "Error: Worktree name required"
                echo "Usage: pwt meta show <worktree>"
                exit 1
            fi
            local meta=$(jq --arg project "$project" --arg name "$name" '.[$project][$name]' "$METADATA_FILE")
            if [ "$meta" = "null" ]; then
                echo -e "${YELLOW}No metadata found for: $name${NC}"
            else
                echo -e "${BLUE}Metadata for $name:${NC}"
                echo "$meta" | jq '.'
            fi
            ;;
        set)
            if [ -z "$name" ] || [ -z "$field" ] || [ -z "$value" ]; then
                pwt_error "Error: Missing arguments"
                echo "Usage: pwt meta set <worktree> <field> <value>"
                echo ""
                echo "Fields: base, description, branch"
                exit 1
            fi
            update_metadata "$name" "$field" "$value"
            echo -e "${GREEN}✓ Updated $name.$field = $value${NC}"
            clear_list_cache  # Invalidate cache so next list shows updated metadata
            ;;
        -h|--help|help)
            cat << 'HELP'
Usage: pwt meta [command] [args]
       pwt meta "text with spaces"   (set description on current worktree)
       pwt meta <key> [value]        (get/set field on current worktree)

Manage worktree metadata - descriptions, ports, and custom fields.
Metadata helps you find and identify worktrees across pwt commands.

COMMANDS:
  list                           List all metadata (default)
  show <worktree>                Show metadata for one worktree
  set <worktree> <field> <value> Set a field on any worktree
  import                         Import existing worktrees

SHORTCUT (from inside a worktree):
  pwt meta "text with spaces"    Set description (spaces = description)
  pwt meta <key>                 Get a field
  pwt meta <key> <value>         Set a field

FIELDS:
  description    Free text describing the worktree purpose
  port           Port number for dev servers (auto-allocated)
  marker         Visual marker for lists (emoji or text)
  <custom>       Any custom field you want (e.g., env, reviewer)

═══════════════════════════════════════════════════════════════════════
WHERE METADATA APPEARS
═══════════════════════════════════════════════════════════════════════

  ┌─────────────────────────────────────────────────────────────────┐
  │ pwt list                                                        │
  │ Shows description and port in the Meta column                   │
  ├─────────────────────────────────────────────────────────────────┤
  │ Worktree       Branch              Status   Meta                │
  │ ─────────────────────────────────────────────────────────────── │
  │ TICKET-123     fix/TICKET-123      ✓ clean  port=3001           │
  │                                             description=auth bug│
  │ TICKET-456     feat/TICKET-456     ● dirty  port=3002           │
  │                                             description=new API │
  └─────────────────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────────────────┐
  │ pwt select                                                      │
  │ Shows port and description in the fzf picker                    │
  ├─────────────────────────────────────────────────────────────────┤
  │ TICKET-123  fix/TICKET-123   :3001  ·  auth bug                 │
  │ TICKET-456  feat/TICKET-456  :3002  ·  new API                  │
  │ @           main             ·      ·  main app                 │
  └─────────────────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────────────────┐
  │ pwt info TICKET-123                                             │
  │ Shows all metadata fields                                       │
  ├─────────────────────────────────────────────────────────────────┤
  │ Worktree: TICKET-123                                            │
  │ Branch:   fix/TICKET-123                                        │
  │ Port:     3001                                                  │
  │ Desc:     auth bug                                              │
  └─────────────────────────────────────────────────────────────────┘

  ┌─────────────────────────────────────────────────────────────────┐
  │ pwt cd <search>                                                 │
  │ Searches BOTH name AND description (case-insensitive)           │
  ├─────────────────────────────────────────────────────────────────┤
  │ $ pwt cd auth            # partial match                        │
  │ $ pwt cd "login bug"     # multi-word search                    │
  │ $ pwt cd "au bug"        # no match? fzf does fuzzy search      │
  │                                                                 │
  │ No match or multiple? Opens fzf for fuzzy selection             │
  └─────────────────────────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════════════
EXAMPLES
═══════════════════════════════════════════════════════════════════════

  Setting description (quickest - text with spaces = description):
  ┌─────────────────────────────────────────────────────────────────┐
  │ $ pwt meta "fixing login auth bug"                              │
  │ ✓ TICKET-123.description = fixing login auth bug                │
  └─────────────────────────────────────────────────────────────────┘

  Setting description (explicit key):
  ┌─────────────────────────────────────────────────────────────────┐
  │ $ pwt meta description "fixing login auth bug"                  │
  │ ✓ TICKET-123.description = fixing login auth bug                │
  └─────────────────────────────────────────────────────────────────┘

  Setting description (on any worktree, from anywhere):
  ┌─────────────────────────────────────────────────────────────────┐
  │ $ pwt meta set TICKET-123 description "fixing login auth bug"   │
  │ ✓ Updated TICKET-123.description = fixing login auth bug        │
  └─────────────────────────────────────────────────────────────────┘

  Getting a field value:
  ┌─────────────────────────────────────────────────────────────────┐
  │ $ pwt meta port                                                 │
  │ 3001                                                            │
  └─────────────────────────────────────────────────────────────────┘

  Viewing all metadata:
  ┌─────────────────────────────────────────────────────────────────┐
  │ $ pwt meta show TICKET-123                                      │
  │ {                                                               │
  │   "port": 3001,                                                 │
  │   "description": "fixing login auth bug",                       │
  │   "branch": "fix/TICKET-123",                                   │
  │   "created_at": "2024-01-15T10:30:00"                           │
  │ }                                                               │
  └─────────────────────────────────────────────────────────────────┘

  Adding custom fields:
  ┌─────────────────────────────────────────────────────────────────┐
  │ $ pwt meta set TICKET-123 reviewer "@john"                      │
  │ $ pwt meta set TICKET-123 env staging                           │
  │ # Custom fields show in pwt list and pwt info                   │
  └─────────────────────────────────────────────────────────────────┘

═══════════════════════════════════════════════════════════════════════
WORKFLOW TIPS
═══════════════════════════════════════════════════════════════════════

  Description flows naturally through the entire workflow:

     # 1. Create with description
     $ pwt create TICKET-123 "auth: fix session timeout"

     # 2. Or set later (from inside worktree)
     $ pwt meta "auth: fix session timeout"

     # 3. Find by description
     $ pwt cd timeout           # partial match
     $ pwt cd "session timeout" # multi-word
     $ pwt cd "ses time"        # no match? fzf fuzzy search

     # 4. See in lists
     $ pwt list                 # Meta column shows description
     $ pwt select               # fzf picker shows description
HELP
            return 0
            ;;
        import)
            # Import metadata for existing worktrees
            echo -e "${BLUE}Importing metadata for existing worktrees...${NC}"
            if [ -d "$WORKTREES_DIR" ] && [ "$(ls -A "$WORKTREES_DIR" 2>/dev/null)" ]; then
                for dir in "$WORKTREES_DIR"/*/; do
                    [ -d "$dir" ] || continue
                    local wt_name=$(basename "$dir")
                    local existing=$(get_metadata "$wt_name" "path")
                    if [ -z "$existing" ]; then
                        local wt_branch=$(git -C "$dir" branch --show-current 2>/dev/null || echo "?")
                        # Try to extract port from directory name (legacy format)
                        local wt_port=""
                        if [[ "$wt_name" =~ -([0-9]{4})$ ]]; then
                            wt_port="${BASH_REMATCH[1]}"
                        else
                            # Allocate new port for new format directories
                            wt_port=$(next_available_port)
                        fi
                        local wt_base_commit=$(git -C "$dir" merge-base HEAD "origin/${DEFAULT_BRANCH:-master}" 2>/dev/null)
                        local wt_base_short=$(git -C "$dir" rev-parse --short "$wt_base_commit" 2>/dev/null || echo "?")
                        local wt_desc=$(echo "$wt_branch" | sed -E 's|^[a-z]+/||' | tr '-' ' ')

                        save_metadata "$wt_name" "$dir" "$wt_branch" "${DEFAULT_BRANCH:-master}" "$wt_base_short" "$wt_port" "$wt_desc"
                        echo -e "  ${GREEN}✓${NC} Imported: $wt_name (port $wt_port)"
                    else
                        echo -e "  ${YELLOW}○${NC} Exists: $wt_name"
                    fi
                done
            fi
            echo -e "${GREEN}Done!${NC}"
            ;;
        *)
            echo -e "${RED}Unknown action: $action${NC}"
            echo "Usage: pwt meta [list|show|set|import]"
            echo ""
            echo "Commands:"
            echo "  list              - List all worktree metadata"
            echo "  show <name>       - Show metadata for a worktree"
            echo "  set <name> <field> <value> - Update a metadata field"
            echo "  import            - Import metadata for existing worktrees"
            exit 1
            ;;
    esac
}

# Command: alias
# Get/set/clear project alias (auto-detects current project)
# Usage: pwt alias [<alias>|--clear]
cmd_alias() {
    local new_alias="${1:-}"

    if [[ "$new_alias" == "-h" || "$new_alias" == "--help" ]]; then
        echo "Usage: pwt alias [<name>|--clear]"
        echo ""
        echo "Set a short alias for the current project."
        echo ""
        echo "Arguments:"
        echo "  name       New alias (must not conflict with commands)"
        echo "  --clear    Remove current alias"
        echo "  (none)     Show current alias"
        echo ""
        echo "Examples:"
        echo "  pwt alias            # show current alias"
        echo "  pwt alias api        # set alias to 'api'"
        echo "  pwt alias --clear    # remove alias"
        echo ""
        echo "Once aliased, use it anywhere:"
        echo "  pwt api list         # same as pwt my-long-project list"
        echo "  pwt api cd TICKET    # same as pwt my-long-project cd TICKET"
        return 0
    fi

    local config_file="$PROJECTS_DIR/$CURRENT_PROJECT/config.json"

    if [ ! -f "$config_file" ]; then
        echo -e "${RED}Project config not found: $CURRENT_PROJECT${NC}"
        exit 1
    fi

    if [ -z "$new_alias" ]; then
        # Show current project and alias
        local current=$(jq -r '.alias // empty' "$config_file")
        if [ -n "$current" ]; then
            echo -e "${GREEN}$CURRENT_PROJECT${NC} → ${CYAN}$current${NC}"
        else
            echo -e "${GREEN}$CURRENT_PROJECT${NC} (no alias)"
        fi
    elif [ "$new_alias" = "--clear" ]; then
        # Clear alias
        local tmp_file
        tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
        jq 'del(.alias)' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
        echo -e "${GREEN}✓ Cleared alias for $CURRENT_PROJECT${NC}"
    else
        # Set alias - validate first
        local reserved_commands="list create remove cd server test meta port project help version config init show set path alias"
        for cmd in $reserved_commands; do
            if [ "$new_alias" = "$cmd" ]; then
                pwt_error "Error: '$new_alias' is a reserved command name"
                exit 1
            fi
        done
        # Check if alias conflicts with existing project name
        if [ -f "$PROJECTS_DIR/$new_alias/config.json" ]; then
            pwt_error "Error: '$new_alias' is already a project name"
            exit 1
        fi
        # Check if alias already used by another project
        for cfg in "$PROJECTS_DIR"/*/config.json; do
            [ -f "$cfg" ] || continue
            local proj_dir=$(dirname "$cfg")
            local proj_name=$(basename "$proj_dir")
            [ "$proj_name" = "$CURRENT_PROJECT" ] && continue
            local other_alias=$(jq -r '.alias // empty' "$cfg")
            if [ "$other_alias" = "$new_alias" ]; then
                pwt_error "Error: Alias '$new_alias' already used by project '$proj_name'"
                exit 1
            fi
        done
        # Set alias
        local tmp_file
        tmp_file="$(mktemp "${config_file}.tmp.XXXXXX")"
        jq --arg alias "$new_alias" '.alias = $alias' "$config_file" > "$tmp_file" && mv "$tmp_file" "$config_file"
        echo -e "${GREEN}✓ $CURRENT_PROJECT${NC} → ${CYAN}$new_alias${NC}"
    fi
}

# Command: cd (internal)
# Output path for worktree navigation
# Usage: pwt _cd [worktree|@|-]
#        pwt cd --select    # interactive fzf picker
#   @ or empty = main worktree
#   - = previous worktree (like cd -)
#   worktree = specific worktree
cmd_cd() {
    # Handle --help
    if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
        echo "Usage: pwt cd [worktree|@|-]"
        echo "       pwt cd <term>      # search by name or description"
        echo "       pwt cd --select    # interactive picker (fzf)"
        echo ""
        echo "Navigate to a worktree (outputs path for shell integration)."
        echo ""
        echo "Arguments:"
        echo "  worktree  Name of the worktree"
        echo "  <term>    Search term (matches name AND description)"
        echo "  @         Main app directory"
        echo "  -         Previous worktree (like cd -)"
        echo "  (none)    Last used worktree, or main"
        echo ""
        echo "Options:"
        echo "  --select, -s    Interactive worktree selector (fzf)"
        echo ""
        echo "Search behavior:"
        echo "  - Partial name match:  pwt cd auth"
        echo "  - Description search:  pwt cd \"auth login\""
        echo "  - If no match found, falls back to fzf for fuzzy selection"
        echo ""
        echo "Tip: Run '$(basename "$0") shell-init' for real cd integration."
        return 0
    fi

    # Handle --select flag
    if [[ "${1:-}" == "--select" || "${1:-}" == "-s" ]]; then
        shift
        if [ -n "${1:-}" ]; then
            cmd_select --query "$1"
        else
            cmd_select
        fi
        return $?
    fi

    local target="${1:-}"

    # Strip trailing slash (from shell completion)
    target="${target%/}"

    # No target: try last-used, fallback to main
    if [ -z "$target" ]; then
        local last=$(get_last_used)
        if [ -n "$last" ] && get_worktree_path "$last" >/dev/null 2>&1; then
            target="$last"
        else
            target="@"
        fi
    fi

    # - = previous worktree (like cd -)
    if [ "$target" = "-" ]; then
        local prev=$(get_previous)
        if [ -z "$prev" ]; then
            echo "No previous worktree" >&2
            return 1
        fi
        target="$prev"
        # Show where we're going (like cd - does)
        if [ "$target" = "@" ]; then
            echo -e "${DIM}~${NC}" >&2
        else
            echo -e "${DIM}$target${NC}" >&2
        fi
    fi

    # Helper to save navigation history
    _save_navigation() {
        local new_target="$1"
        local current=$(get_last_used)
        # Save current as previous (for pwt -)
        if [ -n "$current" ] && [ "$current" != "$new_target" ]; then
            save_previous "$current"
        fi
        save_last_used "$new_target"
    }

    # @ = main worktree
    if [ "$target" = "@" ]; then
        _save_navigation "@"
        echo "$MAIN_APP"
        return 0
    fi

    # current = symlink path (stable path for editors)
    if [ "$target" = "current" ]; then
        local symlink_path=$(get_current_symlink_path)
        if [ -L "$symlink_path" ]; then
            echo "$symlink_path"
            return 0
        else
            echo "No current worktree set. Run 'pwt use <worktree>' first." >&2
            return 1
        fi
    fi

    # Check if it's a worktree name
    local worktree_path
    worktree_path=$(get_worktree_path "$target" 2>/dev/null || echo "$WORKTREES_DIR/$target")
    if [ -d "$worktree_path" ]; then
        _save_navigation "$target"
        echo "$worktree_path"
        return 0
    fi

    # Try partial match by name
    local matches=()
    if [ -d "$WORKTREES_DIR" ]; then
        for dir in "$WORKTREES_DIR"/*/; do
            [ -d "$dir" ] || continue
            local name=$(basename "$dir")
            if [[ "$name" == *"$target"* ]]; then
                matches+=("$dir")
            fi
        done
    fi

    # If no name matches, try searching in descriptions (case-insensitive)
    if [ ${#matches[@]} -eq 0 ] && [ -d "$WORKTREES_DIR" ]; then
        local target_lower=$(echo "$target" | tr '[:upper:]' '[:lower:]')
        for dir in "$WORKTREES_DIR"/*/; do
            [ -d "$dir" ] || continue
            local name=$(basename "$dir")
            local desc=$(get_metadata "$name" "description" 2>/dev/null)
            if [ -n "$desc" ]; then
                local desc_lower=$(echo "$desc" | tr '[:upper:]' '[:lower:]')
                if [[ "$desc_lower" == *"$target_lower"* ]]; then
                    matches+=("$dir")
                fi
            fi
        done
    fi

    if [ ${#matches[@]} -eq 1 ]; then
        local matched_name=$(basename "${matches[0]%/}")
        _save_navigation "$matched_name"
        echo "${matches[0]%/}"
        return 0
    elif [ ${#matches[@]} -gt 1 ]; then
        # Multiple matches: fallback to interactive select with query
        if command -v fzf &>/dev/null && [ -t 0 ]; then
            cmd_select --query "$target"
            return $?
        else
            echo "Multiple matches for '$target':" >&2
            for m in "${matches[@]}"; do
                echo "  $(basename "${m%/}")" >&2
            done
            return 1
        fi
    fi

    # No matches: try fzf fuzzy search as last resort (only if interactive)
    if command -v fzf &>/dev/null && [ -t 0 ]; then
        cmd_select --query "$target"
        return $?
    fi

    echo "Worktree not found: $target" >&2
    return 1
}

# Resolve worktree path without side effects (no navigation history)
# Usage: resolve_worktree_path <target>
#   @ = main worktree
#   - = previous worktree
#   name = exact or fuzzy match
# Returns: path on stdout, or empty string on failure
resolve_worktree_path() {
    local target="$1"

    # Normalize: strip trailing slash (from shell completion)
    target="${target%/}"

    # @ = main worktree
    if [ "$target" = "@" ]; then
        echo "$MAIN_APP"
        return 0
    fi

    # - = previous worktree
    if [ "$target" = "-" ]; then
        local prev=$(get_previous)
        if [ -z "$prev" ]; then
            return 1
        fi
        get_worktree_path "$prev"
        return 0
    fi

    # Exact match
    local exact_path
    exact_path=$(get_worktree_path "$target" 2>/dev/null || echo "")
    if [ -n "$exact_path" ]; then
        echo "$exact_path"
        return 0
    fi

    # Fuzzy match (contains)
    local matches=()
    if [ -d "$WORKTREES_DIR" ]; then
        for dir in "$WORKTREES_DIR"/*/; do
            [ -d "$dir" ] || continue
            local name=$(basename "$dir")
            if [[ "$name" == *"$target"* ]]; then
                matches+=("${dir%/}")
            fi
        done
    fi

    if [ ${#matches[@]} -eq 1 ]; then
        echo "${matches[0]}"
        return 0
    fi

    local project="${CURRENT_PROJECT:-unknown}"
    if [ -f "$METADATA_FILE" ]; then
        while IFS=$'\t' read -r name path; do
            [ -n "$name" ] && [ -n "$path" ] && [ -d "$path" ] || continue
            if [[ "$name" == *"$target"* ]]; then
                matches+=("$path")
            fi
        done < <(jq -r --arg project "$project" '.[$project] // {} | to_entries[] | [.key, (.value.path // "")] | @tsv' "$METADATA_FILE" 2>/dev/null)
    fi

    if [ ${#matches[@]} -eq 1 ]; then
        echo "${matches[0]}"
        return 0
    fi

    return 1
}

# Command: run
# Run a command in a worktree without changing directory
# Usage: pwt run [worktree] <command...>
#   @ = main worktree
#   If worktree is omitted, uses current worktree or main
cmd_run() {
    # Handle --help
    if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
        echo "Usage: pwt run [worktree] <command...>"
        echo ""
        echo "Run a command in a worktree without changing directory."
        echo ""
        echo "Arguments:"
        echo "  worktree   Target worktree (@ for main, optional)"
        echo "  command    Command to run in the worktree"
        echo ""
        echo "If worktree is omitted, runs in current worktree or main."
        echo ""
        echo "Examples:"
        echo "  pwt run TICKET-123 npm test    # in specific worktree"
        echo "  pwt run @ git status           # in main app"
        echo "  pwt run npm test               # in current/main"
        return 0
    fi

    if [ $# -eq 0 ]; then
        echo -e "${RED}Usage: pwt run [worktree] <command...>${NC}"
        exit 1
    fi

    local target=""
    local worktree_path=""

    # Try to resolve first arg as worktree
    local maybe_wt="${1%/}"
    local maybe_path=""

    if [ "$maybe_wt" = "@" ]; then
        target="@"
        worktree_path="$MAIN_APP"
        shift
    else
        maybe_path=$(resolve_worktree_path "$maybe_wt" 2>/dev/null) || true
        if [ -n "$maybe_path" ] && [ -d "$maybe_path" ]; then
            # First arg is a valid worktree
            target="$maybe_wt"
            worktree_path="$maybe_path"
            shift
        else
            # First arg is not a worktree, use current/main
            target="current"
            # Try to get current worktree from symlink
            local current_link="${PWT_DIR}/projects/${CURRENT_PROJECT}/current"
            if [ -L "$current_link" ] && [ -d "$current_link" ]; then
                worktree_path=$(readlink "$current_link")
            else
                worktree_path="$MAIN_APP"
            fi
        fi
    fi

    if [ $# -eq 0 ]; then
        echo -e "${RED}No command specified${NC}"
        exit 1
    fi

    if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
        echo -e "${RED}Worktree not found: $target${NC}"
        exit 1
    fi

    echo -e "${BLUE}Running in $worktree_path:${NC} $*"
    (cd "$worktree_path" && "$@")
}

# Command: restore
# Recover backed up changes from ~/.pwt/trash/
# Backups are created automatically when removing dirty worktrees
# Usage:
#   pwt restore                       # List available backups
#   pwt restore <backup>              # Auto-create worktree and restore
#   pwt restore <backup> <worktree>   # Apply to existing worktree
cmd_restore() {
    local TRASH_DIR="$HOME/.pwt/trash"
    local first_arg="${1:-}"

    # Handle help and list first (don't need trash directory for help)
    case "$first_arg" in
        help|--help|-h)
            echo "Usage: pwt [project] restore [backup] [worktree]"
            echo ""
            echo "  pwt restore                   List available backups"
            echo "  pwt restore list              List available backups"
            echo "  pwt restore <backup>          Recreate worktree and apply backup"
            echo "  pwt restore <backup> <wt>     Apply backup to existing worktree"
            echo ""
            echo "Backups are created automatically when removing dirty worktrees."
            echo "Location: ~/.pwt/trash/"
            return 0
            ;;
        list|--list|-l)
            # Explicit list command - fall through to listing logic
            first_arg=""
            ;;
    esac

    # Ensure trash directory exists
    if [ ! -d "$TRASH_DIR" ]; then
        echo -e "${YELLOW}No backups found.${NC}"
        echo "Backups are created when removing worktrees with uncommitted changes."
        return 0
    fi

    # No argument = list backups
    if [ -z "$first_arg" ]; then
        _restore_list
        return $?
    fi

    # Argument provided = treat as backup name (exact or fuzzy match)
    local backup_name=""

    # Try exact match first
    if [ -f "$TRASH_DIR/${first_arg}.json" ] || [ -f "$TRASH_DIR/${first_arg}.patch" ]; then
        backup_name="$first_arg"
    else
        # Try fuzzy match by worktree name prefix (most recent)
        local matching=$(find "$TRASH_DIR" -maxdepth 1 -name "${first_arg}*.json" -print0 2>/dev/null | \
            xargs -0 ls -t 2>/dev/null | head -1)

        if [ -n "$matching" ]; then
            backup_name=$(basename "$matching" .json)
            echo -e "${DIM}Found: $backup_name${NC}"
        fi
    fi

    if [ -z "$backup_name" ]; then
        echo -e "${RED}Backup not found: $first_arg${NC}"
        echo "Use 'pwt restore' to see available backups."
        return 1
    fi

    local target_worktree="${2:-}"
    _restore_backup "$backup_name" "$target_worktree"
}

# Helper: list available backups
_restore_list() {
    local TRASH_DIR="$HOME/.pwt/trash"

    echo -e "${BLUE}Available backups:${NC}"
    echo ""

    local found=false
    local json_files=()

    # Collect JSON metadata files (newest first)
    while IFS= read -r -d '' file; do
        json_files+=("$file")
        found=true
    done < <(find "$TRASH_DIR" -maxdepth 1 -name "*.json" -print0 2>/dev/null | sort -z -r)

    if [ "$found" = false ]; then
        # Fall back to patch files without JSON (legacy)
        local patches=()
        while IFS= read -r -d '' file; do
            patches+=("$file")
            found=true
        done < <(find "$TRASH_DIR" -maxdepth 1 -name "*.patch" -print0 2>/dev/null | sort -z -r)

        if [ "$found" = false ]; then
            echo -e "${YELLOW}No backups found.${NC}"
            return 0
        fi

        for patch in "${patches[@]}"; do
            local basename=$(basename "$patch" .patch)
            local wt_name=$(echo "$basename" | sed -E 's/_[0-9]{8}_[0-9]{6}$//')
            echo -e "  ${GREEN}$basename${NC} ${DIM}(legacy)${NC}"
            echo -e "     Worktree: ${CYAN}$wt_name${NC}"
            echo ""
        done
    else
        for json_file in "${json_files[@]}"; do
            local basename=$(basename "$json_file" .json)
            local wt_name=$(jq -r '.worktree // empty' "$json_file" 2>/dev/null)
            local branch=$(jq -r '.branch // empty' "$json_file" 2>/dev/null)
            local date=$(jq -r '.date // empty' "$json_file" 2>/dev/null)

            # Check what backup files exist
            local has_patch=false
            local has_untracked=false
            [ -f "$TRASH_DIR/${basename}.patch" ] && has_patch=true
            [ -d "$TRASH_DIR/${basename}_untracked" ] && has_untracked=true

            local contents=""
            [ "$has_patch" = true ] && contents="patch"
            [ "$has_untracked" = true ] && contents="${contents:+$contents+}untracked"

            echo -e "  ${GREEN}$wt_name${NC}  ${DIM}$date${NC}  ${YELLOW}[$contents]${NC}"
            [ -n "$branch" ] && echo -e "     Branch: ${CYAN}$branch${NC}"
            echo ""
        done
    fi

    echo -e "${DIM}Usage: pwt restore <worktree-name> [target-worktree]${NC}"
}

# Helper: restore a backup to a worktree
# Usage: _restore_backup <backup_name> [target_worktree]
#   If target_worktree is empty, auto-creates from backup metadata
_restore_backup() {
    local backup_name="$1"
    local target_worktree="$2"
    local TRASH_DIR="$HOME/.pwt/trash"

    local json_file="$TRASH_DIR/${backup_name}.json"
    local patch_file="$TRASH_DIR/${backup_name}.patch"
    local untracked_dir="$TRASH_DIR/${backup_name}_untracked"

    # Check backup exists
    if [ ! -f "$json_file" ] && [ ! -f "$patch_file" ] && [ ! -d "$untracked_dir" ]; then
        echo -e "${RED}Backup not found: $backup_name${NC}"
        return 1
    fi

    local target_dir=""

    # If target worktree specified, resolve it
    if [ -n "$target_worktree" ]; then
        target_dir=$(resolve_worktree_path "$target_worktree") || true
        if [ -z "$target_dir" ] || [ ! -d "$target_dir" ]; then
            echo -e "${RED}Worktree not found: $target_worktree${NC}"
            return 1
        fi
        echo -e "${BLUE}Restoring to existing worktree: $target_worktree${NC}"
    else
        # Auto-create worktree from metadata
        if [ ! -f "$json_file" ]; then
            echo -e "${RED}Cannot auto-create worktree: no metadata (legacy backup)${NC}"
            echo "Specify target worktree: pwt restore $backup_name <worktree>"
            return 1
        fi

        local branch=$(jq -r '.branch // empty' "$json_file" 2>/dev/null)
        local base=$(jq -r '.base // empty' "$json_file" 2>/dev/null)
        local desc=$(jq -r '.description // empty' "$json_file" 2>/dev/null)

        if [ -z "$branch" ]; then
            echo -e "${RED}Cannot auto-create worktree: no branch in metadata${NC}"
            echo "Specify target worktree: pwt restore $backup_name <worktree>"
            return 1
        fi

        echo -e "${BLUE}Creating worktree from backup metadata...${NC}"
        echo -e "  Branch: ${YELLOW}$branch${NC}"
        echo -e "  Base:   ${base:-master}"
        echo ""

        # Create the worktree using cmd_create
        # Pass description if available
        if [ -n "$desc" ]; then
            cmd_create "$branch" "${base:-master}" "$desc"
        else
            cmd_create "$branch" "${base:-master}"
        fi

        local create_status=$?
        if [ $create_status -ne 0 ]; then
            echo -e "${RED}Failed to create worktree${NC}"
            return 1
        fi

        # Find the newly created worktree
        target_dir=$(resolve_worktree_path "$branch") || true
        if [ -z "$target_dir" ] || [ ! -d "$target_dir" ]; then
            echo -e "${RED}Could not find created worktree${NC}"
            return 1
        fi

        echo ""
    fi

    local restored_something=false

    # Apply patch if exists
    if [ -f "$patch_file" ]; then
        echo -e "${BLUE}Applying patch...${NC}"

        if git -C "$target_dir" apply --check "$patch_file" 2>/dev/null; then
            if git -C "$target_dir" apply "$patch_file"; then
                echo -e "${GREEN}✓ Patch applied successfully${NC}"
                restored_something=true
            else
                echo -e "${RED}Failed to apply patch${NC}"
                echo -e "${YELLOW}Manual: git apply $patch_file${NC}"
            fi
        else
            echo -e "${YELLOW}⚠️  Patch cannot be applied cleanly${NC}"
            echo "Options:"
            echo "  git apply --3way $patch_file"
            echo "  git apply --reject $patch_file"
        fi
    fi

    # Copy untracked files if exist
    if [ -d "$untracked_dir" ]; then
        echo -e "${BLUE}Restoring untracked files...${NC}"

        find "$untracked_dir" -type f | while read -r file; do
            local rel_path="${file#$untracked_dir/}"
            local dest_file="$target_dir/$rel_path"
            local dest_dir=$(dirname "$dest_file")

            mkdir -p "$dest_dir"

            if [ -f "$dest_file" ]; then
                echo -e "  ${YELLOW}⚠️  Skip (exists): $rel_path${NC}"
            else
                cp "$file" "$dest_file"
                echo -e "  ${GREEN}✓ $rel_path${NC}"
            fi
        done

        restored_something=true
    fi

    if [ "$restored_something" = true ]; then
        echo ""
        echo -e "${GREEN}✓ Restore complete!${NC}"
        echo -e "${DIM}Worktree: $target_dir${NC}"
    fi
}

# ============================================
# Editor Commands
# ============================================

# Command: editor add
cmd_editor_add() {
    local name="${1:-}"
    local cmd="${2:-}"

    if [ -z "$name" ] || [ -z "$cmd" ]; then
        echo "Usage: pwt editor add <name> \"<command>\""
        echo "Example: pwt editor add sublime \"subl\""
        echo "Example: pwt editor add cursor \"/Applications/Cursor.app/Contents/Resources/app/bin/cursor\""
        return 1
    fi

    add_editor_tool "$name" "$cmd"
    echo -e "${GREEN}Added '$name' → \"$cmd\"${NC}"
}

# Command: editor remove
cmd_editor_remove() {
    local name="${1:-}"

    if [ -z "$name" ]; then
        echo "Usage: pwt editor remove <name>"
        return 1
    fi

    remove_editor_tool "$name"
    echo -e "${GREEN}Removed '$name'${NC}"
}

# Command: editor list
cmd_editor_list() {
    local config_file="$PWT_DIR/config.json"
    local default_tool=""
    local tools=""

    if [ -f "$config_file" ]; then
        default_tool=$(jq -r '.editor.default // empty' "$config_file" 2>/dev/null)
        tools=$(jq -r '.editor.tools // {} | to_entries[] | "\(.key)\t\(.value)"' "$config_file" 2>/dev/null)
    fi

    if [ -z "$tools" ]; then
        echo "No editors configured."
        echo ""
        echo "Add an editor:  pwt editor add <name> \"<command>\""
        echo "Example:        pwt editor add sublime \"subl\""
        echo ""
        echo "Or set \$EDITOR environment variable."
        [ -n "$EDITOR" ] && echo -e "Current \$EDITOR: ${GREEN}$EDITOR${NC}"
        return
    fi

    echo "Configured editors:"
    while IFS=$'\t' read -r name cmd; do
        if [ "$name" = "$default_tool" ]; then
            echo -e "  ${GREEN}$name${NC} (default) → $cmd"
        else
            echo -e "  $name → $cmd"
        fi
    done <<< "$tools"

    [ -n "$EDITOR" ] && echo -e "\n\$EDITOR fallback: $EDITOR"
}

# Command: editor help
cmd_editor_help() {
    echo "Usage: pwt editor [name] [worktree]"
    echo "       pwt editor:<name> [worktree]"
    echo ""
    echo "Open worktree in editor."
    echo ""
    echo "Commands:"
    echo "  pwt editor add <name> \"<cmd>\"  Add an editor"
    echo "  pwt editor remove <name>        Remove an editor"
    echo "  pwt editor list                 List configured editors"
    echo "  pwt editor:<name> --default     Set default editor"
    echo ""
    echo "Usage:"
    echo "  pwt editor                      Open current in default editor"
    echo "  pwt editor TICKET               Open worktree in default editor"
    echo "  pwt editor @                    Open main app in editor"
    echo "  pwt editor:sublime              Open current in sublime"
    echo "  pwt editor:sublime TICKET       Open worktree in sublime"
    echo "  pwt editor @                    Open main app"
    echo ""
    echo "Resolution: config → \$EDITOR → error"
}

# Command: editor
# Open worktree in configured editor
# Usage: pwt editor [name] [worktree]
#        pwt editor:<name> [worktree]
cmd_editor() {
    local tool="${1:-}"
    shift || true

    # Handle subcommands
    case "$tool" in
        add)
            cmd_editor_add "$@"
            return
            ;;
        remove)
            cmd_editor_remove "$@"
            return
            ;;
        list)
            cmd_editor_list
            return
            ;;
        -h|--help|help)
            cmd_editor_help
            return
            ;;
        --default)
            # pwt editor:<name> --default was called, name is in $tool from dispatch
            echo "Usage: pwt editor:<name> --default"
            return 1
            ;;
    esac

    # Check for --default flag (pwt editor:<name> --default)
    if [ "${1:-}" = "--default" ]; then
        if [ -z "$tool" ]; then
            echo "Usage: pwt editor:<name> --default"
            return 1
        fi
        set_default_editor_tool "$tool"
        echo -e "${GREEN}Default editor set to '$tool'${NC}"
        return
    fi

    # Resolve editor command
    local editor_cmd
    editor_cmd=$(get_editor_cmd "$tool")

    if [ -z "$editor_cmd" ]; then
        echo -e "${RED}No editor configured${NC}"
        echo ""
        echo "Configure an editor:"
        echo "  pwt editor add sublime \"subl\""
        echo "  pwt editor add cursor \"/Applications/Cursor.app/Contents/Resources/app/bin/cursor\""
        echo ""
        echo "Or set \$EDITOR environment variable:"
        echo "  export EDITOR=\"subl\""
        return 1
    fi

    # Determine worktree path
    local target="${1:-}"
    local worktree_path

    if [ "$target" = "@" ]; then
        worktree_path="$MAIN_APP"
    elif [ -z "$target" ]; then
        # No target: use current symlink if exists, else main app
        local symlink_path=$(get_current_symlink_path)
        if [ -L "$symlink_path" ]; then
            worktree_path="$symlink_path"
        else
            worktree_path="$MAIN_APP"
        fi
    else
        worktree_path=$(cmd_cd "$target" 2>/dev/null)
        if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
            echo -e "${RED}Worktree not found: $target${NC}"
            exit 1
        fi
    fi

    echo -e "${BLUE}Opening in $editor_cmd:${NC} $worktree_path"
    "$editor_cmd" "$worktree_path"
}

# ============================================
# AI Tool Commands
# ============================================

# Command: ai add
# Add an AI tool to global config
cmd_ai_add() {
    local name="${1:-}"
    local cmd="${2:-}"

    if [ -z "$name" ] || [ -z "$cmd" ]; then
        echo "Usage: pwt ai add <name> \"<command>\""
        echo "Example: pwt ai add gemini \"gemini --model gemini-2.0-flash\""
        return 1
    fi

    add_ai_tool "$name" "$cmd"
    echo -e "${GREEN}Added '$name' → \"$cmd\"${NC}"
}

# Command: ai remove
# Remove an AI tool from global config
cmd_ai_remove() {
    local name="${1:-}"

    if [ -z "$name" ]; then
        echo "Usage: pwt ai remove <name>"
        return 1
    fi

    remove_ai_tool "$name"
    echo -e "${GREEN}Removed '$name'${NC}"
}

# Command: ai list
# List configured AI tools
cmd_ai_list() {
    local config_file="$PWT_DIR/config.json"
    local default_tool=""
    local tools=""

    if [ -f "$config_file" ]; then
        default_tool=$(jq -r '.ai.default // "claude"' "$config_file" 2>/dev/null)
        tools=$(jq -r '.ai.tools // {} | to_entries[] | "\(.key)\t\(.value)"' "$config_file" 2>/dev/null)
    else
        default_tool="claude"
    fi

    if [ -z "$tools" ]; then
        echo "No tools configured."
        echo ""
        echo "Commands in PATH can be used directly: pwt ai:claude, pwt ai:gemini"
        echo "To add with custom options: pwt ai add <name> \"<command>\""
        return
    fi

    echo "Configured tools:"
    while IFS=$'\t' read -r name cmd; do
        if [ "$name" = "$default_tool" ]; then
            echo -e "  ${GREEN}$name${NC} (default) → $cmd"
        else
            echo -e "  $name → $cmd"
        fi
    done <<< "$tools"
}

# Command: ai:tool --default
# Set default AI tool
cmd_ai_set_default() {
    local name="${1:-}"

    if [ -z "$name" ]; then
        echo "Usage: pwt ai:<name> --default"
        return 1
    fi

    set_default_ai_tool "$name"
    echo -e "${GREEN}Default set to '$name'${NC}"
}

# Command: ai help
# Show AI command help
cmd_ai_help() {
    echo "Usage: pwt ai [tool] [worktree] [-- args...]"
    echo ""
    echo "Start AI coding tool in worktree."
    echo ""
    echo "Commands:"
    echo "  pwt ai add <name> \"<cmd>\"   Add a tool"
    echo "  pwt ai remove <name>         Remove a tool"
    echo "  pwt ai list                  List configured tools"
    echo "  pwt ai:<name> --default      Set default tool"
    echo ""
    echo "Usage:"
    echo "  pwt ai                       Run default tool in current/main"
    echo "  pwt ai TICKET                Run default in worktree"
    echo "  pwt ai @                     Run default in main app"
    echo "  pwt ai:gemini                Run gemini in current/main"
    echo "  pwt ai:gemini TICKET         Run gemini in worktree"
    echo "  pwt ai:codex -- --full-auto  Run with extra args"
    echo "  pwt ai:gemini --add          Add to config + run"
    echo ""
    echo "Resolution: config → PATH → error"
}

# Command: ai
# Start AI coding tool in worktree
# Usage: pwt ai [tool] [worktree] [-- args...]
cmd_ai() {
    local tool="${1:-}"
    shift || true

    # Handle subcommands (management)
    # When called as "pwt ai add" (no ai:tool), tool will be empty
    # and "add" will be in $1. When called as "pwt ai:tool", tool
    # will have the tool name.
    local subcmd="$tool"
    [ -z "$subcmd" ] && subcmd="${1:-}"

    case "$subcmd" in
        add)
            [ -z "$tool" ] && shift  # skip "add" if it was in args
            cmd_ai_add "$@"
            return
            ;;
        remove)
            [ -z "$tool" ] && shift
            cmd_ai_remove "$@"
            return
            ;;
        list)
            [ -z "$tool" ] && shift
            cmd_ai_list "$@"
            return
            ;;
        -h|--help|help)
            cmd_ai_help
            return
            ;;
    esac

    # Parse flags and arguments
    local worktree=""
    local set_default=false
    local add_tool=false
    local extra_args=()

    while [ $# -gt 0 ]; do
        case "$1" in
            --default)
                set_default=true
                shift
                ;;
            --add)
                add_tool=true
                shift
                ;;
            --)
                shift
                extra_args=("$@")
                break
                ;;
            *)
                [ -z "$worktree" ] && worktree="$1"
                shift
                ;;
        esac
    done

    # --default: set tool as default and exit
    if [ "$set_default" = true ]; then
        cmd_ai_set_default "$tool"
        return
    fi

    # --add: check tool exists in PATH before proceeding
    if [ "$add_tool" = true ]; then
        if [ -z "$tool" ]; then
            echo -e "${RED}Cannot use --add without specifying a tool${NC}"
            echo "Use: pwt ai:<tool> --add"
            return 1
        fi
        if ! command -v "$tool" &>/dev/null; then
            echo -e "${RED}'$tool' not found in PATH${NC}"
            return 1
        fi
        add_ai_tool "$tool" "$tool"
        echo -e "${GREEN}Added '$tool' → \"$tool\"${NC}"
    fi

    # Resolve tool (config > PATH)
    local ai_cmd
    ai_cmd=$(resolve_ai_tool "$tool")

    if [ -z "$ai_cmd" ]; then
        echo -e "${RED}No '$tool' configured and no '$tool' command found${NC}"
        echo "Use: pwt ai add $tool \"<command>\""
        return 1
    fi

    # Resolve worktree path
    local worktree_path
    if [ -z "$worktree" ] || [ "$worktree" = "@" ]; then
        worktree_path="$MAIN_APP"
    else
        worktree_path=$(cmd_cd "$worktree" 2>/dev/null) || true
        if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
            echo -e "${RED}Worktree not found: $worktree${NC}"
            return 1
        fi
    fi

    # Get display name for the tool
    local tool_name="${tool:-default}"
    [ "$tool_name" = "default" ] && tool_name=$(jq -r '.ai.default // "claude"' "$PWT_DIR/config.json" 2>/dev/null || echo "claude")

    echo -e "${BLUE}Starting $tool_name in:${NC} $worktree_path"
    (cd "$worktree_path" && eval "$ai_cmd" ${extra_args[@]+"${extra_args[@]}"})
}

# Command: open
# Open worktree in Finder/file manager
# Usage: pwt open [worktree]
cmd_open() {
    local target="${1:-}"
    target="${target%/}"
    local worktree_path

    if [[ "$target" == "-h" || "$target" == "--help" ]]; then
        echo "Usage: pwt open [worktree]"
        echo ""
        echo "Open a worktree directory in Finder."
        echo ""
        echo "Arguments:"
        echo "  worktree   Target worktree (optional, default: main app)"
        echo "  @          Main app directory"
        echo ""
        echo "Examples:"
        echo "  pwt open              # open main app"
        echo "  pwt open TICKET-123   # open worktree"
        echo "  pwt open @            # open main app"
        return 0
    fi

    if [ -z "$target" ] || [ "$target" = "@" ]; then
        worktree_path="$MAIN_APP"
    else
        worktree_path=$(cmd_cd "$target" 2>/dev/null)
        if [ -z "$worktree_path" ] || [ ! -d "$worktree_path" ]; then
            echo -e "${RED}Worktree not found: $target${NC}"
            exit 1
        fi
    fi

    echo -e "${BLUE}Opening in Finder:${NC} $worktree_path"
    open "$worktree_path"
}

# Command: diff
# Show diff between worktrees or worktree vs main
# Usage: pwt diff <worktree1> [worktree2]
cmd_diff() {
    # Handle --help
    if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
        echo "Usage: pwt diff <worktree1> [worktree2]"
        echo ""
        echo "Show file differences between worktrees."
        echo ""
        echo "Arguments:"
        echo "  worktree1  First worktree to compare"
        echo "  worktree2  Second worktree (default: @ for main app)"
        echo ""
        echo "Examples:"
        echo "  pwt diff TICKET-123         # Compare TICKET-123 vs main"
        echo "  pwt diff TICKET-123 @       # Same as above"
        echo "  pwt diff TICKET-123 TICKET-456  # Compare two worktrees"
        return 0
    fi

    local wt1="$1"
    local wt2="${2:-@}"

    if [ -z "$wt1" ]; then
        echo -e "${RED}Usage: pwt diff <worktree1> [worktree2]${NC}"
        echo "  worktree2 defaults to @ (main app)"
        exit 1
    fi

    local path1 path2

    # Resolve worktree1
    if [ "$wt1" = "@" ]; then
        path1="$MAIN_APP"
    else
        path1=$(cmd_cd "$wt1" 2>/dev/null)
        if [ -z "$path1" ] || [ ! -d "$path1" ]; then
            echo -e "${RED}Worktree not found: $wt1${NC}"
            exit 1
        fi
    fi

    # Resolve worktree2
    if [ "$wt2" = "@" ]; then
        path2="$MAIN_APP"
    else
        path2=$(cmd_cd "$wt2" 2>/dev/null)
        if [ -z "$path2" ] || [ ! -d "$path2" ]; then
            echo -e "${RED}Worktree not found: $wt2${NC}"
            exit 1
        fi
    fi

    echo -e "${BLUE}Comparing:${NC}"
    echo "  $wt1: $path1"
    echo "  $wt2: $path2"
    echo ""

    # Get branch and commit info
    local branch1=$(git -C "$path1" branch --show-current 2>/dev/null || echo "detached")
    local branch2=$(git -C "$path2" branch --show-current 2>/dev/null || echo "detached")
    local commit1=$(git -C "$path1" rev-parse HEAD 2>/dev/null)
    local commit2=$(git -C "$path2" rev-parse HEAD 2>/dev/null)

    if [ -z "$commit1" ] || [ -z "$commit2" ]; then
        pwt_error "Error: Could not get commit hashes"
        exit 1
    fi

    echo -e "${BLUE}Branches:${NC} $branch1 vs $branch2"
    echo -e "${BLUE}Commits:${NC} ${commit1:0:8} vs ${commit2:0:8}"
    echo ""

    # Show git diff using commit hashes (works across worktrees via main repo)
    git -C "$MAIN_APP" diff "$commit2".."$commit1" --stat
}

# Command: copy
# Copy files between worktrees
# Usage: pwt copy <src> <dest> <patterns...>
#   src/dest can be @ for main app
cmd_copy() {
    local src="$1"
    local dest="$2"
    shift 2

    if [ -z "$src" ] || [ -z "$dest" ] || [ $# -eq 0 ]; then
        echo -e "${RED}Usage: pwt copy|cp <src> <dest> <patterns...>${NC}"
        echo "  src/dest can be @ for main app"
        echo ""
        echo "Examples:"
        echo "  pwt copy @ TICKET-123 \".env*\"          # main → worktree"
        echo "  pwt copy TICKET-123 @ \".env*\"          # worktree → main"
        echo "  pwt copy TICKET-123 TICKET-456 \"*.json\" # between worktrees"
        exit 1
    fi

    local src_path dest_path

    # Resolve source
    if [ "$src" = "@" ]; then
        src_path="$MAIN_APP"
    else
        src_path=$(cmd_cd "$src" 2>/dev/null)
        if [ -z "$src_path" ] || [ ! -d "$src_path" ]; then
            echo -e "${RED}Worktree not found: $src${NC}"
            exit 1
        fi
    fi

    # Resolve destination
    if [ "$dest" = "@" ]; then
        dest_path="$MAIN_APP"
    else
        dest_path=$(cmd_cd "$dest" 2>/dev/null)
        if [ -z "$dest_path" ] || [ ! -d "$dest_path" ]; then
            echo -e "${RED}Worktree not found: $dest${NC}"
            exit 1
        fi
    fi

    echo -e "${BLUE}Copying from $src to $dest:${NC}"

    local count=0
    for pattern in "$@"; do
        # Use find with -name (pattern is a filename glob, not a path)
        while IFS= read -r -d '' file; do
            local rel_path="${file#$src_path/}"
            local dest_file="$dest_path/$rel_path"

            # Create parent directory if needed
            mkdir -p "$(dirname "$dest_file")"

            cp "$file" "$dest_file"
            echo "  ✓ $rel_path"
            count=$((count + 1))
        done < <(find "$src_path" -name "$pattern" -type f -print0 2>/dev/null)
    done

    if [ $count -eq 0 ]; then
        echo "  No files matched"
    else
        echo -e "${GREEN}Copied $count file(s)${NC}"
    fi
}

# Command: doctor
# Check system health and configuration
# Usage: pwt doctor
cmd_doctor() {
    # Handle --help
    if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
        echo "Usage: pwt doctor"
        echo ""
        echo "Check system health and pwt configuration."
        echo ""
        echo "Checks performed:"
        echo "  - Required tools (git, jq)"
        echo "  - Optional tools (lsof, fzf)"
        echo "  - PWT directory structure"
        echo "  - Project configurations"
        echo "  - Worktree integrity"
        return 0
    fi

    echo -e "${BLUE}pwt doctor${NC}"
    echo ""

    local errors=0

    # Check git
    if command -v git &>/dev/null; then
        local git_version=$(git --version | cut -d' ' -f3)
        echo -e "${GREEN}✓${NC} Git: $git_version"
    else
        echo -e "${RED}✗${NC} Git: not installed"
        errors=$((errors + 1))
    fi

    # Check jq
    if command -v jq &>/dev/null; then
        local jq_version=$(jq --version 2>/dev/null || echo "?")
        echo -e "${GREEN}✓${NC} jq: $jq_version"
    else
        echo -e "${RED}✗${NC} jq: not installed (required)"
        errors=$((errors + 1))
    fi

    # Check lsof
    if has_lsof; then
        echo -e "${GREEN}✓${NC} lsof: installed"
    else
        echo -e "${YELLOW}⚠${NC} lsof: not installed (port detection will be limited)"
        echo "    macOS:  (should be pre-installed)"
        echo "    Ubuntu: sudo apt install lsof"
    fi

    # Check fzf
    if command -v fzf &>/dev/null; then
        local fzf_version=$(fzf --version 2>/dev/null | head -1 || echo "?")
        echo -e "${GREEN}✓${NC} fzf: $fzf_version"
    else
        echo -e "${YELLOW}⚠${NC} fzf: not installed (needed for --select option)"
    fi

    # Check shell integration
    local cmd_name
    cmd_name=$(basename "$0")
    local user_shell
    user_shell=$(basename "${SHELL:-bash}")
    local shell_init_ok=false
    case "$user_shell" in
        fish)
            local fish_config="${HOME}/.config/fish/config.fish"
            if [ -f "$fish_config" ] && grep -q "shell-init.*fish" "$fish_config" 2>/dev/null; then
                shell_init_ok=true
            fi
            ;;
        zsh)
            local zshrc="${HOME}/.zshrc"
            if [ -f "$zshrc" ] && grep -q "shell-init" "$zshrc" 2>/dev/null; then
                shell_init_ok=true
            fi
            ;;
        bash)
            local bashrc="${HOME}/.bashrc"
            local bash_profile="${HOME}/.bash_profile"
            if { [ -f "$bashrc" ] && grep -q "shell-init" "$bashrc" 2>/dev/null; } || \
               { [ -f "$bash_profile" ] && grep -q "shell-init" "$bash_profile" 2>/dev/null; }; then
                shell_init_ok=true
            fi
            ;;
    esac
    if [ "$shell_init_ok" = true ]; then
        echo -e "${GREEN}✓${NC} Shell integration: configured ($user_shell)"
    else
        echo -e "${YELLOW}⚠${NC} Shell integration: not configured (cd won't change directory)"
        _show_shell_init_tip
    fi

    echo ""

    # Check project configuration
    if [ -n "$CURRENT_PROJECT" ]; then
        echo -e "${GREEN}✓${NC} Project: $CURRENT_PROJECT"

        if [ -n "$MAIN_APP" ] && [ -d "$MAIN_APP" ]; then
            echo -e "${GREEN}✓${NC} Main app: $MAIN_APP"
        else
            echo -e "${RED}✗${NC} Main app: not found"
            errors=$((errors + 1))
        fi

        if [ -n "$WORKTREES_DIR" ]; then
            if [ -d "$WORKTREES_DIR" ]; then
                local wt_count=$(ls -d "$WORKTREES_DIR"/*/ 2>/dev/null | wc -l | tr -d ' ')
                echo -e "${GREEN}✓${NC} Worktrees dir: $WORKTREES_DIR ($wt_count worktrees)"
            else
                echo -e "${YELLOW}⚠${NC} Worktrees dir: $WORKTREES_DIR (not created yet)"
            fi
        fi

        # Check Pwtfile
        if [ -n "${PWTFILE:-}" ] && [ -f "$PWTFILE" ]; then
            echo -e "${GREEN}✓${NC} Pwtfile: $PWTFILE"
        elif [ -f "$MAIN_APP/Pwtfile" ]; then
            echo -e "${GREEN}✓${NC} Pwtfile: $MAIN_APP/Pwtfile"
        else
            echo -e "${YELLOW}⚠${NC} Pwtfile: not found (optional)"
        fi

        # Check editor config
        local editor_cfg=$(get_project_config "$CURRENT_PROJECT" "editor" 2>/dev/null)
        if [ -n "$editor_cfg" ]; then
            if command -v "$editor_cfg" &>/dev/null; then
                echo -e "${GREEN}✓${NC} Editor: $editor_cfg"
            else
                echo -e "${YELLOW}⚠${NC} Editor: $editor_cfg (not found in PATH)"
            fi
        fi

        # Check AI config
        local ai_cfg=$(get_project_config "$CURRENT_PROJECT" "ai" 2>/dev/null)
        if [ -n "$ai_cfg" ]; then
            if command -v "$ai_cfg" &>/dev/null; then
                echo -e "${GREEN}✓${NC} AI: $ai_cfg"
            else
                echo -e "${YELLOW}⚠${NC} AI: $ai_cfg (not found in PATH)"
            fi
        fi
    else
        echo -e "${YELLOW}⚠${NC} Project: not detected (run from project directory)"
    fi

    echo ""
    if [ $errors -gt 0 ]; then
        echo -e "${RED}$errors error(s) found${NC}"
        exit 1
    else
        echo -e "${GREEN}All checks passed!${NC}"
    fi
}

# Command: select
# Interactive worktree selector using fzf
# Usage: pwt select [--preview] [--dirty] [--query <text>]
cmd_select() {
    local show_preview=true
    local show_dirty_only=false
    local initial_query=""

    # Parse arguments
    while [ $# -gt 0 ]; do
        case "$1" in
            -p|--preview)
                show_preview=true
                shift
                ;;
            --no-preview)
                show_preview=false
                shift
                ;;
            -d|--dirty)
                show_dirty_only=true
                shift
                ;;
            -h|--help)
                echo "Usage: pwt select [options]"
                echo ""
                echo "Interactive worktree selector with fzf."
                echo ""
                echo "Options:"
                echo "  -p, --preview     Show preview pane (default)"
                echo "  --no-preview      Hide preview pane"
                echo "  -d, --dirty       Only show worktrees with changes"
                echo "  -q, --query STR   Pre-fill search query"
                echo ""
                echo "Keybindings:"
                echo "  Enter     Navigate to selected worktree"
                echo "  Ctrl+E    Open in editor"
                echo "  Ctrl+A    Open AI assistant"
                echo "  Ctrl+O    Open in Finder"
                echo "  Esc       Cancel"
                return 0
                ;;
            -q|--query)
                initial_query="$2"
                shift 2
                ;;
            *)
                # Treat positional args as query
                if [ -z "$initial_query" ]; then
                    initial_query="$1"
                fi
                shift
                ;;
        esac
    done

    # Check if fzf is installed
    if ! command -v fzf &>/dev/null; then
        pwt_error "Error: fzf is required for pwt select"
        echo "Install with: brew install fzf"
        exit 1
    fi

    # Build enriched list of worktrees
    # Format: name|branch|port|status|marker|description
    local items=()

    # Add main app
    if [ -d "$MAIN_APP" ]; then
        local main_branch=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "main")
        local main_status=$(get_status_symbols "$MAIN_APP")
        if [ "$show_dirty_only" = false ] || [ -n "$main_status" ]; then
            items+=("@|$main_branch|·|${main_status:-·}|·|main app")
        fi
    fi

    # Add worktrees with metadata
    while IFS=$'\t' read -r name dir; do
        [ -n "$name" ] && [ -d "$dir" ] || continue
        local branch=$(git -C "$dir" branch --show-current 2>/dev/null || echo "?")
        local port=$(get_metadata "$name" "port")
        local marker=$(get_metadata "$name" "marker")
        local desc=$(get_metadata "$name" "description")
        local status=$(get_status_symbols "$dir")

        # Filter by dirty if requested
        if [ "$show_dirty_only" = true ] && [ -z "$status" ]; then
            continue
        fi

        # Truncate description to 30 chars
        [ -n "$desc" ] && desc="${desc:0:30}"

        items+=("$name|$branch|:${port:-·}|${status:-·}|${marker:-·}|${desc:-·}")
    done < <(list_known_worktree_entries)

    if [ ${#items[@]} -eq 0 ]; then
        if [ "$show_dirty_only" = true ]; then
            echo -e "${YELLOW}No dirty worktrees found${NC}"
        else
            echo -e "${YELLOW}No worktrees found${NC}"
        fi
        exit 0
    fi

    # Format with column for aligned display
    local formatted
    formatted=$(printf '%s\n' "${items[@]}" | column -t -s'|')

    # Build preview command
    local preview_cmd="name=\$(echo {} | awk '{print \$1}'); "
    preview_cmd+="if [ \"\$name\" = \"@\" ]; then "
    preview_cmd+="  echo '=== Main App ===' && git -C \"$MAIN_APP\" status -sb && echo '' && git -C \"$MAIN_APP\" diff --stat 2>/dev/null | head -15; "
    preview_cmd+="else "
    preview_cmd+="  pwt info \"\$name\" 2>/dev/null || echo \"Worktree: \$name\"; "
    preview_cmd+="fi"

    # Run fzf with enhanced options
    local fzf_opts=(
        --height=60%
        --reverse
        --ansi
        --header=$'Worktree selector\n↵:cd  ^E:editor  ^A:ai  ^O:open  Esc:cancel'
        --bind="ctrl-e:execute-silent(pwt editor \$(echo {} | awk '{print \$1}'))+abort"
        --bind="ctrl-a:execute-silent(pwt ai \$(echo {} | awk '{print \$1}'))+abort"
        --bind="ctrl-o:execute-silent(pwt open \$(echo {} | awk '{print \$1}'))+abort"
    )

    if [ "$show_preview" = true ]; then
        fzf_opts+=(
            --preview="$preview_cmd"
            --preview-window=right:45%:wrap
        )
    fi

    # Add initial query if provided
    if [ -n "$initial_query" ]; then
        fzf_opts+=(--query="$initial_query")
    fi

    local selected
    selected=$(echo "$formatted" | fzf "${fzf_opts[@]}")

    if [ -z "$selected" ]; then
        exit 0
    fi

    # Extract worktree name (first column)
    local target=$(echo "$selected" | awk '{print $1}')

    # Output for shell function to cd
    if [ "$target" = "@" ]; then
        echo "$MAIN_APP"
    else
        get_worktree_path "$target"
    fi
}

# Internal command: _select
# Used by shell function to get selection and cd
cmd__select() {
    local result
    result=$(cmd_select "$@")
    echo "$result"
}

# Command: for-each
# Run command in all worktrees
# Usage: pwt for-each <command...>
# If command is a Pwtfile function, runs it via run_pwtfile
cmd_for_each() {
    # Handle --help
    if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
        echo "Usage: pwt for-each <command...>"
        echo ""
        echo "Run a command in all worktrees."
        echo ""
        echo "Arguments:"
        echo "  command    Command to run in each worktree"
        echo ""
        echo "Notes:"
        echo "  - If command is a Pwtfile function, runs it via Pwtfile"
        echo "  - Skips main app, only runs in worktrees"
        echo ""
        echo "Examples:"
        echo "  pwt for-each git status"
        echo "  pwt for-each npm test"
        echo "  pwt for-each migrate      # Runs Pwtfile migrate()"
        return 0
    fi

    if [ $# -eq 0 ]; then
        pwt_error "Error: No command specified"
        echo "Usage: pwt for-each <command...>"
        exit 1
    fi

    local first_arg="$1"
    local is_pwtfile_cmd=false

    # Check if first arg is a Pwtfile command
    if has_pwtfile_command "$first_arg"; then
        is_pwtfile_cmd=true
    fi

    local cmd="$*"
    local count=0

    # Helper to run command in a worktree
    _run_in_worktree() {
        local wt_name="$1"
        local wt_path="$2"
        shift 2

        if [ "$is_pwtfile_cmd" = true ]; then
            # Run as Pwtfile command
            PWT_WORKTREE="$wt_name"
            PWT_WORKTREE_PATH="$wt_path"
            PWT_PORT=$(get_metadata "$wt_name" "port" 2>/dev/null || echo "")
            PWT_BRANCH=$(get_metadata "$wt_name" "branch" 2>/dev/null || echo "")
            export PWT_ARGS="$(_strip_pwt_execution_flags "${@:2}")"  # All args after command name
            (
                cd "$wt_path" || exit 1
                run_pwtfile "$first_arg"
            )
        else
            # Run as shell command
            (
                cd "$wt_path" || exit 1
                export PWT_WORKTREE="$wt_name"
                export PWT_WORKTREE_PATH="$wt_path"
                eval "$cmd"
            )
        fi
    }

    # Run in main app
    echo -e "${BLUE}=== @ (main) ===${NC}"
    _run_in_worktree "@" "$MAIN_APP" "$@"
    echo ""

    # Run in worktrees
    if [ -d "$WORKTREES_DIR" ] && [ "$(ls -A "$WORKTREES_DIR" 2>/dev/null)" ]; then
        for dir in "$WORKTREES_DIR"/*/; do
            [ -d "$dir" ] || continue
            local name=$(basename "$dir")
            count=$((count + 1))

            echo -e "${BLUE}=== $name ===${NC}"
            _run_in_worktree "$name" "$dir" "$@"
            echo ""
        done
    fi

    echo -e "${GREEN}✓ Ran in $((count + 1)) worktrees${NC}"
}

# Command: steps
# List available steps from Pwtfile (functions prefixed with step_)
# Usage: pwt steps
cmd_steps() {
    if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
        echo "Usage: pwt steps"
        echo ""
        echo "List available steps from the project Pwtfile."
        echo ""
        echo "Steps are functions prefixed with 'step_' in the Pwtfile."
        echo "Run them with: pwt step <name>"
        echo ""
        echo "Example Pwtfile:"
        echo "  step_install() { bundle install; }"
        echo "  step_seed()    { rails db:seed; }"
        return 0
    fi

    detect_project
    local pwtfile=$(get_project_pwtfile)

    if [ -z "$pwtfile" ] || [ ! -f "$pwtfile" ]; then
        echo -e "${RED}No Pwtfile found${NC}"
        return 1
    fi

    # Extract step_* function names
    local steps
    steps=$(grep -oE "^step_[a-zA-Z0-9_]+" "$pwtfile" 2>/dev/null | sed 's/^step_//' | sort -u || true)

    if [ -z "$steps" ]; then
        echo -e "${YELLOW}No steps found in Pwtfile${NC}"
        echo "Steps are functions prefixed with 'step_', e.g.:"
        echo "  step_install_deps() { bundle install; }"
        return 0
    fi

    echo -e "${BLUE}Steps ($CURRENT_PROJECT):${NC}"
    echo "$steps" | while read -r step; do
        echo "  $step"
    done
    echo ""
    echo "Usage: pwt step <name>"
}

# Command: step
# Run a single step from Pwtfile
# Usage: pwt step <name> [args...]
cmd_step() {
    local step_name="${1:-}"

    if [[ "$step_name" == "-h" || "$step_name" == "--help" ]]; then
        echo "Usage: pwt step <name> [args...]"
        echo ""
        echo "Run a single step from the project Pwtfile."
        echo ""
        echo "Arguments:"
        echo "  name    Step name (without step_ prefix)"
        echo "  args    Extra arguments (available as \$PWT_ARGS)"
        echo ""
        echo "Examples:"
        echo "  pwt step install          # run step_install()"
        echo "  pwt step seed --fresh     # run step_seed() with args"
        echo "  pwt steps                 # list available steps"
        return 0
    fi

    if [ -z "$step_name" ]; then
        echo "Usage: pwt step <name>"
        echo "Run 'pwt steps' to list available steps"
        return 1
    fi

    detect_project
    local pwtfile=$(get_project_pwtfile)
    local func_name="step_${step_name}"

    if [ -z "$pwtfile" ] || [ ! -f "$pwtfile" ]; then
        echo -e "${RED}No Pwtfile found${NC}"
        return 1
    fi

    if ! has_command_in_file "$pwtfile" "$func_name"; then
        echo -e "${RED}Step not found: $step_name${NC}"
        echo "Run 'pwt steps' to list available steps"
        return 1
    fi

    # Set up context from current directory
    local current_dir=$(pwd -P)
    local resolved_worktrees_dir=""
    local resolved_main_app=""

    if [ -n "$WORKTREES_DIR" ] && [ -d "$WORKTREES_DIR" ]; then
        resolved_worktrees_dir=$(cd "$WORKTREES_DIR" 2>/dev/null && pwd -P)
    fi
    if [ -n "$MAIN_APP" ] && [ -d "$MAIN_APP" ]; then
        resolved_main_app=$(cd "$MAIN_APP" 2>/dev/null && pwd -P)
    fi

    if [ -n "$resolved_worktrees_dir" ] && [[ "$current_dir" == "$resolved_worktrees_dir"/* ]]; then
        PWT_WORKTREE=$(basename "$current_dir")
        PWT_WORKTREE_PATH="$current_dir"
        PWT_PORT=$(get_metadata "$PWT_WORKTREE" "port" 2>/dev/null || echo "")
        PWT_BRANCH=$(get_metadata "$PWT_WORKTREE" "branch" 2>/dev/null || echo "")
    elif [ -n "$resolved_main_app" ] && [[ "$current_dir" == "$resolved_main_app"* ]]; then
        PWT_WORKTREE="@"
        PWT_WORKTREE_PATH="$resolved_main_app"
        PWT_PORT=""
        PWT_BRANCH=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "master")
    else
        PWT_WORKTREE="@"
        PWT_WORKTREE_PATH="${resolved_main_app:-$MAIN_APP}"
        PWT_PORT=""
        PWT_BRANCH=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "master")
    fi

    shift  # Remove step name
    export PWT_ARGS="$(_strip_pwt_execution_flags "$@")"

    run_single_pwtfile "$pwtfile" "$func_name" "Step"
}

# =============================================================================
# Jobs Management
# =============================================================================

# Command: jobs
# Manage background jobs started with --bg
cmd_jobs() {
    load_module jobs
    local subcmd="${1:-list}"
    case "$subcmd" in
        -h|--help|help)
            echo "Usage: pwt jobs [subcommand]"
            echo ""
            echo "Manage background jobs started with --bg."
            echo ""
            echo "Subcommands:"
            echo "  list              List all jobs (default)"
            echo "  logs <job-id> [-f] View job output (-f to follow)"
            echo "  stop <job-id>     Stop a running job"
            echo "  stop --all        Stop all running jobs"
            echo "  clean             Remove stale job entries"
            echo "  help              Show this help"
            echo ""
            echo "Examples:"
            echo "  pwt server --bg              # start server in background"
            echo "  pwt jobs                     # list running jobs"
            echo "  pwt jobs logs abc123         # view job output"
            echo "  pwt jobs logs abc123 -f      # follow job output"
            echo "  pwt jobs stop abc123         # stop a job"
            echo "  pwt jobs stop --all          # stop all jobs"
            return 0
            ;;
        list|ls)
            _jobs_list_formatted
            ;;
        logs|log)
            local follow=false
            if [ "${3:-}" = "-f" ] || [ "${3:-}" = "--follow" ]; then
                follow=true
            fi
            _tail_job_log "${2:-}" "$follow"
            ;;
        stop)
            if [ "${2:-}" = "--all" ]; then
                _stop_all_jobs
            else
                _stop_job "${2:-}"
            fi
            ;;
        clean)
            _clean_jobs
            ;;
        *)
            pwt_error "Unknown jobs subcommand: $subcmd"
            echo "Run 'pwt jobs help' for usage"
            return $EXIT_USAGE
            ;;
    esac
}

# =============================================================================
# Help System
# =============================================================================

# Show concepts section
show_help_concepts() {
    cat << 'EOF'
CONCEPTS
--------
pwt manages git worktrees with automatic port allocation. Each worktree is an
isolated copy of your repo where you can work on a different branch without
switching branches in your main checkout.

Key concept: "pwt use" swaps a symlink. Your editor stays open pointing to
$(pwt current), and when you switch worktrees, it sees different code.

Worktree vs Clone:
  Worktree (default)     Faster, shares git objects, saves disk space
  Clone (--clone)        Full isolation, no branch locks, better for submodules

When to use worktrees:
  - Working on multiple features/tickets simultaneously
  - Testing changes without affecting main development
  - Code review while keeping your work intact
  - Comparing behavior between branches

EOF
}

# Show commands section
show_help_commands() {
    cat << 'EOF'
COMMANDS
--------
Worktree Management:
  create|add <name> [base] [desc] Create new worktree (-e -a --from --clone)
  track <remote-branch> [--name] Create worktree tracking existing remote branch
  adopt [path]                   Register existing git worktree and run setup
  setup [path]                   Run/adopt standard setup for existing worktree
  remove [worktree] [flags]      Remove worktree (--with-branch, --force-branch)
  list [flags]                   List worktrees (-v, --dirty, --porcelain)
  tree [--all|--dirty|--ports]   Visual tree view of worktrees
  info [worktree]                Show worktree details
  status                         Interactive TUI dashboard (like htop)
  select [--preview]             Interactive worktree selector (fzf)
  auto-remove [target] [--execute] Remove worktrees merged into target

Navigation:
  cd [worktree|@|-]              Navigate to worktree (@ main, - previous)
  use <worktree>                 Set worktree as current (atomic symlink swap)
  pick                           Interactive select + auto-use (fzf)
  current [--name|--resolved]    Show current worktree path

Development:
  server [worktree]              Start dev server (from Pwtfile)
  gateway [action]               Stable gateway to a worktree server
  servers [--all]                Show project server status
  run <worktree> <cmd>           Run command in worktree
  for-each <cmd>                 Run command in all worktrees
  shell [worktree]               Open subshell in worktree
  editor [worktree]              Open worktree in editor
  ai [worktree] [-- args]        Start AI tool in worktree

Files & Comparison:
  diff <wt1> [wt2]               Show diff between worktrees
  copy <src> <dest> <patterns>   Copy files between worktrees

Project Setup:
  init [url]                     Initialize project (clone URL or configure repo)
  discover [path]                Find unconfigured git repos
  project [action] [args]        Manage project configs
  doctor                         Check system health and configuration

Other:
  port                           Show current port
  port --find <port>             Find who uses a port
  fix-port [worktree]            Fix port conflict
  restore [backup] [worktree]    Recover backed up changes from trash
  meta [action] [args]           Manage worktree metadata
  plugin [action]                Manage plugins (list, install, create)
  shell-init [shell]             Output shell integration code

EOF
}

# Show navigation section
show_help_navigation() {
    cat << 'EOF'
NAVIGATION
----------
pwt cd <worktree>        Go to worktree directory
pwt cd @                 Go to main app (original checkout)
pwt cd -                 Go to previous worktree
pwt use <worktree>       Switch symlink (editor stays open at $(pwt current))

Shell Integration (add to ~/.zshrc):
  eval "$(pwt shell-init)"

After shell-init:
  $PWT_WORKTREE          Current worktree name (when in worktree)
  $PWT_PREVIOUS_PATH     Previous path (enables 'pwt cd -')

Multi-Project Navigation:
  pwt auto-detects project from current directory. From anywhere:
    pwt myproject list
    pwt myproject create TICKET-123 main
    pwt --project myproject list

  Project as first arg or use --project flag.

EOF
}

# Show Pwtfile section
show_help_pwtfile() {
    cat << 'EOF'
PWTFILE
-------
Pwtfile is a bash file in your project root defining lifecycle hooks and
custom commands:

Lifecycle Hooks:
  setup()       Runs after worktree created
  server()      Runs when "pwt server" called (use exec for stdin)
  teardown()    Runs when worktree removed

Custom Commands:
  run_xxx()     Callable with "pwt xxx"
  xxx()         Also works! Any function becomes "pwt xxx"

Steps (for guided workflows):
  step_xxx()    Define with step_, list with "pwt steps", run with "pwt step xxx"

Configuration:
  PORT_BASE=4001         First port for worktrees (main app typically uses 4000)

Environment Variables (available in Pwtfile):
  $PWT_PORT              Allocated port for this worktree (e.g., 5001)
  $PWT_WORKTREE          Worktree name
  $PWT_WORKTREE_PATH     Full path to worktree
  $PWT_BRANCH            Git branch name
  $PWT_TICKET            Extracted ticket number (from branch name)
  $PWT_PROJECT           Project name
  $PWT_ARGS              Arguments passed to custom commands
  $MAIN_APP              Path to main app (useful for copying files)

Helper Functions:
  pwtfile_symlink "path"      Symlink from main (e.g., node_modules)
  pwtfile_copy "path"         Copy from main (e.g., .env)
  pwtfile_replace_literal     Safe string replacement
  run <cmd>                   Run command (silent on error)

Tips:
  - Use "exec" in server() to keep stdin open for interactive commands
  - Copy secrets from $MAIN_APP: cp "$MAIN_APP/config/master.key" config/
  - For strict Pwtfiles (set -u), all PWT_* vars are pre-defined

Example (Rails):
  PORT_BASE=4001

  setup() {
      [ -f "$MAIN_APP/config/master.key" ] && cp "$MAIN_APP/config/master.key" config/
      bundle install --quiet
      bin/rails db:prepare
  }
  server() { exec env PORT="$PWT_PORT" bin/dev; }
  console() { bin/rails console; }
  migrate() { bin/rails db:migrate; }

Example (Elixir/Phoenix):
  PORT_BASE=4001

  setup() {
      mix deps.get --quiet
      mix ecto.create && mix ecto.migrate
  }
  server() { exec env PORT="$PWT_PORT" mix phx.server; }
  iex() { env PORT="$PWT_PORT" iex -S mix phx.server; }

Example (Node):
  PORT_BASE=3001

  setup() {
      pwtfile_copy ".env"
      npm install
  }
  server() { PORT="$PWT_PORT" npm run dev; }
  browse() { open "http://localhost:$PWT_PORT"; }


GLOBAL PWTFILE
--------------
Location: ~/.pwt/Pwtfile

Commands here work on ALL projects. Project Pwtfile overrides global.
Useful for shared utilities across projects.

EOF
}

# Show full help (all sections)
show_help_all() {
    cat << 'EOF'
================================================================================
                        pwt - Power Worktrees
================================================================================

EOF
    show_help_concepts
    show_help_commands
    show_help_navigation
    show_help_pwtfile
    cat << 'EOF'
PORT SYSTEM
-----------
Each worktree gets a unique port automatically based on PORT_BASE in Pwtfile.
  pwt port                   Show current worktree's port
  pwt port --find 3000       Find which worktree uses port 3000

Port allocation: main app uses PORT_BASE (e.g., 4000), worktrees get
PORT_BASE + 1, +2, etc. (e.g., 4001, 4002, 4003).


QUICK START
-----------
1. Initialize a project:
   pwt init git@github.com:user/app.git   # Clone and configure
   # or in existing repo:
   pwt init                               # Configure current repo

2. Create a worktree:
   pwt create TICKET-123 main "fix bug"   # Create from main branch
   pwt create feature --from v1.2.3       # Create from tag
   pwt create hotfix -e -a                # Create + open editor + start AI

3. Work in worktree:
   pwt cd TICKET-123                      # Navigate to worktree
   pwt server                             # Start dev server
   pwt console                            # Run custom command from Pwtfile

4. Clean up:
   pwt remove TICKET-123                  # Remove when done
   pwt auto-remove main --execute         # Remove all merged worktrees


EXAMPLES
--------
pwt list --dirty                 Only show worktrees with uncommitted changes
pwt run TICKET-123 npm test      Run command in specific worktree
pwt diff TICKET-123              Diff worktree vs main
pwt for-each git status          Run command in all worktrees
pwt restore list                 List available backups
================================================================================
EOF
}

# Show brief help (default)
show_help_brief() {
    echo "pwt - Power Worktrees"
    echo "A tool for managing git worktrees across multiple projects"
    echo ""
    if [ -n "${CURRENT_PROJECT:-}" ]; then
        echo -e "Current project: ${GREEN}$CURRENT_PROJECT${NC}"
        echo ""
    fi
    echo "Commands:"
    echo "  init [url]                     Initialize project (clone URL or configure current repo)"
    echo "  <name> init [url]              Initialize with specific name (pwt pc init <url>)"
    echo "  create|add <name> [base] [desc] Create new worktree (-e -a --from --branch --track)"
    echo "  track <remote-branch> [--name]  Create worktree tracking existing remote branch"
    echo "  adopt [path]                    Register existing git worktree and run setup"
    echo "  setup [path]                    Run/adopt standard setup for existing worktree"
    echo "  list [flags]                   List worktrees (-v/--verbose, --dirty, --porcelain)"
    echo "  list statusline                Output for shell prompts"
    echo "  tree [--all|--dirty|--ports]   Visual tree view of worktrees"
    echo "  status                         Interactive TUI dashboard (like htop)"
    echo "  info [worktree]                Show worktree details"
    echo "  current [--name|--resolved]    Show current (symlink path, --resolved for actual)"
    echo "  use <worktree> [--select]      Set worktree as current (--select for fzf picker)"
    echo "  ps1                            Fast prompt helper (O(1), no git)"
    echo "  remove [worktree] [flags]      Remove worktree (--with-branch, --force-branch)"
    echo "  cd [worktree|@|-] [--select]   Navigate to worktree (--select for fzf picker)"
    echo "  run <worktree> <cmd>           Run command in worktree"
    echo "  for-each <cmd>                 Run command in all worktrees"
    echo "  editor [worktree]              Open worktree in editor"
    echo "  ai [worktree] [-- args]        Start AI tool in worktree"
    echo "  open [worktree]                Open worktree in Finder"
    echo "  diff <wt1> [wt2]               Show diff between worktrees"
    echo "  copy <src> <dest> <patterns>   Copy files between worktrees"
    echo "  server [worktree]              Start dev server (from Pwtfile)"
    echo "  gateway [action]               Stable gateway to a worktree server"
    echo "  servers [--all]                Show project server status"
    echo "  fix-port [worktree]            Fix port conflict"
    echo "  auto-remove [target] [--execute] Remove worktrees merged into target"
    echo "  restore [backup] [worktree]    Recover backed up changes from trash"
    echo "  doctor                         Check system health and configuration"
    echo "  shell-init                     Output shell function for cd integration"
    echo "  meta|m [action] [args]         Manage worktree metadata"
    echo "  alias [name|--clear]           Set project short alias"
    echo "  steps                          List available Pwtfile steps"
    echo "  step <name> [args]             Run a single Pwtfile step"
    echo "  repair|fix [worktree]          Run repair hooks"
    echo "  port [worktree]                Get worktree port number"
    echo "  config [key] [value]           View/set project configuration"
    echo "  project [action] [args]        Manage project configs"
    echo "  plugin [action]                Manage plugins (list, install, create)"
    echo "  jobs [subcommand]              Manage background jobs (list, logs, stop, clean)"
    echo ""
    echo "Global options:"
    echo "  --bg                           Run Pwtfile command in background (daemonize)"
    echo "  --no-input                     Close stdin and set PWT_AGENT=1"
    echo "  --quiet                        Suppress non-essential output"
    echo "  --verbose                      Show detailed output"
    echo ""
    echo "Project selection (in order of priority):"
    echo "  1. pwt <project> <command>     Project as first argument"
    echo "  2. pwt --project <name> ...    Explicit flag"
    echo "  3. Auto-detect from pwd        Inside project or worktree dir"
    echo ""
    echo "First time setup:"
    echo "  pwt project init myproject"
    echo "  pwt project set myproject main_app ~/path/to/app"
    echo "  pwt project set myproject worktrees_dir ~/path/to/worktrees"
    echo "  pwt project set myproject branch_prefix \"user/\""
    echo ""
    echo "Examples:"
    echo "  pwt init git@github.com:user/app.git       # Clone and configure"
    echo "  pwt create TICKET-123 master \"fix\" -e -a  # Create + open editor + start AI"
    echo "  pwt create hotfix --from v1.2.3            # Create from tag"
    echo "  pwt track origin/team/TICKET-123           # Track existing remote branch"
    echo "  pwt create --track origin/team/TICKET-123"
    echo "  pwt create TICKET-123 --branch team/TICKET-123 --from origin/team/TICKET-123"
    echo "  pwt adopt ../repo-worktrees/TICKET-123      # Register manually-created worktree"
    echo "  pwt list --dirty                           # Only show dirty worktrees"
    echo "  pwt run TICKET-123 npm test                # Run command in worktree"
    echo "  pwt diff TICKET-123                        # Diff worktree vs main"
    echo "  pwt auto-remove master                     # Preview cleanup (dry-run by default)"
    echo "  pwt restore list                           # List available backups"
    echo "  pwt doctor                                 # Check configuration"
    echo ""
    echo "Help topics (pwt help <topic>):"
    echo "  concepts    What is pwt, worktree vs clone"
    echo "  commands    Full command reference"
    echo "  navigation  cd, use, multi-project"
    echo "  pwtfile     Syntax, hooks, helpers, examples"
    echo "  all         Everything (good for LLMs)"
    echo ""
    echo "Shell integration (add to ~/.zshrc):"
    echo "  eval \"\$(pwt shell-init)\""
    echo ""
    echo "After shell-init:"
    echo "  \$PWT_WORKTREE      Current worktree name (when in worktree)"
    echo "  \$PWT_PREVIOUS_PATH Previous path (enables 'pwt cd -')"
    echo ""
    echo "Plugins:"
    echo "  Extend pwt with custom commands. Run 'pwt plugin help' for details."
    echo "  Available via plugins: topology, context, benchmark, marker, conflicts, prompt"
}

# Command: help
# Show help with optional topic
cmd_help() {
    local topic="${1:-}"

    # Resolve command aliases
    case "$topic" in
        add) topic="create" ;;
        rm) topic="remove" ;;
        ls) topic="list" ;;
        show) topic="info" ;;
        cp) topic="copy" ;;
        fix) topic="repair" ;;
        cleanup) topic="auto_remove" ;;
        m) topic="meta" ;;
        s) topic="server" ;;
        fix-port) topic="fix_port" ;;
    esac

    case "$topic" in
        ""|brief)
            show_help_brief
            ;;
        concepts)
            show_help_concepts
            ;;
        commands)
            show_help_commands
            ;;
        navigation|nav)
            show_help_navigation
            ;;
        pwtfile|Pwtfile)
            show_help_pwtfile
            ;;
        all|full)
            show_help_all
            ;;
        # Module commands need their module loaded first
        create|remove|repair|auto_remove|track|adopt|setup)
            load_module worktree
            "cmd_$topic" --help
            ;;
        list)
            load_module list
            "cmd_$topic" --help
            ;;
        config|port)
            load_module project
            "cmd_$topic" --help
            ;;
        gateway|servers)
            load_module gateway
            "cmd_$topic" --help
            ;;
        plugin)
            load_module plugin
            "cmd_$topic" --help
            ;;
        meta|cd|select|use|current|info|server|ai|status|run|restore|open|alias|diff|copy|doctor|steps|step|fix_port)
            "cmd_$topic" --help
            ;;
        *)
            echo "Unknown help topic: $topic"
            echo ""
            echo "Available topics:"
            echo "  concepts    What is pwt, worktree vs clone"
            echo "  commands    Full command reference"
            echo "  navigation  cd, use, multi-project"
            echo "  pwtfile     Syntax, hooks, helpers, examples"
            echo "  all         Everything (good for LLMs)"
            echo ""
            echo "Or use: pwt help <command>"
            echo "  e.g. pwt help meta, pwt help cd, pwt help create"
            exit $EXIT_USAGE
            ;;
    esac
}

# Command: shell-init
# Output shell function for cd integration
# Usage: eval "$(pwt shell-init)"
cmd_shell_init() {
    local shell="${1:-}"
    local cmd_name
    cmd_name=$(basename "$0")
    local pwt_path
    pwt_path=$(which "$cmd_name" 2>/dev/null || echo "$0")

    if [ -z "$shell" ]; then
        shell=$(basename "${SHELL:-}")
    fi

    case "$shell" in
        fish)
            cat << 'EOF' | sed "s/__PWT_CMD__/$cmd_name/g"
# pwt shell integration (fish)
# Add to ~/.config/fish/config.fish:
#   pwt shell-init fish | source

function _pwt_is_project
    set -l name $argv[1]
    test -z "$name"; and return 1
    test -d ~/.pwt/projects/$name; and return 0
    for config in ~/.pwt/projects/*/config.json
        test -f "$config"; or continue
        set -l alias_val (jq -r '.alias // empty' "$config" 2>/dev/null)
        if test "$alias_val" = "$name"
            return 0
        end
        jq -e --arg a "$name" '.aliases // [] | index($a) != null' "$config" >/dev/null 2>&1; and return 0
    end
    return 1
end

# Helper: detect project name from current path
function _pwt_detect_project
    set -l path (pwd)

    # Check if in a worktree directory
    if string match -q "*-worktrees/*" -- "$path"
        set -l worktrees_dir (string replace -r '-worktrees/.*' '-worktrees' -- "$path")
        for config in ~/.pwt/projects/*/config.json
            test -f "$config"; or continue
            set -l wt_dir (jq -r '.worktrees_dir // empty' "$config" 2>/dev/null)
            if test "$wt_dir" = "$worktrees_dir"
                basename (dirname "$config")
                return 0
            end
        end
        # Fallback
        basename (dirname "$worktrees_dir")
        return 0
    end

    # Check if in a project's main app directory
    for config in ~/.pwt/projects/*/config.json
        test -f "$config"; or continue
        set -l main_path (jq -r '.path // .main_app // empty' "$config" 2>/dev/null)
        if test -n "$main_path"; and string match -q "$main_path*" -- "$path"
            basename (dirname "$config")
            return 0
        end
    end

    return 1
end

# Helper: update terminal title
function _pwt_update_title --on-variable PWD
    # Check if enabled
    if test "$PWT_TITLE_ENABLED" = "0"
        return 0
    end

    # Get format
    set -l format "$PWT_TITLE_FORMAT"
    if test -z "$format"; and test -f ~/.pwt/config.json
        set format (jq -r '.title.format // empty' ~/.pwt/config.json 2>/dev/null)
        set -l enabled (jq -r '.title.enabled // true' ~/.pwt/config.json 2>/dev/null)
        if test "$enabled" = "false"
            return 0
        end
    end
    test -z "$format"; and set format "{project}:{worktree}"

    # Detect project
    set -l project (_pwt_detect_project)
    if test -z "$project"
        set -e PWT_PROJECT
        set -e PWT_WORKTREE
        return 0
    end

    # Detect worktree
    set -l worktree "@"
    if string match -q "*-worktrees/*" -- (pwd)
        set worktree (basename (pwd))
    end

    # Export vars
    set -gx PWT_PROJECT "$project"
    if test "$worktree" != "@"
        set -gx PWT_WORKTREE "$worktree"
    else
        set -e PWT_WORKTREE
    end

    # Get branch if needed
    set -l branch ""
    if string match -q "*{branch}*" -- "$format"
        set branch (git rev-parse --abbrev-ref HEAD 2>/dev/null; or echo "")
    end

    # Build title
    set -l title "$format"
    set title (string replace -a "{project}" "$project" -- "$title")
    set title (string replace -a "{worktree}" "$worktree" -- "$title")
    set title (string replace -a "{branch}" "$branch" -- "$title")
    set title (string replace -a "{path}" (pwd) -- "$title")
    set title (string replace -a "{dir}" (basename (pwd)) -- "$title")

    # Set terminal title
    printf '\033]0;%s\007' "$title"
end

# Update title on init
_pwt_update_title

function __PWT_CMD__
    set -l is_cd 0
    if test (count $argv) -ge 1
        if test "$argv[1]" = "cd"
            set is_cd 1
        else if test (count $argv) -ge 2; and test "$argv[2]" = "cd"
            set is_cd 1
        else if _pwt_is_project "$argv[1]"
            if test (count $argv) -eq 1
                set is_cd 1
            else if test "$argv[2]" = "-h"; or test "$argv[2]" = "--help"
                command __PWT_CMD__ $argv
                return $status
            else if string match -qr "^@" -- "$argv[2]"
                set is_cd 1
            end
        end
    end

    if test $is_cd -eq 1
        set -l input ""
        set -l project ""
        if test "$argv[1]" = "cd"
            set input "$argv[2]"
        else if test (count $argv) -ge 2; and test "$argv[2]" = "cd"
            set project "$argv[1]"
            set input "$argv[3]"
        else
            set project "$argv[1]"
            if test (count $argv) -ge 2
                set input "$argv[2]"
            else
                set input "@"
            end
        end

        if test "$input" = "-"
            if test -z "$PWT_PREVIOUS_PATH"
                echo "No previous worktree" >&2
                return 1
            end
            set target "$PWT_PREVIOUS_PATH"
        else
            if test -n "$project"
                set target (command __PWT_CMD__ $project _cd $input 2>&1)
            else
                set target (command __PWT_CMD__ _cd $input 2>&1)
            end
            if test $status -ne 0; or test ! -d "$target"
                echo "$target"
                return 1
            end
        end

        if test "$PWD" != "$target"
            set -gx PWT_PREVIOUS_PATH "$PWD"
        end

        cd "$target"

        if string match -q "*-worktrees/*" -- "$target"
            set -gx PWT_WORKTREE (basename "$target")
        else
            set -e PWT_WORKTREE
        end
        return 0
    end

    if test "$argv[1]" = "select"; or test "$argv[2]" = "select"
        set -l target ""
        if test "$argv[1]" = "select"
            set target (command __PWT_CMD__ _select $argv[2..-1] 2>&1)
        else
            set -l project "$argv[1]"
            set target (command __PWT_CMD__ $project _select $argv[3..-1] 2>&1)
        end

        if test $status -ne 0; or test -z "$target"
            test -n "$target"; and echo "$target"
            return 0
        end
        if test ! -d "$target"
            echo "$target"
            return 1
        end
        if test "$PWD" != "$target"
            set -gx PWT_PREVIOUS_PATH "$PWD"
        end
        cd "$target"
        if string match -q "*-worktrees/*" -- "$target"
            set -gx PWT_WORKTREE (basename "$target")
        else
            set -e PWT_WORKTREE
        end
        return 0
    end

    if test "$argv[1]" = "use"; or test "$argv[2]" = "use"
        command __PWT_CMD__ $argv
        set -l exit_code $status

        if string match -q "*/current" -- "$PWD"
            set -l current_path (string replace -r "/current.*" "/current" -- "$PWD")
            if test -L "$current_path"
                cd "$current_path"
                set -gx PWT_WORKTREE (basename (readlink "$current_path"))
            end
        end

        return $exit_code
    end

    command __PWT_CMD__ $argv
end
EOF
            ;;
        *)
            cat << EOF
# $cmd_name shell integration
# Add to ~/.zshrc: eval "\$($cmd_name shell-init zsh)"

# Helper: check if arg is a pwt project
_pwt_is_project() {
    [[ -d ~/.pwt/projects/\$1 ]] && return 0
    # Check aliases
    while IFS= read -r config; do
        [ -f "\$config" ] || continue
        local alias_val=\$(jq -r '.alias // empty' "\$config" 2>/dev/null)
        [[ "\$alias_val" == "\$1" ]] && return 0
        jq -e --arg a "\$1" '.aliases // [] | index(\$a) != null' "\$config" >/dev/null 2>&1 && return 0
    done < <(find ~/.pwt/projects -maxdepth 2 -name config.json 2>/dev/null)
    return 1
}

# Helper: detect project name from current path
_pwt_detect_project() {
    local path="\${1:-\$PWD}"

    # Check if in a worktree directory (path contains -worktrees/)
    if [[ "\$path" == *"-worktrees/"* ]]; then
        # Extract project from worktrees parent dir name: /path/to/project-worktrees/TICKET
        local worktrees_dir="\${path%%-worktrees/*}-worktrees"
        local parent_dir="\${worktrees_dir%/*}"
        local parent_name="\${parent_dir##*/}"
        # Try to find matching project by worktrees_dir
        for config in ~/.pwt/projects/*/config.json; do
            [ -f "\$config" ] || continue
            local wt_dir=\$(jq -r '.worktrees_dir // empty' "\$config" 2>/dev/null)
            if [[ "\$wt_dir" == "\$worktrees_dir" ]]; then
                local config_dir="\${config%/*}"
                echo "\${config_dir##*/}"
                return 0
            fi
        done
        # Fallback: project name from directory
        echo "\$parent_name"
        return 0
    fi

    # Check if in a project's main app directory
    for config in ~/.pwt/projects/*/config.json; do
        [ -f "\$config" ] || continue
        local main_path=\$(jq -r '.path // .main_app // empty' "\$config" 2>/dev/null)
        if [[ -n "\$main_path" && "\$path" == "\$main_path"* ]]; then
            local config_dir="\${config%/*}"
            echo "\${config_dir##*/}"
            return 0
        fi
    done

    return 1
}

# Helper: update terminal title based on current directory
_pwt_update_title() {
    # Check if enabled (default: true)
    if [[ "\${PWT_TITLE_ENABLED:-1}" == "0" ]]; then
        return 0
    fi

    # Get format from env var or config (default: {project}:{worktree})
    local format="\${PWT_TITLE_FORMAT:-}"
    if [[ -z "\$format" && -f ~/.pwt/config.json ]]; then
        format=\$(jq -r '.title.format // empty' ~/.pwt/config.json 2>/dev/null)
        # Check if disabled in config
        local enabled=\$(jq -r '.title.enabled // true' ~/.pwt/config.json 2>/dev/null)
        if [[ "\$enabled" == "false" ]]; then
            return 0
        fi
    fi
    format="\${format:-{project}:{worktree}}"

    # Detect project
    local project=\$(_pwt_detect_project)
    if [[ -z "\$project" ]]; then
        # Not in a pwt project, clear PWT vars
        unset PWT_PROJECT PWT_WORKTREE
        return 0
    fi

    # Detect worktree
    local worktree="@"
    if [[ "\$PWD" == *"-worktrees/"* ]]; then
        worktree="\${PWD##*/}"
    fi

    # Export for other tools
    export PWT_PROJECT="\$project"
    if [[ "\$worktree" != "@" ]]; then
        export PWT_WORKTREE="\$worktree"
    else
        unset PWT_WORKTREE
    fi

    # Get git branch if needed
    local branch=""
    if [[ "\$format" == *"{branch}"* ]]; then
        branch=\$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo "")
    fi

    # Build title with placeholder replacement
    local title="\$format"
    title="\${title//\{project\}/\$project}"
    title="\${title//\{worktree\}/\$worktree}"
    title="\${title//\{branch\}/\$branch}"
    title="\${title//\{path\}/\$PWD}"
    title="\${title//\{dir\}/\${PWD##*/}}"

    # Set terminal title
    printf '\\033]0;%s\\007' "\$title"
}

# Auto-update title on directory change
if [[ -n "\$ZSH_VERSION" ]]; then
    # zsh: use chpwd hook
    autoload -Uz add-zsh-hook 2>/dev/null
    add-zsh-hook chpwd _pwt_update_title 2>/dev/null || {
        # Fallback if add-zsh-hook not available
        chpwd() { _pwt_update_title; }
    }
elif [[ -n "\$BASH_VERSION" ]]; then
    # bash: use PROMPT_COMMAND
    if [[ "\$PROMPT_COMMAND" != *"_pwt_update_title"* ]]; then
        PROMPT_COMMAND="_pwt_update_title\${PROMPT_COMMAND:+; \$PROMPT_COMMAND}"
    fi
fi

# Update title on shell init
_pwt_update_title

$cmd_name() {
    # Check if first arg is project and no command (pwt <project>) or cd command
    local _is_cd=false
    if [[ "\$1" == "cd" ]]; then
        _is_cd=true
    elif [[ "\$2" == "cd" ]]; then
        _is_cd=true
    elif _pwt_is_project "\$1" && [[ -z "\$2" || "\$2" == @* || "\$2" == -h || "\$2" == --help ]]; then
        # pwt <project> without command OR pwt <project> @
        if [[ "\$2" == -h || "\$2" == --help ]]; then
            "$pwt_path" "\$@"
            return \$?
        fi
        _is_cd=true
    fi

    if [[ "\$_is_cd" == true ]]; then
        # pwt cd or pwt <project> cd or pwt <project> - change to worktree directory
        local input target project
        if [[ "\$1" == "cd" ]]; then
            input="\$2"
        elif [[ "\$2" == "cd" ]]; then
            # pwt <project> cd <target>
            project="\$1"
            input="\$3"
        else
            # pwt <project> [worktree] - no cd keyword
            project="\$1"
            input="\${2:-@}"
        fi

        # Handle "-" for previous worktree (like cd -)
        if [[ "\$input" == "-" ]]; then
            if [[ -z "\$PWT_PREVIOUS_PATH" ]]; then
                echo "No previous worktree" >&2
                return 1
            fi
            target="\$PWT_PREVIOUS_PATH"
        else
            if [[ -n "\$project" ]]; then
                target=\$("$pwt_path" "\$project" _cd "\$input" 2>&1)
            else
                target=\$("$pwt_path" _cd "\$input" 2>&1)
            fi
            if [[ \$? -ne 0 ]] || [[ ! -d "\$target" ]]; then
                echo "\$target"
                return 1
            fi
        fi

        # Save current path as previous (if different)
        if [[ "\$PWD" != "\$target" ]]; then
            export PWT_PREVIOUS_PATH="\$PWD"
        fi

        builtin cd "\$target"

        # Set PWT_WORKTREE for prompts and scripts
        if [[ "\$target" == *"-worktrees/"* ]]; then
            export PWT_WORKTREE="\${target##*/}"
        else
            unset PWT_WORKTREE
        fi
    elif [[ "\$1" == "select" ]] || [[ "\$2" == "select" ]]; then
        # pwt select or pwt <project> select - interactive selection with cd
        local target
        if [[ "\$1" == "select" ]]; then
            shift
            target=\$("$pwt_path" _select "\$@" 2>&1)
        else
            # pwt <project> select
            local project="\$1"
            shift 2
            target=\$("$pwt_path" "\$project" _select "\$@" 2>&1)
        fi
        if [[ \$? -ne 0 ]] || [[ -z "\$target" ]]; then
            [[ -n "\$target" ]] && echo "\$target"
            return 0
        fi
        if [[ ! -d "\$target" ]]; then
            echo "\$target"
            return 1
        fi

        # Save current path as previous (if different)
        if [[ "\$PWD" != "\$target" ]]; then
            export PWT_PREVIOUS_PATH="\$PWD"
        fi

        builtin cd "\$target"

        # Set PWT_WORKTREE for prompts and scripts
        if [[ "\$target" == *"-worktrees/"* ]]; then
            export PWT_WORKTREE="\${target##*/}"
        else
            unset PWT_WORKTREE
        fi
    elif [[ "\$1" == "use" ]] || [[ "\$2" == "use" ]]; then
        # pwt use or pwt <project> use - run command then follow symlink if inside current
        "$pwt_path" "\$@"
        local exit_code=\$?

        # If we're inside a 'current' symlink, cd to follow the new target
        if [[ "\$PWD" == *"/current" ]] || [[ "\$PWD" == *"/current/"* ]]; then
            # Re-enter the symlink to follow the new target
            local current_path="\${PWD%%/current*}/current"
            if [[ -L "\$current_path" ]]; then
                builtin cd "\$current_path"
                local _readlink_target=\$(readlink "\$current_path")
                export PWT_WORKTREE="\${_readlink_target##*/}"
            fi
        fi
        return \$exit_code
    else
        "$pwt_path" "\$@"
    fi
}
EOF
            ;;
    esac
}


# Command: setup-shell
# Install shell integration into user's shell config
# Usage: pwt setup-shell
cmd_setup_shell() {
    local shell_name=$(basename "$SHELL")
    local rc_file=""
    local init_line='eval "$(pwt shell-init)"'
    local needs_source=false

    # Detect shell config file
    case "$shell_name" in
        zsh)
            rc_file="$HOME/.zshrc"
            ;;
        bash)
            # macOS uses .bash_profile, Linux uses .bashrc
            if [ -f "$HOME/.bash_profile" ]; then
                rc_file="$HOME/.bash_profile"
            else
                rc_file="$HOME/.bashrc"
            fi
            ;;
        fish)
            rc_file="$HOME/.config/fish/config.fish"
            init_line='pwt shell-init | source'
            ;;
        *)
            echo -e "${RED}Unsupported shell: $shell_name${NC}"
            echo "Add manually to your shell config:"
            echo "  $init_line"
            return 1
            ;;
    esac

    # Check/add shell-init integration
    if grep -q "pwt shell-init" "$rc_file" 2>/dev/null; then
        echo -e "${GREEN}✓ Shell integration already configured${NC}"
    else
        echo "" >> "$rc_file"
        echo "# pwt shell integration (added by pwt setup-shell)" >> "$rc_file"
        echo "$init_line" >> "$rc_file"
        echo -e "${GREEN}✓ Shell integration installed${NC}"
        needs_source=true
    fi

    # Check/add subshell prompt indicator (zsh/bash only)
    if [[ "$shell_name" == "zsh" || "$shell_name" == "bash" ]]; then
        if grep -q "PWT_SHELL" "$rc_file" 2>/dev/null; then
            echo -e "${GREEN}✓ Subshell prompt already configured${NC}"
        else
            echo "" >> "$rc_file"
            echo "# pwt subshell prompt indicator (added by pwt setup-shell)" >> "$rc_file"
            if [[ "$shell_name" == "zsh" ]]; then
                echo '[[ -n "$PWT_SHELL" ]] && PROMPT="%F{blue}(pwt)%f $PROMPT"' >> "$rc_file"
            else
                echo '[[ -n "$PWT_SHELL" ]] && PS1="(pwt) $PS1"' >> "$rc_file"
            fi
            echo -e "${GREEN}✓ Subshell prompt indicator installed${NC}"
            needs_source=true
        fi
    fi

    echo "  File: $rc_file"

    if [ "$needs_source" = true ]; then
        echo ""
        echo "To activate now, run:"
        echo -e "  ${BLUE}source $rc_file${NC}"
        echo ""
        echo "Or restart your terminal."
    fi
}





# Show shell-init tip with instructions for the user's current shell
_show_shell_init_tip() {
    local cmd_name
    cmd_name=$(basename "$0")
    local user_shell
    user_shell=$(basename "${SHELL:-bash}")
    case "$user_shell" in
        fish)
            echo -e "${DIM}Tip: $cmd_name shell-init fish | source  (add to ~/.config/fish/config.fish)${NC}"
            ;;
        zsh)
            echo -e "${DIM}Tip: eval \"\$($cmd_name shell-init zsh)\"  (add to ~/.zshrc)${NC}"
            ;;
        *)
            echo -e "${DIM}Tip: eval \"\$($cmd_name shell-init bash)\"  (add to ~/.bashrc)${NC}"
            ;;
    esac
}

# Handle short flags that look like commands (before the main loop)
case "${1:-}" in
    -v|-V)
        _pwt_show_version
        exit 0
        ;;
    -h)
        set -- "help"
        ;;
    -q)
        PWT_QUIET=true
        shift
        ;;
esac

# Parse global flags
while [[ "${1:-}" == --* ]]; do
    case "$1" in
        --project)
            CURRENT_PROJECT="$2"
            PROJECT_EXPLICIT=true
            shift 2
            ;;
        --help)
            set -- "help"
            break
            ;;
        --version)
            _pwt_show_version
            exit 0
            ;;
        --quiet)
            PWT_QUIET=true
            shift
            ;;
        --verbose)
            PWT_VERBOSE=true
            shift
            ;;
        --bg)
            PWT_BG=true
            shift
            ;;
        --no-input)
            PWT_NO_INPUT=true
            shift
            ;;
        *)
            echo -e "${RED}Unknown option: $1${NC}"
            exit $EXIT_USAGE
            ;;
    esac
done

# Initialize pwt
init_pwt

# Check if first argument is a project name or alias (before command dispatch)
# This allows: pwt myproject list (instead of pwt --project myproject list)
if [ -n "${1:-}" ]; then
    _resolved=$(resolve_project_alias "$1")
    if [ -d "$PWT_PROJECTS_DIR/${_resolved}" ]; then
        # Existing project
        if [ "${2:-}" = "init" ]; then
            # pwt <existing-project> init → show already configured
            echo -e "${YELLOW}Already configured: $_resolved${NC}"
            echo ""
            cat "$PWT_PROJECTS_DIR/${_resolved}/config.json"
            exit 0
        fi
        # Load config and shift for other commands
        CURRENT_PROJECT="$_resolved"
        PROJECT_EXPLICIT=true
        load_project_config "$_resolved"
        shift

        # Check for: pwt <project> <worktree> <pwtfile-cmd|-->
        # e.g., pwt acme @ clean
        #       pwt acme WORKTREE-123 clean
        #       pwt acme @ -- git status
        #       pwt acme 17744 -- git status -- rspec
        if [ -n "${1:-}" ] && [ -n "${2:-}" ]; then
            _wt_target="$1"
            _second_arg="$2"
            _wt_path=""

            # Resolve worktree path (supports @, exact match, and fuzzy match)
            _wt_path=$(resolve_worktree_path "$_wt_target" 2>/dev/null) || true

            if [ -n "$_wt_path" ] && [ -d "$_wt_path" ]; then
                # Check if second arg is -- (run mode: pwt <worktree> -- <cmd>)
                if [ "$_second_arg" = "--" ]; then
                    shift 2  # Remove worktree and -- from args
                    cmd_run "$_wt_target" "$@"
                    exit 0
                fi

                # Check if second arg is a Pwtfile command
                if has_pwtfile_command "$_second_arg"; then
                    shift 2  # Remove worktree and command from args
                    # Set up context and run
                    if [ "$_wt_target" = "@" ]; then
                        PWT_WORKTREE="@"
                        PWT_WORKTREE_PATH="$MAIN_APP"
                        PWT_PORT=""
                        PWT_BRANCH=$(git -C "$MAIN_APP" branch --show-current 2>/dev/null || echo "master")
                    else
                        PWT_WORKTREE="$_wt_target"
                        PWT_WORKTREE_PATH="$_wt_path"
                        _wt_name=$(basename "$_wt_path")
                        PWT_PORT=$(get_metadata "$_wt_name" "port" 2>/dev/null || echo "")
                        PWT_BRANCH=$(get_metadata "$_wt_name" "branch" 2>/dev/null || echo "")
                    fi
                    export PWT_ARGS="$(_strip_pwt_execution_flags "$@")"
                    run_pwtfile "$_second_arg"
                    exit 0
                fi
            fi
            unset _wt_target _second_arg _wt_path
        fi
    elif [ "${2:-}" = "init" ] && [ "$1" != "project" ] && [ "$1" != "gateway" ]; then
        # New project name + init command: pwt <name> init [url]
        # Exclude "project" to allow: pwt project init <name>
        cmd_init_named "$1" "${3:-}"
        exit 0
    fi
    unset _resolved
fi

# Only run main dispatch when executed directly (not when sourced for testing)
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then

# Detect project from current directory (unless already set via --project or first arg)
detect_project

# Check required dependencies
check_dependencies

# Main command dispatch
case "${1:-}" in
    init)
        # Check for --discover flag
        if [ "${2:-}" = "--discover" ]; then
            shift 2  # remove "init" and "--discover"
            cmd_discover "${1:-.}" --init "${@:2}"
        else
            cmd_init "${2:-}"
        fi
        ;;
    discover)
        shift  # remove "discover"
        cmd_discover "$@"
        ;;
    create|add)
        require_project --clone
        shift  # remove "create/add" from args
        load_module worktree
        cmd_create "$@"
        ;;
    track)
        require_project --clone
        shift  # remove "track" from args
        load_module worktree
        cmd_track "$@"
        ;;
    adopt|setup)
        require_project
        _adopt_cmd="$1"
        shift  # remove "adopt/setup" from args
        load_module worktree
        if [ "$_adopt_cmd" = "setup" ]; then
            cmd_setup "$@"
        else
            cmd_adopt "$@"
        fi
        unset _adopt_cmd
        ;;
    list|ls)
        require_project --info-only
        shift  # remove "list" from args
        load_module list
        cmd_list "$@"
        ;;
    tree)
        shift  # remove "tree" from args
        load_module list
        cmd_tree "$@"
        ;;
    status)
        shift  # remove "status" from args
        # Check if --all or --help flags are present - if so, skip project requirement
        _status_needs_project=true
        for _arg in "$@"; do
            case "$_arg" in
                --all|-a|--help|-h) _status_needs_project=false ;;
            esac
        done
        if [ "$_status_needs_project" = true ]; then
            require_project --info-only
        fi
        unset _status_needs_project _arg
        load_module status
        cmd_status "$@"
        ;;
    info|show)
        require_project --info-only
        cmd_info "${2:-}"
        ;;
    current)
        require_project --info-only
        shift  # remove "current" from args
        cmd_current "$@"
        ;;
    use)
        require_project
        shift  # remove "use" from args
        cmd_use "$@"
        ;;
    ps1)
        require_project --info-only
        cmd_ps1
        ;;
    remove|rm)
        require_project
        shift  # remove "remove" from args
        load_module worktree
        cmd_remove "$@"
        ;;
    server|s)
        require_project
        shift
        cmd_server "$@"
        ;;
    gateway)
        require_project
        shift
        load_module gateway
        cmd_gateway "$@"
        ;;
    servers)
        require_project --info-only
        shift
        load_module gateway
        cmd_servers "$@"
        ;;
    repair|fix)
        require_project
        load_module worktree
        cmd_repair "${2:-}"
        ;;
    auto-remove|cleanup)
        require_project
        shift  # remove "auto-remove" from args
        load_module worktree
        cmd_auto_remove "$@"
        ;;
    restore)
        shift  # remove "restore" from args
        cmd_restore "$@"
        ;;
    fix-port)
        require_project
        cmd_fix_port "${2:-}"
        ;;
    meta|m)
        cmd_meta "${2:-}" "${3:-}" "${4:-}" "${5:-}"
        ;;
    alias)
        require_project --info-only
        cmd_alias "${2:-}"
        ;;
    config)
        load_module project
        cmd_config "${2:-}" "${3:-}"
        ;;
    -)
        # Shortcut: pwt - = pwt cd - (go to previous worktree)
        require_project --info-only
        cmd_cd "-"
        _cd_status=$?
        [ -t 1 ] && _show_shell_init_tip >&2
        exit $_cd_status
        ;;
    cd)
        # Public cd - outputs path (use shell-init for actual directory change)
        require_project --info-only
        cmd_cd "${2:-}"
        _cd_status=$?
        # Hint only if interactive (not piped)
        [ -t 1 ] && _show_shell_init_tip >&2
        exit $_cd_status
        ;;
    _cd)
        # Internal: output path for cd (used by shell function)
        require_project --info-only
        cmd_cd "${2:-}"
        ;;
    run)
        require_project
        shift  # remove "run" from args
        cmd_run "$@"
        ;;
    editor|e|editor:*)
        require_project
        _editor_tool=""
        if [[ "$1" == editor:* ]]; then
            _editor_tool="${1#editor:}"
        fi
        shift  # remove "editor" or "editor:tool" from args
        cmd_editor "$_editor_tool" "$@"
        ;;
    ai|ai:*)
        require_project
        _ai_tool=""
        if [[ "$1" == ai:* ]]; then
            _ai_tool="${1#ai:}"
        fi
        shift  # remove "ai" or "ai:tool" from args
        cmd_ai "$_ai_tool" "$@"
        ;;
    open)
        require_project
        cmd_open "${2:-}"
        ;;
    diff)
        require_project
        cmd_diff "${2:-}" "${3:-}"
        ;;
    copy|cp)
        require_project
        shift  # remove "copy" from args
        cmd_copy "$@"
        ;;
    for-each)
        require_project
        shift  # remove "for-each" from args
        cmd_for_each "$@"
        ;;
    steps)
        require_project
        cmd_steps
        ;;
    step)
        require_project
        shift  # remove "step" from args
        cmd_step "$@"
        ;;
    doctor)
        shift  # remove "doctor" from args
        cmd_doctor "$@"
        ;;
    jobs)
        shift
        cmd_jobs "$@"
        ;;
    select|_select)
        require_project --info-only
        shift
        cmd__select "$@"
        ;;
    shell-init)
        cmd_shell_init "${2:-}"
        ;;
    setup-shell)
        cmd_setup_shell
        ;;
    claude-setup)
        shift  # remove "claude-setup" from args
        load_module claude
        cmd_claude_setup "$@"
        ;;
    project)
        load_module project
        cmd_project "${2:-}" "${3:-}" "${4:-}" "${5:-}"
        ;;
    port)
        load_module project
        cmd_port "${2:-}"
        ;;
    version)
        _pwt_show_version
        ;;
    "")
        # No command: if project was explicitly specified, cd to main
        if [ "$PROJECT_EXPLICIT" = true ]; then
            cmd_cd "@"
            exit $?
        fi
        # If inside a project, show list
        if [ -n "$CURRENT_PROJECT" ]; then
            load_module list
            cmd_list
            exit $?
        fi
        # Otherwise show brief help
        echo "pwt - Power Worktrees"
        echo "A tool for managing git worktrees across multiple projects"
        echo ""
        echo "Run 'pwt help' for full usage"
        ;;
    -h|--help|help)
        shift || true
        cmd_help "${1:-}"
        ;;
    plugin)
        shift
        load_module plugin
        cmd_plugin "$@"
        ;;
    *)
        # Try to run as Pwtfile command
        _pwtfile_cmd="$1"
        shift

        # 1. Check for plugin first (global extension)
        # Search plugin directories: user first, then system
        _plugin=""
        _find_plugin_in_dirs() {
            local name="$1"
            local dirs=("$PWT_DIR/plugins")

            # Check all possible system plugin locations
            # Homebrew (check if installed)
            if command -v brew &>/dev/null; then
                local brew_prefix
                brew_prefix="$(brew --prefix 2>/dev/null)"
                [ -d "$brew_prefix/share/pwt/plugins" ] && dirs+=("$brew_prefix/share/pwt/plugins")
            fi

            # Standard Homebrew locations
            [ -d "/opt/homebrew/share/pwt/plugins" ] && dirs+=("/opt/homebrew/share/pwt/plugins")
            [ -d "/usr/local/share/pwt/plugins" ] && dirs+=("/usr/local/share/pwt/plugins")

            # ~/.local (common PREFIX for source/curl installs)
            [ -d "$HOME/.local/share/pwt/plugins" ] && dirs+=("$HOME/.local/share/pwt/plugins")

            # Search all directories (unique)
            local seen=""
            for dir in "${dirs[@]}"; do
                [[ " $seen " =~ " $dir " ]] && continue
                seen+=" $dir "
                local plugin="$dir/pwt-$name"
                if [ -x "$plugin" ]; then
                    echo "$plugin"
                    return 0
                fi
            done
            return 1
        }

        _plugin=$(_find_plugin_in_dirs "$_pwtfile_cmd") || true
        if [ -n "$_plugin" ] && [ -x "$_plugin" ]; then
            # Export context for plugin
            export PWT_PLUGIN_NAME="$_pwtfile_cmd"
            export PWT_VERSION="$PWT_VERSION"
            export PWT_DIR="$PWT_DIR"

            # Project context (may be empty if no project detected)
            if detect_project 2>/dev/null; then
                export PWT_PROJECT="$CURRENT_PROJECT"
                export PWT_MAIN_APP="$MAIN_APP"
                export PWT_WORKTREES_DIR="$WORKTREES_DIR"
                export PWT_DEFAULT_BRANCH="${DEFAULT_BRANCH:-master}"
                export PWT_BASE_PORT="$BASE_PORT"
                export PWT_BRANCH_PREFIX="${BRANCH_PREFIX:-}"

                # Current worktree context (if in one)
                if [ -n "${PWT_WORKTREE:-}" ]; then
                    export PWT_WORKTREE
                    export PWT_WORKTREE_PATH="${WORKTREES_DIR}/${PWT_WORKTREE}"
                    export PWT_PORT=$(get_metadata "$PWT_WORKTREE" "port" 2>/dev/null || echo "")
                    export PWT_BRANCH=$(git -C "${WORKTREES_DIR}/${PWT_WORKTREE}" branch --show-current 2>/dev/null || echo "")
                fi
            fi

            # Execute plugin
            exec "$_plugin" "$@"
        fi

        # 2. Check if project has Pwtfile with this function
        if has_pwtfile_command "$_pwtfile_cmd"; then
            require_project --info-only

            # If project was explicitly specified (pwt <project> <cmd>), require worktree
            # e.g., "pwt acme clean" should error, use "pwt acme @ clean" instead
            if [ "$PROJECT_EXPLICIT" = true ]; then
                pwt_error "Error: Pwtfile command '$_pwtfile_cmd' requires worktree context"
                echo ""
                echo "Usage:"
                echo "  pwt $CURRENT_PROJECT @ $_pwtfile_cmd              # Run in main app"
                echo "  pwt $CURRENT_PROJECT <worktree> $_pwtfile_cmd     # Run in specific worktree"
                echo "  pwt $CURRENT_PROJECT for-each $_pwtfile_cmd       # Run in all worktrees"
                echo "  cd <worktree> && pwt $_pwtfile_cmd                # Run in current directory"
                exit 1
            fi

            run_pwtfile_command "$_pwtfile_cmd" "$@"
        else
            echo -e "${RED}Unknown command: $_pwtfile_cmd${NC}"
            # Try to suggest similar commands (include installed plugins)
            _known_commands="create track adopt setup list tree info current use pick select remove cd run shell editor ai open diff copy server gateway servers fix-port auto-remove restore doctor meta project plugin port help version for-each jobs"
            # Add installed plugins from all directories to suggestions
            _get_all_plugin_dirs() {
                echo "$PWT_DIR/plugins"
                # Homebrew locations
                [ -d "/opt/homebrew/share/pwt/plugins" ] && echo "/opt/homebrew/share/pwt/plugins"
                [ -d "/usr/local/share/pwt/plugins" ] && echo "/usr/local/share/pwt/plugins"
                # ~/.local
                [ -d "$HOME/.local/share/pwt/plugins" ] && echo "$HOME/.local/share/pwt/plugins"
            }
            while IFS= read -r _pdir; do
                [ -d "$_pdir" ] || continue
                for _p in "$_pdir"/pwt-*; do
                    [ -x "$_p" ] && _known_commands+=" $(basename "$_p" | sed 's/^pwt-//')"
                done
            done < <(_get_all_plugin_dirs | sort -u)
            _suggestion=$(find_similar "$_pwtfile_cmd" "$_known_commands")
            if [ -n "$_suggestion" ]; then
                echo -e "Did you mean: ${GREEN}pwt $_suggestion${NC}?"
            fi
            echo "Run 'pwt help' for usage"
            exit $EXIT_USAGE
        fi
        ;;
esac

fi  # End of source guard
