| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261 |
- #!/usr/bin/env bash
- # ============================================================
- # Claude Code — Config doctor
- # Diagnoses symlinks, prerequisites, plugins, permissions,
- # and token budget. Run after install or when something breaks.
- # ============================================================
- set -euo pipefail
- RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
- ERRORS=0; WARNS=0
- pass() { echo -e " ${GREEN}✓${NC} $1"; }
- fail() { echo -e " ${RED}✗${NC} $1"; ERRORS=$((ERRORS + 1)); }
- warn() { echo -e " ${YELLOW}⚠${NC} $1"; WARNS=$((WARNS + 1)); }
- info() { echo -e " ${BLUE}→${NC} $1"; }
- REPO="$(cd "$(dirname "$0")" && pwd)"
- VERSION=$(cat "$REPO/version.txt" 2>/dev/null || echo "unknown")
- # Load shared detection library
- # shellcheck source=lib/detect-plugins.sh
- source "$REPO/lib/detect-plugins.sh"
- echo ""
- echo "═══ claude-config doctor (v${VERSION}) ═══"
- echo ""
- # ────────────────────────────────────────────────────────────
- # 1. Core symlinks
- # ────────────────────────────────────────────────────────────
- echo "── Symlinks ──"
- check_symlink() {
- local name="$1"
- local target="$HOME/.claude/$name"
- if [ ! -e "$target" ] && [ ! -L "$target" ]; then
- fail "~/.claude/$name — MISSING"
- return
- fi
- if [ -L "$target" ]; then
- local real
- real=$(readlink -f "$target" 2>/dev/null || readlink "$target")
- if [ ! -e "$real" ]; then
- fail "~/.claude/$name → $real — BROKEN SYMLINK"
- else
- pass "~/.claude/$name"
- fi
- else
- warn "~/.claude/$name exists but is NOT a symlink (expected symlink to repo)"
- fi
- }
- check_symlink "CLAUDE.md"
- check_symlink "settings.json"
- check_symlink "agents"
- check_symlink "skills"
- check_symlink "hooks/session-start.sh"
- echo ""
- # ────────────────────────────────────────────────────────────
- # 2. GStack submodule
- # ────────────────────────────────────────────────────────────
- echo "── GStack submodule ──"
- if [ -d "$REPO/skills-external/gstack" ] || [ -f "$REPO/skills-external/gstack/.git" ]; then
- pass "Submodule present at skills-external/gstack"
- else
- warn "Submodule not initialized — run: git submodule update --init"
- fi
- if [ -L "$HOME/.claude/skills/gstack" ]; then
- real=$(readlink -f "$HOME/.claude/skills/gstack" 2>/dev/null || readlink "$HOME/.claude/skills/gstack")
- if [ -d "$real" ]; then
- pass "Symlink OK → $real"
- else
- fail "Symlink broken → $real"
- fi
- else
- warn "GStack not symlinked — run: bash link.sh"
- fi
- echo ""
- # ────────────────────────────────────────────────────────────
- # 3. Prerequisites
- # ────────────────────────────────────────────────────────────
- echo "── Prerequisites ──"
- if command -v git &>/dev/null; then
- pass "git $(git --version | awk '{print $3}')"
- else
- fail "git not found"
- fi
- if command -v node &>/dev/null; then
- NODE_VER=$(node --version | sed 's/v//' | cut -d. -f1)
- if [ "$NODE_VER" -ge 18 ]; then
- pass "Node.js $(node --version)"
- else
- warn "Node.js $(node --version) — need >=18"
- fi
- else
- fail "Node.js not found"
- fi
- if command -v cargo &>/dev/null; then
- pass "Cargo $(cargo --version | awk '{print $2}')"
- else
- warn "Cargo not found (RTK unavailable)"
- fi
- if command -v python3 &>/dev/null; then
- pass "Python $(python3 --version | awk '{print $2}')"
- else
- warn "Python3 not found"
- fi
- if command -v claude &>/dev/null; then
- pass "Claude Code $(claude --version 2>/dev/null | head -1 || echo 'installed')"
- else
- fail "Claude Code not found — install from https://code.claude.com"
- fi
- echo ""
- # ────────────────────────────────────────────────────────────
- # 4. Key plugins
- # ────────────────────────────────────────────────────────────
- echo "── Plugins ──"
- if detect_rtk; then
- pass "RTK installed"
- else
- warn "RTK not installed — run install-plugins.sh"
- fi
- if detect_superpowers; then
- pass "Superpowers plugin detected"
- else
- fail "Superpowers not detected — orchestrators (/init-project, /ship-feature) will fail"
- fi
- if detect_context7; then
- pass "Context7 MCP configured"
- else
- info "Context7 MCP not configured (optional — needed for fast-evolving libs)"
- fi
- echo ""
- # ────────────────────────────────────────────────────────────
- # 5. Permissions check
- # ────────────────────────────────────────────────────────────
- echo "── Permissions ──"
- SETTINGS="$HOME/.claude/settings.json"
- if [ -f "$SETTINGS" ] || [ -L "$SETTINGS" ]; then
- if grep -q '"disableBypassPermissionsMode"' "$SETTINGS" 2>/dev/null; then
- pass "Bypass mode disabled"
- else
- warn "disableBypassPermissionsMode not found in settings"
- fi
- DENY_COUNT=$(python3 -c "
- import json
- with open('$SETTINGS') as f:
- d = json.load(f)
- print(len(d.get('permissions',{}).get('deny',[])))
- " 2>/dev/null || echo "?")
- pass "Deny rules: $DENY_COUNT"
- else
- fail "~/.claude/settings.json not found"
- fi
- echo ""
- # ────────────────────────────────────────────────────────────
- # 6. Token budget estimate
- # ────────────────────────────────────────────────────────────
- echo "── Token budget estimate ──"
- TOTAL_CHARS=0
- # Skill descriptions
- for f in "$HOME/.claude/skills/"*/SKILL.md; do
- [ -f "$f" ] || continue
- desc=$(sed -n 's/^description: //p' "$f" 2>/dev/null || true)
- TOTAL_CHARS=$((TOTAL_CHARS + ${#desc}))
- done
- # Agent descriptions
- for f in "$HOME/.claude/agents/"*.md; do
- [ -f "$f" ] || continue
- desc=$(sed -n '/^---$/,/^---$/{ s/^description: //p }' "$f" 2>/dev/null || true)
- TOTAL_CHARS=$((TOTAL_CHARS + ${#desc}))
- done
- if [ "$TOTAL_CHARS" -gt 6000 ]; then
- warn "Custom descriptions: ~${TOTAL_CHARS} chars (budget ~8000) — risk of truncation"
- elif [ "$TOTAL_CHARS" -gt 4000 ]; then
- info "Custom descriptions: ~${TOTAL_CHARS} chars (within budget, moderate margin)"
- else
- pass "Custom descriptions: ~${TOTAL_CHARS} chars (comfortable)"
- fi
- echo ""
- # ────────────────────────────────────────────────────────────
- # 7. File consistency
- # ────────────────────────────────────────────────────────────
- echo "── Consistency ──"
- # Check all skills have disable-model-invocation
- MISSING_DMI=()
- for f in "$HOME/.claude/skills/"*/SKILL.md; do
- [ -f "$f" ] || continue
- name=$(basename "$(dirname "$f")")
- if ! grep -q "disable-model-invocation" "$f" 2>/dev/null; then
- MISSING_DMI+=("$name")
- fi
- done
- if [ ${#MISSING_DMI[@]} -eq 0 ]; then
- pass "All skills have disable-model-invocation"
- else
- warn "Skills missing disable-model-invocation: ${MISSING_DMI[*]}"
- fi
- # Check CRLF
- CRLF_FILES=()
- for f in "$REPO"/*.md "$REPO"/agents/*.md "$REPO"/skills/*/SKILL.md; do
- [ -f "$f" ] || continue
- if grep -qP '\r' "$f" 2>/dev/null; then
- CRLF_FILES+=("$(basename "$f")")
- fi
- done
- if [ ${#CRLF_FILES[@]} -eq 0 ]; then
- pass "No CRLF line endings detected"
- else
- warn "CRLF detected in: ${CRLF_FILES[*]}"
- fi
- echo ""
- # ────────────────────────────────────────────────────────────
- # Summary
- # ────────────────────────────────────────────────────────────
- echo "═══════════════════════════════════════════"
- if [ "$ERRORS" -gt 0 ]; then
- echo -e "${RED} $ERRORS error(s)${NC}, ${YELLOW}$WARNS warning(s)${NC}"
- echo ""
- echo " Fix: cd $REPO && bash link.sh && bash install-plugins.sh"
- exit 1
- elif [ "$WARNS" -gt 0 ]; then
- echo -e " ${GREEN}No errors${NC}, ${YELLOW}$WARNS warning(s)${NC}"
- else
- echo -e " ${GREEN}All checks passed ✓${NC}"
- fi
- echo ""
|