Ryan Malloy 9445e09c48 Add NPM distribution support with hybrid installation approach
Major changes:
- Add package.json with NPM packaging configuration
- Create Node.js CLI interface (bin/claude-hooks.js) with full command set
- Convert bash scripts to Python for better npm integration
- Add npm postinstall/preuninstall hooks for automatic setup
- Update bootstrap prompt to recommend NPM method with git fallback
- Enhance README with NPM-first documentation
- Maintain backward compatibility with existing git installation

Features:
- npm install -g claude-hooks for easy distribution
- claude-hooks init/status/test/backup/uninstall commands
- Automatic Python dependency installation
- Conflict detection and prevention
- Hybrid approach supporting both npm and git workflows

This resolves installation complexity while maintaining developer flexibility.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-19 21:03:13 -06:00

172 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Claude Hooks Test Script for NPM Distribution"""
import os
import sys
import json
import subprocess
import tempfile
from pathlib import Path
# Colors for terminal output
class Colors:
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[1;33m'
BLUE = '\033[0;34m'
NC = '\033[0m' # No Color
def print_colored(message, color):
print(f"{color}{message}{Colors.NC}")
def print_status(message, status="INFO"):
colors = {
"SUCCESS": Colors.GREEN,
"ERROR": Colors.RED,
"WARNING": Colors.YELLOW,
"INFO": Colors.BLUE
}
print_colored(f"[{status}] {message}", colors.get(status, Colors.NC))
def test_hook_script(hook_path, test_input=None):
"""Test a single hook script"""
if not hook_path.exists():
return False, "Script not found"
try:
cmd = [sys.executable, str(hook_path)]
result = subprocess.run(
cmd,
input=test_input or "",
text=True,
capture_output=True,
timeout=10
)
if result.returncode == 0:
return True, "Success"
else:
return False, f"Exit code {result.returncode}: {result.stderr.strip()}"
except subprocess.TimeoutExpired:
return False, "Timeout"
except Exception as e:
return False, str(e)
def main():
print_colored("Claude Hooks Test Suite", Colors.BLUE)
print("=" * 30)
# Get package root
package_root = Path(__file__).parent.parent
hooks_dir = package_root / "hooks"
claude_config_dir = Path.home() / ".config" / "claude"
hooks_config_file = claude_config_dir / "hooks.json"
tests_passed = 0
tests_total = 0
# Test 1: Check configuration exists
print("Testing configuration...")
tests_total += 1
if hooks_config_file.exists():
print_status("✓ Hooks configuration found", "SUCCESS")
tests_passed += 1
else:
print_status("✗ Hooks configuration missing", "ERROR")
print(" Run: claude-hooks init")
# Test 2: Check Python version
print("Testing Python compatibility...")
tests_total += 1
if sys.version_info >= (3, 8):
print_status(f"✓ Python {sys.version_info.major}.{sys.version_info.minor} compatible", "SUCCESS")
tests_passed += 1
else:
print_status(f"✗ Python 3.8+ required, found {sys.version_info.major}.{sys.version_info.minor}", "ERROR")
# Test 3: Test hook scripts
hook_tests = [
("context_monitor.py", '{"prompt": "test prompt", "context_size": 1000}'),
("command_validator.py", '{"command": "pip install requests", "cwd": "/tmp"}'),
("session_logger.py", '{"tool": "Bash", "args": {"command": "echo test"}}'),
("session_finalizer.py", '{"session_duration": 300, "tools_used": ["Bash", "Edit"]}')
]
for script_name, test_input in hook_tests:
print(f"Testing {script_name}...")
tests_total += 1
hook_path = hooks_dir / script_name
success, message = test_hook_script(hook_path, test_input)
if success:
print_status(f"{script_name} working", "SUCCESS")
tests_passed += 1
else:
print_status(f"{script_name} failed: {message}", "ERROR")
# Test 4: Check runtime directories
print("Testing runtime directories...")
tests_total += 1
runtime_dir = package_root / ".claude_hooks"
required_dirs = ["backups", "logs", "patterns"]
missing_dirs = []
for dirname in required_dirs:
if not (runtime_dir / dirname).exists():
missing_dirs.append(dirname)
if not missing_dirs:
print_status("✓ Runtime directories present", "SUCCESS")
tests_passed += 1
else:
print_status(f"✗ Missing directories: {', '.join(missing_dirs)}", "ERROR")
# Test 5: Test shadow learner
print("Testing shadow learner...")
tests_total += 1
try:
# Import test
sys.path.insert(0, str(package_root))
from lib.shadow_learner import ShadowLearner
learner = ShadowLearner(str(package_root / ".claude_hooks" / "patterns"))
# Basic functionality test
learner.learn_command_failure("pip", "pip3", 0.9)
suggestion = learner.get_suggestion("pip")
if suggestion and suggestion.get('suggestion') == 'pip3':
print_status("✓ Shadow learner functional", "SUCCESS")
tests_passed += 1
else:
print_status("✗ Shadow learner not learning correctly", "ERROR")
except Exception as e:
print_status(f"✗ Shadow learner error: {e}", "ERROR")
# Summary
print()
print_colored("Test Results", Colors.BLUE)
print("-" * 20)
if tests_passed == tests_total:
print_status(f"All {tests_total} tests passed! 🎉", "SUCCESS")
print()
print_colored("Claude Hooks is ready to use!", Colors.GREEN)
print("Try running some commands to see the learning in action:")
print("- pip install requests (should suggest pip3)")
print("- python script.py (should suggest python3)")
return 0
else:
print_status(f"{tests_passed}/{tests_total} tests passed", "WARNING")
print()
print_colored("Some issues detected. Check installation:", Colors.YELLOW)
print("1. Run: claude-hooks init")
print("2. Restart Claude Code")
print("3. Run tests again")
return 1
if __name__ == "__main__":
sys.exit(main())