Read the script
#!/usr/bin/env python3
"""SessionStart hook: ensure the RepoBrain CLI and engine are on PATH.
Single cross-platform installer for Claude Code's plugin SessionStart hook.
Idempotent and silent on the happy path. All output goes to stderr (stdout
on a hook is interpreted as injected context).
Strategy:
1. If all RepoBrain commands are on PATH at the bundled versions, exit 0.
2. Ensure pipx exists:
- macOS with Homebrew → `brew install pipx`
- Otherwise → `python -m pip install --user pipx`
Both paths require no sudo.
3. `pipx ensurepath` and prepend the pipx bin dir to PATH for the current
process so the next `pipx install` finds the freshly placed shim.
4. `pipx install --force <plugin_root>/engine`, then inject the bundled CLI
into the same environment with `--include-apps`.
5. If pipx remains unavailable, fall back to `pip install --user --upgrade`.
6. On total failure, print a clear manual-install message and exit 1.
"""
from __future__ import annotations
import os
import re
import shutil
import subprocess
import sys
from pathlib import Path
ENGINE_PACKAGE = "repobrain-engine"
REQUIRED_COMMANDS = ("rb", "rb-ask", "rb-refresh", "rb-mcp")
def log(msg: str) -> None:
print(msg, file=sys.stderr, flush=True)
def has(cmd: str) -> bool:
return shutil.which(cmd) is not None
def user_scripts_bin() -> Path | None:
"""Resolve site.USER_BASE bin/Scripts dir from the current Python."""
try:
import site
base = Path(site.USER_BASE)
except Exception:
return None
candidate = base / ("Scripts" if os.name == "nt" else "bin")
return candidate if candidate.is_dir() else None
def prepend_path(p: Path) -> None:
if not p.is_dir():
return
cur = os.environ.get("PATH", "")
parts = cur.split(os.pathsep)
if str(p) in parts:
return
os.environ["PATH"] = str(p) + os.pathsep + cur
def run(cmd: list[str]) -> int:
try:
return subprocess.run(cmd, stderr=sys.stderr, stdout=sys.stderr).returncode
except FileNotFoundError:
return 127
def ensure_pipx() -> bool:
"""Install pipx if missing. Returns True if pipx is available afterwards."""
if has("pipx"):
return True
# macOS with Homebrew: cleanest path, no sudo.
if sys.platform == "darwin" and has("brew"):
log("[repobrain] Installing pipx via Homebrew...")
run(["brew", "install", "pipx"])
# Cross-platform fallback: pip install --user pipx.
if not has("pipx"):
py = sys.executable or ("python" if has("python") else "python3")
log(f"[repobrain] Installing pipx via pip --user ({py})...")
run([py, "-m", "pip", "install", "--user", "--quiet", "pipx"])
# User-base scripts/bin needs to be on PATH for `pipx` to be findable.
ub = user_scripts_bin()
if ub:
prepend_path(ub)
return has("pipx") or _has_pipx_module()
def _has_pipx_module() -> bool:
"""Some installs put pipx on PATH only after `ensurepath`. Check for the
module so we can invoke it via `python -m pipx` as a fallback."""
try:
subprocess.run(
[sys.executable or "python", "-m", "pipx", "--version"],
check=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
return True
except Exception:
return False
def pipx(args: list[str]) -> int:
if has("pipx"):
return run(["pipx", *args])
return run([sys.executable or "python", "-m", "pipx", *args])
def read_project_version(package_dir: Path) -> str | None:
"""Read a bundled package version from its ``pyproject.toml``."""
try:
text = (package_dir / "pyproject.toml").read_text(encoding="utf-8")
except OSError:
return None
match = re.search(r'^version\s*=\s*"([^"]+)"', text, flags=re.MULTILINE)
return match.group(1) if match else None
def get_installed_engine_version() -> str | None:
"""Return the installed rb-mcp engine version, if it can be queried."""
rb_mcp = shutil.which("rb-mcp")
if rb_mcp is None:
return None
try:
completed = subprocess.run(
[rb_mcp, "--version"],
check=False,
capture_output=True,
text=True,
timeout=15,
)
except Exception:
return None
if completed.returncode != 0:
return None
output = (completed.stdout or completed.stderr).strip()
match = re.search(r"(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.]+)?)", output)
return match.group(1) if match else None
def get_installed_cli_version() -> str | None:
"""Return the installed ``rb`` CLI version, if it can be queried."""
rb = shutil.which("rb")
if rb is None:
return None
try:
completed = subprocess.run(
[rb, "version"],
check=False,
capture_output=True,
text=True,
timeout=15,
)
except Exception:
return None
if completed.returncode != 0:
return None
output = (completed.stdout or completed.stderr).strip()
match = re.search(r"(\d+\.\d+\.\d+(?:[-+][A-Za-z0-9.]+)?)", output)
return match.group(1) if match else None
def required_commands_available() -> bool:
"""Return whether every command promised by the plugin is on PATH."""
return all(has(command) for command in REQUIRED_COMMANDS)
def installation_is_current(
installed_engine: str | None,
expected_engine: str | None,
installed_cli: str | None,
expected_cli: str | None,
) -> bool:
"""Return whether versions and all public commands match the bundle."""
return (
installed_engine == expected_engine
and installed_cli == expected_cli
and None not in (installed_engine, expected_engine, installed_cli, expected_cli)
and required_commands_available()
)
def installed_bundle_is_current(
expected_engine: str | None,
expected_cli: str | None