#!/usr/bin/env bash
# gitree — bare-repo worktree manager
#
# Works inside any bare-repo-with-worktrees project, or anywhere inside one.
# Reads a .gitree config file (walked up from cwd) for workspace-level settings.
#
# Usage:
#   gitree                                  list worktrees and load state
#   gitree list                             same
#   gitree init                             init current dir as gitree workspace root
#   gitree add [<repo>] [<name>|.]          add a repo as a bare worktree project
#   gitree new [<project>] <branch>         create a branch and its worktree
#   gitree convert multi|mono               convert between single- and multi-repo layouts
#   gitree audit [<project>]                show branches that have no worktree
#   gitree goto [<project>] <branch>        cd into a worktree (via shell function)
#   gitree wt [<project>] <branch>          get or create a worktree; print its path
#   gitree remove [<project>] <branch>      remove a worktree safely
#   gitree switch [-l <loc>] [<project>] [<branch>]  point switch symlink at a worktree
#   gitree pull [<project>|*]               fetch + fast-forward main worktree
#   gitree sync [--all|--repo] [--dry-run]  rebase worktree(s) onto @{upstream} + push
#   gitree lock [<project>|*] [<worktree>]  lock a worktree against branch switching
#   gitree unlock [<project>|*] [<worktree>] remove the lock
#   gitree install [--shell-only|--no-shell] [<dir>]  install gitree globally and the wt() shell fn
#   gitree completion <bash|zsh>            print shell completion script
#
# .gitree config (JSON, at workspace root):
#   { "switch": { "default": "/path/", "wpml": "/other/" } }
#
# Shell function — add to your rc for one-word worktree navigation:
#   wt() { local d; d=$(gitree wt "$@") && cd "$d"; }
#
# `gitree install` writes this for you if it can.

set -euo pipefail

GITREE_VERSION="1.1.0"

# ── Platform helpers ──────────────────────────────────────────────────────────

# Cross-platform sed -i (BSD vs GNU)
_sed_i() {
    if sed --version 2>&1 | grep -q GNU 2>/dev/null; then
        sed -i "$@"
    else
        sed -i '' "$@"
    fi
}

# ── Workspace / plugin detection ──────────────────────────────────────────────

_find_workspace_root() {
    local dir="${1:-$PWD}"
    while [[ "$dir" != "/" ]]; do
        [[ -f "$dir/.gitree" ]] && { printf '%s' "$dir"; return 0; }
        dir="$(dirname "$dir")"
    done
    return 1
}

_find_plugin_dir() {
    local dir="${1:-$PWD}"
    while [[ "$dir" != "/" ]]; do
        [[ -d "$dir/.bare" ]] && { printf '%s' "$dir"; return 0; }
        dir="$(dirname "$dir")"
    done
    return 1
}

_load_config() {
    local file="$1/.gitree"
    [[ -f "$file" ]] || return 0

    # Detect JSON vs legacy key=value
    if python3 -c "import json; json.load(open('$file'))" 2>/dev/null; then
        GITREE_CONFIG_FILE="$file"
        GITREE_CONFIG_JSON=1
        # Populate SWITCH_PATH from switch.default for backward compat
        SWITCH_PATH=$(python3 - "$file" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
sw = d.get("switch", {})
if isinstance(sw, dict):   print(sw.get("default", ""))
elif isinstance(sw, str):  print(sw)
PY
        )
        SWITCH_PATH="${SWITCH_PATH/#\~/$HOME}"
    else
        GITREE_CONFIG_FILE="$file"
        GITREE_CONFIG_JSON=0
        while IFS='=' read -r key val; do
            [[ -z "$key" || "$key" =~ ^[[:space:]]*# ]] && continue
            key="${key// /}"
            val="${val/#\~/$HOME}"
            export "$key"="$val"
        done < "$file"
    fi
}

# Return the switch path for a named location ("default" if omitted).
# Works with both JSON and legacy formats.
_get_switch_path() {
    local location="${1:-default}"
    local project="${2:-}"
    if [[ "${GITREE_CONFIG_JSON:-0}" == "1" && -f "${GITREE_CONFIG_FILE:-}" ]]; then
        local p
        p=$(python3 - "$GITREE_CONFIG_FILE" "$location" "$project" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
loc, project = sys.argv[2], sys.argv[3]
sw = d.get("switch", {})
if isinstance(sw, dict):
    entry = sw.get(loc, "")
    if isinstance(entry, dict):
        # Per-project map: project override, else "default" fallback
        path = entry.get(project, "") if project else ""
        if not path:
            path = entry.get("default", "")
        print(path)
    elif isinstance(entry, str):
        print(entry)
elif isinstance(sw, str) and loc == "default":
    print(sw)
PY
        )
        p="${p/#\~/$HOME}"
        printf '%s' "$p"
    else
        # Legacy: only "default" is valid
        if [[ "$location" == "default" ]]; then
            printf '%s' "${SWITCH_PATH:-}"
        fi
    fi
}

# List all named switch locations as "name<TAB>path" lines.
# For per-project map entries, returns the "default" key (the bare location path).
_list_switch_locations() {
    if [[ "${GITREE_CONFIG_JSON:-0}" == "1" && -f "${GITREE_CONFIG_FILE:-}" ]]; then
        python3 - "$GITREE_CONFIG_FILE" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
sw = d.get("switch", {})
if isinstance(sw, dict):
    for k, v in sw.items():
        if isinstance(v, dict):
            print(f"{k}\t{v.get('default', '')}")
        elif isinstance(v, str):
            print(f"{k}\t{v}")
elif isinstance(sw, str):
    print(f"default\t{sw}")
PY
    elif [[ -n "${SWITCH_PATH:-}" ]]; then
        printf 'default\t%s\n' "$SWITCH_PATH"
    fi
}

# List per-project overrides for a switch location as "project<TAB>path" lines.
# Empty output when the location is a plain string or has no overrides.
_list_switch_overrides() {
    local location="$1"
    [[ "${GITREE_CONFIG_JSON:-0}" == "1" && -f "${GITREE_CONFIG_FILE:-}" ]] || return 0
    python3 - "$GITREE_CONFIG_FILE" "$location" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
loc = sys.argv[2]
entry = d.get("switch", {}).get(loc, "")
if isinstance(entry, dict):
    for k, v in entry.items():
        if k == "default":
            continue
        print(f"{k}\t{v}")
PY
}

# Resolve plugin name + dirs from an optional name hint, falling back to cwd.
# Sets: GITREE_PLUGIN_NAME, GITREE_PLUGIN_DIR, GITREE_BARE_DIR
_resolve_plugin() {
    local name_hint="${1:-}"

    if [[ -n "$name_hint" && -n "${GITREE_WORKSPACE:-}" && -d "$GITREE_WORKSPACE/$name_hint/.bare" ]]; then
        GITREE_PLUGIN_NAME="$name_hint"
        GITREE_PLUGIN_DIR="$GITREE_WORKSPACE/$name_hint"
        GITREE_BARE_DIR="$GITREE_PLUGIN_DIR/.bare"
        return 0
    fi

    local pdir
    if pdir=$(_find_plugin_dir 2>/dev/null); then
        GITREE_PLUGIN_NAME="$(basename "$pdir")"
        GITREE_PLUGIN_DIR="$pdir"
        GITREE_BARE_DIR="$pdir/.bare"
        return 0
    fi

    return 1
}

_branch_to_dirname() { printf '%s' "${1//\//--}"; }

_name_from_repo() {
    local url="$1"
    local name="${url##*/}"
    name="${name%.git}"
    name="${name##*:}"
    printf '%s' "$name"
}

# ── Shell function management ─────────────────────────────────────────────────

GITREE_SHELL_FUNCS='# gitree — worktree navigation
wt()          { local d; d=$(gitree wt   "$@") && cd "$d"; }
gitree-goto() { local d; d=$(gitree goto "$@") && cd "$d"; }'

_detect_shell_rc() {
    case "${SHELL:-}" in
        */zsh)  printf '%s' "$HOME/.zshrc";  return ;;
        */bash) printf '%s' "$HOME/.bashrc"; return ;;
    esac
    [[ -f "$HOME/.zshrc" ]]  && { printf '%s' "$HOME/.zshrc";  return; }
    [[ -f "$HOME/.bashrc" ]] && { printf '%s' "$HOME/.bashrc"; return; }
    return 1
}

_shell_fn_installed() {
    local rc
    rc=$(_detect_shell_rc 2>/dev/null) || return 1
    grep -q 'gitree wt' "$rc" 2>/dev/null
}

_goto_fn_installed() {
    local rc
    rc=$(_detect_shell_rc 2>/dev/null) || return 1
    grep -q 'gitree-goto' "$rc" 2>/dev/null
}

_try_install_shell_fn() {
    local rc
    rc=$(_detect_shell_rc 2>/dev/null) || return 1
    [[ -w "$rc" ]] || return 1
    if _shell_fn_installed; then
        # Already has wt() — patch in gitree-goto() if missing
        if ! _goto_fn_installed; then
            printf "gitree-goto() { local d; d=\$(gitree goto \"\$@\") && cd \"\$d\"; }\n" >> "$rc"
            printf 'Added gitree-goto() to %s — run: source %s\n' "$rc" "$rc" >&2
        fi
        return 0
    fi
    printf '\n%s\n' "$GITREE_SHELL_FUNCS" >> "$rc"
    printf 'Added wt() and gitree-goto() to %s — run: source %s\n' "$rc" "$rc" >&2
}

_print_shell_fn_hint() {
    local path="${1:-}"
    printf '\nCould not write to shell config. Add this to your shell for quick navigation:\n\n' >&2
    printf '%s\n' "$GITREE_SHELL_FUNCS" | sed 's/^/  /' >&2
    if [[ -n "$path" ]]; then
        printf '\nOtherwise, worktree is at:\n\n  cd %s\n\n' "$path" >&2
    fi
}

_ensure_shell_fn() {
    local path="${1:-}"
    if _shell_fn_installed && _goto_fn_installed; then return 0; fi
    _try_install_shell_fn 2>&1 && return 0
    _print_shell_fn_hint "$path"
}

# ── Lock hook management ──────────────────────────────────────────────────────

GITREE_HOOK_MARKER='# [gitree-lock-start]'

