Files
rootiest-ai/scripts/generate_plugins.py
T
rootiest aee7facbae
Generate plugin manifests / validate (push) Skipped
Generate plugin manifests / generate (push) Successful in 54s
feat(marketplace): implement plugin marketplace
Implements a new plugin marketplace structure and expands repository
support beyond basic skills to include plugins, MCPs, hooks, etc.
2026-08-24 14:38:52 -04:00

191 lines
6.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate per-agent plugin manifests from the skills/ SSoT.
Reads manifest.yaml (marketplace/org metadata + target list) and every
skills/<name>/SKILL.md (per-skill frontmatter), then regenerates:
- descriptions.json (from SKILL.md frontmatter)
- .claude-plugin/marketplace.json (Claude Code target)
- dist/agy/** (Antigravity CLI target)
Run with --check to only validate the SSoT (frontmatter, manifest.yaml) and
skip writing any output — used as the pull-request gate.
"""
from __future__ import annotations
import json
import shutil
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parent.parent
SKILLS_DIR = ROOT / "skills"
MANIFEST_PATH = ROOT / "manifest.yaml"
REQUIRED_FRONTMATTER_FIELDS = ("name", "description", "version", "author")
class ValidationError(Exception):
pass
def load_manifest() -> dict:
with MANIFEST_PATH.open("r", encoding="utf-8") as f:
manifest = yaml.safe_load(f)
for key in ("marketplace", "bundle", "targets"):
if key not in manifest:
raise ValidationError(f"manifest.yaml is missing required key '{key}'")
return manifest
def parse_frontmatter(skill_md: Path) -> dict:
text = skill_md.read_text(encoding="utf-8")
if not text.startswith("---\n"):
raise ValidationError(f"{skill_md}: missing YAML frontmatter delimiter")
end = text.find("\n---", 4)
if end == -1:
raise ValidationError(f"{skill_md}: unterminated YAML frontmatter")
raw = text[4:end]
data = yaml.safe_load(raw) or {}
for field in REQUIRED_FRONTMATTER_FIELDS:
if not data.get(field):
raise ValidationError(f"{skill_md}: frontmatter missing required field '{field}'")
return data
def discover_skills() -> list[dict]:
skills = []
seen_names = set()
for skill_dir in sorted(SKILLS_DIR.iterdir()):
if not skill_dir.is_dir():
continue
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
raise ValidationError(f"{skill_dir}: missing SKILL.md")
frontmatter = parse_frontmatter(skill_md)
name = frontmatter["name"]
if name != skill_dir.name:
raise ValidationError(
f"{skill_md}: frontmatter name '{name}' does not match directory name '{skill_dir.name}'"
)
if name in seen_names:
raise ValidationError(f"duplicate skill name '{name}'")
seen_names.add(name)
skills.append({"dir": skill_dir, "frontmatter": frontmatter})
if not skills:
raise ValidationError("no skills found under skills/")
return skills
def gen_descriptions_json(skills: list[dict]) -> None:
descriptions = {s["frontmatter"]["name"]: s["frontmatter"]["description"] for s in skills}
out = ROOT / "descriptions.json"
out.write_text(json.dumps(descriptions, indent=2) + "\n", encoding="utf-8")
def gen_claude_code(manifest: dict, skills: list[dict]) -> None:
plugin_dir = ROOT / ".claude-plugin"
plugin_dir.mkdir(exist_ok=True)
plugins = []
for s in skills:
fm = s["frontmatter"]
plugins.append(
{
"name": fm["name"],
"description": fm["description"],
"source": "./",
"skills": [f"./skills/{fm['name']}"],
}
)
plugins.append(
{
"name": manifest["bundle"]["id"],
"description": f"{manifest['marketplace']['description']} (all skills)",
"source": "./",
"skills": ["./skills/"],
}
)
marketplace = {
"name": manifest["marketplace"]["name"],
"owner": manifest["marketplace"]["owner"],
"description": manifest["marketplace"]["description"],
"plugins": plugins,
}
out = plugin_dir / "marketplace.json"
out.write_text(json.dumps(marketplace, indent=2) + "\n", encoding="utf-8")
def gen_agy(manifest: dict, skills: list[dict]) -> None:
agy_dir = ROOT / "dist" / "agy"
if agy_dir.exists():
shutil.rmtree(agy_dir)
agy_dir.mkdir(parents=True)
def write_plugin(plugin_name: str, description: str, members: list[dict]) -> None:
plugin_root = agy_dir / plugin_name
skills_out = plugin_root / "skills"
skills_out.mkdir(parents=True)
for s in members:
dest = skills_out / s["frontmatter"]["name"]
shutil.copytree(s["dir"], dest)
plugin_json = {"name": plugin_name, "description": description}
(plugin_root / "plugin.json").write_text(
json.dumps(plugin_json, indent=2) + "\n", encoding="utf-8"
)
for s in skills:
fm = s["frontmatter"]
write_plugin(fm["name"], fm["description"], [s])
write_plugin(
manifest["bundle"]["id"],
f"{manifest['marketplace']['description']} (all skills)",
skills,
)
dist_readme = ROOT / "dist" / "README.md"
dist_readme.write_text(
"# Generated\n\n"
"This directory is generated by `scripts/generate_plugins.py` from "
"`manifest.yaml` and `skills/`. Do not edit files here directly — "
"edit the source skill instead and regenerate.\n",
encoding="utf-8",
)
GENERATORS = {
"claude-code": gen_claude_code,
"agy": gen_agy,
}
def main() -> int:
check_only = "--check" in sys.argv[1:]
try:
manifest = load_manifest()
skills = discover_skills()
for target in manifest["targets"]:
if target not in GENERATORS:
raise ValidationError(f"manifest.yaml: unknown target '{target}'")
if check_only:
print(f"OK: {len(skills)} skill(s), {len(manifest['targets'])} target(s) validated")
return 0
gen_descriptions_json(skills)
for target in manifest["targets"]:
GENERATORS[target](manifest, skills)
print(f"Generated plugins for {len(skills)} skill(s), targets: {', '.join(manifest['targets'])}")
return 0
except ValidationError as e:
print(f"error: {e}", file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())