144 lines
4.4 KiB
Python
Executable File
144 lines
4.4 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Validate Settings Template Hooks - Pre-commit Hook
|
|
|
|
Ensures hooks referenced in global_settings_template.json actually exist
|
|
in the hooks directory. Prevents "hook not found" errors after install.
|
|
|
|
Usage:
|
|
python3 validate_settings_hooks.py
|
|
|
|
Exit Codes:
|
|
0 - All referenced hooks exist
|
|
1 - Some hooks are missing
|
|
"""
|
|
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
def get_project_root() -> Path:
|
|
"""Find project root by looking for .git directory."""
|
|
current = Path.cwd()
|
|
while current != current.parent:
|
|
if (current / ".git").exists():
|
|
return current
|
|
current = current.parent
|
|
return Path.cwd()
|
|
|
|
|
|
def extract_hook_files(settings: dict) -> list[str]:
|
|
"""Extract hook file names from settings template.
|
|
|
|
Returns:
|
|
List of hook filenames (e.g., ['pre_tool_use.py', 'auto_git_workflow.py'])
|
|
"""
|
|
hooks = []
|
|
|
|
hooks_config = settings.get("hooks", {})
|
|
for lifecycle, matchers in hooks_config.items():
|
|
if not isinstance(matchers, list):
|
|
continue
|
|
for matcher in matchers:
|
|
if not isinstance(matcher, dict):
|
|
continue
|
|
for hook in matcher.get("hooks", []):
|
|
if not isinstance(hook, dict):
|
|
continue
|
|
command = hook.get("command", "")
|
|
# Extract hook filename from command like:
|
|
# "python3 ~/.claude/hooks/pre_tool_use.py"
|
|
# "MCP_AUTO_APPROVE=true python3 ~/.claude/hooks/pre_tool_use.py"
|
|
match = re.search(r'hooks/([a-z_]+\.py)', command)
|
|
if match:
|
|
hooks.append(match.group(1))
|
|
|
|
return hooks
|
|
|
|
|
|
def validate_settings_hooks() -> tuple[bool, list[str]]:
|
|
"""Validate all hooks in settings template exist AND are in install manifest.
|
|
|
|
IMPORTANT: Hooks must be both:
|
|
1. Present in source (plugins/autonomous-dev/hooks/)
|
|
2. Listed in install_manifest.json (so they get installed to ~/.claude/hooks/)
|
|
|
|
Returns:
|
|
Tuple of (success, list of error messages)
|
|
"""
|
|
project_root = get_project_root()
|
|
plugin_dir = project_root / "plugins" / "autonomous-dev"
|
|
|
|
# Load settings template
|
|
template_path = plugin_dir / "config" / "global_settings_template.json"
|
|
if not template_path.exists():
|
|
return True, [] # No template, nothing to validate
|
|
|
|
try:
|
|
settings = json.loads(template_path.read_text())
|
|
except json.JSONDecodeError as e:
|
|
return False, [f"Invalid JSON in settings template: {e}"]
|
|
|
|
# Load install manifest
|
|
manifest_path = plugin_dir / "config" / "install_manifest.json"
|
|
manifest_hooks = set()
|
|
if manifest_path.exists():
|
|
try:
|
|
manifest = json.loads(manifest_path.read_text())
|
|
manifest_hooks = {
|
|
Path(p).name
|
|
for p in manifest.get("components", {}).get("hooks", {}).get("files", [])
|
|
}
|
|
except json.JSONDecodeError:
|
|
pass # Will be caught by other validation
|
|
|
|
# Extract referenced hooks
|
|
referenced_hooks = extract_hook_files(settings)
|
|
if not referenced_hooks:
|
|
return True, [] # No hooks referenced
|
|
|
|
# Check each hook exists in source AND manifest
|
|
hooks_dir = plugin_dir / "hooks"
|
|
errors = []
|
|
|
|
for hook_file in referenced_hooks:
|
|
hook_path = hooks_dir / hook_file
|
|
|
|
# Check 1: Exists in source
|
|
if not hook_path.exists():
|
|
errors.append(f"{hook_file}: Missing from source directory")
|
|
continue
|
|
|
|
# Check 2: Listed in manifest (so it gets installed)
|
|
if hook_file not in manifest_hooks:
|
|
errors.append(
|
|
f"{hook_file}: Exists in source but NOT in install_manifest.json! "
|
|
f"This hook won't be installed to ~/.claude/hooks/"
|
|
)
|
|
|
|
return len(errors) == 0, errors
|
|
|
|
|
|
def main() -> int:
|
|
"""Main entry point."""
|
|
success, missing = validate_settings_hooks()
|
|
|
|
if success:
|
|
print("✅ All settings template hooks exist")
|
|
return 0
|
|
else:
|
|
print("❌ Settings template references missing hooks!")
|
|
print("")
|
|
print("Missing hooks:")
|
|
for hook in sorted(missing):
|
|
print(f" - {hook}")
|
|
print("")
|
|
print("Fix: Either create the hook or update global_settings_template.json")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|