_hook_block() {
cat << 'HOOK'
# [gitree-lock-start] — managed by gitree, do not edit this block
_gitree_lock_check() {
    [[ "$3" == "1" ]] || return 0
    local locked; locked=$(git config --get-all gitree.locked 2>/dev/null) || return 0
    [[ -z "$locked" ]] && return 0
    local name; name=$(basename "$(git rev-parse --show-toplevel 2>/dev/null)") || return 0
    printf '%s\n' "$locked" | grep -qx "$name" || return 0
    local gdir; gdir=$(git rev-parse --git-dir 2>/dev/null) || return 0
    local reflog_msg prev_branch new_branch
    reflog_msg=$(git log -g --pretty='%gs' -1 HEAD 2>/dev/null)
    prev_branch=$(printf '%s' "$reflog_msg" | sed 's/checkout: moving from \(.*\) to .*/\1/')
    new_branch=$(printf '%s'  "$reflog_msg" | sed 's/checkout: moving from .* to \(.*\)/\1/')
    if [[ -n "$prev_branch" ]] && git rev-parse --verify "refs/heads/$prev_branch" >/dev/null 2>&1; then
        printf 'ref: refs/heads/%s\n' "$prev_branch" > "$gdir/HEAD"
    else
        printf '%s\n' "$1" > "$gdir/HEAD"
    fi
    git reset --hard --quiet 2>/dev/null
    if [[ -n "$new_branch" && "$new_branch" != "$prev_branch" ]] && \
       git rev-parse --verify "refs/heads/$new_branch" >/dev/null 2>&1; then
        local ref_count
        ref_count=$(git reflog show "refs/heads/$new_branch" 2>/dev/null | wc -l | tr -d ' ')
        if [[ "$ref_count" -le 1 ]]; then
            git branch -D "$new_branch" >/dev/null 2>&1 || true
        fi
    fi
    printf '\n  ✗  %s is locked — branch switching is disabled here.\n' "$name" >&2
    printf '     Use: gitree wt <branch>  to work on another branch.\n\n' >&2
    return 1
}
_gitree_lock_check "$@" || exit 1
# [gitree-lock-end]
HOOK
}

_install_lock_hook() {
    local bare="$1"
    local hook="$bare/hooks/post-checkout"
    mkdir -p "$bare/hooks"

    if [[ -f "$hook" ]] && grep -qE '\[gitree-lock' "$hook" 2>/dev/null; then
        if ! grep -q 'gitree-lock-start' "$hook" 2>/dev/null; then
            _sed_i 's/# \[gitree-lock\] —/# [gitree-lock-start] —/' "$hook"
            printf '# [gitree-lock-end]\n' >> "$hook"
        fi
        return 0
    fi

    if [[ ! -f "$hook" ]]; then
        printf '#!/usr/bin/env bash\n' > "$hook"
    else
        printf '\n' >> "$hook"
    fi

    _hook_block >> "$hook"
    chmod +x "$hook"
}

_remove_lock_hook() {
    local bare="$1"
    local hook="$bare/hooks/post-checkout"
    [[ -f "$hook" ]] || return 0

    local without_block
    without_block=$(sed '/# \[gitree-lock-start\]/,/# \[gitree-lock-end\]/d' "$hook" \
                    | grep -v '^[[:space:]]*$' | grep -v '^#!')

    if [[ -z "$without_block" ]]; then
        rm -f "$hook"
    else
        _sed_i '/# \[gitree-lock-start\]/,/# \[gitree-lock-end\]/d' "$hook"
    fi
}

_lock_project() {
    local bare="$1" pname="$2" wt_name="${3:-main}"
    local already
    already=$(git -C "$bare" config --get-all gitree.locked 2>/dev/null \
              | grep -x "$wt_name" || true)
    if [[ -n "$already" ]]; then
        printf '  %s/%s already locked\n' "$pname" "$wt_name"
        return 0
    fi
    git -C "$bare" config --add gitree.locked "$wt_name"
    _install_lock_hook "$bare"
    printf '✓ Locked %s/%s\n' "$pname" "$wt_name"
}

_unlock_project() {
    local bare="$1" pname="$2" wt_name="${3:-main}"
    git -C "$bare" config --unset-all gitree.locked "$wt_name" 2>/dev/null || true
    local remaining
    remaining=$(git -C "$bare" config --get-all gitree.locked 2>/dev/null || true)
    [[ -z "$remaining" ]] && _remove_lock_hook "$bare"
    printf '✓ Unlocked %s/%s\n' "$pname" "$wt_name"
}

# ── Manifest helpers ─────────────────────────────────────────────────────────

_config_set_project() {
    local name="$1" url="$2"
    [[ "${GITREE_CONFIG_JSON:-0}" == "1" && -f "${GITREE_CONFIG_FILE:-}" ]] || return 0
    python3 - "$GITREE_CONFIG_FILE" "$name" "$url" <<'PY'
import json, sys
path, name, url = sys.argv[1], sys.argv[2], sys.argv[3]
with open(path) as f: d = json.load(f)
d.setdefault("projects", {})[name] = url
with open(path, "w") as f: json.dump(d, f, indent=2)
f.write("\n")
PY
}

_config_remove_project() {
    local name="$1"
    [[ "${GITREE_CONFIG_JSON:-0}" == "1" && -f "${GITREE_CONFIG_FILE:-}" ]] || return 0
    python3 - "$GITREE_CONFIG_FILE" "$name" <<'PY'
import json, sys
path, name = sys.argv[1], sys.argv[2]
with open(path) as f: d = json.load(f)
d.get("projects", {}).pop(name, None)
with open(path, "w") as f: json.dump(d, f, indent=2)
f.write("\n")
PY
}

# ── Workspace AGENTS.md ──────────────────────────────────────────────────────

_write_workspace_agents_md() {
    local workspace="$1"
    local root_agents="$workspace/AGENTS.md"
    local claude_link="$workspace/CLAUDE.md"
    local ctx_agents="$workspace/.gitree-context/AGENTS.md"

    mkdir -p "$workspace/.gitree-context"

    if [[ ! -f "$root_agents" ]]; then
        cat > "$root_agents" << 'AGENTS_EOF'
<!-- Workspace context: .gitree-context/AGENTS.md — read it first; its instructions take precedence over this file. -->

# gitree workspace

gitree manages git repos as bare repos with linked worktrees. Read `.gitree` at the
workspace root for the authoritative project list (`projects` key) and runtime switch
paths (`switch.*` keys).

## Layout

```
<project>/
├── .bare/              ← bare repo: refs, objects, worktree metadata
├── .git                ← 1-line file: "gitdir: ./.bare"
├── main/               ← default working surface (main branch checked out here)
└── .worktrees/         ← additional worktrees, created on demand
    └── <branch>/
```

- **Inside a worktree** (`main/`, `.worktrees/<branch>/`) — all git ops work normally.
- **From the project container** (`<project>/`) — read-only: `git log`, `git branch -a`,
  `git worktree list`. `git status` errors with "must be run in a work tree" — expected.
- **Default working surface is `main/`** unless explicitly directed elsewhere.

## Key commands

```bash
gitree list [<project>]                          # list worktrees and load state
gitree new [<project>] <branch>                  # create branch + worktree
gitree wt [<project>] <branch>                   # get or create worktree; print path
gitree switch [-l <loc>] [<project>] [<branch>]  # point switch symlink at a worktree
gitree remove [<project>] <branch>               # remove a worktree safely
gitree audit [<project>]                         # show branches with no worktree yet
gitree repair-head [<project>|*]                 # fix bare HEAD drift
gitree restore                                   # clone all projects in .gitree manifest
```

Shell functions (`gitree install`): `wt <branch>` — create/enter worktree.
Branch `/` → dir `--`: `feature/foo` → `.worktrees/feature--foo/`. Pass real branch names.

## Multi-session safety

- **Stay in the worktree you were opened in.** Don't `cd` into a sibling without explicit
  instruction.
- **Don't `git checkout <branch>` inside an existing worktree.** Use `gitree wt` instead.
- **Before creating a worktree**, run `git worktree list` — git refuses to add one for a
  branch already checked out elsewhere.
- **Don't `git worktree remove`** without confirming nobody else is working in it.

## Branch context

`.gitree-context/` at the workspace root holds per-branch context for all projects.
Never committed — synced between machines via rsync alongside `.gitree`.

In a non-main worktree, `.branch-context/` is a symlink to
`.gitree-context/<project>/<branch>/` and contains: `README.md`, `AGENTS.md`, `TODO.md`,
`ROADMAP.md`.

**When starting work in a non-main worktree:**
1. Read `.branch-context/README.md` then `.branch-context/AGENTS.md`
2. If both are stubs, ask the user what the branch is for and populate them
3. Keep `TODO.md` current — check off completed items, add new ones as they arise

**Never write `.branch-context/` files in `main/`** — main has no branch context.
A `.branch-context` symlink in `main/` is stale — flag it.

### Context file roles and anti-drift rules

Each piece of information lives in exactly one place at the right level. Higher-level
files define structure and relationships — they do not summarise or echo lower-level
files. When something changes, update it in one place only.

- **AGENTS.md** — operational instructions: conventions, tooling, how-tos. Not a task
  list, not a README summary. Don't duplicate README context.
- **README.md** — persistent context: architecture, purpose, relationships. For humans
  and agents starting cold. No task-level detail.
- **TODO.md** — tasks at this level only. Per-branch work lives in `.branch-context/TODO.md`.
- **ROADMAP.md** — direction at this level only. Per-feature planning belongs in branches.

### Where files live

Only `AGENTS.md` (entry point, auto-regenerated by gitree) and `.gitree` live at the
workspace root. Everything portable lives under `.gitree-context/`:

```
<workspace>/
├── AGENTS.md   .gitree
└── .gitree-context/
    ├── AGENTS.md                              ← workspace operational context
    ├── README.md   TODO.md   ROADMAP.md       ← workspace-wide (optional)
    └── <project>/
        ├── README.md   TODO.md   ROADMAP.md   ← project-level (optional)
        └── <branch>/                          ← per-branch (symlinked from worktrees)
```

Branches always live under `<project>/` — same for mono and multi-repo — so a mono-repo
workspace can be converted to multi-repo later without restructuring.

**Mono-repo loophole:** when the workspace IS the project, the project-level
README/TODO/ROADMAP/AGENTS can live at `.gitree-context/` root instead of under
`<project>/` (since workspace = project anyway). Saves AI agents an extra traversal.
The branch tree under `<project>/<branch>/` stays put.

Export model: rsync `.gitree` + `.gitree-context/` to another machine, run `gitree restore`,
done. `AGENTS.md` at workspace root is regenerated; no need to sync it.
AGENTS_EOF
        printf '✓ Workspace agent guide written (AGENTS.md)\n' >&2
    fi

    if [[ ! -e "$claude_link" ]]; then
        ln -s AGENTS.md "$claude_link"
        printf '✓ CLAUDE.md → AGENTS.md symlink created\n' >&2
    fi

    if [[ ! -f "$ctx_agents" ]]; then
        local ws_name; ws_name=$(basename "$workspace")
        printf '# %s — workspace context\n\n' "$ws_name" > "$ctx_agents"
        printf '<!-- This file overrides/extends the gitree baseline (../AGENTS.md).\n     Define project relationships, team conventions, and custom agent instructions here. -->\n\n' >> "$ctx_agents"
        printf '## Projects\n\n<!-- Describe the projects in this workspace and how they relate to each other. -->\n\n' >> "$ctx_agents"
        printf '## Custom instructions\n\n<!-- Add workspace-specific agent instructions here. -->\n' >> "$ctx_agents"
        printf '✓ Workspace context stub written (.gitree-context/AGENTS.md)\n' >&2
    fi
}

