Initial commit
This commit is contained in:
434
skills/code.format/code_format.py
Executable file
434
skills/code.format/code_format.py
Executable file
@@ -0,0 +1,434 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user