#!/usr/bin/env python3
"""
Skill Installer v3.0 - Enterprise-grade installer with 11-step redundant workflow.

Detects, validates, copies, registers, and verifies skills in the ecosystem
with maximum redundancy, safety, auto-repair, rollback, and rich diagnostics.

Usage:
    python install_skill.py --source "C:\\path\\to\\skill"
    python install_skill.py --source "C:\\path" --name "my-skill"
    python install_skill.py --source "C:\\path" --force
    python install_skill.py --source "C:\\path" --dry-run
    python install_skill.py --detect
    python install_skill.py --detect --auto
    python install_skill.py --uninstall "skill-name"
    python install_skill.py --health
    python install_skill.py --health --repair
    python install_skill.py --rollback "skill-name"
    python install_skill.py --reinstall-all
    python install_skill.py --status
    python install_skill.py --log [N]
"""

from __future__ import annotations

import os
import sys
import json
import shutil
import hashlib
import subprocess
import re
from pathlib import Path
from datetime import datetime

# Add scripts directory to path for imports
SCRIPT_DIR = Path(__file__).parent.resolve()
sys.path.insert(0, str(SCRIPT_DIR))

from validate_skill import validate, parse_yaml_frontmatter
from detect_skills import detect

# ── Configuration ──────────────────────────────────────────────────────────

# 1. Dynamic Paths (Cross-Platform)
SCRIPT_DIR = Path(__file__).resolve().parent
REPO_ROOT = SCRIPT_DIR.parent.parent.parent.parent
SKILLS_ROOT = REPO_ROOT / ".agent" / "skills"
SCAN_SCRIPT = SKILLS_ROOT / "vibe-coding-skill-orchestrator" / "scripts" / "scan_registry.py"
REGISTRY_PATH = SKILLS_ROOT / "vibe-coding-skill-orchestrator" / "data" / "registry.json"

INSTALLER_DIR = SKILLS_ROOT / "skill-installer"
DATA_DIR = INSTALLER_DIR / "data"
BACKUPS_DIR = DATA_DIR / "backups"
STAGING_DIR = DATA_DIR / "staging"
LOG_PATH = DATA_DIR / "install_log.json"



MAX_BACKUPS_PER_SKILL = 5
MAX_LOG_ENTRIES = 500  # Log rotation threshold
VERSION = "3.0.0"


# ── Console Colors ─────────────────────────────────────────────────────────

class _C:
    """ANSI color codes for terminal output. Degrades gracefully on Windows."""
    _enabled = hasattr(sys.stdout, "isatty") and sys.stdout.isatty()
    # Check if stdout can handle UTF-8 symbols
    _utf8 = False
    try:
        _utf8 = sys.stdout.encoding and sys.stdout.encoding.lower().replace("-", "") in ("utf8", "utf16")
    except Exception:
        pass

    @staticmethod
    def _wrap(code: str, text: str) -> str:
        if _C._enabled:
            return f"\033[{code}m{text}\033[0m"
        return text

    @staticmethod
    def green(t: str) -> str: return _C._wrap("32", t)
    @staticmethod
    def red(t: str) -> str: return _C._wrap("31", t)
    @staticmethod
    def yellow(t: str) -> str: return _C._wrap("33", t)
    @staticmethod
    def cyan(t: str) -> str: return _C._wrap("36", t)
    @staticmethod
    def bold(t: str) -> str: return _C._wrap("1", t)
    @staticmethod
    def dim(t: str) -> str: return _C._wrap("2", t)

    # ASCII-safe symbols for Windows cp1252 compatibility
    OK = "[OK]"
    FAIL = "[FAIL]"
    WARN = "[WARN]"


def _step(n: int, total: int, msg: str):
    """Print a step progress indicator."""
    print(f"  {_C.cyan(f'[{n}/{total}]')} {msg}")


def _ok(msg: str):
    print(f"  {_C.green(_C.OK)} {msg}")


def _warn(msg: str):
    print(f"  {_C.yellow(_C.WARN)} {msg}")


def _fail(msg: str):
    print(f"  {_C.red(_C.FAIL)} {msg}")


# ── Utility Functions ──────────────────────────────────────────────────────

def sanitize_name(name: str) -> str:
    """Sanitize skill name: lowercase, hyphens, no spaces."""
    name = name.strip().lower()
    name = name.replace(" ", "-")
    name = name.replace("_", "-")
    # Remove any chars that aren't alphanumeric or hyphens
    name = "".join(c for c in name if c.isalnum() or c == "-")
    # Remove leading/trailing hyphens and collapse multiples
    while "--" in name:
        name = name.replace("--", "-")
    return name.strip("-")


def md5_dir(path: Path, exclude_dirs: set = None) -> str:
    """Compute combined MD5 hash of all files in a directory.

    Excludes backup/staging dirs and normalizes paths to forward slashes
    for cross-platform consistency.
    """
    if exclude_dirs is None:
        exclude_dirs = {"backups", "staging", ".git", "__pycache__", "node_modules", ".venv"}

    h = hashlib.md5()
    for root, dirs, files in os.walk(path):
        # Filter out excluded directories
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for f in sorted(files):
            fp = Path(root) / f
            try:
                # Normalize to forward slashes for consistent hashing
                rel = fp.relative_to(path).as_posix()
                h.update(rel.encode("utf-8"))
                with open(fp, "rb") as fh:
                    for chunk in iter(lambda: fh.read(8192), b""):
                        h.update(chunk)
            except Exception:
                pass
    return h.hexdigest()


def parse_version(ver: str) -> tuple:
    """Parse a semver string into a comparable tuple.

    Examples: '1.0.0' -> (1,0,0), '2.1' -> (2,1,0), '' -> (0,0,0)
    """
    if not ver:
        return (0, 0, 0)
    parts = re.findall(r'\d+', str(ver))
    while len(parts) < 3:
        parts.append("0")
    try:
        return tuple(int(p) for p in parts[:3])
    except (ValueError, TypeError):
        return (0, 0, 0)


def compare_versions(installed: str, source: str) -> str:
    """Compare two version strings.

    Returns: 'same', 'upgrade', 'downgrade', or 'unknown'.
    """
    inst = parse_version(installed)
    src = parse_version(source)

    if inst == (0, 0, 0) or src == (0, 0, 0):
        return "unknown"
    if inst == src:
        return "same"
    if src > inst:
        return "upgrade"
    return "downgrade"


