doctor.sh 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. #!/usr/bin/env bash
  2. # ============================================================
  3. # Claude Code — Config doctor
  4. # Diagnoses symlinks, prerequisites, plugins, permissions,
  5. # and token budget. Run after install or when something breaks.
  6. # ============================================================
  7. set -euo pipefail
  8. RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
  9. ERRORS=0; WARNS=0
  10. pass() { echo -e " ${GREEN}✓${NC} $1"; }
  11. fail() { echo -e " ${RED}✗${NC} $1"; ERRORS=$((ERRORS + 1)); }
  12. warn() { echo -e " ${YELLOW}⚠${NC} $1"; WARNS=$((WARNS + 1)); }
  13. info() { echo -e " ${BLUE}→${NC} $1"; }
  14. REPO="$(cd "$(dirname "$0")" && pwd)"
  15. VERSION=$(cat "$REPO/version.txt" 2>/dev/null || echo "unknown")
  16. # Load shared detection library
  17. # shellcheck source=lib/detect-plugins.sh
  18. source "$REPO/lib/detect-plugins.sh"
  19. echo ""
  20. echo "═══ claude-config doctor (v${VERSION}) ═══"
  21. echo ""
  22. # ────────────────────────────────────────────────────────────
  23. # 1. Core symlinks
  24. # ────────────────────────────────────────────────────────────
  25. echo "── Symlinks ──"
  26. check_symlink() {
  27. local name="$1"
  28. local target="$HOME/.claude/$name"
  29. if [ ! -e "$target" ] && [ ! -L "$target" ]; then
  30. fail "~/.claude/$name — MISSING"
  31. return
  32. fi
  33. if [ -L "$target" ]; then
  34. local real
  35. real=$(readlink -f "$target" 2>/dev/null || readlink "$target")
  36. if [ ! -e "$real" ]; then
  37. fail "~/.claude/$name → $real — BROKEN SYMLINK"
  38. else
  39. pass "~/.claude/$name"
  40. fi
  41. else
  42. warn "~/.claude/$name exists but is NOT a symlink (expected symlink to repo)"
  43. fi
  44. }
  45. check_symlink "CLAUDE.md"
  46. check_symlink "settings.json"
  47. check_symlink "agents"
  48. check_symlink "skills"
  49. check_symlink "hooks/session-start.sh"
  50. echo ""
  51. # ────────────────────────────────────────────────────────────
  52. # 2. GStack submodule
  53. # ────────────────────────────────────────────────────────────
  54. echo "── GStack submodule ──"
  55. if [ -d "$REPO/skills-external/gstack" ] || [ -f "$REPO/skills-external/gstack/.git" ]; then
  56. pass "Submodule present at skills-external/gstack"
  57. else
  58. warn "Submodule not initialized — run: git submodule update --init"
  59. fi
  60. if [ -L "$HOME/.claude/skills/gstack" ]; then
  61. real=$(readlink -f "$HOME/.claude/skills/gstack" 2>/dev/null || readlink "$HOME/.claude/skills/gstack")
  62. if [ -d "$real" ]; then
  63. pass "Symlink OK → $real"
  64. else
  65. fail "Symlink broken → $real"
  66. fi
  67. else
  68. warn "GStack not symlinked — run: bash link.sh"
  69. fi
  70. echo ""
  71. # ────────────────────────────────────────────────────────────
  72. # 3. Prerequisites
  73. # ────────────────────────────────────────────────────────────
  74. echo "── Prerequisites ──"
  75. if command -v git &>/dev/null; then
  76. pass "git $(git --version | awk '{print $3}')"
  77. else
  78. fail "git not found"
  79. fi
  80. if command -v node &>/dev/null; then
  81. NODE_VER=$(node --version | sed 's/v//' | cut -d. -f1)
  82. if [ "$NODE_VER" -ge 18 ]; then
  83. pass "Node.js $(node --version)"
  84. else
  85. warn "Node.js $(node --version) — need >=18"
  86. fi
  87. else
  88. fail "Node.js not found"
  89. fi
  90. if command -v cargo &>/dev/null; then
  91. pass "Cargo $(cargo --version | awk '{print $2}')"
  92. else
  93. warn "Cargo not found (RTK unavailable)"
  94. fi
  95. if command -v python3 &>/dev/null; then
  96. pass "Python $(python3 --version | awk '{print $2}')"
  97. else
  98. warn "Python3 not found"
  99. fi
  100. if command -v claude &>/dev/null; then
  101. pass "Claude Code $(claude --version 2>/dev/null | head -1 || echo 'installed')"
  102. else
  103. fail "Claude Code not found — install from https://code.claude.com"
  104. fi
  105. echo ""
  106. # ────────────────────────────────────────────────────────────
  107. # 4. Key plugins
  108. # ────────────────────────────────────────────────────────────
  109. echo "── Plugins ──"
  110. if detect_rtk; then
  111. pass "RTK installed"
  112. else
  113. warn "RTK not installed — run install-plugins.sh"
  114. fi
  115. if detect_superpowers; then
  116. pass "Superpowers plugin detected"
  117. else
  118. fail "Superpowers not detected — orchestrators (/init-project, /ship-feature) will fail"
  119. fi
  120. if detect_context7; then
  121. pass "Context7 MCP configured"
  122. else
  123. info "Context7 MCP not configured (optional — needed for fast-evolving libs)"
  124. fi
  125. echo ""
  126. # ────────────────────────────────────────────────────────────
  127. # 5. Permissions check
  128. # ────────────────────────────────────────────────────────────
  129. echo "── Permissions ──"
  130. SETTINGS="$HOME/.claude/settings.json"
  131. if [ -f "$SETTINGS" ] || [ -L "$SETTINGS" ]; then
  132. if grep -q '"disableBypassPermissionsMode"' "$SETTINGS" 2>/dev/null; then
  133. pass "Bypass mode disabled"
  134. else
  135. warn "disableBypassPermissionsMode not found in settings"
  136. fi
  137. DENY_COUNT=$(python3 -c "
  138. import json
  139. with open('$SETTINGS') as f:
  140. d = json.load(f)
  141. print(len(d.get('permissions',{}).get('deny',[])))
  142. " 2>/dev/null || echo "?")
  143. pass "Deny rules: $DENY_COUNT"
  144. else
  145. fail "~/.claude/settings.json not found"
  146. fi
  147. echo ""
  148. # ────────────────────────────────────────────────────────────
  149. # 6. Token budget estimate
  150. # ────────────────────────────────────────────────────────────
  151. echo "── Token budget estimate ──"
  152. TOTAL_CHARS=0
  153. # Skill descriptions
  154. for f in "$HOME/.claude/skills/"*/SKILL.md; do
  155. [ -f "$f" ] || continue
  156. desc=$(sed -n 's/^description: //p' "$f" 2>/dev/null || true)
  157. TOTAL_CHARS=$((TOTAL_CHARS + ${#desc}))
  158. done
  159. # Agent descriptions
  160. for f in "$HOME/.claude/agents/"*.md; do
  161. [ -f "$f" ] || continue
  162. desc=$(sed -n '/^---$/,/^---$/{ s/^description: //p }' "$f" 2>/dev/null || true)
  163. TOTAL_CHARS=$((TOTAL_CHARS + ${#desc}))
  164. done
  165. if [ "$TOTAL_CHARS" -gt 6000 ]; then
  166. warn "Custom descriptions: ~${TOTAL_CHARS} chars (budget ~8000) — risk of truncation"
  167. elif [ "$TOTAL_CHARS" -gt 4000 ]; then
  168. info "Custom descriptions: ~${TOTAL_CHARS} chars (within budget, moderate margin)"
  169. else
  170. pass "Custom descriptions: ~${TOTAL_CHARS} chars (comfortable)"
  171. fi
  172. echo ""
  173. # ────────────────────────────────────────────────────────────
  174. # 7. File consistency
  175. # ────────────────────────────────────────────────────────────
  176. echo "── Consistency ──"
  177. # Check all skills have disable-model-invocation
  178. MISSING_DMI=()
  179. for f in "$HOME/.claude/skills/"*/SKILL.md; do
  180. [ -f "$f" ] || continue
  181. name=$(basename "$(dirname "$f")")
  182. if ! grep -q "disable-model-invocation" "$f" 2>/dev/null; then
  183. MISSING_DMI+=("$name")
  184. fi
  185. done
  186. if [ ${#MISSING_DMI[@]} -eq 0 ]; then
  187. pass "All skills have disable-model-invocation"
  188. else
  189. warn "Skills missing disable-model-invocation: ${MISSING_DMI[*]}"
  190. fi
  191. # Check CRLF
  192. CRLF_FILES=()
  193. for f in "$REPO"/*.md "$REPO"/agents/*.md "$REPO"/skills/*/SKILL.md; do
  194. [ -f "$f" ] || continue
  195. if grep -qP '\r' "$f" 2>/dev/null; then
  196. CRLF_FILES+=("$(basename "$f")")
  197. fi
  198. done
  199. if [ ${#CRLF_FILES[@]} -eq 0 ]; then
  200. pass "No CRLF line endings detected"
  201. else
  202. warn "CRLF detected in: ${CRLF_FILES[*]}"
  203. fi
  204. echo ""
  205. # ────────────────────────────────────────────────────────────
  206. # Summary
  207. # ────────────────────────────────────────────────────────────
  208. echo "═══════════════════════════════════════════"
  209. if [ "$ERRORS" -gt 0 ]; then
  210. echo -e "${RED} $ERRORS error(s)${NC}, ${YELLOW}$WARNS warning(s)${NC}"
  211. echo ""
  212. echo " Fix: cd $REPO && bash link.sh && bash install-plugins.sh"
  213. exit 1
  214. elif [ "$WARNS" -gt 0 ]; then
  215. echo -e " ${GREEN}No errors${NC}, ${YELLOW}$WARNS warning(s)${NC}"
  216. else
  217. echo -e " ${GREEN}All checks passed ✓${NC}"
  218. fi
  219. echo ""