# ── Bare repo setup ───────────────────────────────────────────────────────────

_setup_bare_repo() {
    local repo="$1"
    local target="$2"

    local abs_target
    if [[ "$target" == "." ]]; then
        abs_target="$PWD"
    else
        abs_target="$PWD/$target"
        mkdir -p "$abs_target"
    fi

    local bare="$abs_target/.bare"

    printf 'Cloning %s...\n' "$repo"
    git clone --bare "$repo" "$bare"

    printf 'gitdir: ./.bare\n' > "$abs_target/.git"

    git -C "$bare" config remote.origin.fetch '+refs/heads/*:refs/remotes/origin/*'
    git -C "$bare" fetch --quiet

    local default_branch
    default_branch=$(git -C "$bare" symbolic-ref --short HEAD 2>/dev/null \
        || git -C "$bare" for-each-ref --sort=-committerdate --format='%(refname:short)' \
               refs/heads/ | head -1)
    [[ -z "$default_branch" ]] && default_branch="main"

    # Pin HEAD to the default branch so bare-repo commands never resolve the wrong target
    git -C "$bare" symbolic-ref HEAD "refs/heads/$default_branch"

    printf 'Creating main worktree on branch: %s\n' "$default_branch"
    git -C "$bare" worktree add "../main" "$default_branch"

    git -C "$bare" config --add gitree.locked "main"
    _install_lock_hook "$bare"

    local project_name; project_name=$(basename "$abs_target")
    printf '✓ %s ready — main/ locked\n' "$project_name"

    # Record in workspace manifest (skipped for mono-repo where project IS workspace)
    if [[ "$target" != "." ]]; then
        _config_set_project "$project_name" "$repo"
    fi

    # Initialize branch context directory and ignore rules
    if [[ -n "${GITREE_WORKSPACE:-}" ]]; then
        _ignore_branch_context "$abs_target" "$project_name"
        local pctx; pctx=$(_project_context_root "$project_name")
        mkdir -p "$pctx"
        printf '✓ Branch context ready (%s/)\n' "${pctx#"$GITREE_WORKSPACE"/}"
    fi
}

_repair_worktrees() {
    local pdir="$1"
    [[ -d "$pdir/main" ]] && git -C "$pdir/main" worktree repair 2>/dev/null || true
    if [[ -d "$pdir/.worktrees" ]]; then
        for wt in "$pdir/.worktrees"/*/; do
            [[ -d "$wt" ]] && git -C "$wt" worktree repair 2>/dev/null || true
        done
    fi
}

# ── Worktree get-or-create ────────────────────────────────────────────────────

_resolve_worktree() {
    local bare="$1" pdir="$2" branch="$3"
    local dirname; dirname=$(_branch_to_dirname "$branch")

    local existing
    existing=$(git -C "$bare" worktree list --porcelain | awk -v b="refs/heads/$branch" '
        /^worktree / { wt=$2 }
        $0 == "branch " b { print wt; exit }
    ')

    if [[ -n "$existing" ]]; then
        printf '%s' "$existing"
        return 0
    fi

    if git -C "$bare" rev-parse --verify "refs/heads/$branch" >/dev/null 2>&1; then
        git -C "$bare" worktree add "../.worktrees/$dirname" "$branch" >&2
    elif git -C "$bare" rev-parse --verify "refs/remotes/origin/$branch" >/dev/null 2>&1; then
        git -C "$bare" worktree add "../.worktrees/$dirname" -b "$branch" "origin/$branch" >&2
    else
        printf 'Error: branch %q not found in %s\n' "$branch" "$(basename "$pdir")" >&2
        printf '\nAvailable branches:\n' >&2
        git -C "$bare" for-each-ref --format='  %(refname:short)' refs/heads/ >&2
        return 1
    fi

    printf '%s' "$pdir/.worktrees/$dirname"
}

# ── List helpers ─────────────────────────────────────────────────────────────

_check_head_drift() {
    local bare="$1"
    local bare_head main_branch
    bare_head=$(git -C "$bare" symbolic-ref --short HEAD 2>/dev/null) || return 0
    main_branch=$(git -C "$bare/../main" symbolic-ref --short HEAD 2>/dev/null) || return 0
    [[ "$bare_head" != "$main_branch" ]] || return 0
    printf '  (HEAD drift: bare HEAD=%s, main/=%s — run: gitree repair-head)\n' \
        "$bare_head" "$main_branch"
}

_list_project_worktrees() {
    local bare="$1" pdir="${2%/}"  # strip any trailing slash
    local locked
    locked=$(git -C "$bare" config --get-all gitree.locked 2>/dev/null | tr '\n' ':' || true)

    git -C "$bare" worktree list --porcelain | awk -v locked="$locked" -v pdir="$pdir" '
        BEGIN { n = split(locked, larr, ":") }
        /^worktree / {
            wt = $2
            wtname = wt; sub(".*/", "", wtname)
            display = wt
            if (pdir != "" && substr(wt, 1, length(pdir)) == pdir)
                display = substr(wt, length(pdir) + 2)
        }
        /^branch /   { branch=substr($0,8); sub("refs/heads/","",branch) }
        /^HEAD /     { head=substr($0,6) }
        /^$/ {
            if (wt != "" && wtname != ".bare") {
                lk = ""
                for (i=1; i<=n; i++) if (larr[i] == wtname) { lk = "  [locked]"; break }
                if (branch != "")
                    printf "  %-36s  (branch: %s)%s\n", display, branch, lk
                else
                    printf "  %-36s  (detached: %s)%s\n", display, substr(head,1,10), lk
            }
            wt=""; display=""; branch=""; head=""
        }
    '
}

# Where this project's branch contexts live under .gitree-context/.
# Always .gitree-context/<project>/ — same for mono and multi-repo so that a
# mono-repo workspace can be converted to multi-repo later without restructuring
# the branch context tree. Only the project-level .md files (README/TODO/ROADMAP)
# get the mono-repo "loophole" of living at .gitree-context/ root, since in a
# mono-repo the workspace IS the project — that's a user-managed choice, not
# something gitree creates.
_project_context_root() {
    local project="$1"
    printf '%s' "$GITREE_WORKSPACE/.gitree-context/$project"
}

_ignore_branch_context() {
    local pdir="$1" pname="${2:-$(basename "$pdir")}"

    # 1. Commit .branch-context to main/.gitignore (propagates to clones).
    local gitignore="$pdir/main/.gitignore"
    if ! grep -qx '.branch-context' "$gitignore" 2>/dev/null; then
        if [[ ! -f "$gitignore" ]]; then
            printf '# gitree branch context — local only, never committed\n.branch-context\n' \
                > "$gitignore"
        else
            printf '\n# gitree branch context — local only, never committed\n.branch-context\n' \
                >> "$gitignore"
        fi
        git -C "$pdir/main" add .gitignore
        git -C "$pdir/main" commit -m "chore: ignore .branch-context (gitree)" --quiet
        printf '  %s: committed .branch-context to main/.gitignore\n' "$pname"
    fi

    # 2. Add .branch-context to bare repo's info/exclude — applies to all worktrees
    # regardless of which branch they have checked out. Covers older branches whose
    # .gitignore predates the commit above.
    local exclude="$pdir/.bare/info/exclude"
    if [[ -d "$pdir/.bare/info" ]] && ! grep -qx '.branch-context' "$exclude" 2>/dev/null; then
        [[ -f "$exclude" ]] || touch "$exclude"
        printf '.branch-context\n' >> "$exclude"
        printf '  %s: added .branch-context to .bare/info/exclude\n' "$pname"
    fi
}

_scaffold_branch_context() {
    local project="$1" branch="$2" wt_path="${3:-}"
    [[ -n "${GITREE_WORKSPACE:-}" ]] || return 0
    local dirname; dirname=$(_branch_to_dirname "$branch")
    local context_dir; context_dir="$(_project_context_root "$project")/$dirname"

    mkdir -p "$context_dir"

    local today; today=$(date +%F)

    [[ -f "$context_dir/README.md" ]] || \
        printf '# %s\n\n<!-- What is this branch for? -->\n' \
            "$branch" > "$context_dir/README.md"

    [[ -f "$context_dir/AGENTS.md" ]] || \
        printf '<!-- branch: %s | created: %s -->\n<!-- See README.md for branch overview -->\n' \
            "$branch" "$today" > "$context_dir/AGENTS.md"

    [[ -f "$context_dir/TODO.md" ]] || \
        printf '# TODO — %s\n\n- [ ] \n' "$branch" > "$context_dir/TODO.md"

    [[ -f "$context_dir/ROADMAP.md" ]] || touch "$context_dir/ROADMAP.md"

    # Wire the .branch-context symlink only when a worktree exists for this branch.
    # Branches without worktrees still get their context dir + stub files, ready for
    # whenever a worktree gets created.
    if [[ -n "$wt_path" && -d "$wt_path" ]]; then
        local link="$wt_path/.branch-context"
        local rel
        rel=$(python3 -c "import os; print(os.path.relpath('$context_dir', '$wt_path'))")
        ln -sfn "$rel" "$link"
        printf '✓ Branch context scaffolded + symlinked (%s)\n' "$branch" >&2
    else
        printf '✓ Branch context scaffolded (%s — no worktree yet)\n' "$branch" >&2
    fi
}

# ── Commands ──────────────────────────────────────────────────────────────────