def load_log() -> list:
    """Load install log."""
    if LOG_PATH.exists():
        try:
            data = json.loads(LOG_PATH.read_text(encoding="utf-8"))
            return data.get("operations", [])
        except Exception:
            pass
    return []


def save_log(operations: list):
    """Save install log with rotation (keeps last MAX_LOG_ENTRIES)."""
    DATA_DIR.mkdir(parents=True, exist_ok=True)
    # Rotate: keep only the last N entries
    if len(operations) > MAX_LOG_ENTRIES:
        operations = operations[-MAX_LOG_ENTRIES:]
    data = {
        "version": VERSION,
        "operations": operations,
        "total_operations": len(operations),
        "last_updated": datetime.now().isoformat(),
    }
    LOG_PATH.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")


def append_log(entry: dict):
    """Append entry to install log."""
    ops = load_log()
    ops.append(entry)
    save_log(ops)


def cleanup_old_backups(skill_name: str):
    """Keep only the last N backups for a skill."""
    if not BACKUPS_DIR.exists():
        return

    prefix = f"{skill_name}_"
    backups = sorted(
        [d for d in BACKUPS_DIR.iterdir() if d.is_dir() and d.name.startswith(prefix)],
        key=lambda d: d.stat().st_mtime,
    )

    while len(backups) > MAX_BACKUPS_PER_SKILL:
        old = backups.pop(0)
        try:
            shutil.rmtree(old)
        except Exception:
            pass


def get_all_skill_dirs() -> list:
    """Get all skill directories in the ecosystem (top-level + nested)."""
    dirs = []
    for item in sorted(SKILLS_ROOT.iterdir()):
        if not item.is_dir() or item.name.startswith("."):
            continue
        if item.name == "agent-orchestrator":
            continue
        skill_md = item / "SKILL.md"
        if skill_md.exists():
            dirs.append(item)
        # Check nested (e.g., juntas-comerciais/junta-leiloeiros)
        for child in item.iterdir():
            if child.is_dir() and (child / "SKILL.md").exists():
                if child not in dirs:
                    dirs.append(child)
    return dirs


# ── Installation Steps ─────────────────────────────────────────────────────

def step1_resolve_source(source: str = None, do_detect: bool = False, auto: bool = False) -> dict:
    """STEP 1: Resolve source directory."""
    if source:
        source_path = Path(source).resolve()
        if not source_path.exists():
            return {"success": False, "error": f"Source does not exist: {source_path}"}
        if not (source_path / "SKILL.md").exists():
            return {"success": False, "error": f"No SKILL.md found in {source_path}"}
        return {"success": True, "sources": [str(source_path)]}

    if do_detect:
        result = detect()
        candidates = [c for c in result["candidates"] if not c["already_installed"]]

        if not candidates:
            return {
                "success": False,
                "error": "No uninstalled skills detected",
                "scanned_locations": result.get("scanned_locations", []),
            }

        if auto:
            return {
                "success": True,
                "sources": [c["source_path"] for c in candidates],
                "candidates": candidates,
            }

        # Return candidates for user to choose
        return {
            "success": True,
            "sources": [c["source_path"] for c in candidates],
            "candidates": candidates,
            "interactive": True,
        }

    return {"success": False, "error": "No --source or --detect provided"}


def step2_validate(source_path: Path) -> dict:
    """STEP 2: Validate the skill."""
    result = validate(source_path)
    return result


def step3_determine_name(source_path: Path, name_override: str = None) -> str:
    """STEP 3: Determine skill name."""
    if name_override:
        return sanitize_name(name_override)

    meta = parse_yaml_frontmatter(source_path / "SKILL.md")
    name = meta.get("name", source_path.name)
    return sanitize_name(name)


def step4_check_conflicts(skill_name: str) -> dict:
    """STEP 4: Check for existing skill with same name."""
    dest = SKILLS_ROOT / skill_name

    conflicts = []
    if dest.exists():
        conflicts.append(str(dest))

    return {
        "has_conflicts": len(conflicts) > 0,
        "conflicts": conflicts,
        "destination": str(dest),
    }


def _backup_ignore(directory, contents):
    """Ignore function for shutil.copytree to skip backup/staging dirs."""
    ignored = set()
    dir_path = Path(directory)
    for item in contents:
        item_path = dir_path / item
        # Skip backup and staging directories to prevent recursion
        if item in ("backups", "staging") and dir_path.name == "data":
            ignored.add(item)
        # Skip .git and __pycache__
        if item in (".git", "__pycache__", "node_modules", ".venv"):
            ignored.add(item)
    return ignored


def step5_backup(skill_name: str) -> dict:
    """STEP 5: Backup existing skill before overwrite."""
    dest = SKILLS_ROOT / skill_name
    timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
    backup_name = f"{skill_name}_{timestamp}"
    backup_path = BACKUPS_DIR / backup_name

    BACKUPS_DIR.mkdir(parents=True, exist_ok=True)

    backed_up = []

    if dest.exists():
        try:
            shutil.copytree(dest, backup_path, ignore=_backup_ignore, dirs_exist_ok=True)
            backed_up.append(str(dest))
        except Exception as e:
            return {"success": False, "error": f"Backup failed for {dest}: {e}"}

    # Cleanup old backups
    cleanup_old_backups(skill_name)

    return {
        "success": True,
        "backup_path": str(backup_path),
        "backed_up": backed_up,
    }


def step6_copy_to_skills_root(source_path: Path, skill_name: str) -> dict:
    """STEP 6: Copy to skills root via staging area."""
    dest = SKILLS_ROOT / skill_name
    staging = STAGING_DIR / skill_name

    STAGING_DIR.mkdir(parents=True, exist_ok=True)

    # Clean staging
    if staging.exists():
        shutil.rmtree(staging)

    # Copy to staging first (skip backups/staging to prevent recursion)
    try:
        shutil.copytree(source_path, staging, ignore=_backup_ignore, dirs_exist_ok=True)
    except Exception as e:
        return {"success": False, "error": f"Copy to staging failed: {e}"}

    # Validate staging copy
    staging_skill_md = staging / "SKILL.md"
    if not staging_skill_md.exists():
        shutil.rmtree(staging, ignore_errors=True)
        return {"success": False, "error": "SKILL.md missing after copy to staging"}

    # Verify hash matches
    source_hash = md5_dir(source_path)
    staging_hash = md5_dir(staging)
    if source_hash != staging_hash:
        shutil.rmtree(staging, ignore_errors=True)
        return {
            "success": False,
            "error": f"Hash mismatch: source={source_hash} staging={staging_hash}",
        }

    # Remove existing destination if exists
    if dest.exists():
        try:
            shutil.rmtree(dest)
        except Exception as e:
            shutil.rmtree(staging, ignore_errors=True)
            return {"success": False, "error": f"Cannot remove existing destination: {e}"}

    # Move staging to final destination
    try:
        shutil.move(str(staging), str(dest))
    except Exception as e:
        # Try copy + delete as fallback (cross-device moves)
        try:
            shutil.copytree(staging, dest, dirs_exist_ok=True)
            shutil.rmtree(staging, ignore_errors=True)
        except Exception as e2:
            shutil.rmtree(staging, ignore_errors=True)
            return {"success": False, "error": f"Move failed: {e}, copy fallback failed: {e2}"}

    return {
        "success": True,
        "installed_to": str(dest),
        "hash": source_hash,
    }




