Initial commit
This commit is contained in:
504
skills/plugin.sync/SKILL.md
Normal file
504
skills/plugin.sync/SKILL.md
Normal file
@@ -0,0 +1,504 @@
|
||||
---
|
||||
name: Plugin Sync
|
||||
description: Automatically generate plugin.yaml from Betty Framework registries
|
||||
---
|
||||
|
||||
# plugin.sync
|
||||
|
||||
## Overview
|
||||
|
||||
**plugin.sync** is the synchronization tool that generates `plugin.yaml` from Betty Framework's registry files. It ensures that Claude Code's plugin configuration stays in sync with registered skills, commands, and hooks.
|
||||
|
||||
## Purpose
|
||||
|
||||
Automates the generation of `plugin.yaml` to maintain consistency between:
|
||||
- **Skill Registry** (`registry/skills.json`) – Active skills with entrypoints
|
||||
- **Command Registry** (`registry/commands.json`) – Slash commands
|
||||
- **Hook Registry** (`registry/hooks.json`) – Event-driven hooks
|
||||
- **Plugin Configuration** (`plugin.yaml`) – Claude Code plugin manifest
|
||||
|
||||
This eliminates manual editing of `plugin.yaml` and prevents drift between what's registered and what's exposed to Claude Code.
|
||||
|
||||
## What It Does
|
||||
|
||||
1. **Reads Registries**: Loads `skills.json`, `commands.json`, and `hooks.json`
|
||||
2. **Filters Active Skills**: Processes only skills with `status: active` and defined entrypoints
|
||||
3. **Validates Handlers**: Checks if handler files exist on disk
|
||||
4. **Generates Commands**: Converts skill entrypoints to `plugin.yaml` command format
|
||||
5. **Preserves Metadata**: Maintains existing plugin metadata (author, license, etc.)
|
||||
6. **Writes plugin.yaml**: Outputs formatted plugin configuration to repo root
|
||||
7. **Reports Issues**: Warns about missing handlers or inconsistencies
|
||||
|
||||
## Usage
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```bash
|
||||
python skills/plugin.sync/plugin_sync.py
|
||||
```
|
||||
|
||||
No arguments required - reads from standard registry locations.
|
||||
|
||||
### Via Betty CLI
|
||||
|
||||
```bash
|
||||
/plugin/sync
|
||||
```
|
||||
|
||||
### Expected Registry Structure
|
||||
|
||||
The skill expects these registry files:
|
||||
|
||||
```
|
||||
betty/
|
||||
├── registry/
|
||||
│ ├── skills.json # Registered skills
|
||||
│ ├── commands.json # Registered commands
|
||||
│ └── hooks.json # Registered hooks
|
||||
└── plugin.yaml # Generated output
|
||||
```
|
||||
|
||||
## Behavior
|
||||
|
||||
### 1. Registry Loading
|
||||
|
||||
Reads JSON files from:
|
||||
- `registry/skills.json`
|
||||
- `registry/commands.json`
|
||||
- `registry/hooks.json`
|
||||
|
||||
If a registry file is missing, logs a warning and continues with available data.
|
||||
|
||||
### 2. Skill Processing
|
||||
|
||||
For each skill in `skills.json`:
|
||||
- **Checks status**: Only processes skills with `status: active`
|
||||
- **Looks for entrypoints**: Requires at least one entrypoint definition
|
||||
- **Validates handler**: Checks if handler file exists at `skills/{skill_name}/{handler}`
|
||||
- **Converts format**: Maps skill entrypoint to plugin command schema
|
||||
|
||||
### 3. Command Generation
|
||||
|
||||
Creates a command entry for each active skill entrypoint:
|
||||
|
||||
```yaml
|
||||
- name: skill/validate
|
||||
description: Validate a skill manifest
|
||||
handler:
|
||||
runtime: python
|
||||
script: skills/skill.define/skill_define.py
|
||||
parameters:
|
||||
- name: manifest_path
|
||||
type: string
|
||||
required: true
|
||||
description: Path to skill.yaml file
|
||||
permissions:
|
||||
- filesystem:read
|
||||
- filesystem:write
|
||||
```
|
||||
|
||||
### 4. Handler Validation
|
||||
|
||||
For each handler reference:
|
||||
- Constructs full path: `skills/{skill_name}/{handler_filename}`
|
||||
- Checks file existence on disk
|
||||
- Logs warning if handler file is missing
|
||||
|
||||
### 5. Plugin Generation
|
||||
|
||||
Preserves existing `plugin.yaml` metadata:
|
||||
- Plugin name, version, description
|
||||
- Author information
|
||||
- License
|
||||
- Requirements
|
||||
- Permissions
|
||||
- Config sections
|
||||
|
||||
Replaces the `commands` section with generated entries.
|
||||
|
||||
### 6. Output Writing
|
||||
|
||||
Writes `plugin.yaml` with:
|
||||
- Auto-generated header comment
|
||||
- Properly formatted YAML (2-space indent)
|
||||
- Generation timestamp in metadata
|
||||
- Skill and command counts
|
||||
|
||||
## Outputs
|
||||
|
||||
### Success Response
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"status": "success",
|
||||
"output_path": "/home/user/betty/plugin.yaml",
|
||||
"commands_generated": 18,
|
||||
"warnings": []
|
||||
}
|
||||
```
|
||||
|
||||
### Response with Warnings
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"status": "success",
|
||||
"output_path": "/home/user/betty/plugin.yaml",
|
||||
"commands_generated": 18,
|
||||
"warnings": [
|
||||
"Handler not found for 'api.validate': skills/api.validate/api_validate_missing.py",
|
||||
"Skill 'test.broken' has entrypoint without handler"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Failure Response
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": false,
|
||||
"status": "failed",
|
||||
"error": "Failed to parse JSON from registry/skills.json: Expecting value: line 1 column 1"
|
||||
}
|
||||
```
|
||||
|
||||
## Generated plugin.yaml Structure
|
||||
|
||||
The skill generates a `plugin.yaml` like this:
|
||||
|
||||
```yaml
|
||||
# Betty Framework - Claude Code Plugin
|
||||
# Auto-generated by plugin.sync skill
|
||||
# DO NOT EDIT MANUALLY - Run plugin.sync to regenerate
|
||||
|
||||
name: betty-framework
|
||||
version: 1.0.0
|
||||
description: Betty Framework - Structured AI-assisted engineering
|
||||
author:
|
||||
name: RiskExec
|
||||
email: platform@riskexec.com
|
||||
url: https://github.com/epieczko/betty
|
||||
license: MIT
|
||||
|
||||
metadata:
|
||||
homepage: https://github.com/epieczko/betty
|
||||
repository: https://github.com/epieczko/betty
|
||||
generated_at: "2025-10-23T17:45:00.123456+00:00"
|
||||
generated_by: plugin.sync skill
|
||||
skill_count: 18
|
||||
command_count: 18
|
||||
|
||||
requirements:
|
||||
python: ">=3.11"
|
||||
packages:
|
||||
- pyyaml
|
||||
|
||||
permissions:
|
||||
- filesystem:read
|
||||
- filesystem:write
|
||||
- process:execute
|
||||
|
||||
commands:
|
||||
- name: workflow/validate
|
||||
description: Validates Betty workflow YAML definitions
|
||||
handler:
|
||||
runtime: python
|
||||
script: skills/workflow.validate/workflow_validate.py
|
||||
parameters:
|
||||
- name: workflow.yaml
|
||||
type: string
|
||||
required: true
|
||||
description: Path to the workflow YAML file
|
||||
permissions:
|
||||
- filesystem
|
||||
- read
|
||||
|
||||
- name: skill/define
|
||||
description: Validate a Claude Code skill manifest
|
||||
handler:
|
||||
runtime: python
|
||||
script: skills/skill.define/skill_define.py
|
||||
parameters:
|
||||
- name: manifest_path
|
||||
type: string
|
||||
required: true
|
||||
description: Path to the skill.yaml file
|
||||
permissions:
|
||||
- filesystem
|
||||
- read
|
||||
- write
|
||||
|
||||
# ... more commands ...
|
||||
```
|
||||
|
||||
## Validation and Warnings
|
||||
|
||||
### Handler Existence Check
|
||||
|
||||
For each skill entrypoint, the skill checks:
|
||||
|
||||
```python
|
||||
full_path = f"skills/{skill_name}/{handler_filename}"
|
||||
if not os.path.exists(full_path):
|
||||
warnings.append(f"Handler not found: {full_path}")
|
||||
```
|
||||
|
||||
### Common Warnings
|
||||
|
||||
| Warning | Meaning | Action |
|
||||
|---------|---------|--------|
|
||||
| `Handler not found for 'X'` | Handler file missing from disk | Create the handler or fix the path in skill.yaml |
|
||||
| `Skill 'X' has entrypoint without handler` | Entrypoint missing `handler` field | Add handler field to entrypoint definition |
|
||||
| `Registry file not found` | Registry JSON is missing | Run registry update or check file paths |
|
||||
|
||||
## Examples
|
||||
|
||||
### Example 1: Full Sync After Adding New Skills
|
||||
|
||||
**Scenario**: You've added several new skills and want to sync them to `plugin.yaml`
|
||||
|
||||
```bash
|
||||
# Create skills using skill.create
|
||||
/skill/create data.transform "Transform data between formats"
|
||||
/skill/create api.monitor "Monitor API health" --inputs=endpoint --outputs=status
|
||||
|
||||
# Define skills (validates and registers)
|
||||
/skill/define skills/data.transform/skill.yaml
|
||||
/skill/define skills/api.monitor/skill.yaml
|
||||
|
||||
# Sync to plugin.yaml
|
||||
/plugin/sync
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
INFO: Starting plugin.yaml generation from registries...
|
||||
INFO: Loading registry files...
|
||||
INFO: Generating plugin.yaml configuration...
|
||||
INFO: Added command: /data/transform from skill data.transform
|
||||
INFO: Added command: /api/monitor from skill api.monitor
|
||||
INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml
|
||||
INFO: ✅ Generated 20 commands
|
||||
INFO: 📄 Output: /home/user/betty/plugin.yaml
|
||||
```
|
||||
|
||||
### Example 2: Detecting Missing Handlers
|
||||
|
||||
**Scenario**: A skill's handler file was moved or deleted
|
||||
|
||||
```bash
|
||||
# Remove a handler file
|
||||
rm skills/api.validate/api_validate.py
|
||||
|
||||
# Run sync
|
||||
/plugin/sync
|
||||
```
|
||||
|
||||
**Output**:
|
||||
```
|
||||
INFO: Starting plugin.yaml generation from registries...
|
||||
INFO: Loading registry files...
|
||||
INFO: Generating plugin.yaml configuration...
|
||||
INFO: Added command: /skill/define from skill skill.define
|
||||
WARNING: Handler not found for 'api.validate': skills/api.validate/api_validate.py
|
||||
INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml
|
||||
INFO: ⚠️ Warnings during generation:
|
||||
INFO: - Handler not found for 'api.validate': skills/api.validate/api_validate.py
|
||||
INFO: ✅ Generated 19 commands
|
||||
```
|
||||
|
||||
### Example 3: Initial Plugin Setup
|
||||
|
||||
**Scenario**: Setting up Betty Framework plugin for the first time
|
||||
|
||||
```bash
|
||||
# Initialize registry if needed
|
||||
python -c "from betty.config import ensure_directories; ensure_directories()"
|
||||
|
||||
# Register all existing skills
|
||||
for skill in skills/*/skill.yaml; do
|
||||
/skill/define "$skill"
|
||||
done
|
||||
|
||||
# Generate plugin.yaml
|
||||
/plugin/sync
|
||||
```
|
||||
|
||||
This creates a complete `plugin.yaml` from all registered active skills.
|
||||
|
||||
## Integration
|
||||
|
||||
### With skill.define
|
||||
|
||||
After defining a skill, sync the plugin:
|
||||
|
||||
```bash
|
||||
/skill/define skills/my.skill/skill.yaml
|
||||
/plugin/sync
|
||||
```
|
||||
|
||||
### With Workflows
|
||||
|
||||
Include plugin sync as a workflow step:
|
||||
|
||||
```yaml
|
||||
# workflows/skill_lifecycle.yaml
|
||||
steps:
|
||||
- skill: skill.create
|
||||
args: ["new.skill", "Description"]
|
||||
|
||||
- skill: skill.define
|
||||
args: ["skills/new.skill/skill.yaml"]
|
||||
|
||||
- skill: plugin.sync
|
||||
args: []
|
||||
```
|
||||
|
||||
### With Hooks
|
||||
|
||||
Auto-sync when skills are updated:
|
||||
|
||||
```yaml
|
||||
# .claude/hooks.yaml
|
||||
- event: on_file_save
|
||||
pattern: "skills/*/skill.yaml"
|
||||
command: python skills/skill.define/skill_define.py {file_path} && python skills/plugin.sync/plugin_sync.py
|
||||
blocking: false
|
||||
description: Auto-sync plugin.yaml when skills change
|
||||
```
|
||||
|
||||
## What Gets Included
|
||||
|
||||
### ✅ Included in plugin.yaml
|
||||
|
||||
- Skills with `status: active`
|
||||
- Skills with at least one `entrypoint` defined
|
||||
- All entrypoint parameters and permissions
|
||||
- Skill descriptions and metadata
|
||||
|
||||
### ❌ Not Included
|
||||
|
||||
- Skills with `status: draft`
|
||||
- Skills without entrypoints
|
||||
- Skills marked as internal-only
|
||||
- Test skills (unless marked active)
|
||||
|
||||
## Common Errors
|
||||
|
||||
| Error | Cause | Solution |
|
||||
|-------|-------|----------|
|
||||
| "Failed to parse JSON" | Invalid JSON in registry file | Fix JSON syntax in the registry |
|
||||
| "Registry file not found" | Missing registry file | Ensure registries exist in `registry/` dir |
|
||||
| "Permission denied" | Cannot write plugin.yaml | Check file permissions on plugin.yaml |
|
||||
| All commands missing | No active skills | Mark skills as active in registry |
|
||||
|
||||
## Files Read
|
||||
|
||||
- `registry/skills.json` – Skill registry
|
||||
- `registry/commands.json` – Command registry (future use)
|
||||
- `registry/hooks.json` – Hook registry (future use)
|
||||
- `plugin.yaml` – Existing plugin config (for metadata preservation)
|
||||
- `skills/*/` – Handler file validation
|
||||
|
||||
## Files Modified
|
||||
|
||||
- `plugin.yaml` – Overwritten with generated configuration
|
||||
|
||||
## Exit Codes
|
||||
|
||||
- **0**: Success (plugin.yaml generated successfully)
|
||||
- **1**: Failure (error during generation)
|
||||
|
||||
## Logging
|
||||
|
||||
Logs generation progress:
|
||||
|
||||
```
|
||||
INFO: Starting plugin.yaml generation from registries...
|
||||
INFO: Loading registry files...
|
||||
INFO: Loaded existing plugin.yaml as template
|
||||
INFO: Generating plugin.yaml configuration...
|
||||
INFO: Added command: /skill/create from skill skill.create
|
||||
INFO: Added command: /skill/define from skill skill.define
|
||||
INFO: Added command: /agent/define from skill agent.define
|
||||
INFO: ✅ Written plugin.yaml to /home/user/betty/plugin.yaml
|
||||
INFO: ✅ Generated 18 commands
|
||||
INFO: 📄 Output: /home/user/betty/plugin.yaml
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Run After Registry Changes**: Sync after adding, updating, or removing skills
|
||||
2. **Include in CI/CD**: Add plugin sync to your deployment pipeline
|
||||
3. **Review Before Commit**: Check generated plugin.yaml before committing
|
||||
4. **Keep Registries Clean**: Remove inactive skills to keep plugin.yaml focused
|
||||
5. **Use Hooks**: Set up auto-sync hooks for convenience
|
||||
6. **Version Control**: Always commit plugin.yaml changes with skill changes
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Plugin.yaml Not Updating
|
||||
|
||||
**Problem**: Changes to skills.json don't appear in plugin.yaml
|
||||
|
||||
**Solutions**:
|
||||
- Ensure skill status is `active`
|
||||
- Check that skill has `entrypoints` defined
|
||||
- Verify entrypoint has `command` and `handler` fields
|
||||
- Run `/skill/define` before `/plugin/sync`
|
||||
|
||||
### Handler Not Found Warnings
|
||||
|
||||
**Problem**: Warnings about missing handler files
|
||||
|
||||
**Solutions**:
|
||||
- Check handler path in skill.yaml
|
||||
- Ensure handler file exists at `skills/{skill_name}/{handler_filename}`
|
||||
- Verify file permissions
|
||||
- Update skill.yaml if handler was renamed
|
||||
|
||||
### Commands Not Appearing
|
||||
|
||||
**Problem**: Active skill not generating command
|
||||
|
||||
**Solutions**:
|
||||
- Verify skill has `entrypoints` array in skill.yaml
|
||||
- Check entrypoint has `command` field (e.g., `/skill/validate`)
|
||||
- Ensure `handler` field points to correct file
|
||||
- Check that skill.yaml is in skills registry
|
||||
|
||||
## Architecture
|
||||
|
||||
### Skill Categories
|
||||
|
||||
**Infrastructure** – Plugin.sync maintains the foundation layer by syncing registry state to plugin configuration.
|
||||
|
||||
### Design Principles
|
||||
|
||||
- **Single Source of Truth**: Registry files are the source of truth
|
||||
- **Idempotent**: Can be run multiple times safely
|
||||
- **Validation First**: Checks handlers before generating config
|
||||
- **Preserve Metadata**: Keeps existing plugin metadata intact
|
||||
- **Clear Reporting**: Detailed warnings about issues
|
||||
|
||||
## See Also
|
||||
|
||||
- **skill.define** – Validate and register skills ([SKILL.md](../skill.define/SKILL.md))
|
||||
- **registry.update** – Update skill registry ([SKILL.md](../registry.update/SKILL.md))
|
||||
- **skill.create** – Create new skills ([SKILL.md](../skill.create/SKILL.md))
|
||||
- **Betty Architecture** – Framework overview ([betty-architecture.md](../../docs/betty-architecture.md))
|
||||
|
||||
## Dependencies
|
||||
|
||||
- **registry.update**: Registry management
|
||||
- **betty.config**: Configuration constants and paths
|
||||
- **betty.logging_utils**: Logging infrastructure
|
||||
|
||||
## Status
|
||||
|
||||
**Active** – Production-ready infrastructure skill
|
||||
|
||||
## Version History
|
||||
|
||||
- **0.1.0** (Oct 2025) – Initial implementation with registry sync and handler validation
|
||||
1
skills/plugin.sync/__init__.py
Normal file
1
skills/plugin.sync/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Auto-generated package initializer for skills.
|
||||
323
skills/plugin.sync/plugin_sync.py
Executable file
323
skills/plugin.sync/plugin_sync.py
Executable file
@@ -0,0 +1,323 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
plugin_sync.py – Implementation of the plugin.sync Skill
|
||||
Generates plugin.yaml from registry files (skills.json, commands.json, hooks.json).
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import yaml
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
from betty.config import BASE_DIR
|
||||
from betty.logging_utils import setup_logger
|
||||
|
||||
logger = setup_logger(__name__)
|
||||
|
||||
|
||||
def load_registry_file(registry_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Load a JSON registry file.
|
||||
|
||||
Args:
|
||||
registry_path: Path to the registry JSON file
|
||||
|
||||
Returns:
|
||||
Parsed registry data
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: If registry file doesn't exist
|
||||
json.JSONDecodeError: If JSON is invalid
|
||||
"""
|
||||
try:
|
||||
with open(registry_path) as f:
|
||||
return json.load(f)
|
||||
except FileNotFoundError:
|
||||
logger.warning(f"Registry file not found: {registry_path}")
|
||||
return {}
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to parse JSON from {registry_path}: {e}")
|
||||
raise
|
||||
|
||||
|
||||
def check_handler_exists(handler_path: str, skill_name: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Check if a handler file exists on disk.
|
||||
|
||||
Args:
|
||||
handler_path: Relative path to handler file
|
||||
skill_name: Name of the skill
|
||||
|
||||
Returns:
|
||||
Dictionary with exists flag and full path
|
||||
"""
|
||||
full_path = os.path.join(BASE_DIR, "skills", skill_name, handler_path)
|
||||
exists = os.path.exists(full_path)
|
||||
|
||||
return {
|
||||
"exists": exists,
|
||||
"path": full_path,
|
||||
"relative_path": f"skills/{skill_name}/{handler_path}"
|
||||
}
|
||||
|
||||
|
||||
def convert_skill_to_command(skill: Dict[str, Any], entrypoint: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Convert a skill registry entry with entrypoint to a plugin.yaml command format.
|
||||
|
||||
Args:
|
||||
skill: Skill entry from skills.json
|
||||
entrypoint: Entrypoint entry from the skill
|
||||
|
||||
Returns:
|
||||
Command dictionary in plugin.yaml format
|
||||
"""
|
||||
# Extract command name (remove leading slash if present)
|
||||
command_name = entrypoint.get("command", "").lstrip("/")
|
||||
|
||||
# Build handler section
|
||||
handler_path = f"skills/{skill['name']}/{entrypoint.get('handler', '')}"
|
||||
|
||||
command = {
|
||||
"name": command_name,
|
||||
"description": entrypoint.get("description") or skill.get("description", ""),
|
||||
"handler": {
|
||||
"runtime": entrypoint.get("runtime", "python"),
|
||||
"script": handler_path
|
||||
}
|
||||
}
|
||||
|
||||
# Add parameters from entrypoint or extract from skill inputs
|
||||
if "parameters" in entrypoint and entrypoint["parameters"]:
|
||||
# Use parameters from entrypoint if they exist
|
||||
command["parameters"] = entrypoint["parameters"]
|
||||
elif "inputs" in skill and skill["inputs"]:
|
||||
# Otherwise, convert skill inputs to command parameters
|
||||
parameters = []
|
||||
for inp in skill["inputs"]:
|
||||
if isinstance(inp, dict):
|
||||
# Full input specification
|
||||
param = {
|
||||
"name": inp.get("name", ""),
|
||||
"type": inp.get("type", "string"),
|
||||
"required": inp.get("required", False),
|
||||
"description": inp.get("description", "")
|
||||
}
|
||||
# Add default if present
|
||||
if "default" in inp:
|
||||
param["default"] = inp["default"]
|
||||
parameters.append(param)
|
||||
elif isinstance(inp, str):
|
||||
# Simple string input (legacy format)
|
||||
parameters.append({
|
||||
"name": inp,
|
||||
"type": "string",
|
||||
"required": False,
|
||||
"description": ""
|
||||
})
|
||||
if parameters:
|
||||
command["parameters"] = parameters
|
||||
|
||||
# Add permissions if present
|
||||
if "permissions" in entrypoint:
|
||||
# Convert permissions list to proper format if needed
|
||||
permissions = entrypoint["permissions"]
|
||||
if isinstance(permissions, list):
|
||||
command["permissions"] = permissions
|
||||
|
||||
return command
|
||||
|
||||
|
||||
def generate_plugin_yaml(
|
||||
skills_data: Dict[str, Any],
|
||||
commands_data: Dict[str, Any],
|
||||
hooks_data: Dict[str, Any]
|
||||
) -> tuple[Dict[str, Any], List[str]]:
|
||||
"""
|
||||
Generate plugin.yaml content from registry data.
|
||||
|
||||
Args:
|
||||
skills_data: Parsed skills.json
|
||||
commands_data: Parsed commands.json
|
||||
hooks_data: Parsed hooks.json
|
||||
|
||||
Returns:
|
||||
Tuple of (plugin_yaml_dict, list of warnings)
|
||||
"""
|
||||
warnings = []
|
||||
commands = []
|
||||
|
||||
# Load existing plugin.yaml to preserve header content
|
||||
plugin_yaml_path = os.path.join(BASE_DIR, "plugin.yaml")
|
||||
base_config = {}
|
||||
|
||||
try:
|
||||
with open(plugin_yaml_path) as f:
|
||||
base_config = yaml.safe_load(f) or {}
|
||||
logger.info(f"Loaded existing plugin.yaml as template")
|
||||
except FileNotFoundError:
|
||||
logger.warning("No existing plugin.yaml found, creating from scratch")
|
||||
base_config = {
|
||||
"name": "betty-framework",
|
||||
"version": "1.0.0",
|
||||
"description": "Betty Framework - Structured AI-assisted engineering",
|
||||
"author": {
|
||||
"name": "RiskExec",
|
||||
"email": "platform@riskexec.com",
|
||||
"url": "https://github.com/epieczko/betty"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
# Process active skills with entrypoints
|
||||
skills = skills_data.get("skills", [])
|
||||
for skill in skills:
|
||||
if skill.get("status") != "active":
|
||||
continue
|
||||
|
||||
entrypoints = skill.get("entrypoints", [])
|
||||
if not entrypoints:
|
||||
continue
|
||||
|
||||
skill_name = skill.get("name")
|
||||
|
||||
for entrypoint in entrypoints:
|
||||
handler = entrypoint.get("handler")
|
||||
if not handler:
|
||||
warnings.append(f"Skill '{skill_name}' has entrypoint without handler")
|
||||
continue
|
||||
|
||||
# Check if handler exists on disk
|
||||
handler_check = check_handler_exists(handler, skill_name)
|
||||
if not handler_check["exists"]:
|
||||
warnings.append(
|
||||
f"Handler not found for '{skill_name}': {handler_check['relative_path']}"
|
||||
)
|
||||
|
||||
# Convert to plugin command format
|
||||
command = convert_skill_to_command(skill, entrypoint)
|
||||
commands.append(command)
|
||||
|
||||
logger.info(f"Added command: /{command['name']} from skill {skill_name}")
|
||||
|
||||
# Process commands from commands.json (if any need to be added)
|
||||
# Note: Most commands should already be represented via skills
|
||||
# This is mainly for custom commands that don't map to skills
|
||||
registry_commands = commands_data.get("commands", [])
|
||||
for cmd in registry_commands:
|
||||
if cmd.get("status") == "active":
|
||||
# Check if this command is already in our list
|
||||
cmd_name = cmd.get("name", "").lstrip("/")
|
||||
if not any(c["name"] == cmd_name for c in commands):
|
||||
logger.info(f"Command '{cmd_name}' in registry but no matching active skill found")
|
||||
|
||||
# Build final plugin.yaml structure
|
||||
plugin_config = {
|
||||
**base_config,
|
||||
"commands": commands
|
||||
}
|
||||
|
||||
# Override plugin-level permissions (remove network:none contradiction)
|
||||
plugin_config["permissions"] = [
|
||||
"filesystem:read",
|
||||
"filesystem:write",
|
||||
"process:execute"
|
||||
]
|
||||
|
||||
# Add metadata about generation
|
||||
if "metadata" not in plugin_config:
|
||||
plugin_config["metadata"] = {}
|
||||
|
||||
plugin_config["metadata"]["generated_at"] = datetime.now(timezone.utc).isoformat()
|
||||
plugin_config["metadata"]["generated_by"] = "plugin.sync skill"
|
||||
plugin_config["metadata"]["skill_count"] = len([s for s in skills if s.get("status") == "active"])
|
||||
plugin_config["metadata"]["command_count"] = len(commands)
|
||||
|
||||
return plugin_config, warnings
|
||||
|
||||
|
||||
def write_plugin_yaml(plugin_config: Dict[str, Any], output_path: str):
|
||||
"""
|
||||
Write plugin.yaml to disk with proper formatting.
|
||||
|
||||
Args:
|
||||
plugin_config: Plugin configuration dictionary
|
||||
output_path: Path where to write plugin.yaml
|
||||
"""
|
||||
# Add header comment
|
||||
header = """# Betty Framework - Claude Code Plugin
|
||||
# Auto-generated by plugin.sync skill
|
||||
# DO NOT EDIT MANUALLY - Run plugin.sync to regenerate
|
||||
|
||||
"""
|
||||
|
||||
with open(output_path, 'w') as f:
|
||||
f.write(header)
|
||||
yaml.dump(plugin_config, f, default_flow_style=False, sort_keys=False, indent=2)
|
||||
|
||||
logger.info(f"✅ Written plugin.yaml to {output_path}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main CLI entry point."""
|
||||
logger.info("Starting plugin.yaml generation from registries...")
|
||||
|
||||
# Define registry paths
|
||||
skills_path = os.path.join(BASE_DIR, "registry", "skills.json")
|
||||
commands_path = os.path.join(BASE_DIR, "registry", "commands.json")
|
||||
hooks_path = os.path.join(BASE_DIR, "registry", "hooks.json")
|
||||
|
||||
try:
|
||||
# Load registry files
|
||||
logger.info("Loading registry files...")
|
||||
skills_data = load_registry_file(skills_path)
|
||||
commands_data = load_registry_file(commands_path)
|
||||
hooks_data = load_registry_file(hooks_path)
|
||||
|
||||
# Generate plugin.yaml content
|
||||
logger.info("Generating plugin.yaml configuration...")
|
||||
plugin_config, warnings = generate_plugin_yaml(skills_data, commands_data, hooks_data)
|
||||
|
||||
# Write to file
|
||||
output_path = os.path.join(BASE_DIR, "plugin.yaml")
|
||||
write_plugin_yaml(plugin_config, output_path)
|
||||
|
||||
# Report results
|
||||
result = {
|
||||
"ok": True,
|
||||
"status": "success",
|
||||
"output_path": output_path,
|
||||
"commands_generated": len(plugin_config.get("commands", [])),
|
||||
"warnings": warnings
|
||||
}
|
||||
|
||||
# Print warnings if any
|
||||
if warnings:
|
||||
logger.warning("⚠️ Warnings during generation:")
|
||||
for warning in warnings:
|
||||
logger.warning(f" - {warning}")
|
||||
|
||||
# Print summary
|
||||
logger.info(f"✅ Generated {result['commands_generated']} commands")
|
||||
logger.info(f"📄 Output: {output_path}")
|
||||
|
||||
print(json.dumps(result, indent=2))
|
||||
sys.exit(0)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to generate plugin.yaml: {e}")
|
||||
result = {
|
||||
"ok": False,
|
||||
"status": "failed",
|
||||
"error": str(e)
|
||||
}
|
||||
print(json.dumps(result, indent=2))
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
29
skills/plugin.sync/skill.yaml
Normal file
29
skills/plugin.sync/skill.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
name: plugin.sync
|
||||
version: 0.1.0
|
||||
description: >
|
||||
Automatically generates plugin.yaml from Betty Framework registries.
|
||||
Reads skills.json, commands.json, and hooks.json to build a complete plugin configuration.
|
||||
inputs: []
|
||||
outputs:
|
||||
- plugin.yaml
|
||||
- generation_report.json
|
||||
dependencies:
|
||||
- registry.update
|
||||
status: active
|
||||
|
||||
entrypoints:
|
||||
- command: /plugin/sync
|
||||
handler: plugin_sync.py
|
||||
runtime: python
|
||||
description: >
|
||||
Generate plugin.yaml from registry files. Syncs active skills with entrypoints into plugin commands.
|
||||
parameters: []
|
||||
permissions:
|
||||
- filesystem:read
|
||||
- filesystem:write
|
||||
|
||||
tags:
|
||||
- plugin
|
||||
- registry
|
||||
- automation
|
||||
- infrastructure
|
||||
Reference in New Issue
Block a user