claude-code-ultimate-guide/examples/scripts/audit-scan.sh
Florian BRUNIAUX ebf1506196 fix(audit): ALL_DEPS unbound variable + extended integrations
- Initialize ALL_DEPS="" to prevent unbound variable error outside Node.js projects
- Added 35+ new integration detections: Kinde, LemonSqueezy, Vercel AI SDK,
  TalkJS, Knock, Stream, MapLibre, Mapbox, Google Maps, Bytescale, UploadThing,
  Cloudinary, Forest Admin, Refine, Vercel Analytics, Mixpanel, Hotjar, etc.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 08:42:49 +01:00

674 lines
28 KiB
Bash
Executable file
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# claude-audit-scan.sh - Fast Claude Code setup scanner
#
# Scans your Claude Code configuration (global + project) and outputs
# a structured report of what's configured, what's missing, and quality patterns.
#
# Usage:
# ./audit-scan.sh # Human-readable output (default)
# ./audit-scan.sh --json # JSON output for Claude processing
# ./audit-scan.sh --help # Show this help
set -euo pipefail
# Colors for human output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
# Output mode (default: human)
OUTPUT_MODE="human"
# Parse arguments
while [[ $# -gt 0 ]]; do
case $1 in
--json)
OUTPUT_MODE="json"
shift
;;
--help|-h)
grep '^#' "$0" | sed 's/^# \?//'
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
# Helper functions
check_file() {
[[ -f "$1" ]] && echo "true" || echo "false"
}
count_md_files() {
if [[ -d "$1" ]]; then
# Count all .md files recursively (commands/skills can have subfolders)
find "$1" -type f -name "*.md" ! -name "README.md" 2>/dev/null | wc -l | tr -d ' '
else
echo "0"
fi
}
count_script_files() {
if [[ -d "$1" ]]; then
# Count hook scripts (.sh, .js, .py, .ts)
find "$1" -type f \( -name "*.sh" -o -name "*.js" -o -name "*.py" -o -name "*.ts" \) 2>/dev/null | wc -l | tr -d ' '
else
echo "0"
fi
}
check_pattern() {
local file="$1"
local pattern="$2"
if [[ -f "$file" ]]; then
grep -q "$pattern" "$file" 2>/dev/null && echo "true" || echo "false"
else
echo "false"
fi
}
get_file_lines() {
[[ -f "$1" ]] && wc -l < "$1" | tr -d ' ' || echo "0"
}
count_pattern() {
if [[ -f "$1" ]]; then
local count
count=$(grep -c "$2" "$1" 2>/dev/null) || count="0"
echo "$count"
else
echo "0"
fi
}
# JSON helper - extract value without jq (fallback)
json_extract() {
local file="$1"
local key="$2"
grep -oE "\"$key\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" "$file" 2>/dev/null | head -1 | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/'
}
# JSON helper - extract keys from object (fallback)
json_keys() {
local file="$1"
grep -oE '"[a-zA-Z0-9_@/-]+"[[:space:]]*:' "$file" 2>/dev/null | sed 's/"//g;s/://g;s/[[:space:]]//g' | head -50
}
# Expand home directory
GLOBAL_DIR="${HOME}/.claude"
# === DATA COLLECTION ===
# Global config
GLOBAL_CLAUDE_MD=$(check_file "${GLOBAL_DIR}/CLAUDE.md")
GLOBAL_SETTINGS=$(check_file "${GLOBAL_DIR}/settings.json")
GLOBAL_MCP=$(check_file "${GLOBAL_DIR}/mcp.json")
# Project config
PROJECT_CLAUDE_MD=$(check_file "./CLAUDE.md")
LOCAL_CLAUDE_MD=$(check_file "./.claude/CLAUDE.md")
PROJECT_SETTINGS=$(check_file "./.claude/settings.json")
LOCAL_SETTINGS=$(check_file "./.claude/settings.local.json")
# Extensions (count .md files recursively, hooks are scripts)
AGENTS_COUNT=$(count_md_files "./.claude/agents")
COMMANDS_COUNT=$(count_md_files "./.claude/commands")
SKILLS_COUNT=$(count_md_files "./.claude/skills")
HOOKS_COUNT=$(count_script_files "./.claude/hooks")
RULES_COUNT=$(count_md_files "./.claude/rules")
# === ENHANCED STACK DETECTION ===
TECH_STACK="unknown"
STACK_FRAMEWORK=""
STACK_RUNTIME=""
STACK_TEST=""
STACK_BUNDLER=""
STACK_DB=""
STACK_INTEGRATIONS=""
ALL_DEPS="" # Initialize to avoid unbound variable
# Detect by manifest file
if [[ -f "package.json" ]]; then
TECH_STACK="nodejs"
STACK_RUNTIME="Node.js"
# Extract dependencies (with or without jq)
DEPS=""
DEV_DEPS=""
if command -v jq &> /dev/null; then
DEPS=$(jq -r '.dependencies // {} | keys[]' package.json 2>/dev/null | tr '\n' ' ')
DEV_DEPS=$(jq -r '.devDependencies // {} | keys[]' package.json 2>/dev/null | tr '\n' ' ')
else
# Fallback: grep-based extraction
DEPS=$(grep -A 1000 '"dependencies"' package.json 2>/dev/null | grep -B 1000 -m 1 '}' | grep -oE '"[^"]+":' | sed 's/"//g;s/://g' | tr '\n' ' ')
DEV_DEPS=$(grep -A 1000 '"devDependencies"' package.json 2>/dev/null | grep -B 1000 -m 1 '}' | grep -oE '"[^"]+":' | sed 's/"//g;s/://g' | tr '\n' ' ')
fi
ALL_DEPS="$DEPS $DEV_DEPS"
# Framework detection
[[ "$ALL_DEPS" == *"next"* ]] && STACK_FRAMEWORK="Next.js"
[[ "$ALL_DEPS" == *"nuxt"* ]] && STACK_FRAMEWORK="Nuxt"
[[ "$ALL_DEPS" == *"@angular/core"* ]] && STACK_FRAMEWORK="Angular"
[[ "$ALL_DEPS" == *"vue"* && -z "$STACK_FRAMEWORK" ]] && STACK_FRAMEWORK="Vue"
[[ "$ALL_DEPS" == *"react"* && -z "$STACK_FRAMEWORK" ]] && STACK_FRAMEWORK="React"
[[ "$ALL_DEPS" == *"express"* && -z "$STACK_FRAMEWORK" ]] && STACK_FRAMEWORK="Express"
[[ "$ALL_DEPS" == *"fastify"* && -z "$STACK_FRAMEWORK" ]] && STACK_FRAMEWORK="Fastify"
[[ "$ALL_DEPS" == *"nestjs"* || "$ALL_DEPS" == *"@nestjs/core"* ]] && STACK_FRAMEWORK="NestJS"
[[ "$ALL_DEPS" == *"svelte"* ]] && STACK_FRAMEWORK="Svelte"
[[ "$ALL_DEPS" == *"astro"* ]] && STACK_FRAMEWORK="Astro"
[[ "$ALL_DEPS" == *"remix"* ]] && STACK_FRAMEWORK="Remix"
# Test framework detection
[[ "$ALL_DEPS" == *"vitest"* ]] && STACK_TEST="Vitest"
[[ "$ALL_DEPS" == *"jest"* && -z "$STACK_TEST" ]] && STACK_TEST="Jest"
[[ "$ALL_DEPS" == *"mocha"* && -z "$STACK_TEST" ]] && STACK_TEST="Mocha"
[[ "$ALL_DEPS" == *"playwright"* ]] && STACK_TEST="${STACK_TEST:+$STACK_TEST + }Playwright"
[[ "$ALL_DEPS" == *"cypress"* ]] && STACK_TEST="${STACK_TEST:+$STACK_TEST + }Cypress"
# Bundler detection
[[ "$ALL_DEPS" == *"vite"* ]] && STACK_BUNDLER="Vite"
[[ "$ALL_DEPS" == *"webpack"* && -z "$STACK_BUNDLER" ]] && STACK_BUNDLER="Webpack"
[[ "$ALL_DEPS" == *"esbuild"* && -z "$STACK_BUNDLER" ]] && STACK_BUNDLER="esbuild"
[[ "$ALL_DEPS" == *"turbo"* ]] && STACK_BUNDLER="${STACK_BUNDLER:+$STACK_BUNDLER + }Turborepo"
# Database/ORM detection
[[ "$ALL_DEPS" == *"prisma"* || "$ALL_DEPS" == *"@prisma/client"* ]] && STACK_DB="Prisma"
[[ "$ALL_DEPS" == *"drizzle"* ]] && STACK_DB="Drizzle"
[[ "$ALL_DEPS" == *"typeorm"* ]] && STACK_DB="TypeORM"
[[ "$ALL_DEPS" == *"mongoose"* ]] && STACK_DB="Mongoose (MongoDB)"
[[ "$ALL_DEPS" == *"sequelize"* ]] && STACK_DB="Sequelize"
# Key integrations detection (generic - look for notable packages)
INTEGRATIONS_LIST=""
# Auth
[[ "$ALL_DEPS" == *"@clerk"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Clerk, "
[[ "$ALL_DEPS" == *"next-auth"* || "$ALL_DEPS" == *"@auth/"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}NextAuth, "
[[ "$ALL_DEPS" == *"@supabase"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Supabase, "
[[ "$ALL_DEPS" == *"firebase"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Firebase, "
[[ "$ALL_DEPS" == *"@kinde"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Kinde, "
# Payments
[[ "$ALL_DEPS" == *"stripe"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Stripe, "
[[ "$ALL_DEPS" == *"@lemonsqueezy"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}LemonSqueezy, "
# AI/ML
[[ "$ALL_DEPS" == *"openai"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}OpenAI, "
[[ "$ALL_DEPS" == *"@anthropic"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Anthropic, "
[[ "$ALL_DEPS" == *"langchain"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}LangChain, "
[[ "$ALL_DEPS" == *"@ai-sdk"* || "$ALL_DEPS" == *"ai "* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Vercel AI SDK, "
# Communication / Chat
[[ "$ALL_DEPS" == *"@daily-co"* || "$ALL_DEPS" == *"daily-js"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Daily.co, "
[[ "$ALL_DEPS" == *"twilio"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Twilio, "
[[ "$ALL_DEPS" == *"sendgrid"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}SendGrid, "
[[ "$ALL_DEPS" == *"resend"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Resend, "
[[ "$ALL_DEPS" == *"talkjs"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}TalkJS, "
[[ "$ALL_DEPS" == *"@knocklabs"* || "$ALL_DEPS" == *"knock"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Knock, "
[[ "$ALL_DEPS" == *"stream-chat"* || "$ALL_DEPS" == *"@stream-io"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Stream, "
# Maps
[[ "$ALL_DEPS" == *"maplibre"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}MapLibre, "
[[ "$ALL_DEPS" == *"mapbox"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Mapbox, "
[[ "$ALL_DEPS" == *"@react-google-maps"* || "$ALL_DEPS" == *"google-maps"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Google Maps, "
# File Upload
[[ "$ALL_DEPS" == *"bytescale"* || "$ALL_DEPS" == *"@bytescale"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Bytescale, "
[[ "$ALL_DEPS" == *"uploadthing"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}UploadThing, "
[[ "$ALL_DEPS" == *"cloudinary"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Cloudinary, "
# Admin Panels
[[ "$ALL_DEPS" == *"forest-admin"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Forest Admin, "
[[ "$ALL_DEPS" == *"@refinedev"* || "$ALL_DEPS" == *"refine"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Refine, "
# Monitoring / Analytics
[[ "$ALL_DEPS" == *"@sentry"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Sentry, "
[[ "$ALL_DEPS" == *"posthog"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}PostHog, "
[[ "$ALL_DEPS" == *"@vercel/analytics"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Vercel Analytics, "
[[ "$ALL_DEPS" == *"mixpanel"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Mixpanel, "
[[ "$ALL_DEPS" == *"@hotjar"* || "$ALL_DEPS" == *"hotjar"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Hotjar, "
[[ "$ALL_DEPS" == *"@amplitude"* || "$ALL_DEPS" == *"amplitude"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Amplitude, "
# CMS/Content
[[ "$ALL_DEPS" == *"sanity"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Sanity, "
[[ "$ALL_DEPS" == *"contentful"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Contentful, "
[[ "$ALL_DEPS" == *"@payloadcms"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Payload CMS, "
# State management
[[ "$ALL_DEPS" == *"zustand"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Zustand, "
[[ "$ALL_DEPS" == *"@tanstack/react-query"* || "$ALL_DEPS" == *"react-query"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}TanStack Query, "
[[ "$ALL_DEPS" == *"trpc"* || "$ALL_DEPS" == *"@trpc"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}tRPC, "
[[ "$ALL_DEPS" == *"jotai"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Jotai, "
# Validation
[[ "$ALL_DEPS" == *"zod"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Zod, "
[[ "$ALL_DEPS" == *"yup"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Yup, "
[[ "$ALL_DEPS" == *"valibot"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Valibot, "
# UI Libraries
[[ "$ALL_DEPS" == *"tailwindcss"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Tailwind, "
[[ "$ALL_DEPS" == *"@radix-ui"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Radix UI, "
[[ "$ALL_DEPS" == *"@chakra-ui"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Chakra UI, "
[[ "$ALL_DEPS" == *"@mui"* || "$ALL_DEPS" == *"@material-ui"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Material UI, "
[[ "$ALL_DEPS" == *"daisyui"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}DaisyUI, "
[[ "$ALL_DEPS" == *"@mantine"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Mantine, "
# shadcn/ui special case: not in package.json, check for components/ui folder
if [[ "$ALL_DEPS" == *"shadcn"* ]] || [[ -d "components/ui" ]] || [[ -d "src/components/ui" ]]; then
echo "$INTEGRATIONS_LIST" | grep -q "shadcn" || INTEGRATIONS_LIST="${INTEGRATIONS_LIST}shadcn/ui, "
fi
# Database providers (extends existing DB detection)
[[ "$ALL_DEPS" == *"@neondatabase"* || "$ALL_DEPS" == *"@neon"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Neon, "
[[ "$ALL_DEPS" == *"@planetscale"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}PlanetScale, "
[[ "$ALL_DEPS" == *"@vercel/postgres"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Vercel Postgres, "
[[ "$ALL_DEPS" == *"@upstash"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Upstash, "
[[ "$ALL_DEPS" == *"@libsql"* || "$ALL_DEPS" == *"turso"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Turso, "
# Feature flags
[[ "$ALL_DEPS" == *"@vercel/flags"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Vercel Flags, "
[[ "$ALL_DEPS" == *"launchdarkly"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}LaunchDarkly, "
# Forms
[[ "$ALL_DEPS" == *"react-hook-form"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}React Hook Form, "
[[ "$ALL_DEPS" == *"formik"* ]] && INTEGRATIONS_LIST="${INTEGRATIONS_LIST}Formik, "
# Clean up trailing comma
STACK_INTEGRATIONS=$(echo "$INTEGRATIONS_LIST" | sed 's/, $//')
elif [[ -f "pyproject.toml" ]] || [[ -f "requirements.txt" ]]; then
TECH_STACK="python"
STACK_RUNTIME="Python"
# Python framework detection
PYTHON_DEPS=""
if [[ -f "requirements.txt" ]]; then
PYTHON_DEPS=$(cat requirements.txt 2>/dev/null | tr '\n' ' ')
fi
if [[ -f "pyproject.toml" ]]; then
PYTHON_DEPS="$PYTHON_DEPS $(cat pyproject.toml 2>/dev/null | tr '\n' ' ')"
fi
[[ "$PYTHON_DEPS" == *"django"* ]] && STACK_FRAMEWORK="Django"
[[ "$PYTHON_DEPS" == *"fastapi"* ]] && STACK_FRAMEWORK="FastAPI"
[[ "$PYTHON_DEPS" == *"flask"* && -z "$STACK_FRAMEWORK" ]] && STACK_FRAMEWORK="Flask"
[[ "$PYTHON_DEPS" == *"pytest"* ]] && STACK_TEST="pytest"
[[ "$PYTHON_DEPS" == *"sqlalchemy"* ]] && STACK_DB="SQLAlchemy"
[[ "$PYTHON_DEPS" == *"prisma"* ]] && STACK_DB="Prisma"
elif [[ -f "go.mod" ]]; then
TECH_STACK="go"
STACK_RUNTIME="Go"
[[ -f "go.mod" ]] && grep -q "gin-gonic" go.mod 2>/dev/null && STACK_FRAMEWORK="Gin"
[[ -f "go.mod" ]] && grep -q "echo" go.mod 2>/dev/null && STACK_FRAMEWORK="Echo"
elif [[ -f "Cargo.toml" ]]; then
TECH_STACK="rust"
STACK_RUNTIME="Rust"
[[ -f "Cargo.toml" ]] && grep -q "actix" Cargo.toml 2>/dev/null && STACK_FRAMEWORK="Actix"
[[ -f "Cargo.toml" ]] && grep -q "axum" Cargo.toml 2>/dev/null && STACK_FRAMEWORK="Axum"
elif [[ -f "composer.json" ]]; then
TECH_STACK="php"
STACK_RUNTIME="PHP"
[[ -f "composer.json" ]] && grep -q "laravel" composer.json 2>/dev/null && STACK_FRAMEWORK="Laravel"
[[ -f "composer.json" ]] && grep -q "symfony" composer.json 2>/dev/null && STACK_FRAMEWORK="Symfony"
fi
# Quality patterns
HAS_SECURITY_HOOKS="false"
if [[ -d "./.claude/hooks" ]]; then
grep -l "PreToolUse" ./.claude/hooks/* 2>/dev/null >/dev/null && HAS_SECURITY_HOOKS="true"
fi
# P1.2: Test framework detection (also detect absence)
HAS_TEST_FRAMEWORK="false"
if [[ -n "$STACK_TEST" ]]; then
HAS_TEST_FRAMEWORK="true"
elif [[ -f "jest.config.js" ]] || [[ -f "jest.config.ts" ]] || [[ -f "jest.config.mjs" ]]; then
HAS_TEST_FRAMEWORK="true"
STACK_TEST="Jest"
elif [[ -f "vitest.config.ts" ]] || [[ -f "vitest.config.js" ]] || [[ -f "vitest.config.mts" ]]; then
HAS_TEST_FRAMEWORK="true"
STACK_TEST="Vitest"
elif [[ -f "playwright.config.ts" ]] || [[ -f "playwright.config.js" ]]; then
HAS_TEST_FRAMEWORK="true"
STACK_TEST="Playwright"
elif [[ -f "cypress.config.ts" ]] || [[ -f "cypress.config.js" ]]; then
HAS_TEST_FRAMEWORK="true"
STACK_TEST="Cypress"
else
# Last resort: check for test files
TEST_FILES=$(find . -maxdepth 4 \( -name "*.test.*" -o -name "*.spec.*" -o -name "__tests__" \) 2>/dev/null | head -1)
if [[ -n "$TEST_FILES" ]]; then
HAS_TEST_FRAMEWORK="true"
STACK_TEST="(detected from test files)"
fi
fi
# MCP servers detection
# Claude Code stores MCP config in multiple locations:
# 1. ~/.claude.json under projects.<cwd>.mcpServers (per-project)
# 2. ~/.claude.json under mcpServers (global - applies to ALL projects)
# 3. ./.claude/mcp.json (project-level)
# 4. ~/.claude/mcp.json (legacy global)
MCP_PROJECT_SERVERS=""
MCP_GLOBAL_SERVERS=""
MCP_PROJECT_SOURCE=""
MCP_GLOBAL_SOURCE=""
CURRENT_DIR=$(pwd)
# Check 1: ~/.claude.json (project-specific AND global)
if [[ -f "${HOME}/.claude.json" ]]; then
if command -v jq &> /dev/null; then
# Get project-specific MCP servers
MCP_PROJECT_SERVERS=$(jq -r --arg path "$CURRENT_DIR" '.projects[$path].mcpServers // {} | keys[]' "${HOME}/.claude.json" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_PROJECT_SERVERS" ]]; then
MCP_PROJECT_SOURCE="~/.claude.json (project)"
fi
# Get global MCP servers (top-level mcpServers key)
MCP_GLOBAL_SERVERS=$(jq -r '.mcpServers // {} | keys[]' "${HOME}/.claude.json" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_GLOBAL_SERVERS" ]]; then
MCP_GLOBAL_SOURCE="~/.claude.json (global)"
fi
else
# Fallback grep for project-specific
if grep -q "\"$CURRENT_DIR\"" "${HOME}/.claude.json" 2>/dev/null; then
MCP_PROJECT_SERVERS=$(grep -A 100 "\"$CURRENT_DIR\"" "${HOME}/.claude.json" 2>/dev/null | grep -A 50 "mcpServers" | grep -oE '"[a-zA-Z0-9_-]+"[[:space:]]*:' | head -10 | sed 's/"//g;s/://g' | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_PROJECT_SERVERS" ]]; then
MCP_PROJECT_SOURCE="~/.claude.json (project)"
fi
fi
# Fallback grep for global (look for mcpServers before "projects" key)
MCP_GLOBAL_SERVERS=$(sed -n '1,/"projects"/p' "${HOME}/.claude.json" 2>/dev/null | grep -A 50 '"mcpServers"' | grep -oE '"[a-zA-Z0-9_-]+"[[:space:]]*:[[:space:]]*\{' | head -10 | sed 's/"//g;s/://g;s/{//g' | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_GLOBAL_SERVERS" ]]; then
MCP_GLOBAL_SOURCE="~/.claude.json (global)"
fi
fi
fi
# Check 2: Project-level .claude/mcp.json
if [[ -z "$MCP_PROJECT_SERVERS" && -f "./.claude/mcp.json" ]]; then
if command -v jq &> /dev/null; then
MCP_PROJECT_SERVERS=$(jq -r '.mcpServers // {} | keys[]' "./.claude/mcp.json" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_PROJECT_SERVERS" ]]; then
MCP_PROJECT_SOURCE=".claude/mcp.json (project)"
fi
else
MCP_PROJECT_SERVERS=$(grep -oE '"[a-zA-Z0-9_-]+"[[:space:]]*:' "./.claude/mcp.json" 2>/dev/null | head -20 | sed 's/"//g;s/://g' | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_PROJECT_SERVERS" ]]; then
MCP_PROJECT_SOURCE=".claude/mcp.json (project)"
fi
fi
fi
# Check 3: Legacy global ~/.claude/mcp.json
if [[ -z "$MCP_GLOBAL_SERVERS" && -f "${GLOBAL_DIR}/mcp.json" ]]; then
if command -v jq &> /dev/null; then
MCP_GLOBAL_SERVERS=$(jq -r '.mcpServers // {} | keys[]' "${GLOBAL_DIR}/mcp.json" 2>/dev/null | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_GLOBAL_SERVERS" ]]; then
MCP_GLOBAL_SOURCE="~/.claude/mcp.json (legacy global)"
fi
else
MCP_GLOBAL_SERVERS=$(grep -oE '"[a-zA-Z0-9_-]+"[[:space:]]*:' "${GLOBAL_DIR}/mcp.json" 2>/dev/null | head -20 | sed 's/"//g;s/://g' | tr '\n' ',' | sed 's/,$//')
if [[ -n "$MCP_GLOBAL_SERVERS" ]]; then
MCP_GLOBAL_SOURCE="~/.claude/mcp.json (legacy global)"
fi
fi
fi
# Merge all MCP servers (project + global)
MCP_ALL_SERVERS=""
if [[ -n "$MCP_PROJECT_SERVERS" && -n "$MCP_GLOBAL_SERVERS" ]]; then
MCP_ALL_SERVERS="$MCP_PROJECT_SERVERS,$MCP_GLOBAL_SERVERS"
elif [[ -n "$MCP_PROJECT_SERVERS" ]]; then
MCP_ALL_SERVERS="$MCP_PROJECT_SERVERS"
elif [[ -n "$MCP_GLOBAL_SERVERS" ]]; then
MCP_ALL_SERVERS="$MCP_GLOBAL_SERVERS"
fi
# For backward compatibility
MCP_SERVERS="$MCP_ALL_SERVERS"
MCP_SOURCE=""
if [[ -n "$MCP_PROJECT_SOURCE" && -n "$MCP_GLOBAL_SOURCE" ]]; then
MCP_SOURCE="$MCP_PROJECT_SOURCE + $MCP_GLOBAL_SOURCE"
elif [[ -n "$MCP_PROJECT_SOURCE" ]]; then
MCP_SOURCE="$MCP_PROJECT_SOURCE"
elif [[ -n "$MCP_GLOBAL_SOURCE" ]]; then
MCP_SOURCE="$MCP_GLOBAL_SOURCE"
fi
# Count MCP servers (unique)
MCP_COUNT=0
MCP_PROJECT_COUNT=0
MCP_GLOBAL_COUNT=0
if [[ -n "$MCP_ALL_SERVERS" ]]; then
MCP_COUNT=$(echo "$MCP_ALL_SERVERS" | tr ',' '\n' | sort -u | grep -c . || echo "0")
fi
if [[ -n "$MCP_PROJECT_SERVERS" ]]; then
MCP_PROJECT_COUNT=$(echo "$MCP_PROJECT_SERVERS" | tr ',' '\n' | grep -c . || echo "0")
fi
if [[ -n "$MCP_GLOBAL_SERVERS" ]]; then
MCP_GLOBAL_COUNT=$(echo "$MCP_GLOBAL_SERVERS" | tr ',' '\n' | grep -c . || echo "0")
fi
# === P0.2: Detect MCPs mentioned in CLAUDE.md but not configured ===
MCP_DOCUMENTED=""
MCP_MISSING=""
KNOWN_MCPS="serena context7 sequential playwright morphllm magic filesystem"
# Scan CLAUDE.md files for MCP mentions
scan_for_mcps() {
local file="$1"
if [[ -f "$file" ]]; then
for mcp in $KNOWN_MCPS; do
if grep -qi "$mcp" "$file" 2>/dev/null; then
if [[ -z "$MCP_DOCUMENTED" ]]; then
MCP_DOCUMENTED="$mcp"
else
echo "$MCP_DOCUMENTED" | grep -q "$mcp" || MCP_DOCUMENTED="$MCP_DOCUMENTED,$mcp"
fi
fi
done
fi
}
scan_for_mcps "./CLAUDE.md"
scan_for_mcps "./.claude/CLAUDE.md"
scan_for_mcps "${GLOBAL_DIR}/CLAUDE.md"
# Find MCPs documented but not configured
if [[ -n "$MCP_DOCUMENTED" ]]; then
for mcp in $(echo "$MCP_DOCUMENTED" | tr ',' '\n'); do
if ! echo "$MCP_ALL_SERVERS" | grep -qi "$mcp" 2>/dev/null; then
if [[ -z "$MCP_MISSING" ]]; then
MCP_MISSING="$mcp"
else
MCP_MISSING="$MCP_MISSING,$mcp"
fi
fi
done
fi
# P1.3: MCP Recommendations based on detected stack
MCP_RECOMMENDATIONS=""
# Context7 for modern frameworks (official docs lookup)
if [[ -n "$STACK_FRAMEWORK" ]]; then
if ! echo "$MCP_ALL_SERVERS" | grep -qi "context7" 2>/dev/null; then
MCP_RECOMMENDATIONS="${MCP_RECOMMENDATIONS}context7 ($STACK_FRAMEWORK docs), "
fi
fi
# Sequential for complex architectures (reasoning)
if [[ -n "$STACK_DB" ]] || [[ "$STACK_FRAMEWORK" == "NestJS" ]] || [[ "$STACK_FRAMEWORK" == "Next.js" ]]; then
if ! echo "$MCP_ALL_SERVERS" | grep -qi "sequential" 2>/dev/null; then
MCP_RECOMMENDATIONS="${MCP_RECOMMENDATIONS}sequential-thinking (complex reasoning), "
fi
fi
# Playwright for E2E testing (if no E2E framework)
if [[ "$HAS_TEST_FRAMEWORK" == "false" ]] || [[ -z "$STACK_TEST" ]]; then
if ! echo "$MCP_ALL_SERVERS" | grep -qi "playwright" 2>/dev/null; then
MCP_RECOMMENDATIONS="${MCP_RECOMMENDATIONS}playwright (E2E testing), "
fi
fi
# Serena for large TypeScript projects (symbol navigation)
if [[ "$ALL_DEPS" == *"typescript"* ]] || [[ -f "tsconfig.json" ]]; then
if ! echo "$MCP_ALL_SERVERS" | grep -qi "serena" 2>/dev/null; then
MCP_RECOMMENDATIONS="${MCP_RECOMMENDATIONS}serena (TypeScript symbols), "
fi
fi
# Clean up trailing comma
MCP_RECOMMENDATIONS=$(echo "$MCP_RECOMMENDATIONS" | sed 's/, $//')
# Memory file quality (if exists)
CLAUDE_MD_LINES="0"
CLAUDE_MD_REFS="0"
if [[ -f "./CLAUDE.md" ]]; then
CLAUDE_MD_LINES=$(get_file_lines "./CLAUDE.md")
CLAUDE_MD_REFS=$(count_pattern "./CLAUDE.md" "@")
fi
# Single Source of Truth pattern
HAS_SSOT="false"
NEEDS_SSOT_REFACTOR="false"
CODEBASE_REFS="0"
if [[ -f "./CLAUDE.md" ]]; then
grep -qE "^@|/docs/|/conventions/" "./CLAUDE.md" 2>/dev/null && HAS_SSOT="true"
# Warning: large CLAUDE.md without @references should use SSoT pattern
if [[ $CLAUDE_MD_LINES -gt 100 && $CLAUDE_MD_REFS -eq 0 ]]; then
NEEDS_SSOT_REFACTOR="true"
fi
fi
# P2.1: Also check for implicit SSoT in codebase (even without CLAUDE.md)
# Look for @file.md references in code comments or markdown files
if [[ "$HAS_SSOT" == "false" ]]; then
CODEBASE_REFS=$(find . -maxdepth 3 \( -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.md" \) \
-exec grep -l "@[a-zA-Z0-9_/-]*\.md" {} \; 2>/dev/null | wc -l | tr -d ' ')
if [[ $CODEBASE_REFS -gt 5 ]]; then
HAS_SSOT="true"
fi
fi
# === OUTPUT ===
if [[ "$OUTPUT_MODE" == "json" ]]; then
# JSON output - ensure all values are properly formatted
cat <<EOF
{
"global": {
"claude_md": $GLOBAL_CLAUDE_MD,
"settings": $GLOBAL_SETTINGS,
"mcp_json": $GLOBAL_MCP
},
"project": {
"claude_md": $PROJECT_CLAUDE_MD,
"local_claude_md": $LOCAL_CLAUDE_MD,
"settings": $PROJECT_SETTINGS,
"local_settings": $LOCAL_SETTINGS
},
"extensions": {
"agents": $AGENTS_COUNT,
"commands": $COMMANDS_COUNT,
"skills": $SKILLS_COUNT,
"hooks": $HOOKS_COUNT,
"rules": $RULES_COUNT
},
"stack": {
"type": "$TECH_STACK",
"runtime": "$STACK_RUNTIME",
"framework": "$STACK_FRAMEWORK",
"test": "$STACK_TEST",
"bundler": "$STACK_BUNDLER",
"database": "$STACK_DB",
"integrations": "$STACK_INTEGRATIONS"
},
"quality": {
"has_security_hooks": $HAS_SECURITY_HOOKS,
"has_test_framework": $HAS_TEST_FRAMEWORK,
"has_ssot_references": $HAS_SSOT,
"needs_ssot_refactor": $NEEDS_SSOT_REFACTOR,
"claude_md_lines": $CLAUDE_MD_LINES,
"claude_md_refs": $CLAUDE_MD_REFS
},
"mcp": {
"configured": $([ -n "$MCP_SERVERS" ] && echo "true" || echo "false"),
"count": $MCP_COUNT,
"servers": "$MCP_SERVERS",
"source": "$MCP_SOURCE",
"project_servers": "$MCP_PROJECT_SERVERS",
"global_servers": "$MCP_GLOBAL_SERVERS",
"documented": "$MCP_DOCUMENTED",
"missing": "$MCP_MISSING",
"recommendations": "$MCP_RECOMMENDATIONS"
}
}
EOF
else
# Human-readable output
echo -e "${BLUE}=== CLAUDE CODE SETUP AUDIT ===${NC}\n"
# Stack recap at the top
echo -e "${CYAN}💻 STACK DETECTED${NC}"
if [[ "$TECH_STACK" != "unknown" ]]; then
echo -e " Runtime: ${GREEN}$STACK_RUNTIME${NC}"
[[ -n "$STACK_FRAMEWORK" ]] && echo -e " Framework: ${GREEN}$STACK_FRAMEWORK${NC}"
[[ -n "$STACK_TEST" ]] && echo -e " Testing: $STACK_TEST"
[[ -n "$STACK_BUNDLER" ]] && echo -e " Bundler: $STACK_BUNDLER"
[[ -n "$STACK_DB" ]] && echo -e " Database: $STACK_DB"
[[ -n "$STACK_INTEGRATIONS" ]] && echo -e " Integrations: $STACK_INTEGRATIONS"
else
echo -e " ${YELLOW}⚠️${NC} Could not detect stack (no package.json, pyproject.toml, etc.)"
fi
echo -e "\n${BLUE}📁 GLOBAL CONFIG${NC} (~/.claude/)"
[[ "$GLOBAL_CLAUDE_MD" == "true" ]] && echo -e " ${GREEN}${NC} CLAUDE.md" || echo -e " ${RED}${NC} CLAUDE.md"
[[ "$GLOBAL_SETTINGS" == "true" ]] && echo -e " ${GREEN}${NC} settings.json" || echo -e " ${RED}${NC} settings.json"
[[ "$GLOBAL_MCP" == "true" ]] && echo -e " ${GREEN}${NC} mcp.json (legacy)" || echo -e " ${YELLOW}⚠️${NC} mcp.json (not required, MCP in ~/.claude.json)"
echo -e "\n${BLUE}📁 PROJECT CONFIG${NC} (./)"
[[ "$PROJECT_CLAUDE_MD" == "true" ]] && echo -e " ${GREEN}${NC} CLAUDE.md" || echo -e " ${YELLOW}⚠️${NC} CLAUDE.md (recommended)"
[[ "$LOCAL_CLAUDE_MD" == "true" ]] && echo -e " ${GREEN}${NC} .claude/CLAUDE.md (local)" || echo -e " ${YELLOW}⚠️${NC} .claude/CLAUDE.md (optional)"
[[ "$PROJECT_SETTINGS" == "true" ]] && echo -e " ${GREEN}${NC} .claude/settings.json" || echo -e " ${YELLOW}⚠️${NC} .claude/settings.json (optional)"
echo -e "\n${BLUE}🔧 EXTENSIONS${NC} (.claude/)"
echo -e " Agents: $AGENTS_COUNT"
echo -e " Commands: $COMMANDS_COUNT"
echo -e " Skills: $SKILLS_COUNT"
echo -e " Hooks: $HOOKS_COUNT"
echo -e " Rules: $RULES_COUNT"
echo -e "\n${BLUE}✨ QUALITY PATTERNS${NC}"
[[ "$HAS_SECURITY_HOOKS" == "true" ]] && echo -e " ${GREEN}${NC} Security hooks (PreToolUse)" || echo -e " ${RED}${NC} Security hooks (recommended)"
[[ "$HAS_TEST_FRAMEWORK" == "true" ]] && echo -e " ${GREEN}${NC} Test framework: $STACK_TEST" || echo -e " ${RED}${NC} No test framework detected"
[[ "$HAS_SSOT" == "true" ]] && echo -e " ${GREEN}${NC} Single Source of Truth (@refs)" || echo -e " ${YELLOW}⚠️${NC} SSoT pattern (recommended)"
if [[ "$PROJECT_CLAUDE_MD" == "true" ]]; then
echo -e " ${BLUE}${NC} CLAUDE.md: $CLAUDE_MD_LINES lines, $CLAUDE_MD_REFS @references"
if [[ "$NEEDS_SSOT_REFACTOR" == "true" ]]; then
echo -e " ${RED}⚠️${NC} Large file without @refs → Consider SSoT pattern (split into @docs/)"
elif [[ $CLAUDE_MD_LINES -gt 200 ]]; then
echo -e " ${YELLOW}⚠️${NC} Consider shortening (>200 lines)"
fi
fi
echo -e "\n${BLUE}🔌 MCP SERVERS${NC}"
if [[ -n "$MCP_ALL_SERVERS" ]]; then
echo -e " ${GREEN}${NC} Configured ($MCP_COUNT total): $MCP_ALL_SERVERS"
[[ -n "$MCP_PROJECT_SERVERS" ]] && echo -e " ${BLUE}Project:${NC} $MCP_PROJECT_SERVERS (from $MCP_PROJECT_SOURCE)"
[[ -n "$MCP_GLOBAL_SERVERS" ]] && echo -e " ${BLUE}Global:${NC} $MCP_GLOBAL_SERVERS (from $MCP_GLOBAL_SOURCE)"
else
echo -e " ${YELLOW}⚠️${NC} No MCP servers configured"
fi
# Show MCPs documented but not configured
if [[ -n "$MCP_MISSING" ]]; then
echo -e " ${RED}⚠️${NC} Documented but NOT configured: ${YELLOW}$MCP_MISSING${NC}"
echo -e " Add to ~/.claude.json or .claude/mcp.json to use them"
fi
# Show MCP recommendations
if [[ -n "$MCP_RECOMMENDATIONS" ]]; then
echo -e " ${CYAN}💡${NC} Recommended for your stack: $MCP_RECOMMENDATIONS"
fi
echo -e "\n${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
echo -e "Scan complete! Use ${YELLOW}--json${NC} flag for machine-readable output."
fi