def step8_update_registry() -> dict:
    """STEP 8: Run scan_registry.py to update orchestrator registry."""
    if not SCAN_SCRIPT.exists():
        return {
            "success": False,
            "error": f"scan_registry.py not found at {SCAN_SCRIPT}",
        }

    try:
        result = subprocess.run(
            ["python", str(SCAN_SCRIPT), "--force"],
            capture_output=True,
            text=True,
            timeout=30,
            cwd=str(SKILLS_ROOT),
        )
        if result.returncode == 0:
            try:
                scan_output = json.loads(result.stdout)
            except json.JSONDecodeError:
                scan_output = {"raw": result.stdout[:500]}
            return {"success": True, "scan_output": scan_output}
        else:
            return {
                "success": False,
                "error": f"scan_registry.py failed: {result.stderr[:500]}",
            }
    except subprocess.TimeoutExpired:
        return {"success": False, "error": "scan_registry.py timed out (30s)"}
    except Exception as e:
        return {"success": False, "error": f"Failed to run scan_registry.py: {e}"}


def step9_verify(skill_name: str) -> dict:
    """STEP 9: Verify installation is complete and correct."""
    checks = []

    # Check 1: Skill directory exists
    dest = SKILLS_ROOT / skill_name
    checks.append({
        "check": "skill_dir_exists",
        "pass": dest.exists(),
        "path": str(dest),
    })

    # Check 2: SKILL.md exists and is readable
    skill_md = dest / "SKILL.md"
    skill_md_ok = False
    if skill_md.exists():
        try:
            text = skill_md.read_text(encoding="utf-8")
            skill_md_ok = len(text) > 10
        except Exception:
            pass
    checks.append({
        "check": "skill_md_readable",
        "pass": skill_md_ok,
        "path": str(skill_md),
    })

    # Check 3: Frontmatter parseable
    meta = parse_yaml_frontmatter(skill_md) if skill_md.exists() else {}
    checks.append({
        "check": "frontmatter_parseable",
        "pass": bool(meta.get("name")),
        "name": meta.get("name", ""),
    })

    # Check 4: Appears in registry
    in_registry = False
    if REGISTRY_PATH.exists():
        try:
            registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
            skill_names = [s.get("name", "").lower() for s in registry.get("skills", [])]
            in_registry = skill_name.lower() in skill_names
        except Exception:
            pass
    checks.append({
        "check": "in_registry",
        "pass": in_registry,
    })

    all_passed = all(c["pass"] for c in checks)

    return {
        "success": all_passed,
        "checks": checks,
        "total": len(checks),
        "passed": sum(1 for c in checks if c["pass"]),
        "failed": sum(1 for c in checks if not c["pass"]),
    }


def step10_log(skill_name: str, source: str, result: dict):
    """STEP 10: Log the operation."""
    entry = {
        "timestamp": datetime.now().isoformat(),
        "action": "install",
        "skill_name": skill_name,
        "source": source,
        "destination": str(SKILLS_ROOT / skill_name),
        "registered": result.get("registered", False),
        "registry_updated": result.get("registry_updated", False),
        "backup_path": result.get("backup_path"),
        "success": result.get("success", False),
        "verification": result.get("verification", {}),
        "warnings": result.get("warnings", []),
    }

    try:
        append_log(entry)
    except Exception:
        pass  # Logging failure is non-critical

    return entry


# ── Main Install Workflow ──────────────────────────────────────────────────

