#!/bin/sh # graphify-checkout-hook-start # Auto-rebuilds the knowledge graph (code only) when switching branches. # Installed by: graphify hook install # Deterministic clustering: networkx louvain iterates string-keyed sets whose # order is randomized per-process by PYTHONHASHSEED, so community assignments # churn run-to-run. Pinning it makes graphify-out reproducible. export PYTHONHASHSEED=0 PREV_HEAD=$1 NEW_HEAD=$2 BRANCH_SWITCH=$3 # Only run on branch switches, not file checkouts if [ "$BRANCH_SWITCH" != "1" ]; then exit 0 fi # Only run if graphify-out/ exists (graph has been built before) if [ ! -d "graphify-out" ]; then exit 0 fi # Skip during rebase/merge/cherry-pick GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) [ -d "$GIT_DIR/rebase-merge" ] && exit 0 [ -d "$GIT_DIR/rebase-apply" ] && exit 0 [ -f "$GIT_DIR/MERGE_HEAD" ] && exit 0 [ -f "$GIT_DIR/CHERRY_PICK_HEAD" ] && exit 0 # Detect the correct Python interpreter (handles uv tool, pipx, venv, system installs). # _PINNED was recorded at hook-install time; tried first so the hook works even # when the graphify launcher is not on PATH (common in GUI clients and CI). GRAPHIFY_PYTHON="" _PINNED='/Users/ksv741/.local/share/uv/tools/graphifyy/bin/python' if [ -n "$_PINNED" ] && [ -x "$_PINNED" ] && "$_PINNED" -c "import graphify" 2>/dev/null; then GRAPHIFY_PYTHON="$_PINNED" fi # Second probe: read graphify-out/.graphify_python (written by the skill and # CLI; survives uv-tool reinstalls and is the same source the README documents). if [ -z "$GRAPHIFY_PYTHON" ]; then _GFY_PYTHON_FILE="graphify-out/.graphify_python" if [ -f "$_GFY_PYTHON_FILE" ]; then _FROM_FILE=$(cat "$_GFY_PYTHON_FILE" 2>/dev/null | tr -d '[:space:]') case "$_FROM_FILE" in *[!a-zA-Z0-9/_.@:\-]*) _FROM_FILE="" ;; # allowlist (covers Windows paths) esac if [ -n "$_FROM_FILE" ] && [ -x "$_FROM_FILE" ] && "$_FROM_FILE" -c "import graphify" 2>/dev/null; then GRAPHIFY_PYTHON="$_FROM_FILE" fi fi fi # Third probe: resolve via the graphify launcher on PATH (shebang probe). if [ -z "$GRAPHIFY_PYTHON" ]; then GRAPHIFY_BIN=$(command -v graphify 2>/dev/null) if [ -n "$GRAPHIFY_BIN" ]; then case "$GRAPHIFY_BIN" in *.exe) _SHEBANG="" ;; *) _SHEBANG=$(head -1 "$GRAPHIFY_BIN" | sed 's/^#![[:space:]]*//') ;; esac case "$_SHEBANG" in */env\ *) GRAPHIFY_PYTHON="${_SHEBANG#*/env }" ;; *) GRAPHIFY_PYTHON="$_SHEBANG" ;; esac # Allowlist: only keep characters valid in a filesystem path to prevent # injection if the shebang contains shell metacharacters. case "$GRAPHIFY_PYTHON" in *[!a-zA-Z0-9/_.@-]*) GRAPHIFY_PYTHON="" ;; esac if [ -n "$GRAPHIFY_PYTHON" ] && ! "$GRAPHIFY_PYTHON" -c "import graphify" 2>/dev/null; then GRAPHIFY_PYTHON="" fi fi fi # Last resort: try python3 / python (works for system/venv installs on PATH). if [ -z "$GRAPHIFY_PYTHON" ]; then if command -v python3 >/dev/null 2>&1 && python3 -c "import graphify" 2>/dev/null; then GRAPHIFY_PYTHON="python3" elif command -v python >/dev/null 2>&1 && python -c "import graphify" 2>/dev/null; then GRAPHIFY_PYTHON="python" else echo "[graphify hook] could not locate a Python with graphify installed. Add the graphify bin dir to PATH or re-run 'graphify hook install' from the env where graphify lives." >&2 exit 0 fi fi _GRAPHIFY_LOG="${HOME}/.cache/graphify-rebuild.log" mkdir -p "$(dirname "$_GRAPHIFY_LOG")" export GRAPHIFY_REBUILD_LOG="$_GRAPHIFY_LOG" echo "[graphify] Branch switched - launching background rebuild (log: $_GRAPHIFY_LOG)" "$GRAPHIFY_PYTHON" -c "import os, subprocess, sys _src = ''' from graphify.watch import _rebuild_code, _apply_resource_limits from pathlib import Path import os, signal, sys try: _apply_resource_limits() _timeout = int(os.environ.get('GRAPHIFY_REBUILD_TIMEOUT', '600')) if _timeout > 0 and hasattr(signal, 'SIGALRM'): signal.signal(signal.SIGALRM, lambda *_: (_ for _ in ()).throw(TimeoutError(f'graphify rebuild exceeded {_timeout}s'))) signal.alarm(_timeout) _force = os.environ.get('GRAPHIFY_FORCE', '').lower() in ('1', 'true', 'yes') # post-checkout: branch switch can touch arbitrary files; full rebuild path # (no changed_paths) is correct here. The flock inside _rebuild_code still # prevents pile-ups when commit + checkout fire back-to-back. _root = Path('.') _saved = Path('graphify-out/.graphify_root') if _saved.exists(): _txt = _saved.read_text(encoding='utf-8').strip() if _txt: _root = Path(_txt) _rebuild_code(_root, force=_force) except TimeoutError as exc: print(f'[graphify] {exc}') sys.exit(1) except Exception as exc: print(f'[graphify] Rebuild failed: {exc}') sys.exit(1) ''' _log = os.environ.get('GRAPHIFY_REBUILD_LOG') or os.path.join(os.path.expanduser('~'), '.cache', 'graphify-rebuild.log') try: os.makedirs(os.path.dirname(_log), exist_ok=True) _out = open(_log, 'a', buffering=1, encoding='utf-8', errors='replace') except OSError: _out = subprocess.DEVNULL _kw = dict(stdout=_out, stderr=subprocess.STDOUT, stdin=subprocess.DEVNULL, cwd=os.getcwd(), close_fds=True) _cmd = [sys.executable, '-c', _src] if os.name == 'nt': _flags = 0x00000008 | 0x00000200 # DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP try: subprocess.Popen(_cmd, creationflags=_flags | 0x01000000, **_kw) # + CREATE_BREAKAWAY_FROM_JOB except OSError: subprocess.Popen(_cmd, creationflags=_flags, **_kw) else: subprocess.Popen(_cmd, start_new_session=True, **_kw) " # graphify-checkout-hook-end