cmd_init() {
    if [[ -f "$PWD/.gitree" ]]; then
        printf 'Already a gitree workspace root (%s/.gitree exists).\n' "$PWD"
        _ensure_shell_fn
        return 0
    fi

    local switch=""
    read -r -p "Switch path for runtime symlinks (or leave blank): " switch || true
    switch="${switch/#\~/$HOME}"
    if [[ -n "$switch" ]]; then
        printf '{\n  "switch": {\n    "default": "%s"\n  }\n}\n' "$switch" > "$PWD/.gitree"
    else
        printf '{}\n' > "$PWD/.gitree"
    fi

    printf '\nInitialized gitree workspace at %s\n' "$PWD"

    GITREE_WORKSPACE="$PWD"
    _load_config "$PWD"

    _write_workspace_agents_md "$PWD"

    _ensure_shell_fn
}

cmd_add() {
    local repo="${1:-}"
    local target="${2:-}"

    if [[ -z "$repo" ]]; then
        read -r -p "Repository URL or path: " repo || true
        [[ -z "$repo" ]] && { printf 'No repository given.\n' >&2; exit 1; }
    fi

    local default_name; default_name=$(_name_from_repo "$repo")

    local mono=false
    if [[ -z "$target" ]]; then
        local yn=""
        read -r -p "Mono-repo layout (clone into workspace root, no subdirectory)? [y/N]: " yn || true
        case "$yn" in
            [Yy]|[Yy][Ee][Ss]) mono=true; target="." ;;
            *)
                local name_input=""
                read -r -p "Project folder name [$default_name]: " name_input || true
                target="${name_input:-$default_name}"
                ;;
        esac
    elif [[ "$target" == "." ]]; then
        mono=true
    fi

    if [[ -z "${GITREE_WORKSPACE:-}" ]]; then
        printf '# gitree workspace\n' > "$PWD/.gitree"
        GITREE_WORKSPACE="$PWD"
        _load_config "$PWD"
        printf 'Initialized gitree workspace at %s\n' "$PWD"
        _write_workspace_agents_md "$PWD"
    fi

    _setup_bare_repo "$repo" "$target"
    _ensure_shell_fn
}

cmd_new() {
    local branch="" name_hint=""
    case $# in
        0) printf 'Usage: gitree new [<project>] <branch>\n' >&2; exit 1 ;;
        1) branch="$1" ;;
        *) name_hint="$1"; branch="$2" ;;
    esac

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project directory, and no project name given\n' >&2; exit 1
    fi

    # Create branch if it doesn't already exist locally or remotely
    if git -C "$GITREE_BARE_DIR" rev-parse --verify "refs/heads/$branch" >/dev/null 2>&1; then
        printf 'Branch already exists: %s\n' "$branch" >&2
    elif git -C "$GITREE_BARE_DIR" rev-parse --verify "refs/remotes/origin/$branch" >/dev/null 2>&1; then
        printf 'Tracking remote branch: %s\n' "$branch" >&2
    else
        local base; base=$(git -C "$GITREE_BARE_DIR" symbolic-ref --short HEAD 2>/dev/null || echo "main")
        git -C "$GITREE_BARE_DIR" branch "$branch" "$base"
        printf 'Created branch %s from %s\n' "$branch" "$base" >&2
    fi

    local path
    path=$(_resolve_worktree "$GITREE_BARE_DIR" "$GITREE_PLUGIN_DIR" "$branch")

    [[ "$branch" != "main" ]] && _scaffold_branch_context "$GITREE_PLUGIN_NAME" "$branch" "$path"
    _ensure_shell_fn "$path"
    printf '%s\n' "$path"
}

cmd_convert() {
    local direction="${1:-}"
    case "$direction" in
        multi) _convert_to_multi ;;
        mono)  _convert_to_mono  ;;
        *)
            printf 'Usage: gitree convert multi|mono\n\n' >&2
            printf '  multi  move the single bare repo into a named subdirectory\n' >&2
            printf '  mono   move the only project subfolder up to workspace root\n' >&2
            exit 1
            ;;
    esac
}

_convert_to_multi() {
    local workspace="${GITREE_WORKSPACE:-}"
    [[ -z "$workspace" ]] && { printf 'Error: not inside a gitree workspace\n' >&2; exit 1; }
    [[ -d "$workspace/.bare" ]] || { printf 'Error: no .bare/ at workspace root — not a mono layout\n' >&2; exit 1; }

    local default_name="repo"
    local remote_url
    remote_url=$(git -C "$workspace/.bare" remote get-url origin 2>/dev/null || true)
    [[ -n "$remote_url" ]] && default_name=$(_name_from_repo "$remote_url")

    local name=""
    read -r -p "Project folder name [$default_name]: " name || true
    name="${name:-$default_name}"

    local dest="$workspace/$name"
    [[ -d "$dest" ]] && { printf 'Error: %s already exists\n' "$dest" >&2; exit 1; }
    mkdir -p "$dest"

    mv "$workspace/.bare" "$dest/.bare"
    mv "$workspace/.git"  "$dest/.git"
    [[ -d "$workspace/main" ]]       && mv "$workspace/main"       "$dest/main"
    [[ -d "$workspace/.worktrees" ]] && mv "$workspace/.worktrees" "$dest/.worktrees"

    _repair_worktrees "$dest"
    printf '✓ Converted to multi-repo — project now at %s/\n' "$dest"
}

_convert_to_mono() {
    local workspace="${GITREE_WORKSPACE:-}"
    [[ -z "$workspace" ]] && { printf 'Error: not inside a gitree workspace\n' >&2; exit 1; }
    [[ -d "$workspace/.bare" ]] && { printf 'Error: already a mono-repo\n' >&2; exit 1; }

    local plugin_dirs=()
    for d in "$workspace"/*/; do
        [[ -d "$d.bare" ]] && plugin_dirs+=("${d%/}")
    done

    [[ ${#plugin_dirs[@]} -eq 0 ]] && { printf 'Error: no project subdirectories found\n' >&2; exit 1; }

    if [[ ${#plugin_dirs[@]} -ne 1 ]]; then
        printf 'Error: convert mono requires exactly one project; found %d:\n' "${#plugin_dirs[@]}" >&2
        for d in "${plugin_dirs[@]}"; do printf '  %s\n' "$(basename "$d")" >&2; done
        exit 1
    fi

    local pdir="${plugin_dirs[0]}"
    printf 'Moving %s to workspace root...\n' "$(basename "$pdir")"

    mv "$pdir/.bare" "$workspace/.bare"
    mv "$pdir/.git"  "$workspace/.git"
    [[ -d "$pdir/main" ]]       && mv "$pdir/main"       "$workspace/main"
    [[ -d "$pdir/.worktrees" ]] && mv "$pdir/.worktrees" "$workspace/.worktrees"
    rmdir "$pdir" 2>/dev/null || true

    _repair_worktrees "$workspace"
    printf '✓ Converted to mono-repo.\n'
}

cmd_list() {
    local name_hint="${1:-}"
    local workspace="${GITREE_WORKSPACE:-}"
    local switch_path="${SWITCH_PATH:-}"
    local single_project=""  # set to project name when showing just one

    _print_project() {
        local pdir="$1"
        printf '\n%s:\n' "$(basename "$pdir")"
        _list_project_worktrees "$pdir/.bare" "$pdir"
        _check_head_drift "$pdir/.bare"
    }

    if [[ -n "$workspace" && -d "$workspace/.bare" ]]; then
        # mono-repo workspace
        _print_project "$workspace"
        single_project=$(basename "$workspace")
    elif [[ -n "$workspace" ]]; then
        # multi-project workspace
        local pdir_hint=""
        # if a name was given, look for that project
        if [[ -n "$name_hint" ]]; then
            if [[ -d "$workspace/$name_hint/.bare" ]]; then
                pdir_hint="$workspace/$name_hint"
            else
                printf 'Error: project "%s" not found in workspace\n' "$name_hint" >&2; exit 1
            fi
        # if inside a plugin subdir, auto-detect which project
        elif pdir_hint=$(_find_plugin_dir 2>/dev/null); then
            : # use auto-detected pdir_hint
        fi

        if [[ -n "$pdir_hint" ]]; then
            _print_project "$pdir_hint"
            single_project=$(basename "$pdir_hint")
        else
            local found=0
            for pdir in "$workspace"/*/; do
                [[ -d "$pdir/.bare" ]] || continue
                found=1
                _print_project "$pdir"
            done
            [[ $found -eq 0 ]] && printf '  (no projects — use `gitree add` to add one)\n'
        fi
    else
        local pdir
        if pdir=$(_find_plugin_dir 2>/dev/null); then
            _print_project "$pdir"
            single_project=$(basename "$pdir")
        else
            printf 'Not inside a gitree workspace. Run `gitree init` to create one.\n' >&2
            exit 1
        fi
    fi

    # Runtime load state — one block per switch location
    local any_locations=0
    while IFS=$'\t' read -r loc_name loc_path; do
        loc_path="${loc_path/#\~/$HOME}"

        if [[ -n "$single_project" ]]; then
            # Single project: resolve per-project path (supports override map)
            local resolved; resolved=$(_get_switch_path "$loc_name" "$single_project")
            [[ -z "$resolved" ]] && continue
            resolved="${resolved/#\~/$HOME}"
            [[ -d "$resolved" ]] || continue
            any_locations=1
            local sym="$resolved/$single_project"
            if [[ -L "$sym" ]]; then
                local target; target=$(readlink "$sym")
                local rel="${target##*"$single_project"/}"
                if [[ "$loc_name" == "default" ]]; then
                    printf '\nRuntime → %s\n' "$rel"
                else
                    printf '\nRuntime [%s] → %s\n' "$loc_name" "$rel"
                fi
            fi
        else
            [[ -z "$loc_path" || ! -d "$loc_path" ]] && continue
            any_locations=1
            if [[ "$loc_name" == "default" ]]; then
                printf '\nRuntime load state (%s):\n' "$loc_path"
            else
                printf '\nRuntime [%s] (%s):\n' "$loc_name" "$loc_path"
            fi
            local printed=0
            for sym in "$loc_path"/*/; do
                sym="${sym%/}"
                [[ -L "$sym" ]] || continue
                local target; target=$(readlink "$sym")
                local name; name=$(basename "$sym")
                [[ -n "$workspace" && "$target" == "$workspace"/* ]] || continue
                local rel="${target##*"$name"/}"
                printf '  %-40s → %s\n' "$name" "$rel"
                printed=1
            done
            # Per-project overrides for this location (not in the default dir)
            while IFS=$'\t' read -r ov_project ov_path; do
                [[ -z "$ov_path" ]] && continue
                ov_path="${ov_path/#\~/$HOME}"
                local ov_sym="$ov_path/$ov_project"
                [[ -L "$ov_sym" ]] || continue
                local ov_target; ov_target=$(readlink "$ov_sym")
                [[ -n "$workspace" && "$ov_target" == "$workspace"/* ]] || continue
                local ov_rel="${ov_target##*"$ov_project"/}"
                printf '  %-40s → %s  (%s)\n' "$ov_project" "$ov_rel" "$ov_path"
                printed=1
            done < <(_list_switch_overrides "$loc_name")
            [[ $printed -eq 0 ]] && printf '  (none)\n'
        fi
    done < <(_list_switch_locations)
}

cmd_audit() {
    local name_hint="${1:-}"
    local workspace="${GITREE_WORKSPACE:-}"
    [[ -z "$workspace" ]] && { printf 'Error: no .gitree config found\n' >&2; exit 1; }

    _audit_plugin() {
        local pdir="$1"
        local name; name=$(basename "$pdir")
        local bare="$pdir/.bare"
        while IFS= read -r ref; do
            local branch="${ref#refs/heads/}"
            local has_wt
            has_wt=$(git -C "$bare" worktree list --porcelain | awk -v b="refs/heads/$branch" '
                $0=="branch "b { found=1 } END { print found+0 }
            ')
            if [[ "$has_wt" == "0" ]]; then
                printf '  %-35s  %s\n' "$name" "$branch"
            fi
        done < <(git -C "$bare" for-each-ref --format='%(refname)' refs/heads/)
    }

    printf 'Branches with no worktree:\n'
    if [[ -d "$workspace/.bare" ]]; then
        _audit_plugin "$workspace"; return
    fi
    if [[ -n "$name_hint" ]]; then
        local pdir="$workspace/$name_hint"
        [[ -d "$pdir/.bare" ]] || { printf 'Error: %s not found\n' "$name_hint" >&2; exit 1; }
        _audit_plugin "$pdir"
    else
        local found=0
        for pdir in "$workspace"/*/; do
            [[ -d "$pdir/.bare" ]] || continue; found=1; _audit_plugin "$pdir"
        done
        [[ $found -eq 0 ]] && printf '  (no projects found)\n'
    fi
}

