435 lines
14 KiB
Python
Executable File
435 lines
14 KiB
Python
Executable File
#!/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()
|