def install_single(
    source_path: str,
    name_override: str = None,
    force: bool = False,
    dry_run: bool = False,
    verbose: bool = True,
) -> dict:
    """Install a single skill through the 11-step workflow.

    Args:
        source_path: Path to skill directory containing SKILL.md.
        name_override: Optional name to use instead of frontmatter name.
        force: If True, overwrite existing skill (backup first).
        dry_run: If True, simulate all steps without writing anything.
        verbose: If True, print step-by-step progress to stdout.
    """
    source = Path(source_path).resolve()
    total_steps = 10
    result = {
        "success": False,
        "skill_name": "",
        "installed_to": "",
        "registered": False,
        "registry_updated": False,
        "backup_path": None,
        "warnings": [],
        "steps": {},
        "dry_run": dry_run,
        "installer_version": VERSION,
    }

    if dry_run and verbose:
        print(f"\n{_C.bold(_C.yellow('=== DRY RUN MODE === No changes will be made'))}\n")

    # STEP 1: Already resolved (source is provided)
    if verbose:
        _step(1, total_steps, "Resolving source...")
    if not source.exists() or not (source / "SKILL.md").exists():
        result["error"] = f"Invalid source: {source}"
        if verbose:
            _fail(f"Source invalid: {source}")
        return result

    result["steps"]["1_resolve"] = {"success": True, "source": str(source)}
    if verbose:
        _ok(f"Source: {source}")

    # STEP 2: Validate
    if verbose:
        _step(2, total_steps, "Validating skill...")
    validation = step2_validate(source)
    result["steps"]["2_validate"] = validation

    if not validation["valid"]:
        result["error"] = f"Validation failed: {'; '.join(validation['errors'])}"
        result["warnings"] = validation.get("warnings", [])
        if verbose:
            _fail(f"Validation failed: {len(validation['errors'])} error(s)")
            for e in validation["errors"]:
                _fail(f"  {e}")
        return result

    if verbose:
        _ok(f"Validation passed ({validation['passed']}/{validation['total_checks']} checks)")
        for w in validation.get("warnings", []):
            _warn(f"  {w}")

    result["warnings"].extend(validation.get("warnings", []))

    # STEP 3: Determine name
    if verbose:
        _step(3, total_steps, "Determining skill name...")
    skill_name = step3_determine_name(source, name_override)
    result["skill_name"] = skill_name
    result["steps"]["3_name"] = {"name": skill_name}

    if not skill_name:
        result["error"] = "Could not determine skill name"
        if verbose:
            _fail("Could not determine skill name")
        return result
    if verbose:
        _ok(f"Name: {_C.bold(skill_name)}")

    # Version comparison with installed
    source_meta = parse_yaml_frontmatter(source / "SKILL.md")
    source_version = source_meta.get("version", "")
    dest = SKILLS_ROOT / skill_name
    if dest.exists() and (dest / "SKILL.md").exists():
        installed_meta = parse_yaml_frontmatter(dest / "SKILL.md")
        installed_version = installed_meta.get("version", "")
        ver_cmp = compare_versions(installed_version, source_version)
        result["version_comparison"] = {
            "installed": installed_version,
            "source": source_version,
            "result": ver_cmp,
        }
        if verbose and ver_cmp != "unknown":
            if ver_cmp == "upgrade":
                _ok(f"Version: {installed_version} -> {_C.green(source_version)} (upgrade)")
            elif ver_cmp == "downgrade":
                _warn(f"Version: {installed_version} -> {_C.yellow(source_version)} (downgrade)")
            elif ver_cmp == "same":
                _ok(f"Version: {source_version} (same)")

    # STEP 4: Check conflicts
    if verbose:
        _step(4, total_steps, "Checking conflicts...")
    conflicts = step4_check_conflicts(skill_name)
    result["steps"]["4_conflicts"] = conflicts

    if conflicts["has_conflicts"] and not force:
        result["error"] = (
            f"Skill '{skill_name}' already exists at: {', '.join(conflicts['conflicts'])}. "
            f"Use --force to overwrite."
        )
        if verbose:
            _fail(f"Conflict: skill already exists. Use --force to overwrite.")
        return result
    if verbose:
        if conflicts["has_conflicts"]:
            _warn(f"Conflict detected -- will overwrite (--force)")
        else:
            _ok("No conflicts")

    # STEP 5: Backup (if overwriting)
    if verbose:
        _step(5, total_steps, "Creating backup...")
    backup_result = {"success": True, "backup_path": None}
    if conflicts["has_conflicts"] and force:
        if dry_run:
            backup_result = {"success": True, "backup_path": "(dry-run)", "dry_run": True}
            if verbose:
                _ok("Backup would be created (dry-run)")
        else:
            backup_result = step5_backup(skill_name)
            if not backup_result["success"]:
                result["error"] = f"Backup failed: {backup_result.get('error')}"
                if verbose:
                    _fail(f"Backup failed: {backup_result.get('error')}")
                return result
            result["backup_path"] = backup_result.get("backup_path")
            if verbose:
                _ok(f"Backup saved: {backup_result.get('backup_path', '?')}")
    else:
        if verbose:
            _ok("No backup needed (new install)")

    result["steps"]["5_backup"] = backup_result

    # Check idempotency: same content?
    idempotent = False
    if dest.exists():
        source_hash = md5_dir(source)
        dest_hash = md5_dir(dest)
        if source_hash == dest_hash:
            idempotent = True
            result["idempotent"] = True
            result["installed_to"] = str(dest)
            result["steps"]["6_copy"] = {
                "success": True,
                "installed_to": str(dest),
                "skipped": "identical content already at destination",
                "hash": source_hash,
            }
            if verbose:
                _ok("Content identical -- skipping copy")

    # STEP 6: Copy to skills root (skip if idempotent)
    if not idempotent:
        if verbose:
            _step(6, total_steps, "Copying to skills root via staging...")
        if dry_run:
            result["steps"]["6_copy"] = {
                "success": True,
                "installed_to": str(dest),
                "dry_run": True,
            }
            result["installed_to"] = str(dest)
            if verbose:
                _ok(f"Would copy to: {dest} (dry-run)")
        else:
            copy_result = step6_copy_to_skills_root(source, skill_name)
            result["steps"]["6_copy"] = copy_result

            if not copy_result["success"]:
                result["error"] = f"Copy failed: {copy_result.get('error')}"
                if verbose:
                    _fail(f"Copy failed: {copy_result.get('error')}")
                step10_log(skill_name, str(source), result)
                return result

            result["installed_to"] = copy_result["installed_to"]
            if verbose:
                _ok(f"Copied to: {copy_result['installed_to']}")
    elif verbose and not idempotent:
        _step(6, total_steps, "Copying to skills root...")


    # STEP 7: Update orchestrator registry
    if verbose:
        _step(7, total_steps, "Updating orchestrator registry...")
    
    if not dry_run:
        registry_result = step8_update_registry()
        result["steps"]["7_registry"] = registry_result
        result["registry_updated"] = registry_result["success"]

        if not registry_result["success"]:
            result["warnings"].append(f"Registry update warning: {registry_result.get('error')}")
            if verbose:
                _warn(f"Registry: {registry_result.get('error')}")
        elif verbose:
            _ok("Registry updated")
    else:
        result["steps"]["7_registry"] = {"success": True, "dry_run": True}
        result["registry_updated"] = True
        if verbose:
            _ok("Would update registry (dry-run)")

    # STEP 8: Verifying installation
    if verbose:
        _step(8, total_steps, "Verifying installation...")
    
    if not dry_run:
        verify_result = step9_verify(skill_name)
        result["steps"]["8_verify"] = verify_result
        result["verification"] = verify_result
        if verbose:
            if verify_result["success"]:
                _ok(f"All {verify_result['total']} verification checks passed")
            else:
                failed_checks = [c for c in verify_result["checks"] if not c["pass"]]
                _warn(f"{verify_result['failed']}/{verify_result['total']} checks failed")
                for c in failed_checks:
                    _fail(f"  {c['check']}")
    else:
        result["steps"]["8_verify"] = {"success": True, "dry_run": True}
        result["verification"] = {"success": True, "dry_run": True}
        if verbose:
            _ok("Verification skipped (dry-run)")

    # STEP 9: Packaging ZIP for Claude.ai...
    if verbose:
        _step(9, total_steps, "Packaging ZIP for Claude.ai...")
    
    if not dry_run:
        try:
            from package_skill import package_skill as pkg_skill
            zip_result = pkg_skill(SKILLS_ROOT / skill_name)
            result["steps"]["9_package"] = zip_result
            result["zip_path"] = zip_result.get("zip_path") if zip_result["success"] else None
            if verbose:
                if zip_result["success"]:
                    _ok(f"ZIP: {zip_result.get('zip_path')} ({zip_result.get('zip_size_kb', '?')} KB)")
                else:
                    _warn(f"ZIP: {zip_result.get('error', 'failed')}")
        except Exception as e:
            zip_result = {"success": False, "error": str(e)}
            result["steps"]["9_package"] = zip_result
            result["warnings"].append(f"ZIP packaging warning: {e}")
            if verbose:
                _warn(f"ZIP packaging: {e}")
    else:
        result["steps"]["9_package"] = {"success": True, "dry_run": True}
        if verbose:
            _ok("Would create ZIP (dry-run)")

    # STEP 10: Logging operation...
    if verbose:
        _step(10, total_steps, "Logging operation...")
    if dry_run:
        result["success"] = True
        result["steps"]["10_log"] = {"logged": False, "dry_run": True}
        if verbose:
            _ok("Would log operation (dry-run)")
            print(f"\n{_C.bold(_C.green('DRY RUN COMPLETE'))} -- no changes were made.\n")
    else:
        result["success"] = result.get("verification", {}).get("success", False)
        if not result.get("verification", {}).get("success", True):
            failed_checks = [c for c in result.get("verification", {}).get("checks", []) if not c.get("pass")]
            result["warnings"].append(
                f"Verification: {result['verification'].get('failed', 0)} check(s) failed: "
                + ", ".join(c["check"] for c in failed_checks)
            )

        log_entry = step10_log(skill_name, str(source), result)
        result["steps"]["11_log"] = {"logged": True}
        if verbose:
            _ok("Operation logged")
            if result["success"]:
                print(f"\n{_C.bold(_C.green('SUCCESS'))} -- {_C.bold(skill_name)} installed.\n")
            else:
                print(f"\n{_C.bold(_C.red('FAILED'))} -- see warnings above.\n")

    return result