cmd_wt() {
    local branch="" name_hint=""
    case $# in
        0) printf 'Usage: gitree wt [<project>] <branch>\n' >&2; exit 1 ;;
        1) branch="$1" ;;
        *) name_hint="$1"; branch="$2" ;;
    esac

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project directory, and no project name given\n' >&2; exit 1
    fi

    local path
    path=$(_resolve_worktree "$GITREE_BARE_DIR" "$GITREE_PLUGIN_DIR" "$branch")

    if [[ -n "${GITREE_WORKSPACE:-}" && "$branch" != "main" ]]; then
        local dirname; dirname=$(_branch_to_dirname "$branch")
        local context_dir; context_dir="$(_project_context_root "$GITREE_PLUGIN_NAME")/$dirname"
        local link="$path/.branch-context"
        if [[ -d "$context_dir" && ! -L "$link" ]]; then
            local rel
            rel=$(python3 -c "import os; print(os.path.relpath('$context_dir', '$path'))")
            ln -sfn "$rel" "$link"
        fi
    fi

    _ensure_shell_fn "$path"
    printf '%s\n' "$path"
}

cmd_goto() {
    # Same resolution as wt — the shell goto() function does the actual cd
    cmd_wt "$@"
}

cmd_remove() {
    local branch="" name_hint=""
    case $# in
        0) printf 'Usage: gitree remove [<project>] <branch>\n' >&2; exit 1 ;;
        1) branch="$1" ;;
        *) name_hint="$1"; branch="$2" ;;
    esac

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project directory, and no project name given\n' >&2; exit 1
    fi

    local dirname; dirname=$(_branch_to_dirname "$branch")
    local wt_path="$GITREE_PLUGIN_DIR/.worktrees/$dirname"

    if [[ ! -d "$wt_path" ]]; then
        printf 'Error: no worktree found for branch %q\n' "$branch" >&2
        printf 'Active worktrees:\n' >&2
        git -C "$GITREE_BARE_DIR" worktree list >&2
        exit 1
    fi

    # Warn on uncommitted changes
    local dirty=false
    if ! git -C "$wt_path" diff --quiet 2>/dev/null || \
       ! git -C "$wt_path" diff --cached --quiet 2>/dev/null; then
        dirty=true
    fi

    if [[ "$dirty" == "true" ]]; then
        printf 'Warning: %s has uncommitted changes.\n' "$branch" >&2
        local yn=""
        read -r -p "Remove anyway? [y/N]: " yn || true
        case "$yn" in
            [Yy]|[Yy][Ee][Ss]) ;;
            *) printf 'Aborted.\n'; return 0 ;;
        esac
    fi

    git -C "$GITREE_BARE_DIR" worktree remove --force "$wt_path"
    printf '✓ Removed worktree: %s\n' "$branch"

    # Offer to delete from remote
    if git -C "$GITREE_BARE_DIR" remote get-url origin >/dev/null 2>&1; then
        local yn=""
        read -r -p "Delete branch '$branch' from remote? [y/N]: " yn || true
        case "$yn" in
            [Yy]|[Yy][Ee][Ss])
                if git -C "$GITREE_BARE_DIR" push origin --delete "$branch" 2>/dev/null; then
                    printf '✓ Deleted %s from remote\n' "$branch"
                else
                    printf 'Could not delete from remote (branch may not exist there)\n' >&2
                fi
                ;;
        esac
    fi

    if [[ -n "${GITREE_WORKSPACE:-}" ]]; then
        local context_dir; context_dir="$(_project_context_root "$GITREE_PLUGIN_NAME")/$dirname"
        if [[ -d "$context_dir" ]]; then
            local yn=""
            read -r -p "Archive branch context? [Y/n]: " yn || true
            case "$yn" in
                [Nn]|[Nn][Oo])
                    rm -rf "$context_dir"
                    printf '✓ Branch context deleted\n' >&2
                    ;;
                *)
                    local archive="$GITREE_WORKSPACE/.gitree-context/.archived/$GITREE_PLUGIN_NAME/$dirname"
                    mkdir -p "$(dirname "$archive")"
                    mv "$context_dir" "$archive"
                    printf '✓ Branch context archived (%s)\n' "$dirname" >&2
                    ;;
            esac
        fi
    fi
}

cmd_drop() {
    local name_hint="${1:-}"
    local workspace="${GITREE_WORKSPACE:-}"
    [[ -z "$workspace" ]] && { printf 'Error: no .gitree config found\n' >&2; exit 1; }

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project directory, and no project name given\n' >&2; exit 1
    fi

    local name="$GITREE_PLUGIN_NAME"
    local pdir="$GITREE_PLUGIN_DIR"

    printf 'This will permanently delete %s and all its worktrees.\n' "$name"
    local yn=""
    read -r -p "Continue? [y/N]: " yn || true
    case "$yn" in
        [Yy]|[Yy][Ee][Ss]) ;;
        *) printf 'Aborted.\n'; return 0 ;;
    esac

    # Prune git's worktree registrations first so git doesn't complain
    git -C "$pdir/.bare" worktree prune 2>/dev/null || true
    rm -rf "$pdir"
    _config_remove_project "$name"
    printf '✓ Dropped %s\n' "$name"

    # Clean up stale switch symlinks (resolves per-project overrides)
    while IFS=$'\t' read -r loc_name _; do
        local resolved; resolved=$(_get_switch_path "$loc_name" "$name")
        [[ -z "$resolved" ]] && continue
        resolved="${resolved/#\~/$HOME}"
        local sym="$resolved/$name"
        if [[ -L "$sym" ]]; then
            rm -f "$sym"
            if [[ "$loc_name" == "default" ]]; then
                printf '✓ Removed stale switch symlink: %s\n' "$sym"
            else
                printf '✓ Removed stale switch symlink [%s]: %s\n' "$loc_name" "$sym"
            fi
        fi
    done < <(_list_switch_locations)
}

