#!/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 ImportOrganizer: def __init__(self, input_data: dict[str, Any]): self.input = input_data self.import_groups = { "react": [], "thirdParty": [], "absolute": [], "relative": [], "types": [], } def organize(self) -> dict[str, Any]: """Main organization entry point""" tool_input = self.input.get("tool_input", {}) output = self.input.get("output", {}) content = tool_input.get("content") file_path = tool_input.get("file_path") # Security: Basic input validation if file_path and ( "../" in file_path or "..\\" in file_path or file_path.startswith("/") ): return self.skip("Potentially unsafe file path detected") # Only process TypeScript/JavaScript files file_ext = Path(file_path).suffix if file_path else "" if file_ext not in [".ts", ".tsx", ".js", ".jsx"]: return self.skip("Not a TypeScript/JavaScript file") # Work with the output content if available (PostToolUse), otherwise input content code_content = output.get("content") or content if not code_content: return self.skip("No content to organize") try: organized = self.organize_imports(code_content) # If content changed, write it back if organized != code_content: self.write_organized_content(file_path, organized) return self.success("Imports organized successfully") else: return self.skip("Imports already organized") except Exception as error: return self.error(f"Failed to organize imports: {error}") def organize_imports(self, content: str) -> str: """Parse and organize imports""" lines = content.split("\n") first_import_index = -1 last_import_index = -1 file_header = [] # Find import boundaries and directives for i, line in enumerate(lines): trimmed_line = line.strip() # Check for 'use client' or 'use server' directives if trimmed_line in ["'use client'", '"use client"']: file_header.append(line) continue if trimmed_line in ["'use server'", '"use server"']: file_header.append(line) continue # Skip shebang and comments at the top if i == 0 and trimmed_line.startswith("#!"): file_header.append(line) continue # Detect imports if self.is_import_line(trimmed_line): if first_import_index == -1: first_import_index = i last_import_index = i self.categorize_import(line) elif first_import_index != -1 and trimmed_line != "": # Stop when we hit non-import, non-empty content break # If no imports found, return original content if first_import_index == -1: return content # Build organized imports organized_imports = self.build_organized_imports() # Reconstruct the file before_imports = lines[:first_import_index] after_imports = lines[last_import_index + 1 :] # Combine everything result = [] result.extend(file_header) if file_header: result.append("") # Add blank line after directives result.extend([line for line in before_imports if line not in file_header]) result.extend(organized_imports) result.extend(after_imports) return "\n".join(result) def is_import_line(self, line: str) -> bool: """Check if a line is an import statement""" return bool( re.match(r"^import\s+", line) or re.match(r"^import\s*{", line) or re.match(r"^import\s*type", line) ) def categorize_import(self, import_line: str): """Categorize import into appropriate group""" trimmed = import_line.strip() # Type imports if "import type" in trimmed or "import { type" in trimmed: self.import_groups["types"].append(import_line) return # Extract the module path module_match = re.search(r"from\s+['\"]([^'\"]+)['\"]", import_line) if not module_match: # Handle side-effect imports (import 'module') if "react" in import_line or "next" in import_line: self.import_groups["react"].append(import_line) else: self.import_groups["thirdParty"].append(import_line) return module_path = module_match.group(1) # React/Next.js imports if self.is_react_import(module_path): self.import_groups["react"].append(import_line) # Absolute imports (@/) elif module_path.startswith("@/"): self.import_groups["absolute"].append(import_line) # Relative imports elif module_path.startswith("."): self.import_groups["relative"].append(import_line) # Third-party imports else: self.import_groups["thirdParty"].append(import_line) def is_react_import(self, module_path: str) -> bool: """Check if import is React/Next.js related""" react_patterns = [ "react", "react-dom", "next", "@next", "next/", "@vercel", ] return any( module_path == pattern or module_path.startswith(pattern + "/") for pattern in react_patterns ) def build_organized_imports(self) -> list[str]: """Build organized import groups""" groups = [] # Add each group with proper spacing if self.import_groups["react"]: groups.extend(self.sort_imports(self.import_groups["react"])) if self.import_groups["thirdParty"]: if groups: groups.append("") # Add blank line groups.extend(self.sort_imports(self.import_groups["thirdParty"])) if self.import_groups["absolute"]: if groups: groups.append("") # Add blank line groups.extend(self.sort_imports(self.import_groups["absolute"])) if self.import_groups["relative"]: if groups: groups.append("") # Add blank line groups.extend(self.sort_imports(self.import_groups["relative"])) if self.import_groups["types"]: if groups: groups.append("") # Add blank line groups.extend(self.sort_imports(self.import_groups["types"])) return groups def sort_imports(self, imports: list[str]) -> list[str]: """Sort imports alphabetically within a group""" def get_path(imp: str) -> str: match = re.search(r"from\s+['\"]([^'\"]+)['\"]", imp) return match.group(1) if match else imp return sorted(imports, key=get_path) def write_organized_content(self, file_path: str, content: str): """Write organized content back to file""" try: with open(file_path, "w", encoding="utf-8") as f: f.write(content) except Exception as error: raise Exception(f"Failed to write file: {error}") def success(self, message: str) -> dict[str, Any]: """Return success response""" return {"success": True, "message": f"✅ {message}", "modified": True} def skip(self, reason: str) -> dict[str, Any]: """Return skip response""" return {"success": True, "message": f"â„šī¸ Skipped: {reason}", "modified": False} def error(self, message: str) -> dict[str, Any]: """Return error response""" return {"success": False, "message": f"❌ {message}", "modified": False} def log_import_organizer_activity(input_data, result): """Log import organizer activity to a structured JSON file.""" try: # Ensure log directory exists log_dir = Path.cwd() / "logs" log_dir.mkdir(parents=True, exist_ok=True) log_path = log_dir / "import_organizer.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 and hook event name to the log entry timestamp = datetime.now().strftime("%b %d, %I:%M%p").lower() log_entry = input_data.copy() log_entry["timestamp"] = timestamp log_entry["hook_event_name"] = "ImportOrganizer" log_entry["result"] = result log_entry["working_directory"] = str(Path.cwd()) # Append new data log_data.append(log_entry) # Write back to file with formatting with open(log_path, "w") as f: json.dump(log_data, f, indent=2) except Exception as e: # Don't let logging errors break the hook print(f"Logging error: {e}", file=sys.stderr) def main(): """Main execution""" input_data = None result = None try: input_data = json.load(sys.stdin) # Extract file path for user-friendly message tool_input = input_data.get("tool_input", {}) file_path = tool_input.get("file_path", "") file_name = Path(file_path).name if file_path else "file" # Show friendly message print(f"đŸ“Ļ Organizing imports in {file_name}...", file=sys.stderr) organizer = ImportOrganizer(input_data) result = organizer.organize() # Log the activity log_import_organizer_activity(input_data, result) # Show result to user if result.get("modified", False): print(f"✅ Imports organized in {file_name}", file=sys.stderr) else: print(f"👍 Imports already organized in {file_name}", file=sys.stderr) # For PostToolUse hooks, we don't need to return approve/block print(json.dumps({"message": result["message"]})) except Exception as error: # Log the error if we have input_data if input_data: error_result = { "success": False, "message": f"Import organizer error: {error}", "modified": False, } log_import_organizer_activity(input_data, error_result) print(json.dumps({"message": f"Import organizer error: {error}"})) if __name__ == "__main__": main()