initial project setup: added plugin skill
This commit is contained in:
@@ -0,0 +1,438 @@
|
||||
# Hook Development Patterns
|
||||
|
||||
Comprehensive guide for creating event-driven automation with hooks.
|
||||
|
||||
## Hook Configuration
|
||||
|
||||
### Basic Structure
|
||||
```json
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.py",
|
||||
"script": "hooks/python_linter.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Available Events
|
||||
|
||||
### file-changed
|
||||
Triggered when files are modified in the workspace.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.{js,jsx,ts,tsx}",
|
||||
"script": "hooks/format_code.sh",
|
||||
"description": "Format JavaScript/TypeScript files"
|
||||
}
|
||||
```
|
||||
|
||||
**Event Data**:
|
||||
- `CHANGED_FILE`: Path to modified file
|
||||
- `FILE_EXTENSION`: File extension
|
||||
- `PROJECT_ROOT`: Project root directory
|
||||
|
||||
### git-commit-msg-needed
|
||||
Triggered when creating git commits.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "git-commit-msg-needed",
|
||||
"script": "hooks/generate_commit_msg.py",
|
||||
"description": "Generate conventional commit messages"
|
||||
}
|
||||
```
|
||||
|
||||
**Event Data**:
|
||||
- `STAGED_FILES`: List of staged files
|
||||
- `DIFF_CONTENT`: Git diff content
|
||||
- `BRANCH_NAME`: Current branch
|
||||
|
||||
### task-completed
|
||||
Triggered after task completion.
|
||||
|
||||
```json
|
||||
{
|
||||
"event": "task-completed",
|
||||
"pattern": "*deploy*",
|
||||
"script": "hooks/notify_team.sh",
|
||||
"description": "Notify team of deployment completion"
|
||||
}
|
||||
```
|
||||
|
||||
**Event Data**:
|
||||
- `TASK_NAME`: Completed task name
|
||||
- `TASK_DURATION`: Execution time
|
||||
- `TASK_STATUS`: Success/failure
|
||||
|
||||
## Pattern Matching
|
||||
|
||||
### Glob Patterns
|
||||
```json
|
||||
{
|
||||
"pattern": "**/*.py", // All Python files
|
||||
"pattern": "src/**/*.test.js", // Test files in src
|
||||
"pattern": "*.config.{js,json}", // Config files
|
||||
"pattern": "!**/*.min.js" // Exclude minified
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Patterns
|
||||
```json
|
||||
{
|
||||
"event": "file-changed",
|
||||
"patterns": [
|
||||
"**/*.py",
|
||||
"**/*.pyi"
|
||||
],
|
||||
"script": "hooks/python_type_check.sh"
|
||||
}
|
||||
```
|
||||
|
||||
### Regex Patterns
|
||||
```json
|
||||
{
|
||||
"event": "task-completed",
|
||||
"pattern_type": "regex",
|
||||
"pattern": "^deploy-.*-prod$",
|
||||
"script": "hooks/production_monitor.sh"
|
||||
}
|
||||
```
|
||||
|
||||
## Script Implementation
|
||||
|
||||
### Shell Script Example
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# hooks/format_code.sh
|
||||
|
||||
set -e
|
||||
|
||||
# Access event data
|
||||
FILE_PATH="$CHANGED_FILE"
|
||||
EXTENSION="$FILE_EXTENSION"
|
||||
|
||||
# Format based on file type
|
||||
case "$EXTENSION" in
|
||||
js|jsx|ts|tsx)
|
||||
npx prettier --write "$FILE_PATH"
|
||||
;;
|
||||
py)
|
||||
black "$FILE_PATH"
|
||||
;;
|
||||
go)
|
||||
gofmt -w "$FILE_PATH"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Formatted: $FILE_PATH"
|
||||
```
|
||||
|
||||
### Python Script Example
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
# hooks/generate_commit_msg.py
|
||||
|
||||
import os
|
||||
import json
|
||||
import subprocess
|
||||
|
||||
def generate_commit_message():
|
||||
staged_files = os.environ.get('STAGED_FILES', '').split('\n')
|
||||
diff_content = os.environ.get('DIFF_CONTENT', '')
|
||||
|
||||
# Analyze changes
|
||||
file_types = set()
|
||||
for file in staged_files:
|
||||
if file.endswith('.py'):
|
||||
file_types.add('python')
|
||||
elif file.endswith(('.js', '.tsx')):
|
||||
file_types.add('frontend')
|
||||
|
||||
# Generate message
|
||||
if 'python' in file_types and 'frontend' in file_types:
|
||||
prefix = 'feat(fullstack):'
|
||||
elif 'python' in file_types:
|
||||
prefix = 'feat(backend):'
|
||||
else:
|
||||
prefix = 'feat(frontend):'
|
||||
|
||||
return f"{prefix} Update {len(staged_files)} files"
|
||||
|
||||
if __name__ == "__main__":
|
||||
message = generate_commit_message()
|
||||
print(message)
|
||||
```
|
||||
|
||||
### Node.js Script Example
|
||||
```javascript
|
||||
#!/usr/bin/env node
|
||||
// hooks/validate_json.js
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const filePath = process.env.CHANGED_FILE;
|
||||
|
||||
if (filePath.endsWith('.json')) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
JSON.parse(content);
|
||||
console.log(`✓ Valid JSON: ${path.basename(filePath)}`);
|
||||
} catch (error) {
|
||||
console.error(`✗ Invalid JSON: ${filePath}`);
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Conditional Execution
|
||||
```json
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.py",
|
||||
"script": "hooks/conditional_lint.sh",
|
||||
"conditions": {
|
||||
"branch_pattern": "feature/*",
|
||||
"skip_patterns": ["**/migrations/*"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Chained Hooks
|
||||
```json
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.py",
|
||||
"script": "hooks/format.sh",
|
||||
"order": 1
|
||||
},
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.py",
|
||||
"script": "hooks/lint.sh",
|
||||
"order": 2
|
||||
},
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.py",
|
||||
"script": "hooks/type_check.sh",
|
||||
"order": 3
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Debouncing
|
||||
```json
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.scss",
|
||||
"script": "hooks/compile_sass.sh",
|
||||
"debounce": 1000,
|
||||
"description": "Compile SASS after 1s of no changes"
|
||||
}
|
||||
```
|
||||
|
||||
## Security Best Practices
|
||||
|
||||
### Input Validation
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Validate file paths
|
||||
|
||||
FILE="$CHANGED_FILE"
|
||||
|
||||
# Check for directory traversal
|
||||
if [[ "$FILE" == *".."* ]]; then
|
||||
echo "Error: Invalid file path"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify file exists in project
|
||||
if [[ ! "$FILE" == "$PROJECT_ROOT"* ]]; then
|
||||
echo "Error: File outside project"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
### Safe Command Execution
|
||||
```python
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import shlex
|
||||
import os
|
||||
|
||||
def run_command_safely(cmd, file_path):
|
||||
# Sanitize file path
|
||||
safe_path = shlex.quote(file_path)
|
||||
|
||||
# Use subprocess safely
|
||||
result = subprocess.run(
|
||||
[cmd, safe_path],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30
|
||||
)
|
||||
|
||||
return result.returncode == 0
|
||||
```
|
||||
|
||||
### Environment Variable Safety
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Sanitize environment variables
|
||||
|
||||
# Remove potentially dangerous characters
|
||||
SAFE_BRANCH=$(echo "$BRANCH_NAME" | tr -cd '[:alnum:]/_-')
|
||||
|
||||
# Validate expected format
|
||||
if [[ ! "$SAFE_BRANCH" =~ ^[a-zA-Z0-9/_-]+$ ]]; then
|
||||
echo "Error: Invalid branch name"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### Async Processing
|
||||
```javascript
|
||||
// hooks/async_process.js
|
||||
const { Worker } = require('worker_threads');
|
||||
|
||||
async function processFileAsync(filePath) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const worker = new Worker('./heavy_process.js', {
|
||||
workerData: { filePath }
|
||||
});
|
||||
|
||||
worker.on('message', resolve);
|
||||
worker.on('error', reject);
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
### Caching
|
||||
```python
|
||||
# hooks/cached_analysis.py
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
|
||||
CACHE_DIR = os.path.join(os.environ['CLAUDE_PLUGIN_ROOT'], '.cache')
|
||||
|
||||
def get_file_hash(filepath):
|
||||
with open(filepath, 'rb') as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
def get_cached_result(filepath):
|
||||
file_hash = get_file_hash(filepath)
|
||||
cache_file = os.path.join(CACHE_DIR, f"{file_hash}.json")
|
||||
|
||||
if os.path.exists(cache_file):
|
||||
with open(cache_file, 'r') as f:
|
||||
return json.load(f)
|
||||
return None
|
||||
```
|
||||
|
||||
### Batch Processing
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# Process multiple files efficiently
|
||||
|
||||
# Collect files for batch processing
|
||||
CHANGED_FILES=()
|
||||
while read -r file; do
|
||||
CHANGED_FILES+=("$file")
|
||||
done < <(echo "$STAGED_FILES")
|
||||
|
||||
# Process in batch
|
||||
if [ ${#CHANGED_FILES[@]} -gt 10 ]; then
|
||||
echo "Batch processing ${#CHANGED_FILES[@]} files..."
|
||||
prettier --write "${CHANGED_FILES[@]}"
|
||||
else
|
||||
# Process individually for small batches
|
||||
for file in "${CHANGED_FILES[@]}"; do
|
||||
prettier --write "$file"
|
||||
done
|
||||
fi
|
||||
```
|
||||
|
||||
## Testing Hooks
|
||||
|
||||
### Unit Testing
|
||||
```python
|
||||
# tests/test_hook.py
|
||||
import unittest
|
||||
import os
|
||||
from hooks import generate_commit_msg
|
||||
|
||||
class TestCommitMsgHook(unittest.TestCase):
|
||||
def test_python_files(self):
|
||||
os.environ['STAGED_FILES'] = 'app.py\ntest.py'
|
||||
msg = generate_commit_msg.generate()
|
||||
self.assertIn('backend', msg)
|
||||
```
|
||||
|
||||
### Integration Testing
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# test_hooks.sh
|
||||
|
||||
# Test file-changed hook
|
||||
echo "Testing file-changed hook..."
|
||||
touch test.py
|
||||
CHANGED_FILE="test.py" FILE_EXTENSION="py" ./hooks/format_code.sh
|
||||
|
||||
# Verify result
|
||||
if black --check test.py; then
|
||||
echo "✓ Hook executed successfully"
|
||||
else
|
||||
echo "✗ Hook failed"
|
||||
exit 1
|
||||
fi
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Debug Mode
|
||||
```json
|
||||
{
|
||||
"event": "file-changed",
|
||||
"pattern": "**/*.py",
|
||||
"script": "hooks/lint.sh",
|
||||
"debug": true,
|
||||
"log_output": true
|
||||
}
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Hook not firing**
|
||||
- Check pattern matches
|
||||
- Verify event name
|
||||
- Ensure script is executable
|
||||
|
||||
2. **Script errors**
|
||||
- Add error handling
|
||||
- Check environment variables
|
||||
- Verify dependencies
|
||||
|
||||
3. **Performance issues**
|
||||
- Add debouncing
|
||||
- Implement caching
|
||||
- Use async processing
|
||||
|
||||
4. **Security warnings**
|
||||
- Validate all inputs
|
||||
- Use safe command execution
|
||||
- Restrict file access
|
||||
Reference in New Issue
Block a user