cmd_switch() {
    # Parse -l <location> flag before positional args
    local location="default"
    while [[ $# -gt 0 && "$1" == -* ]]; do
        case "$1" in
            -l|--location) location="${2:?"-l requires a location name"}"; shift 2 ;;
            *) printf 'Unknown flag: %s\n' "$1" >&2; exit 1 ;;
        esac
    done

    # No project/branch args: print current switch state
    if [[ $# -eq 0 ]]; then
        local workspace="${GITREE_WORKSPACE:-}"
        local found=0
        while IFS=$'\t' read -r loc_name loc_path; do
            [[ "$location" != "default" && "$loc_name" != "$location" ]] && continue
            [[ -z "$loc_path" ]] && continue
            loc_path="${loc_path/#\~/$HOME}"
            [[ -d "$loc_path" ]] || continue
            found=1
            if [[ "$loc_name" == "default" ]]; then
                printf 'Switch state (%s):\n' "$loc_path"
            else
                printf 'Switch state [%s] (%s):\n' "$loc_name" "$loc_path"
            fi
            local printed=0
            for sym in "$loc_path"/*/; do
                sym="${sym%/}"
                [[ -L "$sym" ]] || continue
                local target; target=$(readlink "$sym")
                local name; name=$(basename "$sym")
                [[ -z "$workspace" || "$target" == "$workspace"/* ]] || continue
                local rel="${target##*"$name"/}"
                printf '  %-36s → %s\n' "$name" "$rel"
                printed=1
            done
            # Per-project overrides for this location
            while IFS=$'\t' read -r ov_project ov_path; do
                [[ -z "$ov_path" ]] && continue
                ov_path="${ov_path/#\~/$HOME}"
                local ov_sym="$ov_path/$ov_project"
                [[ -L "$ov_sym" ]] || continue
                local ov_target; ov_target=$(readlink "$ov_sym")
                [[ -z "$workspace" || "$ov_target" == "$workspace"/* ]] || continue
                local ov_rel="${ov_target##*"$ov_project"/}"
                printf '  %-36s → %s  (%s)\n' "$ov_project" "$ov_rel" "$ov_path"
                printed=1
            done < <(_list_switch_overrides "$loc_name")
            [[ $printed -eq 0 ]] && printf '  (none)\n'
        done < <(_list_switch_locations)
        if [[ $found -eq 0 ]]; then
            if [[ "$location" == "default" ]]; then
                printf 'No switch locations configured in .gitree\n' >&2
            else
                printf 'Error: switch location "%s" not found in .gitree\n' "$location" >&2
            fi
            exit 1
        fi
        return
    fi

    local branch="" name_hint=""
    case $# in
        1) branch="$1" ;;
        *) name_hint="$1"; branch="$2" ;;
    esac

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project directory, and no project name given\n' >&2; exit 1
    fi

    local pdir="$GITREE_PLUGIN_DIR"
    local bare="$GITREE_BARE_DIR"
    local name="$GITREE_PLUGIN_NAME"

    # Resolve switch path now that we know the project (supports per-project overrides)
    local switch_path
    switch_path=$(_get_switch_path "$location" "$name")
    if [[ -z "$switch_path" ]]; then
        if [[ "$location" == "default" ]]; then
            printf 'Error: no switch location configured in .gitree\n' >&2
        else
            printf 'Error: switch location "%s" not found in .gitree\n' "$location" >&2
        fi
        exit 1
    fi
    switch_path="${switch_path/#\~/$HOME}"

    # Auto-create the switch directory if it doesn't exist yet,
    # but only if its parent exists (error if the root destination is missing).
    if [[ ! -d "$switch_path" ]]; then
        local parent; parent=$(dirname "$switch_path")
        if [[ ! -d "$parent" ]]; then
            printf 'Error: switch location parent does not exist: %s\n' "$parent" >&2
            exit 1
        fi
        mkdir "$switch_path"
        printf 'Created switch directory: %s\n' "$switch_path" >&2
    fi

    local sym="$switch_path/$name"

    if [[ -z "$branch" ]]; then
        [[ -d "$pdir/main" ]] || { printf 'Error: main/ not found for %s\n' "$name" >&2; exit 1; }
        ln -sfn "$pdir/main" "$sym"
        local actual; actual=$(git -C "$pdir/main" branch --show-current)
        printf '✓ [%s] %s → main/ (branch: %s)\n' "$location" "$name" "$actual"
        return
    fi

    local target
    target=$(_resolve_worktree "$bare" "$pdir" "$branch")
    ln -sfn "$target" "$sym"
    printf '✓ [%s] %s → %s (branch: %s)\n' "$location" "$name" "$target" "$branch"
}

cmd_restore() {
    local workspace="${GITREE_WORKSPACE:-}"
    [[ -z "$workspace" ]] && { printf 'Error: no .gitree config found\n' >&2; exit 1; }
    [[ "${GITREE_CONFIG_JSON:-0}" != "1" ]] && {
        printf 'Error: .gitree is not JSON format — no project manifest to restore from\n' >&2; exit 1
    }

    local projects_json
    projects_json=$(python3 - "$GITREE_CONFIG_FILE" <<'PY'
import json, sys
d = json.load(open(sys.argv[1]))
projects = d.get("projects", {})
for name, url in projects.items():
    print(f"{name}\t{url}")
PY
    )

    if [[ -z "$projects_json" ]]; then
        printf 'No projects listed in .gitree\n'
        return 0
    fi

    local restored=0 skipped=0
    while IFS=$'\t' read -r name url; do
        if [[ -d "$workspace/$name/.bare" ]]; then
            printf '  %-40s  already present\n' "$name"
            (( skipped++ )) || true
        else
            printf '  %-40s  cloning...\n' "$name"
            _setup_bare_repo "$url" "$name"
            (( restored++ )) || true
        fi
        local ctx_root; ctx_root=$(GITREE_WORKSPACE="$workspace" _project_context_root "$name")
        local pdir
        if [[ -d "$workspace/.bare" ]]; then pdir="$workspace"; else pdir="$workspace/$name"; fi
        if [[ -d "$ctx_root" ]]; then
            for ctx_dir in "$ctx_root"/*/; do
                [[ -d "$ctx_dir" ]] || continue
                local ctx_dirname; ctx_dirname=$(basename "$ctx_dir")
                local wt_path="$pdir/.worktrees/$ctx_dirname"
                local link="$wt_path/.branch-context"
                if [[ -d "$wt_path" && ! -L "$link" ]]; then
                    local rel
                    rel=$(python3 -c "import os; print(os.path.relpath('$ctx_dir', '$wt_path'))")
                    ln -sfn "$rel" "$link"
                fi
            done
        fi
    done <<< "$projects_json"

    printf '\nDone: %d restored, %d already present\n' "$restored" "$skipped"
}

cmd_repair_head() {
    local workspace="${GITREE_WORKSPACE:-}"
    [[ -z "$workspace" ]] && { printf 'Error: no .gitree config found\n' >&2; exit 1; }

    _repair_one() {
        local bare="$1" name="$2"
        local remote_default
        remote_default=$(git -C "$bare" remote show origin 2>/dev/null \
            | grep 'HEAD branch' | sed 's/.*HEAD branch: //')
        if [[ -z "$remote_default" || "$remote_default" == "(unknown)" ]]; then
            printf '  %-40s  (skipped — cannot reach origin)\n' "$name"
            return
        fi
        local current
        current=$(git -C "$bare" symbolic-ref --short HEAD 2>/dev/null || echo "(detached)")
        if [[ "$current" == "$remote_default" ]]; then
            printf '  %-40s  ok (HEAD → %s)\n' "$name" "$current"
        else
            git -C "$bare" symbolic-ref HEAD "refs/heads/$remote_default"
            printf '  %-40s  fixed (%s → %s)\n' "$name" "$current" "$remote_default"
        fi
    }

    if [[ -d "$workspace/.bare" ]]; then
        _repair_one "$workspace/.bare" "$(basename "$workspace")"
    else
        for pdir in "$workspace"/*/; do
            [[ -d "$pdir/.bare" ]] || continue
            _repair_one "$pdir/.bare" "$(basename "$pdir")"
        done
    fi
}

cmd_pull() {
    local name_hint="${1:-}"
    local workspace="${GITREE_WORKSPACE:-}"

    _pull_project() {
        local bare="$1" pname="$2"
        local pdir; pdir="$(dirname "$bare")"
        printf '%s: ' "$pname"
        if ! git -C "$bare" fetch --quiet origin 2>/dev/null; then
            printf 'no remote\n'; return
        fi
        if [[ ! -d "$pdir/main" ]]; then printf 'no main/ worktree\n'; return; fi
        local branch; branch=$(git -C "$pdir/main" branch --show-current 2>/dev/null)
        [[ -z "$branch" ]] && { printf 'detached HEAD\n'; return; }
        local behind
        behind=$(git -C "$pdir/main" rev-list --count HEAD.."origin/$branch" 2>/dev/null || echo 0)
        if [[ "$behind" -eq 0 ]]; then
            printf 'up to date (%s)\n' "$branch"
        else
            if git -C "$pdir/main" merge --ff-only "origin/$branch" --quiet 2>/dev/null; then
                printf 'fast-forwarded %s commit(s) (%s)\n' "$behind" "$branch"
            else
                printf 'cannot fast-forward — diverged (%s)\n' "$branch"
            fi
        fi
    }

    if [[ "$name_hint" == "*" || $# -gt 1 ]]; then
        [[ -z "$workspace" ]] && { printf 'Error: no workspace root\n' >&2; exit 1; }
        for pdir in "$workspace"/*/; do
            [[ -d "$pdir/.bare" ]] || continue
            _pull_project "$pdir/.bare" "$(basename "$pdir")"
        done
        return
    fi

    if [[ -n "$workspace" && -d "$workspace/.bare" ]]; then
        _pull_project "$workspace/.bare" "$(basename "$workspace")"; return
    fi

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project directory, and no project name given\n' >&2; exit 1
    fi
    _pull_project "$GITREE_BARE_DIR" "$GITREE_PLUGIN_NAME"
}

cmd_lock() {
    local name_hint="${1:-}"
    local worktree_name="${2:-main}"
    local workspace="${GITREE_WORKSPACE:-}"
    # shell-expanded '*': $2 onwards are filenames, not a worktree name
    [[ $# -gt 2 ]] && worktree_name="main"

    if [[ "$name_hint" == "*" || $# -gt 2 ]]; then
        [[ -z "$workspace" ]] && { printf 'Error: no workspace root\n' >&2; exit 1; }
        for pdir in "$workspace"/*/; do
            [[ -d "$pdir/.bare" ]] || continue
            _lock_project "$pdir/.bare" "$(basename "$pdir")" "$worktree_name"
        done
        return
    fi

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project, and no project name given\n' >&2; exit 1
    fi
    _lock_project "$GITREE_BARE_DIR" "$GITREE_PLUGIN_NAME" "$worktree_name"
}

cmd_unlock() {
    local name_hint="${1:-}"
    local worktree_name="${2:-main}"
    local workspace="${GITREE_WORKSPACE:-}"
    # shell-expanded '*': $2 onwards are filenames, not a worktree name
    [[ $# -gt 2 ]] && worktree_name="main"

    if [[ "$name_hint" == "*" || $# -gt 2 ]]; then
        [[ -z "$workspace" ]] && { printf 'Error: no workspace root\n' >&2; exit 1; }
        for pdir in "$workspace"/*/; do
            [[ -d "$pdir/.bare" ]] || continue
            _unlock_project "$pdir/.bare" "$(basename "$pdir")" "$worktree_name"
        done
        return
    fi

    if ! _resolve_plugin "$name_hint"; then
        printf 'Error: not inside a project, and no project name given\n' >&2; exit 1
    fi
    _unlock_project "$GITREE_BARE_DIR" "$GITREE_PLUGIN_NAME" "$worktree_name"
}

cmd_sync() {
    local all=false repo_level=false dry_run=false

    while [[ $# -gt 0 ]]; do
        case "$1" in
            --all)          all=true;        shift ;;
            --repo)         repo_level=true; shift ;;
            --dry-run|-n)   dry_run=true;    shift ;;
            *) printf 'Unknown option: %s\nUsage: gitree sync [--all|--repo] [--dry-run]\n' "$1" >&2; exit 1 ;;
        esac
    done

    # Colors
    local g="" y="" d="" reset=""
    if [[ -t 1 ]]; then
        g=$'\033[32m'; y=$'\033[33m'; d=$'\033[2m'; reset=$'\033[0m'
    fi

    # Sync a single worktree path; caller already ran fetch for this repo
    _sync_wt() {
        local wt="$1" label="$2"
        [[ -d "$wt" ]] || return 0

        local branch
        branch=$(git -C "$wt" branch --show-current 2>/dev/null || true)
        if [[ -z "$branch" ]]; then
            printf '  %-55s  %s(detached — skipped)%s\n' "$label" "$d" "$reset"
            return 0
        fi

        local upstream
        upstream=$(git -C "$wt" rev-parse --abbrev-ref '@{upstream}' 2>/dev/null || true)
        if [[ -z "$upstream" ]]; then
            printf '  %-55s  %s⚠ no upstream configured — skipped%s\n' "$label" "$y" "$reset"
            return 0
        fi

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

        if [[ "$dry_run" == true ]]; then
            if   [[ "$behind" -eq 0 && "$ahead" -eq 0 ]]; then
                printf '  %-55s  %s✓ up to date%s\n'                            "$label" "$g" "$reset"
            elif [[ "$behind" -gt 0 && "$ahead" -gt 0 ]]; then
                printf '  %-55s  %s⚠ %s behind, %s ahead of %s%s\n'            "$label" "$y" "$behind" "$ahead" "$upstream" "$reset"
            elif [[ "$behind" -gt 0 ]]; then
                printf '  %-55s  %s↓ %s commit(s) behind %s%s\n'               "$label" "$y" "$behind" "$upstream" "$reset"
            else
                printf '  %-55s  %s↑ %s commit(s) ahead — not yet pushed%s\n'  "$label" "$d" "$ahead" "$reset"
            fi
            return 0
        fi

        if [[ "$behind" -eq 0 ]]; then
            if [[ "$ahead" -gt 0 ]]; then
                printf '  %-55s  %s✓ up to date (%s unpushed)%s\n' "$label" "$g" "$ahead" "$reset"
            else
                printf '  %-55s  %s✓ up to date%s\n' "$label" "$g" "$reset"
            fi
            return 0
        fi

        # Attempt rebase onto upstream; abort cleanly on conflict
        if git -C "$wt" rebase "$upstream" --quiet 2>/dev/null; then
            local s; [[ "$behind" -ne 1 ]] && s="s" || s=""
            printf '  %-55s  %s✓ synced (%s commit%s replayed)%s\n' "$label" "$g" "$behind" "$s" "$reset"
            if ! git -C "$wt" push --force-with-lease --quiet 2>/dev/null; then
                printf '  %-55s  %s⚠ synced but push failed (no remote write?)%s\n' "$label" "$y" "$reset"
            fi
        else
            git -C "$wt" rebase --abort 2>/dev/null || true
            printf '  %-55s  %s⚠ conflict — restored, needs manual rebase%s\n' "$label" "$y" "$reset"
        fi
    }

    # Fetch once for a repo, then sync all (or one filtered) worktree(s)
    _sync_plugin() {
        local pdir="$1" filter_wt="${2:-}"
        local pname; pname=$(basename "$pdir")
        local bare="$pdir/.bare"

        git -C "$bare" fetch origin --quiet 2>/dev/null || true

        local wt_path="" branch=""
        while IFS= read -r line; do
            case "$line" in
                "worktree "*) wt_path="${line#worktree }" ;;
                "branch "*) branch="${line#branch refs/heads/}" ;;
                "")
                    if [[ -n "$wt_path" && "$wt_path" != *"/.bare" && -n "$branch" ]]; then
                        if [[ -z "$filter_wt" || "$wt_path" == "$filter_wt" ]]; then
                            _sync_wt "$wt_path" "$pname/$branch"
                        fi
                    fi
                    wt_path=""; branch=""
                    ;;
            esac
        done < <(git -C "$bare" worktree list --porcelain && printf '\n')
    }

    # Context-detection cascade:
    #   explicit --repo  → workspace level (all repos)
    #   explicit --all   → project level; escalate to workspace if not in a project
    #   no flag, in worktree     → single worktree
    #   no flag, in project dir  → escalate to --all (all worktrees of this project)
    #   no flag, at workspace root → escalate to --repo (all repos)
    local cwd_wt workspace="${GITREE_WORKSPACE:-}"
    cwd_wt=$(git -C "$PWD" rev-parse --show-toplevel 2>/dev/null || true)

    if [[ "$repo_level" == false && "$all" == false && -z "$cwd_wt" ]]; then
        # Not in a worktree — escalate based on context
        if _resolve_plugin "" 2>/dev/null; then
            all=true        # inside a project container → sync all its worktrees
        else
            repo_level=true # at workspace root → sync everything
        fi
    fi

    if [[ "$all" == true && -z "$cwd_wt" ]] && ! _resolve_plugin "" 2>/dev/null; then
        # --all but not in a project → escalate to --repo
        repo_level=true; all=false
    fi

    printf '\n'

    if [[ "$repo_level" == true ]]; then
        [[ -z "$workspace" ]] && { printf 'Error: no .gitree workspace found\n' >&2; exit 1; }
        for pdir in "$workspace"/*/; do
            [[ -d "${pdir}.bare" ]] || continue
            _sync_plugin "${pdir%/}"
        done
    elif [[ "$all" == true ]]; then
        _resolve_plugin "" || { printf 'Error: not inside a project directory\n' >&2; exit 1; }
        _sync_plugin "$GITREE_PLUGIN_DIR"
    else
        # Single worktree
        [[ -z "$cwd_wt" ]] && { printf 'Error: not inside a worktree\n' >&2; exit 1; }
        _resolve_plugin "" || { printf 'Error: not inside a project directory\n' >&2; exit 1; }
        _sync_plugin "$GITREE_PLUGIN_DIR" "$cwd_wt"
    fi

    printf '\n'
}

cmd_install() {
    local shell_only=false no_shell=false
    local install_dir="/usr/local/bin"
    for arg in "$@"; do
        case "$arg" in
            --shell-only) shell_only=true ;;
            --no-shell)   no_shell=true ;;
            *) install_dir="$arg" ;;
        esac
    done

    if [[ "$shell_only" == "false" ]]; then
        local self; self=$(realpath "$0")
        local dest="$install_dir/gitree"
        if [[ "$self" == "$dest" ]]; then
            printf 'gitree is already installed at %s\n' "$dest"
        elif cp "$self" "$dest" && chmod +x "$dest" 2>/dev/null; then
            printf '✓ Installed gitree to %s\n' "$dest"
        elif sudo cp "$self" "$dest" && sudo chmod +x "$dest"; then
            printf '✓ Installed gitree to %s (via sudo)\n' "$dest"
        else
            printf 'Could not install to %s\nTry: sudo cp %s /usr/local/bin/gitree\n' \
                "$install_dir" "$self" >&2
        fi
    fi

    [[ "$no_shell" == "true" ]] && return 0

    if _shell_fn_installed && _goto_fn_installed; then
        printf 'Shell functions wt() and gitree-goto() already installed.\n'
    elif _try_install_shell_fn; then
        :
    else
        printf '\nCould not write to shell config. Add this manually:\n\n'
        printf '%s\n' "$GITREE_SHELL_FUNCS" | sed 's/^/  /' >&2
        printf '\n' >&2
    fi
}

cmd_completion() {
    local shell="${1:-}"
    case "$shell" in
        bash) _completion_bash ;;
        zsh)  _completion_zsh  ;;
        *)
            printf 'Usage: gitree completion <bash|zsh>\n' >&2
            printf '\nAdd to your shell:\n' >&2
            printf '  bash:  eval "$(gitree completion bash)"\n' >&2
            printf '  zsh:   eval "$(gitree completion zsh)"\n' >&2
            exit 1
            ;;
    esac
}

_completion_bash() {
cat << 'BASH'
_gitree_complete() {
    local cur="${COMP_WORDS[COMP_CWORD]}"
    local prev="${COMP_WORDS[COMP_CWORD-1]}"
    local cmd="${COMP_WORDS[1]}"
    local cmds="list init add new convert audit wt goto remove switch pull lock unlock install completion branch-context repair-head drop restore --help --version"

    _gitree_projects() {
        local d="$PWD" ws=""
        while [[ "$d" != "/" ]]; do
            if [[ -f "$d/.gitree" ]]; then ws="$d"; break; fi
            d=$(dirname "$d")
        done
        [[ -z "$ws" ]] && return
        local p
        for p in "$ws"/*/; do
            [[ -d "${p%/}/.bare" ]] && printf '%s ' "$(basename "${p%/}")"
        done
    }

    case "$prev" in
        gitree)
            COMPREPLY=($(compgen -W "$cmds" -- "$cur")) ;;
        goto|wt|new|switch|lock|unlock|remove)
            local branches projects
            branches=$(git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null | tr '\n' ' ')
            projects=$(_gitree_projects 2>/dev/null)
            COMPREPLY=($(compgen -W "$branches $projects" -- "$cur")) ;;
        convert)
            COMPREPLY=($(compgen -W "multi mono" -- "$cur")) ;;
        completion)
            COMPREPLY=($(compgen -W "bash zsh" -- "$cur")) ;;
        branch-context)
            COMPREPLY=($(compgen -W "init" -- "$cur")) ;;
        *)
            case "$cmd" in
                goto|wt|new|switch|lock|unlock|remove)
                    local branches
                    branches=$(git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null | tr '\n' ' ')
                    COMPREPLY=($(compgen -W "$branches" -- "$cur")) ;;
            esac ;;
    esac
}
complete -F _gitree_complete gitree
BASH
}