# ── Uninstall ─────────────────────────────────────────────────────────────

def uninstall_skill(skill_name: str, keep_backup: bool = True) -> dict:
    """Uninstall a skill: remove from skills root, .claude/skills/, and registry."""
    skill_name = sanitize_name(skill_name)
    result = {
        "success": False,
        "skill_name": skill_name,
        "removed": [],
        "backup_path": None,
    }

    dest = SKILLS_ROOT / skill_name
    if not dest.exists():
        result["error"] = f"Skill '{skill_name}' not found"
        return result

    # Backup before removing
    if keep_backup and dest.exists():
        timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
        backup_path = BACKUPS_DIR / f"{skill_name}_{timestamp}"
        BACKUPS_DIR.mkdir(parents=True, exist_ok=True)
        try:
            shutil.copytree(dest, backup_path, dirs_exist_ok=True)
            result["backup_path"] = str(backup_path)
        except Exception as e:
            result["error"] = f"Backup failed: {e}"
            return result

    # Remove from skills root
    if dest.exists():
        try:
            shutil.rmtree(dest)
            result["removed"].append(str(dest))
        except Exception as e:
            result["error"] = f"Failed to remove {dest}: {e}"
            return result


    # Update registry
    registry_result = step8_update_registry()

    # Remove ZIP from Desktop if exists
    zip_path = Path(os.path.expanduser("~")) / "Desktop" / f"{skill_name}.zip"
    if zip_path.exists():
        try:
            zip_path.unlink()
            result["removed"].append(str(zip_path))
        except Exception:
            pass

    # Log operation
    entry = {
        "timestamp": datetime.now().isoformat(),
        "action": "uninstall",
        "skill_name": skill_name,
        "removed": result["removed"],
        "backup_path": result.get("backup_path"),
        "success": True,
    }
    try:
        append_log(entry)
    except Exception:
        pass

    result["success"] = True
    result["registry_updated"] = registry_result.get("success", False)
    return result


# ── Health Check ──────────────────────────────────────────────────────────

def health_check() -> dict:
    """Run a global health check on all installed skills."""
    results = []

    # Load registry
    registry_skills = []
    if REGISTRY_PATH.exists():
        try:
            registry = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
            registry_skills = registry.get("skills", [])
        except Exception:
            pass

    registry_names = {s.get("name", "").lower() for s in registry_skills}

    # Check all skill directories in skills root
    for item in sorted(SKILLS_ROOT.iterdir()):
        if not item.is_dir():
            continue
        if item.name.startswith("."):
            continue
        if item.name in ("agent-orchestrator", "skill-installer"):
            continue

        skill_md = item / "SKILL.md"
        if not skill_md.exists():
            continue

        meta = parse_yaml_frontmatter(skill_md)
        name = meta.get("name", item.name).lower()

        checks = {
            "name": name,
            "dir": str(item),
            "skill_md_exists": skill_md.exists(),
            "frontmatter_ok": bool(meta.get("name") and meta.get("description")),
            "in_registry": name in registry_names,
            "has_scripts": (item / "scripts").exists(),
            "has_references": (item / "references").exists(),
        }

        # Count issues
        issues = []
        if not checks["frontmatter_ok"]:
            issues.append("invalid frontmatter (missing name or description)")
        if not checks["in_registry"]:
            issues.append("not in orchestrator registry")

        checks["healthy"] = len(issues) == 0
        checks["issues"] = issues
        results.append(checks)

    # Also check nested skills (e.g., juntas-comerciais/junta-leiloeiros)
    for parent in SKILLS_ROOT.iterdir():
        if not parent.is_dir() or parent.name.startswith("."):
            continue
        if parent.name in ("agent-orchestrator", "skill-installer"):
            continue
        for child in parent.iterdir():
            if child.is_dir() and (child / "SKILL.md").exists():
                # Skip if already checked at top level
                if any(r["dir"] == str(child) for r in results):
                    continue
                meta = parse_yaml_frontmatter(child / "SKILL.md")
                name = meta.get("name", child.name).lower()
                checks = {
                    "name": name,
                    "dir": str(child),
                    "skill_md_exists": True,
                    "frontmatter_ok": bool(meta.get("name") and meta.get("description")),
                    "in_registry": name in registry_names,
                    "has_scripts": (child / "scripts").exists(),
                    "has_references": (child / "references").exists(),
                }
                issues = []
                if not checks["frontmatter_ok"]:
                    issues.append("invalid frontmatter")
                if not checks["in_registry"]:
                    issues.append("not in orchestrator registry")
                checks["healthy"] = len(issues) == 0
                checks["issues"] = issues
                results.append(checks)

    healthy = sum(1 for r in results if r["healthy"])
    unhealthy = sum(1 for r in results if not r["healthy"])

    # Check for registry duplicates
    from collections import Counter
    reg_name_counts = Counter(s.get("name", "").lower() for s in registry_skills)
    duplicates = {name: count for name, count in reg_name_counts.items() if count > 1}

    return {
        "total_skills": len(results),
        "healthy": healthy,
        "unhealthy": unhealthy,
        "registry_duplicates": duplicates,
        "skills": results,
    }


