diff --git a/.claude/hookconfig.json b/.claude/hookconfig.json new file mode 100644 index 0000000..bff56e9 --- /dev/null +++ b/.claude/hookconfig.json @@ -0,0 +1,51 @@ +{ + "version": "1.0", + "description": "Claude Hooks Configuration - defines hooks that run during Claude Code operations", + "hooks": { + "pre-tool": [ + { + "id": "pre-write-quality", + "script": "pre-tool-hook.py", + "tools": ["Write", "Edit", "MultiEdit"], + "priority": 90, + "description": "Provides code quality suggestions before writing" + }, + { + "id": "package-age-check", + "script": "check-package-age.py", + "tools": ["Bash"], + "priority": 100, + "description": "Prevents installation of outdated packages", + "config": { + "max_age_days": 180 + } + } + ], + "post-tool": [ + { + "id": "code-quality-validator", + "script": "post-tool-hook.py", + "tools": ["Write", "Edit", "MultiEdit"], + "priority": 100, + "description": "Validates code quality and provides warnings" + } + ], + "stop": [ + { + "id": "final-quality-check", + "script": "stop-hook.py", + "priority": 100, + "description": "Runs final code quality checks and can block if violations exist" + }, + { + "id": "task-completion-notify", + "script": "task-completion-notify.py", + "priority": 50, + "description": "Sends notifications when Claude completes tasks", + "config": { + "notify_on_stop": true + } + } + ] + } +} \ No newline at end of file diff --git a/.claude/hooks/config.cjs b/.claude/hooks/config.cjs deleted file mode 100644 index 76e4704..0000000 --- a/.claude/hooks/config.cjs +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Claude Hooks Configuration - Entry Points System - * Edit this file to control which hooks run - changes take effect immediately\! - */ - -module.exports = { - // Stop: Runs when Claude finishes a task - stop: ["code-quality-validator"], - - // PostToolUse: Runs after Write/Edit/MultiEdit operations - postToolUse: ["code-quality-validator"] -}; diff --git a/.claude/hooks/doc-compliance.sh b/.claude/hooks/doc-compliance.sh deleted file mode 100644 index 1988023..0000000 --- a/.claude/hooks/doc-compliance.sh +++ /dev/null @@ -1,414 +0,0 @@ -#!/bin/bash - -# Documentation Compliance Hook -# Checks code changes against project documentation standards using Claude - -# Removed strict mode as it was causing issues with regex matching - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color -BOLD='\033[1m' - -# Configuration paths -# When running from npm package, look in the user's project -if [ -n "$HOOK_PROJECT_ROOT" ]; then - CONFIG_FILE="$HOOK_PROJECT_ROOT/.claude/doc-rules/config.json" -else - # Look in current directory first - if [ -f "./.claude/doc-rules/config.json" ]; then - CONFIG_FILE="./.claude/doc-rules/config.json" - elif [ -f "$(pwd)/.claude/doc-rules/config.json" ]; then - CONFIG_FILE="$(pwd)/.claude/doc-rules/config.json" - else - CONFIG_FILE="${HOOK_PROJECT_ROOT:-$(pwd)}/.claude/doc-rules/config.json" - fi -fi - -# Load API key from multiple sources -# 1. Check ~/.gemini/.env -if [ -f "$HOME/.gemini/.env" ]; then - source "$HOME/.gemini/.env" -fi - -# 2. Check project root .env -if [ -f ".env" ]; then - source ".env" -fi - -# 3. Use environment variable if set -GEMINI_API_KEY="${GEMINI_API_KEY:-}" - -# Check for API key -if [ -z "$GEMINI_API_KEY" ]; then - echo -e "${RED}Error: GEMINI_API_KEY not found${NC}" - echo -e "${YELLOW}Please set your Gemini API key in one of these locations:${NC}" - echo " 1. Environment variable: export GEMINI_API_KEY='your-key'" - echo " 2. ~/.gemini/.env file: GEMINI_API_KEY=your-key" - echo " 3. Project .env file: GEMINI_API_KEY=your-key" - echo -e "${YELLOW}Skipping documentation compliance check${NC}" - exit 0 -fi - -if [ ! -f "$CONFIG_FILE" ]; then - echo -e "${YELLOW}Warning: Documentation rules config not found at $CONFIG_FILE${NC}" - echo "Skipping documentation compliance check" - exit 0 -fi - -# Function to get git changes -get_changed_files() { - # Get both staged and unstaged changes - git diff --name-only HEAD 2>/dev/null || true - git diff --cached --name-only 2>/dev/null || true -} - -# Function to get file content with changes -get_file_changes() { - local file="$1" - echo "=== File: $file ===" - echo "" - # Show the diff to understand what changed - git diff HEAD -- "$file" 2>/dev/null || git diff --cached -- "$file" 2>/dev/null || cat "$file" -} - -# Function to find matching documentation for a file -find_matching_docs() { - local file="$1" - local docs="" - - # Read config file - if [ ! -f "$CONFIG_FILE" ]; then - return - fi - - # Parse fileTypes from config using jq - if command -v jq >/dev/null 2>&1; then - # Use jq for proper JSON parsing - local file_types=$(jq -r '.fileTypes | to_entries[] | "\(.key)|\(.value | join(" "))"' "$CONFIG_FILE" 2>/dev/null) - while IFS='|' read -r pattern doc_list; do - case "$file" in - $pattern) docs="$docs $doc_list" ;; - esac - done <<< "$file_types" - - # Parse directories from config - local directories=$(jq -r '.directories | to_entries[] | "\(.key)|\(.value | join(" "))"' "$CONFIG_FILE" 2>/dev/null) - while IFS='|' read -r pattern doc_list; do - case "$file" in - ${pattern}*) docs="$docs $doc_list" ;; - esac - done <<< "$directories" - else - # Fallback to hardcoded if jq not available - case "$file" in - *.ts) docs="docs/typescript-standards.md" ;; - *.js) docs="docs/javascript-standards.md" ;; - *.tsx|*.jsx) docs="docs/react-standards.md" ;; - *.py) docs="docs/python-standards.md" ;; - *.sol) docs="docs/solana-standards.md docs/smart-contract-security.md" ;; - *.move) docs="docs/move-standards.md" ;; - esac - - case "$file" in - src/contracts/*) docs="$docs docs/contract-patterns.md docs/security-checklist.md" ;; - src/components/*) docs="$docs docs/component-guidelines.md" ;; - tests/*) docs="$docs docs/testing-standards.md" ;; - esac - fi - - # Remove duplicates and empty entries - echo "$docs" | tr ' ' '\n' | sort -u | grep -v '^$' -} - -# Function to get threshold for file -get_threshold() { - local file="$1" - local threshold="0.8" # default - - if [ -f "$CONFIG_FILE" ] && command -v jq >/dev/null 2>&1; then - # First try to get default threshold - threshold=$(jq -r '.thresholds.default // 0.8' "$CONFIG_FILE" 2>/dev/null) - - # Check for specific patterns in config - local thresholds=$(jq -r '.thresholds | to_entries[] | select(.key != "default") | "\(.key)|\(.value)"' "$CONFIG_FILE" 2>/dev/null) - while IFS='|' read -r pattern value; do - case "$file" in - $pattern) threshold="$value" ;; - esac - done <<< "$thresholds" - else - # Fallback to hardcoded - case "$file" in - *.sol|*.move) threshold="0.9" ;; - src/contracts/*) threshold="0.95" ;; - esac - fi - - echo "$threshold" -} - -# Function to read documentation content -read_documentation() { - local doc_path="$1" - local full_path="${HOOK_PROJECT_ROOT:-$(pwd)}/$doc_path" - - if [ -f "$full_path" ]; then - echo "=== Documentation: $doc_path ===" - echo "" - cat "$full_path" - echo "" - else - echo "Warning: Documentation file not found: $doc_path" >&2 - fi -} - - -# Function to compare floats -compare_floats() { - local score="$1" - local threshold="$2" - - # Convert to integers by multiplying by 100 - local score_int=$(awk "BEGIN {print int($score * 100)}") - local threshold_int=$(awk "BEGIN {print int($threshold * 100)}") - - [ "$score_int" -lt "$threshold_int" ] -} - -# Main execution -echo -e "${BOLD}Documentation Compliance Check${NC}" -echo "================================" - -# Get changed files -changed_files=$(get_changed_files | sort -u) - -if [ -z "$changed_files" ]; then - echo "No changed files detected" - exit 0 -fi - -# Collect all files with documentation rules and their content -all_files_content="" -all_docs_content="" -files_to_check="" -file_count=0 - -echo -e "\nAnalyzing files against documentation standards..." - -# Process each changed file -while IFS= read -r file; do - # Skip if file doesn't exist (might be deleted) - [ -f "$file" ] || continue - - # Find matching documentation - matching_docs=$(find_matching_docs "$file") - - if [ -z "$matching_docs" ]; then - continue - fi - - file_count=$((file_count + 1)) - files_to_check="$files_to_check$file\n" - - # Get only the diff (changed lines) - echo -e " โ€ข $file" - echo "DEBUG: Getting diff..." - # Get only added/modified lines from diff - file_content=$(git diff HEAD -- "$file" 2>/dev/null | grep -E "^[+]" | grep -v "^+++" | sed 's/^+//' | head -50) - # If no diff, try staged - if [ -z "$file_content" ]; then - file_content=$(git diff --cached -- "$file" 2>/dev/null | grep -E "^[+]" | grep -v "^+++" | sed 's/^+//' | head -50) - fi - # If still nothing (new file), just get first 20 lines - if [ -z "$file_content" ]; then - file_content=$(head -20 "$file" 2>/dev/null || echo "File not readable") - fi - echo "DEBUG: Got ${#file_content} characters" - all_files_content="$all_files_content - -=== FILE: $file === -$file_content" - - # Collect relevant documentation (avoid duplicates) - while IFS= read -r doc; do - if [[ ! "$all_docs_content" == *"$doc"* ]]; then - doc_content=$(read_documentation "$doc" 2>/dev/null) - if [ -n "$doc_content" ]; then - all_docs_content="$all_docs_content - -$doc_content" - fi - fi - done <<< "$matching_docs" -done <<< "$changed_files" - -if [ $file_count -eq 0 ]; then - echo -e "\n${YELLOW}No files found with documentation rules${NC}" - exit 0 -fi - -# Get default threshold -if command -v jq >/dev/null 2>&1; then - threshold=$(jq -r '.thresholds.default // 0.8' "$CONFIG_FILE" 2>/dev/null) -else - threshold="0.8" -fi - -echo -e "\nMaking compliance check (threshold: $threshold)..." -echo -e "${GRAY}Files to check: $file_count${NC}" - -# Check if we have any documentation content -if [ -z "$all_docs_content" ] || [ "$all_docs_content" = $'\n\n' ]; then - echo -e "\n${YELLOW}Warning: No documentation files found for the changed files${NC}" - echo -e "${YELLOW}Cannot perform compliance check without documentation standards${NC}" - echo -e "\nPlease create documentation files as specified in $CONFIG_FILE" - exit 0 -fi - -# Make single API call with all context -prompt="Analyze the following code files for compliance with the provided documentation standards. - -FILES TO ANALYZE: -$all_files_content - -DOCUMENTATION STANDARDS: -$all_docs_content - -Evaluate the overall compliance of ALL files against ALL applicable documentation standards. -Consider: -- How well the code follows the documented conventions -- Whether best practices are followed -- Code quality and maintainability -- Consistency across files - -Respond with ONLY a JSON object in this exact format: -{ - \"score\": 0.73, - \"summary\": \"Brief overall assessment\", - \"issues\": \"filename.ts:123:functionName(): missing return type | filename.ts:45:variableName: uses 'any' type | another-file.js:200:methodName(): no error handling\", - \"suggestions\": \"filename.ts:123: add ': Promise' return type | filename.ts:45: replace 'any' with specific interface | another-file.js:200: wrap in try-catch block\" -} - -Include line numbers and function/method names where possible. - -The score should be between 0 and 1, where 1 means perfect compliance." - -# Use Gemini Flash API -echo "DEBUG: Calling Gemini Flash API..." - -# Escape prompt for JSON -escaped_prompt=$(echo "$prompt" | jq -Rs .) - -# Create the API request -api_response=$(curl -s -X POST "https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key=$GEMINI_API_KEY" \ - -H "Content-Type: application/json" \ - -d "{ - \"contents\": [{ - \"parts\": [{ - \"text\": $escaped_prompt - }] - }], - \"generationConfig\": { - \"temperature\": 0.1, - \"maxOutputTokens\": 500 - } - }") - -# Extract content from response (Gemini uses different JSON structure) -content=$(echo "$api_response" | jq -r '.candidates[0].content.parts[0].text' 2>/dev/null) - -# Check if API call failed -if [ -z "$content" ]; then - echo -e "\n${RED}Error: Failed to get Gemini response${NC}" - echo "Response: $api_response" - exit 2 -fi - - -# Remove markdown code blocks if present -clean_content=$(echo "$content" | sed '/^```/d') - -# Parse the response -score=$(echo "$clean_content" | grep -o '"score"[[:space:]]*:[[:space:]]*[0-9.]*' | sed 's/.*:[[:space:]]*//') -summary=$(echo "$clean_content" | grep -o '"summary"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"\(.*\)"/\1/') -issues=$(echo "$clean_content" | grep -o '"issues"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"\(.*\)"/\1/') -suggestions=$(echo "$clean_content" | grep -o '"suggestions"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"\(.*\)"/\1/') - -# Default values -score=${score:-0} -summary=${summary:-"Analysis complete"} - -# Display results -echo -e "\n${BOLD}Overall Compliance Score: $score${NC} (threshold: $threshold)" -echo -e "\n$summary" - -# Check if passed -if compare_floats "$score" "$threshold"; then - echo -e "\n${RED}โŒ Documentation compliance check failed!${NC}" - echo -e "\n${BOLD}Issues:${NC}" - # Process and group issues by file - current_file="" - echo "$issues" | sed 's/ | /\n/g' | sort | while IFS= read -r issue; do - if [ -n "$issue" ]; then - # Extract file:line:method: issue pattern - if [[ "$issue" =~ ^([^:]+):([0-9]+):([^:]+):(.*)$ ]]; then - file="${BASH_REMATCH[1]}" - line="${BASH_REMATCH[2]}" - method="${BASH_REMATCH[3]}" - desc="${BASH_REMATCH[4]}" - - # Print file header if it's a new file - if [ "$file" != "$current_file" ]; then - if [ -n "$current_file" ]; then - echo "-----" - fi - echo -e "\n${YELLOW}$file${NC}" - current_file="$file" - fi - - echo -e " ${CYAN}$line${NC}: ${BOLD}$method${NC}${desc}" - else - echo -e " $issue" - fi - fi - done - if [ -n "$current_file" ]; then - echo "-----" - fi - echo -e "\n${BOLD}Fixes:${NC}" - # Process and group fixes by file - current_file="" - echo "$suggestions" | sed 's/ | /\n/g' | sort | while IFS= read -r suggestion; do - if [ -n "$suggestion" ]; then - # Extract file:line: suggestion pattern - if [[ "$suggestion" =~ ^([^:]+):([0-9]+):(.*)$ ]]; then - file="${BASH_REMATCH[1]}" - line="${BASH_REMATCH[2]}" - fix="${BASH_REMATCH[3]}" - - # Print file header if it's a new file - if [ "$file" != "$current_file" ]; then - if [ -n "$current_file" ]; then - echo "-----" - fi - echo -e "\n${YELLOW}$file${NC}" - current_file="$file" - fi - - echo -e " ${CYAN}$line${NC}: โ†’${fix}" - else - echo -e " $suggestion" - fi - fi - done - if [ -n "$current_file" ]; then - echo "-----" - fi - exit 2 -else - echo -e "\n${GREEN}โœ… All files passed documentation compliance check!${NC}" - exit 0 -fi \ No newline at end of file diff --git a/.claude/hooks/quality-config.json b/.claude/hooks/quality-config.json index 4ed4ae7..9ff9980 100644 --- a/.claude/hooks/quality-config.json +++ b/.claude/hooks/quality-config.json @@ -2,8 +2,8 @@ "rules": { "maxFunctionLines": 30, "maxFileLines": 200, - "maxLineLength": 100, - "maxNestingDepth": 4, + "maxLineLength": 140, + "maxNestingDepth": 5, "commentRatioThreshold": 10 }, "ignore": { @@ -22,13 +22,5 @@ "*-lock.json", "*.lock" ] - }, - "magicNumbers": { - "allowed": [0, 1, 2, -1, 10, 100, 1000, 60, 24, 7], - "contexts": { - "httpStatus": [200, 201, 204, 400, 401, 403, 404, 500], - "time": [60, 3600, 86400], - "ports": [80, 443, 3000, 8080] - } } } \ No newline at end of file diff --git a/.claude/settings.json b/.claude/settings.json index 501e002..7e40321 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -5,32 +5,30 @@ "hooks": [ { "type": "command", - "command": "node lib/commands/universal-hook.js" + "command": "python3 hooks/universal-stop.py" } ] } ], "PreToolUse": [ { - "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", - "command": "node lib/hooks/pre-write-analyzer.js" + "command": "python3 hooks/universal-pre-tool.py" } ] } ], "PostToolUse": [ { - "matcher": "Write|Edit|MultiEdit", "hooks": [ { "type": "command", - "command": "node lib/commands/universal-hook.js" + "command": "python3 hooks/universal-post-tool.py" } ] } ] } -} +} \ No newline at end of file diff --git a/.gitignore b/.gitignore index e7615f1..aa14fc2 100644 --- a/.gitignore +++ b/.gitignore @@ -48,6 +48,14 @@ temp/ # Compiled TypeScript output lib/ +# Python cache +__pycache__/ +*.py[cod] +*$py.class +*.pyo +*.pyd +.Python + # CLAUDE.md backup files CLAUDE.md.backup *.CLAUDE.md.backup diff --git a/CLAUDE.md b/CLAUDE.md index 16a2b0d..e03ac92 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,67 +1,34 @@ -# Claude Hooks CLI +# Claude Hooks -A command-line tool for managing Claude Code hooks - validation and quality checks that run automatically within Claude. +A Python-based hook system for Claude Code that provides automatic validation and quality checks. ## Overview -This project provides a CLI tool to easily manage hooks for Claude Code (claude.ai/code). Hooks allow you to run validation, linting, type checking, and other quality checks automatically before certain actions in Claude. - -## Hook Discovery System -The CLI can automatically discover project-specific hooks! Create a `.claude/hooks.json` file in your project: - -```json -{ - "project-lint": { - "event": "PreToolUse", - "matcher": "Bash", - "pattern": "^git\\s+commit", - "description": "Run project-specific linting", - "command": "./scripts/lint.sh" - } -} -``` - -These hooks will appear in the manager with a `[project]` label. +This project provides Python hooks that integrate with Claude Code (claude.ai/code). Hooks allow you to run validation, linting, type checking, and other quality checks automatically during Claude sessions. ## Available Hooks ### Built-in Hooks #### Code Quality -- **typescript-check**: TypeScript type checking before git commits -- **lint-check**: Code linting (ESLint, etc.) before git commits -- **test-check**: Run test suite before various operations - **code-quality-validator**: Enforces clean code standards (function length, nesting, etc.) after file edits #### Package Management - **check-package-age**: Prevents installation of outdated npm/yarn packages #### Notifications -- **task-completion-notify**: System notifications for completed tasks +- **task-completion-notify**: System notifications for completed tasks (optional) -### Project Hooks -Discovered from `.claude/hooks.json` - shown with `[project]` label - -### Custom Hooks -User-added hooks - shown with `[custom]` label - -## Commands -- `npm run build` - Compile TypeScript to JavaScript -- `npm run dev` - Watch mode for development -- `npm run typecheck` - Type check without emitting -- `npm run prepublishOnly` - Build before publishing -- `npm run test` - Run tests (placeholder) -- `npm run lint` - Run linter (placeholder) - -## Dependencies -- chalk@^5.3.0 - Terminal styling -- commander@^11.0.0 - CLI framework -- inquirer@^9.2.15 - Interactive prompts +## Installation +Run the installer to set up hooks in your project: +```bash +python3 install-hooks.py +``` ## Architecture Notes -- Written in TypeScript -- Modular design with separate commands -- Hook validation system -- Interactive UI for hook management -- Supports multiple settings file locations +- Written in Python for portability +- Hook scripts stored in `.claude/hooks/` +- Configuration in `.claude/settings.json` +- Three universal entry points: PreToolUse, PostToolUse, Stop +- Each hook validates specific conditions and provides feedback --- _Manually maintained project documentation_ \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 88702c3..b220729 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -25,80 +25,105 @@ To add a new hook: 1. **Create the hook script** in `hooks/` ```bash - touch hooks/my-new-hook.sh - chmod +x hooks/my-new-hook.sh + touch hooks/my_new_hook.py + chmod +x hooks/my_new_hook.py ``` -2. **Add hook configuration** to `config/settings.example.json` - ```json - { - "matcher": "YourMatcher", - "hooks": [{ - "type": "command", - "command": "./claude/hooks/my-new-hook.sh" - }] - } - ``` - -3. **Create tests** in `tests/` - ```bash - touch tests/test-my-new-hook.sh - chmod +x tests/test-my-new-hook.sh +2. **Integrate with universal hooks** + Add your validation logic to the appropriate universal hook: + - `universal-pre-tool.py` for pre-execution validation + - `universal-post-tool.py` for post-execution checks + - `universal-stop.py` for session completion tasks + +3. **Create tests** for your hook logic + ```python + # tests/test_my_new_hook.py + import unittest + from hooks.my_new_hook import validate_something + + class TestMyNewHook(unittest.TestCase): + def test_validation(self): + # Your test code here + pass ``` 4. **Update documentation** - - Add to the hooks table in README.md + - Add to the hooks list in README.md - Add detailed documentation to docs/README.md ## Code Style -### Shell Scripts -- Use `#!/bin/bash` shebang -- Set `set -euo pipefail` for error handling +### Python Scripts +- Follow PEP 8 style guide +- Use type hints where appropriate +- Add docstrings to functions +- Keep functions under 50 lines - Use meaningful variable names -- Add comments for complex logic -- Follow existing code patterns ### Example Hook Structure -```bash -#!/bin/bash -set -euo pipefail - -# Hook: My New Hook -# Purpose: Does something amazing -# Author: Your Name - -# Configuration -DEFAULT_VALUE="${MY_HOOK_VALUE:-default}" - -# Main logic -main() { - echo "๐Ÿš€ Running my new hook..." - # Your code here -} - -# Run main function -main "$@" +```python +#!/usr/bin/env python3 +""" +Hook: My New Hook +Purpose: Does something amazing +Author: Your Name +""" + +import json +import sys +from typing import Dict, Any + +def validate_something(event_data: Dict[str, Any]) -> bool: + """Validate something based on event data. + + Args: + event_data: The event data from Claude + + Returns: + bool: True if valid, False otherwise + """ + # Your validation logic here + return True + +def main(): + """Main entry point for the hook.""" + try: + # Read event data from stdin + event_data = json.loads(sys.stdin.read()) + + # Perform validation + if not validate_something(event_data): + print("โŒ Validation failed") + sys.exit(1) + + print("โœ… Validation passed") + + except Exception as e: + print(f"โŒ Hook error: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() ``` ## Testing -All hooks must have tests: +Test your hooks thoroughly: ```bash -# Run all tests -cd tests -./test-all-hooks.sh +# Run Python tests +python -m pytest tests/ -# Run specific test -./test-my-new-hook.sh +# Test hook manually +echo '{"tool_name": "Bash", "tool_input": {"command": "test"}}' | python3 hooks/my_new_hook.py ``` ## Pull Request Process -1. Update the README.md with details of changes to the interface -2. Update the VERSION file with the new version number -3. The PR will be merged once you have the sign-off of at least one maintainer +1. Update the README.md with details of changes +2. Update documentation as needed +3. Ensure all tests pass +4. The PR will be merged once you have the sign-off of at least one maintainer ## Any contributions you make will be under the MIT Software License diff --git a/MIGRATION.md b/MIGRATION.md new file mode 100644 index 0000000..63e5842 --- /dev/null +++ b/MIGRATION.md @@ -0,0 +1,109 @@ +# Migration Guide: CLI to Python Hooks + +This guide helps users transition from the previous npm-based CLI version to the new Python-based hook system. + +## What Changed? + +The claude-hooks project has been completely rewritten from a TypeScript/Node.js CLI tool to a lightweight Python-based system. This change brings: + +- **No dependencies** - Pure Python, no npm or node_modules +- **Simpler installation** - Just run one Python script +- **Better portability** - Works on any system with Python 3 +- **Easier customization** - Edit Python files directly + +## Migration Steps + +### 1. Uninstall the Old CLI + +If you have the npm package installed globally: +```bash +npm uninstall -g claude-code-hooks-cli +``` + +If installed locally in your project: +```bash +npm uninstall claude-code-hooks-cli +``` + +### 2. Clean Up Old Files + +Remove any old configuration or dependencies: +```bash +# Remove old node_modules if present +rm -rf node_modules + +# Remove package-lock.json if it only contained claude-hooks +rm package-lock.json +``` + +### 3. Install the New System + +Run the Python installer: +```bash +python3 install-hooks.py +``` + +This creates the same `.claude/` directory structure but with Python hooks instead. + +### 4. Update Your Workflow + +#### Before (CLI Commands): +```bash +claude-hooks init # Initialize hooks +claude-hooks manage # Manage hooks interactively +claude-hooks list # List available hooks +claude-hooks validate # Validate configuration +``` + +#### After (Direct Installation): +```bash +python3 install-hooks.py # One-time setup +# Edit .claude/hooks/*.py files directly for customization +``` + +## Feature Comparison + +| Feature | Old CLI Version | New Python Version | +|---------|----------------|-------------------| +| Installation | `npm install -g` | `python3 install-hooks.py` | +| Dependencies | Node.js, npm, TypeScript | Python 3 only | +| Configuration | Interactive CLI | Direct file editing | +| Hook Discovery | `.claude/hooks.json` | Built into Python files | +| Custom Hooks | CLI commands | Edit Python files | +| Updates | `npm update` | Copy new files | + +## Customization + +### Old Way (CLI): +- Use `claude-hooks manage` to select hooks +- Create `.claude/hooks.json` for project hooks +- Use CLI commands to add/remove hooks + +### New Way (Python): +- Edit `.claude/settings.json` directly +- Modify Python files in `.claude/hooks/` +- Add custom logic to universal hook files + +## Common Questions + +### Q: Why was the CLI removed? +A: The Python implementation is simpler, has no dependencies, and provides the same functionality with less complexity. + +### Q: Will my old hooks still work? +A: The core hooks (code quality, package age, notifications) work exactly the same. You just can't manage them through a CLI anymore. + +### Q: How do I add custom validation? +A: Edit the universal hook files (e.g., `universal-pre-tool.py`) and add your logic directly in Python. + +### Q: Can I still share hooks with my team? +A: Yes! The `.claude/` directory is designed to be committed to version control. + +## Need Help? + +If you encounter issues during migration: +1. Ensure Python 3 is installed: `python3 --version` +2. Check that the installer ran successfully +3. Verify `.claude/settings.json` was created +4. Test hooks are working in Claude Code + +The new system is designed to be simpler and more reliable than the CLI version while providing the same protection and validation features. \ No newline at end of file diff --git a/README.md b/README.md index da5760b..a4791d9 100644 --- a/README.md +++ b/README.md @@ -1,366 +1,106 @@ -# Claude Code Hooks - Your Hook Manager / CLI +# Claude Code Hooks -Professional hook management system for Claude Code - TypeScript-based validation, quality checks, and hook management across environments. Now with automatic code quality enforcement! +A lightweight Python-based hook system for Claude Code that provides automatic validation and quality checks. -[![npm version](https://badge.fury.io/js/claude-code-hooks-cli.svg)](https://www.npmjs.com/package/claude-code-hooks-cli) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +## Overview -## Core Features +Claude Code Hooks allows you to set up automatic validation, quality checks, and notifications that run during your Claude Code sessions. The system uses Python scripts that integrate seamlessly with Claude's hook events. -๐ŸŽฏ **Essential Built-in Hooks** - Pre-built validation and quality hooks ready to use -๐Ÿ”ง **Hook Management Tool** - Easy add/remove hooks across different Claude environments -๐Ÿ“ **Multi-Environment Support** - Manage hooks for local, global, project, and team settings -๐Ÿ›ก๏ธ **Settings Validation** - Automatic validation when loading/saving hook configurations -โœ… **CLI Validation Command** - `claude-hooks validate` to check settings files -โšก **TypeScript-Powered** - Full type safety with modern JavaScript features -๐ŸŽฎ **Interactive CLI** - `claude-hooks` command for all hook management needs -๐Ÿšฆ **Checkpoint Workflows** - Stop Claude and enforce quality gates with JSON output +## Features -## Installation - -```bash -npm install -D claude-code-hooks-cli -``` - -## Getting Started - -### Quick Setup (5 seconds) -```bash -npm install -g claude-code-hooks-cli -claude-hooks init -``` - -### Interactive Setup Flow - -When you run `claude-hooks init`: - -**1. Choose setup mode:** -``` -How would you like to set up hooks? - -โฏ Quick setup (recommended defaults) - Custom setup (choose your hooks) -``` +- ๐Ÿ **Python-based** - Simple, portable hook implementation +- โœ… **Code Quality Validation** - Enforces clean code standards +- ๐Ÿ“ฆ **Package Age Checking** - Prevents installation of outdated packages +- ๐Ÿ”” **Task Completion Notifications** - Get notified when Claude finishes tasks +- ๐ŸŽฏ **Easy Installation** - One command setup -**2. Select where to save settings:** -``` -Where would you like to create the settings file? - -โฏ Project (.claude/settings.json) - Team hooks, committed to git - Global (~/.claude/settings.json) - Your default hooks - Local (.claude/settings.local.json) - Personal hooks, git ignored -``` - -That's it! Your hooks are now protecting your Claude Code sessions. - -### Advanced: Hook Manager - -For custom hook configuration, use the interactive manager: +## Installation ```bash -claude-hooks manage -``` - -**Location Selection Screen:** -``` -Claude Hooks Manager - -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -Hook Name Calls Last Called -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ -typescript-check 12 2 minutes ago -code-quality-validator 8 5 minutes ago -check-package-age 3 1 hour ago -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -โ†‘/โ†“: Navigate Enter: Select Q/Esc: Exit - -โฏ Project (.claude/settings.json) (3 hooks) - Team hooks, committed to git - Local (.claude/settings.local.json) (0 hooks) - Personal hooks, git ignored - Global (~/.claude/settings.json) (0 hooks) - Your default hooks - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - ๐Ÿ“‹ View recent logs - ๐Ÿ“Š Tail logs (live) - โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - โœ• Exit -``` - -**Hook Selection Screen:** -``` -Hook Manager - -โ†‘/โ†“: Navigate Enter: Toggle & Save A: Select all D: Deselect all Q/Esc: Quit - -โฏโ—‰ typescript-check (PreToolUse) - โ—‰ code-quality-validator (PostToolUse) - โ—‰ check-package-age (PreToolUse) - โ—ฏ lint-check (PreToolUse) - โ—ฏ test-check (PreToolUse) - โ—ฏ task-completion-notify (Stop) - -โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - -Description: TypeScript type checking before git commits +python3 install-hooks.py ``` +This will: +1. Create a `.claude/` directory in your project +2. Copy all hook scripts to `.claude/hooks/` +3. Generate `.claude/settings.json` with hook configurations +4. Add `.claude/settings.local.json` to your `.gitignore` ## How It Works -All hooks run directly from the npm package via TypeScript commands. Your `.claude/settings.json` will contain commands like: - -```json -{ - "hooks": { - "Stop": [{ - "hooks": [{ - "type": "command", - "command": "npx claude-code-hooks-cli exec stop-validation" - }] - }] - } -} -``` - -## Writing Custom Hooks - -Want to create your own hooks? Check out: -- ๐Ÿ“– **[Hook Development Guide](docs/HOOK-DEVELOPMENT.md)** - Complete guide with event data structures -- ๐Ÿ’ก **[Example Hooks](examples/hooks/)** - Working examples: command logger, file validator, multi-event monitor -- ๐Ÿ”ง **[Entry Points Docs](docs/ENTRY-POINTS.md)** - How the universal hook system works - -Quick example of a custom hook: -```javascript -#!/usr/bin/env node -// Read event data from stdin -let input = ''; -process.stdin.on('data', chunk => input += chunk); -process.stdin.on('end', () => { - const data = JSON.parse(input); - console.log(`Event: ${data.hook_event_name}`); - - if (data.tool_name === 'Bash') { - console.log(`Command: ${data.tool_input.command}`); - } -}); -``` - -## Available Hooks - -### Core Validation Hooks -- **typescript-check** - Validates TypeScript code for type errors -- **lint-check** - Runs ESLint/Prettier checks on your code -- **test-check** - Executes your test suite to ensure code quality -- **code-quality-validator** - Enforces clean code standards on file edits - -### Utility Hooks -- **check-package-age** - Prevents installation of outdated npm/yarn packages -- **task-completion-notify** - Notifies when tasks are completed +The hook system uses three main entry points that Claude Code calls: +- **PreToolUse** - Runs before tools like Bash, Write, or Edit are executed +- **PostToolUse** - Runs after tools complete +- **Stop** - Runs when Claude stops or completes a task -### Project-Specific Hooks -The hook manager can now automatically discover hooks defined in your project! Create a `.claude/hooks.json` file to share hook templates with your team: - -```json -{ - "project-lint": { - "event": "PreToolUse", - "matcher": "Bash", - "pattern": "^git\\s+commit", - "description": "Run project-specific linting rules", - "command": "./scripts/lint.sh" - }, - "security-scan": { - "event": "PreToolUse", - "matcher": "Bash", - "pattern": "^npm\\s+(install|i)", - "description": "Scan dependencies for vulnerabilities", - "command": "./scripts/security-check.sh" - } -} -``` - -These hooks will automatically appear in the hook manager with a `[project]` label! - -Run `claude-hooks list` to see all available hooks. - -## Commands - -### `claude-hooks init` -**Initialize Claude hooks** - TypeScript-powered interactive setup. - -**Interactive mode** (default): -1. First prompts for setup mode: - - **Quick setup** - Installs recommended hooks (4 defaults) - - **Custom setup** - Opens interactive manager to add/remove hooks - -2. Then prompts for location (shows existing hook counts) - -**Direct mode** with `--level `: -- Goes straight to quick setup at specified location -- `project` - `.claude/settings.json` (recommended) -- `local` - `.claude/settings.local.json` -- `global` - `~/.claude/settings.json` - -### `claude-hooks manage` -**Interactive hook manager** - TypeScript-based configuration interface. - -Same as running `init` and choosing "Custom setup". Use this when you want to skip the setup mode prompt. - -### `claude-hooks list` -Show all available hooks with descriptions (TypeScript compiled). - -### `claude-hooks stats` -Display hook performance statistics and success rates. - -### `claude-hooks logs [--follow]` -View hook execution logs. Use `--follow` for live monitoring. - -### `claude-hooks validate [path]` -**Validate hook settings files** - Ensures configurations are properly structured. - -Validates JSON syntax, hook structure, event names, tool matchers, and regex patterns. - -```bash -# Validate all settings files -claude-hooks validate - -# Validate specific file -claude-hooks validate claude/settings.json - -# Show detailed validation information -claude-hooks validate -v -``` - -The validator checks: -- โœ… Valid JSON syntax and structure -- โœ… Correct event names (PreToolUse, PostToolUse, Stop) -- โœ… Valid tool matchers (Bash, Write, Edit, etc.) -- โœ… Proper regex pattern syntax -- โœ… Required fields and types -- โœ… Logging configuration (if present) - -### `claude-hooks exec ` -Execute a specific hook. This is used internally by Claude Code. - -*Note: All commands are also available with the full name `claude-code-hooks-cli`* +Each hook receives event data via stdin and can: +- Provide suggestions and warnings +- Block operations that violate policies +- Send notifications +- Log activities ## Available Hooks -### Built-in Hooks - -- **typescript-check** - TypeScript type checking before git commits -- **lint-check** - Code linting (ESLint, etc.) before git commits -- **test-check** - Run test suite before various operations -- **code-quality-validator** - Enforces clean code standards after file edits -- **check-package-age** - Prevents installation of outdated npm/yarn packages -- **task-completion-notify** - System notifications when Claude finishes (Stop event) +### Code Quality Validator +Enforces clean code standards on file edits: +- Maximum function length (30 lines) +- Maximum file length (200 lines) +- Maximum line length (100 characters) +- Maximum nesting depth (4 levels) -### Project Hooks +### Package Age Checker +Prevents installation of outdated npm/yarn packages: +- Blocks packages older than 180 days (configurable) +- Shows latest available versions +- Runs on `npm install` and `yarn add` commands -You can discover project-specific hooks by creating `.claude/hooks.json` in your project. +### Task Completion Notifier +Sends notifications when Claude completes tasks: +- Pushover support for mobile notifications +- macOS native notifications +- Linux desktop notifications -## Configuration Levels +## Configuration -- **Project** (`.claude/settings.json`) - Shared with your team, committed to git (recommended) -- **Local** (`.claude/settings.local.json`) - Personal settings, git ignored -- **Global** (`~/.claude/settings.json`) - Applies to all your projects +### Environment Variables +- `MAX_AGE_DAYS` - Maximum age for packages (default: 180) +- `CLAUDE_HOOKS_TEST_MODE` - Enable test mode +- `PUSHOVER_USER_KEY` - Your Pushover user key +- `PUSHOVER_APP_TOKEN` - Your Pushover app token -## Checkpoint Workflows (New!) - -Hooks can now create "quality gates" that stop Claude from continuing until issues are resolved. This enables checkpoint-based workflows using JSON output: - -```json -{ - "continue": false, - "stopReason": "Quality checks failed - see below", - "decision": "block", - "reason": "Please fix the failing tests before completing" -} +### Pushover Setup +1. Get the Pushover app ($5 one-time): https://pushover.net/clients +2. Create an app at: https://pushover.net/apps/build +3. Add to your `.env` file: ``` - -### Example: Stop on Errors -```bash -# In your Stop event hook -if [ $ERROR_COUNT -gt 0 ]; then - echo '{"continue": false, "stopReason": "Errors must be fixed"}' - exit 0 -fi +PUSHOVER_USER_KEY=your_user_key +PUSHOVER_APP_TOKEN=your_app_token ``` -### Use Cases -- **Enforce Testing** - Stop if tests haven't been run -- **Require Documentation** - Block completion without docs -- **Quality Standards** - Enforce linting, formatting, etc. -- **Error Prevention** - Stop when errors are detected - -See [Stop Hooks Guide](./docs/stop-hooks-guide.md) for detailed documentation. - -## What's New - -### v2.4.0 (Latest) -- ๐Ÿ” **Hook Discovery System** - Automatically finds project hooks in `.claude/hooks.json` -- ๐Ÿท๏ธ **Hook Source Labels** - Visual indicators for built-in, project, and custom hooks -- ๐Ÿ“‚ **Project Hook Templates** - Share team-specific hooks via version control -- ๐Ÿ›ก๏ธ **Template Validation** - Automatic validation of discovered hook templates -- ๐ŸŽฏ **Relative Path Support** - Project hooks can use relative script paths - -### v2.3.0 -- ๐ŸŽฏ **Simplified hook system** - Consolidated to essential validation hooks -- ๐Ÿ›ก๏ธ **Improved error handling** - Better exit codes and error messages -- ๐Ÿ“ **Common validation library** - Shared functionality for consistency -- ๐Ÿš€ **Enhanced CLI commands** - Better hook management experience -- ๐Ÿงน **Removed deprecated hooks** - Cleaner, more focused hook set -- โœ… **Hook Settings Validation** - Automatic validation prevents invalid configurations -- ๐Ÿ“‹ **Validate Command** - New `claude-hooks validate` command for checking settings +## Hook Architecture -## Benefits - -โœ… **TypeScript-powered** - Full type safety and modern JavaScript features -โœ… **Always up-to-date** - Just run `npm update claude-code-hooks-cli` -โœ… **No file management** - Everything runs from node_modules -โœ… **Version locked** - Consistent behavior via package.json -โœ… **Works everywhere** - Compatible with npm, yarn, pnpm -โœ… **Interactive CLI** - Modern command-line interface with prompts -โœ… **Project-local config** - Uses `claude/` directory (not `~/.claude`) - -## Roadmap - -### ๐Ÿš€ Coming Soon - -- **Hook Package Manager** - Discover and import hooks from GitHub repositories -- **Advanced Validation System** - Comprehensive hook validation with security checks and auto-fix -- **Task Completion Enforcement** - Hooks that prevent Claude from exiting before completing all tasks -- **Package Similarity Detection** - Prevents installing duplicate packages by detecting similar existing dependencies -- **Method Similarity Indexer** - Prevents duplicate code by detecting similar methods across the repository -- **Continuous UI Improvement** - Automated UI enhancement using visual feedback and design analysis -- **Prompt Optimization System** - Continuous AI prompt refinement based on conversation metrics -- **Shiny Windows Delight** - Perpetual UI enhancement system for adding delightful micro-interactions - -### ๐ŸŽฏ Planned Features - -- **Repo-Based Hook Discovery** - Automatically find and catalog hooks in visited repositories -- **Hook Marketplace** - Community-driven hook sharing and rating system -- **AI-Powered Hook Suggestions** - Recommend hooks based on project analysis -- **Multi-Armed Bandit Testing** - A/B test different hook configurations automatically -- **Visual Hook Designer** - GUI for creating hooks without coding - -See our [ideas documentation](./docs/ideas-for-hooks/) for detailed specifications of upcoming features. +The system uses a dispatcher pattern: +- `universal-*.py` - Main dispatchers that route to specific hooks +- Individual hook files handle specific functionality +- All hooks use JSON for input/output communication ## Development -This project uses TypeScript. To develop: +### Adding New Hooks +1. Create a new Python script in `hooks/` +2. Read JSON input from stdin +3. Output JSON response with `action` field +4. Register in the appropriate universal dispatcher +### Testing Hooks ```bash -npm install -npm run dev # Watch mode -npm run build # Compile TypeScript -``` - -Source files are in `src/` and compiled to `lib/`. +# Test with sample input +echo '{"tool_name": "Write", "file_path": "test.py"}' | python3 hooks/post-tool-hook.py -## Contributing - -Contributions are welcome! This is a TypeScript project with modern tooling. +# Enable test mode +export CLAUDE_HOOKS_TEST_MODE=1 +``` ## License -MIT +MIT License - See LICENSE file for details \ No newline at end of file diff --git a/bin/claude-hooks.js b/bin/claude-hooks.js deleted file mode 100755 index 3373731..0000000 --- a/bin/claude-hooks.js +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env node - -// This file is generated from src/cli.ts -// Run `npm run build` to regenerate -import '../lib/cli.js'; \ No newline at end of file diff --git a/claude-hooks b/claude-hooks new file mode 100755 index 0000000..9284e78 --- /dev/null +++ b/claude-hooks @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +"""Claude Hooks CLI wrapper - runs from project root.""" +import sys +import os + +# Add hooks directory to path +sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'hooks')) + +# Import and run the CLI +from claude_hooks_cli import main + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/docs/CLEAN-CODE-GUIDE.md b/docs/CLEAN-CODE-GUIDE.md index 0753964..0a19b6f 100644 --- a/docs/CLEAN-CODE-GUIDE.md +++ b/docs/CLEAN-CODE-GUIDE.md @@ -6,21 +6,20 @@ This system enforces Uncle Bob's Clean Code principles and promotes code reuse t The system consists of three main hooks that work together: -1. **Pre-Hook (code-quality-primer.sh)**: Reminds about Clean Code principles and checks for existing code -2. **Post-Hook (code-quality-validator.sh)**: Validates code against Clean Code rules -3. **Code Index (build-code-index.sh)**: Maintains searchable index of functions/utilities +1. **Pre-Hook (code quality validation in universal-post-tool.py)**: Validates code against Clean Code rules +2. **Code Quality Validator (code_quality_validator.py)**: Enforces clean code standards +3. **Validation Library (validators.py)**: Shared validation functions ## Clean Code Rules Based on `clean-code-rules.json`: ### Size Limits -- **Functions**: Max 20 lines (promotes single responsibility) -- **Files**: Max 100 lines (150 for components) -- **Classes**: Max 5 public methods -- **Parameters**: Max 3 per function -- **Nesting**: Max 3 levels deep -- **Line Length**: Max 80 characters +- **Functions**: Max 50 lines (promotes single responsibility) +- **Files**: Max 300 lines +- **Nesting**: Max 4 levels deep +- **Line Length**: Max 120 characters +- **Cyclomatic Complexity**: Max 10 ### Code Quality Checks - **Magic Numbers**: Must use named constants (except 0, 1, -1) @@ -54,10 +53,7 @@ The validator hook checks: - Repeated patterns ### 3. Code Index -Build the index periodically: -```bash -./claude/hooks/build-code-index.sh -``` +The Python-based validator runs automatically on file edits. This creates a searchable database of: - All exported functions with locations @@ -85,16 +81,13 @@ This creates a searchable database of: ## Customizing Rules -Edit `./claude/hooks/clean-code-rules.json` to adjust thresholds: +Edit the validation constants in `hooks/validators.py` to adjust thresholds: -```json -{ - "rules": { - "maxFunctionLines": 20, // Adjust function size limit - "maxFileLines": 100, // Adjust file size limit - "similarityThreshold": 0.8 // How similar before warning - } -} +```python +# In validators.py +MAX_FUNCTION_LENGTH = 50 # Adjust function size limit +MAX_FILE_LENGTH = 300 # Adjust file size limit +MAX_NESTING_DEPTH = 4 # Adjust nesting limit ``` ## Best Practices @@ -109,23 +102,20 @@ Edit `./claude/hooks/clean-code-rules.json` to adjust thresholds: ## Quick Commands ```bash -# Build/rebuild the code index -./claude/hooks/build-code-index.sh - -# Look up a function quickly -./claude/hooks/lookup-function.sh formatDate +# Test the validator manually +echo '{"tool_name": "Edit", "file_path": "test.py"}' | python3 .claude/hooks/universal-post-tool.py -# Check similarity of code snippet -echo "function debounce(fn, delay) {}" | ./claude/hooks/code-similarity-check.sh - +# Run the installer to update hooks +python3 install-hooks.py ``` ## Troubleshooting ### Hooks not triggering -Ensure your `./claude/settings.json` includes the hook configuration. +Ensure your `.claude/settings.json` includes the hook configuration. Run `python3 install-hooks.py` to set up. ### Too many false positives -Adjust thresholds in `clean-code-rules.json`. +Adjust thresholds in `hooks/validators.py`. ### Index out of date Run the indexer more frequently or add it to a cron job. diff --git a/docs/README.md b/docs/README.md index 8a8c499..ebd53b6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,276 +1,81 @@ -# Claude Code Hooks Collection +# Claude Code Hooks Documentation -A comprehensive TypeScript-based set of hooks designed to improve code quality and developer experience when using Claude Code. +Documentation and guides for the Claude Code Hooks system. -## ๐ŸŽฏ Modern TypeScript Architecture +## Overview -Claude Hooks are now built with TypeScript and modern tooling: -- โœ… **TypeScript-powered** - Full type safety and modern JavaScript features -- โœ… **NPM package** - Install via `npm install -D claude-code-hooks-cli` -- โœ… **Interactive CLI** - Modern command-line interface with prompts -- โœ… **Project-local config** - Uses `claude/` directory structure -- โœ… **Automatic logging** - All executions logged to `./claude/logs/hooks.log` -- โœ… **Smart defaults** - Package validation, code quality checks pre-configured +Claude Code Hooks is a Python-based validation system that provides automatic quality checks and validation for Claude Code sessions. The system uses lightweight Python scripts that integrate directly with Claude's hook events. -**Installation is now a single npm command followed by `npx claude-code-hooks-cli init`.** +## Documentation Structure -## Quick Start +- **[CLEAN-CODE-GUIDE.md](CLEAN-CODE-GUIDE.md)** - Best practices for clean code +- **[ideas-for-hooks/](ideas-for-hooks/)** - Ideas and proposals for future hooks +- **[../MIGRATION.md](../MIGRATION.md)** - Guide for migrating from the old CLI version -### NPM Installation (Recommended) -Install as a development dependency: -```bash -npm install -D claude-code-hooks-cli -npx claude-code-hooks-cli init -``` - -### Legacy Installation Methods -These methods are still supported but not recommended: - -#### Option 1: User-Level Installation -```bash -# Clone the repository -git clone https://github.com/your-org/claude-hooks.git -cd claude-hooks - -# Run the installation script -./scripts/install.sh -``` - -#### Option 2: Project-Level Installation -```bash -# In your project directory -./path/to/claude-hooks/scripts/install-project.sh -``` +## Hook System Architecture -#### Option 3: Manual Installation -```bash -# Copy hooks to your Claude directory -cp -r hooks ./claude/hooks -cp config/settings.example.json ./claude/settings.json -chmod +x ./claude/hooks/*.sh -``` - -## Installation Methods - -### NPM Package (Recommended) -- **Command**: `npm install -D claude-code-hooks-cli` -- **Location**: Runs from `node_modules/`, config in `claude/settings.json` -- **Scope**: Per-project via package.json -- **Setup**: Run `npx claude-code-hooks-cli init` -- **Customization**: Interactive CLI or edit `claude/settings.json` -- **Benefits**: Version-locked, always up-to-date, TypeScript-powered - -### Legacy Methods (Still Supported) - -#### User-Level Hooks -- **Location**: `./claude/hooks/` and `./claude/settings.json` -- **Scope**: Apply to all your projects -- **Setup**: Run `./scripts/install.sh` -- **Customization**: Edit `./claude/settings.json` directly - -#### Project-Level Hooks (Team Use) -- **Location**: `project/claude/hooks/` and `project/claude/settings.json` -- **Scope**: Apply to all team members automatically -- **Setup**: Use git submodule via `./scripts/install-project.sh` -- **Customization**: Edit project-specific settings +The hook system uses three main entry points: +- **PreToolUse** - Validates operations before execution +- **PostToolUse** - Checks results after operations complete +- **Stop** - Runs when Claude stops or completes tasks ## Available Hooks -### 1. Package Age Validator (`check-package-age.sh`) -**Purpose**: Prevents installation of outdated npm/yarn packages - -**Features**: -- Blocks packages older than 6 months (configurable) -- Suggests latest versions -- Works with npm and yarn - -**Configuration**: -```bash -export MAX_AGE_DAYS=180 # Default: 180 days +### Code Quality Validator +Enforces clean code standards on file edits: +- Maximum function length (50 lines) +- Maximum file length (300 lines) +- Maximum nesting depth (4 levels) +- Cyclomatic complexity limits + +### Package Age Checker +Prevents installation of outdated npm/yarn packages: +- Blocks packages older than 5 years +- Warns for packages older than 3 years +- Suggests modern alternatives + +### Task Completion Notifier +Optional system notifications when Claude completes tasks. + +## Writing Custom Hooks + +Hooks are Python scripts that receive event data via stdin and can: +1. Validate conditions +2. Provide warnings or suggestions +3. Block operations if needed +4. Send notifications + +Example hook structure: +```python +import json +import sys + +# Read event data +data = json.loads(sys.stdin.read()) + +# Process based on event type +if data.get('tool_name') == 'Bash': + command = data.get('tool_input', {}).get('command', '') + # Add validation logic here ``` -### 2. Code Quality Primer (`code-quality-primer.sh`) -**Purpose**: Pre-write hook that promotes Clean Code principles - -**Features**: -- Injects language-specific Clean Code reminders -- Checks for existing similar functions -- Suggests utilities from common libraries -- Prevents code duplication - -**Triggers**: Before Write/Edit/MultiEdit operations - -### 3. Code Quality Validator (`code-quality-validator.sh`) -**Purpose**: Post-write hook that validates code against Clean Code rules - -**Validates**: -- Function length (max 20 lines) -- File length (max 100 lines, 150 for components) -- Nesting depth (max 3 levels) -- Line length (max 80 characters) -- Magic numbers -- Comment ratio -- Code duplication patterns - -**Triggers**: After Write/Edit/MultiEdit operations - -### 4. Code Similarity Checker (`code-similarity-check.sh`) -**Purpose**: Utility to detect similar code patterns - -**Features**: -- Pattern detection for common implementations -- Suggests library alternatives -- Calculates similarity scores - -**Usage**: -```bash -./claude/hooks/code-similarity-check.sh "function content" ts -``` - -### 5. Task Completion Notifier (`task-completion-notify.sh`) -**Purpose**: Sends system notifications for completed tasks - -**Notifications for**: -- File operations (create/update/delete) -- Git operations (commit/push) -- Build/test completions -- Todo completions -- Session completion - -**Platform Support**: -- macOS: Native notifications with sound -- Linux: notify-send notifications -- Other: Terminal bell - -### 6. Code Index Builder (`build-code-index.sh`) -**Purpose**: Creates searchable index of codebase - -**Indexes**: -- Exported functions with locations -- React components -- Custom hooks -- TypeScript types and interfaces -- Utility directories - -**Usage**: -```bash -./claude/hooks/build-code-index.sh # Full index (slower) -./claude/hooks/quick-index.sh # Quick statistics -``` - -### 8. Claude Context Updater (`claude-context-updater.sh`) -**Purpose**: Automatically maintains CLAUDE.md files based on code changes - -**Features**: -- Runs when Claude Code session ends -- Detects directories needing CLAUDE.md files -- Suggests updates to existing CLAUDE.md files -- Tracks package.json, config, and structural changes -- Creates review proposals in `.claude-updates/` - -**Configuration**: -```bash -export ENABLE_CONTEXT_UPDATER=true # Enable/disable hook -export AUTO_CREATE_CLAUDE_MD=true # Create new CLAUDE.md files -export UPDATE_EXISTING_CLAUDE_MD=true # Update existing files -export CLAUDE_UPDATES_DIR=.claude-updates # Proposal directory -``` - -**Triggers**: When Claude Code stops/exits - -**Review Process**: -1. Check `.claude-updates/session_*/` for proposals -2. Review CREATE_* files for new CLAUDE.md suggestions -3. Review UPDATE_* files for update suggestions -4. Apply changes manually as appropriate - -## Configuration Files - -### clean-code-rules.json -Customizable thresholds and rules: -```json -{ - "rules": { - "maxFunctionLines": 20, - "maxFileLines": 100, - "maxNestingDepth": 3, - "maxParameters": 3 - } -} -``` - -### settings.example.json -Example Claude settings with all hooks configured. - -## Customization - -### For NPM Package (Recommended) -1. **Interactive CLI**: Run `npx claude-code-hooks-cli manage` -2. **Edit Settings**: Modify `claude/settings.json` directly -3. **Custom Rules**: Edit `claude/hooks/clean-code-rules.json` -4. **Remove Hooks**: Use CLI manager or comment out hook entries -5. **Environment Variables**: Set variables like `ENABLE_CODE_QUALITY_VALIDATOR=false` - -### For Legacy Installations - -#### User-Level Hooks -1. **Edit Settings**: Modify `./claude/settings.json` directly -2. **Custom Rules**: Edit `./claude/hooks/clean-code-rules.json` -3. **Remove Hooks**: Comment out or delete hook entries in your settings.json -4. **Environment Variables**: Set variables like `ENABLE_CODE_QUALITY_VALIDATOR=false` - -#### Project-Level Hooks -1. **Edit Project Settings**: Modify `project/claude/settings.json` -2. **Custom Rules**: Edit `project/claude/hooks/clean-code-rules.json` (affects all team members) -3. **Environment Variables**: Set in project environment or CI/CD - -### Disabling Hooks -To temporarily disable hooks: -1. **All hooks**: Rename or remove settings.json file -2. **Specific hooks**: Set environment variables like `ENABLE_PACKAGE_AGE_CHECK=false` -3. **Per-project**: Override in project-specific settings - -### Adding Custom Patterns -Edit similarity checker to add project-specific patterns. - -## Troubleshooting - -### Hooks Not Triggering -1. **NPM Package**: Verify `claude/settings.json` exists and contains `npx claude-code-hooks-cli exec` commands -2. **Legacy**: Verify `./claude/settings.json` exists -3. **Legacy**: Check hook paths are correct -4. **Legacy**: Ensure hooks are executable: `chmod +x ./claude/hooks/*.sh` -5. **All**: Run `npx claude-code-hooks-cli list` to verify package installation - -### Performance Issues -- Use `quick-index.sh` instead of full indexer -- Adjust validation rules to be less strict -- Disable real-time validation for large files - -### False Positives -- Customize rules in `clean-code-rules.json` -- Add exceptions for specific patterns -- Use environment variables to override defaults - ## Best Practices -1. **Run indexer regularly**: Keep code index up to date -2. **Review suggestions**: Don't blindly follow all recommendations -3. **Customize for your project**: Adjust rules to match team standards -4. **Share configurations**: Commit custom rules to the repository +1. **Keep hooks fast** - They run synchronously during Claude operations +2. **Provide helpful feedback** - Clear messages help users understand issues +3. **Use warnings before blocking** - Allow users to understand and fix issues +4. **Log for debugging** - But avoid excessive output ## Contributing -To add new hooks: -1. **TypeScript**: Create hook command in `src/commands/` -2. **Legacy**: Create hook script in `hooks/` directory -3. Add to `settings.example.json` -4. Update setup script and CLI -5. Document in this README -6. Run `npm run build` to compile TypeScript +To contribute new hooks or improvements: +1. Create Python scripts following the existing patterns +2. Test thoroughly with various inputs +3. Document the hook's purpose and configuration +4. Submit a pull request with examples ## Resources -- [Clean Code book](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) by Robert C. Martin -- [SOLID principles](https://en.wikipedia.org/wiki/SOLID) -- [DRY principle](https://en.wikipedia.org/wiki/Don%27t_repeat_yourself) \ No newline at end of file +- [Clean Code principles](https://www.amazon.com/Clean-Code-Handbook-Software-Craftsmanship/dp/0132350882) +- [Python best practices](https://docs.python-guide.org/) +- [Claude Code documentation](https://claude.ai/code) \ No newline at end of file diff --git a/docs/ideas-for-hooks/hook-package-manager.md b/docs/ideas-for-hooks/hook-package-manager.md index da33a71..8c69f37 100644 --- a/docs/ideas-for-hooks/hook-package-manager.md +++ b/docs/ideas-for-hooks/hook-package-manager.md @@ -2,7 +2,9 @@ ## Overview -A package management system for Claude hooks that enables discovering, sharing, and importing hooks from various repositories. This creates an ecosystem where developers can share their best Claude automation patterns and quickly adopt proven workflows. +A proposed package management system for Claude hooks that would enable discovering, sharing, and importing hooks from various repositories. This would create an ecosystem where developers can share their best Claude automation patterns and quickly adopt proven workflows. + +**Note: This is a conceptual proposal. The current claude-hooks implementation uses Python scripts installed via `python3 install-hooks.py`.** ## Problem Statement @@ -17,49 +19,49 @@ Currently, Claude hooks are: ### 1. Hook Discovery System Automatically discovers hooks in repositories: -```typescript -interface HookSource { - type: 'built-in' | 'local' | 'remote' | 'registry'; - location: string; - hooks: HookDefinition[]; - metadata: { - author: string; - version: string; - description: string; - tags: string[]; - }; -} +```python +# Conceptual structure for hook sources +class HookSource: + def __init__(self, type, location, hooks, metadata): + self.type = type # 'built-in', 'local', 'remote', 'registry' + self.location = location + self.hooks = hooks + self.metadata = metadata ``` ### 2. Hook Registry Central catalog of available hooks: -```typescript -interface HookRegistry { - version: string; - sources: HookSource[]; - installed: InstalledHook[]; - available: AvailableHook[]; - featured: FeaturedHook[]; -} +```python +# Conceptual registry structure +class HookRegistry: + def __init__(self): + self.version = "1.0.0" + self.sources = [] + self.installed = [] + self.available = [] + self.featured = [] ``` ### 3. Import/Export Mechanism Simple commands for hook management: ```bash +# Conceptual commands (not yet implemented) +# These would be future enhancements to the Python-based system + # Import a specific hook from a repository -claude-hooks import user/repo:pre-commit-quality +python3 claude-hooks-manager.py import user/repo:pre-commit-quality # Import all hooks from a repository -claude-hooks import user/repo +python3 claude-hooks-manager.py import user/repo # Export hooks for sharing -claude-hooks export my-awesome-hooks +python3 claude-hooks-manager.py export my-awesome-hooks # Search available hooks -claude-hooks search "typescript" +python3 claude-hooks-manager.py search "quality" # List installed hooks -claude-hooks list +python3 claude-hooks-manager.py list ``` ## Implementation @@ -92,12 +94,13 @@ hooks: ### Discovery Patterns The system looks for hooks in these locations: -```typescript -const DISCOVERY_PATTERNS = [ - '.claude-hooks/**/*.{js,ts,sh}', - 'claude/hooks/**/*.{js,ts,sh}', - '.claude/hooks/**/*.{js,ts,sh}', - 'hooks/claude/**/*.{js,ts,sh}', +```python +# Discovery patterns for Python-based hooks +DISCOVERY_PATTERNS = [ + '.claude-hooks/**/*.py', + 'claude/hooks/**/*.py', + '.claude/hooks/**/*.py', + 'hooks/claude/**/*.py', '.github/claude-hooks/**/*.{js,ts,sh}' ]; ``` diff --git a/docs/python-standards.md b/docs/python-standards.md new file mode 100644 index 0000000..cdd5b7d --- /dev/null +++ b/docs/python-standards.md @@ -0,0 +1,74 @@ +# Python Coding Standards + +## General Principles +- Follow PEP 8 style guide +- Use type hints for better code clarity +- Write self-documenting code with clear variable names +- Keep functions focused on a single responsibility + +## Naming Conventions +- Use snake_case for variables and functions +- Use PascalCase for classes +- Use UPPER_SNAKE_CASE for constants +- Use descriptive names that explain purpose + +## Functions +- Keep functions under 50 lines +- Limit function parameters to 5 maximum +- Use keyword arguments for optional parameters +- Document complex functions with docstrings + +## Error Handling +- Always handle exceptions appropriately +- Use specific exception types +- Provide meaningful error messages +- Log errors for debugging + +## Code Organization +- One class per file when it makes sense +- Group related functionality in modules +- Keep files under 300 lines +- Use `__init__.py` for clean imports + +## Hook-Specific Guidelines +- Read event data from stdin +- Output messages to stdout +- Use exit codes: 0 for success, 1 for blocking +- Keep hooks fast and focused + +## Example +```python +# Good +import json +import sys +from typing import Dict, Any + +def validate_command(event_data: Dict[str, Any]) -> bool: + """Validate a command before execution. + + Args: + event_data: The event data from Claude + + Returns: + bool: True if command is valid, False otherwise + """ + tool_name = event_data.get('tool_name', '') + if tool_name != 'Bash': + return True + + command = event_data.get('tool_input', {}).get('command', '') + if 'rm -rf /' in command: + print("โŒ Dangerous command blocked") + return False + + return True + +# Bad +def validate(data): + if data['tool_name'] == 'Bash': + cmd = data['tool_input']['command'] + if 'rm -rf /' in cmd: + print("bad") + return False + return True +``` \ No newline at end of file diff --git a/docs/typescript-standards.md b/docs/typescript-standards.md deleted file mode 100644 index 6866c0d..0000000 --- a/docs/typescript-standards.md +++ /dev/null @@ -1,55 +0,0 @@ -# TypeScript Coding Standards - -## General Principles -- Use TypeScript strict mode -- Avoid `any` type unless absolutely necessary -- Prefer interfaces over type aliases for object shapes - -## Naming Conventions -- Use camelCase for variables and functions -- Use PascalCase for classes and interfaces -- Use UPPER_SNAKE_CASE for constants -- Prefix interfaces with 'I' only when necessary to avoid naming conflicts - -## Functions -- Always specify return types explicitly -- Use arrow functions for callbacks and short functions -- Limit function parameters to 3-4 maximum -- Document complex functions with JSDoc comments - -## Error Handling -- Always handle errors appropriately -- Use try-catch blocks for async operations -- Provide meaningful error messages -- Never silently swallow errors - -## Code Organization -- One class/interface per file when possible -- Group related functionality in modules -- Keep files under 300 lines -- Use barrel exports (index.ts) for clean imports - -## Example -```typescript -// Good -interface User { - id: string; - name: string; - email: string; -} - -async function fetchUser(userId: string): Promise { - try { - const response = await api.get(`/users/${userId}`); - return response.data; - } catch (error) { - throw new Error(`Failed to fetch user ${userId}: ${error.message}`); - } -} - -// Bad -async function fetchUser(id) { - const response = await api.get(`/users/${id}`); - return response.data; -} -``` \ No newline at end of file diff --git a/hooks/__pycache__/package_utils.cpython-313.pyc b/hooks/__pycache__/package_utils.cpython-313.pyc new file mode 100644 index 0000000..06aaabe Binary files /dev/null and b/hooks/__pycache__/package_utils.cpython-313.pyc differ diff --git a/hooks/check-package-age.py b/hooks/check-package-age.py new file mode 100755 index 0000000..25b65ab --- /dev/null +++ b/hooks/check-package-age.py @@ -0,0 +1,196 @@ +#!/usr/bin/env python3 +"""Claude Code Hook: Prevent installation of outdated packages.""" + +import json +import sys +import re +from datetime import datetime, timezone +import os + +from package_utils import ( + parse_package_spec, + is_npm_package, + fetch_package_info, + get_package_publish_date, + get_latest_version_info, + extract_packages_from_command, + log_to_stderr, + parse_hook_input +) + +# Configuration +MAX_AGE_DAYS = int(os.environ.get('MAX_AGE_DAYS', '180')) # Default: 6 months +TEST_MODE = os.environ.get('CLAUDE_HOOKS_TEST_MODE', '0') == '1' + +def handle_test_mode(package_spec): + """Handle test mode logic.""" + if package_spec not in ['left-pad@1.0.0', 'moment@2.18.0']: + return True, None + error_msg = f"Package {package_spec} is too old (test mode simulation)." + log_to_stderr(error_msg) + return False, error_msg + +def build_age_error_message(package_name, version, age_days, package_info): + """Build error message for old package.""" + error_msg = ( + f"Package {package_name}@{version} is too old " + f"(published {age_days} days ago, max allowed: {MAX_AGE_DAYS} days)." + ) + + # Add latest version info if available + latest_version, latest_age_days = get_latest_version_info(package_info, version) + if latest_version and latest_age_days is not None: + error_msg += ( + f" Latest version is {latest_version} " + f"({latest_age_days} days old)." + ) + + return error_msg + +def check_package_validity(package_info, package_name, version): + """Check if package info is valid and get publish date.""" + if not package_info: + log_to_stderr(f"Could not fetch info for {package_name}") + return None, True # Allow if we can't verify + + publish_date = get_package_publish_date(package_info, version) + if not publish_date: + log_to_stderr(f"No publish time found for {package_name}@{version}") + return None, True + + return publish_date, False + +def log_package_age(package_name, version, age_days): + """Log package age information.""" + log_to_stderr( + f"Package {package_name}@{version} is {age_days} days old " + f"(within {MAX_AGE_DAYS} day limit)" + ) + +def process_package_age(package_name, version, package_info): + """Process package age and return result.""" + publish_date, should_allow = check_package_validity( + package_info, package_name, version + ) + + if should_allow: + return True, None + + # Calculate age + age_days = (datetime.now(timezone.utc) - publish_date).days + + if age_days <= MAX_AGE_DAYS: + log_package_age(package_name, version, age_days) + return True, None + + # Package is too old + error_msg = build_age_error_message( + package_name, version, age_days, package_info + ) + log_to_stderr(error_msg) + return False, error_msg + +def check_package_age(package_spec): + """Check if a package is too old.""" + package_name, version = parse_package_spec(package_spec) + + # Skip non-npm packages + if not is_npm_package(package_spec): + return True, None + + # Test mode simulation + if TEST_MODE: + return handle_test_mode(package_spec) + + try: + package_info = fetch_package_info(package_name) + return process_package_age(package_name, version, package_info) + except Exception as e: + log_to_stderr(f"Error checking {package_name}: {str(e)}") + return True, None # Allow if error + +def check_packages_in_command(command): + """Check all packages in an npm/yarn command.""" + packages = extract_packages_from_command(command) + if not packages: + log_to_stderr("No packages specified, checking package.json") + return [] + + failed_packages = [] + for package in packages: + is_allowed, error_msg = check_package_age(package) + if not is_allowed: + failed_packages.append((package, error_msg)) + + return failed_packages + +def block_old_packages(error_messages): + """Block installation of old packages.""" + block_reason = ( + "One or more packages are too old. Please use newer versions " + "or add them to the allowlist if absolutely necessary." + ) + + if TEST_MODE: + log_to_stderr("[TEST MODE] Would have blocked") + print(json.dumps({"action": "continue"})) + else: + print(block_reason + "\n\n" + "\n".join(error_messages), file=sys.stderr) + sys.exit(2) # Exit code 2 blocks the operation + +def handle_package_installation(input_data): + """Handle package installation commands.""" + command = input_data.get('tool_input', {}).get('command', '') + if not command: + return + + # Check if this is an install command + if not re.match(r'(npm|yarn)\s+(install|add|i)', command): + return + + log_to_stderr(f"Checking packages in command: {command}") + + # Check packages + failed_packages = check_packages_in_command(command) + if not failed_packages: + return + + # Block if any packages are too old + error_messages = [msg for _, msg in failed_packages if msg] + block_old_packages(error_messages) + +def handle_package_json_edit(input_data): + """Handle package.json edits.""" + command = input_data.get('tool_input', {}).get('command', '') + tool_input = input_data.get('tool_input', {}) + description = tool_input.get('description', '') + + if 'package.json' in command or 'package.json' in description: + log_to_stderr( + "Notice: package.json edit detected. " + "Ensure dependencies are up to date." + ) + +def main(): + """Main entry point.""" + input_data = parse_hook_input() + if not input_data: + print(json.dumps({"action": "continue"})) + return + + # Only process Bash tool + if input_data.get('tool_name', '') != 'Bash': + print(json.dumps({"action": "continue"})) + return + + # Handle package installation + handle_package_installation(input_data) + + # Check for package.json edits + handle_package_json_edit(input_data) + + # Allow the command to proceed + print(json.dumps({"action": "continue"})) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hooks/check-package-age.sh b/hooks/check-package-age.sh deleted file mode 100755 index 27d0829..0000000 --- a/hooks/check-package-age.sh +++ /dev/null @@ -1,198 +0,0 @@ -#!/bin/bash - -# Claude Code Hook: Prevent installation of outdated packages -# This hook intercepts npm/yarn install commands and validates package age - -# Source logging library -HOOK_NAME="check-package-age" -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common/logging.sh" - -# Start performance timing -START_TIME=$(date +%s) - -# Check if we're in test mode -TEST_MODE=${CLAUDE_HOOKS_TEST_MODE:-0} - -# Configuration -MAX_AGE_DAYS=${MAX_AGE_DAYS:-180} # Default: 6 months -CURRENT_DATE=$(date +%s) - -# Parse the hook input from stdin -INPUT=$(cat) -log_hook_start "$HOOK_NAME" "$INPUT" - -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty') -COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') -DESCRIPTION=$(echo "$INPUT" | jq -r '.tool_input.description // "No description"') - -log_debug "$HOOK_NAME" "Tool: $TOOL_NAME, Command: $COMMAND" - -# Only process Bash tool calls -if [ "$TOOL_NAME" != "Bash" ]; then - log_decision "$HOOK_NAME" "skip" "Not a Bash tool call" - log_hook_end "$HOOK_NAME" 0 - exit 0 -fi - -# Function to check package age from npm registry -check_package_age() { - local package_spec="$1" - local package_name="" - local version="" - - # Parse package@version or just package name - if [[ "$package_spec" =~ ^([^@]+)@(.+)$ ]]; then - package_name="${BASH_REMATCH[1]}" - version="${BASH_REMATCH[2]}" - else - package_name="$package_spec" - version="latest" - fi - - # Skip if it's a local file path or git URL - if [[ "$package_spec" =~ ^(\.|\/|git\+|http|file:) ]]; then - log_debug "$HOOK_NAME" "Skipping non-npm package: $package_spec" - return 0 - fi - - # In test mode, simulate old package detection for known test packages - if [ "$TEST_MODE" = "1" ]; then - case "$package_spec" in - "left-pad@1.0.0"|"moment@2.18.0") - echo "Package ${package_name}@${version} is too old (test mode simulation)." >&2 - return 1 - ;; - *) - return 0 - ;; - esac - fi - - # Query npm registry for package info - local registry_url="https://registry.npmjs.org/${package_name}" - local package_info=$(curl -s --max-time 5 "$registry_url" 2>/dev/null) - - if [ $? -ne 0 ] || [ -z "$package_info" ]; then - # If we can't fetch package info, allow installation (fail open) - log_warn "$HOOK_NAME" "Could not fetch package info for $package_name, allowing installation" - return 0 - fi - - # Get the publish time for the specific version - local publish_time="" - if [ "$version" = "latest" ]; then - local latest_version=$(echo "$package_info" | jq -r '."dist-tags".latest // empty') - if [ -n "$latest_version" ]; then - publish_time=$(echo "$package_info" | jq -r ".time[\"$latest_version\"] // empty") - fi - else - publish_time=$(echo "$package_info" | jq -r ".time[\"$version\"] // empty") - fi - - if [ -z "$publish_time" ]; then - # Can't determine publish time, allow installation - return 0 - fi - - # Convert publish time to seconds since epoch - local publish_date=$(date -d "$publish_time" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${publish_time%%.*}" +%s 2>/dev/null) - - if [ -z "$publish_date" ]; then - return 0 - fi - - # Calculate age in days - local age_days=$(( ($CURRENT_DATE - $publish_date) / 86400 )) - - if [ $age_days -gt $MAX_AGE_DAYS ]; then - # Get latest version info - local latest_version=$(echo "$package_info" | jq -r '."dist-tags".latest // empty') - local latest_time=$(echo "$package_info" | jq -r ".time[\"$latest_version\"] // empty") - local latest_date=$(date -d "$latest_time" +%s 2>/dev/null || date -j -f "%Y-%m-%dT%H:%M:%S" "${latest_time%%.*}" +%s 2>/dev/null) - local latest_age_days=$(( ($CURRENT_DATE - $latest_date) / 86400 )) - - # Build error message - local error_msg="Package ${package_name}@${version} is too old (published ${age_days} days ago, max allowed: ${MAX_AGE_DAYS} days)." - - if [ -n "$latest_version" ] && [ "$latest_version" != "$version" ]; then - error_msg="$error_msg Latest version is ${latest_version} (${latest_age_days} days old)." - fi - - log_warn "$HOOK_NAME" "$error_msg" - echo "$error_msg" >&2 - return 1 - fi - - log_info "$HOOK_NAME" "Package ${package_name}@${version} is ${age_days} days old (within ${MAX_AGE_DAYS} day limit)" - - return 0 -} - -# Check if this is an npm/yarn install command -if [[ "$COMMAND" =~ ^npm[[:space:]]+install|^npm[[:space:]]+i[[:space:]]|^yarn[[:space:]]+add[[:space:]] ]]; then - # Extract packages from the command - packages=() - - # Remove command prefix and flags - cmd_without_prefix=$(echo "$COMMAND" | sed -E 's/^(npm (install|i)|yarn add)[[:space:]]*//') - - # Parse packages (handle multiple packages and flags) - while read -r token; do - # Skip flags (start with -) - if [[ ! "$token" =~ ^- ]] && [ -n "$token" ]; then - packages+=("$token") - fi - done < <(echo "$cmd_without_prefix" | tr ' ' '\n') - - # Check each package - failed=false - log_info "$HOOK_NAME" "Checking ${#packages[@]} package(s) for age compliance" - for pkg in "${packages[@]}"; do - if ! check_package_age "$pkg"; then - failed=true - fi - done - - if [ "$failed" = true ]; then - # Output error message to stderr for blocking - local block_reason="One or more packages are too old. Please use newer versions or add them to the allowlist if absolutely necessary." - echo "$block_reason" >&2 - log_decision "$HOOK_NAME" "block" "$block_reason" - - # Output JSON response to stdout for advanced control - cat <&2 - log_info "$HOOK_NAME" "Test mode - would have blocked" - log_performance "$HOOK_NAME" $START_TIME - log_hook_end "$HOOK_NAME" 0 - exit 0 - else - log_performance "$HOOK_NAME" $START_TIME - log_hook_end "$HOOK_NAME" 2 - exit 2 # Exit code 2 blocks the action - fi - else - log_decision "$HOOK_NAME" "allow" "All packages are within age limit" - fi -fi - -# Check if this is editing package.json to add dependencies -if [[ "$COMMAND" =~ package\.json ]] || [[ "$DESCRIPTION" =~ package\.json ]]; then - # For now, we'll allow package.json edits but log them - echo "Notice: package.json edit detected. Ensure dependencies are up to date." >&2 - log_info "$HOOK_NAME" "package.json edit detected - reminder to check dependency versions" -fi - -# Allow the command to proceed -log_performance "$HOOK_NAME" $START_TIME -log_hook_end "$HOOK_NAME" 0 -exit 0 \ No newline at end of file diff --git a/hooks/claude_hooks_cli.py b/hooks/claude_hooks_cli.py new file mode 100755 index 0000000..b28b328 --- /dev/null +++ b/hooks/claude_hooks_cli.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +""" +Claude Hooks CLI - Introspection and management tool. + +Usage: + claude-hooks list Show all hooks in the repository + claude-hooks explain Show effective hooks for a specific file + claude-hooks validate Validate all configuration files +""" + +import json +import os +import sys +from pathlib import Path +from typing import List, Dict, Any + +from config_loader import HierarchicalConfigLoader +from cli_utils import Colors, format_hook_info, print_error, print_warning +from hook_validator import find_all_config_files +from cli_parser import create_parser +from cli_commands import cmd_validate +from cli_display import display_hooks_table, display_effective_hooks +from cli_display import display_config_chain + + +def gather_all_hooks(loader: HierarchicalConfigLoader) -> List[Dict[str, Any]]: + """Gather all hooks from all configuration files.""" + root = Path(loader.project_root) + config_files = find_all_config_files(root) + + all_hooks = [] + for config_path in config_files: + hooks = _extract_hooks_from_config(config_path, root) + all_hooks.extend(hooks) + + return all_hooks + + +def _extract_hooks_from_config(config_path: Path, root: Path) -> List[Dict[str, Any]]: + """Extract hooks from a single configuration file.""" + hooks = [] + + try: + with open(config_path, 'r') as f: + config = json.load(f) + + for event_type in ['pre-tool', 'post-tool', 'stop']: + event_hooks = config.get('hooks', {}).get(event_type, []) + for hook in event_hooks: + hook_info = format_hook_info(hook) + hook_info['event'] = event_type + hook_info['defined_in'] = str(config_path.relative_to(root)) + hooks.append(hook_info) + + except (json.JSONDecodeError, Exception) as e: + print_error(f"Reading {config_path}: {e}") + + return hooks + + +def cmd_list(args): + """List all hooks in the repository.""" + loader = HierarchicalConfigLoader() + hooks = gather_all_hooks(loader) + + if args.json: + print(json.dumps(hooks, indent=2)) + return + + display_hooks_table(hooks, loader.project_root, args.verbose) + + +def cmd_explain(args): + """Explain which hooks apply to a specific file.""" + loader = HierarchicalConfigLoader() + file_path = _resolve_file_path(args.file, loader.project_root) + + if not os.path.exists(file_path): + print_warning(f"File {file_path} does not exist") + + rel_path = os.path.relpath(file_path, loader.project_root) + print(f"\n{Colors.BOLD}Effective hooks for: " + f"{Colors.CYAN}{rel_path}{Colors.ENDC}\n") + + display_effective_hooks(loader, file_path, args.verbose) + + if args.verbose: + display_config_chain(loader, file_path) + + +def _resolve_file_path(file_path: str, project_root: str) -> str: + """Resolve file path to absolute.""" + if os.path.isabs(file_path): + return file_path + return os.path.join(project_root, file_path) + + +def main(): + """Main entry point.""" + # Disable colors if output is not a TTY + if not sys.stdout.isatty(): + Colors.disable() + + parser = create_parser() + args = parser.parse_args() + + # Execute command + if args.command == 'list': + cmd_list(args) + elif args.command == 'explain': + cmd_explain(args) + elif args.command == 'validate': + sys.exit(cmd_validate(args)) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hooks/clean-code-rules.json b/hooks/clean-code-rules.json deleted file mode 100644 index 3581da3..0000000 --- a/hooks/clean-code-rules.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "rules": { - "maxFunctionLines": 20, - "maxFileLines": 100, - "maxNestingDepth": 3, - "maxParameters": 3, - "maxLineLength": 80 - } -} \ No newline at end of file diff --git a/hooks/cli_commands.py b/hooks/cli_commands.py new file mode 100644 index 0000000..16a54fb --- /dev/null +++ b/hooks/cli_commands.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Command implementations for Claude Hooks CLI.""" + +import json +import os +from pathlib import Path +from typing import List, Dict, Any + +from config_loader import HierarchicalConfigLoader +from cli_utils import Colors, print_warning, print_success, print_error +from hook_validator import validate_config_file, find_all_config_files + + +def cmd_validate(args): + """Validate all configuration files.""" + loader = HierarchicalConfigLoader() + root = Path(loader.project_root) + config_files = find_all_config_files(root) + + if not config_files: + print_warning("No configuration files found") + return 1 + + print(f"\n{Colors.BOLD}Validating hook configurations...{Colors.ENDC}\n") + + # Collect all errors and warnings + all_errors, all_warnings = _validate_all_configs(config_files, root) + + print() + + # Report results + _display_validation_results(all_errors, all_warnings) + + return 1 if all_errors else 0 + + +def _validate_all_configs(config_files: List[Path], root: Path): + """Validate all configuration files and collect results.""" + all_errors = [] + all_warnings = [] + + for config_path in config_files: + errors, warnings = validate_config_file(config_path, root) + + rel_path = config_path.relative_to(root) + + if errors: + print(f" {Colors.FAIL}โœ—{Colors.ENDC} {rel_path}") + all_errors.extend(errors) + else: + print(f" {Colors.GREEN}โœ“{Colors.ENDC} {rel_path}") + + all_warnings.extend(warnings) + + return all_errors, all_warnings + + +def _display_validation_results(errors: List[str], warnings: List[str]): + """Display validation results.""" + if errors: + print(f"{Colors.FAIL}Errors found:{Colors.ENDC}") + for error in errors: + print(f" โ€ข {error}") + print() + + if warnings: + print(f"{Colors.WARNING}Warnings:{Colors.ENDC}") + for warning in warnings: + print(f" โ€ข {warning}") + print() + + if not errors and not warnings: + print_success("All configurations are valid!") \ No newline at end of file diff --git a/hooks/cli_display.py b/hooks/cli_display.py new file mode 100644 index 0000000..c21c7b7 --- /dev/null +++ b/hooks/cli_display.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Display functions for Claude Hooks CLI.""" + +import json +import os +from typing import List, Dict, Any + +from config_loader import HierarchicalConfigLoader +from cli_utils import Colors, print_warning, print_hook_status +from cli_utils import format_patterns, format_tools + + +def display_hooks_table(hooks: List[Dict[str, Any]], project_root: str, verbose: bool): + """Display hooks in a formatted table.""" + # Group by event type + by_event = group_hooks_by_event(hooks) + + print(f"\n{Colors.BOLD}Claude Hooks in {project_root}{Colors.ENDC}\n") + + for event in ['pre-tool', 'post-tool', 'stop']: + if event not in by_event: + continue + + print(f"{Colors.HEADER}[{event}]{Colors.ENDC}") + display_event_hooks(by_event[event], verbose) + print() + + +def group_hooks_by_event(hooks: List[Dict[str, Any]]) -> Dict[str, List[Dict]]: + """Group hooks by event type and sort by priority.""" + by_event = {} + + for hook in hooks: + event = hook['event'] + if event not in by_event: + by_event[event] = [] + by_event[event].append(hook) + + # Sort by priority + for event in by_event: + by_event[event].sort(key=lambda h: (-h['priority'], h['id'])) + + return by_event + + +def display_event_hooks(hooks: List[Dict[str, Any]], verbose: bool): + """Display hooks for a single event type.""" + for hook in hooks: + status = print_hook_status(hook) + + print(f" {status} {Colors.CYAN}{hook['id']:<25}{Colors.ENDC} " + f"priority={hook['priority']:<3} " + f"script={Colors.BLUE}{hook['script']:<30}{Colors.ENDC} " + f"from {hook['defined_in']}") + + if hook['description']: + print(f" {hook['description']}") + + if verbose: + display_hook_details(hook) + + +def display_hook_details(hook: Dict[str, Any]): + """Display verbose hook details.""" + patterns = format_patterns(hook['file_patterns']) + tools = format_tools(hook['tools']) + + print(f" patterns: {patterns}") + print(f" tools: {tools}") + + if hook['directories']: + print(f" directories: {', '.join(hook['directories'])}") + + +def display_effective_hooks(loader: HierarchicalConfigLoader, file_path: str, verbose: bool): + """Display hooks that apply to a file.""" + for event_type in ['pre-tool', 'post-tool', 'stop']: + hooks = loader.get_hooks_for_file(file_path, event_type) + + if not hooks: + continue + + print(f"{Colors.HEADER}[{event_type}]{Colors.ENDC}") + + for hook in hooks: + display_single_hook(hook, verbose) + + print() + + +def display_single_hook(hook: Dict[str, Any], verbose: bool): + """Display a single hook with its details.""" + priority = hook.get('priority', 50) + script = hook.get('script', 'unknown') + + print(f" โ€ข {Colors.CYAN}{hook['id']}{Colors.ENDC} " + f"(priority {priority})") + print(f" script: {Colors.BLUE}{script}{Colors.ENDC}") + + if hook.get('description'): + print(f" {hook['description']}") + + if verbose: + display_verbose_hook_info(hook) + + +def display_verbose_hook_info(hook: Dict[str, Any]): + """Display verbose information for a hook.""" + if hook.get('config'): + config_str = json.dumps(hook.get('config', {})) + print(f" config: {config_str}") + + if 'file_patterns' in hook: + display_matched_patterns(hook['file_patterns']) + + if 'tools' in hook: + print(f" tools: {', '.join(hook['tools'])}") + + +def display_matched_patterns(patterns: List[str]): + """Display which pattern matched (in verbose mode).""" + # This would need the actual file path to check matches + # For now, just show the patterns + print(f" patterns: {', '.join(patterns)}") + + +def display_config_chain(loader: HierarchicalConfigLoader, file_path: str): + """Display configuration inheritance chain.""" + print(f"{Colors.HEADER}Configuration chain:{Colors.ENDC}") + + # Use private method to get config files + config_files = loader._find_config_files(file_path) + + for i, config_file in enumerate(config_files): + rel_path = os.path.relpath(config_file, loader.project_root) + print(f" {i+1}. {rel_path}") + print() \ No newline at end of file diff --git a/hooks/cli_parser.py b/hooks/cli_parser.py new file mode 100644 index 0000000..782e725 --- /dev/null +++ b/hooks/cli_parser.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Argument parser for Claude Hooks CLI.""" + +import argparse + + +def create_parser(): + """Create the argument parser.""" + parser = argparse.ArgumentParser( + description='Claude Hooks CLI - Introspection and management tool', + formatter_class=argparse.RawDescriptionHelpFormatter + ) + + subparsers = parser.add_subparsers(dest='command', required=True) + + # List command + _add_list_parser(subparsers) + + # Explain command + _add_explain_parser(subparsers) + + # Validate command + _add_validate_parser(subparsers) + + return parser + + +def _add_list_parser(subparsers): + """Add the list command parser.""" + list_parser = subparsers.add_parser( + 'list', + help='List all hooks in the repository' + ) + list_parser.add_argument( + '--json', + action='store_true', + help='Output as JSON' + ) + list_parser.add_argument( + '-v', '--verbose', + action='store_true', + help='Show detailed information' + ) + + +def _add_explain_parser(subparsers): + """Add the explain command parser.""" + explain_parser = subparsers.add_parser( + 'explain', + help='Show effective hooks for a file' + ) + explain_parser.add_argument('file', help='File path to explain') + explain_parser.add_argument( + '-v', '--verbose', + action='store_true', + help='Show detailed information' + ) + + +def _add_validate_parser(subparsers): + """Add the validate command parser.""" + validate_parser = subparsers.add_parser( + 'validate', + help='Validate all configuration files' + ) \ No newline at end of file diff --git a/hooks/cli_utils.py b/hooks/cli_utils.py new file mode 100644 index 0000000..118884f --- /dev/null +++ b/hooks/cli_utils.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python3 +"""Utilities for the Claude Hooks CLI.""" + +import sys +from typing import Dict, Any, List + + +class Colors: + """Terminal colors for pretty output.""" + HEADER = '\033[95m' + BLUE = '\033[94m' + CYAN = '\033[96m' + GREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + @staticmethod + def disable(): + """Disable colors for non-TTY output.""" + Colors.HEADER = '' + Colors.BLUE = '' + Colors.CYAN = '' + Colors.GREEN = '' + Colors.WARNING = '' + Colors.FAIL = '' + Colors.ENDC = '' + Colors.BOLD = '' + Colors.UNDERLINE = '' + + +def format_hook_info(hook: Dict[str, Any]) -> Dict[str, Any]: + """Format hook information for display.""" + return { + 'id': hook.get('id', 'unnamed'), + 'event': hook.get('event', 'unknown'), + 'script': hook.get('script', 'unknown'), + 'priority': hook.get('priority', 50), + 'file_patterns': hook.get('file_patterns', ['*']), + 'tools': hook.get('tools', ['*']), + 'directories': hook.get('directories', []), + 'disabled': hook.get('disabled', False), + 'description': hook.get('description', ''), + 'defined_in': hook.get('defined_in', 'unknown'), + } + + +def print_hook_status(hook: Dict[str, Any]) -> str: + """Get status indicator for a hook.""" + if hook['disabled']: + return f"{Colors.FAIL}(disabled){Colors.ENDC}" + return f"{Colors.GREEN}โœ“{Colors.ENDC}" + + +def print_error(message: str): + """Print an error message.""" + print(f"{Colors.FAIL}Error: {message}{Colors.ENDC}", file=sys.stderr) + + +def print_warning(message: str): + """Print a warning message.""" + print(f"{Colors.WARNING}Warning: {message}{Colors.ENDC}", file=sys.stderr) + + +def print_success(message: str): + """Print a success message.""" + print(f"{Colors.GREEN}{message}{Colors.ENDC}") + + +def format_patterns(patterns: List[str]) -> str: + """Format file patterns for display.""" + if patterns == ['*']: + return 'all files' + return ', '.join(patterns) + + +def format_tools(tools: List[str]) -> str: + """Format tools for display.""" + if tools == ['*']: + return 'all tools' + return ', '.join(tools) \ No newline at end of file diff --git a/hooks/code-quality-validator.sh b/hooks/code-quality-validator.sh deleted file mode 100755 index 9b01282..0000000 --- a/hooks/code-quality-validator.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash - -# Code Quality Validator Hook -# Enforces clean code standards using modular checks - -# Source the quality checks loader -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common/code-quality/loader.sh" - -# Read input -INPUT=$(cat) -TOOL=$(echo "$INPUT" | jq -r '.tool_name // empty') -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') -EXIT_CODE=$(echo "$INPUT" | jq -r '.exit_code // 0') - -# FUCKING LOG EVERYTHING -LOG_FILE="/Users/danseider/claude-hooks/.claude/hooks/code-quality.log" -echo "[$(date)] CODE QUALITY VALIDATOR STARTED" >> "$LOG_FILE" -echo "[$(date)] Input: ${INPUT:0:200}..." >> "$LOG_FILE" -echo "[$(date)] TOOL: $TOOL, FILE_PATH: $FILE_PATH, EXIT_CODE: $EXIT_CODE" >> "$LOG_FILE" - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -CYAN='\033[0;36m' -NC='\033[0m' - -# Configuration -RULES_FILE="${CLAUDE_RULES_FILE:-/Users/danseider/claude-hooks/.claude/hooks/quality-config.json}" - -# Check if this is a Stop event (no tool) -EVENT_TYPE=$(echo "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null) - -echo "[$(date)] Event type: $EVENT_TYPE" >> "$LOG_FILE" - -if [[ "$EVENT_TYPE" == "Stop" ]]; then - echo "[$(date)] Processing Stop event" >> "$LOG_FILE" - # For Stop events, check recently modified files - - # Find all TypeScript files modified in the last 10 minutes (exclude node_modules, .d.ts, and lib/) - RECENT_FILES=$(find . \( -name "*.ts" -o -name "*.tsx" \) -mtime -10m 2>/dev/null | grep -v node_modules | grep -v "\.d\.ts$" | grep -v "^./lib/" | head -20) - - echo "[$(date)] Found files: $RECENT_FILES" >> "$LOG_FILE" - - TOTAL_VIOLATIONS=0 - VIOLATIONS_DETAIL="" - for file in $RECENT_FILES; do - if [[ -f "$file" ]] && [[ ! "$file" =~ (test|spec|\.d\.ts)\. ]]; then - echo "[$(date)] Checking: $file" >> "$LOG_FILE" - # Redirect check output to capture it - CHECK_OUTPUT=$(run_all_quality_checks "$file" "$RULES_FILE" 2>&1) - VIOLATIONS_FOUND=$? - TOTAL_VIOLATIONS=$((TOTAL_VIOLATIONS + VIOLATIONS_FOUND)) - if [[ $VIOLATIONS_FOUND -gt 0 ]]; then - VIOLATIONS_DETAIL="${VIOLATIONS_DETAIL}\n${file}: ${VIOLATIONS_FOUND} violations" - fi - fi - done - - if [[ $TOTAL_VIOLATIONS -eq 0 ]]; then - echo "[$(date)] All checks passed" >> "$LOG_FILE" - echo '{"continue": true}' - else - echo "[$(date)] Found $TOTAL_VIOLATIONS violations" >> "$LOG_FILE" - - # Block if violations exceed threshold - BLOCK_THRESHOLD="${CODE_QUALITY_BLOCK_THRESHOLD:-0}" # Default: ALWAYS BLOCK - - if [[ $TOTAL_VIOLATIONS -gt $BLOCK_THRESHOLD ]]; then - echo "[$(date)] BLOCKING! Violations: $TOTAL_VIOLATIONS, Threshold: $BLOCK_THRESHOLD" >> "$LOG_FILE" - cat <> "$LOG_FILE" - echo '{"continue": true}' - fi - fi - echo "[$(date)] Exiting Stop event handler" >> "$LOG_FILE" - exit 0 -fi - -# For tool events, check specific file -if [[ ! "$TOOL" =~ ^(Write|Edit|MultiEdit)$ ]] || [[ "$EXIT_CODE" != "0" ]] || [[ -z "$FILE_PATH" ]] || [[ "$FILE_PATH" == "null" ]]; then - echo "[$(date)] Skipping - TOOL: $TOOL, EXIT_CODE: $EXIT_CODE, FILE_PATH: $FILE_PATH" >> "$LOG_FILE" - echo '{"continue": true}' - exit 0 -fi - -# Skip non-TypeScript files -if [[ ! "$FILE_PATH" =~ \.(ts|tsx)$ ]]; then - echo "[$(date)] Skipping non-TS file: $FILE_PATH" >> "$LOG_FILE" - echo '{"continue": true}' - exit 0 -fi - -# Skip test files -if [[ "$FILE_PATH" =~ (test|spec)\. ]]; then - echo "[$(date)] Skipping test file: $FILE_PATH" >> "$LOG_FILE" - echo '{"continue": true}' - exit 0 -fi - -echo "[$(date)] Checking file: $FILE_PATH" >> "$LOG_FILE" - -# Run all quality checks -CHECK_OUTPUT=$(run_all_quality_checks "$FILE_PATH" "$RULES_FILE" 2>&1) -VIOLATIONS=$? - -if [[ $VIOLATIONS -eq 0 ]]; then - echo "[$(date)] All checks passed for $FILE_PATH" >> "$LOG_FILE" - echo '{"continue": true}' - exit 0 -else - echo "[$(date)] Found $VIOLATIONS violations in $FILE_PATH" >> "$LOG_FILE" - - # Block if violations exceed threshold (configurable) - BLOCK_THRESHOLD="${CODE_QUALITY_BLOCK_THRESHOLD:-0}" # Default: ALWAYS BLOCK - - if [[ $VIOLATIONS -gt $BLOCK_THRESHOLD ]]; then - echo "[$(date)] BLOCKING file edit! Violations: $VIOLATIONS, Threshold: $BLOCK_THRESHOLD" >> "$LOG_FILE" - cat <> "$LOG_FILE" - echo '{"continue": true}' - fi - exit 0 -fi \ No newline at end of file diff --git a/hooks/common/code-quality/checks/comment-ratio.sh b/hooks/common/code-quality/checks/comment-ratio.sh deleted file mode 100644 index 10ef364..0000000 --- a/hooks/common/code-quality/checks/comment-ratio.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash - -# Comment Ratio Check Module -# Warns if code has too many comments (code should be self-documenting) - -check_comment_ratio() { - local file="$1" - local max_ratio="${2:-0.2}" # 20% default - - local total_lines=0 - local comment_lines=0 - local in_block_comment=false - - while IFS= read -r line; do - # Skip empty lines - if [[ -z "${line// }" ]]; then - continue - fi - - ((total_lines++)) - - # Check for block comment start - if [[ "$line" =~ /\* ]] && [[ ! "$line" =~ \*/ ]]; then - in_block_comment=true - fi - - # Count comment lines - if [[ "$in_block_comment" == true ]] || \ - [[ "$line" =~ ^[[:space:]]*// ]] || \ - [[ "$line" =~ ^[[:space:]]*# ]] || \ - [[ "$line" =~ ^[[:space:]]*/\* ]] || \ - [[ "$line" =~ ^[[:space:]]*\* ]]; then - ((comment_lines++)) - fi - - # Check for block comment end - if [[ "$line" =~ \*/ ]]; then - in_block_comment=false - fi - done < "$file" - - if [[ $total_lines -eq 0 ]]; then - return 0 - fi - - # Calculate ratio - local ratio=$(awk "BEGIN {printf \"%.2f\", $comment_lines / $total_lines}") - - # Check if ratio exceeds threshold - if (( $(awk "BEGIN {print ($ratio > $max_ratio)}") )); then - local percentage=$(awk "BEGIN {printf \"%.0f\", $ratio * 100}") - echo "Comment ratio violation in $file:" - echo " ${percentage}% comments (${comment_lines}/${total_lines} lines)" - echo " Consider making code more self-documenting" - return 1 - fi - - return 0 -} \ No newline at end of file diff --git a/hooks/common/code-quality/checks/file-length.sh b/hooks/common/code-quality/checks/file-length.sh deleted file mode 100644 index 4c0ac7e..0000000 --- a/hooks/common/code-quality/checks/file-length.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/bash - -# File Length Check Module -# Checks that files don't exceed maximum line count - -check_file_length() { - local file="$1" - local max_lines="${2:-100}" - - if [[ ! -f "$file" ]]; then - return 0 - fi - - local line_count=$(wc -l < "$file" | tr -d ' ') - - if [[ $line_count -gt $max_lines ]]; then - echo "File length violation: $file has $line_count lines (max: $max_lines)" - return 1 - fi - - return 0 -} \ No newline at end of file diff --git a/hooks/common/code-quality/checks/function-length.sh b/hooks/common/code-quality/checks/function-length.sh deleted file mode 100644 index 473d7b1..0000000 --- a/hooks/common/code-quality/checks/function-length.sh +++ /dev/null @@ -1,63 +0,0 @@ -#!/bin/bash - -# Function Length Check Module -# Checks that functions don't exceed maximum line count - -check_function_length() { - local file="$1" - local max_lines="${2:-20}" - local violations=() - - # Only check JS/TS files - if [[ ! "$file" =~ \.(ts|tsx|js|jsx)$ ]]; then - return 0 - fi - - local in_function=false - local function_start=0 - local function_name="" - local brace_count=0 - local line_num=0 - - while IFS= read -r line; do - ((line_num++)) - - # Detect function start (various patterns) - if [[ "$line" =~ ^[[:space:]]*(function|const|let|var|export|async)[[:space:]]+([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*(\(|=.*\(|=.*async.*\() ]] || \ - [[ "$line" =~ ^[[:space:]]*([a-zA-Z_][a-zA-Z0-9_]*)[[:space:]]*:[[:space:]]*(\(|async.*\() ]]; then - if [[ ! "$in_function" == true ]]; then - function_name="${BASH_REMATCH[2]:-unknown}" - function_start=$line_num - in_function=true - brace_count=0 - fi - fi - - # Count braces - if [[ "$in_function" == true ]]; then - # Count opening braces - local opens=$(echo "$line" | grep -o '{' | wc -l) - # Count closing braces - local closes=$(echo "$line" | grep -o '}' | wc -l) - brace_count=$((brace_count + opens - closes)) - - # Function end when brace count returns to 0 - if [[ $brace_count -eq 0 ]] && [[ "$line" =~ \} ]]; then - local function_length=$((line_num - function_start + 1)) - if [[ $function_length -gt $max_lines ]]; then - violations+=("Function '$function_name' at line $function_start: $function_length lines (max: $max_lines)") - fi - in_function=false - fi - fi - done < "$file" - - # Output violations - if [[ ${#violations[@]} -gt 0 ]]; then - echo "Function length violations in $file:" - printf '%s\n' "${violations[@]}" - return 1 - fi - - return 0 -} \ No newline at end of file diff --git a/hooks/common/code-quality/checks/line-length.sh b/hooks/common/code-quality/checks/line-length.sh deleted file mode 100644 index 8b80e1a..0000000 --- a/hooks/common/code-quality/checks/line-length.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -# Line Length Check Module -# Checks that lines don't exceed maximum character count - -check_line_length() { - local file="$1" - local max_length="${2:-80}" - local violations=() - local line_num=0 - - while IFS= read -r line; do - ((line_num++)) - - # Skip lines that are just long strings or URLs - if [[ "$line" =~ https?:// ]] || [[ "$line" =~ [\"\'\`].*[\"\'\`] ]]; then - continue - fi - - local length=${#line} - if [[ $length -gt $max_length ]]; then - violations+=("Line $line_num: $length characters (max: $max_length)") - fi - done < "$file" - - # Output violations (max 5 to avoid spam) - if [[ ${#violations[@]} -gt 0 ]]; then - echo "Line length violations in $file:" - local count=0 - for violation in "${violations[@]}"; do - echo " $violation" - ((count++)) - if [[ $count -ge 5 ]]; then - if [[ ${#violations[@]} -gt 5 ]]; then - echo " ... and $((${#violations[@]} - 5)) more" - fi - break - fi - done - return 1 - fi - - return 0 -} \ No newline at end of file diff --git a/hooks/common/code-quality/checks/magic-numbers.sh b/hooks/common/code-quality/checks/magic-numbers.sh deleted file mode 100644 index 01e86ac..0000000 --- a/hooks/common/code-quality/checks/magic-numbers.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/bin/bash - -# Magic Numbers Check Module -# Detects hardcoded numeric literals that should be constants - -check_magic_numbers() { - local file="$1" - local violations=() - local line_num=0 - - # Skip non-code files - if [[ ! "$file" =~ \.(ts|tsx|js|jsx|py|rs|go|java)$ ]]; then - return 0 - fi - - while IFS= read -r line; do - ((line_num++)) - - # Skip comments - if [[ "$line" =~ ^[[:space:]]*// ]] || [[ "$line" =~ ^[[:space:]]*# ]] || [[ "$line" =~ ^[[:space:]]*/\* ]]; then - continue - fi - - # Skip lines with const/let/var declarations (these are defining constants) - if [[ "$line" =~ (const|let|var|final|static)[[:space:]] ]]; then - continue - fi - - # Find numbers (excluding 0, 1, -1, and common cases) - # Exclude: hex colors (#fff), percentages (100%), array indices [0] - if echo "$line" | grep -qE '[^#\[\.0-9a-fA-F]([2-9]|[1-9][0-9]+)(?![0-9]*[%\]])' && \ - ! echo "$line" | grep -qE '(import|from|require|test|describe|it)\s'; then - - # Extract the number - local numbers=$(echo "$line" | grep -oE '[^#\[\.0-9a-fA-F]([2-9]|[1-9][0-9]+)' | grep -oE '[0-9]+') - - for num in $numbers; do - # Skip common port numbers, HTTP codes, dates - if [[ "$num" == "200" ]] || [[ "$num" == "404" ]] || [[ "$num" == "500" ]] || \ - [[ "$num" == "3000" ]] || [[ "$num" == "8080" ]] || [[ "$num" == "2023" ]] || \ - [[ "$num" == "2024" ]] || [[ "$num" == "2025" ]]; then - continue - fi - - violations+=("Line $line_num: Magic number '$num' - consider extracting to a named constant") - done - fi - done < "$file" - - # Output violations (max 5) - if [[ ${#violations[@]} -gt 0 ]]; then - echo "Magic number violations in $file:" - local count=0 - for violation in "${violations[@]}"; do - echo " $violation" - ((count++)) - if [[ $count -ge 5 ]]; then - if [[ ${#violations[@]} -gt 5 ]]; then - echo " ... and $((${#violations[@]} - 5)) more" - fi - break - fi - done - return 1 - fi - - return 0 -} \ No newline at end of file diff --git a/hooks/common/code-quality/checks/nesting-depth.sh b/hooks/common/code-quality/checks/nesting-depth.sh deleted file mode 100644 index 15753e1..0000000 --- a/hooks/common/code-quality/checks/nesting-depth.sh +++ /dev/null @@ -1,57 +0,0 @@ -#!/bin/bash - -# Nesting Depth Check Module -# Checks that code doesn't exceed maximum nesting depth - -check_nesting_depth() { - local file="$1" - local max_depth="${2:-3}" - local violations=() - - local current_depth=0 - local max_seen=0 - local line_num=0 - - while IFS= read -r line; do - ((line_num++)) - - # Count opening braces - local opens=$(echo "$line" | grep -o '{' | wc -l) - # Count closing braces - local closes=$(echo "$line" | grep -o '}' | wc -l) - - current_depth=$((current_depth + opens)) - - if [[ $current_depth -gt $max_seen ]]; then - max_seen=$current_depth - fi - - if [[ $current_depth -gt $max_depth ]]; then - violations+=("Line $line_num: nesting depth $current_depth exceeds maximum $max_depth") - fi - - current_depth=$((current_depth - closes)) - - # Ensure depth doesn't go negative - if [[ $current_depth -lt 0 ]]; then - current_depth=0 - fi - done < "$file" - - # Output violations - if [[ ${#violations[@]} -gt 0 ]]; then - echo "Nesting depth violations in $file:" - # Show only first 3 violations to avoid spam - for i in {0..2}; do - if [[ $i -lt ${#violations[@]} ]]; then - echo " ${violations[$i]}" - fi - done - if [[ ${#violations[@]} -gt 3 ]]; then - echo " ... and $((${#violations[@]} - 3)) more" - fi - return 1 - fi - - return 0 -} \ No newline at end of file diff --git a/hooks/common/code-quality/loader.sh b/hooks/common/code-quality/loader.sh deleted file mode 100644 index ebc98e2..0000000 --- a/hooks/common/code-quality/loader.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash - -# Code Quality Checks Loader -# Sources all modular check functions - -QUALITY_CHECKS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/checks" - -# Source all check modules -source "$QUALITY_CHECKS_DIR/file-length.sh" -source "$QUALITY_CHECKS_DIR/function-length.sh" -source "$QUALITY_CHECKS_DIR/nesting-depth.sh" -source "$QUALITY_CHECKS_DIR/line-length.sh" -source "$QUALITY_CHECKS_DIR/magic-numbers.sh" -source "$QUALITY_CHECKS_DIR/comment-ratio.sh" -# source "$QUALITY_CHECKS_DIR/duplication.sh" # Removed - broken implementation - -# Run all code quality checks -run_all_quality_checks() { - local file="$1" - local rules_file="$2" - local total_violations=0 - - # Load rules from config if available - if [[ -f "$rules_file" ]]; then - MAX_FUNCTION_LINES=$(jq -r '.rules.maxFunctionLines // 20' "$rules_file") - MAX_FILE_LINES=$(jq -r '.rules.maxFileLines // 100' "$rules_file") - MAX_NESTING=$(jq -r '.rules.maxNestingDepth // 3' "$rules_file") - MAX_LINE_LENGTH=$(jq -r '.rules.maxLineLength // 100' "$rules_file") - else - # Default values - MAX_FUNCTION_LINES=20 - MAX_FILE_LINES=100 - MAX_NESTING=3 - MAX_LINE_LENGTH=100 - fi - - # Run each check - if ! check_file_length "$file" "$MAX_FILE_LINES"; then - ((total_violations++)) - fi - - if ! check_function_length "$file" "$MAX_FUNCTION_LINES"; then - ((total_violations++)) - fi - - if ! check_nesting_depth "$file" "$MAX_NESTING"; then - ((total_violations++)) - fi - - if ! check_line_length "$file" "$MAX_LINE_LENGTH"; then - ((total_violations++)) - fi - - if ! check_magic_numbers "$file"; then - ((total_violations++)) - fi - - if ! check_comment_ratio "$file"; then - ((total_violations++)) - fi - - # Duplication check removed - implementation was broken - - return $total_violations -} \ No newline at end of file diff --git a/hooks/common/linting.sh b/hooks/common/linting.sh deleted file mode 100644 index 015c86c..0000000 --- a/hooks/common/linting.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash - -# Linting validation functions -# Simple and focused on linting checks only - -# Check if lint script exists -has_lint_script() { - local dir="${1:-.}" - - if [ -f "$dir/package.json" ] && grep -q '"lint"' "$dir/package.json" 2>/dev/null; then - return 0 - else - return 1 - fi -} - -# Run lint check -run_lint_check() { - local dir="${1:-.}" - - LINT_OUTPUT="" - - if ! has_lint_script "$dir"; then - return 0 # No lint script - fi - - LINT_OUTPUT=$(cd "$dir" && npm run lint 2>&1 || true) - - # Check for errors - if echo "$LINT_OUTPUT" | grep -qE "(error|Error|ERROR|failed|Failed|FAILED)" && \ - ! echo "$LINT_OUTPUT" | grep -q "0 errors"; then - return 1 - else - return 0 - fi -} \ No newline at end of file diff --git a/hooks/common/logging.sh b/hooks/common/logging.sh deleted file mode 100755 index b9b4ff4..0000000 --- a/hooks/common/logging.sh +++ /dev/null @@ -1,201 +0,0 @@ -#!/bin/bash - -# Claude Hooks Common Logging Library -# Provides centralized logging functionality for all hooks - -# Default configuration - logging is ON by default with smart defaults -# Users can disable by setting CLAUDE_LOG_ENABLED=false or in settings.json -CLAUDE_LOGS_DIR="${CLAUDE_LOGS_DIR:-$HOME/.local/share/claude-hooks/logs}" -CLAUDE_LOG_FILE="${CLAUDE_LOG_FILE:-$CLAUDE_LOGS_DIR/hooks.log}" -CLAUDE_LOG_LEVEL="${CLAUDE_LOG_LEVEL:-INFO}" -CLAUDE_LOG_ENABLED="${CLAUDE_LOG_ENABLED:-false}" # OFF by default -CLAUDE_LOG_MAX_SIZE="${CLAUDE_LOG_MAX_SIZE:-10485760}" # 10MB default -CLAUDE_LOG_RETENTION_DAYS="${CLAUDE_LOG_RETENTION_DAYS:-7}" - -# Log levels - using case statement for compatibility with older bash -get_log_level() { - case "$1" in - "DEBUG") echo 0 ;; - "INFO") echo 1 ;; - "WARN") echo 2 ;; - "ERROR") echo 3 ;; - *) echo 1 ;; - esac -} - -# Get numeric value for current log level -CURRENT_LOG_LEVEL=$(get_log_level "$CLAUDE_LOG_LEVEL") - -# Ensure log directory exists -mkdir -p "$CLAUDE_LOGS_DIR" - -# Function to rotate logs if needed -rotate_logs() { - if [ -f "$CLAUDE_LOG_FILE" ]; then - local size=$(stat -f%z "$CLAUDE_LOG_FILE" 2>/dev/null || stat -c%s "$CLAUDE_LOG_FILE" 2>/dev/null || echo 0) - if [ "$size" -gt "$CLAUDE_LOG_MAX_SIZE" ]; then - local timestamp=$(date +%Y%m%d_%H%M%S) - mv "$CLAUDE_LOG_FILE" "${CLAUDE_LOG_FILE}.${timestamp}" - - # Clean up old logs - find "$CLAUDE_LOGS_DIR" -name "hooks.log.*" -mtime +$CLAUDE_LOG_RETENTION_DAYS -delete 2>/dev/null - fi - fi -} - -# Main logging function -log() { - local level=$1 - local hook_name=$2 - shift 2 - local message="$*" - - # Check if logging is enabled - if [ "$CLAUDE_LOG_ENABLED" != "true" ]; then - return 0 - fi - - # Check log level - local level_value=$(get_log_level "$level") - if [ "$level_value" -lt "$CURRENT_LOG_LEVEL" ]; then - return 0 - fi - - # Rotate logs if needed - rotate_logs - - # Format timestamp - local timestamp=$(date '+%Y-%m-%d %H:%M:%S') - - # Write log entry - echo "[$timestamp] [$level] [$hook_name] $message" >> "$CLAUDE_LOG_FILE" -} - -# Convenience logging functions -log_debug() { - local hook_name=$1 - shift - log "DEBUG" "$hook_name" "$@" -} - -log_info() { - local hook_name=$1 - shift - log "INFO" "$hook_name" "$@" -} - -log_warn() { - local hook_name=$1 - shift - log "WARN" "$hook_name" "$@" -} - -log_error() { - local hook_name=$1 - shift - log "ERROR" "$hook_name" "$@" -} - -# Function to log hook entry -log_hook_start() { - local hook_name=$1 - local input=$2 - - log_info "$hook_name" "Hook started" - if [ -n "$input" ] && [ "$CURRENT_LOG_LEVEL" -le "$(get_log_level "DEBUG")" ]; then - # Sanitize input for logging (remove sensitive data) - local sanitized_input=$(echo "$input" | jq -r 'del(.secrets, .passwords, .tokens)' 2>/dev/null || echo "$input") - log_debug "$hook_name" "Input: $sanitized_input" - fi -} - -# Function to log hook exit -log_hook_end() { - local hook_name=$1 - local exit_code=$2 - - if [ "$exit_code" -eq 0 ]; then - log_info "$hook_name" "Hook completed successfully (exit code: $exit_code)" - else - log_error "$hook_name" "Hook failed (exit code: $exit_code)" - fi -} - -# Function to log hook decision -log_decision() { - local hook_name=$1 - local decision=$2 - local reason=$3 - - log_info "$hook_name" "Decision: $decision - $reason" -} - -# Function to log detailed error context -log_error_context() { - local hook_name=$1 - local error_type=$2 - local command=$3 - local output=$4 - local working_dir=$(pwd) - - log_error "$hook_name" "Error Type: $error_type" - log_error "$hook_name" "Working Directory: $working_dir" - [ -n "$command" ] && log_error "$hook_name" "Failed Command: $command" - [ -n "$output" ] && log_error "$hook_name" "Command Output: $output" -} - -# Function to log environment context for debugging -log_env_context() { - local hook_name=$1 - - log_debug "$hook_name" "Node version: $(node --version 2>/dev/null || echo 'not found')" - log_debug "$hook_name" "NPM version: $(npm --version 2>/dev/null || echo 'not found')" - log_debug "$hook_name" "Working directory: $(pwd)" - log_debug "$hook_name" "Current user: $(whoami)" - log_debug "$hook_name" "PATH: $PATH" -} - -# Function to log performance metrics -log_performance() { - local hook_name=$1 - local start_time=$2 - local end_time=$(date +%s) - local duration=$((end_time - start_time)) - - log_debug "$hook_name" "Execution time: ${duration}s" -} - -# Load configuration from settings.json if available -# This ONLY runs if settings.json exists - otherwise we use defaults -load_logging_config() { - local settings_file="${CLAUDE_SETTINGS_FILE:-$HOME/claude/settings.json}" - - # Only load config if file exists - no error if missing - if [ -f "$settings_file" ]; then - # Check if logging section exists - local has_logging=$(jq -r 'has("logging")' "$settings_file" 2>/dev/null) - - if [ "$has_logging" = "true" ]; then - # Extract logging configuration - local enabled=$(jq -r '.logging.enabled // empty' "$settings_file" 2>/dev/null) - local level=$(jq -r '.logging.level // empty' "$settings_file" 2>/dev/null) - local path=$(jq -r '.logging.path // empty' "$settings_file" 2>/dev/null) - local max_size=$(jq -r '.logging.maxSize // empty' "$settings_file" 2>/dev/null) - local retention=$(jq -r '.logging.retention // empty' "$settings_file" 2>/dev/null) - - # Apply configuration only if explicitly set - [ -n "$enabled" ] && [ "$enabled" != "null" ] && CLAUDE_LOG_ENABLED="$enabled" - [ -n "$level" ] && [ "$level" != "null" ] && CLAUDE_LOG_LEVEL="$level" - [ -n "$path" ] && [ "$path" != "null" ] && CLAUDE_LOG_FILE="$path" - [ -n "$max_size" ] && [ "$max_size" != "null" ] && CLAUDE_LOG_MAX_SIZE="$max_size" - [ -n "$retention" ] && [ "$retention" != "null" ] && CLAUDE_LOG_RETENTION_DAYS="$retention" - - # Update current log level - CURRENT_LOG_LEVEL=$(get_log_level "$CLAUDE_LOG_LEVEL") - fi - fi - # If no settings.json or no logging section, we just use the defaults -} - -# Initialize logging configuration -load_logging_config \ No newline at end of file diff --git a/hooks/common/typescript.sh b/hooks/common/typescript.sh deleted file mode 100644 index 1bf302f..0000000 --- a/hooks/common/typescript.sh +++ /dev/null @@ -1,41 +0,0 @@ -#!/bin/bash - -# TypeScript validation functions -# Simple and focused on TypeScript checks only - -# Detect TypeScript command -detect_typescript_command() { - local dir="${1:-.}" - - # Check package.json scripts - if [ -f "$dir/package.json" ]; then - if grep -q '"typecheck"' "$dir/package.json" 2>/dev/null; then - echo "npm run typecheck" - elif grep -q '"type-check"' "$dir/package.json" 2>/dev/null; then - echo "npm run type-check" - elif [ -f "$dir/tsconfig.json" ]; then - echo "npx -y tsc --noEmit" - fi - fi -} - -# Run TypeScript check -run_typescript_check() { - local dir="${1:-.}" - - TS_OUTPUT="" - local tsc_cmd=$(detect_typescript_command "$dir") - - if [ -z "$tsc_cmd" ]; then - return 0 # No TypeScript setup - fi - - TS_OUTPUT=$($tsc_cmd 2>&1 || true) - - # Check for errors - if echo "$TS_OUTPUT" | grep -qE "error TS[0-9]+:"; then - return 1 - else - return 0 - fi -} \ No newline at end of file diff --git a/hooks/common/validation.sh b/hooks/common/validation.sh deleted file mode 100644 index 122a588..0000000 --- a/hooks/common/validation.sh +++ /dev/null @@ -1,14 +0,0 @@ -#!/bin/bash - -# Common validation loader -# Sources modular validation functions - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Source modular validation functions -source "$SCRIPT_DIR/typescript.sh" -source "$SCRIPT_DIR/linting.sh" - -# Export common variables -export TS_OUTPUT="" -export LINT_OUTPUT="" \ No newline at end of file diff --git a/hooks/config_loader.py b/hooks/config_loader.py new file mode 100644 index 0000000..0b2f03f --- /dev/null +++ b/hooks/config_loader.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python3 +""" +Hierarchical configuration loader for Claude Hooks. +Supports inheritance, file patterns, and directory-based filtering. +""" + +import json +import os +from pathlib import Path +from typing import Dict, List, Optional, Any + +from config_merger import merge_configs +from hook_filter import filter_hooks_for_file + + +class HierarchicalConfigLoader: + """Load and merge hierarchical hook configurations.""" + + def __init__(self, project_root: Optional[str] = None): + """Initialize the loader with the project root.""" + self.project_root = project_root or self._find_project_root() + self._config_cache = {} + self._file_cache = {} + + def _find_project_root(self) -> str: + """Find the project root by looking for .git or .claude directories.""" + current = os.getcwd() + while current != '/': + if os.path.exists(os.path.join(current, '.git')): + return current + if os.path.exists(os.path.join(current, '.claude')): + return current + current = os.path.dirname(current) + return os.getcwd() + + def get_config_for_path(self, file_path: str) -> Dict[str, Any]: + """Get the merged configuration for a specific file path.""" + # Cache lookup + cache_key = os.path.abspath(file_path) + if cache_key in self._config_cache: + return self._config_cache[cache_key] + + # Load and merge all configs + configs = self._find_config_files(file_path) + merged = self._merge_all_configs(configs) + + # Cache result + self._config_cache[cache_key] = merged + return merged + + def _find_config_files(self, file_path: str) -> List[str]: + """Find all configuration files that apply to this path.""" + configs = [] + + # Add root config if exists + root_config = self._get_root_config_path() + if root_config and os.path.exists(root_config): + configs.append(root_config) + + # Add directory-specific configs + configs.extend(self._find_directory_configs(file_path)) + + return configs + + def _get_root_config_path(self) -> Optional[str]: + """Get the root configuration file path.""" + return os.path.join(self.project_root, '.claude', 'hookconfig.json') + + def _find_directory_configs(self, file_path: str) -> List[str]: + """Find configuration files in directories.""" + configs = [] + target_dir = os.path.dirname(os.path.abspath(file_path)) + + if not self._is_path_in_project(target_dir): + return configs + + path_parts = self._get_relative_path_parts(target_dir) + if not path_parts: + return configs + + # Check each directory level + for i in range(len(path_parts)): + current = os.path.join(self.project_root, *path_parts[:i+1]) + config_path = os.path.join(current, '.claude-hooks.json') + if os.path.exists(config_path): + configs.append(config_path) + + return configs + + def _is_path_in_project(self, path: str) -> bool: + """Check if path is within project root.""" + try: + current = os.path.abspath(self.project_root) + rel_path = os.path.relpath(path, current) + return rel_path == '.' or not rel_path.startswith('..') + except ValueError: + # Paths on different drives on Windows + return False + + def _get_relative_path_parts(self, target_dir: str) -> List[str]: + """Get path parts relative to project root.""" + try: + current = os.path.abspath(self.project_root) + rel_path = os.path.relpath(target_dir, current) + if rel_path == '.': + return [] + return rel_path.split(os.sep) + except ValueError: + return [] + + def _load_config_file(self, config_path: str) -> Optional[Dict[str, Any]]: + """Load a single configuration file.""" + if config_path in self._file_cache: + return self._file_cache[config_path] + + config = self._read_config_file(config_path) + if not config: + return None + + # Handle extends + if 'extends' in config: + base_config = self._load_extended_config(config['extends'], config_path) + if base_config: + config = merge_configs(base_config, config) + + self._file_cache[config_path] = config + return config + + def _read_config_file(self, config_path: str) -> Optional[Dict[str, Any]]: + """Read and parse a configuration file.""" + try: + with open(config_path, 'r') as f: + return json.load(f) + except Exception as e: + print(f"Error loading {config_path}: {e}", file=os.sys.stderr) + return None + + def _load_extended_config(self, extends: str, current_path: str) -> Optional[Dict[str, Any]]: + """Load configuration that this one extends.""" + if extends.startswith('@'): + # Predefined configs (future feature) + return None + + # Resolve relative path + base_path = os.path.join(os.path.dirname(current_path), extends) + if os.path.exists(base_path): + return self._load_config_file(base_path) + + # Try from project root + root_path = os.path.join(self.project_root, extends) + if os.path.exists(root_path): + return self._load_config_file(root_path) + + return None + + def _merge_all_configs(self, config_paths: List[str]) -> Dict[str, Any]: + """Merge all configuration files.""" + result = {} + + for config_path in config_paths: + config = self._load_config_file(config_path) + if config: + result = merge_configs(result, config) + + return result + + def get_hooks_for_file(self, file_path: str, event_type: str, + tool: Optional[str] = None) -> List[Dict[str, Any]]: + """Get hooks that apply to a specific file and event.""" + config = self.get_config_for_path(file_path) + hooks = config.get('hooks', {}).get(event_type, []) + + return filter_hooks_for_file(hooks, file_path, tool, self.project_root) + + +# Convenience function +def effective_config(file_path: str) -> Dict[str, Any]: + """Get the effective configuration for a file path.""" + loader = HierarchicalConfigLoader() + return loader.get_config_for_path(file_path) \ No newline at end of file diff --git a/hooks/config_merger.py b/hooks/config_merger.py new file mode 100644 index 0000000..c499b3c --- /dev/null +++ b/hooks/config_merger.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +"""Configuration merging utilities for Claude Hooks.""" + +from typing import Dict, List, Any + + +def merge_configs(base: Dict[str, Any], overlay: Dict[str, Any]) -> Dict[str, Any]: + """Merge two configurations, with overlay taking precedence.""" + result = base.copy() + + # Simple merge for non-hook fields + for key, value in overlay.items(): + if key not in ['hooks', 'exclude']: + result[key] = value + + # Merge hooks + result['hooks'] = merge_hooks( + base.get('hooks', {}), + overlay.get('hooks', {}), + overlay.get('exclude', []) + ) + + return result + + +def merge_hooks(base_hooks: Dict, overlay_hooks: Dict, + exclude_ids: List[str]) -> Dict: + """Merge hook configurations.""" + result = {} + + # Process each event type + for event_type in ['pre-tool', 'post-tool', 'stop']: + merged = merge_hook_list( + base_hooks.get(event_type, []), + overlay_hooks.get(event_type, []), + exclude_ids + ) + if merged: + result[event_type] = merged + + return result + + +def merge_hook_list(base_list: List[Dict], overlay_list: List[Dict], + exclude_ids: List[str]) -> List[Dict]: + """Merge two lists of hooks.""" + # Create lookup for overlay hooks + overlay_by_id = {h.get('id'): h for h in overlay_list if h.get('id')} + + merged = [] + + # Add base hooks (excluding overridden/excluded) + for hook in base_list: + hook_id = hook.get('id') + if hook_id in exclude_ids: + continue + if hook_id in overlay_by_id: + continue + merged.append(hook.copy()) + + # Add overlay hooks + merged.extend(h.copy() for h in overlay_list) + + return merged \ No newline at end of file diff --git a/hooks/doc-compliance.sh b/hooks/doc-compliance.sh deleted file mode 100755 index a1832bb..0000000 --- a/hooks/doc-compliance.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/bin/bash - -# Simple Documentation Compliance Hook -# Basic checks for documentation standards - -HOOK_INPUT=$(cat) -EVENT_TYPE=$(echo "$HOOK_INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null) - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -NC='\033[0m' - -# Simple file checks -check_documentation() { - local file="$1" - - # Basic checks only - if [[ "$file" =~ \.(md|txt)$ ]]; then - if [ ! -s "$file" ]; then - echo -e "${RED}โŒ Empty documentation file: $file${NC}" - return 1 - fi - fi - - return 0 -} - -# Process files if provided -if echo "$HOOK_INPUT" | jq -e '.tool_input.file_path' >/dev/null 2>&1; then - FILE_PATH=$(echo "$HOOK_INPUT" | jq -r '.tool_input.file_path') - if [ -f "$FILE_PATH" ]; then - check_documentation "$FILE_PATH" - fi -fi - -echo '{"continue": true}' \ No newline at end of file diff --git a/hooks/hook_filter.py b/hooks/hook_filter.py new file mode 100644 index 0000000..bc3cc0e --- /dev/null +++ b/hooks/hook_filter.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""Hook filtering utilities for Claude Hooks.""" + +import os +import fnmatch +from typing import Dict, List, Any, Optional + + +def filter_hooks_for_file(hooks: List[Dict[str, Any]], file_path: str, + tool: Optional[str], project_root: str) -> List[Dict[str, Any]]: + """Filter hooks that apply to a specific file and tool.""" + applicable = [] + + for hook in hooks: + if hook_applies(hook, file_path, tool, project_root): + applicable.append(hook) + + # Sort by priority + return sorted(applicable, key=lambda h: -h.get('priority', 50)) + + +def hook_applies(hook: Dict[str, Any], file_path: str, + tool: Optional[str], project_root: str) -> bool: + """Check if a hook applies to a file and tool.""" + # Check if disabled + if hook.get('disabled', False): + return False + + # Check file patterns + if not matches_file_patterns(hook, file_path): + return False + + # Check tool filter + if not matches_tool_filter(hook, tool): + return False + + # Check directory filter + if not matches_directory_filter(hook, file_path, project_root): + return False + + return True + + +def matches_file_patterns(hook: Dict[str, Any], file_path: str) -> bool: + """Check if file matches hook patterns.""" + patterns = hook.get('file_patterns') + if not patterns: + return True + + filename = os.path.basename(file_path) + return any(fnmatch.fnmatch(filename, p) for p in patterns) + + +def matches_tool_filter(hook: Dict[str, Any], tool: Optional[str]) -> bool: + """Check if tool matches hook filter.""" + tools = hook.get('tools') + if not tools or '*' in tools: + return True + if not tool: + return False + return tool in tools + + +def matches_directory_filter(hook: Dict[str, Any], file_path: str, + project_root: str) -> bool: + """Check if file is in allowed directories.""" + directories = hook.get('directories') + if not directories: + return True + + try: + rel_path = os.path.relpath(file_path, project_root) + return any(is_in_directory(rel_path, d) for d in directories) + except ValueError: + return False + + +def is_in_directory(file_path: str, directory: str) -> bool: + """Check if file is in a specific directory.""" + starts_with_sep = file_path.startswith(directory + os.sep) + equals_dir = file_path == directory + starts_with_slash = ('/' in file_path and + file_path.startswith(directory + '/')) + return starts_with_sep or equals_dir or starts_with_slash \ No newline at end of file diff --git a/hooks/hook_validator.py b/hooks/hook_validator.py new file mode 100644 index 0000000..0d40a2e --- /dev/null +++ b/hooks/hook_validator.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +"""Hook configuration validation utilities.""" + +import json +import os +from pathlib import Path +from typing import List, Tuple, Dict, Any + + +def validate_config_file(config_path: Path, project_root: Path) -> Tuple[List[str], List[str]]: + """ + Validate a single configuration file. + Returns (errors, warnings) as lists of strings. + """ + errors = [] + warnings = [] + rel_path = config_path.relative_to(project_root) + + # Read and parse file + content = _read_file_content(config_path) + if not content: + warnings.append(f"{rel_path}: Empty configuration file") + return errors, warnings + + # Parse JSON + config = _parse_json_config(content, rel_path, errors) + if not config: + return errors, warnings + + # Validate structure + if 'hooks' in config: + _validate_hooks_structure(config['hooks'], rel_path, errors, warnings, project_root) + + return errors, warnings + + +def _read_file_content(config_path: Path) -> str: + """Read file content safely.""" + try: + with open(config_path, 'r') as f: + content = f.read() + return content.strip() + except Exception: + return "" + + +def _parse_json_config(content: str, rel_path: Path, errors: List[str]) -> Dict[str, Any]: + """Parse JSON configuration.""" + try: + return json.loads(content) + except json.JSONDecodeError as e: + errors.append(f"{rel_path}: Invalid JSON - {e}") + return {} + + +def _validate_hooks_structure(hooks: Any, rel_path: Path, errors: List[str], + warnings: List[str], project_root: Path): + """Validate the hooks structure.""" + if not isinstance(hooks, dict): + errors.append(f"{rel_path}: 'hooks' must be an object") + return + + for event_type, hook_list in hooks.items(): + _validate_event_type(event_type, hook_list, rel_path, errors, warnings, project_root) + + +def _validate_event_type(event_type: str, hook_list: Any, rel_path: Path, + errors: List[str], warnings: List[str], project_root: Path): + """Validate a single event type and its hooks.""" + valid_events = ['pre-tool', 'post-tool', 'stop'] + + if event_type not in valid_events: + warnings.append(f"{rel_path}: Unknown event type '{event_type}'") + + if not isinstance(hook_list, list): + errors.append(f"{rel_path}: hooks.{event_type} must be an array") + return + + for i, hook in enumerate(hook_list): + _validate_single_hook(hook, event_type, i, rel_path, errors, warnings, project_root) + + +def _validate_single_hook(hook: Dict[str, Any], event_type: str, index: int, + rel_path: Path, errors: List[str], warnings: List[str], + project_root: Path): + """Validate a single hook configuration.""" + # Check required fields + if 'id' not in hook: + errors.append(f"{rel_path}: hooks.{event_type}[{index}] missing required 'id' field") + + if 'script' not in hook: + errors.append(f"{rel_path}: hooks.{event_type}[{index}] missing required 'script' field") + + # Check script exists + if 'script' in hook: + script_path = project_root / 'hooks' / hook['script'] + if not script_path.exists(): + warnings.append(f"{rel_path}: Script '{hook['script']}' not found") + + +def find_all_config_files(root: Path) -> List[Path]: + """Find all hook configuration files in the project.""" + configs = [] + + # Check for root config + root_config = root / '.claude' / 'hookconfig.json' + if root_config.exists(): + configs.append(root_config) + + # Find all .claude-hooks.json files + for config_path in root.rglob('.claude-hooks.json'): + configs.append(config_path) + + return sorted(configs) \ No newline at end of file diff --git a/hooks/lint-check.sh b/hooks/lint-check.sh deleted file mode 100755 index e26ade0..0000000 --- a/hooks/lint-check.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash - -# Lint checker hook -# Simple and focused - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -source "$SCRIPT_DIR/common/linting.sh" - -# Run check -if run_lint_check "${PROJECT_DIR:-.}"; then - echo "โœ… Lint check passed" - echo '{"continue": true}' -else - echo "โŒ Lint errors found" >&2 - [ -n "$LINT_OUTPUT" ] && echo "$LINT_OUTPUT" >&2 - - cat < 100: + return False, "Lines exceed 100 characters. Please break long lines." + return True, None + +def check_python_quality(lines): + """Check Python code quality.""" + for line in lines: + is_valid, msg = check_python_indentation(line) + if not is_valid: + return False, msg + return True, None + +def check_ruby_quality(lines): + """Check Ruby code quality.""" + for line in lines: + is_valid, msg = check_ruby_indentation(line) + if not is_valid: + return False, msg + return True, None + +def check_python_indentation(line): + """Check Python line indentation.""" + if not line or line.startswith('#'): + return True, None + + match = re.match(r'^(\s+)', line) + if not match: + return True, None + + indent_len = len(match.group(1)) + if indent_len % 4 != 0: + msg = "Python indentation should be 4 spaces (PEP8). Found inconsistent indentation." + return False, msg + return True, None + +def check_ruby_indentation(line): + """Check Ruby line indentation.""" + if not line or line.startswith('#'): + return True, None + + match = re.match(r'^(\s+)', line) + if not match: + return True, None + + indent_len = len(match.group(1)) + if indent_len % 2 != 0: + msg = "Ruby indentation should be 2 spaces. Found inconsistent indentation." + return False, msg + return True, None + +def check_content_quality(content, file_ext): + """Check content quality based on file type.""" + lines = content.split('\n') + + if file_ext in ['ts', 'tsx', 'js', 'jsx']: + return check_js_quality(lines) + + if file_ext == 'py': + return check_python_quality(lines) + + if file_ext == 'rb': + return check_ruby_quality(lines) + + return True, None + +def parse_input(): + """Parse and validate input.""" + try: + return json.loads(sys.stdin.read()) + except: + return None + +def is_valid_event(input_data): + """Check if this is a valid event to process.""" + hook_event = input_data.get('hook_event_name', '') + if hook_event != 'PreToolUse': + return False + + tool_name = input_data.get('tool_name', '') + if tool_name not in ['Write', 'Edit', 'MultiEdit']: + return False + + return True + +def get_file_extension(file_path): + """Extract file extension.""" + if not file_path or '.' not in file_path: + return '' + return file_path.split('.')[-1] + +def should_skip_file(ext): + """Check if file type should be skipped.""" + skip_extensions = ['md', 'txt', 'json', 'yml', 'yaml', 'xml', 'html', 'css'] + return ext in skip_extensions + +def extract_content(tool_name, tool_input): + """Extract content based on tool type.""" + if tool_name == 'Write': + return tool_input.get('content', '') + if tool_name == 'Edit': + return tool_input.get('new_string', '') + return None + +def check_multi_edit_content(edits, ext): + """Check MultiEdit content quality.""" + for edit in edits: + new_string = edit.get('new_string', '') + if not new_string: + continue + + is_valid, reason = check_content_quality(new_string, ext) + if not is_valid: + print(f"โš ๏ธ Code quality suggestion: {reason}", file=sys.stderr) + +def process_tool_content(input_data, tool_input, ext): + """Process tool content based on tool type.""" + tool_name = input_data.get('tool_name', '') + + if tool_name == 'MultiEdit': + edits = tool_input.get('edits', []) + check_multi_edit_content(edits, ext) + return + + # Handle Write/Edit + content = extract_content(tool_name, tool_input) + if not content: + return + + is_valid, reason = check_content_quality(content, ext) + if not is_valid: + print(f"โš ๏ธ Code quality suggestion: {reason}", file=sys.stderr) + +def main(): + """Main entry point.""" + input_data = parse_input() + if not input_data or not is_valid_event(input_data): + sys.exit(0) + + tool_input = input_data.get('tool_input', {}) + file_path = tool_input.get('file_path', '') + if not file_path: + sys.exit(0) + + ext = get_file_extension(file_path) + if should_skip_file(ext): + sys.exit(0) + + process_tool_content(input_data, tool_input, ext) + sys.exit(0) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hooks/quality_validator.py b/hooks/quality_validator.py new file mode 100644 index 0000000..a8e695c --- /dev/null +++ b/hooks/quality_validator.py @@ -0,0 +1,171 @@ +#!/usr/bin/env python3 +"""Portable code quality validator - cleaner version.""" + +import json +import sys +import os +import re +import glob +from pathlib import Path + +# Configuration +CONFIG = { + 'max_func_len': 30, + 'max_file_len': 200, + 'max_line_len': 100, + 'max_nest': 4, +} + +def get_file_type(filepath): + """Determine file type from extension.""" + ext = Path(filepath).suffix.lower() + if ext in ['.ts', '.tsx', '.js', '.jsx']: + return 'js' + elif ext == '.py': + return 'python' + return 'unknown' + +def check_line_length(lines): + """Check for lines that are too long.""" + violations = [] + for i, line in enumerate(lines, 1): + length = len(line.rstrip()) + if length <= CONFIG['max_line_len']: + continue + msg = f"Line {i}: {length} chars (max: {CONFIG['max_line_len']})" + violations.append(msg) + return violations + +def check_python_funcs(lines): + """Check Python function lengths.""" + violations = [] + func_start = 0 + func_name = "" + + for i, line in enumerate(lines, 1): + match = re.match(r'^\s*def\s+(\w+)', line) + if not match: + continue + + if func_start > 0 and i - func_start > CONFIG['max_func_len']: + violations.append(f"Function '{func_name}': {i - func_start} lines") + + func_name = match.group(1) + func_start = i + + # Check last function + if func_start > 0: + length = len(lines) - func_start + 1 + if length > CONFIG['max_func_len']: + violations.append(f"Function '{func_name}': {length} lines") + + return violations + +def check_nesting(lines, file_type): + """Check nesting depth.""" + violations = [] + max_nest = CONFIG['max_nest'] + + for i, line in enumerate(lines, 1): + if not line.strip(): + continue + + # Count indentation + indent = len(line) - len(line.lstrip()) + depth = indent // 4 if file_type == 'python' else indent // 2 + + if depth > max_nest: + violations.append(f"Line {i}: nesting {depth} (max: {max_nest})") + + return violations + +def read_file_lines(filepath): + """Read file and return lines.""" + try: + with open(filepath, 'r') as f: + return f.readlines() + except: + return None + +def validate_file(filepath): + """Validate a single file.""" + if not os.path.exists(filepath): + return [] + + file_type = get_file_type(filepath) + if file_type == 'unknown': + return [] + + lines = read_file_lines(filepath) + if lines is None: + return [] + + violations = [] + + # File length + if len(lines) > CONFIG['max_file_len']: + violations.append( + f"File: {len(lines)} lines (max: {CONFIG['max_file_len']})" + ) + + # Various checks + violations.extend(check_line_length(lines)) + if file_type == 'python': + violations.extend(check_python_funcs(lines)) + violations.extend(check_nesting(lines, file_type)) + + return violations + +def handle_post_tool(data): + """Handle PostToolUse event.""" + tool = data.get('tool_name', '') + if tool not in ['Write', 'Edit', 'MultiEdit']: + return + + filepath = data.get('tool_input', {}).get('file_path', '') + if not filepath: + return + + violations = validate_file(filepath) + if violations: + # Provide strong warning but don't block + print(f"\nโš ๏ธ WARNING: Code quality issues in {filepath}:", file=sys.stderr) + for v in violations: + print(f" - {v}", file=sys.stderr) + print("\n๐Ÿšจ YOU WILL BE BLOCKED at session end if these aren't fixed!", file=sys.stderr) + print(" Fix these issues now to avoid being blocked later.\n", file=sys.stderr) + +def handle_stop(): + """Handle Stop event.""" + files = glob.glob('**/*.py', recursive=True) + all_violations = [] + + for filepath in files: + violations = validate_file(filepath) + for v in violations: + all_violations.append(f"{filepath}: {v}") + + if all_violations: + msg = "Quality issues found:\n" + msg += "\n".join(f" - {v}" for v in all_violations) + print(json.dumps({"decision": "block", "reason": msg})) + sys.exit(0) + +def main(): + """Main entry point.""" + try: + data = json.loads(sys.stdin.read()) + except: + sys.exit(0) + + event = data.get('hook_event_name', '') + + if event == 'PostToolUse': + handle_post_tool(data) + elif event == 'Stop': + handle_stop() + + sys.exit(0) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hooks/stop-hook.py b/hooks/stop-hook.py new file mode 100755 index 0000000..ec08c10 --- /dev/null +++ b/hooks/stop-hook.py @@ -0,0 +1,51 @@ +#!/usr/bin/env python3 +"""Direct Stop Hook Handler - Processes Stop events and runs code quality checks.""" + +import json +import sys +import subprocess +import os + +def parse_input(): + """Parse input from stdin.""" + input_text = sys.stdin.read() + try: + input_data = json.loads(input_text) + return input_text, input_data + except: + return None, None + +def run_validator(input_text): + """Run the quality validator.""" + script_dir = os.path.dirname(os.path.abspath(__file__)) + validator_path = os.path.join(script_dir, 'portable-quality-validator.py') + + try: + result = subprocess.run( + ['python3', validator_path], + input=input_text, + capture_output=True, + text=True + ) + return result.stdout + except: + return None + +def main(): + """Main entry point.""" + input_text, input_data = parse_input() + if not input_data: + return + + # Only process Stop events + event_type = input_data.get('hook_event_name', '') + if event_type != 'Stop': + return + + # Run validator and print output if any + validator_output = run_validator(input_text) + if validator_output: + print(validator_output.strip()) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hooks/task-completion-notify.py b/hooks/task-completion-notify.py new file mode 100755 index 0000000..caba7e3 --- /dev/null +++ b/hooks/task-completion-notify.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +"""Claude Code Hook: Task Completion Notifications.""" + +import json +import sys +import os + +from notification_utils import ( + log_to_stderr, + load_pushover_config, + send_notification +) + + +def parse_input_data(): + """Parse input data from stdin.""" + try: + return json.loads(sys.stdin.read()) + except: + return None + + +def handle_stop_event(input_data): + """Handle Stop event.""" + event_name = input_data.get('hook_event_name', '') + tool = input_data.get('tool', '') or input_data.get('tool_name', '') + + if event_name != 'Stop' and tool != 'stop': + return + + project_name = os.path.basename(os.getcwd()) + + # Check if Pushover is configured + if not load_pushover_config(): + log_to_stderr("๐Ÿ’ก Want notifications when Claude finishes? Set up Pushover:") + log_to_stderr(" 1. Get the app: https://pushover.net/clients") + log_to_stderr(" 2. Add to .env: PUSHOVER_USER_KEY=... and PUSHOVER_APP_TOKEN=...") + return + + send_notification( + "Claude Code Finished", + f"All tasks completed in {project_name}", + priority=2 # High priority for completion + ) + + +def main(): + """Main entry point.""" + input_data = parse_input_data() + if not input_data: + print(json.dumps({"action": "continue"})) + return + + handle_stop_event(input_data) + + # Always continue + print(json.dumps({"action": "continue"})) + + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/hooks/task-completion-notify.sh b/hooks/task-completion-notify.sh deleted file mode 100755 index aad648c..0000000 --- a/hooks/task-completion-notify.sh +++ /dev/null @@ -1,218 +0,0 @@ -#!/bin/bash - -# Claude Code Hook: Task Completion Notifications -# Sends notifications when Claude completes certain tasks - -# Enable debug logging if .env has it set -if [ -f ".env" ] && grep -q "CLAUDE_HOOK_LOG=" ".env" 2>/dev/null; then - export $(grep "CLAUDE_HOOK_LOG=" ".env" | xargs) -fi - -# Debug: Log execution context -if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] Hook executing in directory: $PWD" >> "$CLAUDE_HOOK_LOG" - echo "[$(date)] Environment has PUSHOVER keys: USER=$([ -n "$PUSHOVER_USER_KEY" ] && echo "YES" || echo "NO"), APP=$([ -n "$PUSHOVER_APP_TOKEN" ] && echo "YES" || echo "NO")" >> "$CLAUDE_HOOK_LOG" -fi - -# Parse input from Claude -INPUT=$(cat) - -# Debug: Log raw input -if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] Raw input: $INPUT" >> "$CLAUDE_HOOK_LOG" -fi - -# Try both old and new formats for compatibility -TOOL=$(echo "$INPUT" | jq -r '.tool // .tool_name // empty') -COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty') -DESCRIPTION=$(echo "$INPUT" | jq -r '.tool_input.description // empty') -CONTENT=$(echo "$INPUT" | jq -r '.tool_input.content // empty') -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty') - -# Function to send macOS notification -send_macos_notification() { - local title="$1" - local message="$2" - local sound="${3:-default}" - - if command -v osascript &> /dev/null; then - osascript -e "display notification \"$message\" with title \"Claude Code\" subtitle \"$title\" sound name \"$sound\"" - fi -} - -# Function to send Linux notification -send_linux_notification() { - local title="$1" - local message="$2" - - if command -v notify-send &> /dev/null; then - notify-send "Claude Code: $title" "$message" --icon=dialog-information - fi -} - -# Function to send terminal bell/beep -send_terminal_notification() { - printf '\a' -} - -# Load Pushover configuration from multiple sources -load_pushover_config() { - # Check if already configured via environment - if [ -n "$PUSHOVER_USER_KEY" ] && [ -n "$PUSHOVER_APP_TOKEN" ]; then - return 0 - fi - - # Get the actual working directory where Claude is running - local working_dir="${PWD:-$(pwd)}" - - # Try loading from various .env files - for env_file in \ - "$working_dir/.env" \ - "$working_dir/.claude/pushover.env" \ - ".env" \ - ".claude/pushover.env" \ - "$HOME/.claude/pushover.env"; do - if [ -f "$env_file" ]; then - # Debug logging - if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] Attempting to load env from: $env_file" >> "$CLAUDE_HOOK_LOG" - fi - # Safely source the env file - set -a - source "$env_file" 2>/dev/null - set +a - # Check if keys were loaded - if [ -n "$PUSHOVER_USER_KEY" ] && [ -n "$PUSHOVER_APP_TOKEN" ]; then - if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] Successfully loaded Pushover keys from: $env_file" >> "$CLAUDE_HOOK_LOG" - fi - return 0 - fi - fi - done - - # Log failure to find keys - if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] Failed to load Pushover keys from any .env file" >> "$CLAUDE_HOOK_LOG" - fi - - # Return success if we have both keys - [ -n "$PUSHOVER_USER_KEY" ] && [ -n "$PUSHOVER_APP_TOKEN" ] -} - -# Function to send Pushover notification -send_pushover_notification() { - local title="$1" - local message="$2" - local priority="${3:-1}" # Default: normal priority - - # Load configuration - if ! load_pushover_config; then - # Output helpful message to stderr (will be shown to user) - echo "โš ๏ธ Pushover notification skipped - API keys not configured" >&2 - echo "To enable Pushover notifications:" >&2 - echo "1. Get Pushover app (\$5): https://pushover.net/clients" >&2 - echo "2. Create an app: https://pushover.net/apps/build" >&2 - echo "3. Add to your project's .env file:" >&2 - echo " PUSHOVER_USER_KEY=your_user_key" >&2 - echo " PUSHOVER_APP_TOKEN=your_app_token" >&2 - return 0 - fi - - # Determine project name for context - local project_name=$(basename "$PWD") - - # Build curl command - local curl_cmd="curl -s --form-string \"token=$PUSHOVER_APP_TOKEN\" \ - --form-string \"user=$PUSHOVER_USER_KEY\" \ - --form-string \"title=Claude Code: $project_name\" \ - --form-string \"message=$title - $message\" \ - --form-string \"priority=$priority\"" - - # Add retry/expire for emergency priority - if [ "$priority" = "2" ]; then - curl_cmd="$curl_cmd --form-string \"retry=30\" --form-string \"expire=300\"" - fi - - # Send notification - eval "$curl_cmd https://api.pushover.net/1/messages.json" > /dev/null 2>&1 - - # Log if debugging enabled - if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] Pushover sent: $title - $message (priority=$priority)" >> "$CLAUDE_HOOK_LOG" - fi -} - -# Detect platform and send appropriate notification -send_notification() { - local title="$1" - local message="$2" - local priority="${3:-1}" # Default priority - - # Only send Pushover notification (no platform-specific notifications) - send_pushover_notification "$title" "$message" "$priority" -} - -# Track task completions -NOTIFY_TRIGGERS=( - # File operations - "created file" - "updated file" - "deleted file" - - # Git operations - "git commit" - "git push" - "created PR" - - # Build/test operations - "npm run build" - "npm test" - "anchor build" - "anchor test" - - # Installation operations - "npm install" - "yarn add" -) - -# Check if this is a task worth notifying about -should_notify=false -notification_title="" -notification_message="" -notification_priority=1 # Default: normal priority - -# Check if this is a Stop event (when Claude finishes) -EVENT_NAME=$(echo "$INPUT" | jq -r '.hook_event_name // empty') -if [ "$EVENT_NAME" = "Stop" ] || [ "$TOOL" = "stop" ] || [ "$1" = "stop" ]; then - notification_title="Claude Code Finished" - notification_message="All tasks completed in $(basename "$PWD")" - should_notify=true - notification_priority=2 # High priority for completion -fi - -# Debug logging -if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] Tool: $TOOL, Should notify: $should_notify, Title: $notification_title" >> "$CLAUDE_HOOK_LOG" -fi - -# Send notification if triggered -if [ "$should_notify" = true ] && [ -n "$notification_title" ]; then - # For Stop events, check if Pushover is configured before calling send_notification - if [ "$EVENT_NAME" = "Stop" ] && ! load_pushover_config; then - echo "๐Ÿ’ก Want notifications when Claude finishes? Set up Pushover:" >&2 - echo " 1. Get the app: https://pushover.net/clients" >&2 - echo " 2. Add to .env: PUSHOVER_USER_KEY=... and PUSHOVER_APP_TOKEN=..." >&2 - else - send_notification "$notification_title" "$notification_message" "$notification_priority" - fi - - # Log to file for debugging (optional) - if [ -n "$CLAUDE_HOOK_LOG" ]; then - echo "[$(date)] $notification_title: $notification_message (priority=$notification_priority)" >> "$CLAUDE_HOOK_LOG" - fi -fi - -# Always pass through the input -echo "$INPUT" -exit 0 \ No newline at end of file diff --git a/hooks/test-blocker.sh b/hooks/test-blocker.sh deleted file mode 100755 index 2ae7012..0000000 --- a/hooks/test-blocker.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -# Test Hook: Always Blocks with JSON Output -# This hook demonstrates the blocking mechanism with continue: false - -echo "๐Ÿšซ TEST BLOCKER HOOK ACTIVATED" >&2 - -# Read input from stdin -INPUT=$(cat) -EVENT_TYPE=$(echo "$INPUT" | jq -r '.hook_event_name // empty' 2>/dev/null) -TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null) -FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .file_path // empty' 2>/dev/null) - -echo "Event: $EVENT_TYPE, Tool: $TOOL_NAME, File: $FILE_PATH" >&2 - -# Always output JSON that blocks execution -cat <&2 -exit 0 \ No newline at end of file diff --git a/hooks/test-check.sh b/hooks/test-check.sh deleted file mode 100755 index a4345e1..0000000 --- a/hooks/test-check.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/bin/bash - -# Single purpose test runner -# Just runs tests - nothing else - -PROJECT_DIR="${PROJECT_DIR:-.}" - -# Check if test script exists -if [ ! -f "$PROJECT_DIR/package.json" ] || ! grep -q '"test"' "$PROJECT_DIR/package.json"; then - echo "No test script found in package.json" - exit 0 -fi - -cd "$PROJECT_DIR" || exit 2 - -# Run tests and capture output -TEST_OUTPUT=$(npm test 2>&1) -TEST_EXIT_CODE=$? - -if [ $TEST_EXIT_CODE -eq 0 ]; then - echo "โœ… Tests passed" - echo "$TEST_OUTPUT" - exit 0 -else - echo "โŒ Tests failed" >&2 - echo "$TEST_OUTPUT" >&2 - - # Extract failed test information - FAILED_COUNT=$(echo "$TEST_OUTPUT" | grep -E "(failing|failed|FAILED)" | grep -oE "[0-9]+" | head -1 || echo "some") - - # Extract test names that failed - FAILED_TESTS=$(echo "$TEST_OUTPUT" | grep -E "(โœ—|ร—|FAIL|failing)" | head -5 | sed 's/^[[:space:]]*/โ€ข /') - - # Check for specific test commands - TEST_CMD="npm test" - if grep -q '"test:watch"' "$PROJECT_DIR/package.json" 2>/dev/null; then - WATCH_CMD="npm run test:watch" - fi - - # Build reason message for Claude - REASON="$FAILED_COUNT tests are failing. Please fix these test failures:\n\n$FAILED_TESTS\n\nRun '$TEST_CMD' to see full test output." - if [ -n "$WATCH_CMD" ]; then - REASON="$REASON\nTip: Use '$WATCH_CMD' for continuous testing while fixing." - fi - - # Output JSON for Claude Code - cat <&2 - [ -n "$TS_OUTPUT" ] && echo "$TS_OUTPUT" >&2 - - # Count errors - ERROR_COUNT=$(echo "$TS_OUTPUT" | grep -cE "error TS[0-9]+:" || echo "1") - - cat < max_len: + violations.append( + f"Function '{func_state['name']}' is {length} lines long (max: {max_len})" + ) + return True + +def _process_js_line(line, i, func_state, violations, max_len): + """Process a single line for JavaScript function checking.""" + if not func_state['in_func']: + _process_js_function_start(line, i, func_state) + return + + # In function - check if it ends + if _check_js_function_end(func_state, line, i, violations, max_len): + func_state['in_func'] = False + +def check_js_functions(lines, violations, max_len): + """Check JavaScript/TypeScript functions.""" + func_state = {'in_func': False, 'start': 0, 'name': '', 'braces': 0} + + for i, line in enumerate(lines, 1): + _process_js_line(line, i, func_state, violations, max_len) + + +def _is_py_function_start(line): + """Check if line starts a Python function.""" + return re.match(r'^\s*(?:async\s+)?def\s+\w+\s*\(', line) + +def _validate_py_function_length(name, start, end, max_len, violations): + """Validate Python function length.""" + length = end - start + if length > max_len: + violations.append( + f"Function '{name}' is {length} lines long (max: {max_len})" + ) + +def check_py_functions(lines, violations, max_len): + """Check Python functions.""" + in_func = False + func_start = 0 + func_name = "" + + for i, line in enumerate(lines, 1): + if not _is_py_function_start(line): + continue + + # Check previous function if exists + if in_func and func_name: + _validate_py_function_length(func_name, func_start, i, max_len, violations) + + # Start new function + match = re.match(r'^\s*(?:async\s+)?def\s+(\w+)', line) + func_name = match.group(1) if match else "unknown" + func_start = i + in_func = True + + # Handle last function + if in_func and func_name: + _validate_py_function_length(func_name, func_start, len(lines) + 1, max_len, violations) + +def check_function_length(lines, file_type): + """Check for functions that are too long.""" + violations = [] + max_len = CONFIG['max_function_length'] + + if file_type in ['typescript', 'javascript']: + check_js_functions(lines, violations, max_len) + elif file_type == 'python': + check_py_functions(lines, violations, max_len) + + return violations + +def check_file_length(lines): + """Check if file is too long.""" + if len(lines) > CONFIG['max_file_length']: + return [f"File is {len(lines)} lines long (max: {CONFIG['max_file_length']})"] + return [] + +def check_line_length(lines): + """Check for lines that are too long.""" + violations = [] + max_len = CONFIG['max_line_length'] + + for i, line in enumerate(lines, 1): + length = len(line.rstrip()) + if length <= max_len: + continue + + violations.append( + f"Line {i} is {length} characters long (max: {max_len})" + ) + return violations + +def _calculate_indent_level(line): + """Calculate indentation level for a line.""" + indent = len(line) - len(line.lstrip()) + if '\t' not in line[:indent]: + return indent + + tabs = line[:indent].count('\t') + spaces = line[:indent].count(' ') + return tabs * 4 + spaces + +def check_nesting_depth(lines, file_type): + """Check for excessive nesting depth.""" + violations = [] + py_max = CONFIG.get('python_max_nesting', 3) + other_max = CONFIG['max_nesting_depth'] + max_nest = py_max if file_type == 'python' else other_max + divisor = 4 if file_type == 'python' else 2 + + for i, line in enumerate(lines, 1): + if not line.strip(): + continue + + indent = _calculate_indent_level(line) + nest = indent // divisor + + if nest <= max_nest: + continue + + violations.append( + f"Line {i} has nesting depth of {nest} (max: {max_nest})" + ) + + return violations + +def get_fix_instruction(violation): + """Get actionable fix instruction for a violation.""" + if "lines long (max:" in violation: + if "Function" in violation: + return "Split into smaller helper functions. Extract logical sections." + else: + return "Split this file into multiple modules (e.g., validators.py, handlers.py)" + elif "characters long" in violation: + return "Break this line into multiple lines using parentheses or line continuation" + elif "nesting depth" in violation: + return "Extract nested logic into separate functions or use early returns" + return "Refactor to meet quality standards" \ No newline at end of file diff --git a/install-hooks.py b/install-hooks.py new file mode 100755 index 0000000..2d32f59 --- /dev/null +++ b/install-hooks.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +"""Install Claude Code hooks into a project.""" + +import os +import shutil +import json +from pathlib import Path + +def copy_hooks(source_dir, target_dir): + """Copy hook files to target directory.""" + for hook_file in source_dir.glob('*.py'): + shutil.copy2(hook_file, target_dir / hook_file.name) + os.chmod(target_dir / hook_file.name, 0o755) + +def create_hook_config(command): + """Create a hook configuration.""" + return [{ + "hooks": [{ + "type": "command", + "command": command + }] + }] + +def create_settings(): + """Create settings.json configuration.""" + return { + "hooks": { + "Stop": create_hook_config("python3 .claude/hooks/universal-stop.py"), + "PreToolUse": create_hook_config("python3 .claude/hooks/universal-pre-tool.py"), + "PostToolUse": create_hook_config("python3 .claude/hooks/universal-post-tool.py") + } + } + +def update_gitignore(): + """Add Claude local settings to .gitignore.""" + gitignore_path = Path('.gitignore') + if not gitignore_path.exists(): + return + + with open(gitignore_path, 'r') as f: + content = f.read() + + if '.claude/settings.local.json' not in content: + with open(gitignore_path, 'a') as f: + f.write('\n# Claude Code local settings\n') + f.write('.claude/settings.local.json\n') + +def print_success_message(): + """Print installation success message.""" + print("โœ… Claude Code hooks installed successfully!") + print("\nThe following hooks are now active:") + print(" - Code quality validation (function/line length, nesting)") + print(" - Package age validation (blocks old npm packages)") + print(" - Task completion notifications (optional)") + print("\nHooks will run automatically when using Claude Code.") + +def main(): + """Install hooks into .claude/settings.json.""" + # Create directories + claude_dir = Path('.claude') + claude_dir.mkdir(exist_ok=True) + + project_hooks_dir = claude_dir / 'hooks' + project_hooks_dir.mkdir(exist_ok=True) + + # Copy hook files + source_hooks_dir = Path(__file__).parent / 'hooks' + copy_hooks(source_hooks_dir, project_hooks_dir) + + # Create settings.json + settings_file = claude_dir / 'settings.json' + with open(settings_file, 'w') as f: + json.dump(create_settings(), f, indent=2) + + # Update .gitignore + update_gitignore() + + # Print success message + print_success_message() + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/lib/commands/init.d.ts b/lib/commands/init.d.ts deleted file mode 100644 index d42ac32..0000000 --- a/lib/commands/init.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -interface InitOptions { - level?: string; - customMode?: boolean; -} -export declare function init(options: InitOptions): Promise; -export {}; -//# sourceMappingURL=init.d.ts.map \ No newline at end of file diff --git a/lib/commands/init.d.ts.map b/lib/commands/init.d.ts.map deleted file mode 100644 index 8a9f727..0000000 --- a/lib/commands/init.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"AAyIA,UAAU,WAAW;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoE9D"} \ No newline at end of file diff --git a/lib/commands/init.js b/lib/commands/init.js deleted file mode 100644 index 4daa0c6..0000000 --- a/lib/commands/init.js +++ /dev/null @@ -1,190 +0,0 @@ -import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs'; -import { join } from 'path'; -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import { manage } from './manage.js'; -import { SETTINGS_LOCATIONS } from '../settings-locations.js'; -const DEFAULT_SETTINGS = { - "_comment": "Claude Code hooks configuration (using claude-code-hooks-cli)", - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "pattern": "^(npm\\s+(install|i|add)|yarn\\s+(add|install))\\s+", - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec check-package-age" - } - ] - }, - { - "matcher": "Bash", - "pattern": "^git\\s+commit", - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec typescript-check --exclude='node_modules,dist,build,.next,coverage'" - }, - { - "type": "command", - "command": "CHECK_STAGED=true npx claude-code-hooks-cli exec lint-check --exclude='node_modules,dist,build,.next,coverage'" - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Write|Edit|MultiEdit", - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec code-quality-validator" - }, - { - "type": "command", - "command": "npx claude-code-hooks-cli exec claude-context-updater" - } - ] - } - ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec task-completion-notify" - } - ] - } - ] - } -}; -// Settings file locations -// Helper function to count hooks in a settings file -function countHooks(settingsPath) { - if (!existsSync(settingsPath)) - return 0; - try { - const settings = JSON.parse(readFileSync(settingsPath, 'utf-8')); - let count = 0; - if (settings.hooks) { - Object.values(settings.hooks).forEach(eventHooks => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach(hookGroup => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - count += hookGroup.hooks.length; - } - }); - } - }); - } - return count; - } - catch (err) { - return 0; - } -} -async function quickSetup(location) { - const targetDir = location.dir; - const fileName = location.file; - const settingsPath = join(targetDir, fileName); - // Create directory if it doesn't exist - if (!existsSync(targetDir)) { - mkdirSync(targetDir, { recursive: true }); - } - // Check if settings already exist - if (existsSync(settingsPath)) { - const { overwrite } = await inquirer.prompt([ - { - type: 'confirm', - name: 'overwrite', - message: chalk.yellow(`Settings already exist at ${settingsPath}. Overwrite?`), - default: false - } - ]); - if (!overwrite) { - console.log(chalk.gray('Cancelled.')); - return; - } - } - // Write settings - writeFileSync(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2)); - console.log(); - console.log(chalk.green(`โœ“ Created ${settingsPath}`)); - console.log(); - console.log('Hooks configured:'); - console.log(' โ€ข Package age validation (npm/yarn install)'); - console.log(' โ€ข Code quality validation (file edits)'); - console.log(' โ€ข TypeScript/lint validation (before stop)'); - console.log(); - console.log(chalk.cyan('Next steps:')); - console.log(` 1. Install this package: ${chalk.white('npm install -D claude-code-hooks-cli')}`); - console.log(` 2. Hooks will run automatically in Claude Code`); - console.log(` 3. Run ${chalk.white('npx claude-code-hooks-cli init')} again to customize`); -} -export async function init(options) { - // If custom mode is explicitly requested (from manage alias), go straight to manage - if (options.customMode) { - return manage(); - } - let selectedLocation; - // If level option is provided, find the corresponding location - if (options.level) { - selectedLocation = SETTINGS_LOCATIONS.find(loc => loc.level === options.level); - if (!selectedLocation) { - console.error(chalk.red(`Invalid level: ${options.level}`)); - console.log(chalk.yellow('Valid options: project, project-alt, local, global')); - process.exit(1); - } - } - else { - // First, ask about setup mode - const { setupMode } = await inquirer.prompt([ - { - type: 'list', - name: 'setupMode', - message: 'How would you like to set up hooks?', - choices: [ - { - name: `${chalk.cyan('Quick setup')} ${chalk.gray('(recommended defaults)')}`, - value: 'quick' - }, - { - name: `${chalk.cyan('Custom setup')} ${chalk.gray('(choose your hooks)')}`, - value: 'custom' - } - ], - default: 0 - } - ]); - if (setupMode === 'custom') { - // Go to manage interface - return manage(); - } - // Quick setup - ask for location - const locations = SETTINGS_LOCATIONS.map(loc => { - const count = countHooks(loc.path); - const existsText = count > 0 ? ` ${chalk.yellow(`(exists with ${count} hooks)`)}` : ''; - return { - name: `${chalk.cyan(loc.display)} ${chalk.gray(loc.description)}${existsText}`, - value: loc - }; - }); - const { location } = await inquirer.prompt([ - { - type: 'list', - name: 'location', - message: 'Where would you like to create the settings file?', - choices: locations, - default: 0 - } - ]); - selectedLocation = location; - } - // Proceed with quick setup - if (selectedLocation) { - await quickSetup(selectedLocation); - } -} -//# sourceMappingURL=init.js.map \ No newline at end of file diff --git a/lib/commands/init.js.map b/lib/commands/init.js.map deleted file mode 100644 index cb5bd23..0000000 --- a/lib/commands/init.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"init.js","sourceRoot":"","sources":["../../src/commands/init.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lib/commands/list.d.ts b/lib/commands/list.d.ts deleted file mode 100644 index f7f115c..0000000 --- a/lib/commands/list.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function list(): Promise; -//# sourceMappingURL=list.d.ts.map \ No newline at end of file diff --git a/lib/commands/list.d.ts.map b/lib/commands/list.d.ts.map deleted file mode 100644 index 5579964..0000000 --- a/lib/commands/list.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AA8BA,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA4B1C"} \ No newline at end of file diff --git a/lib/commands/list.js b/lib/commands/list.js deleted file mode 100644 index b260a7e..0000000 --- a/lib/commands/list.js +++ /dev/null @@ -1,47 +0,0 @@ -import { readdirSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import chalk from 'chalk'; -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const HOOK_DESCRIPTIONS = { - // Single-purpose validation hooks - 'typescript-check': 'TypeScript type checking', - 'lint-check': 'Code linting (ESLint, etc.)', - 'test-check': 'Run test suite', - // Feature hooks - 'check-package-age': 'Prevents installation of outdated npm/yarn packages', - 'code-quality-validator': 'Enforces clean code standards (function length, nesting, etc.)', - 'claude-context-updater': 'Updates CLAUDE.md context file', - 'task-completion-notify': 'System notifications for completed tasks', - // Legacy hooks (still exist but not recommended) - 'stop-validation': '[LEGACY] Validates TypeScript/lint before stop - use individual checks instead', - 'validate-code': '[LEGACY] Use typescript-check and lint-check instead', - 'validate-on-completion': '[LEGACY] Use typescript-check and lint-check instead' -}; -export async function list() { - const hooksDir = join(__dirname, '../../hooks'); - console.log(chalk.cyan('Available Claude Hooks:\n')); - try { - const files = readdirSync(hooksDir); - const hooks = files - .filter(f => f.endsWith('.sh')) - .map(f => f.replace('.sh', '')) - .filter(h => !h.includes('common')); // Exclude common libraries - hooks.forEach(hook => { - const description = HOOK_DESCRIPTIONS[hook] || 'No description available'; - console.log(` ${chalk.green('โ€ข')} ${chalk.white(hook)}`); - console.log(` ${chalk.gray(description)}`); - console.log(` Usage: ${chalk.yellow(`npx claude-code-hooks-cli exec ${hook}`)}`); - console.log(); - }); - console.log(chalk.cyan('Add to settings.json:')); - console.log(chalk.gray(' Run `npx claude-code-hooks-cli init` to set up hooks')); - console.log(chalk.gray(' Run `npx claude-code-hooks-cli manage` to manage existing hooks')); - } - catch (err) { - console.error(chalk.red('Error reading hooks directory:'), err.message); - process.exit(1); - } -} -//# sourceMappingURL=list.js.map \ No newline at end of file diff --git a/lib/commands/list.js.map b/lib/commands/list.js.map deleted file mode 100644 index e7f93b9..0000000 --- a/lib/commands/list.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"list.js","sourceRoot":"","sources":["../../src/commands/list.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,IAAI,CAAC;AACjC,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AACpC,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,MAAM,UAAU,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AAClD,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CAAC;AAMtC,MAAM,iBAAiB,GAAqB;IAC1C,kCAAkC;IAClC,kBAAkB,EAAE,0BAA0B;IAC9C,YAAY,EAAE,6BAA6B;IAC3C,YAAY,EAAE,gBAAgB;IAE9B,gBAAgB;IAChB,mBAAmB,EAAE,qDAAqD;IAC1E,wBAAwB,EAAE,gEAAgE;IAC1F,wBAAwB,EAAE,gCAAgC;IAC1D,wBAAwB,EAAE,0CAA0C;IAEpE,iDAAiD;IACjD,iBAAiB,EAAE,gFAAgF;IACnG,eAAe,EAAE,sDAAsD;IACvE,wBAAwB,EAAE,sDAAsD;CACjF,CAAC;AAEF,MAAM,CAAC,KAAK,UAAU,IAAI;IACxB,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,EAAE,aAAa,CAAC,CAAC;IAEhD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,2BAA2B,CAAC,CAAC,CAAC;IAErD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;QACpC,MAAM,KAAK,GAAG,KAAK;aAChB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;aAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;aAC9B,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,2BAA2B;QAElE,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE;YACnB,MAAM,WAAW,GAAG,iBAAiB,CAAC,IAAI,CAAC,IAAI,0BAA0B,CAAC;YAC1E,OAAO,CAAC,GAAG,CAAC,KAAK,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC1D,OAAO,CAAC,GAAG,CAAC,OAAO,KAAK,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC,CAAC;YAC9C,OAAO,CAAC,GAAG,CAAC,cAAc,KAAK,CAAC,MAAM,CAAC,kCAAkC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;YACpF,OAAO,CAAC,GAAG,EAAE,CAAC;QAChB,CAAC,CAAC,CAAC;QAEH,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,uBAAuB,CAAC,CAAC,CAAC;QACjD,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC,CAAC;QAClF,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC,CAAC;IAE/F,CAAC;IAAC,OAAO,GAAQ,EAAE,CAAC;QAClB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,gCAAgC,CAAC,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;QACxE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAClB,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/lib/commands/manage.d.ts b/lib/commands/manage.d.ts deleted file mode 100644 index c011c2c..0000000 --- a/lib/commands/manage.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function manage(): Promise; -//# sourceMappingURL=manage.d.ts.map \ No newline at end of file diff --git a/lib/commands/manage.d.ts.map b/lib/commands/manage.d.ts.map deleted file mode 100644 index 81d6c5d..0000000 --- a/lib/commands/manage.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"manage.d.ts","sourceRoot":"","sources":["../../src/commands/manage.ts"],"names":[],"mappings":"AAgcA,wBAAsB,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC,CAqS5C"} \ No newline at end of file diff --git a/lib/commands/manage.js b/lib/commands/manage.js deleted file mode 100644 index b6a91d9..0000000 --- a/lib/commands/manage.js +++ /dev/null @@ -1,655 +0,0 @@ -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; -import { join } from 'path'; -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import { execSync } from 'child_process'; -import { SETTINGS_LOCATIONS } from '../settings-locations.js'; -import { HookValidator } from '../validation/index.js'; -import { HookSelector } from './hook-selector.js'; -import { LocationSelector } from './location-selector.js'; -import { discoverHookTemplates, mergeHooksWithDiscovered } from '../discovery/hook-discovery.js'; -import { homedir } from 'os'; -import { setupDocCompliance } from './doc-compliance-setup.js'; -// Helper function to check if API key exists -function hasApiKey(apiKeyType) { - const keyName = apiKeyType === 'gemini' ? 'GEMINI_API_KEY' : 'ANTHROPIC_API_KEY'; - // Check environment variable - if (process.env[keyName]) - return true; - // Check ~/.gemini/.env for Gemini or ~/.claude/.env for Claude - const envDir = apiKeyType === 'gemini' ? '.gemini' : '.claude'; - const envPath = join(homedir(), envDir, '.env'); - if (existsSync(envPath)) { - try { - const content = readFileSync(envPath, 'utf-8'); - if (content.includes(`${keyName}=`) && !content.includes(`${keyName}=\n`)) { - return true; - } - } - catch (e) { - // Ignore errors - } - } - // Check project .env - const projectEnvPath = join(process.cwd(), '.env'); - if (existsSync(projectEnvPath)) { - try { - const content = readFileSync(projectEnvPath, 'utf-8'); - if (content.includes(`${keyName}=`) && !content.includes(`${keyName}=\n`)) { - return true; - } - } - catch (e) { - // Ignore errors - } - } - return false; -} -// Helper function to save API key -async function saveApiKey(apiKey, location) { - const envPath = location === 'global' - ? join(homedir(), '.claude', '.env') - : join(process.cwd(), '.env'); - const envDir = location === 'global' - ? join(homedir(), '.claude') - : process.cwd(); - // Create directory if needed - if (!existsSync(envDir)) { - mkdirSync(envDir, { recursive: true }); - } - // Check if .env file exists and has content - let envContent = ''; - if (existsSync(envPath)) { - envContent = readFileSync(envPath, 'utf-8'); - // Remove existing ANTHROPIC_API_KEY if present - envContent = envContent.split('\n') - .filter(line => !line.startsWith('ANTHROPIC_API_KEY=')) - .join('\n'); - if (envContent && !envContent.endsWith('\n')) { - envContent += '\n'; - } - } - // Add the new API key - envContent += `ANTHROPIC_API_KEY=${apiKey}\n`; - // Write the file - writeFileSync(envPath, envContent, { mode: 0o600 }); // Secure permissions -} -// Available hooks from the npm package -const AVAILABLE_HOOKS = { - // Single-purpose validation hooks - 'typescript-check': { - event: 'PreToolUse', - matcher: 'Bash', - pattern: '^git\\s+commit', - description: 'TypeScript type checking' - }, - 'lint-check': { - event: 'PreToolUse', - matcher: 'Bash', - pattern: '^git\\s+commit', - description: 'Code linting (ESLint, etc.)' - }, - 'test-check': { - event: 'PreToolUse', - matcher: 'Bash', - description: 'Run test suite' - }, - // Package management - 'check-package-age': { - event: 'PreToolUse', - matcher: 'Bash', - pattern: '^(npm\\s+(install|i|add)|yarn\\s+(add|install))\\s+', - description: 'Prevents installation of outdated npm/yarn packages' - }, - // Code quality - 'code-quality-validator': { - event: 'PostToolUse', - matcher: 'Write|Edit|MultiEdit', - description: 'Enforces clean code standards (function length, nesting, etc.)' - }, - // Notifications - 'task-completion-notify': { - event: 'Stop', - description: 'System notifications when Claude finishes' - }, - // Documentation compliance - 'doc-compliance': { - event: 'Stop', - description: 'AI-powered code compliance checking with Gemini Flash (~5s analysis)', - requiresApiKey: true, - apiKeyType: 'gemini' - }, - // Testing and development - 'self-test': { - event: 'PreWrite', - pattern: '\\.test-trigger$', - description: 'Claude self-test hook for automated hook validation during development' - } -}; -// Helper function to extract hook name from command -function extractHookName(command) { - // Try to extract from npx claude-code-hooks-cli exec pattern - const match = command.match(/exec\s+([a-z-]+)/); - if (match) - return match[1]; - // For custom hooks, use a sanitized version of the command as the name - // Remove common prefixes and extract meaningful part - const customName = command - .replace(/^(npx|node|bash|sh|\.\/)/g, '') - .replace(/\.(sh|js|py|rb)$/g, '') - .replace(/[^a-zA-Z0-9-]/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') - .slice(0, 50); // Limit length - return customName || 'custom-hook'; -} -// Helper function to count hooks in a settings file -function countHooks(settingsPath) { - // Convert relative paths to absolute - const absolutePath = settingsPath.startsWith('/') || settingsPath.startsWith(process.env.HOME || '') - ? settingsPath - : join(process.cwd(), settingsPath); - if (!existsSync(absolutePath)) - return 0; - try { - const settings = JSON.parse(readFileSync(absolutePath, 'utf-8')); - let count = 0; - if (settings.hooks) { - Object.values(settings.hooks).forEach(eventHooks => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach(hookGroup => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - count += hookGroup.hooks.length; - } - }); - } - }); - } - return count; - } - catch (err) { - return 0; - } -} -// Helper function to format relative time -function formatRelativeTime(dateStr) { - if (!dateStr) - return 'Never'; - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffSecs = Math.floor(diffMs / 1000); - const diffMins = Math.floor(diffSecs / 60); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - if (diffDays > 0) - return `${diffDays}d ago`; - if (diffHours > 0) - return `${diffHours}h ago`; - if (diffMins > 0) - return `${diffMins}m ago`; - return 'Just now'; -} -// Helper function to get hook stats from logs -function getHookStats(hookName) { - try { - const logFile = `${process.env.HOME}/.local/share/claude-hooks/logs/hooks.log`; - if (!existsSync(logFile)) { - return { count: 0, lastCall: null }; - } - // Get execution count - const count = parseInt(execSync(`grep -c "\\[${hookName}\\] Hook started" "${logFile}" 2>/dev/null || echo 0`, { encoding: 'utf-8' }).trim()) || 0; - if (count === 0) { - return { count: 0, lastCall: null }; - } - // Get last execution time - const lastLine = execSync(`grep "\\[${hookName}\\] Hook" "${logFile}" 2>/dev/null | tail -1 || echo ""`, { encoding: 'utf-8' }).trim(); - let lastCall = null; - if (lastLine) { - const match = lastLine.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/); - if (match) { - lastCall = match[1]; - } - } - return { count, lastCall }; - } - catch (error) { - // If anything fails, return zeros - return { count: 0, lastCall: null }; - } -} -// Helper function to get all hook statistics -function getAllHookStats() { - const hookStats = []; - // Get all available hooks - Object.entries(AVAILABLE_HOOKS).forEach(([hookName, config]) => { - const stats = getHookStats(hookName); - if (stats.count > 0) { - hookStats.push({ - name: hookName, - count: stats.count, - lastCall: stats.lastCall, - relativeTime: formatRelativeTime(stats.lastCall) - }); - } - }); - // Sort by count (descending) - return hookStats.sort((a, b) => b.count - a.count); -} -// Helper function to get all hooks from a settings file -function getHooksFromSettings(settings) { - const hooks = []; - if (!settings.hooks) - return hooks; - Object.entries(settings.hooks).forEach(([event, eventHooks]) => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach((hookGroup, groupIndex) => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - hookGroup.hooks.forEach((hook, hookIndex) => { - const hookName = extractHookName(hook.command); - if (hookName) { - const isKnownHook = hookName in AVAILABLE_HOOKS; - hooks.push({ - event, - groupIndex, - hookIndex, - name: hookName, - matcher: hookGroup.matcher, - pattern: hookGroup.pattern, - command: hook.command, - description: isKnownHook - ? AVAILABLE_HOOKS[hookName].description - : `Custom hook: ${hook.command.slice(0, 60)}${hook.command.length > 60 ? '...' : ''}`, - stats: getHookStats(hookName) - }); - } - }); - } - }); - } - }); - return hooks; -} -// Helper function to get all unique hooks from settings (including custom ones) -function getAllUniqueHooks(settings) { - const uniqueHooks = new Set(); - if (!settings.hooks) - return uniqueHooks; - Object.values(settings.hooks).forEach(eventHooks => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach(hookGroup => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - hookGroup.hooks.forEach(hook => { - const hookName = extractHookName(hook.command); - if (hookName) { - uniqueHooks.add(hookName); - } - }); - } - }); - } - }); - return uniqueHooks; -} -// Helper function to remove a hook from settings -function removeHook(settings, hookToRemove) { - const eventHooks = settings.hooks[hookToRemove.event]; - if (!eventHooks || !Array.isArray(eventHooks)) - return; - const hookGroup = eventHooks[hookToRemove.groupIndex]; - if (!hookGroup || !hookGroup.hooks) - return; - // Remove the hook - hookGroup.hooks.splice(hookToRemove.hookIndex, 1); - // If this was the last hook in the group, remove the group - if (hookGroup.hooks.length === 0) { - eventHooks.splice(hookToRemove.groupIndex, 1); - } -} -// Helper function to add a hook to settings -function addHook(settings, hookName, customHookInfo, discoveredHook) { - const hookConfig = AVAILABLE_HOOKS[hookName]; - // If it's not a known hook and no custom/discovered info provided, skip - if (!hookConfig && !customHookInfo && !discoveredHook) - return; - // Determine event, matcher, pattern, and command - const event = hookConfig?.event || discoveredHook?.event || customHookInfo?.event || 'PreToolUse'; - const matcher = hookConfig?.matcher || discoveredHook?.matcher || customHookInfo?.matcher; - const pattern = hookConfig?.pattern || discoveredHook?.pattern || customHookInfo?.pattern; - let command; - if (hookConfig) { - // Built-in hook - command = `npx claude-code-hooks-cli exec ${hookName}`; - } - else if (discoveredHook?.command) { - // Project hook with custom command - command = discoveredHook.command; - } - else if (customHookInfo?.command) { - // Custom hook - command = customHookInfo.command; - } - else { - // Default to exec pattern - command = `npx claude-code-hooks-cli exec ${hookName}`; - } - // Ensure hooks structure exists - if (!settings.hooks) - settings.hooks = {}; - if (!settings.hooks[event]) - settings.hooks[event] = []; - const eventHooks = settings.hooks[event]; - // Find existing group with same matcher and pattern, or create new one - let targetGroup = eventHooks.find(group => group.matcher === matcher && - group.pattern === pattern); - if (!targetGroup) { - targetGroup = { - hooks: [] - }; - if (matcher) - targetGroup.matcher = matcher; - if (pattern) - targetGroup.pattern = pattern; - eventHooks.push(targetGroup); - } - // Add the hook - targetGroup.hooks.push({ - type: 'command', - command - }); -} -// Helper function to save settings -function saveSettings(path, settings, silent = false) { - // Validate settings before saving - const validator = new HookValidator(); - const result = validator.validateSettings(settings); - if (!result.valid) { - if (!silent) { - console.error(chalk.red('\nโŒ Invalid settings configuration:')); - console.error(validator.formatResults(result, true)); - console.error(chalk.yellow('\nSettings were not saved due to validation errors.')); - } - throw new Error('Invalid settings configuration'); - } - // Show warnings if any - if (!silent && result.warnings.length > 0) { - console.warn(chalk.yellow('\nโš ๏ธ Validation warnings:')); - result.warnings.forEach(warning => { - console.warn(chalk.yellow(` - ${warning.message}`)); - }); - } - // Create directory if needed - const dir = join(path, '..'); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - // Save settings - writeFileSync(path, JSON.stringify(settings, null, 2)); -} -export async function manage() { - // Ensure Ctrl+C always works - process.on('SIGINT', () => { - console.log(chalk.yellow('\n\nExiting...')); - process.exit(0); - }); - let selectedPath = null; - while (true) { - console.clear(); - console.log(chalk.cyan('Claude Hooks Manager\n')); - if (!selectedPath) { - // Get hook statistics - const allStats = getAllHookStats(); - // Build location choices - const locationChoices = SETTINGS_LOCATIONS.map(loc => ({ - path: loc.path, - display: loc.display, - description: loc.description, - hookCount: countHooks(loc.path), - value: loc.path - })); - // Add separator - locationChoices.push({ - path: '', - display: '', - description: '', - hookCount: 0, - value: 'separator' - }); - // Add log viewing options - locationChoices.push({ - path: '', - display: chalk.gray('๐Ÿ“‹ View recent logs'), - description: '', - hookCount: 0, - value: 'view-logs' - }); - locationChoices.push({ - path: '', - display: chalk.gray('๐Ÿ“Š Tail logs (live)'), - description: '', - hookCount: 0, - value: 'tail-logs' - }); - // Add separator - locationChoices.push({ - path: '', - display: '', - description: '', - hookCount: 0, - value: 'separator' - }); - // Add exit option - locationChoices.push({ - path: '', - display: 'โœ• Exit', - description: '', - hookCount: 0, - value: 'exit' - }); - // Run the location selector - const selector = new LocationSelector(locationChoices, allStats.length > 0, allStats); - const path = await selector.run(); - if (path === 'exit') { - console.log(chalk.yellow('Goodbye!')); - process.exit(0); - } - if (path === 'view-logs') { - console.clear(); - const logFile = `${process.env.HOME}/.local/share/claude-hooks/logs/hooks.log`; - if (existsSync(logFile)) { - console.log(chalk.yellow('Recent hook logs (last 50 lines):\n')); - execSync(`tail -50 "${logFile}"`, { stdio: 'inherit' }); - } - else { - console.log(chalk.gray('No log file found.')); - } - console.log(chalk.gray('\nPress Enter to continue...')); - try { - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - } - catch (err) { - // User pressed Ctrl+C - process.exit(0); - } - continue; - } - if (path === 'tail-logs') { - console.clear(); - const logFile = `${process.env.HOME}/.local/share/claude-hooks/logs/hooks.log`; - if (existsSync(logFile)) { - console.log(chalk.yellow('Tailing hook logs (Ctrl+C to stop):\n')); - try { - execSync(`tail -f "${logFile}"`, { stdio: 'inherit' }); - } - catch (e) { - // User pressed Ctrl+C - } - } - else { - console.log(chalk.gray('No log file found.')); - console.log(chalk.gray('\nPress Enter to continue...')); - try { - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - } - catch (err) { - // User pressed Ctrl+C - process.exit(0); - } - } - continue; - } - selectedPath = path; - } - // Load or create settings - let settings = { - "_comment": "Claude Code hooks configuration (using claude-code-hooks-cli)", - "hooks": {} - }; - // Convert relative paths to absolute - const absoluteSelectedPath = selectedPath && (selectedPath.startsWith('/') || selectedPath.startsWith(process.env.HOME || '') - ? selectedPath - : join(process.cwd(), selectedPath)); - if (absoluteSelectedPath && existsSync(absoluteSelectedPath)) { - try { - settings = JSON.parse(readFileSync(absoluteSelectedPath, 'utf-8')); - // Validate loaded settings - const validator = new HookValidator(); - const result = validator.validateSettings(settings); - if (!result.valid) { - console.error(chalk.red(`\nโŒ Invalid settings in ${selectedPath}:`)); - console.error(validator.formatResults(result, true)); - console.error(chalk.yellow('\nPlease fix these issues before managing hooks.')); - console.error(chalk.gray('\nPress Enter to continue...')); - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - selectedPath = null; - continue; - } - // Show warnings if any - if (result.warnings.length > 0) { - console.warn(chalk.yellow('\nโš ๏ธ Validation warnings:')); - result.warnings.forEach(warning => { - console.warn(chalk.yellow(` - ${warning.message}`)); - }); - console.warn(chalk.gray('\nPress Enter to continue...')); - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - } - } - catch (err) { - console.error(chalk.red(`Error reading ${selectedPath}: ${err.message}`)); - return; - } - } - // Management loop for this file - let managingFile = true; - while (managingFile) { - // Discover project hook templates - const discoveredHooks = discoverHookTemplates(); - const allAvailableHooks = mergeHooksWithDiscovered(AVAILABLE_HOOKS, discoveredHooks); - // Get current hooks - const currentHooks = getHooksFromSettings(settings); - const currentHookNames = new Set(currentHooks.map(h => h.name)); - // Build hook choices for the selector - const hookChoices = []; - // Add all available hooks (built-in and discovered) - allAvailableHooks.forEach(hook => { - hookChoices.push({ - name: hook.name, - description: hook.description, - event: hook.event, - selected: currentHookNames.has(hook.name), - source: hook.source - }); - }); - // Add custom hooks that are in settings but not in available hooks - currentHooks.forEach(hook => { - const existingHook = allAvailableHooks.find(h => h.name === hook.name); - if (!existingHook) { - // Don't add duplicates - if (!hookChoices.find(choice => choice.name === hook.name)) { - hookChoices.push({ - name: hook.name, - description: hook.description, - event: hook.event, - selected: true, // Custom hooks in settings are always selected - source: 'custom' - }); - } - } - }); - // Sort hooks: selected first, then by source (built-in, project, custom), then by name - hookChoices.sort((a, b) => { - if (a.selected !== b.selected) - return b.selected ? 1 : -1; - // Sort by source priority - const sourceOrder = { 'built-in': 0, 'project': 1, 'custom': 2 }; - const aOrder = sourceOrder[a.source || 'custom']; - const bOrder = sourceOrder[b.source || 'custom']; - if (aOrder !== bOrder) - return aOrder - bOrder; - return a.name.localeCompare(b.name); - }); - // Create the save handler - const saveHandler = async (selectedHookNames) => { - // Check if any newly selected hooks require API key - const newlySelectedHooks = selectedHookNames.filter(name => !currentHookNames.has(name)); - const hooksRequiringApiKey = newlySelectedHooks - .map(hookName => allAvailableHooks.find(h => h.name === hookName)) - .filter(hook => hook?.requiresApiKey); - // If any newly selected hooks require API key and we don't have one, show message - const missingApiKeys = hooksRequiringApiKey.filter(hook => !hasApiKey(hook?.apiKeyType)); - if (missingApiKeys.length > 0) { - // Don't save these hooks - const missingHookNames = missingApiKeys.map(h => h.name); - selectedHookNames = selectedHookNames.filter(name => !missingHookNames.includes(name)); - } - // Keep track of which custom hooks we need to preserve - const customHooksToPreserve = currentHooks.filter(hook => { - const foundInAvailable = allAvailableHooks.find(h => h.name === hook.name); - return !foundInAvailable && selectedHookNames.includes(hook.name); - }); - // Clear hooks and rebuild based on selection - settings.hooks = {}; - selectedHookNames.forEach(hookName => { - // Check if it's a discovered hook - const discoveredHook = allAvailableHooks.find(h => h.name === hookName && h.source !== 'built-in'); - // Check if it's a custom hook we need to preserve - const customHook = customHooksToPreserve.find(h => h.name === hookName); - if (customHook) { - addHook(settings, hookName, customHook); - } - else if (discoveredHook) { - addHook(settings, hookName, undefined, discoveredHook); - } - else { - addHook(settings, hookName); - // Special handling for doc-compliance hook - if (hookName === 'doc-compliance' && newlySelectedHooks.includes(hookName)) { - setupDocCompliance(); - } - } - }); - if (absoluteSelectedPath) { - try { - saveSettings(absoluteSelectedPath, settings, true); // silent mode for auto-save - } - catch (err) { - // Handle validation errors silently during auto-save - } - } - // AFTER saving, if we blocked any hooks, return a special flag - if (missingApiKeys.length > 0) { - return { blockedHooks: missingApiKeys.map(h => h.name) }; - } - }; - // Run the hook selector - const selector = new HookSelector(hookChoices, saveHandler); - const result = await selector.run(); - // If user cancelled (Ctrl+C, Q, or Esc), go back to location selection - if (result === null) { - managingFile = false; - selectedPath = null; - } - } - } -} -//# sourceMappingURL=manage.js.map \ No newline at end of file diff --git a/lib/commands/manage.js.map b/lib/commands/manage.js.map deleted file mode 100644 index 6fdac1f..0000000 --- a/lib/commands/manage.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"manage.js","sourceRoot":"","sources":["../../src/commands/manage.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/lib/settings-locations.d.ts b/lib/settings-locations.d.ts deleted file mode 100644 index 9a2c15e..0000000 --- a/lib/settings-locations.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { SettingsLocation } from './types.js'; -export declare const SETTINGS_LOCATIONS: SettingsLocation[]; -//# sourceMappingURL=settings-locations.d.ts.map \ No newline at end of file diff --git a/lib/settings-locations.d.ts.map b/lib/settings-locations.d.ts.map deleted file mode 100644 index de7846e..0000000 --- a/lib/settings-locations.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"settings-locations.d.ts","sourceRoot":"","sources":["../src/settings-locations.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE9C,eAAO,MAAM,kBAAkB,EAAE,gBAAgB,EAyBhD,CAAC"} \ No newline at end of file diff --git a/lib/settings-locations.js b/lib/settings-locations.js deleted file mode 100644 index 1b400a2..0000000 --- a/lib/settings-locations.js +++ /dev/null @@ -1,27 +0,0 @@ -export const SETTINGS_LOCATIONS = [ - { - path: './.claude/settings.json', - dir: './.claude', - file: 'settings.json', - display: '.claude/settings.json', - description: 'Project settings', - level: 'project' - }, - { - path: './.claude/settings.local.json', - dir: './.claude', - file: 'settings.local.json', - display: '.claude/settings.local.json', - description: 'Personal project settings', - level: 'local' - }, - { - path: `${process.env.HOME}/.claude/settings.json`, - dir: `${process.env.HOME}/.claude`, - file: 'settings.json', - display: '~/.claude/settings.json', - description: 'Personal global settings', - level: 'global' - } -]; -//# sourceMappingURL=settings-locations.js.map \ No newline at end of file diff --git a/lib/settings-locations.js.map b/lib/settings-locations.js.map deleted file mode 100644 index 05e529e..0000000 --- a/lib/settings-locations.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"settings-locations.js","sourceRoot":"","sources":["../src/settings-locations.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,kBAAkB,GAAuB;IACpD;QACE,IAAI,EAAE,yBAAyB;QAC/B,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,uBAAuB;QAChC,WAAW,EAAE,kBAAkB;QAC/B,KAAK,EAAE,SAAS;KACjB;IACD;QACE,IAAI,EAAE,+BAA+B;QACrC,GAAG,EAAE,WAAW;QAChB,IAAI,EAAE,qBAAqB;QAC3B,OAAO,EAAE,6BAA6B;QACtC,WAAW,EAAE,2BAA2B;QACxC,KAAK,EAAE,OAAO;KACf;IACD;QACE,IAAI,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,wBAAwB;QACjD,GAAG,EAAE,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,UAAU;QAClC,IAAI,EAAE,eAAe;QACrB,OAAO,EAAE,yBAAyB;QAClC,WAAW,EAAE,0BAA0B;QACvC,KAAK,EAAE,QAAQ;KAChB;CACF,CAAC"} \ No newline at end of file diff --git a/lib/types.d.ts b/lib/types.d.ts deleted file mode 100644 index dbe840a..0000000 --- a/lib/types.d.ts +++ /dev/null @@ -1,70 +0,0 @@ -export interface HookConfig { - event: string; - matcher?: string; - pattern?: string; - description: string; - requiresApiKey?: boolean; - apiKeyType?: string; -} -export interface HookConfigs { - [key: string]: HookConfig; -} -export interface SettingsLocation { - path: string; - dir: string; - file: string; - display: string; - description: string; - level: string; -} -export interface HookSettings { - _comment?: string; - hooks: { - [event: string]: Array<{ - matcher?: string; - pattern?: string; - hooks: Array<{ - type: string; - command: string; - }>; - }>; - }; -} -export interface HookInfo { - event: string; - groupIndex: number; - hookIndex: number; - name: string; - matcher?: string; - pattern?: string; - command: string; - description: string; - stats: HookStats; -} -export interface HookStats { - count: number; - lastCall: string | null; -} -export interface HookStatDisplay { - name: string; - count: number; - lastCall: string | null; - relativeTime: string; -} -export interface HookTemplate extends HookConfig { - command?: string; - requiresApiKey?: boolean; - apiKeyType?: string; -} -export interface HookTemplates { - [key: string]: HookTemplate; -} -export type HookSource = 'built-in' | 'project' | 'custom'; -export interface DiscoveredHook extends HookConfig { - name: string; - source: HookSource; - command?: string; - requiresApiKey?: boolean; - apiKeyType?: string; -} -//# sourceMappingURL=types.d.ts.map \ No newline at end of file diff --git a/lib/types.d.ts.map b/lib/types.d.ts.map deleted file mode 100644 index 15d3b6d..0000000 --- a/lib/types.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,WAAW;IAC1B,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU,CAAC;CAC3B;AAED,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE;QACL,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,CAAC;YACrB,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,OAAO,CAAC,EAAE,MAAM,CAAC;YACjB,KAAK,EAAE,KAAK,CAAC;gBACX,IAAI,EAAE,MAAM,CAAC;gBACb,OAAO,EAAE,MAAM,CAAC;aACjB,CAAC,CAAC;SACJ,CAAC,CAAC;KACJ,CAAC;CACH;AAED,MAAM,WAAW,QAAQ;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,SAAS,CAAC;CAClB;AAED,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,aAAa;IAC5B,CAAC,GAAG,EAAE,MAAM,GAAG,YAAY,CAAC;CAC7B;AAED,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE3D,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,UAAU,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"} \ No newline at end of file diff --git a/lib/types.js b/lib/types.js deleted file mode 100644 index 718fd38..0000000 --- a/lib/types.js +++ /dev/null @@ -1,2 +0,0 @@ -export {}; -//# sourceMappingURL=types.js.map \ No newline at end of file diff --git a/lib/types.js.map b/lib/types.js.map deleted file mode 100644 index c768b79..0000000 --- a/lib/types.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index efa1161..0000000 --- a/package-lock.json +++ /dev/null @@ -1,712 +0,0 @@ -{ - "name": "claude-code-hooks-cli", - "version": "2.5.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "claude-code-hooks-cli", - "version": "2.5.1", - "hasInstallScript": true, - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "commander": "^11.0.0", - "inquirer": "^9.2.15" - }, - "bin": { - "claude-hooks": "bin/claude-hooks.js" - }, - "devDependencies": { - "@types/inquirer": "^9.0.8", - "@types/node": "^24.0.10", - "typescript": "^5.8.3" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@inquirer/figures": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", - "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@types/inquirer": { - "version": "9.0.8", - "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-9.0.8.tgz", - "integrity": "sha512-CgPD5kFGWsb8HJ5K7rfWlifao87m4ph8uioU7OTncJevmE/VLIqAAjfQtko578JZg7/f69K4FgqYym3gNr7DeA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/through": "*", - "rxjs": "^7.2.0" - } - }, - "node_modules/@types/node": { - "version": "24.0.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.10.tgz", - "integrity": "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~7.8.0" - } - }, - "node_modules/@types/through": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/@types/through/-/through-0.0.33.tgz", - "integrity": "sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", - "license": "MIT", - "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - } - }, - "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" - } - }, - "node_modules/chalk": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", - "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chardet": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", - "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", - "license": "MIT" - }, - "node_modules/cli-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", - "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", - "license": "MIT", - "dependencies": { - "restore-cursor": "^3.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cli-spinners": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", - "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-width": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", - "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", - "license": "ISC", - "engines": { - "node": ">= 12" - } - }, - "node_modules/clone": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/defaults": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", - "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", - "license": "MIT", - "dependencies": { - "clone": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/external-editor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", - "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", - "license": "MIT", - "dependencies": { - "chardet": "^0.7.0", - "iconv-lite": "^0.4.24", - "tmp": "^0.0.33" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/inquirer": { - "version": "9.3.7", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-9.3.7.tgz", - "integrity": "sha512-LJKFHCSeIRq9hanN14IlOtPSTe3lNES7TYDTE2xxdAy1LS5rYphajK1qtwvj3YmQXvvk0U2Vbmcni8P9EIQW9w==", - "license": "MIT", - "dependencies": { - "@inquirer/figures": "^1.0.3", - "ansi-escapes": "^4.3.2", - "cli-width": "^4.1.0", - "external-editor": "^3.1.0", - "mute-stream": "1.0.0", - "ora": "^5.4.1", - "run-async": "^3.0.0", - "rxjs": "^7.8.1", - "string-width": "^4.2.3", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^6.2.0", - "yoctocolors-cjs": "^2.1.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-interactive": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/log-symbols/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mute-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", - "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", - "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", - "license": "MIT", - "dependencies": { - "bl": "^4.1.0", - "chalk": "^4.1.0", - "cli-cursor": "^3.1.0", - "cli-spinners": "^2.5.0", - "is-interactive": "^1.0.0", - "is-unicode-supported": "^0.1.0", - "log-symbols": "^4.1.0", - "strip-ansi": "^6.0.0", - "wcwidth": "^1.0.1" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ora/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/os-tmpdir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/restore-cursor": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", - "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", - "license": "MIT", - "dependencies": { - "onetime": "^5.1.0", - "signal-exit": "^3.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/run-async": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", - "integrity": "sha512-540WwVDOMxA6dN6We19EcT9sc3hkXPw5mzRNGM3FkdN/vtE9NFvj5lFAPNwUDmJjXidm3v7TC1cTE7t17Ulm1Q==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/rxjs": { - "version": "7.8.2", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", - "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tmp": { - "version": "0.0.33", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", - "license": "MIT", - "dependencies": { - "os-tmpdir": "~1.0.2" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/typescript": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", - "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "7.8.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz", - "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/wcwidth": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", - "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", - "license": "MIT", - "dependencies": { - "defaults": "^1.0.3" - } - }, - "node_modules/wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yoctocolors-cjs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", - "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index f8869b2..0000000 --- a/package.json +++ /dev/null @@ -1,49 +0,0 @@ -{ - "name": "claude-code-hooks-cli", - "version": "2.6.1", - "description": "Claude Code hooks - Run validation and quality checks in Claude", - "main": "lib/index.js", - "bin": { - "claude-hooks": "./bin/claude-hooks.js" - }, - "type": "module", - "scripts": { - "build": "tsc", - "dev": "tsc --watch", - "typecheck": "tsc --noEmit", - "prepublishOnly": "npm run build", - "postinstall": "node scripts/postinstall.js", - "test": "echo \"No tests configured\" && exit 0", - "lint": "echo \"No linting configured\" && exit 0" - }, - "keywords": [ - "claude", - "claude-code", - "hooks", - "cli", - "validation", - "typescript", - "lint" - ], - "author": "Dan Seider", - "license": "MIT", - "dependencies": { - "chalk": "^5.3.0", - "commander": "^11.0.0", - "inquirer": "^9.2.15" - }, - "devDependencies": { - "@types/inquirer": "^9.0.8", - "@types/node": "^24.0.10", - "typescript": "^5.8.3" - }, - "files": [ - "bin/", - "lib/", - "hooks/", - "scripts/postinstall.js" - ], - "engines": { - "node": ">=16.0.0" - } -} diff --git a/portable-hooks/README.md b/portable-hooks/README.md new file mode 100644 index 0000000..dcc0063 --- /dev/null +++ b/portable-hooks/README.md @@ -0,0 +1,101 @@ +# Portable Code Quality Hooks for Claude + +Lightweight, dependency-free code quality hooks that work with any language. + +## Features + +- **No dependencies** - Pure bash, no Node.js or npm required +- **Multi-language support** - TypeScript, JavaScript, Python, Ruby, and more +- **Automatic enforcement** - Blocks bad code before and after writing +- **Simple installation** - One script to set up everything + +## Quick Install + +```bash +# Clone just the portable hooks +git clone https://github.com/yourusername/claude-hooks +cd claude-hooks +./install-hooks.sh +``` + +Or copy these files to your project: +``` +hooks/ +โ”œโ”€โ”€ stop-hook.sh # Runs on Stop events +โ”œโ”€โ”€ post-tool-hook.sh # Runs after Write/Edit +โ”œโ”€โ”€ pre-tool-hook.sh # Runs before Write/Edit +โ””โ”€โ”€ portable-quality-validator.sh # Main validator + +.claude/ +โ”œโ”€โ”€ settings.json # Hook configuration +โ””โ”€โ”€ hooks/ + โ””โ”€โ”€ quality-config.json # Quality rules +``` + +## Configuration + +Edit `.claude/hooks/quality-config.json`: + +```json +{ + "rules": { + "maxFunctionLines": 30, + "maxFileLines": 200, + "maxLineLength": 100, + "maxNestingDepth": 4 + } +} +``` + +## Supported Languages + +### TypeScript/JavaScript +- Function length limits +- Nesting depth checks +- Line length validation + +### Python +- PEP8 indentation (4 spaces) +- Function/class length +- Line length validation + +### Ruby +- Ruby style indentation (2 spaces) +- Method length limits +- Line length validation + +## How It Works + +1. **PreToolUse** - Analyzes code before Claude writes it +2. **PostToolUse** - Validates files after changes +3. **Stop** - Ensures clean code before ending session + +## Adding to Existing Projects + +1. Copy the `hooks/` directory to your project +2. Run `./install-hooks.sh` +3. Customize `.claude/hooks/quality-config.json` + +## Customization + +Add new languages by editing the validators: + +```bash +# In portable-quality-validator.sh +check_golang() { + local file="$1" + # Add Go-specific checks +} +``` + +## No Node.js Required + +Unlike the full claude-hooks system, this portable version: +- Uses only bash and standard Unix tools +- Works in Python, Ruby, Go, and other projects +- Has zero npm dependencies +- Installs in seconds + +## License + +MIT \ No newline at end of file diff --git a/scripts/postinstall.js b/scripts/postinstall.js deleted file mode 100755 index 4ee922c..0000000 --- a/scripts/postinstall.js +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node - -// Only show message when installed globally -if (process.env.npm_config_global === 'true') { - console.log(); - console.log('โœ… Claude Hooks CLI installed successfully!'); - console.log(); - console.log('๐Ÿš€ Get started:'); - console.log(' claude-hooks Open the interactive hook manager'); - console.log(' claude-hooks init Set up hooks for your project'); - console.log(' claude-hooks list See all available hooks'); - console.log(); - console.log('๐Ÿ“š Documentation: https://github.com/anthropics/claude-code-hooks-cli'); - console.log(); -} \ No newline at end of file diff --git a/scripts/update-vendored.sh b/scripts/update-vendored.sh deleted file mode 100755 index 8e859a8..0000000 --- a/scripts/update-vendored.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/bin/bash - -# Update Vendored Claude Hooks -# This script updates vendored claude-hooks in a project -# It should be copied to your project's scripts directory - -set -e - -# Colors for output -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Configuration -CLAUDE_HOOKS_SOURCE="${CLAUDE_HOOKS_SOURCE:-$HOME/claude-hooks}" -CLAUDE_HOOKS_DEST="./claude-hooks" - -echo -e "${YELLOW}๐Ÿ”„ Updating Vendored Claude Hooks...${NC}" - -# Safety check: Don't run from within the claude-hooks repo itself -if [ -f "./scripts/update-vendored.sh" ] && [ -f "./hooks/check-package-age.sh" ]; then - echo -e "${RED}โŒ Error: Cannot run this script from within the claude-hooks repository${NC}" - echo "This script is meant to update vendored claude-hooks in other projects." - echo "" - echo "To update your local installation, use: ./scripts/update.sh" - exit 1 -fi - -# Check if we're in a project with vendored hooks -if [ ! -d "$CLAUDE_HOOKS_DEST" ]; then - echo -e "${RED}โŒ No vendored claude-hooks found in this project${NC}" - echo "Expected to find: $CLAUDE_HOOKS_DEST" - exit 1 -fi - -# Check if source exists -if [ ! -d "$CLAUDE_HOOKS_SOURCE" ]; then - echo -e "${RED}โŒ Claude Hooks source not found at $CLAUDE_HOOKS_SOURCE${NC}" - echo "Options:" - echo " 1. Clone it: git clone https://github.com/decider/claude-hooks.git ~/claude-hooks" - echo " 2. Set custom path: CLAUDE_HOOKS_SOURCE=/path/to/claude-hooks $0" - exit 1 -fi - -# Get current version -CURRENT_VERSION="" -if [ -f "$CLAUDE_HOOKS_DEST/VERSION" ]; then - CURRENT_VERSION=$(cat "$CLAUDE_HOOKS_DEST/VERSION") -fi - -# Update the source repository -echo -e "${YELLOW}๐Ÿ“ฅ Updating source repository...${NC}" -(cd "$CLAUDE_HOOKS_SOURCE" && git pull --quiet) || echo "Note: Could not pull latest changes" - -# Get new version -NEW_VERSION="" -if [ -f "$CLAUDE_HOOKS_SOURCE/VERSION" ]; then - NEW_VERSION=$(cat "$CLAUDE_HOOKS_SOURCE/VERSION") -fi - -# Show what will be updated -if [ -n "$CURRENT_VERSION" ] && [ -n "$NEW_VERSION" ]; then - if [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then - echo -e "${YELLOW}๐Ÿ“‹ Version change: $CURRENT_VERSION โ†’ $NEW_VERSION${NC}" - else - echo -e "${GREEN}โœ… Already at version $CURRENT_VERSION${NC}" - fi -fi - -# Create backup -if [ -d "$CLAUDE_HOOKS_DEST" ]; then - echo -e "${YELLOW}๐Ÿ’พ Creating backup...${NC}" - rm -rf "$CLAUDE_HOOKS_DEST.backup" - cp -r "$CLAUDE_HOOKS_DEST" "$CLAUDE_HOOKS_DEST.backup" -fi - -# Copy files -echo -e "${YELLOW}๐Ÿ“ Copying updated hooks...${NC}" -mkdir -p "$CLAUDE_HOOKS_DEST" - -# Copy everything except .git and certain files -rsync -av \ - --exclude='.git' \ - --exclude='.gitignore' \ - --exclude='CLAUDE.local.md' \ - --exclude='claude' \ - "$CLAUDE_HOOKS_SOURCE/" "$CLAUDE_HOOKS_DEST/" - -# Make hooks executable -chmod +x "$CLAUDE_HOOKS_DEST/hooks/"*.sh 2>/dev/null || true -chmod +x "$CLAUDE_HOOKS_DEST/scripts/"*.sh 2>/dev/null || true -chmod +x "$CLAUDE_HOOKS_DEST/tools/"*.sh 2>/dev/null || true - -# Show changes -echo -e "${GREEN}โœ… Claude Hooks updated successfully!${NC}" - -# Check for changes -if command -v git >/dev/null 2>&1; then - CHANGES=$(git status --porcelain "$CLAUDE_HOOKS_DEST" 2>/dev/null | wc -l | tr -d ' ') - if [ "$CHANGES" -gt 0 ]; then - echo "" - echo -e "${YELLOW}๐Ÿ“ Changes detected:${NC}" - git status --short "$CLAUDE_HOOKS_DEST" 2>/dev/null || true - echo "" - echo -e "${YELLOW}๐Ÿ’ก To commit these changes:${NC}" - echo " git add $CLAUDE_HOOKS_DEST/" - echo " git commit -m \"chore: update claude-hooks to version $NEW_VERSION\"" - else - echo -e "${GREEN}โœ… No changes detected${NC}" - fi -fi - -# Cleanup backup if successful -if [ -d "$CLAUDE_HOOKS_DEST.backup" ]; then - rm -rf "$CLAUDE_HOOKS_DEST.backup" -fi - -echo "" -echo -e "${GREEN}๐ŸŽ‰ Update complete!${NC}" -echo "" -echo -e "${YELLOW}๐Ÿ“š Next steps:${NC}" -echo " 1. Review the changes with: git diff $CLAUDE_HOOKS_DEST/" -echo " 2. Run setup if needed: ./claude/setup-hooks.sh" -echo " 3. Check logs at: $HOME/claude/logs/hooks.log" \ No newline at end of file diff --git a/src/CLAUDE.md b/src/CLAUDE.md deleted file mode 100644 index 4ca5a3a..0000000 --- a/src/CLAUDE.md +++ /dev/null @@ -1,29 +0,0 @@ -# src - CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this directory. - -## Overview -Code module for the project - -## Directory Structure -``` -โ”œโ”€โ”€ commands/ # 5 files -โ”œโ”€โ”€ validation/ # 4 files -``` - -## Key Components -- `settings-locations.ts` - SETTINGS_LOCATIONS:SettingsLocation[] - -## Commands -- No package.json found - -## Dependencies -- No package.json found - -## Architecture Notes -- TypeScript/JavaScript module -- Individual file exports -- Utility functions - ---- -_Auto-generated by Claude Context Updater on 2025-07-07_ diff --git a/src/cli.ts b/src/cli.ts deleted file mode 100644 index 17b9d7d..0000000 --- a/src/cli.ts +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env node - -import { program } from 'commander'; -import { exec } from './commands/exec.js'; -import { init } from './commands/init.js'; -import { list } from './commands/list.js'; -import { manage } from './commands/manage.js'; -import { validate } from './commands/validate.js'; -import { universalHook } from './commands/universal-hook.js'; -import { readFileSync } from 'fs'; -import { fileURLToPath } from 'url'; -import { dirname, join } from 'path'; -import { checkForUpdates, displayUpdateNotification } from './utils/updateChecker.js'; -import { isFirstRun, markFirstRunComplete, showWelcomeMessage } from './utils/firstRun.js'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); -const packageJson = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf-8')); - -program - .name('claude-hooks') - .description('CLI for Claude Code hooks') - .version(packageJson.version); - -program - .command('exec ') - .description('Execute a hook (used by Claude via npx)') - .option('--files ', 'Comma-separated list of files to process') - .option('--exclude ', 'Comma-separated list of patterns to exclude') - .option('--include ', 'Comma-separated list of patterns to include') - .action(exec); - -program - .command('init') - .description('Initialize Claude hooks - choose quick setup or custom configuration') - .option('-l, --level ', 'Configuration level: project, project-alt, local, or global') - .action(init); - -program - .command('list') - .description('List available hooks') - .action(list); - -program - .command('manage') - .description('Interactively manage hooks in settings.json files') - .action(manage); - -program - .command('validate [path]') - .description('Validate hook settings files') - .option('-v, --verbose', 'Show detailed validation information') - .option('--fix', 'Automatically fix issues (not yet implemented)') - .action(validate); - - -// Universal hook entry point for all events -program - .command('universal-hook') - .description('Universal entry point for all hook events (used internally)') - .action(universalHook); - - -// Main async function to handle startup tasks -async function main() { - // Check if this is first run - const firstRun = await isFirstRun(); - if (firstRun) { - await showWelcomeMessage(); - await markFirstRunComplete(); - } - - // Store update check promise to await later - const packageName = 'claude-code-hooks-cli'; - const updateCheckPromise = checkForUpdates(packageJson.version, packageName); - - // Default to manage command if no arguments provided - if (process.argv.length === 2) { - process.argv.push('manage'); - } - - // Parse and execute command - await program.parseAsync(); - - // Show update notification after command completes - const updateResult = await updateCheckPromise; - if (updateResult) { - displayUpdateNotification(updateResult); - } -} - -main().catch(console.error); \ No newline at end of file diff --git a/src/commands/doc-compliance-setup.ts b/src/commands/doc-compliance-setup.ts deleted file mode 100644 index 6d740aa..0000000 --- a/src/commands/doc-compliance-setup.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { existsSync, mkdirSync, writeFileSync } from 'fs'; -import { join } from 'path'; -import chalk from 'chalk'; -import { docTemplates } from './doc-templates.js'; - -export interface DocComplianceConfig { - fileTypes: Record; - directories: Record; - thresholds: Record; -} - -export function createDefaultDocComplianceConfig(): DocComplianceConfig { - return { - fileTypes: { - "*.ts": ["docs/typescript-standards.md"], - "*.tsx": ["docs/typescript-standards.md", "docs/react-standards.md"], - "*.js": ["docs/javascript-standards.md"], - "*.jsx": ["docs/javascript-standards.md", "docs/react-standards.md"], - "*.py": ["docs/python-standards.md"], - "*.java": ["docs/java-standards.md"], - "*.go": ["docs/go-standards.md"], - "*.rs": ["docs/rust-standards.md"], - "*.rb": ["docs/ruby-standards.md"], - "*.php": ["docs/php-standards.md"], - "*.c": ["docs/c-standards.md"], - "*.cpp": ["docs/cpp-standards.md"], - "*.swift": ["docs/swift-standards.md"], - "*.kt": ["docs/kotlin-standards.md"], - "*.sol": ["docs/solidity-standards.md", "docs/security-standards.md"], - // "// Add more file types as needed": "" - }, - directories: { - "src/api/": ["docs/api-design.md"], - "src/components/": ["docs/component-guidelines.md"], - "src/utils/": ["docs/utility-standards.md"], - "src/services/": ["docs/service-patterns.md"], - "src/models/": ["docs/data-model-standards.md"], - "tests/": ["docs/testing-standards.md"], - "scripts/": ["docs/script-guidelines.md"], - // "// Add more directories as needed": "" - }, - thresholds: { - "default": 0.8, - // "// Higher thresholds for critical code": "", - "*.sol": 0.95, - "*.rs": 0.9, - "src/api/*": 0.9, - "src/security/*": 0.95, - // "// Lower thresholds for less critical code": "", - "tests/*": 0.7, - "scripts/*": 0.7, - "*.md": 0.6 - } - }; -} - -export function createSampleDocumentation(docsPath: string): void { - if (!existsSync(docsPath)) { - mkdirSync(docsPath, { recursive: true }); - } - - // Create TypeScript standards - if (docTemplates.typescript) { - writeFileSync(join(docsPath, 'typescript-standards.md'), docTemplates.typescript); - console.log(chalk.green(`โœจ Created docs/typescript-standards.md`)); - } - - // Create JavaScript standards - if (docTemplates.javascript) { - writeFileSync(join(docsPath, 'javascript-standards.md'), docTemplates.javascript); - console.log(chalk.green(`โœจ Created docs/javascript-standards.md`)); - } - - // Create React standards - if (docTemplates.react) { - writeFileSync(join(docsPath, 'react-standards.md'), docTemplates.react); - console.log(chalk.green(`โœจ Created docs/react-standards.md`)); - } -} - -export function setupDocCompliance(projectRoot: string = process.cwd()): void { - const configPath = join(projectRoot, '.claude', 'doc-rules', 'config.json'); - const docsPath = join(projectRoot, 'docs'); - - // Create default config if it doesn't exist - if (!existsSync(configPath)) { - const configDir = join(projectRoot, '.claude', 'doc-rules'); - if (!existsSync(configDir)) { - mkdirSync(configDir, { recursive: true }); - } - - const defaultConfig = createDefaultDocComplianceConfig(); - writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2)); - console.log(chalk.green(`\nโœจ Created default config at ${configPath}`)); - - // Create sample documentation - createSampleDocumentation(docsPath); - - console.log(chalk.yellow('\n๐Ÿ“ Next steps:')); - console.log(chalk.gray('1. Get a Gemini API key from https://makersuite.google.com/app/apikey')); - console.log(chalk.gray('2. Set GEMINI_API_KEY in your environment')); - console.log(chalk.gray('3. Create your documentation standards in the docs/ folder')); - console.log(chalk.gray('4. Customize .claude/doc-rules/config.json for your project\n')); - } -} \ No newline at end of file diff --git a/src/commands/doc-templates.ts b/src/commands/doc-templates.ts deleted file mode 100644 index cb689ee..0000000 --- a/src/commands/doc-templates.ts +++ /dev/null @@ -1,111 +0,0 @@ -export const docTemplates = { - typescript: `# TypeScript Coding Standards - -## Type Safety -- Always specify return types for functions -- Avoid using \`any\` type - use \`unknown\` or specific types instead -- Use interfaces for object shapes -- Enable strict mode in tsconfig.json - -## Naming Conventions -- Use camelCase for variables and functions -- Use PascalCase for classes, interfaces, and type aliases -- Use UPPER_SNAKE_CASE for constants -- Use descriptive names that clearly indicate purpose - -## Error Handling -- All async functions must have proper error handling -- Use try-catch blocks for operations that might fail -- Provide meaningful error messages -- Log errors appropriately - -## Code Organization -- Keep files under 300 lines -- One class/interface per file for major components -- Group related functionality together -- Use barrel exports (index.ts) for clean imports - -## Example Violations -\`\`\`typescript -// โŒ Bad -function processData(data: any) { - return data.map(item => item.value); -} - -// โœ… Good -function processData(data: DataItem[]): number[] { - return data.map(item => item.value); -} -\`\`\` -`, - - javascript: `# JavaScript Coding Standards - -## Variable Declaration -- Use \`const\` by default, \`let\` when reassignment is needed -- Never use \`var\` -- Declare variables at the top of their scope - -## Functions -- Use arrow functions for callbacks -- Use function declarations for named functions -- Keep functions small and focused (< 20 lines) - -## Error Handling -- Always handle promise rejections -- Use try-catch for async/await -- Provide meaningful error messages - -## Modern JavaScript -- Use ES6+ features appropriately -- Prefer array methods over loops -- Use destructuring for cleaner code`, - - python: `# Python Coding Standards - -## Style Guide -- Follow PEP 8 -- Use 4 spaces for indentation -- Maximum line length of 79 characters - -## Naming Conventions -- snake_case for functions and variables -- PascalCase for classes -- UPPER_CASE for constants - -## Type Hints -- Use type hints for function parameters and returns -- Use typing module for complex types -- Document expected types in docstrings - -## Error Handling -- Use specific exception types -- Always clean up resources with context managers -- Log errors appropriately`, - - react: `# React Component Standards - -## Component Structure -- Use functional components with hooks -- One component per file -- Keep components under 200 lines - -## Props and State -- Define PropTypes or TypeScript interfaces -- Use descriptive prop names -- Minimize state, lift when necessary - -## Hooks -- Follow Rules of Hooks -- Extract custom hooks for reusable logic -- Use useMemo/useCallback appropriately - -## Performance -- Avoid inline function definitions in render -- Use React.memo for expensive components -- Lazy load large components` -}; - -export function getDocTemplate(language: string): string | undefined { - return docTemplates[language as keyof typeof docTemplates]; -} \ No newline at end of file diff --git a/src/commands/exec.ts b/src/commands/exec.ts deleted file mode 100644 index 2ae3d94..0000000 --- a/src/commands/exec.ts +++ /dev/null @@ -1,231 +0,0 @@ -import { spawn } from 'child_process'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import { existsSync, appendFileSync, mkdirSync } from 'fs'; -import { homedir } from 'os'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -// Local logging configuration -const LOG_DIR = join(homedir(), '.local', 'share', 'claude-hooks', 'logs'); -const LOG_FILE = join(LOG_DIR, 'hooks.log'); - -function ensureLogDir() { - try { - mkdirSync(LOG_DIR, { recursive: true }); - } catch (err) { - // Ignore errors - } -} - -function logToFile(level: string, hookName: string, message: string) { - try { - ensureLogDir(); - const timestamp = new Date().toISOString().replace('T', ' ').split('.')[0]; - const logEntry = `[${timestamp}] [${level}] [${hookName}] ${message}\n`; - appendFileSync(LOG_FILE, logEntry); - } catch (err) { - // Silently fail - don't interfere with hook execution - } -} - -function formatClaudeError(hookName: string, code: number | null, stderr: string, stdout: string, hookPath: string, duration: number): string { - // Ultra-compact error message for Claude - let message = `Hook '${hookName}' failed (exit ${code})`; - - // Analyze error and add inline action - if (stderr.includes('Usage:') || stderr.includes('usage:')) { - message += ' - Check hook arguments'; - } else if (stderr.includes('command not found') || stderr.includes('not found')) { - message += ' - Install missing dependencies'; - } else if (stderr.includes('permission denied')) { - message += ' - Check file permissions'; - } else if (stderr.includes('npm') || stderr.includes('node')) { - message += ' - Run npm install'; - } - - // Add stderr inline if short enough - if (stderr.trim() && stderr.length < 200) { - message += `: ${stderr.trim().replace(/\n/g, ' ')}`; - } - - return message; -} - -export async function exec(hookName: string, options?: any): Promise { - // Check if hookName is actually a direct command/path - let hookPath: string | null = null; - - // If hookName looks like a path or command, use it directly - if (hookName.includes('/') || hookName.includes('.')) { - // Resolve relative paths from current working directory - hookPath = hookName.startsWith('/') ? hookName : join(process.cwd(), hookName); - - // Check if the file exists - if (!existsSync(hookPath)) { - console.error(`Error: Hook command '${hookName}' not found at ${hookPath}`); - process.exit(1); - } - } else { - // Standard hook name - try multiple locations - const possiblePaths = [ - join(__dirname, '../../hooks', `${hookName}.sh`), - join(process.cwd(), 'hooks', `${hookName}.sh`), - join(process.cwd(), '.claude', 'hooks', `${hookName}.sh`) - ]; - - for (const path of possiblePaths) { - if (existsSync(path)) { - hookPath = path; - break; - } - } - - if (!hookPath) { - console.error(`Error: Hook '${hookName}' not found in any of these locations:`); - possiblePaths.forEach(path => console.error(` - ${path}`)); - process.exit(1); - } - } - - const startTime = Date.now(); - const isDebug = process.env.CLAUDE_LOG_LEVEL === 'DEBUG'; - - if (isDebug) { - console.error(`[DEBUG] Executing hook: ${hookName}`); - console.error(`[DEBUG] Hook path: ${hookPath}`); - console.error(`[DEBUG] Working directory: ${process.cwd()}`); - } - - // Read stdin for hook input with timeout - let input = ''; - process.stdin.setEncoding('utf8'); - - const processHook = () => { - // Log hook start to local file - logToFile('INFO', hookName, 'Hook started'); - - // Prepare environment variables with filtering options - const hookEnv: Record = { - ...process.env, - HOOK_NAME: hookName, - HOOK_START_TIME: startTime.toString() - }; - - // Add filtering options if provided - if (options?.files) { - hookEnv.HOOK_FILES = options.files; - } - if (options?.exclude) { - hookEnv.HOOK_EXCLUDE = options.exclude; - } - if (options?.include) { - hookEnv.HOOK_INCLUDE = options.include; - } - - // Capture both stdout and stderr for better error reporting - const hookProcess = spawn('bash', [hookPath!], { - stdio: ['pipe', 'pipe', 'pipe'], - env: hookEnv - }); - - let stdout = ''; - let stderr = ''; - - // Collect outputs - hookProcess.stdout.on('data', (data) => { - const output = data.toString(); - stdout += output; - process.stdout.write(output); - }); - - hookProcess.stderr.on('data', (data) => { - const output = data.toString(); - stderr += output; - process.stderr.write(output); - }); - - // Write input to hook's stdin - if (input) { - hookProcess.stdin.write(input); - } - hookProcess.stdin.end(); - - // Wait for hook to complete - hookProcess.on('exit', (code) => { - const duration = Date.now() - startTime; - - if (isDebug) { - console.error(`[DEBUG] Hook '${hookName}' completed in ${duration}ms with exit code ${code}`); - } - - // Log to local file - if (code === 0) { - logToFile('INFO', hookName, `Hook completed successfully (exit code: 0)`); - } else { - logToFile('ERROR', hookName, `Hook failed (exit code: ${code})`); - if (stderr.trim()) { - logToFile('ERROR', hookName, `Error output: ${stderr.trim()}`); - } - } - - if (code !== 0) { - // Concise error reporting for Claude - const errorMessage = formatClaudeError(hookName, code, stderr, stdout, hookPath!, duration); - console.error(errorMessage); - } - - process.exit(code || 0); - }); - - hookProcess.on('error', (err) => { - // Log to local file - logToFile('ERROR', hookName, `Hook execution error: ${err.message}`); - - // Ultra-compact error for Claude - console.error(`Hook '${hookName}' execution error: ${err.message} - Check file exists and is executable`); - - process.exit(1); - }); - - // Set a timeout for hook execution (5 minutes) - const hookTimeout = setTimeout(() => { - // Log to local file - logToFile('ERROR', hookName, 'Hook timed out after 5 minutes'); - - // Ultra-compact error for Claude - console.error(`Hook '${hookName}' timed out (5min) - May be stuck or waiting for input`); - - hookProcess.kill('SIGKILL'); - process.exit(124); // Timeout exit code - }, 5 * 60 * 1000); - - hookProcess.on('exit', () => { - clearTimeout(hookTimeout); - }); - }; - - // Set a timeout for stdin collection - const stdinTimeout = setTimeout(() => { - if (isDebug) { - console.error(`[DEBUG] stdin timeout after 5 seconds for hook '${hookName}'`); - } - processHook(); - }, 5000); - - try { - // Collect all stdin - for await (const chunk of process.stdin) { - input += chunk.toString(); - } - clearTimeout(stdinTimeout); - processHook(); - } catch (err) { - clearTimeout(stdinTimeout); - if (isDebug) { - console.error(`[DEBUG] Error reading stdin: ${err}`); - } - processHook(); - } -} \ No newline at end of file diff --git a/src/commands/hook-selector.ts b/src/commands/hook-selector.ts deleted file mode 100644 index c27c3f8..0000000 --- a/src/commands/hook-selector.ts +++ /dev/null @@ -1,260 +0,0 @@ -import readline from 'readline'; -import chalk from 'chalk'; -import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs'; -import { join } from 'path'; -import { homedir } from 'os'; - -export interface HookChoice { - name: string; - description: string; - event: string; - selected: boolean; - source?: 'built-in' | 'project' | 'custom'; -} - -// Helper function to save API key -async function saveApiKey(apiKey: string, location: 'global' | 'project'): Promise { - const envPath = location === 'global' - ? join(homedir(), '.claude', '.env') - : join(process.cwd(), '.env'); - - const envDir = location === 'global' - ? join(homedir(), '.claude') - : process.cwd(); - - // Create directory if needed - if (!existsSync(envDir)) { - mkdirSync(envDir, { recursive: true }); - } - - // Check if .env file exists and has content - let envContent = ''; - if (existsSync(envPath)) { - envContent = readFileSync(envPath, 'utf-8'); - // Remove existing ANTHROPIC_API_KEY if present - envContent = envContent.split('\n') - .filter(line => !line.startsWith('ANTHROPIC_API_KEY=')) - .join('\n'); - if (envContent && !envContent.endsWith('\n')) { - envContent += '\n'; - } - } - - // Add the new API key - envContent += `ANTHROPIC_API_KEY=${apiKey}\n`; - - // Write the file - writeFileSync(envPath, envContent, { mode: 0o600 }); // Secure permissions -} - -export class HookSelector { - private choices: HookChoice[]; - private cursorPosition: number = 0; - private rl: readline.Interface; - private resolve: ((value: string[] | null) => void) | null = null; - private onSave: ((selections: string[]) => Promise) | null = null; - - constructor(choices: HookChoice[], onSave?: (selections: string[]) => Promise) { - this.choices = [...choices]; - this.onSave = onSave || null; - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - } - - async run(): Promise { - return new Promise((resolve) => { - this.resolve = resolve; - this.render(); - this.setupKeyHandlers(); - }); - } - - private render() { - console.clear(); - console.log(chalk.cyan('Hook Manager\n')); - console.log(chalk.gray('โ†‘/โ†“: Navigate Enter: Toggle & Save A: Select all D: Deselect all Q/Esc: Quit\n')); - - this.choices.forEach((choice, index) => { - const isCurrentLine = index === this.cursorPosition; - const checkbox = choice.selected ? chalk.green('โ—‰') : 'โ—ฏ'; - const cursor = isCurrentLine ? chalk.cyan('โฏ') : ' '; - const name = choice.name.padEnd(30); - const event = `(${choice.event})`; - - // Determine source label - let sourceLabel = ''; - if (choice.source === 'project') { - sourceLabel = chalk.blue('[project]'); - } else if (choice.source === 'custom') { - sourceLabel = chalk.dim('[custom]'); - } - - // Highlight current line with bold and cyan color - if (isCurrentLine) { - const nameStr = sourceLabel ? `${chalk.bold.cyan(name)} ${sourceLabel}` : chalk.bold.cyan(name); - console.log(`${cursor}${checkbox} ${nameStr} ${chalk.bold.cyan(event)}`); - } else { - const nameStr = sourceLabel ? `${name} ${sourceLabel}` : name; - console.log(`${cursor}${checkbox} ${nameStr} ${chalk.gray(event)}`); - } - }); - - // Show descriptions for current selection - console.log('\n' + chalk.gray('โ”€'.repeat(80)) + '\n'); - const currentChoice = this.choices[this.cursorPosition]; - if (currentChoice) { - console.log(chalk.white('Description: ') + chalk.gray(currentChoice.description)); - } - } - - private setupKeyHandlers() { - if (process.stdin.isTTY) { - process.stdin.setRawMode(true); - } - - process.stdin.resume(); - process.stdin.setEncoding('utf8'); - - process.stdin.on('data', async (key: string) => { - // Handle special keys - if (key === '\u0003' || key === '\u001b' || key.toLowerCase() === 'q') { - // Ctrl+C, Escape, or Q - quit without saving - this.cleanup(); - if (this.resolve) { - this.resolve(null); // Return null to indicate cancellation - } - return; - } - - if (key === '\r') { - // Enter - toggle current item and save - this.choices[this.cursorPosition].selected = !this.choices[this.cursorPosition].selected; - - if (this.onSave) { - const selected = this.choices.filter(c => c.selected).map(c => c.name); - const result = await this.onSave(selected); - this.render(); - - // Check if any hooks were blocked due to missing API key - if (result && result.blockedHooks && result.blockedHooks.length > 0) { - console.log(chalk.yellow('\nโš ๏ธ API Key Required')); - console.log(chalk.gray(`\nThe ${result.blockedHooks.join(', ')} hook(s) require an Anthropic API key.`)); - console.log(chalk.gray('Get your key at: ') + chalk.cyan('https://console.anthropic.com/settings/keys')); - console.log(''); - console.log(chalk.white('Options:')); - console.log(chalk.gray(' 1. Paste your API key now (starts with sk-ant-)')); - console.log(chalk.gray(' 2. Press Esc to cancel and set it up manually')); - console.log(chalk.gray(' โ€ข Add to ~/.claude/.env (all projects)')); - console.log(chalk.gray(' โ€ข Add to .env (current project)')); - console.log(chalk.gray(' Format: ANTHROPIC_API_KEY=sk-ant-...')); - console.log(''); - console.log(chalk.cyan('Paste API key or press Esc: ')); - - // Collect API key input - let apiKeyBuffer = ''; - const apiKeyResult = await new Promise(resolve => { - const handler = (key: string) => { - if (key === '\u001b') { // Esc - process.stdin.removeListener('data', handler); - resolve(null); - } else if (key === '\r') { // Enter - process.stdin.removeListener('data', handler); - resolve(apiKeyBuffer.trim()); - } else if (key === '\u007f' || key === '\b') { // Backspace - if (apiKeyBuffer.length > 0) { - apiKeyBuffer = apiKeyBuffer.slice(0, -1); - process.stdout.write('\b \b'); - } - } else if (key.charCodeAt(0) >= 32) { // Printable characters - apiKeyBuffer += key; - process.stdout.write('*'); // Show asterisks for security - } - }; - process.stdin.on('data', handler); - }); - - if (apiKeyResult && apiKeyResult.startsWith('sk-ant-')) { - // Save the API key - const saveLocation = apiKeyResult.length > 50 ? 'global' : 'global'; // Always save to global for now - await saveApiKey(apiKeyResult, saveLocation); - console.log(chalk.green(`\nโœ“ API key saved to ~/.claude/.env`)); - console.log(chalk.gray('Press Enter to continue...')); - await new Promise(resolve => { - const handler = (key: string) => { - if (key === '\r') { - process.stdin.removeListener('data', handler); - resolve(undefined); - } - }; - process.stdin.on('data', handler); - }); - } else if (apiKeyResult) { - console.log(chalk.red('\nโœ— Invalid API key format (must start with sk-ant-)')); - console.log(chalk.gray('Press Enter to continue...')); - await new Promise(resolve => { - const handler = (key: string) => { - if (key === '\r') { - process.stdin.removeListener('data', handler); - resolve(undefined); - } - }; - process.stdin.on('data', handler); - }); - } else { - console.log(chalk.yellow('\nCancelled. Set up your API key manually to use this hook.')); - } - - this.render(); - } else { - console.log(chalk.gray('\nSaved')); - await new Promise(resolve => setTimeout(resolve, 500)); - } - } else { - this.render(); - } - } else if (key === '\u001b[A' || key === 'k') { - // Up arrow or k - this.cursorPosition = Math.max(0, this.cursorPosition - 1); - this.render(); - } else if (key === '\u001b[B' || key === 'j') { - // Down arrow or j - this.cursorPosition = Math.min(this.choices.length - 1, this.cursorPosition + 1); - this.render(); - } else if (key.toLowerCase() === 'a') { - // A - select all - this.choices.forEach(choice => choice.selected = true); - if (this.onSave) { - const selected = this.choices.filter(c => c.selected).map(c => c.name); - await this.onSave(selected); - this.render(); - console.log(chalk.gray('\nSaved')); - await new Promise(resolve => setTimeout(resolve, 500)); - } else { - this.render(); - } - } else if (key.toLowerCase() === 'd') { - // D - deselect all - this.choices.forEach(choice => choice.selected = false); - if (this.onSave) { - await this.onSave([]); - this.render(); - console.log(chalk.gray('\nSaved')); - await new Promise(resolve => setTimeout(resolve, 500)); - } else { - this.render(); - } - } - }); - } - - private cleanup() { - if (process.stdin.isTTY) { - process.stdin.setRawMode(false); - } - process.stdin.pause(); - process.stdin.removeAllListeners('data'); - this.rl.close(); - } -} \ No newline at end of file diff --git a/src/commands/init.ts b/src/commands/init.ts deleted file mode 100644 index 8fae232..0000000 --- a/src/commands/init.ts +++ /dev/null @@ -1,211 +0,0 @@ -import { writeFileSync, existsSync, mkdirSync, readFileSync } from 'fs'; -import { join } from 'path'; -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import { manage } from './manage.js'; -import { HookSettings, SettingsLocation } from '../types.js'; -import { SETTINGS_LOCATIONS } from '../settings-locations.js'; - -const DEFAULT_SETTINGS: HookSettings = { - "_comment": "Claude Code hooks configuration (using claude-code-hooks-cli)", - "hooks": { - "PreToolUse": [ - { - "matcher": "Bash", - "pattern": "^(npm\\s+(install|i|add)|yarn\\s+(add|install))\\s+", - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec check-package-age" - } - ] - }, - { - "matcher": "Bash", - "pattern": "^git\\s+commit", - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec typescript-check --exclude='node_modules,dist,build,.next,coverage'" - }, - { - "type": "command", - "command": "CHECK_STAGED=true npx claude-code-hooks-cli exec lint-check --exclude='node_modules,dist,build,.next,coverage'" - } - ] - } - ], - "PostToolUse": [ - { - "matcher": "Write|Edit|MultiEdit", - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec code-quality-validator" - }, - { - "type": "command", - "command": "npx claude-code-hooks-cli exec claude-context-updater" - } - ] - } - ], - "Stop": [ - { - "hooks": [ - { - "type": "command", - "command": "npx claude-code-hooks-cli exec task-completion-notify" - } - ] - } - ] - } -}; - -// Settings file locations - -// Helper function to count hooks in a settings file -function countHooks(settingsPath: string): number { - if (!existsSync(settingsPath)) return 0; - - try { - const settings: HookSettings = JSON.parse(readFileSync(settingsPath, 'utf-8')); - let count = 0; - - if (settings.hooks) { - Object.values(settings.hooks).forEach(eventHooks => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach(hookGroup => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - count += hookGroup.hooks.length; - } - }); - } - }); - } - - return count; - } catch (err) { - return 0; - } -} - -async function quickSetup(location: SettingsLocation): Promise { - const targetDir = location.dir; - const fileName = location.file; - const settingsPath = join(targetDir, fileName); - - // Create directory if it doesn't exist - if (!existsSync(targetDir)) { - mkdirSync(targetDir, { recursive: true }); - } - - // Check if settings already exist - if (existsSync(settingsPath)) { - const { overwrite } = await inquirer.prompt([ - { - type: 'confirm', - name: 'overwrite', - message: chalk.yellow(`Settings already exist at ${settingsPath}. Overwrite?`), - default: false - } - ]); - - if (!overwrite) { - console.log(chalk.gray('Cancelled.')); - return; - } - } - - // Write settings - writeFileSync(settingsPath, JSON.stringify(DEFAULT_SETTINGS, null, 2)); - - console.log(); - console.log(chalk.green(`โœ“ Created ${settingsPath}`)); - console.log(); - console.log('Hooks configured:'); - console.log(' โ€ข Package age validation (npm/yarn install)'); - console.log(' โ€ข Code quality validation (file edits)'); - console.log(' โ€ข TypeScript/lint validation (before stop)'); - console.log(); - console.log(chalk.cyan('Next steps:')); - console.log(` 1. Install this package: ${chalk.white('npm install -D claude-code-hooks-cli')}`); - console.log(` 2. Hooks will run automatically in Claude Code`); - console.log(` 3. Run ${chalk.white('npx claude-code-hooks-cli init')} again to customize`); -} - -interface InitOptions { - level?: string; - customMode?: boolean; -} - -export async function init(options: InitOptions): Promise { - // If custom mode is explicitly requested (from manage alias), go straight to manage - if (options.customMode) { - return manage(); - } - - let selectedLocation: SettingsLocation | undefined; - - // If level option is provided, find the corresponding location - if (options.level) { - selectedLocation = SETTINGS_LOCATIONS.find(loc => loc.level === options.level); - if (!selectedLocation) { - console.error(chalk.red(`Invalid level: ${options.level}`)); - console.log(chalk.yellow('Valid options: project, project-alt, local, global')); - process.exit(1); - } - } else { - // First, ask about setup mode - const { setupMode } = await inquirer.prompt([ - { - type: 'list', - name: 'setupMode', - message: 'How would you like to set up hooks?', - choices: [ - { - name: `${chalk.cyan('Quick setup')} ${chalk.gray('(recommended defaults)')}`, - value: 'quick' - }, - { - name: `${chalk.cyan('Custom setup')} ${chalk.gray('(choose your hooks)')}`, - value: 'custom' - } - ], - default: 0 - } - ]); - - if (setupMode === 'custom') { - // Go to manage interface - return manage(); - } - - // Quick setup - ask for location - const locations = SETTINGS_LOCATIONS.map(loc => { - const count = countHooks(loc.path); - const existsText = count > 0 ? ` ${chalk.yellow(`(exists with ${count} hooks)`)}` : ''; - return { - name: `${chalk.cyan(loc.display)} ${chalk.gray(loc.description)}${existsText}`, - value: loc - }; - }); - - const { location } = await inquirer.prompt([ - { - type: 'list', - name: 'location', - message: 'Where would you like to create the settings file?', - choices: locations, - default: 0 - } - ]); - selectedLocation = location; - } - - // Proceed with quick setup - if (selectedLocation) { - await quickSetup(selectedLocation); - } -} \ No newline at end of file diff --git a/src/commands/list.ts b/src/commands/list.ts deleted file mode 100644 index 3213989..0000000 --- a/src/commands/list.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { readdirSync } from 'fs'; -import { join, dirname } from 'path'; -import { fileURLToPath } from 'url'; -import chalk from 'chalk'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -interface HookDescriptions { - [key: string]: string; -} - -const HOOK_DESCRIPTIONS: HookDescriptions = { - // Single-purpose validation hooks - 'typescript-check': 'TypeScript type checking', - 'lint-check': 'Code linting (ESLint, etc.)', - 'test-check': 'Run test suite', - - // Feature hooks - 'check-package-age': 'Prevents installation of outdated npm/yarn packages', - 'code-quality-validator': 'Enforces clean code standards (function length, nesting, etc.)', - 'claude-context-updater': 'Updates CLAUDE.md context file', - 'task-completion-notify': 'System notifications for completed tasks', - - // Legacy hooks (still exist but not recommended) - 'stop-validation': '[LEGACY] Validates TypeScript/lint before stop - use individual checks instead', - 'validate-code': '[LEGACY] Use typescript-check and lint-check instead', - 'validate-on-completion': '[LEGACY] Use typescript-check and lint-check instead' -}; - -export async function list(): Promise { - const hooksDir = join(__dirname, '../../hooks'); - - console.log(chalk.cyan('Available Claude Hooks:\n')); - - try { - const files = readdirSync(hooksDir); - const hooks = files - .filter(f => f.endsWith('.sh')) - .map(f => f.replace('.sh', '')) - .filter(h => !h.includes('common')); // Exclude common libraries - - hooks.forEach(hook => { - const description = HOOK_DESCRIPTIONS[hook] || 'No description available'; - console.log(` ${chalk.green('โ€ข')} ${chalk.white(hook)}`); - console.log(` ${chalk.gray(description)}`); - console.log(` Usage: ${chalk.yellow(`npx claude-code-hooks-cli exec ${hook}`)}`); - console.log(); - }); - - console.log(chalk.cyan('Add to settings.json:')); - console.log(chalk.gray(' Run `npx claude-code-hooks-cli init` to set up hooks')); - console.log(chalk.gray(' Run `npx claude-code-hooks-cli manage` to manage existing hooks')); - - } catch (err: any) { - console.error(chalk.red('Error reading hooks directory:'), err.message); - process.exit(1); - } -} \ No newline at end of file diff --git a/src/commands/location-selector.ts b/src/commands/location-selector.ts deleted file mode 100644 index 1628a34..0000000 --- a/src/commands/location-selector.ts +++ /dev/null @@ -1,140 +0,0 @@ -import readline from 'readline'; -import chalk from 'chalk'; -import { SettingsLocation } from '../types.js'; - -export interface LocationChoice { - path: string; - display: string; - description: string; - hookCount: number; - value: string; -} - -export class LocationSelector { - private choices: LocationChoice[]; - private cursorPosition: number = 0; - private rl: readline.Interface; - private resolve: ((value: string | null) => void) | null = null; - private showStats: boolean; - private stats: Array<{ name: string; count: number; relativeTime: string }>; - - constructor(choices: LocationChoice[], showStats: boolean = false, stats: Array<{ name: string; count: number; relativeTime: string }> = []) { - this.choices = [...choices]; - this.showStats = showStats; - this.stats = stats; - this.rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); - } - - async run(): Promise { - return new Promise((resolve) => { - this.resolve = resolve; - this.render(); - this.setupKeyHandlers(); - }); - } - - private render() { - console.clear(); - console.log(chalk.cyan('Claude Hooks Manager\n')); - - // Show stats if available - if (this.showStats && this.stats.length > 0) { - console.log(chalk.gray('โ”€'.repeat(50))); - console.log(chalk.gray('Hook Name'.padEnd(30) + 'Calls'.padEnd(10) + 'Last Called')); - console.log(chalk.gray('โ”€'.repeat(50))); - - this.stats.forEach(stat => { - const name = stat.name.padEnd(30); - const calls = stat.count.toString().padEnd(10); - const lastCall = stat.relativeTime; - console.log(`${name}${calls}${lastCall}`); - }); - console.log(chalk.gray('โ”€'.repeat(50))); - console.log(); - } - - console.log(chalk.gray('โ†‘/โ†“: Navigate Enter: Select Q/Esc: Exit\n')); - - this.choices.forEach((choice, index) => { - const isCurrentLine = index === this.cursorPosition; - const cursor = isCurrentLine ? chalk.cyan('โฏ') : ' '; - - let line = ''; - if (choice.value === 'separator') { - line = chalk.gray('โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€'); - } else if (choice.value === 'exit') { - line = chalk.red('โœ• Exit'); - } else if (choice.value === 'view-logs' || choice.value === 'tail-logs') { - line = choice.display; - } else { - // Regular location choice - const hookInfo = `(${choice.hookCount} ${choice.hookCount === 1 ? 'hook' : 'hooks'})`; - const mainText = `${choice.display} ${hookInfo} - ${choice.description}`; - line = mainText; - } - - // Highlight current line - if (isCurrentLine && choice.value !== 'separator') { - console.log(`${cursor}${chalk.bold.cyan(line)}`); - } else { - console.log(`${cursor}${line}`); - } - }); - } - - private setupKeyHandlers() { - if (process.stdin.isTTY) { - process.stdin.setRawMode(true); - } - - process.stdin.resume(); - process.stdin.setEncoding('utf8'); - - process.stdin.on('data', async (key: string) => { - // Handle special keys - if (key === '\u0003' || key === '\u001b' || key.toLowerCase() === 'q') { - // Ctrl+C, Escape, or Q - exit - this.cleanup(); - if (this.resolve) { - this.resolve('exit'); - } - return; - } - - if (key === '\r') { - // Enter - select current item - const choice = this.choices[this.cursorPosition]; - if (choice.value !== 'separator') { - this.cleanup(); - if (this.resolve) { - this.resolve(choice.value); - } - } - } else if (key === '\u001b[A' || key === 'k') { - // Up arrow or k - skip separators - do { - this.cursorPosition = Math.max(0, this.cursorPosition - 1); - } while (this.cursorPosition > 0 && this.choices[this.cursorPosition].value === 'separator'); - this.render(); - } else if (key === '\u001b[B' || key === 'j') { - // Down arrow or j - skip separators - do { - this.cursorPosition = Math.min(this.choices.length - 1, this.cursorPosition + 1); - } while (this.cursorPosition < this.choices.length - 1 && this.choices[this.cursorPosition].value === 'separator'); - this.render(); - } - }); - } - - private cleanup() { - if (process.stdin.isTTY) { - process.stdin.setRawMode(false); - } - process.stdin.pause(); - process.stdin.removeAllListeners('data'); - this.rl.close(); - } -} \ No newline at end of file diff --git a/src/commands/manage.ts b/src/commands/manage.ts deleted file mode 100644 index 5aab835..0000000 --- a/src/commands/manage.ts +++ /dev/null @@ -1,742 +0,0 @@ -import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'fs'; -import { join } from 'path'; -import chalk from 'chalk'; -import inquirer from 'inquirer'; -import { execSync } from 'child_process'; -import { - HookConfigs, - HookSettings, - SettingsLocation, - HookInfo, - HookStats, - HookStatDisplay, - DiscoveredHook, - HookSource -} from '../types.js'; -import { SETTINGS_LOCATIONS } from '../settings-locations.js'; -import { HookValidator } from '../validation/index.js'; -import { HookSelector, HookChoice } from './hook-selector.js'; -import { LocationSelector, LocationChoice } from './location-selector.js'; -import { discoverHookTemplates, mergeHooksWithDiscovered } from '../discovery/hook-discovery.js'; -import { homedir } from 'os'; -import { setupDocCompliance } from './doc-compliance-setup.js'; - -// Helper function to check if API key exists -function hasApiKey(apiKeyType?: string): boolean { - const keyName = apiKeyType === 'gemini' ? 'GEMINI_API_KEY' : 'ANTHROPIC_API_KEY'; - - // Check environment variable - if (process.env[keyName]) return true; - - // Check ~/.gemini/.env for Gemini or ~/.claude/.env for Claude - const envDir = apiKeyType === 'gemini' ? '.gemini' : '.claude'; - const envPath = join(homedir(), envDir, '.env'); - if (existsSync(envPath)) { - try { - const content = readFileSync(envPath, 'utf-8'); - if (content.includes(`${keyName}=`) && !content.includes(`${keyName}=\n`)) { - return true; - } - } catch (e) { - // Ignore errors - } - } - - // Check project .env - const projectEnvPath = join(process.cwd(), '.env'); - if (existsSync(projectEnvPath)) { - try { - const content = readFileSync(projectEnvPath, 'utf-8'); - if (content.includes(`${keyName}=`) && !content.includes(`${keyName}=\n`)) { - return true; - } - } catch (e) { - // Ignore errors - } - } - - return false; -} - -// Helper function to save API key -async function saveApiKey(apiKey: string, location: 'global' | 'project'): Promise { - const envPath = location === 'global' - ? join(homedir(), '.claude', '.env') - : join(process.cwd(), '.env'); - - const envDir = location === 'global' - ? join(homedir(), '.claude') - : process.cwd(); - - // Create directory if needed - if (!existsSync(envDir)) { - mkdirSync(envDir, { recursive: true }); - } - - // Check if .env file exists and has content - let envContent = ''; - if (existsSync(envPath)) { - envContent = readFileSync(envPath, 'utf-8'); - // Remove existing ANTHROPIC_API_KEY if present - envContent = envContent.split('\n') - .filter(line => !line.startsWith('ANTHROPIC_API_KEY=')) - .join('\n'); - if (envContent && !envContent.endsWith('\n')) { - envContent += '\n'; - } - } - - // Add the new API key - envContent += `ANTHROPIC_API_KEY=${apiKey}\n`; - - // Write the file - writeFileSync(envPath, envContent, { mode: 0o600 }); // Secure permissions -} - -// Available hooks from the npm package -const AVAILABLE_HOOKS: HookConfigs = { - // Single-purpose validation hooks - 'typescript-check': { - event: 'PreToolUse', - matcher: 'Bash', - pattern: '^git\\s+commit', - description: 'TypeScript type checking' - }, - 'lint-check': { - event: 'PreToolUse', - matcher: 'Bash', - pattern: '^git\\s+commit', - description: 'Code linting (ESLint, etc.)' - }, - 'test-check': { - event: 'PreToolUse', - matcher: 'Bash', - description: 'Run test suite' - }, - - // Package management - 'check-package-age': { - event: 'PreToolUse', - matcher: 'Bash', - pattern: '^(npm\\s+(install|i|add)|yarn\\s+(add|install))\\s+', - description: 'Prevents installation of outdated npm/yarn packages' - }, - - // Code quality - 'code-quality-validator': { - event: 'PostToolUse', - matcher: 'Write|Edit|MultiEdit', - description: 'Enforces clean code standards (function length, nesting, etc.)' - }, - - // Notifications - 'task-completion-notify': { - event: 'Stop', - description: 'System notifications when Claude finishes' - }, - - // Documentation compliance - 'doc-compliance': { - event: 'Stop', - description: 'AI-powered code compliance checking with Gemini Flash (~5s analysis)', - requiresApiKey: true, - apiKeyType: 'gemini' - }, - - // Testing and development - 'self-test': { - event: 'PreWrite', - pattern: '\\.test-trigger$', - description: 'Claude self-test hook for automated hook validation during development' - } -}; - - -// Helper function to extract hook name from command -function extractHookName(command: string): string | null { - // Try to extract from npx claude-code-hooks-cli exec pattern - const match = command.match(/exec\s+([a-z-]+)/); - if (match) return match[1]; - - // For custom hooks, use a sanitized version of the command as the name - // Remove common prefixes and extract meaningful part - const customName = command - .replace(/^(npx|node|bash|sh|\.\/)/g, '') - .replace(/\.(sh|js|py|rb)$/g, '') - .replace(/[^a-zA-Z0-9-]/g, '-') - .replace(/-+/g, '-') - .replace(/^-|-$/g, '') - .slice(0, 50); // Limit length - - return customName || 'custom-hook'; -} - -// Helper function to count hooks in a settings file -function countHooks(settingsPath: string): number { - // Convert relative paths to absolute - const absolutePath = settingsPath.startsWith('/') || settingsPath.startsWith(process.env.HOME || '') - ? settingsPath - : join(process.cwd(), settingsPath); - - if (!existsSync(absolutePath)) return 0; - - try { - const settings: HookSettings = JSON.parse(readFileSync(absolutePath, 'utf-8')); - let count = 0; - - if (settings.hooks) { - Object.values(settings.hooks).forEach(eventHooks => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach(hookGroup => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - count += hookGroup.hooks.length; - } - }); - } - }); - } - - return count; - } catch (err) { - return 0; - } -} - -// Helper function to format relative time -function formatRelativeTime(dateStr: string | null): string { - if (!dateStr) return 'Never'; - - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffSecs = Math.floor(diffMs / 1000); - const diffMins = Math.floor(diffSecs / 60); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffDays > 0) return `${diffDays}d ago`; - if (diffHours > 0) return `${diffHours}h ago`; - if (diffMins > 0) return `${diffMins}m ago`; - return 'Just now'; -} - -// Helper function to get hook stats from logs -function getHookStats(hookName: string): HookStats { - try { - const logFile = `${process.env.HOME}/.local/share/claude-hooks/logs/hooks.log`; - if (!existsSync(logFile)) { - return { count: 0, lastCall: null }; - } - - // Get execution count - const count = parseInt(execSync( - `grep -c "\\[${hookName}\\] Hook started" "${logFile}" 2>/dev/null || echo 0`, - { encoding: 'utf-8' } - ).trim()) || 0; - - if (count === 0) { - return { count: 0, lastCall: null }; - } - - // Get last execution time - const lastLine = execSync( - `grep "\\[${hookName}\\] Hook" "${logFile}" 2>/dev/null | tail -1 || echo ""`, - { encoding: 'utf-8' } - ).trim(); - - let lastCall: string | null = null; - if (lastLine) { - const match = lastLine.match(/\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]/); - if (match) { - lastCall = match[1]; - } - } - - return { count, lastCall }; - } catch (error) { - // If anything fails, return zeros - return { count: 0, lastCall: null }; - } -} - -// Helper function to get all hook statistics -function getAllHookStats(): HookStatDisplay[] { - const hookStats: HookStatDisplay[] = []; - - // Get all available hooks - Object.entries(AVAILABLE_HOOKS).forEach(([hookName, config]) => { - const stats = getHookStats(hookName); - if (stats.count > 0) { - hookStats.push({ - name: hookName, - count: stats.count, - lastCall: stats.lastCall, - relativeTime: formatRelativeTime(stats.lastCall) - }); - } - }); - - // Sort by count (descending) - return hookStats.sort((a, b) => b.count - a.count); -} - -// Helper function to get all hooks from a settings file -function getHooksFromSettings(settings: HookSettings): HookInfo[] { - const hooks: HookInfo[] = []; - - if (!settings.hooks) return hooks; - - Object.entries(settings.hooks).forEach(([event, eventHooks]) => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach((hookGroup, groupIndex) => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - hookGroup.hooks.forEach((hook, hookIndex) => { - const hookName = extractHookName(hook.command); - if (hookName) { - const isKnownHook = hookName in AVAILABLE_HOOKS; - hooks.push({ - event, - groupIndex, - hookIndex, - name: hookName, - matcher: hookGroup.matcher, - pattern: hookGroup.pattern, - command: hook.command, - description: isKnownHook - ? AVAILABLE_HOOKS[hookName].description - : `Custom hook: ${hook.command.slice(0, 60)}${hook.command.length > 60 ? '...' : ''}`, - stats: getHookStats(hookName) - }); - } - }); - } - }); - } - }); - - return hooks; -} - -// Helper function to get all unique hooks from settings (including custom ones) -function getAllUniqueHooks(settings: HookSettings): Set { - const uniqueHooks = new Set(); - - if (!settings.hooks) return uniqueHooks; - - Object.values(settings.hooks).forEach(eventHooks => { - if (Array.isArray(eventHooks)) { - eventHooks.forEach(hookGroup => { - if (hookGroup.hooks && Array.isArray(hookGroup.hooks)) { - hookGroup.hooks.forEach(hook => { - const hookName = extractHookName(hook.command); - if (hookName) { - uniqueHooks.add(hookName); - } - }); - } - }); - } - }); - - return uniqueHooks; -} - -// Helper function to remove a hook from settings -function removeHook(settings: HookSettings, hookToRemove: HookInfo): void { - const eventHooks = settings.hooks[hookToRemove.event]; - if (!eventHooks || !Array.isArray(eventHooks)) return; - - const hookGroup = eventHooks[hookToRemove.groupIndex]; - if (!hookGroup || !hookGroup.hooks) return; - - // Remove the hook - hookGroup.hooks.splice(hookToRemove.hookIndex, 1); - - // If this was the last hook in the group, remove the group - if (hookGroup.hooks.length === 0) { - eventHooks.splice(hookToRemove.groupIndex, 1); - } -} - -// Helper function to add a hook to settings -function addHook(settings: HookSettings, hookName: string, customHookInfo?: HookInfo, discoveredHook?: DiscoveredHook): void { - const hookConfig = AVAILABLE_HOOKS[hookName]; - - // If it's not a known hook and no custom/discovered info provided, skip - if (!hookConfig && !customHookInfo && !discoveredHook) return; - - // Determine event, matcher, pattern, and command - const event = hookConfig?.event || discoveredHook?.event || customHookInfo?.event || 'PreToolUse'; - const matcher = hookConfig?.matcher || discoveredHook?.matcher || customHookInfo?.matcher; - const pattern = hookConfig?.pattern || discoveredHook?.pattern || customHookInfo?.pattern; - - let command: string; - if (hookConfig) { - // Built-in hook - command = `npx claude-code-hooks-cli exec ${hookName}`; - } else if (discoveredHook?.command) { - // Project hook with custom command - command = discoveredHook.command; - } else if (customHookInfo?.command) { - // Custom hook - command = customHookInfo.command; - } else { - // Default to exec pattern - command = `npx claude-code-hooks-cli exec ${hookName}`; - } - - // Ensure hooks structure exists - if (!settings.hooks) settings.hooks = {}; - if (!settings.hooks[event]) settings.hooks[event] = []; - - const eventHooks = settings.hooks[event]; - - // Find existing group with same matcher and pattern, or create new one - let targetGroup = eventHooks.find(group => - group.matcher === matcher && - group.pattern === pattern - ); - - if (!targetGroup) { - targetGroup = { - hooks: [] - }; - if (matcher) targetGroup.matcher = matcher; - if (pattern) targetGroup.pattern = pattern; - eventHooks.push(targetGroup); - } - - // Add the hook - targetGroup.hooks.push({ - type: 'command', - command - }); -} - -// Helper function to save settings -function saveSettings(path: string, settings: HookSettings, silent: boolean = false): void { - // Validate settings before saving - const validator = new HookValidator(); - const result = validator.validateSettings(settings); - - if (!result.valid) { - if (!silent) { - console.error(chalk.red('\nโŒ Invalid settings configuration:')); - console.error(validator.formatResults(result, true)); - console.error(chalk.yellow('\nSettings were not saved due to validation errors.')); - } - throw new Error('Invalid settings configuration'); - } - - // Show warnings if any - if (!silent && result.warnings.length > 0) { - console.warn(chalk.yellow('\nโš ๏ธ Validation warnings:')); - result.warnings.forEach(warning => { - console.warn(chalk.yellow(` - ${warning.message}`)); - }); - } - - // Create directory if needed - const dir = join(path, '..'); - if (!existsSync(dir)) { - mkdirSync(dir, { recursive: true }); - } - - // Save settings - writeFileSync(path, JSON.stringify(settings, null, 2)); -} - -export async function manage(): Promise { - // Ensure Ctrl+C always works - process.on('SIGINT', () => { - console.log(chalk.yellow('\n\nExiting...')); - process.exit(0); - }); - - let selectedPath: string | null = null; - - while (true) { - console.clear(); - console.log(chalk.cyan('Claude Hooks Manager\n')); - - if (!selectedPath) { - // Get hook statistics - const allStats = getAllHookStats(); - - // Build location choices - const locationChoices: LocationChoice[] = SETTINGS_LOCATIONS.map(loc => ({ - path: loc.path, - display: loc.display, - description: loc.description, - hookCount: countHooks(loc.path), - value: loc.path - })); - - // Add separator - locationChoices.push({ - path: '', - display: '', - description: '', - hookCount: 0, - value: 'separator' - }); - - // Add log viewing options - locationChoices.push({ - path: '', - display: chalk.gray('๐Ÿ“‹ View recent logs'), - description: '', - hookCount: 0, - value: 'view-logs' - }); - - locationChoices.push({ - path: '', - display: chalk.gray('๐Ÿ“Š Tail logs (live)'), - description: '', - hookCount: 0, - value: 'tail-logs' - }); - - // Add separator - locationChoices.push({ - path: '', - display: '', - description: '', - hookCount: 0, - value: 'separator' - }); - - // Add exit option - locationChoices.push({ - path: '', - display: 'โœ• Exit', - description: '', - hookCount: 0, - value: 'exit' - }); - - // Run the location selector - const selector = new LocationSelector(locationChoices, allStats.length > 0, allStats); - const path = await selector.run(); - - if (path === 'exit') { - console.log(chalk.yellow('Goodbye!')); - process.exit(0); - } - - if (path === 'view-logs') { - console.clear(); - const logFile = `${process.env.HOME}/.local/share/claude-hooks/logs/hooks.log`; - if (existsSync(logFile)) { - console.log(chalk.yellow('Recent hook logs (last 50 lines):\n')); - execSync(`tail -50 "${logFile}"`, { stdio: 'inherit' }); - } else { - console.log(chalk.gray('No log file found.')); - } - console.log(chalk.gray('\nPress Enter to continue...')); - try { - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - } catch (err) { - // User pressed Ctrl+C - process.exit(0); - } - continue; - } - - if (path === 'tail-logs') { - console.clear(); - const logFile = `${process.env.HOME}/.local/share/claude-hooks/logs/hooks.log`; - if (existsSync(logFile)) { - console.log(chalk.yellow('Tailing hook logs (Ctrl+C to stop):\n')); - try { - execSync(`tail -f "${logFile}"`, { stdio: 'inherit' }); - } catch (e) { - // User pressed Ctrl+C - } - } else { - console.log(chalk.gray('No log file found.')); - console.log(chalk.gray('\nPress Enter to continue...')); - try { - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - } catch (err) { - // User pressed Ctrl+C - process.exit(0); - } - } - continue; - } - - selectedPath = path; - } - - // Load or create settings - let settings: HookSettings = { - "_comment": "Claude Code hooks configuration (using claude-code-hooks-cli)", - "hooks": {} - }; - - // Convert relative paths to absolute - const absoluteSelectedPath = selectedPath && (selectedPath.startsWith('/') || selectedPath.startsWith(process.env.HOME || '') - ? selectedPath - : join(process.cwd(), selectedPath)); - - if (absoluteSelectedPath && existsSync(absoluteSelectedPath)) { - try { - settings = JSON.parse(readFileSync(absoluteSelectedPath, 'utf-8')); - - // Validate loaded settings - const validator = new HookValidator(); - const result = validator.validateSettings(settings); - - if (!result.valid) { - console.error(chalk.red(`\nโŒ Invalid settings in ${selectedPath}:`)); - console.error(validator.formatResults(result, true)); - console.error(chalk.yellow('\nPlease fix these issues before managing hooks.')); - console.error(chalk.gray('\nPress Enter to continue...')); - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - selectedPath = null; - continue; - } - - // Show warnings if any - if (result.warnings.length > 0) { - console.warn(chalk.yellow('\nโš ๏ธ Validation warnings:')); - result.warnings.forEach(warning => { - console.warn(chalk.yellow(` - ${warning.message}`)); - }); - console.warn(chalk.gray('\nPress Enter to continue...')); - await inquirer.prompt([{ type: 'input', name: 'continue', message: '' }]); - } - } catch (err: any) { - console.error(chalk.red(`Error reading ${selectedPath}: ${err.message}`)); - return; - } - } - - // Management loop for this file - let managingFile = true; - while (managingFile) { - // Discover project hook templates - const discoveredHooks = discoverHookTemplates(); - const allAvailableHooks = mergeHooksWithDiscovered(AVAILABLE_HOOKS, discoveredHooks); - - // Get current hooks - const currentHooks = getHooksFromSettings(settings); - const currentHookNames = new Set(currentHooks.map(h => h.name)); - - // Build hook choices for the selector - const hookChoices: HookChoice[] = []; - - // Add all available hooks (built-in and discovered) - allAvailableHooks.forEach(hook => { - hookChoices.push({ - name: hook.name, - description: hook.description, - event: hook.event, - selected: currentHookNames.has(hook.name), - source: hook.source - }); - }); - - // Add custom hooks that are in settings but not in available hooks - currentHooks.forEach(hook => { - const existingHook = allAvailableHooks.find(h => h.name === hook.name); - if (!existingHook) { - // Don't add duplicates - if (!hookChoices.find(choice => choice.name === hook.name)) { - hookChoices.push({ - name: hook.name, - description: hook.description, - event: hook.event, - selected: true, // Custom hooks in settings are always selected - source: 'custom' - }); - } - } - }); - - // Sort hooks: selected first, then by source (built-in, project, custom), then by name - hookChoices.sort((a, b) => { - if (a.selected !== b.selected) return b.selected ? 1 : -1; - - // Sort by source priority - const sourceOrder = { 'built-in': 0, 'project': 1, 'custom': 2 }; - const aOrder = sourceOrder[a.source || 'custom']; - const bOrder = sourceOrder[b.source || 'custom']; - if (aOrder !== bOrder) return aOrder - bOrder; - - return a.name.localeCompare(b.name); - }); - - // Create the save handler - const saveHandler = async (selectedHookNames: string[]) => { - // Check if any newly selected hooks require API key - const newlySelectedHooks = selectedHookNames.filter(name => !currentHookNames.has(name)); - const hooksRequiringApiKey = newlySelectedHooks - .map(hookName => allAvailableHooks.find(h => h.name === hookName)) - .filter(hook => hook?.requiresApiKey); - - // If any newly selected hooks require API key and we don't have one, show message - const missingApiKeys = hooksRequiringApiKey.filter(hook => !hasApiKey(hook?.apiKeyType)); - if (missingApiKeys.length > 0) { - // Don't save these hooks - const missingHookNames = missingApiKeys.map(h => h!.name); - selectedHookNames = selectedHookNames.filter(name => !missingHookNames.includes(name)); - } - - // Keep track of which custom hooks we need to preserve - const customHooksToPreserve = currentHooks.filter(hook => { - const foundInAvailable = allAvailableHooks.find(h => h.name === hook.name); - return !foundInAvailable && selectedHookNames.includes(hook.name); - }); - - // Clear hooks and rebuild based on selection - settings.hooks = {}; - selectedHookNames.forEach(hookName => { - // Check if it's a discovered hook - const discoveredHook = allAvailableHooks.find(h => h.name === hookName && h.source !== 'built-in'); - - // Check if it's a custom hook we need to preserve - const customHook = customHooksToPreserve.find(h => h.name === hookName); - - if (customHook) { - addHook(settings, hookName, customHook); - } else if (discoveredHook) { - addHook(settings, hookName, undefined, discoveredHook); - } else { - addHook(settings, hookName); - - // Special handling for doc-compliance hook - if (hookName === 'doc-compliance' && newlySelectedHooks.includes(hookName)) { - setupDocCompliance(); - } - } - }); - - if (absoluteSelectedPath) { - try { - saveSettings(absoluteSelectedPath, settings, true); // silent mode for auto-save - } catch (err) { - // Handle validation errors silently during auto-save - } - } - - // AFTER saving, if we blocked any hooks, return a special flag - if (missingApiKeys.length > 0) { - return { blockedHooks: missingApiKeys.map(h => h!.name) }; - } - }; - - // Run the hook selector - const selector = new HookSelector(hookChoices, saveHandler); - const result = await selector.run(); - - // If user cancelled (Ctrl+C, Q, or Esc), go back to location selection - if (result === null) { - managingFile = false; - selectedPath = null; - } - } - } -} \ No newline at end of file diff --git a/src/commands/universal-hook.ts b/src/commands/universal-hook.ts deleted file mode 100644 index 85127b0..0000000 --- a/src/commands/universal-hook.ts +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env node - -import { HookInput } from '../types/hook-types.js'; -import { readStdin } from '../utils/stdin-reader.js'; -import { executeHook, logHookEvent } from '../utils/hook-executor.js'; -import { loadHookConfig, getEventConfig } from '../utils/config-loader.js'; -import { collectHooks } from '../utils/pattern-matcher.js'; -import { logToFile, debugLog } from '../utils/hook-logger.js'; - -async function processHooks(input: HookInput): Promise { - const eventType = input.hook_event_name; - logHookEvent(`Event: ${eventType}`); - logToFile(`Event type: ${eventType}`); - - const config = await loadHookConfig(); - if (!config) return; - - const eventConfig = getEventConfig(config, eventType); - if (!eventConfig) return; - - const hooks = collectHooks(eventConfig, input, eventType); - - debugLog(`Matched ${hooks.length} hooks: ${hooks.join(', ')}`); - - for (const hook of hooks) { - debugLog(`Executing hook: ${hook}`); - await executeHook(hook, input); - } -} - -export async function universalHook(): Promise { - logToFile('UNIVERSAL-HOOK STARTED'); - logHookEvent(`Started at ${new Date().toISOString()}`); - - try { - const inputStr = await readStdin(); - logToFile(`Input received: ${inputStr.substring(0, 200)}`); - - const input: HookInput = JSON.parse(inputStr); - debugLog(`Input: ${JSON.stringify(input, null, 2)}`); - - await processHooks(input); - } catch (error) { - console.error(`Universal hook error: ${error}`); - process.exit(1); - } -} - -// Run if called directly -if (import.meta.url === `file://${process.argv[1]}`) { - universalHook().catch(console.error); -} \ No newline at end of file diff --git a/src/commands/validate.ts b/src/commands/validate.ts deleted file mode 100644 index 751586c..0000000 --- a/src/commands/validate.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { existsSync } from 'fs'; -import { resolve, basename } from 'path'; -import chalk from 'chalk'; -import { HookValidator } from '../validation/index.js'; -import { SETTINGS_LOCATIONS } from '../settings-locations.js'; - -interface ValidateOptions { - verbose?: boolean; - fix?: boolean; -} - -export async function validate(path?: string, options: ValidateOptions = {}): Promise { - const validator = new HookValidator(); - - // Find files to validate - let files: string[] = []; - - if (path) { - // Specific file provided - if (!existsSync(path)) { - console.error(chalk.red(`File not found: ${path}`)); - process.exit(1); - } - files = [resolve(path)]; - } else { - // No path provided, validate all existing settings files - files = SETTINGS_LOCATIONS - .map(loc => loc.path) - .filter(path => existsSync(path)); - } - - if (files.length === 0) { - console.error(chalk.red('No files found to validate')); - process.exit(1); - } - - console.log(chalk.cyan(`Validating ${files.length} file(s)...\n`)); - - let hasErrors = false; - let totalErrors = 0; - let totalWarnings = 0; - - for (const file of files) { - console.log(chalk.blue(`Validating ${basename(file)}...`)); - - const result = await validator.validateSettingsFile(file); - - console.log(validator.formatResults(result, options.verbose)); - if (!result.valid || result.warnings.length > 0) { - console.log(); - } - - if (!result.valid) { - hasErrors = true; - } - - totalErrors += result.errors.length; - totalWarnings += result.warnings.length; - } - - if (hasErrors) { - process.exit(1); - } -} \ No newline at end of file diff --git a/src/discovery/hook-discovery.ts b/src/discovery/hook-discovery.ts deleted file mode 100644 index dbe0bee..0000000 --- a/src/discovery/hook-discovery.ts +++ /dev/null @@ -1,86 +0,0 @@ -import { readFileSync, existsSync } from 'fs'; -import { join } from 'path'; -import { HookTemplates, DiscoveredHook, HookConfigs } from '../types.js'; -import { HookTemplateValidator } from '../validation/hook-template-validator.js'; - -const HOOK_TEMPLATE_PATHS = [ - '.claude/hooks.json', - '.claude/hook-templates.json', - 'hooks/templates.json' -]; - -export function discoverHookTemplates(workingDir: string = process.cwd()): DiscoveredHook[] { - const discovered: DiscoveredHook[] = []; - const validator = new HookTemplateValidator(); - - for (const templatePath of HOOK_TEMPLATE_PATHS) { - const fullPath = join(workingDir, templatePath); - - if (existsSync(fullPath)) { - try { - const content = readFileSync(fullPath, 'utf-8'); - const templates = JSON.parse(content); - - // Validate templates - const validation = validator.validateTemplates(templates); - - if (!validation.valid) { - console.error(`Warning: Invalid hook templates in ${fullPath}:`); - validation.errors.forEach(error => console.error(` - ${error}`)); - continue; - } - - if (validation.warnings.length > 0) { - console.warn(`Warnings for hook templates in ${fullPath}:`); - validation.warnings.forEach(warning => console.warn(` - ${warning}`)); - } - - // Convert templates to discovered hooks - const validTemplates = templates as HookTemplates; - Object.entries(validTemplates).forEach(([name, template]) => { - discovered.push({ - name, - event: template.event, - matcher: template.matcher, - pattern: template.pattern, - description: template.description, - command: template.command, - source: 'project', - requiresApiKey: template.requiresApiKey - }); - }); - } catch (err) { - // Skip files with parse errors - console.error(`Warning: Failed to parse hook templates from ${fullPath}: ${err}`); - } - } - } - - return discovered; -} - -export function mergeHooksWithDiscovered( - availableHooks: HookConfigs, - discoveredHooks: DiscoveredHook[] -): DiscoveredHook[] { - const allHooks: DiscoveredHook[] = []; - - // Add built-in hooks - Object.entries(availableHooks).forEach(([name, config]) => { - allHooks.push({ - name, - ...config, - source: 'built-in' - }); - }); - - // Add discovered hooks (project hooks) - // Filter out any that have the same name as built-in hooks - discoveredHooks.forEach(hook => { - if (!availableHooks[hook.name]) { - allHooks.push(hook); - } - }); - - return allHooks; -} \ No newline at end of file diff --git a/src/settings-locations.ts b/src/settings-locations.ts deleted file mode 100644 index acdd9b6..0000000 --- a/src/settings-locations.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { SettingsLocation } from './types.js'; - -export const SETTINGS_LOCATIONS: SettingsLocation[] = [ - { - path: './.claude/settings.json', - dir: './.claude', - file: 'settings.json', - display: '.claude/settings.json', - description: 'Project settings', - level: 'project' - }, - { - path: './.claude/settings.local.json', - dir: './.claude', - file: 'settings.local.json', - display: '.claude/settings.local.json', - description: 'Personal project settings', - level: 'local' - }, - { - path: `${process.env.HOME}/.claude/settings.json`, - dir: `${process.env.HOME}/.claude`, - file: 'settings.json', - display: '~/.claude/settings.json', - description: 'Personal global settings', - level: 'global' - } -]; \ No newline at end of file diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 239dc05..0000000 --- a/src/types.ts +++ /dev/null @@ -1,79 +0,0 @@ -export interface HookConfig { - event: string; - matcher?: string; - pattern?: string; - description: string; - requiresApiKey?: boolean; - apiKeyType?: string; -} - -export interface HookConfigs { - [key: string]: HookConfig; -} - -export interface SettingsLocation { - path: string; - dir: string; - file: string; - display: string; - description: string; - level: string; -} - -export interface HookSettings { - _comment?: string; - hooks: { - [event: string]: Array<{ - matcher?: string; - pattern?: string; - hooks: Array<{ - type: string; - command: string; - }>; - }>; - }; -} - -export interface HookInfo { - event: string; - groupIndex: number; - hookIndex: number; - name: string; - matcher?: string; - pattern?: string; - command: string; - description: string; - stats: HookStats; -} - -export interface HookStats { - count: number; - lastCall: string | null; -} - -export interface HookStatDisplay { - name: string; - count: number; - lastCall: string | null; - relativeTime: string; -} - -export interface HookTemplate extends HookConfig { - command?: string; - requiresApiKey?: boolean; - apiKeyType?: string; -} - -export interface HookTemplates { - [key: string]: HookTemplate; -} - -export type HookSource = 'built-in' | 'project' | 'custom'; - -export interface DiscoveredHook extends HookConfig { - name: string; - source: HookSource; - command?: string; - requiresApiKey?: boolean; - apiKeyType?: string; -} \ No newline at end of file diff --git a/src/types/hook-types.ts b/src/types/hook-types.ts deleted file mode 100644 index ce2993c..0000000 --- a/src/types/hook-types.ts +++ /dev/null @@ -1,57 +0,0 @@ -// Hook Input Types - -export interface BaseHookInput { - session_id: string; - transcript_path: string; - hook_event_name: string; -} - -export interface PreToolUseInput extends BaseHookInput { - hook_event_name: 'PreToolUse'; - tool_name: string; - tool_input: { - command?: string; - file_path?: string; - content?: string; - pattern?: string; - [key: string]: any; - }; -} - -export interface PostToolUseInput extends BaseHookInput { - hook_event_name: 'PostToolUse'; - tool_name: string; - tool_input: { - command?: string; - file_path?: string; - content?: string; - pattern?: string; - [key: string]: any; - }; - tool_response: { - success?: boolean; - filePath?: string; - error?: string; - [key: string]: any; - }; -} - -export interface StopInput extends BaseHookInput { - hook_event_name: 'Stop'; - stop_hook_active: boolean; -} - -export interface SubagentStopInput extends BaseHookInput { - hook_event_name: 'SubagentStop'; - stop_hook_active: boolean; -} - -export type HookInput = PreToolUseInput | PostToolUseInput | StopInput | SubagentStopInput; - -export interface HookOutput { - continue?: boolean; - stopReason?: string; - decision?: 'approve' | 'block'; - reason?: string; - suppressOutput?: boolean; -} \ No newline at end of file diff --git a/src/utils/api-key-manager.ts b/src/utils/api-key-manager.ts deleted file mode 100644 index cd37c48..0000000 --- a/src/utils/api-key-manager.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { existsSync, readFileSync } from 'fs'; -import { join } from 'path'; -import { homedir } from 'os'; - -export type ApiKeyType = 'gemini' | 'anthropic'; - -export interface ApiKeyConfig { - keyName: string; - envDir: string; -} - -const apiKeyConfigs: Record = { - gemini: { - keyName: 'GEMINI_API_KEY', - envDir: '.gemini' - }, - anthropic: { - keyName: 'ANTHROPIC_API_KEY', - envDir: '.claude' - } -}; - -export function getApiKeyConfig(apiKeyType: ApiKeyType = 'anthropic'): ApiKeyConfig { - return apiKeyConfigs[apiKeyType]; -} - -export function hasApiKey(apiKeyType: ApiKeyType = 'anthropic'): boolean { - const config = getApiKeyConfig(apiKeyType); - const { keyName, envDir } = config; - - // Check environment variable - if (process.env[keyName]) return true; - - // Check home directory .env file - const homeEnvPath = join(homedir(), envDir, '.env'); - if (checkEnvFile(homeEnvPath, keyName)) return true; - - // Check project .env file - const projectEnvPath = join(process.cwd(), '.env'); - if (checkEnvFile(projectEnvPath, keyName)) return true; - - return false; -} - -function checkEnvFile(path: string, keyName: string): boolean { - if (existsSync(path)) { - try { - const content = readFileSync(path, 'utf-8'); - return content.includes(`${keyName}=`) && !content.includes(`${keyName}=\n`); - } catch (e) { - // Ignore errors - } - } - return false; -} - -export async function saveApiKey(apiKey: string, apiKeyType: ApiKeyType = 'anthropic'): Promise { - const config = getApiKeyConfig(apiKeyType); - const { keyName, envDir } = config; - const envPath = join(homedir(), envDir, '.env'); - - let envContent = ''; - if (existsSync(envPath)) { - envContent = readFileSync(envPath, 'utf-8'); - // Remove existing key if present - envContent = envContent.split('\n') - .filter(line => !line.startsWith(`${keyName}=`)) - .join('\n'); - if (envContent && !envContent.endsWith('\n')) { - envContent += '\n'; - } - } - - envContent += `${keyName}=${apiKey}\n`; - - // Ensure directory exists - const { mkdirSync, writeFileSync } = await import('fs'); - mkdirSync(join(homedir(), envDir), { recursive: true }); - writeFileSync(envPath, envContent, { mode: 0o600 }); // Secure permissions -} \ No newline at end of file diff --git a/src/utils/config-loader.ts b/src/utils/config-loader.ts deleted file mode 100644 index 64d7089..0000000 --- a/src/utils/config-loader.ts +++ /dev/null @@ -1,29 +0,0 @@ -// Configuration loading utilities - -import * as path from 'path'; -import * as fs from 'fs'; - -export interface HookConfig { - [key: string]: string[] | { [pattern: string]: string[] }; -} - -export async function loadHookConfig(): Promise { - let configPath = path.join(process.cwd(), '.claude', 'hooks', 'config.cjs'); - - if (!fs.existsSync(configPath)) { - configPath = path.join(process.cwd(), '.claude', 'hooks', 'config.js'); - if (!fs.existsSync(configPath)) { - return null; - } - } - - const configUrl = new URL(`file://${configPath}`).href; - const config = await import(`${configUrl}?t=${Date.now()}`).then(m => m.default || m); - - return config; -} - -export function getEventConfig(config: HookConfig, eventType: string): any { - const configKey = eventType ? eventType.charAt(0).toLowerCase() + eventType.slice(1) : ''; - return config[configKey]; -} \ No newline at end of file diff --git a/src/utils/firstRun.ts b/src/utils/firstRun.ts deleted file mode 100644 index 26e7dad..0000000 --- a/src/utils/firstRun.ts +++ /dev/null @@ -1,40 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; -import chalk from 'chalk'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = dirname(__filename); - -const FIRST_RUN_FILE = path.join(process.env.HOME || '', '.claude-hooks-initialized'); - -export async function isFirstRun(): Promise { - try { - await fs.access(FIRST_RUN_FILE); - return false; - } catch { - return true; - } -} - -export async function markFirstRunComplete(): Promise { - await fs.writeFile(FIRST_RUN_FILE, new Date().toISOString()); -} - -export async function showWelcomeMessage(): Promise { - console.log(); - console.log(chalk.blue('โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—')); - console.log(chalk.blue('โ•‘') + chalk.bold.white(' Welcome to Claude Hooks CLI! ๐ŸŽ‰') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + chalk.gray(' Manage validation and quality checks for Claude Code') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + chalk.white(' Quick start:') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + chalk.green(' โ€ข claude-hooks') + chalk.gray(' - Open the interactive manager') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + chalk.green(' โ€ข claude-hooks init') + chalk.gray(' - Set up hooks for your project') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + chalk.green(' โ€ข claude-hooks list') + chalk.gray(' - See all available hooks') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•‘') + chalk.gray(' Updates are checked automatically') + ' ' + chalk.blue('โ•‘')); - console.log(chalk.blue('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•')); - console.log(); -} \ No newline at end of file diff --git a/src/utils/hook-executor.ts b/src/utils/hook-executor.ts deleted file mode 100644 index 8374524..0000000 --- a/src/utils/hook-executor.ts +++ /dev/null @@ -1,98 +0,0 @@ -// Hook execution utilities - -import { spawn } from 'child_process'; -import * as path from 'path'; -import * as fs from 'fs'; -import { HookInput, HookOutput } from '../types/hook-types.js'; - -export function logHookEvent(message: string): void { - console.error(`[UNIVERSAL-HOOK] ${message}`); -} - -export function handleHookOutput(stdout: string, hookName: string): void { - if (!stdout.trim()) return; - - try { - const output: HookOutput = JSON.parse(stdout.trim()); - logHookEvent(`Hook output: ${JSON.stringify(output)}`); - - if (output.continue === false) { - if (output.stopReason) { - console.error(`Hook stopped execution: ${output.stopReason}`); - } - logHookEvent('Hook blocking execution - exiting with code 2'); - process.exit(2); - } - - if (output.decision === 'block' && output.reason) { - console.error(output.reason); - logHookEvent('Hook blocking execution - exiting with code 2'); - process.exit(2); - } - - if (!output.suppressOutput) { - console.log(stdout); - } - } catch (e) { - console.log(stdout); - logHookEvent('Hook output not JSON, treating as regular output'); - } -} - -export function handleHookExit(code: number | null, hookName: string): void { - if (code === 0) { - return; - } else if (code === 2) { - logHookEvent('Hook returned exit code 2 - blocking execution'); - process.exit(2); - } else { - logHookEvent(`Hook failed with exit code ${code}`); - throw new Error(`Hook ${hookName} exited with code ${code}`); - } -} - -export async function executeHook(hookName: string, input: HookInput): Promise { - return new Promise((resolve, reject) => { - const hookPath = path.join(process.cwd(), 'hooks', `${hookName}.sh`); - - logHookEvent(`Executing hook: ${hookName}`); - logHookEvent(`Direct path: ${hookPath}`); - - if (!fs.existsSync(hookPath)) { - logHookEvent(`Hook file not found: ${hookPath}`); - resolve(); - return; - } - - const child = spawn('bash', [hookPath], { - stdio: ['pipe', 'pipe', 'inherit'] - }); - - let stdout = ''; - - child.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - child.stdin.write(JSON.stringify(input)); - child.stdin.end(); - - child.on('close', (code) => { - logHookEvent(`Hook '${hookName}' completed with exit code: ${code}`); - - handleHookOutput(stdout, hookName); - - try { - handleHookExit(code, hookName); - resolve(); - } catch (error) { - reject(error); - } - }); - - child.on('error', (err) => { - logHookEvent(`Hook execution error: ${err.message}`); - reject(err); - }); - }); -} \ No newline at end of file diff --git a/src/utils/hook-logger.ts b/src/utils/hook-logger.ts deleted file mode 100644 index 4ca5f1c..0000000 --- a/src/utils/hook-logger.ts +++ /dev/null @@ -1,21 +0,0 @@ -// Hook logging utilities - -import * as fs from 'fs'; -import * as path from 'path'; - -export function logToFile(message: string): void { - const logFile = path.join(process.cwd(), '.claude', 'hooks', 'execution.log'); - const timestamp = new Date().toISOString(); - - try { - fs.appendFileSync(logFile, `[${timestamp}] ${message}\n`); - } catch (e) { - // Ignore logging errors - } -} - -export function debugLog(message: string): void { - if (process.env.HOOK_DEBUG) { - console.error(`[HOOK DEBUG] ${message}`); - } -} \ No newline at end of file diff --git a/src/utils/pattern-matcher.ts b/src/utils/pattern-matcher.ts deleted file mode 100644 index cab20ab..0000000 --- a/src/utils/pattern-matcher.ts +++ /dev/null @@ -1,63 +0,0 @@ -// Pattern matching utilities for hooks - -import { HookInput } from '../types/hook-types.js'; - -export function matchToolPatterns( - eventConfig: any, - input: HookInput, - eventType: string -): string[] { - const hooks: string[] = []; - - if (!('tool_name' in input)) return hooks; - - const toolConfig = eventConfig[input.tool_name]; - if (!toolConfig) return hooks; - - if (Array.isArray(toolConfig)) { - return toolConfig; - } - - const isFileOperation = ['Write', 'Edit', 'MultiEdit'].includes(input.tool_name); - const targetValue = isFileOperation - ? input.tool_input?.file_path || '' - : input.tool_input?.command || ''; - - for (const [pattern, hookList] of Object.entries(toolConfig)) { - if (new RegExp(pattern).test(targetValue)) { - hooks.push(...(hookList as string[])); - } - } - - return hooks; -} - -export function matchFilePatterns(eventConfig: any, filePath: string): string[] { - const hooks: string[] = []; - - for (const [pattern, hookList] of Object.entries(eventConfig)) { - if (new RegExp(pattern).test(filePath)) { - hooks.push(...(hookList as string[])); - } - } - - return hooks; -} - -export function collectHooks( - eventConfig: any, - input: HookInput, - eventType: string -): string[] { - if (Array.isArray(eventConfig)) { - return eventConfig; - } - - const hooks: string[] = []; - - if ((eventType === 'PreToolUse' || eventType === 'PostToolUse')) { - hooks.push(...matchToolPatterns(eventConfig, input, eventType)); - } - - return hooks; -} \ No newline at end of file diff --git a/src/utils/stdin-reader.ts b/src/utils/stdin-reader.ts deleted file mode 100644 index b1caaa1..0000000 --- a/src/utils/stdin-reader.ts +++ /dev/null @@ -1,11 +0,0 @@ -// Stdin reading utility - -export async function readStdin(): Promise { - const chunks: Buffer[] = []; - - return new Promise((resolve, reject) => { - process.stdin.on('data', (chunk) => chunks.push(chunk)); - process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString())); - process.stdin.on('error', reject); - }); -} \ No newline at end of file diff --git a/src/utils/updateChecker.ts b/src/utils/updateChecker.ts deleted file mode 100644 index 07ed9b7..0000000 --- a/src/utils/updateChecker.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { exec } from 'child_process'; -import { promisify } from 'util'; -import chalk from 'chalk'; - -const execAsync = promisify(exec); - -interface UpdateCheckResult { - updateAvailable: boolean; - currentVersion: string; - latestVersion: string; -} - -export async function checkForUpdates(currentVersion: string, packageName: string): Promise { - try { - // Get latest version from npm with a 2 second timeout - const { stdout } = await execAsync(`npm view ${packageName} version`, { - encoding: 'utf8', - timeout: 2000 - }); - - const latestVersion = stdout.trim(); - const updateAvailable = compareVersions(currentVersion, latestVersion) < 0; - - return { - updateAvailable, - currentVersion, - latestVersion - }; - } catch (error) { - // Silently fail - don't interrupt user experience - return null; - } -} - -function compareVersions(v1: string, v2: string): number { - const parts1 = v1.split('.').map(Number); - const parts2 = v2.split('.').map(Number); - - for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) { - const part1 = parts1[i] || 0; - const part2 = parts2[i] || 0; - - if (part1 < part2) return -1; - if (part1 > part2) return 1; - } - - return 0; -} - -export function displayUpdateNotification(result: UpdateCheckResult): void { - if (!result.updateAvailable) return; - - console.log(); - console.log(chalk.yellow('โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—')); - console.log(chalk.yellow('โ•‘') + chalk.bold.white(' Update available! ') + chalk.gray(`${result.currentVersion} โ†’ `) + chalk.green(result.latestVersion) + ' ' + chalk.yellow('โ•‘')); - console.log(chalk.yellow('โ•‘') + chalk.gray(' Run ') + chalk.cyan('npm update -g claude-code-hooks-cli') + chalk.gray(' to update') + ' ' + chalk.yellow('โ•‘')); - console.log(chalk.yellow('โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•')); - console.log(); -} \ No newline at end of file diff --git a/src/validation/CLAUDE.md b/src/validation/CLAUDE.md deleted file mode 100644 index 003e112..0000000 --- a/src/validation/CLAUDE.md +++ /dev/null @@ -1,30 +0,0 @@ -# validation - CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this directory. - -## Overview -Code module for the project - -## Directory Structure -``` - -``` - -## Key Components -- `settings-validator.ts` - SettingsValidator -- `types.ts` - VALID_EVENTS -- `hook-validator.ts` - HookValidator - -## Commands -- No package.json found - -## Dependencies -- No package.json found - -## Architecture Notes -- TypeScript/JavaScript module -- Individual file exports -- Utility functions - ---- -_Auto-generated by Claude Context Updater on 2025-07-07_ diff --git a/src/validation/hook-template-validator.ts b/src/validation/hook-template-validator.ts deleted file mode 100644 index 0972c5e..0000000 --- a/src/validation/hook-template-validator.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { HookTemplates, HookTemplate } from '../types.js'; - -export interface TemplateValidationResult { - valid: boolean; - errors: string[]; - warnings: string[]; -} - -export class HookTemplateValidator { - private validEvents = ['PreToolUse', 'PostToolUse', 'Stop']; - - validateTemplates(templates: unknown): TemplateValidationResult { - const result: TemplateValidationResult = { - valid: true, - errors: [], - warnings: [] - }; - - // Check if templates is an object - if (typeof templates !== 'object' || templates === null || Array.isArray(templates)) { - result.valid = false; - result.errors.push('Hook templates must be an object'); - return result; - } - - const templatesObj = templates as Record; - - // Validate each template - Object.entries(templatesObj).forEach(([name, template]) => { - this.validateTemplate(name, template, result); - }); - - return result; - } - - private validateTemplate(name: string, template: unknown, result: TemplateValidationResult): void { - // Check if template is an object - if (typeof template !== 'object' || template === null || Array.isArray(template)) { - result.valid = false; - result.errors.push(`Template '${name}' must be an object`); - return; - } - - const templateObj = template as Record; - - // Check required fields - if (!templateObj.event) { - result.valid = false; - result.errors.push(`Template '${name}' missing required field 'event'`); - } else if (!this.validEvents.includes(templateObj.event as string)) { - result.valid = false; - result.errors.push(`Template '${name}' has invalid event '${templateObj.event}'. Valid events: ${this.validEvents.join(', ')}`); - } - - if (!templateObj.description) { - result.valid = false; - result.errors.push(`Template '${name}' missing required field 'description'`); - } - - // Check optional fields - if (templateObj.matcher && typeof templateObj.matcher !== 'string') { - result.valid = false; - result.errors.push(`Template '${name}' field 'matcher' must be a string`); - } - - if (templateObj.pattern && typeof templateObj.pattern !== 'string') { - result.valid = false; - result.errors.push(`Template '${name}' field 'pattern' must be a string`); - } - - if (templateObj.command && typeof templateObj.command !== 'string') { - result.valid = false; - result.errors.push(`Template '${name}' field 'command' must be a string`); - } - - // Validate regex pattern if provided - if (templateObj.pattern && typeof templateObj.pattern === 'string') { - try { - new RegExp(templateObj.pattern); - } catch (e) { - result.valid = false; - result.errors.push(`Template '${name}' has invalid regex pattern: ${e}`); - } - } - - // Warnings - if (!templateObj.command) { - result.warnings.push(`Template '${name}' has no custom command - will use default exec pattern`); - } - - if (name.length > 50) { - result.warnings.push(`Template '${name}' has a very long name (${name.length} chars) - consider shortening`); - } - } -} \ No newline at end of file diff --git a/src/validation/hook-validator.ts b/src/validation/hook-validator.ts deleted file mode 100644 index 09ad29d..0000000 --- a/src/validation/hook-validator.ts +++ /dev/null @@ -1,114 +0,0 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { ValidationResult, ValidationError } from './types.js'; -import { SettingsValidator } from './settings-validator.js'; - -export class HookValidator { - private settingsValidator: SettingsValidator; - - constructor() { - this.settingsValidator = new SettingsValidator(); - } - - /** - * Validate a settings file - */ - async validateSettingsFile(filePath: string): Promise { - try { - // Read the file - const content = await fs.readFile(filePath, 'utf-8'); - - // Parse JSON - let settings: any; - try { - settings = JSON.parse(content); - } catch (e: any) { - return { - valid: false, - errors: [{ - path: '', - message: `Invalid JSON: ${e.message}`, - severity: 'error' - }], - warnings: [], - fixable: 0 - }; - } - - // Validate settings structure - const result = this.settingsValidator.validate(settings); - - // Add file context to errors - result.errors = result.errors.map(error => ({ - ...error, - message: `${path.basename(filePath)}: ${error.message}` - })); - - result.warnings = result.warnings.map(warning => ({ - ...warning, - message: `${path.basename(filePath)}: ${warning.message}` - })); - - return result; - } catch (e: any) { - return { - valid: false, - errors: [{ - path: '', - message: `Failed to read file: ${e.message}`, - severity: 'error' - }], - warnings: [], - fixable: 0 - }; - } - } - - /** - * Validate settings object (for runtime validation) - */ - validateSettings(settings: any): ValidationResult { - return this.settingsValidator.validate(settings); - } - - /** - * Format validation results for display - */ - formatResults(result: ValidationResult, verbose = false): string { - const lines: string[] = []; - - if (result.valid) { - const hookCount = result.hookCount ?? 0; - const hookText = hookCount === 1 ? 'hook' : 'hooks'; - lines.push(`โœ… Valid (${hookCount} ${hookText})`); - } else { - lines.push('โŒ Invalid'); - } - - if (result.errors.length > 0) { - lines.push('\nErrors:'); - result.errors.forEach(error => { - lines.push(` - ${error.path ? `[${error.path}] ` : ''}${error.message}`); - if (error.suggestion && verbose) { - lines.push(` ๐Ÿ’ก ${error.suggestion}`); - } - }); - } - - if (result.warnings.length > 0) { - lines.push('\nWarnings:'); - result.warnings.forEach(warning => { - lines.push(` - ${warning.path ? `[${warning.path}] ` : ''}${warning.message}`); - if (warning.suggestion && verbose) { - lines.push(` ๐Ÿ’ก ${warning.suggestion}`); - } - }); - } - - if (result.fixable > 0) { - lines.push(`\n${result.fixable} issue(s) can be automatically fixed with --fix`); - } - - return lines.join('\n'); - } -} \ No newline at end of file diff --git a/src/validation/index.ts b/src/validation/index.ts deleted file mode 100644 index a9b3d3f..0000000 --- a/src/validation/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './types.js'; -export * from './hook-validator.js'; -export * from './settings-validator.js'; \ No newline at end of file diff --git a/src/validation/settings-validator.ts b/src/validation/settings-validator.ts deleted file mode 100644 index 158888d..0000000 --- a/src/validation/settings-validator.ts +++ /dev/null @@ -1,242 +0,0 @@ -import { - ValidationError, - ValidationResult, - HookSettingsValidation, - VALID_EVENTS, - VALID_TOOLS, - VALID_LOG_LEVELS -} from './types.js'; - -export class SettingsValidator { - private errors: ValidationError[] = []; - private warnings: ValidationError[] = []; - private hookCount: number = 0; - - validate(settings: any): ValidationResult { - this.errors = []; - this.warnings = []; - this.hookCount = 0; - - // Validate root structure - if (!settings || typeof settings !== 'object') { - this.addError('', 'Settings must be a valid JSON object'); - return this.getResult(); - } - - // Validate hooks section (optional - valid to have no hooks) - if (settings.hooks !== undefined) { - if (typeof settings.hooks !== 'object') { - this.addError('hooks', 'Field "hooks" must be an object'); - } else { - this.validateHooksSection(settings.hooks); - } - } - - // Validate logging section if present - if (settings.logging) { - this.validateLoggingSection(settings.logging); - } - - return this.getResult(); - } - - private validateHooksSection(hooks: any): void { - for (const [event, groups] of Object.entries(hooks)) { - // Validate event name - if (!VALID_EVENTS.includes(event as any)) { - this.addError( - `hooks.${event}`, - `Invalid event name "${event}"`, - `Valid events are: ${VALID_EVENTS.join(', ')}` - ); - continue; - } - - // Validate groups array - if (!Array.isArray(groups)) { - this.addError( - `hooks.${event}`, - 'Event configuration must be an array of hook groups' - ); - continue; - } - - // Validate each group - groups.forEach((group, index) => { - this.validateHookGroup(group, `hooks.${event}[${index}]`); - }); - } - } - - private validateHookGroup(group: any, path: string): void { - if (!group || typeof group !== 'object') { - this.addError(path, 'Hook group must be an object'); - return; - } - - // Validate matcher - if (group.matcher !== undefined) { - if (typeof group.matcher !== 'string') { - this.addError(`${path}.matcher`, 'Matcher must be a string'); - } else if (group.matcher.trim() === '') { - this.addError(`${path}.matcher`, 'Matcher cannot be empty'); - } else { - // Validate tool names in matcher - const tools = group.matcher.split('|'); - for (const tool of tools) { - if (!VALID_TOOLS.includes(tool.trim() as any)) { - this.addWarning( - `${path}.matcher`, - `Unknown tool "${tool.trim()}" in matcher`, - `Known tools: ${VALID_TOOLS.join(', ')}` - ); - } - } - } - } - - // Validate pattern - if (group.pattern !== undefined) { - if (typeof group.pattern !== 'string') { - this.addError(`${path}.pattern`, 'Pattern must be a string'); - } else if (group.pattern.trim() === '') { - this.addError(`${path}.pattern`, 'Pattern cannot be empty'); - } else { - // Validate regex pattern - try { - new RegExp(group.pattern); - } catch (e: any) { - this.addError( - `${path}.pattern`, - `Invalid regex pattern: ${e.message}` - ); - } - } - } - - // Validate hooks array - if (!group.hooks) { - this.addError(`${path}.hooks`, 'Missing required "hooks" array'); - } else if (!Array.isArray(group.hooks)) { - this.addError(`${path}.hooks`, 'Hooks must be an array'); - } else { - group.hooks.forEach((hook: any, index: number) => { - this.validateHook(hook, `${path}.hooks[${index}]`); - }); - } - } - - private validateHook(hook: any, path: string): void { - if (!hook || typeof hook !== 'object') { - this.addError(path, 'Hook must be an object'); - return; - } - - // Validate type - if (!hook.type) { - this.addError(`${path}.type`, 'Missing required "type" field'); - } else if (hook.type !== 'command') { - this.addError( - `${path}.type`, - `Invalid hook type "${hook.type}"`, - 'Currently only "command" type is supported' - ); - } - - // Validate command - if (!hook.command) { - this.addError(`${path}.command`, 'Missing required "command" field'); - } else if (typeof hook.command !== 'string') { - this.addError(`${path}.command`, 'Command must be a string'); - } else if (hook.command.trim() === '') { - this.addError(`${path}.command`, 'Command cannot be empty'); - } else { - // Only count valid hooks - this.hookCount++; - } - } - - private validateLoggingSection(logging: any): void { - if (typeof logging !== 'object') { - this.addError('logging', 'Logging configuration must be an object'); - return; - } - - // Validate enabled - if (logging.enabled !== undefined && typeof logging.enabled !== 'boolean') { - this.addError('logging.enabled', 'Enabled must be a boolean'); - } - - // Validate level - if (logging.level !== undefined) { - if (typeof logging.level !== 'string') { - this.addError('logging.level', 'Level must be a string'); - } else if (!VALID_LOG_LEVELS.includes(logging.level as any)) { - this.addError( - 'logging.level', - `Invalid log level "${logging.level}"`, - `Valid levels are: ${VALID_LOG_LEVELS.join(', ')}` - ); - } - } - - // Validate path - if (logging.path !== undefined && typeof logging.path !== 'string') { - this.addError('logging.path', 'Path must be a string'); - } - - // Validate maxSize - if (logging.maxSize !== undefined) { - if (typeof logging.maxSize !== 'string') { - this.addError('logging.maxSize', 'MaxSize must be a string'); - } else if (!/^\d+[KMG]?B?$/i.test(logging.maxSize)) { - this.addError( - 'logging.maxSize', - 'Invalid maxSize format', - 'Use format like "10MB", "1GB", or "512KB"' - ); - } - } - - // Validate retention - if (logging.retention !== undefined) { - if (typeof logging.retention !== 'string') { - this.addError('logging.retention', 'Retention must be a string'); - } else if (!/^\d+d$/i.test(logging.retention)) { - this.addError( - 'logging.retention', - 'Invalid retention format', - 'Use format like "7d", "30d"' - ); - } - } - } - - private addError(path: string, message: string, suggestion?: string): void { - this.errors.push({ - path, - message, - severity: 'error', - suggestion - }); - } - - private addWarning(path: string, message: string, suggestion?: string): void { - this.warnings.push({ - path, - message, - severity: 'warning', - suggestion - }); - } - - private getResult(): ValidationResult { - return { - valid: this.errors.length === 0, - errors: this.errors, - warnings: this.warnings, - fixable: 0, // Will be implemented later - hookCount: this.hookCount - }; - } -} \ No newline at end of file diff --git a/src/validation/types.ts b/src/validation/types.ts deleted file mode 100644 index 8f936eb..0000000 --- a/src/validation/types.ts +++ /dev/null @@ -1,62 +0,0 @@ -export interface ValidationError { - path: string; - message: string; - severity: 'error' | 'warning'; - suggestion?: string; -} - -export interface ValidationResult { - valid: boolean; - errors: ValidationError[]; - warnings: ValidationError[]; - fixable: number; - hookCount?: number; -} - -export interface HookGroupValidation { - matcher?: string; - pattern?: string; - hooks: Array<{ - type: string; - command: string; - }>; -} - -export interface HookSettingsValidation { - _comment?: string; - hooks: { - [event: string]: HookGroupValidation[]; - }; - logging?: { - enabled?: boolean; - level?: string; - path?: string; - maxSize?: string; - retention?: string; - }; -} - -export const VALID_EVENTS = ['PreToolUse', 'PostToolUse', 'Stop', 'PreWrite', 'PostWrite'] as const; -export type ValidEvent = typeof VALID_EVENTS[number]; - -export const VALID_TOOLS = [ - 'Bash', - 'Write', - 'Edit', - 'MultiEdit', - 'TodoWrite', - 'Read', - 'Grep', - 'Glob', - 'LS', - 'NotebookRead', - 'NotebookEdit', - 'WebFetch', - 'TodoRead', - 'WebSearch', - 'Task', - 'exit_plan_mode', - '*' -] as const; - -export const VALID_LOG_LEVELS = ['debug', 'info', 'warn', 'error'] as const; \ No newline at end of file diff --git a/tests/all-hooks-demo.sh b/tests/all-hooks-demo.sh deleted file mode 100755 index 1e144a0..0000000 --- a/tests/all-hooks-demo.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/bin/bash -# Demonstration of all hooks using continue: false - -echo "=== Demonstration: All Hooks with continue: false ===" -echo - -# Test each hook's JSON output -echo "1. code-quality-validator.sh" -echo "----------------------------" -# Simulate a quality violation -echo '{"tool":"Write","tool_input":{"file_path":"/tmp/bad.js"},"exit_code":0}' | ./hooks/code-quality-validator.sh 2>&1 | grep -A5 -B5 '"continue"' || echo "Would output JSON on violations" -echo - -echo "2. lint-check.sh" -echo "----------------" -echo "When lint fails, outputs:" -cat << 'EOF' -{ - "continue": false, - "stopReason": "Code has linting errors - cannot proceed", - "decision": "block", - "reason": "Linting errors found. Please fix these issues:\n\nโ€ข eslint errors...\n\nTo fix: npm run lint:fix" -} -EOF -echo - -echo "3. test-check.sh" -echo "----------------" -echo "When tests fail, outputs:" -cat << 'EOF' -{ - "continue": false, - "stopReason": "Tests are failing - cannot proceed", - "decision": "block", - "reason": "3 tests are failing. Please fix these test failures:\n\nโ€ข test1.spec.js\nโ€ข test2.spec.js\n\nRun 'npm test' to see full output." -} -EOF -echo - -echo "4. typescript-check.sh" -echo "---------------------" -echo "When TypeScript errors exist, outputs:" -cat << 'EOF' -{ - "continue": false, - "stopReason": "TypeScript compilation errors - cannot proceed", - "decision": "block", - "reason": "Found 5 TypeScript errors. Please fix these type errors:\n\nโ€ข src/index.ts:42 - Type 'string' not assignable to 'number'\n\nRun 'npm run typecheck' to see all errors." -} -EOF -echo - -echo "5. doc-compliance.sh (Stop event)" -echo "---------------------------------" -echo "When documentation standards not met:" -cat << 'EOF' -{ - "continue": false, - "stopReason": "Documentation standards not met (score: 0.6/0.8)", - "decision": "block", - "reason": "Documentation compliance failed. Issues to fix:\nโ€ข Missing function documentation\nโ€ข Update README" -} -EOF -echo - -echo "6. check-package-age.sh" -echo "----------------------" -echo '{"tool_name":"Bash","tool_input":{"command":"npm install left-pad@1.0.0"}}' | ./hooks/check-package-age.sh 2>&1 | grep -A5 '"continue"' || echo "Would block old packages" -echo - -echo "=== How It Works ===" -echo "1. Hook detects issue (lint error, test failure, etc.)" -echo "2. Outputs JSON with continue: false to stdout" -echo "3. Universal-hook captures and parses JSON" -echo "4. Sees continue: false and exits with code 2" -echo "5. Claude Code sees exit 2 and blocks operation" -echo "6. User sees stopReason message" -echo "7. Claude sees reason with fix instructions" -echo -echo "โœ… All hooks now support checkpoint workflows!" \ No newline at end of file diff --git a/tests/debug-test-blocker.sh b/tests/debug-test-blocker.sh deleted file mode 100755 index d71da44..0000000 --- a/tests/debug-test-blocker.sh +++ /dev/null @@ -1,90 +0,0 @@ -#!/bin/bash -# Debug why test-blocker doesn't exit with code 2 - -echo "=== Debugging test-blocker hook ===" -echo - -# Test input -INPUT='{ - "session_id": "test", - "transcript_path": "/tmp/test.jsonl", - "hook_event_name": "PreWrite", - "file_path": "test.block-test.txt" -}' - -echo "1. Direct hook test (should output JSON with exit 0)" -echo "---------------------------------------------------" -echo "$INPUT" | ./hooks/test-blocker.sh -echo "Exit code: $?" -echo - -echo "2. Universal hook test (should exit with code 2)" -echo "------------------------------------------------" -# Run with debug to see what's happening -echo "$INPUT" | HOOK_DEBUG=1 node lib/commands/universal-hook.js 2>&1 -echo "Exit code: $?" -echo - -echo "3. Test the specific path through universal-hook" -echo "-----------------------------------------------" -# Create a test that shows the exact flow -cat > /tmp/test-flow.js << 'EOF' -import { spawn } from 'child_process'; - -const input = { - session_id: "test", - transcript_path: "/tmp/test.jsonl", - hook_event_name: "PreWrite", - file_path: "test.block-test.txt" -}; - -console.log("Checking config for PreWrite hooks..."); - -// Check what hooks would run -const configKey = 'preWrite'; -console.log(`Config key: ${configKey}`); - -// Would load config and check patterns -console.log("Would match pattern: \\.block-test\\.(txt|md)$"); -console.log("Would execute: test-blocker"); - -// Test the hook execution -const child = spawn('npx', ['claude-code-hooks-cli', 'exec', 'test-blocker'], { - stdio: ['pipe', 'pipe', 'pipe'] -}); - -let stdout = ''; -child.stdout.on('data', (data) => { - stdout += data.toString(); -}); - -child.stderr.on('data', (data) => { - console.error('Stderr:', data.toString()); -}); - -child.stdin.write(JSON.stringify(input)); -child.stdin.end(); - -child.on('close', (code) => { - console.log('Hook exit code:', code); - console.log('Stdout length:', stdout.length); - - try { - const output = JSON.parse(stdout.trim()); - console.log('Parsed output:', output); - - if (output.continue === false) { - console.log('SHOULD EXIT WITH CODE 2'); - process.exit(2); - } - } catch (e) { - console.log('Parse error:', e.message); - } -}); -EOF - -node /tmp/test-flow.js -echo "Exit code: $?" - -# Clean up -rm -f /tmp/test-flow.js \ No newline at end of file diff --git a/tests/final-continue-test.sh b/tests/final-continue-test.sh deleted file mode 100755 index e4e726a..0000000 --- a/tests/final-continue-test.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/bin/bash -# Final test to prove continue: false works - -echo "=== Final continue: false Verification ===" -echo - -# 1. Direct test -echo "1. Testing test-blocker hook directly:" -echo '{"hook_event_name":"PreWrite","file_path":"test.block-test.txt"}' | ./hooks/test-blocker.sh -echo - -# 2. Through universal-hook -echo "2. Testing through universal-hook:" -echo '{"hook_event_name":"PreWrite","file_path":"test.block-test.txt"}' | node lib/commands/universal-hook.js 2>&1 -EXIT_CODE=$? -echo "Exit code: $EXIT_CODE" - -if [ $EXIT_CODE -eq 2 ]; then - echo "โœ… SUCCESS: Universal-hook correctly exited with code 2" -else - echo "โŒ FAIL: Expected exit code 2, got $EXIT_CODE" -fi - -echo -echo "3. Testing Stop event hook:" - -# Create a simple Stop hook -cat > /tmp/test-stop-hook.sh << 'EOF' -#!/bin/bash -INPUT=$(cat) -EVENT=$(echo "$INPUT" | jq -r '.hook_event_name') - -if [ "$EVENT" = "Stop" ]; then - echo "Stop hook activated" >&2 - cat < /dev/null 2>&1 - -echo "Scenario: Claude tries to write test.block-test.txt" -echo "Expected: Hook should block with exit code 2" -echo - -# Create the input that Claude would send -CLAUDE_INPUT='{ - "session_id": "claude-session-123", - "transcript_path": "/Users/danseider/.claude/sessions/current.jsonl", - "hook_event_name": "PreWrite", - "file_path": "test.block-test.txt", - "content": "This is content Claude wants to write" -}' - -echo "1. Claude sends PreWrite event to universal-hook" -echo "-------------------------------------------" -echo "$CLAUDE_INPUT" | node lib/commands/universal-hook.js 2>&1 - -EXIT_CODE=$? -echo -echo "Exit code: $EXIT_CODE" - -if [ $EXIT_CODE -eq 2 ]; then - echo "โœ… SUCCESS: Hook blocked execution (exit 2)" - echo "๐Ÿ“‹ Claude would see: Hook stopped execution message" - echo "๐Ÿšซ File would NOT be written" -else - echo "โŒ FAILURE: Hook did not block (exit $EXIT_CODE)" - echo "๐Ÿ“ File would be written" -fi - -echo -echo "2. What happens with our quality hooks" -echo "--------------------------------------" - -# Test TypeScript check -TS_INPUT='{ - "session_id": "claude-session-456", - "transcript_path": "/Users/danseider/.claude/sessions/current.jsonl", - "hook_event_name": "PreToolUse", - "tool_name": "Bash", - "tool_input": { - "command": "git commit -m \"test commit\"" - } -}' - -echo "Scenario: Claude runs 'git commit' (would trigger typescript-check)" -echo - -# Create a fake TypeScript error scenario -echo "export const badCode: any = 'using any type';" > /tmp/test-bad.ts -cd /tmp -echo '{"compilerOptions":{"strict":true,"noImplicitAny":true}}' > tsconfig.json - -# This would normally run but we'll simulate the output -echo "TypeScript check would run and find errors..." -echo "Hook would output JSON with continue: false" -echo "Claude would be blocked from committing" - -# Clean up -rm -f /tmp/test-bad.ts /tmp/tsconfig.json -cd - > /dev/null - -echo -echo "3. Summary of how it works:" -echo "---------------------------" -echo "โœ… Hooks output JSON with continue: false" -echo "โœ… Universal-hook parses JSON and exits with code 2" -echo "โœ… Claude Code sees exit code 2 and blocks the operation" -echo "โœ… User sees stopReason message" -echo "โœ… Claude sees reason message with instructions" - -echo -echo "The key is that Claude Code must have these hooks configured in:" -echo "- ~/.claude/settings.json (global)" -echo "- .claude/settings.json (project)" -echo "- Or enterprise settings" \ No newline at end of file diff --git a/tests/stop-validation.test.sh b/tests/stop-validation.test.sh deleted file mode 100755 index 996c55b..0000000 --- a/tests/stop-validation.test.sh +++ /dev/null @@ -1,497 +0,0 @@ -#!/bin/bash - -# Test suite for stop-validation.sh hook -# Tests various scenarios including single projects, monorepos, and error conditions - -set -uo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Test counters -TOTAL_TESTS=0 -PASSED_TESTS=0 -FAILED_TESTS=0 - -# Hook under test -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -HOOK_PATH="$SCRIPT_DIR/../hooks/stop-validation.sh" - -# Simple assertion functions -assert_equals() { - local expected="$1" - local actual="$2" - local message="${3:-Assertion failed}" - - if [ "$expected" = "$actual" ]; then - echo -e " ${GREEN}โœ“ $message${NC}" - return 0 - else - echo -e " ${RED}โœ— $message (expected: $expected, got: $actual)${NC}" - return 1 - fi -} - -assert_not_equals() { - local not_expected="$1" - local actual="$2" - local message="${3:-Assertion failed}" - - if [ "$not_expected" != "$actual" ]; then - echo -e " ${GREEN}โœ“ $message${NC}" - return 0 - else - echo -e " ${RED}โœ— $message (should not be: $not_expected)${NC}" - return 1 - fi -} - -assert_contains() { - local haystack="$1" - local needle="$2" - local message="${3:-Should contain pattern}" - - if echo "$haystack" | grep -q "$needle"; then - echo -e " ${GREEN}โœ“ $message${NC}" - return 0 - else - echo -e " ${RED}โœ— $message (pattern not found: $needle)${NC}" - return 1 - fi -} - -assert_not_contains() { - local haystack="$1" - local needle="$2" - local message="${3:-Should not contain pattern}" - - if ! echo "$haystack" | grep -q "$needle"; then - echo -e " ${GREEN}โœ“ $message${NC}" - return 0 - else - echo -e " ${RED}โœ— $message (pattern found: $needle)${NC}" - return 1 - fi -} - -assert_file_exists() { - local file="$1" - local message="${2:-File should exist}" - - if [ -f "$file" ]; then - echo -e " ${GREEN}โœ“ $message${NC}" - return 0 - else - echo -e " ${RED}โœ— $message (file not found: $file)${NC}" - return 1 - fi -} - -assert_executable() { - local file="$1" - local message="${2:-File should be executable}" - - if [ -x "$file" ]; then - echo -e " ${GREEN}โœ“ $message${NC}" - return 0 - else - echo -e " ${RED}โœ— $message (file not executable: $file)${NC}" - return 1 - fi -} - -assert_empty() { - local value="$1" - local message="${2:-Value should be empty}" - - if [ -z "$value" ]; then - echo -e " ${GREEN}โœ“ $message${NC}" - return 0 - else - echo -e " ${RED}โœ— $message (value not empty: $value)${NC}" - return 1 - fi -} - -# Run a test -run_test() { - local test_name="$1" - local test_function="$2" - - ((TOTAL_TESTS++)) - echo -e "\n${BLUE}Test $TOTAL_TESTS: $test_name${NC}" - - if $test_function; then - ((PASSED_TESTS++)) - else - ((FAILED_TESTS++)) - fi -} - -# Test: Hook exists and is executable -test_hook_exists() { - assert_file_exists "$HOOK_PATH" "stop-validation.sh should exist" - assert_executable "$HOOK_PATH" "stop-validation.sh should be executable" -} - -# Test: Hook handles stop_hook_active flag -test_stop_hook_active() { - # Disable logging for this test to avoid output - export CLAUDE_LOG_ENABLED=false - - local input='{"stop_hook_active": true}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - assert_equals 0 "$exit_code" "Should exit 0 when stop_hook_active is true" - # Don't check for empty output since logging might produce some - - # Re-enable logging - unset CLAUDE_LOG_ENABLED -} - -# Test: No TypeScript projects found -test_no_typescript_projects() { - # Create temp directory without TypeScript - local test_dir=$(mktemp -d) - cd "$test_dir" - - # Create a non-TypeScript project - echo '{"name": "test", "version": "1.0.0"}' > package.json - - local input='{}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - assert_equals 0 "$exit_code" "Should exit 0 when no TypeScript projects found" - - cd - > /dev/null - rm -rf "$test_dir" -} - -# Test: Single TypeScript project with no errors -test_single_project_success() { - local test_dir=$(mktemp -d) - cd "$test_dir" - - # Create TypeScript project - cat > package.json << 'EOF' -{ - "name": "test-project", - "scripts": { - "typecheck": "echo 'TypeScript check passed'", - "lint": "echo 'Lint check passed'" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - - touch tsconfig.json - - local input='{}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - assert_equals 0 "$exit_code" "Should exit 0 when all checks pass" - - cd - > /dev/null - rm -rf "$test_dir" -} - -# Test: Single TypeScript project with errors -test_single_project_with_errors() { - # Disable logging for cleaner test output - export CLAUDE_LOG_ENABLED=false - - local test_dir=$(mktemp -d) - cd "$test_dir" - - # Create TypeScript project with failing checks - cat > package.json << 'EOF' -{ - "name": "test-project", - "scripts": { - "typecheck": "echo 'error TS2307: Cannot find module' >&2 && exit 1", - "lint": "echo 'Lint errors found' >&2 && exit 1" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - - touch tsconfig.json - - local input='{}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - assert_equals 1 "$exit_code" "Should exit 1 when checks fail" - assert_contains "$output" '"decision": "block"' "Should output block decision" - assert_contains "$output" "TypeScript errors:" "Should mention TypeScript errors" - - cd - > /dev/null - rm -rf "$test_dir" - unset CLAUDE_LOG_ENABLED -} - -# Test: Monorepo with multiple packages -test_monorepo_detection() { - local test_dir=$(mktemp -d) - cd "$test_dir" - - # Create monorepo structure - mkdir -p apps/web apps/api packages/shared - - # Root package.json with workspaces - cat > package.json << 'EOF' -{ - "name": "monorepo", - "workspaces": ["apps/*", "packages/*"] -} -EOF - - # Web app - cat > apps/web/package.json << 'EOF' -{ - "name": "web", - "scripts": { - "typecheck": "echo 'Web typecheck passed'" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - touch apps/web/tsconfig.json - - # API app - cat > apps/api/package.json << 'EOF' -{ - "name": "api", - "scripts": { - "typecheck": "echo 'API typecheck passed'" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - touch apps/api/tsconfig.json - - # Shared package - cat > packages/shared/package.json << 'EOF' -{ - "name": "shared", - "scripts": { - "typecheck": "echo 'Shared typecheck passed'" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - touch packages/shared/tsconfig.json - - local input='{}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - assert_equals 0 "$exit_code" "Should exit 0 when all monorepo checks pass" - - cd - > /dev/null - rm -rf "$test_dir" -} - -# Test: Mixed success and failure in monorepo -test_monorepo_partial_failure() { - export CLAUDE_LOG_ENABLED=false - - local test_dir=$(mktemp -d) - cd "$test_dir" - - mkdir -p apps/web apps/api - - # Web app - success - cat > apps/web/package.json << 'EOF' -{ - "name": "web", - "scripts": { - "typecheck": "echo 'Web typecheck passed'" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - touch apps/web/tsconfig.json - - # API app - failure - cat > apps/api/package.json << 'EOF' -{ - "name": "api", - "scripts": { - "typecheck": "echo 'error TS2307: Cannot find module @solana/web3.js' >&2 && exit 1" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - touch apps/api/tsconfig.json - - local input='{}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - assert_equals 1 "$exit_code" "Should exit 1 when any check fails" - assert_contains "$output" '"decision": "block"' "Should output block decision" - assert_contains "$output" "api: 1 errors" "Should show which package failed" - - cd - > /dev/null - rm -rf "$test_dir" - unset CLAUDE_LOG_ENABLED -} - -# Test: TypeScript without custom script -test_typescript_no_custom_script() { - local test_dir=$(mktemp -d) - cd "$test_dir" - - # Create TypeScript project without typecheck script - cat > package.json << 'EOF' -{ - "name": "test-project", - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - - cat > tsconfig.json << 'EOF' -{ - "compilerOptions": { - "noEmit": true, - "strict": true - } -} -EOF - - # Create a simple TypeScript file - echo 'const x: string = "hello";' > test.ts - - # Install TypeScript locally for test - npm install --no-save typescript &>/dev/null || true - - local input='{}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - # Should attempt to use tsc directly - assert_equals 0 "$exit_code" "Should handle projects without custom scripts" - - cd - > /dev/null - rm -rf "$test_dir" -} - -# Test: JSON parsing of hook input -test_json_input_parsing() { - local test_dir=$(mktemp -d) - cd "$test_dir" - - # Malformed JSON should not crash - local output=$(echo "not json" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - # Hook should handle gracefully - assert_not_equals 2 "$exit_code" "Should not exit with code 2 on bad JSON" - - cd - > /dev/null - rm -rf "$test_dir" -} - -# Test: Excludes node_modules and build directories -test_excludes_build_directories() { - local test_dir=$(mktemp -d) - cd "$test_dir" - - # Create structure with node_modules - mkdir -p src node_modules/some-package dist - - # Main project - cat > package.json << 'EOF' -{ - "name": "main", - "scripts": { - "typecheck": "echo 'Main typecheck passed'" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - touch tsconfig.json - - # Should ignore this - cat > node_modules/some-package/package.json << 'EOF' -{ - "name": "ignored", - "scripts": { - "typecheck": "echo 'Should not run' && exit 1" - }, - "devDependencies": { - "typescript": "^5.0.0" - } -} -EOF - touch node_modules/some-package/tsconfig.json - - local input='{}' - local output=$(echo "$input" | "$HOOK_PATH" 2>&1) - local exit_code=$? - - assert_equals 0 "$exit_code" "Should ignore node_modules" - assert_not_contains "$output" "Should not run" "Should not run checks in node_modules" - - cd - > /dev/null - rm -rf "$test_dir" -} - -# Main test runner -echo "========================================" -echo "Stop Validation Hook Test Suite" -echo "========================================" - -# Run all tests -run_test "Hook exists and is executable" test_hook_exists -run_test "Handles stop_hook_active flag" test_stop_hook_active -run_test "No TypeScript projects found" test_no_typescript_projects -run_test "Single TypeScript project with no errors" test_single_project_success -run_test "Single TypeScript project with errors" test_single_project_with_errors -run_test "Monorepo detection" test_monorepo_detection -run_test "Mixed success and failure in monorepo" test_monorepo_partial_failure -run_test "TypeScript without custom script" test_typescript_no_custom_script -run_test "JSON parsing of hook input" test_json_input_parsing -run_test "Excludes node_modules and build directories" test_excludes_build_directories - -# Summary -echo -e "\n========================================" -echo -e "${BLUE}Test Summary${NC}" -echo "========================================" -echo -e "Total tests: $TOTAL_TESTS" -echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}" -echo -e "Failed: ${RED}$FAILED_TESTS${NC}" - -if [ $FAILED_TESTS -eq 0 ]; then - echo -e "\n${GREEN}All tests passed!${NC}" - exit 0 -else - echo -e "\n${RED}Some tests failed!${NC}" - exit 1 -fi \ No newline at end of file diff --git a/tests/test-all-hooks-blocking.sh b/tests/test-all-hooks-blocking.sh deleted file mode 100755 index 6195787..0000000 --- a/tests/test-all-hooks-blocking.sh +++ /dev/null @@ -1,200 +0,0 @@ -#!/bin/bash -# Test all hooks for proper continue: false handling - -echo "=== Testing All Hooks for continue: false Mechanism ===" -echo - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -# Track results -PASSED=0 -FAILED=0 - -# Function to test a hook -test_hook() { - local hook_name="$1" - local input_json="$2" - local expected_exit="$3" - local description="$4" - - echo -e "${YELLOW}Testing: $hook_name${NC}" - echo "Description: $description" - - # Run the hook directly first to see output - echo "$input_json" | ./hooks/$hook_name 2>&1 | head -20 - - # Now test through universal-hook - local output=$(echo "$input_json" | node lib/commands/universal-hook.js 2>&1) - local exit_code=$? - - echo "Exit code: $exit_code (expected: $expected_exit)" - - if [ $exit_code -eq $expected_exit ]; then - echo -e "${GREEN}โœ… PASS${NC}" - ((PASSED++)) - - # Check for continue: false in output - if echo "$output" | grep -q "Hook stopped execution"; then - echo -e "${GREEN}โœ… Stop message found${NC}" - fi - else - echo -e "${RED}โŒ FAIL${NC}" - echo "Output: $output" - ((FAILED++)) - fi - - echo "---" - echo -} - -# Build first -echo "Building TypeScript..." -npm run build > /dev/null 2>&1 - -# Test 1: test-blocker (should always block) -test_hook "test-blocker.sh" '{ - "session_id": "test", - "transcript_path": "/tmp/test.jsonl", - "hook_event_name": "PreWrite", - "file_path": "test.block-test.txt" -}' 2 "Should always block with continue: false" - -# Test 2: code-quality-validator (simulate violation) -# First create a bad quality file -cat > /tmp/bad-quality.js << 'EOF' -function veryLongFunctionNameThatExceedsLimits() { - // Deeply nested code - if (true) { - if (true) { - if (true) { - if (true) { - if (true) { - console.log("Too deeply nested!"); - } - } - } - } - } - // Add more lines to exceed function length - console.log(1); - console.log(2); - console.log(3); - console.log(4); - console.log(5); - console.log(6); - console.log(7); - console.log(8); - console.log(9); - console.log(10); - console.log(11); - console.log(12); - console.log(13); - console.log(14); - console.log(15); -} -EOF - -test_hook "code-quality-validator.sh" '{ - "tool": "Write", - "tool_input": {"file_path": "/tmp/bad-quality.js"}, - "exit_code": 0 -}' 2 "Should block on quality violations" - -# Test 3: lint-check (simulate lint failure) -# Mock npm run lint to fail -test_hook "lint-check.sh" '{ - "session_id": "test", - "hook_event_name": "PreToolUse", - "tool_name": "Bash", - "tool_input": {"command": "git commit -m test"} -}' 0 "Would block if lint fails (need mock)" - -# Test 4: typescript-check (simulate TS error) -test_hook "typescript-check.sh" '{ - "session_id": "test", - "hook_event_name": "PreToolUse", - "tool_name": "Bash", - "tool_input": {"command": "git commit -m test"} -}' 0 "Would block if TS fails (need mock)" - -# Test 5: Edge case - empty JSON -echo -e "${YELLOW}Testing: Edge case - empty stdin${NC}" -echo "" | node lib/commands/universal-hook.js 2>&1 -exit_code=$? -if [ $exit_code -ne 0 ]; then - echo -e "${GREEN}โœ… Properly handles empty input${NC}" - ((PASSED++)) -else - echo -e "${RED}โŒ Should fail on empty input${NC}" - ((FAILED++)) -fi -echo - -# Test 6: Edge case - malformed JSON -echo -e "${YELLOW}Testing: Edge case - malformed JSON${NC}" -echo "{invalid json" | node lib/commands/universal-hook.js 2>&1 -exit_code=$? -if [ $exit_code -ne 0 ]; then - echo -e "${GREEN}โœ… Properly handles malformed JSON${NC}" - ((PASSED++)) -else - echo -e "${RED}โŒ Should fail on malformed JSON${NC}" - ((FAILED++)) -fi -echo - -# Test 7: Test JSON with different continue values -echo -e "${YELLOW}Testing: Custom hook with continue: true${NC}" -cat > /tmp/test-continue-true.sh << 'EOF' -#!/bin/bash -cat <&1 -exit_code=$? -if [ $exit_code -eq 0 ]; then - echo -e "${GREEN}โœ… continue: true allows execution${NC}" - ((PASSED++)) -else - echo -e "${RED}โŒ continue: true should not block${NC}" - ((FAILED++)) -fi - -# Restore config -mv .claude/hooks/config.cjs.bak .claude/hooks/config.cjs -rm -f .claude/hooks/config.cjs.tmp - -# Clean up -rm -f /tmp/bad-quality.js /tmp/test-continue-true.sh - -echo -echo "=== Test Summary ===" -echo -e "Passed: ${GREEN}$PASSED${NC}" -echo -e "Failed: ${RED}$FAILED${NC}" -echo -echo "Key findings:" -echo "- Hooks properly output JSON with continue: false" -echo "- Universal-hook correctly parses JSON and exits with code 2" -echo "- Edge cases are handled appropriately" - -if [ $FAILED -eq 0 ]; then - echo -e "${GREEN}โœ… All tests passed!${NC}" -else - echo -e "${RED}โŒ Some tests failed${NC}" -fi \ No newline at end of file diff --git a/tests/test-all-hooks-simple.sh b/tests/test-all-hooks-simple.sh deleted file mode 100755 index 91575af..0000000 --- a/tests/test-all-hooks-simple.sh +++ /dev/null @@ -1,121 +0,0 @@ -#!/bin/bash - -# Simple test suite for all Claude Code hooks -# This script tests all configured hooks with basic validation - -set -uo pipefail -# Note: Removed -e flag to prevent early exit on hook failures during testing - -# Export test mode to prevent hooks from blocking during tests -export CLAUDE_HOOKS_TEST_MODE=1 - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Test counters -TOTAL_TESTS=0 -PASSED_TESTS=0 -FAILED_TESTS=0 - -echo "========================================" -echo "Claude Code Hooks Simple Test Suite" -echo "========================================" -echo -e "${YELLOW}Testing all configured hooks${NC}" -echo "" - -# Function to run a simple test -simple_test() { - local test_name="$1" - local hook_input="$2" - local expected_exit="$3" - - ((TOTAL_TESTS++)) - - echo -e "${BLUE}Test $TOTAL_TESTS: $test_name${NC}" - - # Run the test - local exit_code=0 - local output="" - output=$(echo "$hook_input" | claude/hooks/check-package-age.sh 2>&1) || exit_code=$? - - # Check result - local success=false - if [ "$expected_exit" = "BLOCK" ]; then - if echo "$output" | grep -q "\[TEST MODE\] Would have blocked"; then - echo -e " ${GREEN}โœ“ Correctly blocked (test mode)${NC}" - success=true - elif [ $exit_code -eq 2 ]; then - echo -e " ${GREEN}โœ“ Correctly blocked${NC}" - success=true - else - echo -e " ${RED}โœ— Expected blocking${NC}" - fi - elif [ "$expected_exit" = "PASS" ]; then - if [ $exit_code -eq 0 ]; then - echo -e " ${GREEN}โœ“ Correctly passed${NC}" - success=true - else - echo -e " ${RED}โœ— Expected pass but got exit code $exit_code${NC}" - fi - fi - - if [ "$success" = true ]; then - ((PASSED_TESTS++)) - else - ((FAILED_TESTS++)) - echo -e " ${YELLOW}Output: $(echo "$output" | head -1)${NC}" - fi - -} - -# Test 1: Package age hook - old package -simple_test \ - "Block old package (left-pad@1.0.0)" \ - '{"session_id": "test-1", "transcript_path": "/tmp/test-1.jsonl", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "npm install left-pad@1.0.0", "description": "Install old package"}}' \ - "BLOCK" - -# Test 2: Package age hook - recent package -simple_test \ - "Allow recent package (commander)" \ - '{"session_id": "test-2", "transcript_path": "/tmp/test-2.jsonl", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "npm install commander", "description": "Install recent package"}}' \ - "PASS" - -# Test 3: Non-npm command -simple_test \ - "Ignore non-npm command" \ - '{"session_id": "test-3", "transcript_path": "/tmp/test-3.jsonl", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "ls -la", "description": "List files"}}' \ - "PASS" - -# Test 4: Yarn old package -simple_test \ - "Block old yarn package (moment@2.18.0)" \ - '{"session_id": "test-4", "transcript_path": "/tmp/test-4.jsonl", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "yarn add moment@2.18.0", "description": "Install old package with yarn"}}' \ - "BLOCK" - -# Test 5: Non-Bash tool -simple_test \ - "Ignore non-Bash tool" \ - '{"session_id": "test-5", "transcript_path": "/tmp/test-5.jsonl", "hook_event_name": "PreToolUse", "tool_name": "Write", "tool_input": {"file_path": "/tmp/test.js", "content": "console.log(\"test\")"}}' \ - "PASS" - -# Summary -echo "" -echo "========================================" -echo "Test Summary" -echo "========================================" -echo "Total tests: $TOTAL_TESTS" -echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}" -echo -e "Failed: ${RED}$FAILED_TESTS${NC}" -echo "" - -if [ $FAILED_TESTS -eq 0 ]; then - echo -e "${GREEN}All tests passed! ๐ŸŽ‰${NC}" - exit 0 -else - echo -e "${RED}Some tests failed. Please check the output above.${NC}" - exit 1 -fi \ No newline at end of file diff --git a/tests/test-all-hooks.sh b/tests/test-all-hooks.sh deleted file mode 100755 index 5bc26cb..0000000 --- a/tests/test-all-hooks.sh +++ /dev/null @@ -1,240 +0,0 @@ -#!/bin/bash - -# Comprehensive test suite for all Claude Code hooks -# This script tests all configured hooks through simulated Claude commands - -set -uo pipefail -# Note: Removed -e flag to prevent early exit on hook failures during testing - -# Export test mode to prevent hooks from blocking during tests -export CLAUDE_HOOKS_TEST_MODE=1 - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' - -# Test counters -TOTAL_TESTS=0 -PASSED_TESTS=0 -FAILED_TESTS=0 - -# Test output directory -TEST_DIR="/tmp/claude-hooks-test-$$" -mkdir -p "$TEST_DIR" - -# Cleanup on exit -trap "rm -rf $TEST_DIR" EXIT - -# Function to run a hook test -test_hook() { - local test_name="$1" - local hook_event="$2" - local tool_name="$3" - local tool_input="$4" - local expected_behavior="$5" - - ((TOTAL_TESTS++)) - - echo -e "\n${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" - echo -e "${BLUE}Test $TOTAL_TESTS: $test_name${NC}" - echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" - echo "Hook Event: $hook_event" - echo "Tool: $tool_name" - - # Create hook input - local hook_input=$(cat <$tmp_out 2>$tmp_err || exit_code=$? - output=$(cat $tmp_err $tmp_out 2>/dev/null) - rm -f $tmp_out $tmp_err - elif echo "$tool_input" | grep -q "git commit"; then - output=$(echo "$hook_input" | claude/hooks/pre-commit-check.sh 2>&1) || exit_code=$? - fi - ;; - "PreToolUse:Write"|"PreToolUse:Edit"|"PreToolUse:MultiEdit") - # Run code quality primer - output=$(echo "$hook_input" | claude/hooks/code-quality-primer.sh 2>&1) || exit_code=$? - # Run code similarity check - output+=$(echo "$hook_input" | claude/hooks/code-similarity-check.sh 2>&1) || exit_code=$? - ;; - "PostToolUse:Write"|"PostToolUse:Edit"|"PostToolUse:MultiEdit") - # Run post-write checks - output=$(echo "$hook_input" | claude/hooks/post-write.sh 2>&1) || exit_code=$? - # Run context updater - output+=$(echo "$hook_input" | claude/hooks/claude-context-updater.sh 2>&1) || exit_code=$? - ;; - "PreToolUse:TodoWrite") - # Check for completed todos - if echo "$tool_input" | grep -q '"status".*"completed"'; then - output=$(echo "$hook_input" | claude/hooks/pre-completion-check.sh 2>&1) || exit_code=$? - fi - ;; - esac - - # Evaluate results - local success=false - case "$expected_behavior" in - "BLOCK") - # In test mode, check for the test mode message - if echo "$output" | grep -q "\[TEST MODE\] Would have blocked with exit code 2"; then - echo -e "${GREEN}โœ“ Correctly blocked (test mode simulation)${NC}" - success=true - elif [ $exit_code -eq 2 ]; then - echo -e "${GREEN}โœ“ Correctly blocked (exit code 2)${NC}" - success=true - else - echo -e "${RED}โœ— Expected blocking but got exit code $exit_code${NC}" - fi - ;; - "PASS") - if [ $exit_code -eq 0 ]; then - echo -e "${GREEN}โœ“ Correctly passed (exit code 0)${NC}" - success=true - else - echo -e "${RED}โœ— Expected pass but got exit code $exit_code${NC}" - fi - ;; - "OUTPUT") - if [ -n "$output" ]; then - echo -e "${GREEN}โœ“ Generated output${NC}" - echo "Output preview: $(echo "$output" | head -3 | tr '\n' ' ')..." - success=true - else - echo -e "${RED}โœ— Expected output but got none${NC}" - fi - ;; - esac - - if [ "$success" = true ]; then - ((PASSED_TESTS++)) - else - ((FAILED_TESTS++)) - echo -e "${YELLOW}Full output:${NC}" - echo "$output" | head -20 - fi -} - -# Header -echo "========================================" -echo "Claude Code Hooks Comprehensive Test Suite" -echo "========================================" -echo -e "${YELLOW}Testing all configured hooks${NC}" - -# Test 1: Package age hook - old package -test_hook \ - "Package Age - Block old package" \ - "PreToolUse" \ - "Bash" \ - '{"command": "npm install left-pad@1.0.0", "description": "Install old package"}' \ - "BLOCK" - -# Test 2: Package age hook - recent package -test_hook \ - "Package Age - Allow recent package" \ - "PreToolUse" \ - "Bash" \ - '{"command": "npm install commander", "description": "Install recent package"}' \ - "PASS" - -# Skip test 3 for now - pre-commit hook might be hanging -# # Test 3: Pre-commit hook -# test_hook \ -# "Pre-commit - Git commit command" \ -# "PreToolUse" \ -# "Bash" \ -# '{"command": "git commit -m \"Test commit\"", "description": "Create git commit"}' \ -# "OUTPUT" - -# Test 4: Code quality primer -test_hook \ - "Code Quality Primer - Before write" \ - "PreToolUse" \ - "Write" \ - '{"file_path": "/tmp/test.js", "content": "function test() { return true; }"}' \ - "OUTPUT" - -# Skip Test 5: Code similarity check (needs JSON input handling fix) -# test_hook \ -# "Code Similarity - Check for duplicates" \ -# "PreToolUse" \ -# "Edit" \ -# '{"file_path": "/tmp/test.js", "old_string": "test", "new_string": "test2"}' \ -# "PASS" - -# Test 5: Post-write hook -test_hook \ - "Post-write - After file write" \ - "PostToolUse" \ - "Write" \ - '{"file_path": "/tmp/test.ts", "content": "const x: string = \"test\";"}' \ - "OUTPUT" - -# Test 6: Context updater -test_hook \ - "Context Updater - After edit" \ - "PostToolUse" \ - "Edit" \ - '{"file_path": "/tmp/test.js", "old_string": "old", "new_string": "new"}' \ - "OUTPUT" - -# Test 7: Pre-completion check -test_hook \ - "Pre-completion - Completed todo" \ - "PreToolUse" \ - "TodoWrite" \ - '{"todos": [{"id": "1", "content": "Test", "status": "completed", "priority": "high"}]}' \ - "OUTPUT" - -# Test 8: Non-matching commands should pass -test_hook \ - "Non-matching - Regular bash command" \ - "PreToolUse" \ - "Bash" \ - '{"command": "ls -la", "description": "List files"}' \ - "PASS" - -# Summary -echo "" -echo "========================================" -echo "Test Summary" -echo "========================================" -echo "Total tests: $TOTAL_TESTS" -echo -e "Passed: ${GREEN}$PASSED_TESTS${NC}" -echo -e "Failed: ${RED}$FAILED_TESTS${NC}" -echo "" - -if [ $FAILED_TESTS -eq 0 ]; then - echo -e "${GREEN}All tests passed! ๐ŸŽ‰${NC}" - exit 0 -else - echo -e "${RED}Some tests failed. Please check the output above.${NC}" - exit 1 -fi \ No newline at end of file diff --git a/tests/test-continue-false-complete.sh b/tests/test-continue-false-complete.sh deleted file mode 100755 index de754cb..0000000 --- a/tests/test-continue-false-complete.sh +++ /dev/null @@ -1,168 +0,0 @@ -#!/bin/bash -# Complete test suite for continue: false mechanism - -echo "=== Complete continue: false Test Suite ===" -echo - -# Colors -GREEN='\033[0;32m' -RED='\033[0;31m' -YELLOW='\033[1;33m' -NC='\033[0m' - -# Test counter -TOTAL=0 -PASSED=0 - -# Test function -run_test() { - local name="$1" - local command="$2" - local expected_exit="$3" - - ((TOTAL++)) - echo -e "${YELLOW}Test $TOTAL: $name${NC}" - - eval "$command" - local exit_code=$? - - if [ $exit_code -eq $expected_exit ]; then - echo -e "${GREEN}โœ… PASS (exit: $exit_code)${NC}" - ((PASSED++)) - else - echo -e "${RED}โŒ FAIL (exit: $exit_code, expected: $expected_exit)${NC}" - fi - echo -} - -# Build first -npm run build > /dev/null 2>&1 - -# Test 1: Test-blocker through universal-hook -run_test "test-blocker via universal-hook" \ - 'echo '\''{"hook_event_name":"PreWrite","file_path":"test.block-test.txt"}'\'' | node lib/commands/universal-hook.js 2>&1 | grep -q "Hook stopped execution" && exit 2 || exit $?' \ - 2 - -# Test 2: Direct execution should output JSON -run_test "test-blocker direct execution" \ - 'echo '\''{"hook_event_name":"PreWrite","file_path":"test.txt"}'\'' | ./hooks/test-blocker.sh 2>&1 | grep -q "\"continue\": false"' \ - 0 - -# Test 3: Test doc-compliance with Stop event -run_test "doc-compliance Stop event (no Gemini key)" \ - 'echo '\''{"hook_event_name":"Stop","stop_hook_active":false}'\'' | ./hooks/doc-compliance.sh 2>&1 | grep -q "Skipping documentation compliance"' \ - 0 - -# Test 4: Create a Stop event test hook -cat > /tmp/stop-test-hook.sh << 'EOF' -#!/bin/bash -INPUT=$(cat) -EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // empty') -STOP_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false') - -if [ "$EVENT" = "Stop" ] && [ "$STOP_ACTIVE" = "false" ]; then - cat < /tmp/continue-true-hook.sh << 'EOF' -#!/bin/bash -cat <> .claude/hooks/config.cjs << 'EOF' - -// Temporary test hook -module.exports.preWrite['\\.allow-test\\.txt$'] = ['/tmp/continue-true-hook.sh']; -EOF - -run_test "Hook with continue: true should not block" \ - 'echo '\''{"hook_event_name":"PreWrite","file_path":"test.allow-test.txt"}'\'' | node lib/commands/universal-hook.js 2>&1; exit $?' \ - 0 - -# Restore config -mv .claude/hooks/config.cjs.bak .claude/hooks/config.cjs - -# Test 6: Multiple hooks, one blocks -cat > /tmp/multi-hook-test.sh << 'EOF' -#!/bin/bash -# First hook allows -echo '{"continue": true}' > /tmp/hook1.out -cat < /tmp/multi-hook-block.sh << 'EOF' -#!/bin/bash -# Second hook blocks -cat <&1 | grep -q "\"continue\": false"' \ - 0 - -# Clean up -rm -f /tmp/stop-test-hook.sh /tmp/continue-true-hook.sh /tmp/multi-hook-test.sh /tmp/multi-hook-block.sh /tmp/hook1.out - -echo "=== Test Summary ===" -echo -e "Total tests: $TOTAL" -echo -e "Passed: ${GREEN}$PASSED${NC}" -echo -e "Failed: ${RED}$((TOTAL - PASSED))${NC}" -echo - -if [ $PASSED -eq $TOTAL ]; then - echo -e "${GREEN}โœ… All tests passed! The continue: false mechanism is working correctly.${NC}" -else - echo -e "${RED}โŒ Some tests failed. Check the output above.${NC}" -fi - -echo -echo "Key confirmations:" -echo "โœ“ Hooks output JSON with continue: false" -echo "โœ“ Universal-hook exits with code 2 when continue: false" -echo "โœ“ Stop events can block with proper JSON output" -echo "โœ“ continue: true allows execution to proceed" \ No newline at end of file diff --git a/tests/test-hooks-integration.sh b/tests/test-hooks-integration.sh deleted file mode 100755 index 4dc9ad4..0000000 --- a/tests/test-hooks-integration.sh +++ /dev/null @@ -1,138 +0,0 @@ -#!/bin/bash - -# Integration test for Claude Code hooks -# This script runs actual npm commands to test the hooks in action - -set -euo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -echo "========================================" -echo "Claude Code Hooks Integration Test" -echo "========================================" -echo "" -echo -e "${YELLOW}This test will run actual npm commands through Claude Code${NC}" -echo -e "${YELLOW}to verify hooks are working correctly.${NC}" -echo "" - -# Function to print test header -test_header() { - echo "" - echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" - echo -e "${BLUE}Test: $1${NC}" - echo -e "${BLUE}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" -} - -# Function to check if package is installed -check_package() { - local package="$1" - if npm list "$package" --depth=0 2>/dev/null | grep -q "$package"; then - return 0 - else - return 1 - fi -} - -# Clean up any existing test packages -echo -e "${YELLOW}Cleaning up any existing test packages...${NC}" -npm uninstall left-pad moment lodash commander 2>/dev/null || true - -# Test 1: Verify old package is blocked -test_header "Old Package Blocking" -echo "Attempting to install left-pad@1.0.0 (from 2016)..." -echo -e "${YELLOW}This should be BLOCKED by the hook${NC}" -echo "" -echo "Run this command:" -echo -e "${GREEN}npm install left-pad@1.0.0${NC}" -echo "" -echo "Expected: Command should fail with 'Execution stopped by PreToolUse hook'" -echo "" -read -p "Press Enter after running the command..." - -if check_package "left-pad"; then - echo -e "${RED}โœ— FAILED: Package was installed when it should have been blocked!${NC}" -else - echo -e "${GREEN}โœ“ PASSED: Package was correctly blocked${NC}" -fi - -# Test 2: Verify recent package is allowed -test_header "Recent Package Installation" -echo "Attempting to install commander (actively maintained)..." -echo -e "${YELLOW}This should be ALLOWED by the hook${NC}" -echo "" -echo "Run this command:" -echo -e "${GREEN}npm install commander${NC}" -echo "" -echo "Expected: Command should succeed" -echo "" -read -p "Press Enter after running the command..." - -if check_package "commander"; then - echo -e "${GREEN}โœ“ PASSED: Package was correctly installed${NC}" -else - echo -e "${RED}โœ— FAILED: Package was not installed when it should have been allowed!${NC}" -fi - -# Clean up -echo "" -echo -e "${YELLOW}Cleaning up test packages...${NC}" -npm uninstall commander 2>/dev/null || true - -# Test 3: Test with multiple packages -test_header "Multiple Package Installation (Mixed)" -echo "Attempting to install multiple packages with one old..." -echo -e "${YELLOW}This should be BLOCKED because one package is old${NC}" -echo "" -echo "Run this command:" -echo -e "${GREEN}npm install lodash left-pad@1.0.0${NC}" -echo "" -echo "Expected: Command should fail due to left-pad being old" -echo "" -read -p "Press Enter after running the command..." - -if check_package "left-pad" || check_package "lodash"; then - echo -e "${RED}โœ— FAILED: Packages were installed when they should have been blocked!${NC}" -else - echo -e "${GREEN}โœ“ PASSED: Installation was correctly blocked${NC}" -fi - -# Test 4: Test environment variable override -test_header "Environment Variable Override" -echo "Testing MAX_AGE_DAYS environment variable..." -echo -e "${YELLOW}This should ALLOW old packages when MAX_AGE_DAYS is set high${NC}" -echo "" -echo "Run this command:" -echo -e "${GREEN}MAX_AGE_DAYS=10000 npm install moment@2.18.0${NC}" -echo "" -echo "Expected: Command should succeed (10000 days allows packages from 2017)" -echo "" -read -p "Press Enter after running the command..." - -if check_package "moment"; then - echo -e "${GREEN}โœ“ PASSED: Environment variable override worked${NC}" -else - echo -e "${RED}โœ— FAILED: Package was blocked despite environment variable!${NC}" -fi - -# Final cleanup -echo "" -echo -e "${YELLOW}Final cleanup...${NC}" -npm uninstall left-pad moment lodash commander 2>/dev/null || true - -echo "" -echo "========================================" -echo -e "${GREEN}Integration Test Complete!${NC}" -echo "========================================" -echo "" -echo "Summary:" -echo "- Old packages should be blocked โœ“" -echo "- Recent packages should be allowed โœ“" -echo "- Mixed installs with old packages should be blocked โœ“" -echo "- Environment variables should override defaults โœ“" -echo "" -echo -e "${YELLOW}Note: This test requires manual interaction to work with Claude Code${NC}" \ No newline at end of file diff --git a/tests/test-hooks-live.sh b/tests/test-hooks-live.sh deleted file mode 100755 index 522beae..0000000 --- a/tests/test-hooks-live.sh +++ /dev/null @@ -1,178 +0,0 @@ -#!/bin/bash - -# Live integration test for Claude Code hooks -# This script provides test commands that should be run through Claude Code - -set -euo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -CYAN='\033[0;36m' -NC='\033[0m' - -echo "========================================" -echo "Claude Code Hooks Live Integration Tests" -echo "========================================" -echo "" -echo -e "${YELLOW}This test suite provides commands to run through Claude Code${NC}" -echo -e "${YELLOW}to verify all hooks are working correctly.${NC}" -echo "" - -# Function to display test section -test_section() { - echo "" - echo -e "${CYAN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" - echo -e "${CYAN}$1${NC}" - echo -e "${CYAN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" -} - -# Function to display test case -test_case() { - local test_name="$1" - local command="$2" - local expected="$3" - - echo "" - echo -e "${BLUE}Test: $test_name${NC}" - echo -e "Command: ${GREEN}$command${NC}" - echo -e "Expected: ${YELLOW}$expected${NC}" - echo "" -} - -# Create test files -echo "Setting up test environment..." -mkdir -p /tmp/claude-test -cat > /tmp/claude-test/sample.js << 'EOF' -// Sample JavaScript file for testing -function calculateTotal(items) { - let total = 0; - for (let i = 0; i < items.length; i++) { - total += items[i].price * items[i].quantity; - } - return total; -} - -function formatCurrency(amount) { - return '$' + amount.toFixed(2); -} -EOF - -# Test 1: Package Age Hook -test_section "1. PACKAGE AGE HOOK TESTS" - -test_case \ - "Block old package" \ - "npm install left-pad@1.0.0" \ - "Should be BLOCKED with 'Execution stopped by PreToolUse hook'" - -test_case \ - "Allow recent package" \ - "npm install commander" \ - "Should SUCCEED and install the package" - -test_case \ - "Block old package with yarn" \ - "yarn add moment@2.18.0" \ - "Should be BLOCKED" - -# Test 2: Pre-Commit Hook -test_section "2. PRE-COMMIT HOOK TESTS" - -test_case \ - "Git commit triggers checks" \ - "git add /tmp/claude-test/sample.js && git commit -m 'Test commit'" \ - "Should run TypeScript/lint checks (may fail if not in git repo)" - -# Test 3: Code Quality Hooks (Write) -test_section "3. CODE QUALITY HOOKS - WRITE TESTS" - -test_case \ - "Write triggers quality primer" \ - "Write a new file at /tmp/claude-test/long-function.js with a function longer than 20 lines" \ - "Should show Clean Code reminder BEFORE writing" - -echo "Example content to write:" -cat << 'EOF' -function veryLongFunction() { - console.log("Line 1"); - console.log("Line 2"); - console.log("Line 3"); - console.log("Line 4"); - console.log("Line 5"); - console.log("Line 6"); - console.log("Line 7"); - console.log("Line 8"); - console.log("Line 9"); - console.log("Line 10"); - console.log("Line 11"); - console.log("Line 12"); - console.log("Line 13"); - console.log("Line 14"); - console.log("Line 15"); - console.log("Line 16"); - console.log("Line 17"); - console.log("Line 18"); - console.log("Line 19"); - console.log("Line 20"); - console.log("Line 21"); - console.log("Line 22"); -} -EOF - -# Test 4: Code Quality Hooks (Edit) -test_section "4. CODE QUALITY HOOKS - EDIT TESTS" - -test_case \ - "Edit triggers similarity check" \ - "Edit /tmp/claude-test/sample.js and add a duplicate of calculateTotal function" \ - "Should check for similar functions BEFORE edit" - -# Test 5: Post-Write Hooks -test_section "5. POST-WRITE HOOK TESTS" - -test_case \ - "TypeScript file triggers post-write checks" \ - "Write a TypeScript file at /tmp/claude-test/test.ts with: const x: string = 123;" \ - "Should show TypeScript errors AFTER writing" - -# Test 6: Todo Completion Hook -test_section "6. TODO COMPLETION HOOK TESTS" - -test_case \ - "Marking todo as completed" \ - "Use TodoWrite to mark a task as completed" \ - "Should trigger pre-completion checks" - -# Test 7: Context Updater -test_section "7. CONTEXT UPDATER TESTS" - -test_case \ - "Edit triggers context update" \ - "Edit any file in the project" \ - "Should update CLAUDE.md if significant changes detected" - -# Cleanup section -test_section "CLEANUP" - -echo "To clean up test files, run:" -echo -e "${GREEN}rm -rf /tmp/claude-test${NC}" -echo "" - -# Summary -test_section "TEST SUMMARY" - -echo "Run each command above through Claude Code and verify:" -echo "" -echo "โœ“ Package age hook blocks old packages" -echo "โœ“ Pre-commit hook runs checks before git commits" -echo "โœ“ Code quality primer shows reminders before writes" -echo "โœ“ Code similarity check detects duplicates" -echo "โœ“ Post-write hook validates TypeScript" -echo "โœ“ Todo completion triggers quality checks" -echo "โœ“ Context updater maintains documentation" -echo "" -echo -e "${YELLOW}Note: Some hooks may show warnings or errors - this is expected${NC}" -echo -e "${YELLOW}The important thing is that they run at the right time${NC}" \ No newline at end of file diff --git a/tests/test-json-handling.js b/tests/test-json-handling.js deleted file mode 100755 index d5a3d4a..0000000 --- a/tests/test-json-handling.js +++ /dev/null @@ -1,203 +0,0 @@ -#!/usr/bin/env node -/** - * Test to verify JSON output handling in universal-hook - */ - -import { spawn } from 'child_process'; -import path from 'path'; -import { fileURLToPath } from 'url'; - -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -console.log('=== Testing JSON Output Handling ===\n'); - -// Test 1: Test hook that outputs JSON with continue: false -async function testJsonBlocking() { - console.log('Test 1: Hook outputs JSON with continue: false'); - console.log('----------------------------------------------'); - - const testInput = { - session_id: "test-123", - transcript_path: "/tmp/test.jsonl", - hook_event_name: "PreWrite", - file_path: "test.block-test.txt", - content: "This should be blocked" - }; - - return new Promise((resolve) => { - const child = spawn('node', [path.join(__dirname, '../lib/commands/universal-hook.js')], { - stdio: ['pipe', 'pipe', 'pipe'], - env: { ...process.env, HOOK_DEBUG: '1' } - }); - - let stdout = ''; - let stderr = ''; - - child.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - child.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - child.on('close', (code) => { - console.log('Exit code:', code); - console.log('Stdout:', stdout); - console.log('Stderr:', stderr); - - if (code === 2) { - console.log('โœ… Correctly exited with code 2'); - if (stderr.includes('Hook stopped execution:')) { - console.log('โœ… Stop message found in stderr'); - } else { - console.log('โŒ Stop message NOT found in stderr'); - } - } else { - console.log('โŒ Did NOT exit with code 2'); - } - - console.log(); - resolve(); - }); - - // Send input - child.stdin.write(JSON.stringify(testInput)); - child.stdin.end(); - }); -} - -// Test 2: Direct test of executeHook function -async function testExecuteHook() { - console.log('Test 2: Direct test of executeHook function'); - console.log('-------------------------------------------'); - - // Create a minimal test to see if executeHook captures stdout - const testScript = ` -import { spawn } from 'child_process'; - -async function executeHook(hookName, input) { - return new Promise((resolve, reject) => { - const child = spawn('npx', ['claude-code-hooks-cli', 'exec', hookName], { - stdio: ['pipe', 'pipe', 'inherit'] // This should capture stdout - }); - - let stdout = ''; - - child.stdout.on('data', (data) => { - stdout += data.toString(); - console.log('[CAPTURED STDOUT]:', data.toString()); - }); - - child.stdin.write(JSON.stringify(input)); - child.stdin.end(); - - child.on('close', (code) => { - console.log('[STDOUT LENGTH]:', stdout.length); - console.log('[EXIT CODE]:', code); - - if (stdout.trim()) { - try { - const output = JSON.parse(stdout.trim()); - console.log('[PARSED JSON]:', output); - - if (output.continue === false) { - console.log('[BLOCKING] continue: false detected'); - process.exit(2); - } - } catch (e) { - console.log('[NOT JSON]:', stdout); - } - } - - resolve(); - }); - }); -} - -// Test it -executeHook('test-blocker', { - session_id: "test", - transcript_path: "/tmp/test.jsonl", - hook_event_name: "PreWrite", - file_path: "test.txt" -}).catch(console.error); -`; - - const child = spawn('node', ['-e', testScript], { - stdio: 'inherit' - }); - - return new Promise((resolve) => { - child.on('close', () => { - console.log(); - resolve(); - }); - }); -} - -// Test 3: Check if npx command works correctly -async function testNpxCommand() { - console.log('Test 3: Test npx command directly'); - console.log('---------------------------------'); - - const testInput = { - session_id: "test-123", - hook_event_name: "PreWrite", - file_path: "test.txt" - }; - - return new Promise((resolve) => { - const child = spawn('npx', ['claude-code-hooks-cli', 'exec', 'test-blocker'], { - stdio: ['pipe', 'pipe', 'pipe'] - }); - - let stdout = ''; - let stderr = ''; - - child.stdout.on('data', (data) => { - stdout += data.toString(); - }); - - child.stderr.on('data', (data) => { - stderr += data.toString(); - }); - - child.on('close', (code) => { - console.log('Exit code:', code); - console.log('Stdout length:', stdout.length); - console.log('First 200 chars of stdout:', stdout.substring(0, 200)); - - try { - const parsed = JSON.parse(stdout.trim()); - console.log('โœ… Valid JSON output'); - console.log('continue:', parsed.continue); - console.log('stopReason:', parsed.stopReason); - } catch (e) { - console.log('โŒ Invalid JSON output'); - } - - console.log(); - resolve(); - }); - - child.stdin.write(JSON.stringify(testInput)); - child.stdin.end(); - }); -} - -// Run all tests -async function runTests() { - await testJsonBlocking(); - await testExecuteHook(); - await testNpxCommand(); - - console.log('=== Summary ==='); - console.log('Check above results to verify:'); - console.log('1. Universal hook exits with code 2 when continue: false'); - console.log('2. executeHook properly captures stdout'); - console.log('3. JSON is parsed correctly'); -} - -runTests().catch(console.error); \ No newline at end of file diff --git a/tests/test-package-age-automated.sh b/tests/test-package-age-automated.sh deleted file mode 100755 index aec74ad..0000000 --- a/tests/test-package-age-automated.sh +++ /dev/null @@ -1,191 +0,0 @@ -#!/bin/bash - -# Automated test suite for package age hook -# Runs tests directly without manual Claude Code interaction - -set -euo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -# Test tracking -TESTS_RUN=0 -TESTS_PASSED=0 -TESTS_FAILED=0 - -# Test function -test_hook() { - local test_name="$1" - local command="$2" - local expected_exit_code="$3" - local expected_output_pattern="$4" - - ((TESTS_RUN++)) - - echo -e "\n${BLUE}Test $TESTS_RUN: $test_name${NC}" - echo "Command: $command" - - # Create hook input - local hook_input=$(cat <&1) || exit_code=$? - exit_code=${exit_code:-0} - - # Check exit code - local exit_code_pass=false - if [ "$exit_code" -eq "$expected_exit_code" ]; then - exit_code_pass=true - echo -e " Exit code: ${GREEN}$exit_code (expected)${NC}" - else - echo -e " Exit code: ${RED}$exit_code (expected: $expected_exit_code)${NC}" - fi - - # Check output pattern if provided - local output_pass=true - if [ -n "$expected_output_pattern" ]; then - if echo "$output" | grep -q "$expected_output_pattern"; then - echo -e " Output: ${GREEN}Contains expected pattern${NC}" - else - output_pass=false - echo -e " Output: ${RED}Missing expected pattern: $expected_output_pattern${NC}" - echo " Actual output:" - echo "$output" | sed 's/^/ /' - fi - fi - - # Overall test result - if [ "$exit_code_pass" = true ] && [ "$output_pass" = true ]; then - echo -e " ${GREEN}โœ“ PASSED${NC}" - ((TESTS_PASSED++)) - else - echo -e " ${RED}โœ— FAILED${NC}" - ((TESTS_FAILED++)) - fi -} - -echo "========================================" -echo "Package Age Hook Automated Test Suite" -echo "========================================" - -# Test 1: Old npm package should be blocked -test_hook \ - "Block old npm package (left-pad@1.0.0)" \ - "npm install left-pad@1.0.0" \ - 2 \ - "too old" - -# Test 2: Recent package should pass -test_hook \ - "Allow recent npm package" \ - "npm install commander" \ - 0 \ - "" - -# Test 3: Old yarn package should be blocked -test_hook \ - "Block old yarn package" \ - "yarn add moment@2.18.0" \ - 2 \ - "too old" - -# Test 4: Non-npm/yarn commands should pass -test_hook \ - "Allow non-package commands" \ - "ls -la" \ - 0 \ - "" - -# Test 5: npm ci should pass -test_hook \ - "Allow npm ci" \ - "npm ci" \ - 0 \ - "" - -# Test 6: Git URL should pass -test_hook \ - "Allow git URL packages" \ - "npm install git+https://github.com/user/repo.git" \ - 0 \ - "" - -# Test 7: Local path should pass -test_hook \ - "Allow local path packages" \ - "npm install ./local-package" \ - 0 \ - "" - -# Test 8: Multiple packages with one old should block -test_hook \ - "Block when one package is old" \ - "npm install lodash left-pad@1.0.0 commander" \ - 2 \ - "left-pad@1.0.0 is too old" - -# Test 9: npm install with flags -test_hook \ - "Handle npm install with flags" \ - "npm install --save-dev left-pad@1.0.0" \ - 2 \ - "too old" - -# Test 10: Tool name filtering -echo -e "\n${BLUE}Test $((++TESTS_RUN)): Non-Bash tool should be ignored${NC}" -hook_input=$(cat <&1; then - echo -e " ${GREEN}โœ“ PASSED${NC}" - ((TESTS_PASSED++)) -else - echo -e " ${RED}โœ— FAILED${NC}" - ((TESTS_FAILED++)) -fi - -# Summary -echo "" -echo "========================================" -echo "Test Summary" -echo "========================================" -echo "Total tests: $TESTS_RUN" -echo -e "Passed: ${GREEN}$TESTS_PASSED${NC}" -echo -e "Failed: ${RED}$TESTS_FAILED${NC}" -echo "" - -if [ $TESTS_FAILED -eq 0 ]; then - echo -e "${GREEN}All tests passed! ๐ŸŽ‰${NC}" - exit 0 -else - echo -e "${RED}Some tests failed!${NC}" - exit 1 -fi \ No newline at end of file diff --git a/tests/test-package-age-hook.sh b/tests/test-package-age-hook.sh deleted file mode 100755 index c546f8f..0000000 --- a/tests/test-package-age-hook.sh +++ /dev/null @@ -1,156 +0,0 @@ -#!/bin/bash - -# Test suite for the package age hook -# This script tests the hook through Claude Code commands - -set -euo pipefail - -# Colors -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' - -# Test counter -TESTS_PASSED=0 -TESTS_FAILED=0 - -# Test results file -TEST_RESULTS="/tmp/package-age-hook-test-results.txt" -TEST_LOG="/tmp/package-age-hook-test.log" - -# Clean up function -cleanup() { - rm -f "$TEST_RESULTS" "$TEST_LOG" - # Clean up any test packages - npm uninstall left-pad moment lodash commander 2>/dev/null || true -} - -# Run cleanup on exit -trap cleanup EXIT - -# Test function -run_test() { - local test_name="$1" - local expected_result="$2" - local test_command="$3" - - echo -e "\n${YELLOW}Running test: $test_name${NC}" - echo "Command: $test_command" - - # Clear previous results - echo "" > "$TEST_RESULTS" - - # Run the test command and capture the result - if eval "$test_command" > "$TEST_LOG" 2>&1; then - echo "SUCCESS" > "$TEST_RESULTS" - else - echo "BLOCKED" > "$TEST_RESULTS" - fi - - local actual_result=$(cat "$TEST_RESULTS") - - if [ "$actual_result" = "$expected_result" ]; then - echo -e "${GREEN}โœ“ PASSED${NC}: $test_name" - ((TESTS_PASSED++)) - else - echo -e "${RED}โœ— FAILED${NC}: $test_name" - echo " Expected: $expected_result" - echo " Actual: $actual_result" - echo " Log output:" - cat "$TEST_LOG" | sed 's/^/ /' - ((TESTS_FAILED++)) - fi -} - -# Function to simulate Claude Code hook execution -simulate_hook() { - local command="$1" - local hook_input=$(cat < /tmp/test-stop-hook.sh << 'EOF' -#!/bin/bash -INPUT=$(cat) -EVENT=$(echo "$INPUT" | jq -r '.hook_event_name') - -if [ "$EVENT" = "Stop" ]; then - cat </dev/null) - -if [ -z "$old_files" ]; then - echo -e "${GREEN}โœ… No old log files found${NC}" -else - echo -e "${YELLOW}Found old log files:${NC}" - echo "$old_files" | while read file; do - size=$(du -h "$file" | cut -f1) - age=$(( ($(date +%s) - $(stat -f%m "$file" 2>/dev/null || stat -c%Y "$file" 2>/dev/null)) / 86400 )) - echo -e " ๐Ÿ“„ $file (${size}, ${age} days old)" - done - - # Calculate total size - total_size=$(echo "$old_files" | xargs du -ch 2>/dev/null | tail -1 | cut -f1) - echo -e "\nTotal size of old files: ${YELLOW}$total_size${NC}" - - # Ask for confirmation - echo "" - read -p "Delete these old log files? (y/N): " confirm - - if [[ "$confirm" =~ ^[Yy]$ ]]; then - echo "$old_files" | while read file; do - rm -f "$file" - echo -e "${GREEN}โœ… Deleted: $file${NC}" - done - echo -e "\n${GREEN}โœ… Old log files cleaned up${NC}" - else - echo -e "${YELLOW}โš ๏ธ Cleanup cancelled${NC}" - fi -fi - -# Option to rotate current log -if [ -f "$LOG_FILE" ]; then - echo "" - read -p "Rotate current log file? (y/N): " rotate - - if [[ "$rotate" =~ ^[Yy]$ ]]; then - timestamp=$(date +%Y%m%d_%H%M%S) - rotated_file="${LOG_FILE}.${timestamp}" - mv "$LOG_FILE" "$rotated_file" - echo -e "${GREEN}โœ… Rotated to: $rotated_file${NC}" - - # Create new empty log file - touch "$LOG_FILE" - echo -e "${GREEN}โœ… Created new log file${NC}" - fi -fi - -# Show disk usage -echo -e "\n๐Ÿ“Š Log directory disk usage:" -du -sh "$LOGS_DIR" 2>/dev/null || echo "Unable to calculate" - -echo -e "\n${GREEN}โœ… Log maintenance complete${NC}" \ No newline at end of file diff --git a/tools/view-logs.sh b/tools/view-logs.sh deleted file mode 100755 index 59bcd5e..0000000 --- a/tools/view-logs.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash - -# Claude Hooks Log Viewer -# Interactive tool to view and analyze hook logs - -LOG_FILE="${CLAUDE_LOG_FILE:-$HOME/.local/share/claude-hooks/logs/hooks.log}" -LINES_TO_SHOW=50 - -# Colors -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -RED='\033[0;31m' -CYAN='\033[0;36m' -NC='\033[0m' # No Color - -echo -e "${CYAN}๐Ÿ” Claude Hooks Log Viewer${NC}" -echo -e "${CYAN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" - -# Check if log file exists -if [ ! -f "$LOG_FILE" ]; then - echo -e "${YELLOW}โš ๏ธ No log file found at: $LOG_FILE${NC}" - echo -e "${YELLOW} Hooks may not have run yet or logging may be disabled${NC}" - exit 0 -fi - -# Function to colorize log levels -colorize_logs() { - sed -E "s/\[ERROR\]/$(printf '\033[0;31m')[ERROR]$(printf '\033[0m')/g" | \ - sed -E "s/\[WARN\]/$(printf '\033[1;33m')[WARN]$(printf '\033[0m')/g" | \ - sed -E "s/\[INFO\]/$(printf '\033[0;32m')[INFO]$(printf '\033[0m')/g" | \ - sed -E "s/\[DEBUG\]/$(printf '\033[0;36m')[DEBUG]$(printf '\033[0m')/g" -} - -# Main menu -while true; do - echo "" - echo "๐Ÿ“‹ Options:" - echo " 1) View recent logs (last $LINES_TO_SHOW lines)" - echo " 2) View all logs" - echo " 3) Filter by hook name" - echo " 4) Filter by log level" - echo " 5) Show statistics" - echo " 6) Follow logs in real-time" - echo " 7) Search logs" - echo " q) Quit" - echo "" - read -p "Select an option: " choice || exit 0 - - # Exit on empty input or EOF - if [ -z "$choice" ]; then - echo -e "${YELLOW}No input received. Exiting...${NC}" - exit 0 - fi - - case $choice in - 1) - echo -e "\n${CYAN}๐Ÿ“œ Recent Logs:${NC}" - tail -n $LINES_TO_SHOW "$LOG_FILE" | colorize_logs - ;; - 2) - echo -e "\n${CYAN}๐Ÿ“œ All Logs:${NC}" - cat "$LOG_FILE" | colorize_logs | less -R - ;; - 3) - read -p "Enter hook name to filter: " hook_name - echo -e "\n${CYAN}๐Ÿ“œ Logs for hook: $hook_name${NC}" - grep "\[$hook_name\]" "$LOG_FILE" | colorize_logs | less -R - ;; - 4) - echo "Select log level:" - echo " 1) ERROR" - echo " 2) WARN" - echo " 3) INFO" - echo " 4) DEBUG" - read -p "Choice: " level_choice - - case $level_choice in - 1) level="ERROR" ;; - 2) level="WARN" ;; - 3) level="INFO" ;; - 4) level="DEBUG" ;; - *) continue ;; - esac - - echo -e "\n${CYAN}๐Ÿ“œ $level logs:${NC}" - grep "\[$level\]" "$LOG_FILE" | colorize_logs | less -R - ;; - 5) - echo -e "\n${CYAN}๐Ÿ“Š Hook Statistics:${NC}" - echo -e "${CYAN}โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”${NC}" - - # Total logs - total_logs=$(wc -l < "$LOG_FILE") - echo -e "Total log entries: ${GREEN}$total_logs${NC}" - - # Logs by level - echo -e "\nLogs by level:" - for level in ERROR WARN INFO DEBUG; do - count=$(grep -c "\[$level\]" "$LOG_FILE" 2>/dev/null || echo 0) - case $level in - ERROR) color=$RED ;; - WARN) color=$YELLOW ;; - INFO) color=$GREEN ;; - DEBUG) color=$CYAN ;; - esac - printf " ${color}%-7s${NC}: %d\n" "$level" "$count" - done - - # Most active hooks - echo -e "\nMost active hooks:" - awk -F'[][]' '/\[.*\] \[.*\] \[.*\]/ {print $6}' "$LOG_FILE" | \ - sort | uniq -c | sort -rn | head -10 | \ - while read count hook; do - printf " %-30s: %d\n" "$hook" "$count" - done - - # Recent errors - error_count=$(grep -c "\[ERROR\]" "$LOG_FILE" 2>/dev/null || echo 0) - if [ $error_count -gt 0 ]; then - echo -e "\n${RED}Recent errors:${NC}" - grep "\[ERROR\]" "$LOG_FILE" | tail -5 | colorize_logs - fi - ;; - 6) - echo -e "\n${CYAN}๐Ÿ“ก Following logs (Ctrl+C to stop):${NC}" - tail -f "$LOG_FILE" | colorize_logs - ;; - 7) - read -p "Enter search term: " search_term - echo -e "\n${CYAN}๐Ÿ” Search results for: $search_term${NC}" - grep -i "$search_term" "$LOG_FILE" | colorize_logs | less -R - ;; - q|Q) - echo -e "${GREEN}๐Ÿ‘‹ Goodbye!${NC}" - exit 0 - ;; - *) - echo -e "${RED}Invalid option${NC}" - ;; - esac -done \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json deleted file mode 100644 index 2a10615..0000000 --- a/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2020", - "module": "ES2020", - "lib": ["ES2020"], - "outDir": "./lib", - "rootDir": "./src", - "strict": true, - "esModuleInterop": true, - "skipLibCheck": true, - "forceConsistentCasingInFileNames": true, - "moduleResolution": "node", - "allowSyntheticDefaultImports": true, - "declaration": true, - "declarationMap": true, - "sourceMap": true, - "types": ["node"] - }, - "include": ["src/**/*"], - "exclude": ["node_modules", "lib", "hooks", "bin"] -} \ No newline at end of file diff --git a/universal-hook-wrapper.sh b/universal-hook-wrapper.sh deleted file mode 100755 index 06da726..0000000 --- a/universal-hook-wrapper.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# Universal hook wrapper for Claude Code - -# Always log that we're starting -echo "[WRAPPER] Universal hook wrapper called at $(date)" >> /Users/danseider/claude-hooks/.claude/hooks/wrapper.log - -# Change to the project directory -cd /Users/danseider/claude-hooks - -# Run the universal hook -node lib/commands/universal-hook.js \ No newline at end of file