Initial commit
This commit is contained in:
278
skills/code.format/SKILL.md
Normal file
278
skills/code.format/SKILL.md
Normal file
@@ -0,0 +1,278 @@
|
||||
# code.format
|
||||
|
||||
Format code using Prettier, supporting multiple languages and file types. This skill can format individual files or entire directories, check formatting without making changes, and respect custom Prettier configurations.
|
||||
|
||||
## Overview
|
||||
|
||||
**Purpose:** Automatically format code using Prettier to maintain consistent code style across your project.
|
||||
|
||||
**Command:** `/code/format`
|
||||
|
||||
**Version:** 0.1.0
|
||||
|
||||
## Features
|
||||
|
||||
- Format individual files or entire directories
|
||||
- Support for 15+ file types (JavaScript, TypeScript, CSS, HTML, JSON, YAML, Markdown, and more)
|
||||
- Auto-detect Prettier configuration files (.prettierrc, prettier.config.js, etc.)
|
||||
- Check-only mode to validate formatting without modifying files
|
||||
- Custom file pattern filtering
|
||||
- Detailed formatting reports
|
||||
- Automatic discovery of local and global Prettier installations
|
||||
|
||||
## Supported File Types
|
||||
|
||||
- **JavaScript**: .js, .jsx, .mjs, .cjs
|
||||
- **TypeScript**: .ts, .tsx
|
||||
- **CSS/Styles**: .css, .scss, .less
|
||||
- **HTML**: .html, .htm
|
||||
- **JSON**: .json
|
||||
- **YAML**: .yaml, .yml
|
||||
- **Markdown**: .md, .mdx
|
||||
- **GraphQL**: .graphql, .gql
|
||||
- **Vue**: .vue
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Prettier must be installed either globally or locally in your project:
|
||||
|
||||
```bash
|
||||
# Global installation
|
||||
npm install -g prettier
|
||||
|
||||
# Or local installation (recommended)
|
||||
npm install --save-dev prettier
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Format a single file:
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py --path src/index.js
|
||||
```
|
||||
|
||||
Format an entire directory:
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py --path src/
|
||||
```
|
||||
|
||||
### Advanced Usage
|
||||
|
||||
**Check formatting without modifying files:**
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py --path src/ --check
|
||||
```
|
||||
|
||||
**Format only specific file types:**
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py --path src/ --patterns "**/*.ts,**/*.tsx"
|
||||
```
|
||||
|
||||
**Use custom Prettier configuration:**
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py --path src/ --config-path .prettierrc.custom
|
||||
```
|
||||
|
||||
**Dry run (check without writing):**
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py --path src/ --no-write
|
||||
```
|
||||
|
||||
**Output as YAML:**
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py --path src/ --output-format yaml
|
||||
```
|
||||
|
||||
## CLI Arguments
|
||||
|
||||
| Argument | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `--path` | Yes | - | File or directory path to format |
|
||||
| `--config-path` | No | Auto-detect | Path to custom Prettier configuration file |
|
||||
| `--check` | No | false | Only check formatting without modifying files |
|
||||
| `--patterns` | No | All supported | Comma-separated glob patterns (e.g., "**/*.js,**/*.ts") |
|
||||
| `--no-write` | No | false | Don't write changes (dry run mode) |
|
||||
| `--output-format` | No | json | Output format: json or yaml |
|
||||
|
||||
## Configuration
|
||||
|
||||
The skill will automatically search for Prettier configuration files in this order:
|
||||
|
||||
1. Custom config specified via `--config-path`
|
||||
2. `.prettierrc` in the target directory or parent directories
|
||||
3. `.prettierrc.json`, `.prettierrc.yml`, `.prettierrc.yaml`
|
||||
4. `.prettierrc.js`, `.prettierrc.cjs`
|
||||
5. `prettier.config.js`, `prettier.config.cjs`
|
||||
6. Prettier defaults if no config found
|
||||
|
||||
## Output Format
|
||||
|
||||
The skill returns a JSON object with detailed formatting results:
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"status": "success",
|
||||
"message": "Formatted 5 files. 3 already formatted.",
|
||||
"formatted_count": 5,
|
||||
"already_formatted_count": 3,
|
||||
"needs_formatting_count": 0,
|
||||
"checked_count": 8,
|
||||
"error_count": 0,
|
||||
"files_formatted": [
|
||||
"src/components/Header.tsx",
|
||||
"src/utils/helpers.js"
|
||||
],
|
||||
"files_already_formatted": [
|
||||
"src/index.ts",
|
||||
"src/App.tsx",
|
||||
"src/config.json"
|
||||
],
|
||||
"files_need_formatting": [],
|
||||
"files_with_errors": []
|
||||
}
|
||||
```
|
||||
|
||||
### Response Fields
|
||||
|
||||
- **ok**: Boolean indicating overall success
|
||||
- **status**: Status string ("success" or "failed")
|
||||
- **message**: Human-readable summary
|
||||
- **formatted_count**: Number of files that were formatted
|
||||
- **already_formatted_count**: Number of files that were already properly formatted
|
||||
- **needs_formatting_count**: Number of files that need formatting (check mode only)
|
||||
- **checked_count**: Total number of files processed
|
||||
- **error_count**: Number of files that encountered errors
|
||||
- **files_formatted**: List of files that were formatted
|
||||
- **files_already_formatted**: List of files that were already formatted
|
||||
- **files_need_formatting**: List of files needing formatting (check mode)
|
||||
- **files_with_errors**: List of files with errors and error messages
|
||||
|
||||
## Error Handling
|
||||
|
||||
The skill gracefully handles various error scenarios:
|
||||
|
||||
- **Prettier not installed**: Clear error message with installation instructions
|
||||
- **Invalid path**: Validation error if path doesn't exist
|
||||
- **Syntax errors**: Reports files with syntax errors without stopping
|
||||
- **Permission errors**: Reports files that couldn't be read/written
|
||||
- **Timeouts**: 30-second timeout per file with clear error reporting
|
||||
|
||||
## Integration with Agents
|
||||
|
||||
Include this skill in your agent's configuration:
|
||||
|
||||
```yaml
|
||||
name: my.agent
|
||||
version: 1.0.0
|
||||
skills_available:
|
||||
- code.format
|
||||
```
|
||||
|
||||
Then invoke it programmatically:
|
||||
|
||||
```python
|
||||
from skills.code_format.code_format import CodeFormat
|
||||
|
||||
formatter = CodeFormat()
|
||||
result = formatter.execute(
|
||||
path="src/",
|
||||
check_only=True,
|
||||
file_patterns="**/*.{ts,tsx}"
|
||||
)
|
||||
|
||||
if result["ok"]:
|
||||
print(f"Checked {result['checked_count']} files")
|
||||
print(f"{result['needs_formatting_count']} files need formatting")
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Format a React Project
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py \
|
||||
--path src/ \
|
||||
--patterns "**/*.{js,jsx,ts,tsx,css,json}"
|
||||
```
|
||||
|
||||
### Example 2: Pre-commit Check
|
||||
|
||||
```bash
|
||||
python3 skills/code.format/code_format.py \
|
||||
--path src/ \
|
||||
--check \
|
||||
--output-format json
|
||||
|
||||
# Exit code 0 if all files formatted, 1 otherwise
|
||||
```
|
||||
|
||||
### Example 3: Format Only Changed Files
|
||||
|
||||
```bash
|
||||
# Get changed files from git
|
||||
CHANGED_FILES=$(git diff --name-only --diff-filter=ACMR | grep -E '\.(js|ts|jsx|tsx)$' | tr '\n' ',')
|
||||
|
||||
# Format only those files
|
||||
python3 skills/code.format/code_format.py \
|
||||
--path . \
|
||||
--patterns "$CHANGED_FILES"
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
Run the test suite:
|
||||
|
||||
```bash
|
||||
pytest skills/code.format/test_code_format.py -v
|
||||
```
|
||||
|
||||
Run specific tests:
|
||||
|
||||
```bash
|
||||
pytest skills/code.format/test_code_format.py::TestCodeFormat::test_single_file -v
|
||||
```
|
||||
|
||||
## Permissions
|
||||
|
||||
This skill requires the following permissions:
|
||||
|
||||
- **filesystem:read** - To read files and configurations
|
||||
- **filesystem:write** - To write formatted files
|
||||
- **process:execute** - To run the Prettier command
|
||||
|
||||
## Artifact Metadata
|
||||
|
||||
**Produces:**
|
||||
- `formatting-report` (application/json) - Detailed formatting operation results
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
**Issue**: "Prettier is not installed"
|
||||
- **Solution**: Install Prettier globally (`npm install -g prettier`) or locally in your project
|
||||
|
||||
**Issue**: No files found to format
|
||||
- **Solution**: Check your file patterns and ensure files exist in the target path
|
||||
|
||||
**Issue**: Configuration file not found
|
||||
- **Solution**: Ensure your config file exists and the path is correct, or let it auto-detect
|
||||
|
||||
**Issue**: Timeout errors
|
||||
- **Solution**: Very large files may timeout (30s limit). Format them individually or increase timeout in code
|
||||
|
||||
## Created By
|
||||
|
||||
This skill was generated by **meta.skill**, the skill creator meta-agent, and enhanced with full Prettier integration.
|
||||
|
||||
---
|
||||
|
||||
*Part of the Betty Framework*
|
||||
1
skills/code.format/__init__.py
Normal file
1
skills/code.format/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
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()
|
||||
56
skills/code.format/skill.yaml
Normal file
56
skills/code.format/skill.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
name: code.format
|
||||
version: 0.1.0
|
||||
description: Format code using Prettier, supporting multiple languages and file types.
|
||||
This skill can format individual files or entire directories, check formatting without
|
||||
making changes, and respect custom Prettier configurations.
|
||||
|
||||
inputs:
|
||||
- name: path
|
||||
type: string
|
||||
required: true
|
||||
description: File or directory path to format
|
||||
- name: config_path
|
||||
type: string
|
||||
required: false
|
||||
description: Path to custom Prettier configuration file
|
||||
- name: check_only
|
||||
type: boolean
|
||||
required: false
|
||||
default: false
|
||||
description: Only check formatting without modifying files
|
||||
- name: file_patterns
|
||||
type: string
|
||||
required: false
|
||||
description: Comma-separated glob patterns to filter files (e.g., "**/*.js,**/*.ts")
|
||||
- name: write
|
||||
type: boolean
|
||||
required: false
|
||||
default: true
|
||||
description: Write formatted output to files (default true, use false for dry run)
|
||||
|
||||
outputs:
|
||||
- name: formatting_report.json
|
||||
type: application/json
|
||||
description: JSON report with formatting results, files processed, and any errors
|
||||
- name: formatted_files
|
||||
type: text/plain
|
||||
description: Updated files with proper formatting (when write=true)
|
||||
|
||||
status: active
|
||||
|
||||
permissions:
|
||||
- filesystem:read
|
||||
- filesystem:write
|
||||
- process:execute
|
||||
|
||||
entrypoints:
|
||||
- command: /code/format
|
||||
handler: code_format.py
|
||||
runtime: python
|
||||
description: Format code using Prettier
|
||||
|
||||
artifact_metadata:
|
||||
produces:
|
||||
- type: formatting-report
|
||||
format: application/json
|
||||
description: Detailed report of formatting operation results
|
||||
392
skills/code.format/test_code_format.py
Normal file
392
skills/code.format/test_code_format.py
Normal file
@@ -0,0 +1,392 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Tests for code.format skill
|
||||
|
||||
Generated by meta.skill and enhanced with comprehensive test coverage
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
|
||||
# Add parent directory to path
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "../..")))
|
||||
|
||||
from skills.code_format import code_format
|
||||
|
||||
|
||||
class TestCodeFormat:
|
||||
"""Tests for CodeFormat skill"""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test fixtures"""
|
||||
self.skill = code_format.CodeFormat()
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
def teardown_method(self):
|
||||
"""Cleanup test fixtures"""
|
||||
if os.path.exists(self.temp_dir):
|
||||
shutil.rmtree(self.temp_dir)
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test skill initializes correctly"""
|
||||
assert self.skill is not None
|
||||
assert self.skill.base_dir is not None
|
||||
assert isinstance(self.skill.SUPPORTED_EXTENSIONS, set)
|
||||
assert len(self.skill.SUPPORTED_EXTENSIONS) > 0
|
||||
assert isinstance(self.skill.CONFIG_FILES, list)
|
||||
assert len(self.skill.CONFIG_FILES) > 0
|
||||
|
||||
def test_supported_extensions(self):
|
||||
"""Test that all expected file types are supported"""
|
||||
extensions = self.skill.SUPPORTED_EXTENSIONS
|
||||
|
||||
# Check JavaScript extensions
|
||||
assert '.js' in extensions
|
||||
assert '.jsx' in extensions
|
||||
assert '.mjs' in extensions
|
||||
assert '.cjs' in extensions
|
||||
|
||||
# Check TypeScript extensions
|
||||
assert '.ts' in extensions
|
||||
assert '.tsx' in extensions
|
||||
|
||||
# Check style extensions
|
||||
assert '.css' in extensions
|
||||
assert '.scss' in extensions
|
||||
assert '.less' in extensions
|
||||
|
||||
# Check other formats
|
||||
assert '.json' in extensions
|
||||
assert '.yaml' in extensions
|
||||
assert '.md' in extensions
|
||||
assert '.html' in extensions
|
||||
|
||||
def test_check_prettier_installed_with_npx(self):
|
||||
"""Test Prettier detection via npx"""
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = Mock(returncode=0, stdout="3.0.0\n")
|
||||
|
||||
is_installed, cmd = self.skill._check_prettier_installed()
|
||||
|
||||
assert is_installed is True
|
||||
assert cmd == 'npx prettier'
|
||||
|
||||
def test_check_prettier_installed_global(self):
|
||||
"""Test Prettier detection via global installation"""
|
||||
with patch('subprocess.run') as mock_run:
|
||||
# First call (npx) fails
|
||||
mock_run.side_effect = [
|
||||
FileNotFoundError(),
|
||||
Mock(returncode=0, stdout="3.0.0\n")
|
||||
]
|
||||
|
||||
with patch('shutil.which', return_value='/usr/local/bin/prettier'):
|
||||
is_installed, cmd = self.skill._check_prettier_installed()
|
||||
|
||||
assert is_installed is True
|
||||
assert cmd == 'prettier'
|
||||
|
||||
def test_check_prettier_not_installed(self):
|
||||
"""Test when Prettier is not installed"""
|
||||
with patch('subprocess.run', side_effect=FileNotFoundError()):
|
||||
with patch('shutil.which', return_value=None):
|
||||
is_installed, cmd = self.skill._check_prettier_installed()
|
||||
|
||||
assert is_installed is False
|
||||
assert cmd is None
|
||||
|
||||
def test_find_config_file_custom(self):
|
||||
"""Test finding custom config file"""
|
||||
# Create a custom config file
|
||||
config_path = Path(self.temp_dir) / '.prettierrc.custom'
|
||||
config_path.write_text('{"semi": true}')
|
||||
|
||||
found_config = self.skill._find_config_file(
|
||||
Path(self.temp_dir),
|
||||
custom_config=str(config_path)
|
||||
)
|
||||
|
||||
assert found_config == config_path
|
||||
|
||||
def test_find_config_file_auto_detect(self):
|
||||
"""Test auto-detecting config file"""
|
||||
# Create a .prettierrc file
|
||||
config_path = Path(self.temp_dir) / '.prettierrc'
|
||||
config_path.write_text('{"semi": true}')
|
||||
|
||||
found_config = self.skill._find_config_file(Path(self.temp_dir))
|
||||
|
||||
assert found_config == config_path
|
||||
|
||||
def test_find_config_file_none(self):
|
||||
"""Test when no config file exists"""
|
||||
found_config = self.skill._find_config_file(Path(self.temp_dir))
|
||||
assert found_config is None
|
||||
|
||||
def test_discover_files_single_file(self):
|
||||
"""Test discovering a single file"""
|
||||
test_file = Path(self.temp_dir) / 'test.js'
|
||||
test_file.write_text('console.log("test");')
|
||||
|
||||
files = self.skill._discover_files(test_file)
|
||||
|
||||
assert len(files) == 1
|
||||
assert files[0] == test_file
|
||||
|
||||
def test_discover_files_directory(self):
|
||||
"""Test discovering files in a directory"""
|
||||
# Create test files
|
||||
(Path(self.temp_dir) / 'test1.js').write_text('console.log("test1");')
|
||||
(Path(self.temp_dir) / 'test2.ts').write_text('console.log("test2");')
|
||||
(Path(self.temp_dir) / 'test.txt').write_text('not supported')
|
||||
|
||||
files = self.skill._discover_files(Path(self.temp_dir))
|
||||
|
||||
assert len(files) == 2
|
||||
assert any(f.name == 'test1.js' for f in files)
|
||||
assert any(f.name == 'test2.ts' for f in files)
|
||||
|
||||
def test_discover_files_with_patterns(self):
|
||||
"""Test discovering files with glob patterns"""
|
||||
# Create test files
|
||||
(Path(self.temp_dir) / 'test1.js').write_text('console.log("test1");')
|
||||
(Path(self.temp_dir) / 'test2.ts').write_text('console.log("test2");')
|
||||
(Path(self.temp_dir) / 'test3.css').write_text('body { margin: 0; }')
|
||||
|
||||
files = self.skill._discover_files(Path(self.temp_dir), patterns=['*.js'])
|
||||
|
||||
assert len(files) == 1
|
||||
assert files[0].name == 'test1.js'
|
||||
|
||||
def test_discover_files_ignores_node_modules(self):
|
||||
"""Test that node_modules is ignored"""
|
||||
# Create node_modules directory with files
|
||||
node_modules = Path(self.temp_dir) / 'node_modules'
|
||||
node_modules.mkdir()
|
||||
(node_modules / 'test.js').write_text('console.log("test");')
|
||||
|
||||
# Create regular file
|
||||
(Path(self.temp_dir) / 'app.js').write_text('console.log("app");')
|
||||
|
||||
files = self.skill._discover_files(Path(self.temp_dir))
|
||||
|
||||
assert len(files) == 1
|
||||
assert files[0].name == 'app.js'
|
||||
|
||||
def test_format_file_success(self):
|
||||
"""Test formatting a file successfully"""
|
||||
test_file = Path(self.temp_dir) / 'test.js'
|
||||
test_file.write_text('console.log("test");')
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = Mock(returncode=0, stdout='', stderr='')
|
||||
|
||||
result = self.skill._format_file(test_file, 'prettier', check_only=False)
|
||||
|
||||
assert result['ok'] is True
|
||||
assert result['status'] == 'formatted'
|
||||
assert result['file'] == str(test_file)
|
||||
|
||||
def test_format_file_check_mode_needs_formatting(self):
|
||||
"""Test check mode when file needs formatting"""
|
||||
test_file = Path(self.temp_dir) / 'test.js'
|
||||
test_file.write_text('console.log("test");')
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = Mock(
|
||||
returncode=1,
|
||||
stdout='',
|
||||
stderr='Code style issues found'
|
||||
)
|
||||
|
||||
result = self.skill._format_file(test_file, 'prettier', check_only=True)
|
||||
|
||||
assert result['ok'] is True
|
||||
assert result['status'] == 'needs_formatting'
|
||||
|
||||
def test_format_file_error(self):
|
||||
"""Test formatting with error"""
|
||||
test_file = Path(self.temp_dir) / 'test.js'
|
||||
test_file.write_text('invalid syntax {{{')
|
||||
|
||||
with patch('subprocess.run') as mock_run:
|
||||
mock_run.return_value = Mock(
|
||||
returncode=1,
|
||||
stdout='',
|
||||
stderr='Syntax error'
|
||||
)
|
||||
|
||||
result = self.skill._format_file(test_file, 'prettier', check_only=False)
|
||||
|
||||
assert result['ok'] is False
|
||||
assert result['status'] == 'error'
|
||||
assert 'error' in result
|
||||
|
||||
def test_execute_prettier_not_installed(self):
|
||||
"""Test execute when Prettier is not installed"""
|
||||
with patch.object(self.skill, '_check_prettier_installed', return_value=(False, None)):
|
||||
result = self.skill.execute(path=self.temp_dir)
|
||||
|
||||
assert result['ok'] is False
|
||||
assert result['status'] == 'failed'
|
||||
assert 'not installed' in result['error'].lower()
|
||||
|
||||
def test_execute_invalid_path(self):
|
||||
"""Test execute with invalid path"""
|
||||
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
|
||||
result = self.skill.execute(path='/nonexistent/path')
|
||||
|
||||
assert result['ok'] is False
|
||||
assert result['status'] == 'failed'
|
||||
assert 'does not exist' in result['error'].lower()
|
||||
|
||||
def test_execute_no_files(self):
|
||||
"""Test execute when no files are found"""
|
||||
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
|
||||
with patch.object(self.skill, '_discover_files', return_value=[]):
|
||||
result = self.skill.execute(path=self.temp_dir)
|
||||
|
||||
assert result['ok'] is True
|
||||
assert result['status'] == 'success'
|
||||
assert result['formatted_count'] == 0
|
||||
assert 'No files found' in result['message']
|
||||
|
||||
def test_execute_successful_formatting(self):
|
||||
"""Test successful formatting execution"""
|
||||
# Create test files
|
||||
test_file = Path(self.temp_dir) / 'test.js'
|
||||
test_file.write_text('console.log("test");')
|
||||
|
||||
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
|
||||
with patch.object(self.skill, '_format_file') as mock_format:
|
||||
mock_format.return_value = {
|
||||
'ok': True,
|
||||
'status': 'formatted',
|
||||
'file': str(test_file)
|
||||
}
|
||||
|
||||
result = self.skill.execute(path=str(test_file))
|
||||
|
||||
assert result['ok'] is True
|
||||
assert result['status'] == 'success'
|
||||
assert result['formatted_count'] == 1
|
||||
assert result['error_count'] == 0
|
||||
|
||||
def test_execute_check_mode(self):
|
||||
"""Test execute in check mode"""
|
||||
test_file = Path(self.temp_dir) / 'test.js'
|
||||
test_file.write_text('console.log("test");')
|
||||
|
||||
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
|
||||
with patch.object(self.skill, '_format_file') as mock_format:
|
||||
mock_format.return_value = {
|
||||
'ok': True,
|
||||
'status': 'needs_formatting',
|
||||
'file': str(test_file)
|
||||
}
|
||||
|
||||
result = self.skill.execute(path=str(test_file), check_only=True)
|
||||
|
||||
assert result['ok'] is True
|
||||
assert result['status'] == 'success'
|
||||
assert result['needs_formatting_count'] == 1
|
||||
assert 'need formatting' in result['message'].lower()
|
||||
|
||||
def test_execute_with_patterns(self):
|
||||
"""Test execute with file patterns"""
|
||||
# Create test files
|
||||
(Path(self.temp_dir) / 'test.js').write_text('console.log("test");')
|
||||
(Path(self.temp_dir) / 'test.ts').write_text('console.log("test");')
|
||||
|
||||
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
|
||||
with patch.object(self.skill, '_discover_files') as mock_discover:
|
||||
mock_discover.return_value = [Path(self.temp_dir) / 'test.js']
|
||||
|
||||
result = self.skill.execute(
|
||||
path=self.temp_dir,
|
||||
file_patterns='*.js'
|
||||
)
|
||||
|
||||
# Verify patterns were parsed correctly
|
||||
mock_discover.assert_called_once()
|
||||
call_args = mock_discover.call_args
|
||||
assert call_args[0][1] == ['*.js']
|
||||
|
||||
def test_execute_with_errors(self):
|
||||
"""Test execute when some files have errors"""
|
||||
test_file = Path(self.temp_dir) / 'test.js'
|
||||
test_file.write_text('invalid')
|
||||
|
||||
with patch.object(self.skill, '_check_prettier_installed', return_value=(True, 'prettier')):
|
||||
with patch.object(self.skill, '_format_file') as mock_format:
|
||||
mock_format.return_value = {
|
||||
'ok': False,
|
||||
'status': 'error',
|
||||
'file': str(test_file),
|
||||
'error': 'Syntax error'
|
||||
}
|
||||
|
||||
result = self.skill.execute(path=str(test_file))
|
||||
|
||||
assert result['ok'] is True # Overall success even with file errors
|
||||
assert result['status'] == 'success'
|
||||
assert result['error_count'] == 1
|
||||
assert len(result['files_with_errors']) == 1
|
||||
|
||||
def test_execute_exception_handling(self):
|
||||
"""Test execute handles exceptions gracefully"""
|
||||
with patch.object(self.skill, '_check_prettier_installed', side_effect=Exception('Test error')):
|
||||
result = self.skill.execute(path=self.temp_dir)
|
||||
|
||||
assert result['ok'] is False
|
||||
assert result['status'] == 'failed'
|
||||
assert 'error' in result
|
||||
|
||||
|
||||
def test_cli_help(capsys):
|
||||
"""Test CLI help message"""
|
||||
sys.argv = ["code_format.py", "--help"]
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
code_format.main()
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
captured = capsys.readouterr()
|
||||
assert "Format code using Prettier" in captured.out
|
||||
|
||||
|
||||
def test_cli_missing_path(capsys):
|
||||
"""Test CLI with missing required path argument"""
|
||||
sys.argv = ["code_format.py"]
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
code_format.main()
|
||||
|
||||
assert exc_info.value.code != 0
|
||||
|
||||
|
||||
def test_cli_execution():
|
||||
"""Test CLI execution with mocked skill"""
|
||||
sys.argv = ["code_format.py", "--path", "/tmp", "--check"]
|
||||
|
||||
with patch.object(code_format.CodeFormat, 'execute') as mock_execute:
|
||||
mock_execute.return_value = {
|
||||
'ok': True,
|
||||
'status': 'success',
|
||||
'message': 'Test'
|
||||
}
|
||||
|
||||
with pytest.raises(SystemExit) as exc_info:
|
||||
code_format.main()
|
||||
|
||||
assert exc_info.value.code == 0
|
||||
mock_execute.assert_called_once()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v"])
|
||||
Reference in New Issue
Block a user