# ── Auto-Repair ──────────────────────────────────────────────────────────

def repair_health(verbose: bool = True) -> dict:
    """Run health check and automatically fix all issues found.

    Fixes:
    - Skills not registered in .claude/skills/ -> registers them
    - Skills not in orchestrator registry -> triggers registry scan
    - Registry duplicates -> triggers re-scan with deduplication
    """
    if verbose:
        print(f"\n{_C.bold('=== HEALTH CHECK + AUTO-REPAIR ===')}\n")

    health = health_check()
    repairs = []
    errors = []

    unhealthy_skills = [s for s in health["skills"] if not s["healthy"]]

    if not unhealthy_skills and not health["registry_duplicates"]:
        if verbose:
            _ok(f"All {health['total_skills']} skills are healthy. Nothing to repair.")
        health["repairs"] = []
        return health


    # Fix: update registry to pick up missing skills and remove duplicates
    needs_registry_update = (
        any("not in orchestrator registry" in "; ".join(s["issues"]) for s in unhealthy_skills)
        or health["registry_duplicates"]
    )
    if needs_registry_update:
        if verbose:
            _step(2, 2, "Updating orchestrator registry...")
        reg_result = step8_update_registry()
        if reg_result["success"]:
            repairs.append({"action": "registry_update", "success": True})
            if verbose:
                _ok("Registry updated")
        else:
            errors.append({"action": "registry_update", "error": reg_result.get("error")})
            if verbose:
                _fail(f"Registry update failed: {reg_result.get('error')}")

    # Re-run health check to confirm
    health_after = health_check()

    result = {
        "before": {
            "healthy": health["healthy"],
            "unhealthy": health["unhealthy"],
            "duplicates": len(health["registry_duplicates"]),
        },
        "after": {
            "healthy": health_after["healthy"],
            "unhealthy": health_after["unhealthy"],
            "duplicates": len(health_after["registry_duplicates"]),
        },
        "repairs": repairs,
        "errors": errors,
        "skills": health_after["skills"],
    }

    if verbose:
        fixed = health["unhealthy"] - health_after["unhealthy"]
        print(f"\n{_C.bold('Result:')} Fixed {_C.green(str(fixed))} of {health['unhealthy']} issues.")
        if health_after["unhealthy"] > 0:
            _warn(f"{health_after['unhealthy']} issues remaining")
        else:
            _ok("All skills healthy!")
        print()

    return result


# ── Rollback ─────────────────────────────────────────────────────────────

def rollback_skill(skill_name: str, verbose: bool = True) -> dict:
    """Restore a skill from its latest backup.

    Finds the most recent backup for the given skill and restores it
    to the skills root, re-registers, and updates the registry.
    """
    skill_name = sanitize_name(skill_name)
    result = {
        "success": False,
        "skill_name": skill_name,
        "restored_from": None,
    }

    if not BACKUPS_DIR.exists():
        result["error"] = "No backups directory found"
        if verbose:
            _fail("No backups directory found")
        return result

    # Find backups for this skill
    prefix = f"{skill_name}_"
    backups = sorted(
        [d for d in BACKUPS_DIR.iterdir() if d.is_dir() and d.name.startswith(prefix)],
        key=lambda d: d.stat().st_mtime,
        reverse=True,
    )

    if not backups:
        result["error"] = f"No backups found for skill '{skill_name}'"
        if verbose:
            _fail(f"No backups found for '{skill_name}'")
            # Show available backups
            all_backups = [d.name for d in BACKUPS_DIR.iterdir() if d.is_dir()]
            if all_backups:
                print(f"  Available backups: {', '.join(sorted(set(b.rsplit('_', 2)[0] for b in all_backups)))}")
        return result

    latest_backup = backups[0]
    backup_skill_md = latest_backup / "SKILL.md"

    if not backup_skill_md.exists():
        result["error"] = f"Backup is invalid (no SKILL.md): {latest_backup}"
        if verbose:
            _fail(f"Backup invalid: {latest_backup}")
        return result

    if verbose:
        timestamp = latest_backup.name.replace(f"{skill_name}_", "")
        print(f"\n{_C.bold(f'=== ROLLBACK: {skill_name} ===')}")
        print(f"  Backup: {latest_backup.name} ({timestamp})")

    # Restore to skills root
    dest = SKILLS_ROOT / skill_name
    if verbose:
        _step(1, 3, "Restoring from backup...")

    try:
        if dest.exists():
            shutil.rmtree(dest)
        shutil.copytree(latest_backup, dest, ignore=_backup_ignore, dirs_exist_ok=True)
        result["restored_from"] = str(latest_backup)
        if verbose:
            _ok(f"Restored to: {dest}")
    except Exception as e:
        result["error"] = f"Restore failed: {e}"
        if verbose:
            _fail(f"Restore failed: {e}")
        return result

    # Update registry
    if verbose:
        _step(2, 2, "Updating registry...")
    step8_update_registry()
    if verbose:
        _ok("Registry updated")

    # Log operation
    append_log({
        "timestamp": datetime.now().isoformat(),
        "action": "rollback",
        "skill_name": skill_name,
        "backup_used": str(latest_backup),
        "success": True,
    })

    result["success"] = True
    if verbose:
        print(f"\n{_C.bold(_C.green('ROLLBACK COMPLETE'))}\n")
    return result


# ── Reinstall All ────────────────────────────────────────────────────────