_completion_zsh() {
cat << 'ZSH'
#compdef gitree
_gitree() {
    local state
    _arguments '1: :->cmd' '*: :->args'

    _gitree_projects() {
        local d="$PWD" ws=""
        while [[ "$d" != "/" ]]; do
            [[ -f "$d/.gitree" ]] && { ws="$d"; break }
            d="${d:h}"
        done
        [[ -z "$ws" ]] && return
        local p
        for p in "$ws"/*/; do
            [[ -d "${p%/}/.bare" ]] && print -r -- "${${p%/}:t}"
        done
    }

    case $state in
        cmd)
            _values 'command' \
                'list[show worktrees and load state]' \
                'init[init workspace root]' \
                'add[add a repo]' \
                'new[create branch and worktree]' \
                'convert[convert layout]' \
                'audit[show branches without worktrees]' \
                'goto[cd into a worktree]' \
                'wt[get or create a worktree]' \
                'remove[remove a worktree]' \
                'switch[point symlink at worktree]' \
                'pull[fetch and fast-forward]' \
                'lock[lock a worktree]' \
                'unlock[unlock a worktree]' \
                'drop[remove a project]' \
                'restore[clone all manifest projects]' \
                'install[install gitree globally]' \
                'completion[print shell completion script]' \
                'repair-head[reset bare HEAD to remote default branch]' \
                'branch-context[scaffold branch context for existing repos]'
            ;;
        args)
            case ${words[2]} in
                goto|wt|new|remove|switch|lock|unlock)
                    local branches projects
                    branches=(${(f)"$(git for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null)"})
                    projects=(${(f)"$(_gitree_projects 2>/dev/null)"})
                    _describe 'branch' branches
                    [[ ${#projects} -gt 0 ]] && _describe 'project' projects
                    ;;
                convert) _values 'layout' 'multi' 'mono' ;;
                completion) _values 'shell' 'bash' 'zsh' ;;
                branch-context) _values 'action' 'init' ;;
            esac
            ;;
    esac
}
_gitree
ZSH
}

cmd_branch_context() {
    local action="${1:-}"
    case "$action" in
        init) shift; _branch_context_init "$@" ;;
        *)
            printf 'Usage: gitree branch-context init [<project>|*]\n' >&2
            exit 1
            ;;
    esac
}

_branch_context_init() {
    local name_hint="${1:-}"
    local workspace="${GITREE_WORKSPACE:-}"
    [[ -z "$workspace" ]] && { printf 'Error: no .gitree config found\n' >&2; exit 1; }

    _write_workspace_agents_md "$workspace"

    _bc_init_project() {
        local pdir="$1" pname="$2"

        _ignore_branch_context "$pdir" "$pname"

        mkdir -p "$(_project_context_root "$pname")"

        # Scaffold context for every branch in the bare repo (skip main).
        # Branches with existing worktrees also get the .branch-context symlink wired.
        local bare="$pdir/.bare"
        [[ -d "$bare" ]] || bare="$pdir/.git"
        if [[ -d "$bare" || -f "$bare" ]]; then
            while IFS= read -r branch; do
                [[ -z "$branch" || "$branch" == "main" ]] && continue
                local dirname; dirname=$(_branch_to_dirname "$branch")
                local wt_path="$pdir/.worktrees/$dirname"
                [[ -d "$wt_path" ]] || wt_path=""
                _scaffold_branch_context "$pname" "$branch" "$wt_path"
            done < <(git -C "$pdir" for-each-ref --format='%(refname:short)' refs/heads/ 2>/dev/null)
        fi

        printf '✓ %s: branch context initialized\n' "$pname"
    }

    # mono-repo
    if [[ -d "$workspace/.bare" ]]; then
        _bc_init_project "$workspace" "$(basename "$workspace")"
        return
    fi

    # multi-repo
    # $# -gt 1 handles shell-expanded '*' (e.g. `init *` expands to filenames in cwd)
    if [[ "$name_hint" == "*" || $# -gt 1 ]]; then
        local found=0
        for pdir in "$workspace"/*/; do
            [[ -d "${pdir%/}/.bare" ]] || continue
            found=1
            _bc_init_project "${pdir%/}" "$(basename "${pdir%/}")"
        done
        [[ $found -eq 0 ]] && printf 'No projects found in workspace\n'
        return
    fi

    if [[ -n "$name_hint" ]]; then
        local pdir="$workspace/$name_hint"
        [[ -d "$pdir/.bare" ]] || { printf 'Error: project "%s" not found\n' "$name_hint" >&2; exit 1; }
        _bc_init_project "$pdir" "$name_hint"
    else
        if ! _resolve_plugin ""; then
            printf 'Error: not inside a project directory, and no project name given\n' >&2; exit 1
        fi
        _bc_init_project "$GITREE_PLUGIN_DIR" "$GITREE_PLUGIN_NAME"
    fi
}

# ── Bootstrap ─────────────────────────────────────────────────────────────────

GITREE_WORKSPACE=""
SWITCH_PATH=""
GITREE_CONFIG_FILE=""
GITREE_CONFIG_JSON=0

if _ws=$(_find_workspace_root 2>/dev/null); then
    GITREE_WORKSPACE="$_ws"
    _load_config "$_ws"
fi

# ── Help ──────────────────────────────────────────────────────────────────────

_help() {
    local b="" d="" g="" y="" c="" r=""
    if [[ -t 1 ]]; then
        b=$'\033[1m'; d=$'\033[2m'; g=$'\033[32m'
        y=$'\033[33m'; c=$'\033[36m'; r=$'\033[0m'
    fi

    printf '\n  %s%s%s  bare-repo worktree manager  %sv%s%s\n\n' \
        "$b" "gitree" "$r" "$d" "$GITREE_VERSION" "$r"

    printf '%sUsage%s\n'   "$b" "$r"
    printf '  gitree %s<command>%s [args]\n\n' "$c" "$r"

    printf '%sSetup%s\n' "$b" "$r"
    printf '  %sinit%s                              init current directory as a workspace root\n' "$g" "$r"
    printf '  %sadd%s [<repo>] [<name>|.]           add a repo %s(interactive if no args)%s\n' "$g" "$r" "$d" "$r"
    printf '  %srestore%s                           clone all projects listed in .gitree\n' "$g" "$r"
    printf '  %sdrop%s [<project>]                  remove a project from disk and manifest\n' "$g" "$r"
    printf '  %sconvert%s multi|mono                convert between single- and multi-repo layouts\n\n' "$g" "$r"

    printf '%sWorktrees%s\n' "$b" "$r"
    printf '  %snew%s [<project>] %s<branch>%s          create a branch and its worktree\n' "$g" "$r" "$c" "$r"
    printf '  %sgoto%s [<project>] %s<branch>%s         cd into a worktree (via gitree-goto shell fn)\n' "$g" "$r" "$c" "$r"
    printf '  %swt%s [<project>] %s<branch>%s           get or create a worktree; print its path\n' "$g" "$r" "$c" "$r"
    printf '  %sremove%s [<project>] %s<branch>%s       remove a worktree safely\n' "$g" "$r" "$c" "$r"
    printf '  %saudit%s [<project>]                 show branches that have no worktree\n\n' "$g" "$r"

    printf '%sRuntime%s\n' "$b" "$r"
    printf '  %sswitch%s [-l loc] [<project>] [%s<branch>%s]  point switch symlink at a worktree\n' "$g" "$r" "$c" "$r"
    printf '  %spull%s [<project>|*]                fetch + fast-forward main worktree\n' "$g" "$r"
    printf '  %ssync%s [--all|--repo] [--dry-run]   rebase worktree(s) onto upstream + push\n' "$g" "$r"
    printf '  %slist%s                              show all worktrees and load state  %s(default)%s\n\n' \
        "$g" "$r" "$d" "$r"

    printf '%sSafety%s\n' "$b" "$r"
    printf '  %slock%s [<project>|*] [<worktree>]   lock against branch switching %s(default: main)%s\n' \
        "$g" "$r" "$d" "$r"
    printf '  %sunlock%s [<project>|*] [<worktree>] remove the lock\n\n' "$g" "$r"

    printf '%sTools%s\n' "$b" "$r"
    printf '  %sinstall%s [--shell-only|--no-shell] [<dir>]  install gitree; --no-shell skips rc patching\n' "$g" "$r"
    printf '  %scompletion%s <bash|zsh>             print shell completion script\n' "$g" "$r"
    printf '  %srepair-head%s                       reset bare HEAD to remote default branch\n' "$g" "$r"
    printf '  %sbranch-context init%s [<project>|*] scaffold .gitree-context/ for existing repos\n\n' "$g" "$r"

    printf '%sConfig%s  %s.gitree (JSON) at workspace root%s\n' "$b" "$r" "$d" "$r"
    printf '  %s{ "switch": { "default": "/path/", "wpml": "/path2/" } }%s\n\n' "$d" "$r"

    printf '%sShell functions%s  %sadded by install / init%s\n' "$b" "$r" "$d" "$r"
    printf '  %swt()          { local d; d=$(gitree wt   "$@") && cd "$d"; }%s\n' "$d" "$r"
    printf '  %sgitree-goto() { local d; d=$(gitree goto "$@") && cd "$d"; }%s\n' "$d" "$r"
    printf '  Then: %swt <branch>%s or %sgitree-goto <branch>%s  cds into the worktree\n\n' "$c" "$r" "$c" "$r"

    printf '%sExamples%s\n' "$b" "$r"
    printf '  gitree init\n'
    printf '  gitree add https://github.com/org/repo\n'
    printf '  gitree new feature/my-thing             %s# branch + worktree in one step%s\n' "$d" "$r"
    printf '  %sgitree-goto%s feature/my-thing           %s# cd into it (shell fn)%s\n' "$c" "$r" "$d" "$r"
    printf '  %swt%s feature/my-thing                   %s# same; also prints path%s\n' "$c" "$r" "$d" "$r"
    printf '  gitree pull %s*%s                          %s# update all projects%s\n' "$c" "$r" "$d" "$r"
    printf '  gitree lock %s*%s                          %s# lock main in all projects%s\n' "$c" "$r" "$d" "$r"
    printf '  eval "%s$(gitree completion zsh)%s"\n' "$c" "$r"
    printf '\n'
}

# ── Dispatch ──────────────────────────────────────────────────────────────────

main() {
    local sub="${1:-list}"
    case "$sub" in
        list)       shift; cmd_list "$@" ;;
        init)       cmd_init ;;
        add)        shift; cmd_add "$@" ;;
        new)        shift; cmd_new "$@" ;;
        convert)    shift; cmd_convert "$@" ;;
        audit)      shift; cmd_audit "$@" ;;
        wt)         shift; cmd_wt "$@" ;;
        goto)       shift; cmd_goto "$@" ;;
        remove|rm)  shift; cmd_remove "$@" ;;
        switch)     shift; cmd_switch "$@" ;;
        pull)       shift; cmd_pull "$@" ;;
        sync)       shift; cmd_sync "$@" ;;
        lock)       shift; cmd_lock "$@" ;;
        unlock)     shift; cmd_unlock "$@" ;;
        install)      shift; cmd_install "$@" ;;
        restore)      cmd_restore ;;
        drop)         shift; cmd_drop "$@" ;;
        repair-head)  cmd_repair_head ;;
        branch-context) shift; cmd_branch_context "$@" ;;
        completion) shift; cmd_completion "$@" ;;
        -v|--version|version) printf 'gitree %s\n' "$GITREE_VERSION" ;;
        -h|--help|help) _help ;;
        *)
            printf 'Unknown subcommand: %s\n\n' "$sub" >&2
            _help >&2
            exit 1
            ;;
    esac
}

main "$@"
