#!/usr/bin/env python3 """ code.format - Format code using Prettier This skill formats code using Prettier, supporting multiple languages and file types. It can format individual files or entire directories, check formatting without making changes, and respect custom Prettier configurations. Generated by meta.skill with Betty Framework certification """ import os import sys import json import yaml import subprocess import shutil from pathlib import Path from typing import Dict, List, Any, Optional from betty.config import BASE_DIR from betty.logging_utils import setup_logger from betty.certification import certified_skill logger = setup_logger(__name__) class CodeFormat: """ Format code using Prettier, supporting multiple languages and file types. """ # Supported file extensions SUPPORTED_EXTENSIONS = { '.js', '.jsx', '.mjs', '.cjs', # JavaScript '.ts', '.tsx', # TypeScript '.css', '.scss', '.less', # Styles '.html', '.htm', # HTML '.json', # JSON '.yaml', '.yml', # YAML '.md', '.mdx', # Markdown '.graphql', '.gql', # GraphQL '.vue', # Vue } # Prettier config file names CONFIG_FILES = [ '.prettierrc', '.prettierrc.json', '.prettierrc.yml', '.prettierrc.yaml', '.prettierrc.json5', '.prettierrc.js', '.prettierrc.cjs', 'prettier.config.js', 'prettier.config.cjs', ] def __init__(self, base_dir: str = BASE_DIR): """Initialize skill""" self.base_dir = Path(base_dir) def _check_prettier_installed(self) -> tuple[bool, Optional[str]]: """ Check if Prettier is installed and return the command to use. Returns: Tuple of (is_installed, command_path) """ # Check for npx prettier (local installation) try: result = subprocess.run( ['npx', 'prettier', '--version'], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: logger.info(f"Found Prettier via npx: {result.stdout.strip()}") return True, 'npx prettier' except (subprocess.SubprocessError, FileNotFoundError): pass # Check for global prettier installation prettier_path = shutil.which('prettier') if prettier_path: try: result = subprocess.run( ['prettier', '--version'], capture_output=True, text=True, timeout=5 ) if result.returncode == 0: logger.info(f"Found Prettier globally: {result.stdout.strip()}") return True, 'prettier' except subprocess.SubprocessError: pass return False, None def _find_config_file(self, start_path: Path, custom_config: Optional[str] = None) -> Optional[Path]: """ Find Prettier configuration file. Args: start_path: Path to start searching from custom_config: Optional custom config file path Returns: Path to config file or None """ if custom_config: config_path = Path(custom_config) if config_path.exists(): logger.info(f"Using custom config: {config_path}") return config_path else: logger.warning(f"Custom config not found: {config_path}") # Search upwards from start_path current = start_path if start_path.is_dir() else start_path.parent while current != current.parent: # Stop at root for config_name in self.CONFIG_FILES: config_path = current / config_name if config_path.exists(): logger.info(f"Found config: {config_path}") return config_path current = current.parent logger.info("No Prettier config found, will use defaults") return None def _discover_files(self, path: Path, patterns: Optional[List[str]] = None) -> List[Path]: """ Discover files to format. Args: path: File or directory path patterns: Optional glob patterns to filter files Returns: List of file paths to format """ if path.is_file(): return [path] files = [] if patterns: for pattern in patterns: files.extend(path.rglob(pattern)) else: # Find all files with supported extensions for ext in self.SUPPORTED_EXTENSIONS: files.extend(path.rglob(f'*{ext}')) # Filter out common ignore patterns ignored_dirs = {'node_modules', '.git', 'dist', 'build', '.next', 'coverage', '__pycache__'} filtered_files = [ f for f in files if f.is_file() and not any(ignored in f.parts for ignored in ignored_dirs) ] logger.info(f"Discovered {len(filtered_files)} files to format") return filtered_files def _format_file(self, file_path: Path, prettier_cmd: str, check_only: bool = False, config_path: Optional[Path] = None) -> Dict[str, Any]: """ Format a single file using Prettier. Args: file_path: Path to file to format prettier_cmd: Prettier command to use check_only: Only check formatting without modifying config_path: Optional path to config file Returns: Dict with formatting result """ cmd = prettier_cmd.split() cmd.append(str(file_path)) if check_only: cmd.append('--check') else: cmd.append('--write') if config_path: cmd.extend(['--config', str(config_path)]) try: result = subprocess.run( cmd, capture_output=True, text=True, timeout=30 ) if result.returncode == 0: if check_only and result.stdout: # File is already formatted return { 'file': str(file_path), 'status': 'already_formatted', 'ok': True } else: # File was formatted return { 'file': str(file_path), 'status': 'formatted', 'ok': True } else: # Formatting failed or file needs formatting (in check mode) if check_only and 'Code style issues' in result.stderr: return { 'file': str(file_path), 'status': 'needs_formatting', 'ok': True } else: return { 'file': str(file_path), 'status': 'error', 'ok': False, 'error': result.stderr or result.stdout } except subprocess.TimeoutExpired: logger.error(f"Timeout formatting {file_path}") return { 'file': str(file_path), 'status': 'error', 'ok': False, 'error': 'Timeout after 30 seconds' } except Exception as e: logger.error(f"Error formatting {file_path}: {e}") return { 'file': str(file_path), 'status': 'error', 'ok': False, 'error': str(e) } @certified_skill("code.format") def execute( self, path: str, config_path: Optional[str] = None, check_only: bool = False, file_patterns: Optional[str] = None, write: bool = True ) -> Dict[str, Any]: """ Execute the code formatting skill. Args: path: File or directory path to format config_path: Path to custom Prettier configuration file check_only: Only check formatting without modifying files file_patterns: Comma-separated glob patterns to filter files write: Write formatted output to files (default: True) Returns: Dict with execution results including: - ok: Overall success status - status: Status message - formatted_count: Number of files formatted - checked_count: Number of files checked - error_count: Number of files with errors - files_formatted: List of formatted file paths - files_already_formatted: List of already formatted file paths - files_with_errors: List of files that had errors """ try: logger.info(f"Executing code.format on: {path}") # Check if Prettier is installed is_installed, prettier_cmd = self._check_prettier_installed() if not is_installed: return { "ok": False, "status": "failed", "error": "Prettier is not installed. Install it with: npm install -g prettier or npm install --save-dev prettier" } # Validate path target_path = Path(path) if not target_path.exists(): return { "ok": False, "status": "failed", "error": f"Path does not exist: {path}" } # Find config file config_file = self._find_config_file(target_path, config_path) # Parse file patterns patterns = None if file_patterns: patterns = [p.strip() for p in file_patterns.split(',')] # Discover files files = self._discover_files(target_path, patterns) if not files: return { "ok": True, "status": "success", "message": "No files found to format", "formatted_count": 0, "checked_count": 0, "error_count": 0 } # Format files results = [] for file_path in files: result = self._format_file( file_path, prettier_cmd, check_only=check_only or not write, config_path=config_file ) results.append(result) # Aggregate results files_formatted = [r['file'] for r in results if r['status'] == 'formatted'] files_already_formatted = [r['file'] for r in results if r['status'] == 'already_formatted'] files_need_formatting = [r['file'] for r in results if r['status'] == 'needs_formatting'] files_with_errors = [ {'file': r['file'], 'error': r.get('error', 'Unknown error')} for r in results if r['status'] == 'error' ] response = { "ok": True, "status": "success", "formatted_count": len(files_formatted), "already_formatted_count": len(files_already_formatted), "needs_formatting_count": len(files_need_formatting), "checked_count": len(files), "error_count": len(files_with_errors), "files_formatted": files_formatted, "files_already_formatted": files_already_formatted, "files_need_formatting": files_need_formatting, "files_with_errors": files_with_errors } if check_only: response["message"] = f"Checked {len(files)} files. {len(files_need_formatting)} need formatting." else: response["message"] = f"Formatted {len(files_formatted)} files. {len(files_already_formatted)} already formatted." logger.info(f"Skill completed: {response['message']}") return response except Exception as e: logger.error(f"Error executing skill: {e}") return { "ok": False, "status": "failed", "error": str(e) } def main(): """CLI entry point""" import argparse parser = argparse.ArgumentParser( description="Format code using Prettier, supporting multiple languages and file types." ) parser.add_argument( "--path", required=True, help="File or directory path to format" ) parser.add_argument( "--config-path", help="Path to custom Prettier configuration file" ) parser.add_argument( "--check", action="store_true", help="Only check formatting without modifying files" ) parser.add_argument( "--patterns", help="Comma-separated glob patterns to filter files (e.g., '**/*.js,**/*.ts')" ) parser.add_argument( "--no-write", action="store_true", help="Don't write changes (dry run)" ) parser.add_argument( "--output-format", choices=["json", "yaml"], default="json", help="Output format" ) args = parser.parse_args() # Create skill instance skill = CodeFormat() # Execute skill result = skill.execute( path=args.path, config_path=args.config_path, check_only=args.check, file_patterns=args.patterns, write=not args.no_write ) # Output result if args.output_format == "json": print(json.dumps(result, indent=2)) else: print(yaml.dump(result, default_flow_style=False)) # Exit with appropriate code sys.exit(0 if result.get("ok") else 1) if __name__ == "__main__": main()