Files
gh-aojdevstudio-dev-utils-m…/hooks/scripts/pnpm-enforcer.py
2025-11-29 17:57:23 +08:00

203 lines
6.7 KiB
Python
Executable File

#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.10"
# dependencies = []
# ///
import json
import re
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
class PnpmEnforcer:
def __init__(self, input_data: dict[str, Any]):
self.input = input_data
def detect_npm_usage(self, command: str) -> dict[str, Any] | None:
"""Check if command contains npm or npx usage"""
if not command or not isinstance(command, str):
return None
# Common npm/npx patterns to block
npm_patterns = [
r"(?:^|\s|;|&&|\|\|)npm\s+",
r"(?:^|\s|;|&&|\|\|)npx\s+",
r"(?:^|\s|;|&&|\|\|)npm$",
r"(?:^|\s|;|&&|\|\|)npx$",
]
for pattern in npm_patterns:
match = re.search(pattern, command)
if match:
return {
"detected": True,
"original": command.strip(),
"suggestion": self.generate_pnpm_alternative(command),
}
return None
def generate_pnpm_alternative(self, command: str) -> str:
"""Generate pnpm alternative for npm/npx commands"""
# Common npm -> pnpm conversions
conversions = [
# Basic package management
(r"npm install(?:\s|$)", "pnpm install"),
(r"npm i(?:\s|$)", "pnpm install"),
(r"npm install\s+(.+)", r"pnpm add \1"),
(r"npm i\s+(.+)", r"pnpm add \1"),
(r"npm install\s+--save-dev\s+(.+)", r"pnpm add -D \1"),
(r"npm install\s+-D\s+(.+)", r"pnpm add -D \1"),
# Global installs are project-specific in CDEV
(
r"npm install\s+--global\s+(.+)",
r"# Global installs not supported - use npx or install as dev dependency",
),
(
r"npm install\s+-g\s+(.+)",
r"# Global installs not supported - use npx or install as dev dependency",
),
# Uninstall
(r"npm uninstall\s+(.+)", r"pnpm remove \1"),
(r"npm remove\s+(.+)", r"pnpm remove \1"),
(r"npm rm\s+(.+)", r"pnpm remove \1"),
# Scripts
(r"npm run\s+(.+)", r"pnpm run \1"),
(r"npm start", "pnpm start"),
(r"npm test", "pnpm test"),
(r"npm build", "pnpm build"),
(r"npm dev", "pnpm dev"),
# Other commands
(r"npm list", "pnpm list"),
(r"npm ls", "pnpm list"),
(r"npm outdated", "pnpm outdated"),
(r"npm update", "pnpm update"),
(r"npm audit", "pnpm audit"),
(r"npm ci", "pnpm install --frozen-lockfile"),
# npx commands
(r"npx\s+(.+)", r"pnpm dlx \1"),
(r"npx", "pnpm dlx"),
]
suggestion = command
for pattern, replacement in conversions:
if re.search(pattern, command):
suggestion = re.sub(pattern, replacement, command)
break
# If no specific conversion found, do basic substitution
if suggestion == command:
suggestion = re.sub(r"(?:^|\s)npm(?:\s|$)", " pnpm ", command)
suggestion = re.sub(r"(?:^|\s)npx(?:\s|$)", " pnpm dlx ", suggestion)
suggestion = suggestion.strip()
return suggestion
def validate(self) -> dict[str, Any]:
"""Validate and process the bash command"""
try:
# Parse Claude Code hook input format
tool_name = self.input.get("tool_name")
if tool_name != "Bash":
return self.approve()
tool_input = self.input.get("tool_input", {})
command = tool_input.get("command")
if not command:
return self.approve()
# Check for npm/npx usage
npm_usage = self.detect_npm_usage(command)
if npm_usage:
return self.block(npm_usage)
return self.approve()
except Exception as error:
return self.approve(f"PNPM enforcer error: {error}")
def approve(self, custom_message: str | None = None) -> dict[str, Any]:
"""Approve the command"""
return {"approve": True, "message": custom_message or "✅ Command approved"}
def block(self, npm_usage: dict[str, Any]) -> dict[str, Any]:
"""Block npm/npx command and suggest pnpm alternative"""
message = [
"🚫 NPM/NPX Usage Blocked",
"",
f'❌ Blocked command: {npm_usage["original"]}',
f'✅ Use this instead: {npm_usage["suggestion"]}',
"",
"📋 Why pnpm?",
" • Faster installation and better disk efficiency",
" • More reliable dependency resolution",
" • Better monorepo support",
" • Consistent with project standards",
"",
"💡 Quick pnpm reference:",
" • pnpm install → Install dependencies",
" • pnpm add <pkg> → Add package",
" • pnpm add -D <pkg> → Add dev dependency",
" • pnpm run <script> → Run package script",
" • pnpm dlx <cmd> → Execute package (like npx)",
"",
"Please use the suggested pnpm command instead.",
]
return {"approve": False, "message": "\n".join(message)}
def main():
"""Main execution"""
try:
input_data = json.load(sys.stdin)
# Ensure log directory exists
log_dir = Path.cwd() / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
log_path = log_dir / "pnpm_enforcer.json"
# Read existing log data or initialize empty list
if log_path.exists():
with open(log_path) as f:
try:
log_data = json.load(f)
except (json.JSONDecodeError, ValueError):
log_data = []
else:
log_data = []
# Add timestamp to the log entry
timestamp = datetime.now().strftime("%b %d, %I:%M%p").lower()
input_data["timestamp"] = timestamp
# Process enforcement logic
enforcer = PnpmEnforcer(input_data)
result = enforcer.validate()
# Add result to log entry
input_data["enforcement_result"] = result
# Append new data to log
log_data.append(input_data)
# Write back to file with formatting
with open(log_path, "w") as f:
json.dump(log_data, f, indent=2)
print(json.dumps(result))
except Exception as error:
print(json.dumps({"approve": True, "message": f"PNPM enforcer error: {error}"}))
if __name__ == "__main__":
main()