doctor.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  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. # Expected: CLAUDE.md, settings.json, agents, skills, templates, hooks/session-start.sh
  27. _EXPECTED_LINKS=7
  28. _LINK_PASS=0
  29. check_symlink() {
  30. local name="$1"
  31. local target="$HOME/.claude/$name"
  32. if [ ! -e "$target" ] && [ ! -L "$target" ]; then
  33. fail "~/.claude/$name — MISSING"
  34. return
  35. fi
  36. if [ -L "$target" ]; then
  37. # readlink -f is not available on macOS BSD — use -f with fallback
  38. local real
  39. real=$(readlink -f "$target" 2>/dev/null) || real=$(readlink "$target")
  40. if [ ! -e "$real" ]; then
  41. fail "~/.claude/$name → $real — BROKEN SYMLINK"
  42. else
  43. pass "~/.claude/$name"; _LINK_PASS=$((_LINK_PASS + 1))
  44. fi
  45. else
  46. warn "~/.claude/$name exists but is NOT a symlink (expected symlink to repo)"
  47. fi
  48. }
  49. check_symlink "CLAUDE.md"
  50. check_symlink "settings.json"
  51. check_symlink "agents"
  52. check_symlink "skills"
  53. check_symlink "templates"
  54. check_symlink "lib"
  55. check_symlink "hooks/session-start.sh"
  56. info "Symlinks: ${_LINK_PASS}/${_EXPECTED_LINKS} OK"
  57. unset _EXPECTED_LINKS _LINK_PASS
  58. echo ""
  59. # ────────────────────────────────────────────────────────────
  60. # 2. GStack submodule
  61. # ────────────────────────────────────────────────────────────
  62. echo "── GStack submodule ──"
  63. GSTACK_DIR="$REPO/skills-external/gstack"
  64. if [ -f "$GSTACK_DIR/.git" ] || [ -d "$GSTACK_DIR/.git" ]; then
  65. pass "Submodule initialized at skills-external/gstack"
  66. warn "GStack tracks branch = main (no commit hash pin). Review upstream before updating."
  67. elif [ -d "$GSTACK_DIR" ]; then
  68. warn "skills-external/gstack exists but submodule not initialized — run: git submodule update --init"
  69. else
  70. warn "GStack submodule missing — run: git submodule update --init"
  71. fi
  72. if [ -L "$HOME/.claude/skills/gstack" ]; then
  73. real=$(readlink -f "$HOME/.claude/skills/gstack" 2>/dev/null || readlink "$HOME/.claude/skills/gstack")
  74. if [ -d "$real" ]; then
  75. pass "Symlink OK → $real"
  76. # Check for skills/ subdirectory (referenced by plugin-advisor PHASE 1)
  77. gstack_skills_count=$(ls "$HOME/.claude/skills/gstack/skills/" 2>/dev/null | wc -l | tr -d ' ')
  78. if [ "${gstack_skills_count:-0}" -gt 0 ]; then
  79. pass "GStack: ${gstack_skills_count} skills available"
  80. else
  81. warn "GStack symlink OK but no skills/ subdirectory found — may need: cd skills-external/gstack && ./setup"
  82. fi
  83. else
  84. fail "Symlink broken → $real"
  85. fi
  86. else
  87. warn "GStack not symlinked — run: bash link.sh"
  88. fi
  89. echo ""
  90. # ────────────────────────────────────────────────────────────
  91. # 3. Prerequisites
  92. # ────────────────────────────────────────────────────────────
  93. echo "── Prerequisites ──"
  94. if command -v git &>/dev/null; then
  95. pass "git $(git --version | awk '{print $3}')"
  96. else
  97. fail "git not found"
  98. fi
  99. if command -v node &>/dev/null; then
  100. NODE_VER=$(node --version | sed 's/v//' | cut -d. -f1)
  101. if [ "$NODE_VER" -ge 18 ]; then
  102. pass "Node.js $(node --version)"
  103. else
  104. warn "Node.js $(node --version) — need >=18"
  105. fi
  106. else
  107. fail "Node.js not found"
  108. fi
  109. if command -v cargo &>/dev/null; then
  110. pass "Cargo $(cargo --version | awk '{print $2}')"
  111. else
  112. warn "Cargo not found (RTK unavailable)"
  113. fi
  114. if command -v python3 &>/dev/null; then
  115. pass "Python $(python3 --version | awk '{print $2}')"
  116. else
  117. warn "Python3 not found"
  118. fi
  119. if command -v claude &>/dev/null; then
  120. pass "Claude Code $(claude --version 2>/dev/null | head -1 || echo 'installed')"
  121. else
  122. fail "Claude Code not found — install from https://code.claude.com"
  123. fi
  124. echo ""
  125. # ────────────────────────────────────────────────────────────
  126. # 4. Key plugins
  127. # ────────────────────────────────────────────────────────────
  128. echo "── Plugins ──"
  129. if detect_rtk; then
  130. pass "RTK installed"
  131. else
  132. warn "RTK not installed — run install-plugins.sh"
  133. fi
  134. if detect_superpowers; then
  135. pass "Superpowers plugin detected"
  136. else
  137. fail "Superpowers not detected — orchestrators (/init-project, /ship-feature) will fail"
  138. fi
  139. if detect_context7; then
  140. pass "Context7 CLI (ctx7) installed"
  141. else
  142. info "Context7 CLI not installed (optional — needed for fast-evolving libs: npm install -g ctx7)"
  143. fi
  144. if detect_gsd; then
  145. pass "GSD v2 installed ($(gsd --version 2>/dev/null | head -1 || echo 'gsd'))"
  146. else
  147. info "GSD v2 not installed (optional — run: npm install -g gsd-pi)"
  148. fi
  149. if detect_ruflo; then
  150. pass "Ruflo CLI installed ($(ruflo --version 2>/dev/null | head -1 || echo 'installed'))"
  151. else
  152. info "Ruflo CLI not installed (optional — enterprise multi-agent: npm install -g ruflo@latest --omit=optional)"
  153. fi
  154. if detect_graphifyy; then
  155. pass "Graphifyy installed (graphify CLI)"
  156. else
  157. info "Graphifyy not installed (optional — codebase knowledge graph: pipx install graphifyy)"
  158. fi
  159. echo ""
  160. # ────────────────────────────────────────────────────────────
  161. # 5. Permissions check
  162. # ────────────────────────────────────────────────────────────
  163. echo "── Permissions ──"
  164. SETTINGS="$HOME/.claude/settings.json"
  165. if [ -f "$SETTINGS" ] || [ -L "$SETTINGS" ]; then
  166. if grep -q '"disableBypassPermissionsMode"' "$SETTINGS" 2>/dev/null; then
  167. pass "Bypass mode disabled"
  168. else
  169. warn "disableBypassPermissionsMode not found in settings"
  170. fi
  171. DENY_COUNT=$(python3 -c "
  172. import json
  173. with open('$SETTINGS') as f:
  174. d = json.load(f)
  175. print(len(d.get('permissions',{}).get('deny',[])))
  176. " 2>/dev/null || echo "?")
  177. if [ "$DENY_COUNT" = "?" ]; then
  178. warn "Could not parse deny count (python3 unavailable or JSON parse error)"
  179. else
  180. EXPECTED_DENY=100
  181. if [ "$DENY_COUNT" -eq "$EXPECTED_DENY" ] 2>/dev/null; then
  182. pass "Deny rules: $DENY_COUNT"
  183. else
  184. warn "Deny rules: $DENY_COUNT (expected $EXPECTED_DENY) — settings may have been manually modified"
  185. fi
  186. fi
  187. else
  188. fail "~/.claude/settings.json not found"
  189. fi
  190. echo ""
  191. # ────────────────────────────────────────────────────────────
  192. # 6. Token budget estimate
  193. # ────────────────────────────────────────────────────────────
  194. echo "── Token budget estimate ──"
  195. # Reference: Claude Code Pro plan ~11k tokens/5h session (session budget, not context window).
  196. # Seuils: WARNING >15%, CRITICAL >30% of session budget.
  197. CLAUDE_MD_CHARS=$(wc -c < "$REPO/CLAUDE.md" 2>/dev/null || echo 0)
  198. CLAUDE_MD_TOKENS=$((CLAUDE_MD_CHARS / 4))
  199. # Skill descriptions only (frontmatter description field — loaded passively at startup)
  200. SKILL_DESC_CHARS=0
  201. for f in "$HOME/.claude/skills/"*/SKILL.md; do
  202. [ -f "$f" ] || continue
  203. desc=$(grep "^description:" "$f" 2>/dev/null | head -1 | sed 's/^description: *//' )
  204. SKILL_DESC_CHARS=$((SKILL_DESC_CHARS + ${#desc}))
  205. done
  206. SKILL_DESC_TOKENS=$((SKILL_DESC_CHARS / 4))
  207. SKILL_COUNT=$(find "$HOME/.claude/skills/" -maxdepth 2 -name "SKILL.md" 2>/dev/null | wc -l | tr -d ' ')
  208. # Plugin passive cost estimates (tokens)
  209. PLUGIN_TOKENS=0
  210. if detect_superpowers 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 800)); fi
  211. if detect_gstack 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 2750)); fi
  212. if detect_frontend_design 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 200)); fi
  213. if detect_uiux_pro_max 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 400)); fi
  214. if detect_context7 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 200)); fi
  215. if detect_ruflo 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 1000)); fi
  216. if detect_graphifyy 2>/dev/null; then PLUGIN_TOKENS=$((PLUGIN_TOKENS + 300)); fi
  217. TOTAL_TOKENS=$((CLAUDE_MD_TOKENS + SKILL_DESC_TOKENS + PLUGIN_TOKENS))
  218. SESSION_BUDGET=11000
  219. PCT=$((TOTAL_TOKENS * 100 / SESSION_BUDGET))
  220. echo ""
  221. echo " CLAUDE.md: ~${CLAUDE_MD_TOKENS}t"
  222. echo " Skill descriptions: ~${SKILL_DESC_TOKENS}t (${SKILL_COUNT} skills)"
  223. echo " Plugin passive cost: ~${PLUGIN_TOKENS}t (active plugins)"
  224. echo " ─────────────────────────────────────────"
  225. info " Total: ~${TOTAL_TOKENS}t"
  226. info " Session budget (Pro): ${SESSION_BUDGET}t"
  227. info " Usage: ~${PCT}%"
  228. echo ""
  229. if [ "$PCT" -gt 30 ]; then
  230. warn "CRITICAL: ${PCT}% of session budget — /plugin-check to disable unused plugins"
  231. elif [ "$PCT" -gt 15 ]; then
  232. warn "WARNING: ${PCT}% of session budget — consider disabling unused toggle plugins"
  233. else
  234. pass "Budget: ${PCT}% (comfortable)"
  235. fi
  236. # Per-file breakdown (skill bodies — loaded on demand, shown for awareness)
  237. if [ "$TOTAL_TOKENS" -gt 2000 ]; then
  238. info "Skill/agent bodies (loaded on demand, >200t each):"
  239. for f in "$HOME/.claude/skills/"*/SKILL.md "$HOME/.claude/agents/"*.md; do
  240. [ -f "$f" ] || continue
  241. size=$(wc -c < "$f" 2>/dev/null || echo 0)
  242. tokens=$((size / 4))
  243. if [ "$tokens" -gt 200 ]; then
  244. label=$(basename "$(dirname "$f")" 2>/dev/null)
  245. [ "$label" = "." ] && label=$(basename "$f")
  246. info " ~${tokens}t ${label}"
  247. fi
  248. done
  249. fi
  250. echo ""
  251. # ────────────────────────────────────────────────────────────
  252. # 7. File consistency
  253. # ────────────────────────────────────────────────────────────
  254. echo "── Consistency ──"
  255. # Check all skills have disable-model-invocation
  256. MISSING_DMI=()
  257. for f in "$HOME/.claude/skills/"*/SKILL.md; do
  258. [ -f "$f" ] || continue
  259. name=$(basename "$(dirname "$f")")
  260. if ! grep -q "disable-model-invocation" "$f" 2>/dev/null; then
  261. MISSING_DMI+=("$name")
  262. fi
  263. done
  264. if [ ${#MISSING_DMI[@]} -eq 0 ]; then
  265. pass "All skills have disable-model-invocation"
  266. else
  267. warn "Skills missing disable-model-invocation: ${MISSING_DMI[*]}"
  268. fi
  269. # Check expected skills are present
  270. EXPECTED_SKILLS=(
  271. "analyze" "health" "init-project" "onboard" "plugin-check"
  272. "readme" "refactor" "ship-feature" "status"
  273. )
  274. MISSING_SKILLS=()
  275. for skill in "${EXPECTED_SKILLS[@]}"; do
  276. if [ ! -f "$HOME/.claude/skills/${skill}/SKILL.md" ]; then
  277. MISSING_SKILLS+=("${skill}/")
  278. fi
  279. done
  280. if [ ${#MISSING_SKILLS[@]} -eq 0 ]; then
  281. pass "All ${#EXPECTED_SKILLS[@]} expected skills present (analyze, health, init-project, onboard, plugin-check, readme, refactor, ship-feature, status)"
  282. else
  283. warn "Missing skills: ${MISSING_SKILLS[*]} — run: bash link.sh"
  284. fi
  285. # Check expected agents are present
  286. EXPECTED_AGENTS=(
  287. "analyzer" "interviewer" "plugin-advisor" "readme-updater"
  288. "refactorer" "scaffolder" "onboarder" "status-reporter"
  289. )
  290. MISSING_AGENTS=()
  291. for agent in "${EXPECTED_AGENTS[@]}"; do
  292. if [ ! -f "$HOME/.claude/agents/${agent}.md" ]; then
  293. MISSING_AGENTS+=("${agent}.md")
  294. fi
  295. done
  296. if [ ${#MISSING_AGENTS[@]} -eq 0 ]; then
  297. pass "All 8 agents present (analyzer, interviewer, plugin-advisor, readme-updater, refactorer, scaffolder, onboarder, status-reporter)"
  298. else
  299. warn "Missing agents: ${MISSING_AGENTS[*]} — run: bash link.sh"
  300. fi
  301. # Check CRLF — portable: grep -P not available on macOS BSD grep
  302. CRLF_FILES=()
  303. for f in "$REPO"/*.md "$REPO"/agents/*.md "$REPO"/skills/*/SKILL.md; do
  304. [ -f "$f" ] || continue
  305. if grep -c $'\r' "$f" 2>/dev/null | grep -q "^[^0]"; then
  306. CRLF_FILES+=("$(basename "$f")")
  307. fi
  308. done
  309. if [ ${#CRLF_FILES[@]} -eq 0 ]; then
  310. pass "No CRLF line endings detected"
  311. else
  312. warn "CRLF detected in: ${CRLF_FILES[*]}"
  313. fi
  314. echo ""
  315. # ────────────────────────────────────────────────────────────
  316. # Summary
  317. # ────────────────────────────────────────────────────────────
  318. echo "═══════════════════════════════════════════"
  319. if [ "$ERRORS" -gt 0 ]; then
  320. echo -e "${RED} $ERRORS error(s)${NC}, ${YELLOW}$WARNS warning(s)${NC}"
  321. echo ""
  322. echo " Fix: cd $REPO && bash link.sh && bash install-plugins.sh"
  323. exit 1
  324. elif [ "$WARNS" -gt 0 ]; then
  325. echo -e " ${GREEN}No errors${NC}, ${YELLOW}$WARNS warning(s)${NC}"
  326. else
  327. echo -e " ${GREEN}All checks passed ✓${NC}"
  328. fi
  329. echo ""