-
Notifications
You must be signed in to change notification settings - Fork 0
fix: eliminate code duplication with shared modules and composite actions #652
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -8,9 +8,10 @@ | |||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||
| import re | ||||||||||||||||||||||||||||||||
| from common import load_hook_input, get_command | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| data = json.load(sys.stdin) | ||||||||||||||||||||||||||||||||
| cmd = (data.get("tool_input", {}) or {}).get("command") or "" | ||||||||||||||||||||||||||||||||
| data = load_hook_input() | ||||||||||||||||||||||||||||||||
| cmd = get_command(data) | ||||||||||||||||||||||||||||||||
|
Comment on lines
8
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused The 🧹 Proposed fix import sys
-import json
import re
from common import load_hook_input, get_command📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if not cmd.strip(): | ||||||||||||||||||||||||||||||||
| sys.exit(0) | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,10 +2,10 @@ | |||||||||||||||||||||||||||||||||
| import sys | ||||||||||||||||||||||||||||||||||
| import json | ||||||||||||||||||||||||||||||||||
| import shlex | ||||||||||||||||||||||||||||||||||
| from common import load_hook_input, get_command | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| # Read input from Claude | ||||||||||||||||||||||||||||||||||
| data = json.load(sys.stdin) | ||||||||||||||||||||||||||||||||||
| cmd = (data.get("tool_input", {}) or {}).get("command") or "" | ||||||||||||||||||||||||||||||||||
| data = load_hook_input() | ||||||||||||||||||||||||||||||||||
| cmd = get_command(data) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
2
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove unused The 🧹 Proposed fix import sys
-import json
import shlex
from common import load_hook_input, get_command📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| tokens = shlex.split(cmd) if cmd else [] | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if not tokens: | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,139 @@ | ||
| #!/usr/bin/env python3 | ||
| """Shared utilities for Claude Code hooks. | ||
|
|
||
| Consolidates common patterns used across hook files: | ||
| - JSON input parsing from stdin | ||
| - Tool context extraction | ||
| - Output formatting (headers, sections, status) | ||
| - Git operations | ||
| - Package manager detection | ||
| """ | ||
| import sys | ||
| import json | ||
| import re | ||
| import subprocess | ||
| from pathlib import Path | ||
| from typing import Optional | ||
|
|
||
|
|
||
| def load_hook_input() -> dict: | ||
| """Load and return JSON input from stdin.""" | ||
| return json.load(sys.stdin) | ||
|
|
||
|
|
||
| def parse_tool_context(data: dict) -> tuple: | ||
| """Extract standard tool context fields. | ||
|
|
||
| Returns: | ||
| tuple: (tool_name, tool_input, tool_response) | ||
| """ | ||
| tool_name = data.get("tool_name", "") | ||
| tool_input = data.get("tool_input", {}) or {} | ||
| tool_response = data.get("tool_response", {}) or {} | ||
| return tool_name, tool_input, tool_response | ||
|
|
||
|
|
||
| def get_command(data: dict) -> str: | ||
| """Extract command string from hook input data.""" | ||
| tool_input = data.get("tool_input", {}) or {} | ||
| return (tool_input.get("command") or "").strip() | ||
|
|
||
|
|
||
| def is_bash_command(tool_name: str) -> bool: | ||
| """Check if the tool is the Bash tool.""" | ||
| return tool_name == "Bash" | ||
|
|
||
|
|
||
| def is_help_command(command: str) -> bool: | ||
| """Check if command is a help/dry-run command.""" | ||
| return "--help" in command or "-h" in command | ||
|
|
||
|
|
||
| def extract_pr_url(text: str) -> Optional[tuple]: | ||
| """Extract PR URL and components from text. | ||
|
|
||
| Returns: | ||
| Optional[tuple]: (pr_url, owner, repo, pr_number) or None | ||
| """ | ||
| pattern = r"https://github\.com/([^/]+)/([^/]+)/pull/(\d+)" | ||
| match = re.search(pattern, text) | ||
| if match: | ||
| return match.group(0), match.group(1), match.group(2), match.group(3) | ||
| return None | ||
|
|
||
|
|
||
| # ============================================================================ | ||
| # Output formatting | ||
| # ============================================================================ | ||
|
|
||
| def print_header(message: str, width: int = 60) -> None: | ||
| """Print formatted header with separators to stderr.""" | ||
| print("", file=sys.stderr, flush=True) | ||
| print("=" * width, file=sys.stderr, flush=True) | ||
| print(message, file=sys.stderr, flush=True) | ||
| print("=" * width, file=sys.stderr, flush=True) | ||
|
|
||
|
|
||
| def print_footer(width: int = 60) -> None: | ||
| """Print footer separator to stderr.""" | ||
| print("", file=sys.stderr, flush=True) | ||
| print("=" * width, file=sys.stderr, flush=True) | ||
|
|
||
|
|
||
| def print_section(title: str, width: int = 40) -> None: | ||
| """Print section header to stderr.""" | ||
| print("", file=sys.stderr, flush=True) | ||
| print(f"## {title}", file=sys.stderr, flush=True) | ||
| print("-" * width, file=sys.stderr, flush=True) | ||
|
|
||
|
|
||
| def print_status(message: str) -> None: | ||
| """Print status message to stderr.""" | ||
| print(message, file=sys.stderr, flush=True) | ||
|
|
||
|
|
||
| # ============================================================================ | ||
| # Git operations | ||
| # ============================================================================ | ||
|
|
||
| def get_git_root() -> Optional[Path]: | ||
| """Get repository root directory, return None if not a git repo.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["git", "rev-parse", "--show-toplevel"], | ||
| capture_output=True, text=True, timeout=10 | ||
| ) | ||
| root = result.stdout.strip() | ||
| return Path(root) if root and result.returncode == 0 else None | ||
| except (subprocess.TimeoutExpired, FileNotFoundError): | ||
| return None | ||
|
|
||
|
|
||
| def get_changed_files(ref: str = "HEAD", cwd: Optional[str] = None) -> list: | ||
| """Get list of changed files for given ref.""" | ||
| try: | ||
| result = subprocess.run( | ||
| ["git", "diff-tree", "--no-commit-id", "--name-only", "-r", ref], | ||
| capture_output=True, text=True, timeout=5, | ||
| cwd=cwd, | ||
| ) | ||
| if result.returncode == 0 and result.stdout.strip(): | ||
| return result.stdout.strip().split("\n") | ||
| return [] | ||
| except (subprocess.TimeoutExpired, OSError): | ||
| return [] | ||
|
|
||
|
|
||
| # ============================================================================ | ||
| # Package manager detection | ||
| # ============================================================================ | ||
|
|
||
| def detect_package_manager(root: Path) -> str: | ||
| """Detect package manager from lock files.""" | ||
| if (root / "pnpm-lock.yaml").exists(): | ||
| return "pnpm" | ||
| if (root / "yarn.lock").exists(): | ||
| return "yarn" | ||
| if (root / "bun.lockb").exists() or (root / "bun.lock").exists(): | ||
| return "bun" | ||
| return "npm" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove unused
jsonimport.The
jsonmodule is imported but no longer used after refactoring to useload_hook_input().🧹 Proposed fix
import sys -import json from common import load_hook_input🤖 Prompt for AI Agents