197 lines
6.6 KiB
Bash
Executable File
197 lines
6.6 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# TypeScript and Code Quality Check Hook
|
|
#
|
|
# This hook validates TypeScript files when they are created or modified.
|
|
# It runs TypeScript type checking and Biome linting to ensure code quality.
|
|
#
|
|
# Installation:
|
|
# 1. Copy to: .claude/hooks/on-tool-use/typescript-check.sh
|
|
# 2. Make executable: chmod +x .claude/hooks/on-tool-use/typescript-check.sh
|
|
# 3. Configure Claude Code to use this hook for Write/Edit operations
|
|
#
|
|
|
|
# Set up logging
|
|
LOG_FILE="$HOME/.claude/hooks/typescript-check.log"
|
|
mkdir -p "$(dirname "$LOG_FILE")"
|
|
|
|
# Function to log with timestamp
|
|
log() {
|
|
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" >> "$LOG_FILE"
|
|
}
|
|
|
|
# Start logging
|
|
log "=== Hook execution started (JSON mode) ==="
|
|
|
|
# Read JSON input from stdin
|
|
input=$(cat)
|
|
|
|
# Log the raw input
|
|
log "Raw input received: $input"
|
|
|
|
# Extract file path from the JSON input
|
|
file_path=$(echo "$input" | jq -r '.tool_input.file_path // .tool_input.target_file // empty')
|
|
|
|
log "Extracted file_path: '$file_path'"
|
|
|
|
# Check if file path is not empty and is a TypeScript file
|
|
if [[ -n "$file_path" && "$file_path" != "null" && "$file_path" != "empty" ]]; then
|
|
log "File path is not empty: $file_path"
|
|
|
|
# Check if it's a TypeScript file
|
|
if [[ "$file_path" == *.ts || "$file_path" == *.tsx ]]; then
|
|
log "File is a TypeScript file: $file_path"
|
|
|
|
# Check if the file actually exists
|
|
if [[ -f "$file_path" ]]; then
|
|
log "Running quality checks on $file_path..."
|
|
|
|
# Get the directory containing the TypeScript file
|
|
dir=$(dirname "$file_path")
|
|
log "File directory: $dir"
|
|
|
|
# Look for project root (contains package.json)
|
|
project_root="$dir"
|
|
while [[ "$project_root" != "/" ]]; do
|
|
if [[ -f "$project_root/package.json" ]]; then
|
|
log "Found project root at: $project_root"
|
|
break
|
|
fi
|
|
project_root=$(dirname "$project_root")
|
|
done
|
|
|
|
# If no package.json found, use file directory
|
|
if [[ ! -f "$project_root/package.json" ]]; then
|
|
log "No package.json found, using file directory: $dir"
|
|
project_root="$dir"
|
|
fi
|
|
|
|
# Function to find command in project
|
|
find_cmd() {
|
|
local cmd_name=$1
|
|
local paths=(
|
|
"$(command -v "$cmd_name" 2>/dev/null)"
|
|
"$project_root/node_modules/.bin/$cmd_name"
|
|
"$HOME/.bun/bin/$cmd_name"
|
|
"/usr/local/bin/$cmd_name"
|
|
"/opt/homebrew/bin/$cmd_name"
|
|
)
|
|
|
|
for path in "${paths[@]}"; do
|
|
if [[ -x "$path" && -n "$path" ]]; then
|
|
echo "$path"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Variables to track results
|
|
tsc_success=true
|
|
biome_success=true
|
|
all_output=""
|
|
|
|
# Change to project root for all operations
|
|
cd "$project_root" || exit 1
|
|
|
|
# 1. Run TypeScript type check
|
|
tsc_cmd=$(find_cmd tsc)
|
|
if [[ -n "$tsc_cmd" ]]; then
|
|
log "Found tsc at: $tsc_cmd"
|
|
log "Running: $tsc_cmd --noEmit"
|
|
|
|
tsc_output=$("$tsc_cmd" --noEmit 2>&1)
|
|
tsc_exit_code=$?
|
|
|
|
if [[ -n "$tsc_output" ]]; then
|
|
log "tsc output: $tsc_output"
|
|
all_output="TypeScript Check:\n$tsc_output\n\n"
|
|
fi
|
|
|
|
log "tsc exit code: $tsc_exit_code"
|
|
|
|
if [[ $tsc_exit_code -ne 0 ]]; then
|
|
log "TypeScript check found type errors"
|
|
tsc_success=false
|
|
fi
|
|
else
|
|
log "WARNING: TypeScript compiler not found"
|
|
all_output="WARNING: TypeScript compiler not found. Install TypeScript to enable type checking.\n\n"
|
|
fi
|
|
|
|
# 2. Run Biome check
|
|
biome_cmd=$(find_cmd biome)
|
|
if [[ -n "$biome_cmd" ]]; then
|
|
log "Found biome at: $biome_cmd"
|
|
log "Running: $biome_cmd check $file_path"
|
|
|
|
biome_output=$("$biome_cmd" check "$file_path" 2>&1)
|
|
biome_exit_code=$?
|
|
|
|
if [[ -n "$biome_output" ]]; then
|
|
log "biome output: $biome_output"
|
|
all_output="${all_output}Biome Check:\n$biome_output"
|
|
fi
|
|
|
|
log "biome exit code: $biome_exit_code"
|
|
|
|
if [[ $biome_exit_code -ne 0 ]]; then
|
|
log "Biome check found errors"
|
|
biome_success=false
|
|
fi
|
|
else
|
|
log "WARNING: Biome not found"
|
|
all_output="${all_output}WARNING: Biome not found. Install @biomejs/biome to enable linting."
|
|
fi
|
|
|
|
# Determine final result
|
|
if [[ "$tsc_success" == true && "$biome_success" == true ]]; then
|
|
log "All checks passed successfully"
|
|
decision='{"suppressOutput": false}'
|
|
log "Hook decision: $decision"
|
|
echo "$decision"
|
|
else
|
|
# Errors found - block the operation
|
|
log "Checks failed - blocking operation"
|
|
|
|
reason="Code quality checks failed for $file_path"
|
|
|
|
if [[ "$tsc_success" == false ]]; then
|
|
reason="$reason\n\n❌ TypeScript errors found"
|
|
fi
|
|
|
|
if [[ "$biome_success" == false ]]; then
|
|
reason="$reason\n\n❌ Biome lint/format errors found"
|
|
fi
|
|
|
|
if [[ -n "$all_output" ]]; then
|
|
reason="$reason:\n\n$all_output\n\n💡 Run these commands to fix:\n bun run format\n bun run lint:fix\n bun run type-check"
|
|
else
|
|
reason="$reason. Please fix these issues before proceeding."
|
|
fi
|
|
|
|
# Return blocking JSON response
|
|
decision=$(jq -n --arg reason "$reason" '{
|
|
"decision": "block",
|
|
"reason": $reason
|
|
}')
|
|
log "Hook decision: $decision"
|
|
echo "$decision"
|
|
fi
|
|
else
|
|
log "File does not exist: $file_path"
|
|
echo '{}'
|
|
fi
|
|
else
|
|
log "Not a TypeScript file, skipping: $file_path"
|
|
echo '{}'
|
|
fi
|
|
else
|
|
log "File path is empty or null, skipping"
|
|
echo '{}'
|
|
fi
|
|
|
|
log "=== Hook execution completed (JSON mode) ==="
|
|
log ""
|