|
| 1 | +#!/usr/bin/env bash |
| 2 | +# ContextAtlas post-checkout hook — 后台自动检测并修复当前项目索引健康 |
| 3 | +# |
| 4 | +# 触发条件:git checkout / git switch(分支切换) |
| 5 | +# 行为:后台异步执行,不阻塞 git 操作 |
| 6 | +# 1. 快速健康检查 (--quick, ~300ms) |
| 7 | +# 2. 若状态异常,自动修复安全项 |
| 8 | +# 3. 幽灵清理每 24 小时最多执行一次 |
| 9 | +# 4. 输出写入 ~/.cache/contextatlas/hooks.log |
| 10 | + |
| 11 | +# 快速路径:只对分支切换触发($3=1 表示分支切换) |
| 12 | +if [ "${3:-0}" != "1" ]; then |
| 13 | + exit 0 |
| 14 | +fi |
| 15 | + |
| 16 | +# 快速路径:检查是否有 contextatlas CLI |
| 17 | +if ! command -v contextatlas &>/dev/null; then |
| 18 | + exit 0 |
| 19 | +fi |
| 20 | + |
| 21 | +# 防抖:如果最近 60 秒内已触发过,跳过 |
| 22 | +LOCK_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/contextatlas" |
| 23 | +mkdir -p "$LOCK_DIR" |
| 24 | +LOCK_FILE="$LOCK_DIR/.hook-debounce" |
| 25 | +if [ -f "$LOCK_FILE" ]; then |
| 26 | + LAST_RUN=$(stat -c %Y "$LOCK_FILE" 2>/dev/null || echo 0) |
| 27 | + NOW=$(date +%s) |
| 28 | + ELAPSED=$((NOW - LAST_RUN)) |
| 29 | + if [ "$ELAPSED" -lt 60 ]; then |
| 30 | + exit 0 |
| 31 | + fi |
| 32 | +fi |
| 33 | +touch "$LOCK_FILE" |
| 34 | + |
| 35 | +# 后台异步执行,不阻塞 git |
| 36 | +nohup bash -c ' |
| 37 | + LOG_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/contextatlas" |
| 38 | + LOG_FILE="$LOG_DIR/hooks.log" |
| 39 | + CLEANUP_LOCK="$LOG_DIR/.cleanup-daily" |
| 40 | +
|
| 41 | + echo "[$(date -Iseconds)] post-checkout triggered (quick mode)" >> "$LOG_FILE" |
| 42 | +
|
| 43 | + HEALTH=$(contextatlas health:check --quick --json 2>>"$LOG_FILE") || { |
| 44 | + echo "[$(date -Iseconds)] health:check failed, skipping" >> "$LOG_FILE" |
| 45 | + exit 0 |
| 46 | + } |
| 47 | +
|
| 48 | + STATUS=$(echo "$HEALTH" | node -e " |
| 49 | + try { |
| 50 | + const d = JSON.parse(require('"'"'fs'"'"').readFileSync('"'"'/dev/stdin'"'"','"'"'utf8'"'"')); |
| 51 | + process.stdout.write(d.overall?.status || '"'"'unknown'"'"'); |
| 52 | + } catch { process.stdout.write('"'"'error'"'"'); } |
| 53 | + " 2>/dev/null) || STATUS="error" |
| 54 | +
|
| 55 | + if [ "$STATUS" = "healthy" ]; then |
| 56 | + echo "[$(date -Iseconds)] health: healthy, skipping" >> "$LOG_FILE" |
| 57 | + exit 0 |
| 58 | + fi |
| 59 | +
|
| 60 | + echo "[$(date -Iseconds)] health: $STATUS, auto-maintaining..." >> "$LOG_FILE" |
| 61 | +
|
| 62 | + # 1. 幽灵清理(每 24 小时最多一次) |
| 63 | + NOW=$(date +%s) |
| 64 | + RUN_CLEANUP=false |
| 65 | + if [ -f "$CLEANUP_LOCK" ]; then |
| 66 | + LAST_CLEANUP=$(stat -c %Y "$CLEANUP_LOCK" 2>/dev/null || echo 0) |
| 67 | + ELAPSED=$((NOW - LAST_CLEANUP)) |
| 68 | + if [ "$ELAPSED" -gt 86400 ]; then |
| 69 | + RUN_CLEANUP=true |
| 70 | + fi |
| 71 | + else |
| 72 | + RUN_CLEANUP=true |
| 73 | + fi |
| 74 | +
|
| 75 | + if [ "$RUN_CLEANUP" = true ]; then |
| 76 | + touch "$CLEANUP_LOCK" |
| 77 | + # 先检查有没有,再清理 |
| 78 | + STALE_COUNT=$(contextatlas hub:cleanup-stale-indexes --dry-run --json 2>/dev/null | node -e " |
| 79 | + try { |
| 80 | + const d = JSON.parse(require('"'"'fs'"'"').readFileSync('"'"'/dev/stdin'"'"','"'"'utf8'"'"')); |
| 81 | + process.stdout.write(String(d.staleCount || 0)); |
| 82 | + } catch { process.stdout.write('"'"'0'"'"'); } |
| 83 | + " 2>/dev/null || echo "0") |
| 84 | +
|
| 85 | + echo "[$(date -Iseconds)] stale check: $STALE_COUNT stale directories" >> "$LOG_FILE" |
| 86 | +
|
| 87 | + if [ "$STALE_COUNT" -gt 0 ]; then |
| 88 | + contextatlas hub:cleanup-ghost --mode tmp --json &>/dev/null |
| 89 | + contextatlas hub:cleanup-stale-indexes --json &>/dev/null |
| 90 | + echo "[$(date -Iseconds)] ghost cleanup done ($STALE_COUNT removed)" >> "$LOG_FILE" |
| 91 | + else |
| 92 | + contextatlas hub:cleanup-ghost --mode tmp --json &>/dev/null |
| 93 | + echo "[$(date -Iseconds)] ghost cleanup done (hub only, no stale dirs)" >> "$LOG_FILE" |
| 94 | + fi |
| 95 | + else |
| 96 | + echo "[$(date -Iseconds)] cleanup skipped (daily throttle)" >> "$LOG_FILE" |
| 97 | + fi |
| 98 | +
|
| 99 | + # 2. 重建当前项目 catalog |
| 100 | + contextatlas memory:rebuild-catalog &>/dev/null |
| 101 | + echo "[$(date -Iseconds)] catalog rebuild done" >> "$LOG_FILE" |
| 102 | +
|
| 103 | + # 3. 记录告警(最多 5 条) |
| 104 | + echo "$HEALTH" | node -e " |
| 105 | + try { |
| 106 | + const d = JSON.parse(require('"'"'fs'"'"').readFileSync('"'"'/dev/stdin'"'"','"'"'utf8'"'"')); |
| 107 | + const alerts = d.overall?.issues || []; |
| 108 | + alerts.slice(0, 5).forEach(a => console.log('"'"' alert: '"'"' + a)); |
| 109 | + } catch {} |
| 110 | + " 2>/dev/null >> "$LOG_FILE" || true |
| 111 | +
|
| 112 | + echo "[$(date -Iseconds)] maintenance complete" >> "$LOG_FILE" |
| 113 | +' &>/dev/null & |
| 114 | + |
| 115 | +exit 0 |
0 commit comments