#!/bin/bash
# ABOUTME: Unified pre-commit hook for code quality enforcement
# ABOUTME: Runs lint/type-check for frontend/SDK, blocks suspicious files and claude_docs
set -e
# NOTE: AI commit message validation is handled by commit-msg hook (runs after message is written)
# This hook runs BEFORE the commit message exists, so we only check files here
# Check for claude_docs files (AI documentation that should never be committed)
check_claude_docs() {
local claude_docs_files=$(git diff --cached --name-only --diff-filter=ACM | grep '^claude_docs/' || true)
if [ -n "$claude_docs_files" ]; then
echo "❌ ERROR: Commit blocked - Found claude_docs/ files:"
echo "$claude_docs_files" | sed 's/^/ - /'
echo ""
echo "Files in claude_docs/ are AI working notes and must never be committed."
echo "Please unstage them with: git reset HEAD claude_docs/"
return 1
fi
return 0
}
# Check for *.md files in root directory (except standard docs and AGENTS.md)
check_root_markdown() {
local root_md_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^[^/]+\.md$' | grep -v -E '^(README|CHANGELOG|CONTRIBUTING|SECURITY|LICENSE|AGENTS)\.md$' || true)
if [ -n "$root_md_files" ]; then
echo "❌ ERROR: Commit blocked - Found markdown files in root directory:"
echo "$root_md_files" | sed 's/^/ - /'
echo ""
echo "Only README.md, CHANGELOG.md, CONTRIBUTING.md, SECURITY.md, LICENSE.md, and AGENTS.md are allowed in the root directory."
echo "Please move these files to a subdirectory or remove them."
echo "To unstage: git reset HEAD <file>"
return 1
fi
return 0
}
# Check for files with suspicious patterns (backups, old files, etc.)
check_suspicious_files() {
local suspicious_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(bak|orig|tmp|swp)$|_old\.|old_|\.old\.|rs:' || true)
if [ -n "$suspicious_files" ]; then
echo "❌ ERROR: Commit blocked - Found suspicious files:"
echo "$suspicious_files" | sed 's/^/ - /'
echo ""
echo "These files appear to be backups, old versions, or contain invalid patterns."
echo "Please remove them or rename them before committing."
return 1
fi
return 0
}
# Check frontend lint and type-check (only if frontend files are staged)
check_frontend_lint() {
local frontend_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^frontend/.*\.(ts|tsx|js|jsx)$' | grep -v node_modules || true)
if [ -n "$frontend_files" ]; then
echo "🔍 Running frontend lint checks..."
# Check if frontend node_modules exists
if [ ! -d "frontend/node_modules" ]; then
echo "⚠️ Warning: frontend/node_modules not found. Skipping frontend lint."
echo " Run 'cd frontend && npm install' to enable lint checks."
return 0
fi
# Run lint
if ! (cd frontend && npm run lint --silent 2>/dev/null); then
echo "❌ ERROR: Frontend lint check failed!"
echo ""
echo "Please fix lint errors before committing."
echo "Run: cd frontend && npm run lint"
return 1
fi
# Run type-check
if ! (cd frontend && npm run type-check --silent 2>/dev/null); then
echo "❌ ERROR: Frontend type-check failed!"
echo ""
echo "Please fix TypeScript errors before committing."
echo "Run: cd frontend && npm run type-check"
return 1
fi
echo "✅ Frontend lint checks passed"
fi
return 0
}
# Check SDK lint and type-check (only if SDK files are staged)
check_sdk_lint() {
local sdk_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^sdk/src/.*\.ts$' || true)
if [ -n "$sdk_files" ]; then
echo "🔍 Running SDK lint checks..."
# Check if SDK node_modules exists
if [ ! -d "sdk/node_modules" ]; then
echo "⚠️ Warning: sdk/node_modules not found. Skipping SDK lint."
echo " Run 'cd sdk && bun install' to enable lint checks."
return 0
fi
# Try bun first, fall back to npm
local run_cmd="npm run"
if command -v bun &>/dev/null; then
run_cmd="bun run"
fi
# Run lint (suppress output, bun doesn't support --silent flag)
if ! (cd sdk && $run_cmd lint >/dev/null 2>&1); then
echo "❌ ERROR: SDK lint check failed!"
echo ""
echo "Please fix lint errors before committing."
echo "Run: cd sdk && $run_cmd lint"
return 1
fi
# Run type-check (suppress output, bun doesn't support --silent flag)
if ! (cd sdk && $run_cmd type-check >/dev/null 2>&1); then
echo "❌ ERROR: SDK type-check failed!"
echo ""
echo "Please fix TypeScript errors before committing."
echo "Run: cd sdk && $run_cmd type-check"
return 1
fi
echo "✅ SDK lint checks passed"
fi
return 0
}
# Check for SPDX license headers in source files
check_license_headers() {
local missing_headers=""
# Get staged source files (added or modified)
# Rust files in src/ and tests/
local rs_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(src|tests)/.*\.rs$' | grep -v '/target/' || true)
# JS/TS files in sdk/, frontend/, and scripts/ (excluding node_modules)
local js_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^(sdk|frontend|scripts)/.*\.(ts|tsx|js|jsx)$' | grep -v node_modules || true)
# HTML files in templates/
local html_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^templates/.*\.html$' || true)
# Shell scripts in scripts/
local sh_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^scripts/.*\.sh$' || true)
# Python files in scripts/
local py_files=$(git diff --cached --name-only --diff-filter=ACM | grep -E '^scripts/.*\.py$' || true)
# NOTE: Markdown files excluded - LICENSE.md covers all docs, headers break markdown processors
# Check Rust files
for file in $rs_files; do
if [ -f "$file" ] && ! grep -q "SPDX-License-Identifier:" "$file" 2>/dev/null; then
missing_headers="$missing_headers\n - $file"
fi
done
# Check JS/TS files
for file in $js_files; do
if [ -f "$file" ] && ! grep -q "SPDX-License-Identifier:" "$file" 2>/dev/null; then
missing_headers="$missing_headers\n - $file"
fi
done
# Check HTML files
for file in $html_files; do
if [ -f "$file" ] && ! grep -q "SPDX-License-Identifier:" "$file" 2>/dev/null; then
missing_headers="$missing_headers\n - $file"
fi
done
# Check Shell scripts
for file in $sh_files; do
if [ -f "$file" ] && ! grep -q "SPDX-License-Identifier:" "$file" 2>/dev/null; then
missing_headers="$missing_headers\n - $file"
fi
done
# Check Python files
for file in $py_files; do
if [ -f "$file" ] && ! grep -q "SPDX-License-Identifier:" "$file" 2>/dev/null; then
missing_headers="$missing_headers\n - $file"
fi
done
if [ -n "$missing_headers" ]; then
echo "❌ ERROR: Commit blocked - Missing SPDX license headers:"
echo -e "$missing_headers"
echo ""
echo "All source files must have SPDX headers. Format:"
echo " // SPDX-License-Identifier: MIT OR Apache-2.0"
echo " // Copyright (c) 2025 Pierre Fitness Intelligence"
echo ""
echo "Run: ./scripts/add-license-headers.sh to fix automatically"
return 1
fi
return 0
}
# Main execution
main() {
local checks_passed=true
# Run all checks (AI message check handled by commit-msg hook)
if ! check_claude_docs; then
checks_passed=false
fi
if ! check_root_markdown; then
checks_passed=false
fi
if ! check_suspicious_files; then
checks_passed=false
fi
if ! check_license_headers; then
checks_passed=false
fi
if ! check_frontend_lint; then
checks_passed=false
fi
if ! check_sdk_lint; then
checks_passed=false
fi
if [ "$checks_passed" = false ]; then
exit 1
fi
echo "✅ All pre-commit checks passed"
exit 0
}
main "$@"