def reinstall_all(force: bool = True, verbose: bool = True) -> dict:
    """Re-register every installed skill in one pass.

    Iterates all skill directories, re-copies SKILL.md to .claude/skills/,
    re-packages ZIPs, and updates the registry.
    """
    if verbose:
        print(f"\n{_C.bold('=== REINSTALL ALL SKILLS ===')}\n")

    skill_dirs = get_all_skill_dirs()
    results_list = []

    for i, skill_dir in enumerate(skill_dirs, 1):
        meta = parse_yaml_frontmatter(skill_dir / "SKILL.md")
        name = meta.get("name", skill_dir.name)
        name = sanitize_name(name)

        if verbose:
            print(f"  [{i}/{len(skill_dirs)}] {_C.bold(name)}...")


        # Re-package ZIP
        zip_result = {"success": False}
        try:
            from package_skill import package_skill as pkg_skill
            zip_result = pkg_skill(skill_dir)
        except Exception:
            pass

        r = {
            "skill": name,
            "zipped": zip_result.get("success", False),
        }
        results_list.append(r)

        if verbose:
            zip_status = _C.green("ZIP-OK") if zip_result.get("success") else _C.yellow("ZIP-WARN")
            print(f"    {zip_status}")

    # Final registry update
    if verbose:
        print(f"\n  Updating registry...")
    step8_update_registry()

    zipped_ok = sum(1 for r in results_list if r["zipped"])

    result = {
        "total": len(results_list),
        "zipped": zipped_ok,
        "results": results_list,
    }

    if verbose:
        print(f"\n{_C.bold('Result:')} {len(results_list)} skills processed, {zipped_ok} zipped.")
        print()

    append_log({
        "timestamp": datetime.now().isoformat(),
        "action": "reinstall_all",
        "total": len(results_list),
        "zipped": zipped_ok,
        "success": True,
    })

    return result


# ── Status Dashboard ─────────────────────────────────────────────────────

def show_status(verbose: bool = True) -> dict:
    """Rich status dashboard showing all skills, versions, and health."""
    health = health_check()

    # Load registry for version info
    registry_skills = {}
    if REGISTRY_PATH.exists():
        try:
            reg = json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
            for s in reg.get("skills", []):
                registry_skills[s.get("name", "").lower()] = s
        except Exception:
            pass

    # Count backups per skill
    backup_counts = {}
    if BACKUPS_DIR.exists():
        for d in BACKUPS_DIR.iterdir():
            if d.is_dir():
                # Extract skill name (everything before last _TIMESTAMP)
                parts = d.name.rsplit("_", 2)
                if len(parts) >= 3:
                    bname = parts[0]
                else:
                    bname = d.name
                backup_counts[bname] = backup_counts.get(bname, 0) + 1

    # Log stats
    log_ops = load_log()
    install_count = sum(1 for o in log_ops if o.get("action") == "install")
    uninstall_count = sum(1 for o in log_ops if o.get("action") == "uninstall")
    rollback_count = sum(1 for o in log_ops if o.get("action") == "rollback")

    if verbose:
        print(f"\n{_C.bold('+' + '='*62 + '+')}")
        print(f"{_C.bold('|')}  {_C.bold(_C.cyan('Skill Installer v' + VERSION + ' -- Status Dashboard'))}              {_C.bold('|')}")
        print(f"{_C.bold('+' + '='*62 + '+')}\n")

        # Skills table header
        print(f"  {'Name':<24} {'Version':<10} {'Health':<10} {'Registered':<12} {'Backups':<8}")
        print(f"  {'-'*24} {'-'*10} {'-'*10} {'-'*12} {'-'*8}")

        for skill in health["skills"]:
            name = skill["name"][:22]
            reg_entry = registry_skills.get(skill["name"], {})
            version = reg_entry.get("version", "-") or "-"
            status = _C.green("OK") if skill["healthy"] else _C.red("ISSUE")
            registered = _C.green("Yes") if skill["claude_registered"] else _C.red("No")
            backups = str(backup_counts.get(skill["name"], 0))
            print(f"  {name:<24} {version:<10} {status:<19} {registered:<21} {backups:<8}")

            if not skill["healthy"]:
                for issue in skill["issues"]:
                    print(f"    {_C.dim(f'  -> {issue}')}")

        print(f"\n  {_C.bold('Summary:')}")
        print(f"    Skills: {_C.bold(str(health['total_skills']))} total, "
              f"{_C.green(str(health['healthy']))} healthy, "
              f"{_C.red(str(health['unhealthy'])) if health['unhealthy'] else '0'} unhealthy")
        if health["registry_duplicates"]:
            print(f"    {_C.yellow('Duplicates:')} {health['registry_duplicates']}")

        print(f"\n  {_C.bold('Operations Log:')}")
        print(f"    Installs: {install_count} | Uninstalls: {uninstall_count} | Rollbacks: {rollback_count}")
        print(f"    Total logged: {len(log_ops)}")
        print()

    return {
        "health": health,
        "backup_counts": backup_counts,
        "log_stats": {
            "total": len(log_ops),
            "installs": install_count,
            "uninstalls": uninstall_count,
            "rollbacks": rollback_count,
        },
    }


# ── Log Viewer ───────────────────────────────────────────────────────────

def show_log(n: int = 20, verbose: bool = True) -> list:
    """Show the last N log entries."""
    ops = load_log()
    recent = ops[-n:] if len(ops) > n else ops

    if verbose:
        print(f"\n{_C.bold(f'=== Last {len(recent)} Operations ===')}\n")
        for op in reversed(recent):
            ts = op.get("timestamp", "?")[:19]
            action = op.get("action", "?")
            name = op.get("skill_name", "?")
            success = op.get("success", False)

            # Color the action
            if action == "install":
                action_str = _C.green("INSTALL")
            elif action == "uninstall":
                action_str = _C.red("UNINSTALL")
            elif action == "rollback":
                action_str = _C.yellow("ROLLBACK")
            elif action == "reinstall_all":
                action_str = _C.cyan("REINSTALL-ALL")
            else:
                action_str = action.upper()

            status = _C.green(_C.OK) if success else _C.red(_C.FAIL)
            print(f"  {_C.dim(ts)}  {action_str:<22} {name:<24} {status}")

        print()

    return recent


# ── CLI Entry Point ───────────────────────────────────────────────────────

