Files
gh-slamb2k-mad-skills-dev-flow/skills/start-right/scripts/setup_git_hooks.py
2025-11-30 08:58:05 +08:00

270 lines
8.6 KiB
Python
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Set up git hooks using husky (Node.js) or lefthook (universal)
This script:
- Detects project type
- Installs and configures appropriate git hooks tool
- Sets up pre-commit and pre-push hooks with validation checks
"""
import subprocess
import sys
import json
from pathlib import Path
def run_command(cmd, check=True, capture_output=True):
"""Run a shell command and return the result."""
result = subprocess.run(
cmd,
shell=True,
check=check,
capture_output=capture_output,
text=True
)
return result
def is_node_project():
"""Check if this is a Node.js project."""
return Path("package.json").exists()
def setup_husky(checks):
"""Set up husky for Node.js projects."""
print("📦 Installing husky...")
# Install husky
try:
run_command("npm install --save-dev husky")
run_command("npx husky init")
print("✅ Husky installed and initialized")
except subprocess.CalledProcessError as e:
print(f"❌ Failed to install husky: {e}")
return False
# Create pre-commit hook
pre_commit_commands = []
if "format" in checks:
pre_commit_commands.append("npm run format:check || (echo '❌ Format check failed. Run npm run format' && exit 1)")
if "lint" in checks:
pre_commit_commands.append("npm run lint")
if "type-check" in checks:
pre_commit_commands.append("npm run type-check")
if pre_commit_commands:
hook_content = "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n"
hook_content += "\n".join(pre_commit_commands)
with open(".husky/pre-commit", "w") as f:
f.write(hook_content)
Path(".husky/pre-commit").chmod(0o755)
print("✅ Created pre-commit hook")
# Create pre-push hook
pre_push_commands = []
if "test" in checks:
pre_push_commands.append("npm run test")
if "build" in checks:
pre_push_commands.append("npm run build")
if pre_push_commands:
hook_content = "#!/bin/sh\n. \"$(dirname \"$0\")/_/husky.sh\"\n\n"
hook_content += "\n".join(pre_push_commands)
with open(".husky/pre-push", "w") as f:
f.write(hook_content)
Path(".husky/pre-push").chmod(0o755)
print("✅ Created pre-push hook")
# Update package.json with scripts if they don't exist
update_package_json_scripts(checks)
return True
def setup_lefthook(checks):
"""Set up lefthook for any project type."""
print("📦 Installing lefthook...")
# Check if lefthook is installed
try:
run_command("lefthook version")
except subprocess.CalledProcessError:
print("Installing lefthook globally...")
# Try to install via common package managers
try:
run_command("brew install lefthook", check=False)
except:
try:
run_command("go install github.com/evilmartians/lefthook@latest", check=False)
except:
print("❌ Could not install lefthook. Please install manually:")
print(" brew install lefthook")
print(" OR")
print(" go install github.com/evilmartians/lefthook@latest")
return False
# Create lefthook.yml configuration
config = {
"pre-commit": {
"parallel": True,
"commands": {}
},
"pre-push": {
"parallel": False,
"commands": {}
}
}
# Pre-commit checks
if "format" in checks:
config["pre-commit"]["commands"]["format-check"] = {
"run": "npm run format:check || echo 'Run: npm run format'",
}
if "lint" in checks:
config["pre-commit"]["commands"]["lint"] = {
"run": "npm run lint" if is_node_project() else "echo 'Configure linting for your project'",
}
if "type-check" in checks:
config["pre-commit"]["commands"]["type-check"] = {
"run": "npm run type-check",
}
# Pre-push checks
if "test" in checks:
config["pre-push"]["commands"]["test"] = {
"run": "npm run test" if is_node_project() else "echo 'Configure tests for your project'",
}
if "build" in checks:
config["pre-push"]["commands"]["build"] = {
"run": "npm run build" if is_node_project() else "echo 'Configure build for your project'",
}
# Write configuration
import yaml
try:
with open("lefthook.yml", "w") as f:
yaml.dump(config, f, default_flow_style=False)
except ImportError:
# Fallback to manual YAML writing if pyyaml not available
with open("lefthook.yml", "w") as f:
f.write("pre-commit:\n")
f.write(" parallel: true\n")
f.write(" commands:\n")
for cmd_name, cmd_config in config["pre-commit"]["commands"].items():
f.write(f" {cmd_name}:\n")
f.write(f" run: {cmd_config['run']}\n")
f.write("\npre-push:\n")
f.write(" parallel: false\n")
f.write(" commands:\n")
for cmd_name, cmd_config in config["pre-push"]["commands"].items():
f.write(f" {cmd_name}:\n")
f.write(f" run: {cmd_config['run']}\n")
print("✅ Created lefthook.yml")
# Install git hooks
try:
run_command("lefthook install")
print("✅ Installed git hooks")
except subprocess.CalledProcessError:
print("⚠️ Run 'lefthook install' to activate hooks")
return True
def update_package_json_scripts(checks):
"""Update package.json with necessary npm scripts if they don't exist."""
if not is_node_project():
return
with open("package.json", "r") as f:
pkg = json.load(f)
scripts = pkg.get("scripts", {})
modified = False
suggested_scripts = {
"format": "prettier --write .",
"format:check": "prettier --check .",
"lint": "eslint .",
"type-check": "tsc --noEmit",
"test": "jest",
"build": "tsc"
}
for script_name, script_cmd in suggested_scripts.items():
if script_name not in scripts:
# Only add if the check is enabled
check_type = script_name.split(":")[0] if ":" in script_name else script_name
if check_type in checks:
scripts[script_name] = script_cmd
modified = True
print(f" Added npm script: {script_name}")
if modified:
pkg["scripts"] = scripts
with open("package.json", "w") as f:
json.dump(pkg, f, indent=2)
print("✅ Updated package.json scripts")
def main():
if len(sys.argv) < 2:
print("Usage: setup_git_hooks.py [--husky|--lefthook] [--checks format,lint,type-check,test,build]")
print("\nOptions:")
print(" --husky Use husky (Node.js projects only)")
print(" --lefthook Use lefthook (universal)")
print(" --checks <list> Comma-separated list of checks to enable")
print("\nExample:")
print(" setup_git_hooks.py --husky --checks format,lint,test")
sys.exit(1)
# Parse arguments
use_husky = "--husky" in sys.argv
use_lefthook = "--lefthook" in sys.argv
# Get checks list
checks = ["format", "lint", "test"] # defaults
if "--checks" in sys.argv:
idx = sys.argv.index("--checks")
if idx + 1 < len(sys.argv):
checks = sys.argv[idx + 1].split(",")
# Auto-detect if not specified
if not use_husky and not use_lefthook:
if is_node_project():
use_husky = True
else:
use_lefthook = True
print("🪝 Setting up git hooks...")
print(f" Tool: {'husky' if use_husky else 'lefthook'}")
print(f" Checks: {', '.join(checks)}")
print()
if use_husky:
if not is_node_project():
print("❌ Husky requires a Node.js project (package.json)")
print(" Use --lefthook for non-Node projects")
sys.exit(1)
success = setup_husky(checks)
else:
success = setup_lefthook(checks)
if success:
print("\n✅ Git hooks configured!")
print("\nHooks will run:")
print(" Pre-commit:", ", ".join([c for c in checks if c in ["format", "lint", "type-check"]]))
print(" Pre-push:", ", ".join([c for c in checks if c in ["test", "build"]]))
if __name__ == "__main__":
main()