def main():
    args = sys.argv[1:]

    source = None
    name_override = None
    force = "--force" in args
    dry_run = "--dry-run" in args
    do_detect = "--detect" in args
    auto = "--auto" in args
    do_uninstall = "--uninstall" in args
    do_health = "--health" in args
    do_repair = "--repair" in args
    do_rollback = "--rollback" in args
    do_reinstall_all = "--reinstall-all" in args
    do_status = "--status" in args
    do_log = "--log" in args
    json_output = "--json" in args

    if "--source" in args:
        idx = args.index("--source")
        if idx + 1 < len(args):
            source = args[idx + 1]

    if "--name" in args:
        idx = args.index("--name")
        if idx + 1 < len(args):
            name_override = args[idx + 1]

    # ── Status dashboard ──
    if do_status:
        result = show_status(verbose=not json_output)
        if json_output:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0)

    # ── Log viewer ──
    if do_log:
        n = 20
        idx = args.index("--log")
        if idx + 1 < len(args):
            try:
                n = int(args[idx + 1])
            except ValueError:
                pass
        result = show_log(n=n, verbose=not json_output)
        if json_output:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0)

    # ── Health check (with optional auto-repair) ──
    if do_health:
        if do_repair:
            result = repair_health(verbose=not json_output)
            if json_output:
                print(json.dumps(result, indent=2, ensure_ascii=False))
            remaining = result.get("after", {}).get("unhealthy", 0)
            sys.exit(0 if remaining == 0 else 1)
        else:
            result = health_check()
            if json_output:
                print(json.dumps(result, indent=2, ensure_ascii=False))
            else:
                # Pretty print health
                print(f"\n{_C.bold('=== HEALTH CHECK ===')}\n")
                for s in result["skills"]:
                    if s["healthy"]:
                        _ok(s["name"])
                    else:
                        _fail(f"{s['name']}: {'; '.join(s['issues'])}")
                print(f"\n  {_C.bold(str(result['healthy']))}/{result['total_skills']} healthy")
                if result["unhealthy"] > 0:
                    print(f"  {_C.yellow('Tip:')} run with --repair to auto-fix issues")
                if result["registry_duplicates"]:
                    print(f"  {_C.yellow('Duplicates:')} {result['registry_duplicates']}")
                print()
            sys.exit(0 if result["unhealthy"] == 0 else 1)

    # ── Rollback ──
    if do_rollback:
        idx = args.index("--rollback")
        if idx + 1 >= len(args):
            print(json.dumps({"error": "Usage: --rollback <skill-name>"}, indent=2))
            sys.exit(1)
        skill_name = args[idx + 1]
        result = rollback_skill(skill_name, verbose=not json_output)
        if json_output:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0 if result["success"] else 1)

    # ── Reinstall all ──
    if do_reinstall_all:
        result = reinstall_all(force=True, verbose=not json_output)
        if json_output:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0)

    # ── Uninstall ──
    if do_uninstall:
        idx = args.index("--uninstall")
        if idx + 1 >= len(args):
            print(json.dumps({"error": "Usage: --uninstall <skill-name>"}, indent=2))
            sys.exit(1)
        skill_name = args[idx + 1]
        result = uninstall_skill(skill_name)
        print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0 if result["success"] else 1)

    # ── No arguments: show usage ──
    if not source and not do_detect:
        print(f"\n{_C.bold(_C.cyan('Skill Installer v' + VERSION))}\n")
        print(f"  {_C.bold('Install:')}")
        print(f"    --source <path>                  Install skill from path")
        print(f"    --source <path> --force           Overwrite if exists")
        print(f"    --source <path> --name <name>     Custom name override")
        print(f"    --source <path> --dry-run         Simulate without changes")
        print(f"    --detect                          Auto-detect uninstalled skills")
        print(f"    --detect --auto                   Detect and install all")
        print(f"")
        print(f"  {_C.bold('Manage:')}")
        print(f"    --uninstall <name>               Uninstall (with backup)")
        print(f"    --rollback <name>                Restore from latest backup")
        print(f"    --reinstall-all                  Re-register + re-package all skills")
        print(f"")
        print(f"  {_C.bold('Monitor:')}")
        print(f"    --health                         Health check all skills")
        print(f"    --health --repair                Health check + auto-fix issues")
        print(f"    --status                         Rich status dashboard")
        print(f"    --log [N]                        Show last N operations (default: 20)")
        print(f"")
        print(f"  {_C.bold('Flags:')}")
        print(f"    --json                           Output JSON instead of pretty text")
        print(f"    --force                          Force overwrite")
        print(f"    --dry-run                        Simulate without changes")
        print()
        sys.exit(1)

    # ── Install from source ──
    if source:
        result = install_single(source, name_override, force, dry_run=dry_run, verbose=not json_output)
        if json_output:
            print(json.dumps(result, indent=2, ensure_ascii=False))
        sys.exit(0 if result["success"] else 1)

    # ── Detection mode ──
    elif do_detect:
        resolve = step1_resolve_source(do_detect=True, auto=auto)

        if not resolve["success"]:
            print(json.dumps(resolve, indent=2, ensure_ascii=False))
            sys.exit(1)

        if resolve.get("interactive") and not auto:
            if json_output:
                print(json.dumps({
                    "mode": "interactive",
                    "message": "Skills detected but not installed.",
                    "candidates": resolve["candidates"],
                }, indent=2, ensure_ascii=False))
            else:
                print(f"\n{_C.bold('=== Detected Uninstalled Skills ===')}\n")
                for i, c in enumerate(resolve["candidates"], 1):
                    name = c.get("name", "?")
                    src = c.get("source_path", "?")
                    loc = c.get("location_type", "?")
                    valid = _C.green(_C.OK) if c.get("valid_frontmatter") else _C.red(_C.FAIL)
                    print(f"  {i}. {_C.bold(name)} {valid}")
                    print(f"     {_C.dim(src)} ({loc})")
                print(f"\n  Run with --auto to install all, or --source <path> to install one.\n")
            sys.exit(0)

        # Auto mode: install all candidates
        results = []
        for src in resolve["sources"]:
            r = install_single(src, force=force, dry_run=dry_run, verbose=not json_output)
            results.append(r)

        total = len(results)
        success = sum(1 for r in results if r["success"])
        failed = total - success

        summary = {
            "mode": "auto",
            "total": total,
            "success": success,
            "failed": failed,
            "results": results,
        }

        if json_output:
            print(json.dumps(summary, indent=2, ensure_ascii=False))
        sys.exit(0 if failed == 0 else 1)


if __name__ == "__main__":
    main()
