feat(bundles): add editorial bundle plugins

This commit is contained in:
sickn33
2026-03-27 08:48:03 +01:00
parent 8eff08b706
commit dffac91d3b
1052 changed files with 212282 additions and 68 deletions

View File

@@ -8,6 +8,7 @@ import sys
from pathlib import Path
from _project_paths import find_repo_root
from sync_editorial_bundles import load_editorial_bundles, render_bundles_doc
import sync_repo_metadata
from update_readme import configure_utf8_output, load_metadata, apply_metadata
@@ -31,8 +32,22 @@ def _expected_getting_started(content: str, metadata: dict) -> str:
return sync_repo_metadata.sync_getting_started(content, metadata)
def _expected_bundles(content: str, metadata: dict) -> str:
return sync_repo_metadata.sync_bundles_doc(content, metadata)
def _expected_bundles(content: str, metadata: dict, root: Path) -> str:
manifest_path = root / "data" / "editorial-bundles.json"
template_path = root / "tools" / "templates" / "editorial-bundles.md.tmpl"
if manifest_path.is_file() and template_path.is_file():
bundles = load_editorial_bundles(root)
return render_bundles_doc(root, metadata, bundles)
bundle_count = sync_repo_metadata.count_documented_bundles(content)
if bundle_count == 0:
bundle_count = 36
expected, _ = sync_repo_metadata.replace_if_present(
content,
sync_repo_metadata.BUNDLES_FOOTER_RE,
f"_Last updated: March 2026 | Total Skills: {metadata['total_skills_label']} | Total Bundles: {bundle_count}_",
)
return expected
def _expected_regex_sync(content: str, replacements: list[tuple[str, str]]) -> str:
@@ -55,7 +70,7 @@ def find_local_consistency_issues(base_dir: str | Path) -> list[str]:
file_checks = [
("README.md", _expected_readme),
("docs/users/getting-started.md", _expected_getting_started),
("docs/users/bundles.md", _expected_bundles),
("docs/users/bundles.md", lambda content, current_metadata: _expected_bundles(content, current_metadata, root)),
("docs/integrations/jetski-cortex.md", _expected_jetski_cortex),
(
"docs/users/claude-code-skills.md",

View File

@@ -1,9 +1,12 @@
#!/usr/bin/env python3
import sys
import json
import re
import sys
from pathlib import Path
SAFE_SKILL_ID_PATTERN = re.compile(r"^[A-Za-z0-9._-]+$")
SAFE_SKILL_ID_PATTERN = re.compile(
r"^(?!.*(?:^|/)\.{1,2}(?:/|$))[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$"
)
def is_safe_skill_id(skill_id):
@@ -22,39 +25,84 @@ def format_skills_for_batch(skill_ids):
return "\n".join(safe_skill_ids) + "\n"
def get_bundle_skills(bundle_queries, bundles_path=None):
def _manifest_bundles_path():
return Path(__file__).parent.parent.parent / "data" / "editorial-bundles.json"
def _normalize_bundle_query(query):
normalized = re.sub(r"[^a-z0-9]+", "-", (query or "").lower()).strip("-")
return re.sub(r"-{2,}", "-", normalized)
def _read_editorial_manifest(manifest_path):
payload = json.loads(manifest_path.read_text(encoding="utf-8"))
bundles = payload.get("bundles", [])
return bundles if isinstance(bundles, list) else []
def _read_legacy_markdown_bundles(markdown_path):
content = markdown_path.read_text(encoding="utf-8")
sections = re.split(r"\n### ", content)
bundles = []
for section in sections:
header_line = section.split("\n")[0].strip()
if not header_line:
continue
name_match = re.search(r'"([^"]+)"', header_line)
bundle_name = name_match.group(1) if name_match else header_line
skills = re.findall(r"- \[`([^`]+)`\]", section)
bundles.append(
{
"id": _normalize_bundle_query(bundle_name),
"name": bundle_name,
"skills": [{"id": skill_id} for skill_id in skills],
}
)
return bundles
def _load_bundles_data(bundles_path=None):
if bundles_path is None:
bundles_path = Path(__file__).parent.parent.parent / "docs" / "users" / "bundles.md"
bundles_path = _manifest_bundles_path()
else:
bundles_path = Path(bundles_path)
if not bundles_path.exists():
print(f"Error: {bundles_path} not found", file=sys.stderr)
return []
content = bundles_path.read_text(encoding="utf-8")
# Split by bundle headers
sections = re.split(r'\n### ', content)
if bundles_path.suffix.lower() == ".json":
return _read_editorial_manifest(bundles_path)
return _read_legacy_markdown_bundles(bundles_path)
def get_bundle_skills(bundle_queries, bundles_path=None):
selected_skills = set()
bundles = _load_bundles_data(bundles_path)
for query in bundle_queries:
query = query.lower().strip('"\'')
raw_query = query.lower().strip('"\'')
query_slug = _normalize_bundle_query(raw_query)
found = False
for section in sections:
header_line = section.split('\n')[0].lower()
if query in header_line:
for bundle in bundles:
bundle_name = str(bundle.get("name", "")).lower()
bundle_id = str(bundle.get("id", ""))
if raw_query in bundle_name or query_slug == bundle_id:
found = True
# Extract skill names from bullet points: - [`skill-name`](../../skills/skill-name/)
skills = re.findall(r'- \[`([^`]+)`\]', section)
skills = []
for skill in bundle.get("skills", []):
if isinstance(skill, str):
skills.append(skill)
elif isinstance(skill, dict) and "id" in skill:
skills.append(skill["id"])
selected_skills.update(filter_safe_skill_ids(skills))
if not found:
# If query not found in any header, check if it's a skill name itself
# (Just in case the user passed a skill name instead of a bundle)
if is_safe_skill_id(query):
selected_skills.add(query)
if is_safe_skill_id(raw_query):
selected_skills.add(raw_query)
return sorted(list(selected_skills))

View File

@@ -0,0 +1,636 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import json
import re
import shutil
from pathlib import Path
from typing import Any
from _project_paths import find_repo_root
from update_readme import configure_utf8_output, load_metadata
SAFE_SKILL_ID_RE = re.compile(
r"^(?!.*(?:^|/)\.{1,2}(?:/|$))[A-Za-z0-9._-]+(?:/[A-Za-z0-9._-]+)*$"
)
REPO_URL = "https://github.com/sickn33/antigravity-awesome-skills"
AUTHOR = {
"name": "sickn33 and contributors",
"url": REPO_URL,
}
ROOT_CLAUDE_PLUGIN_NAME = "antigravity-awesome-skills"
ROOT_CODEX_PLUGIN_NAME = "antigravity-awesome-skills"
EDITORIAL_BUNDLES_PATH = Path("data") / "editorial-bundles.json"
EDITORIAL_TEMPLATE_PATH = Path("tools") / "templates" / "editorial-bundles.md.tmpl"
CLAUDE_MARKETPLACE_PATH = Path(".claude-plugin") / "marketplace.json"
CLAUDE_PLUGIN_PATH = Path(".claude-plugin") / "plugin.json"
CODEX_MARKETPLACE_PATH = Path(".agents") / "plugins" / "marketplace.json"
CODEX_ROOT_PLUGIN_PATH = Path("plugins") / ROOT_CODEX_PLUGIN_NAME / ".codex-plugin" / "plugin.json"
ACRONYM_TOKENS = {
"ab": "A/B",
"adb": "ADB",
"adr": "ADR",
"ads": "ADS",
"ai": "AI",
"api": "API",
"apis": "APIs",
"app": "App",
"apps": "Apps",
"aso": "ASO",
"aws": "AWS",
"bat": "BAT",
"ci": "CI",
"cli": "CLI",
"cms": "CMS",
"crm": "CRM",
"cro": "CRO",
"css": "CSS",
"csv": "CSV",
"dag": "DAG",
"dbt": "dbt",
"ddd": "DDD",
"devops": "DevOps",
"docx": "DOCX",
"dx": "DX",
"e2e": "E2E",
"expo": "Expo",
"fastapi": "FastAPI",
"github": "GitHub",
"gitlab": "GitLab",
"grafana": "Grafana",
"html": "HTML",
"ios": "iOS",
"jwt": "JWT",
"k8s": "K8s",
"kpi": "KPI",
"langfuse": "Langfuse",
"langgraph": "LangGraph",
"linux": "Linux",
"llm": "LLM",
"llms": "LLMs",
"mcp": "MCP",
"nextjs": "Next.js",
"nodejs": "Node.js",
"oauth2": "OAuth2",
"odoo": "Odoo",
"openai": "OpenAI",
"owasp": "OWASP",
"pdf": "PDF",
"php": "PHP",
"postgres": "Postgres",
"pr": "PR",
"prd": "PRD",
"pwa": "PWA",
"python": "Python",
"rag": "RAG",
"rails": "Rails",
"react": "React",
"rest": "REST",
"rpc": "RPC",
"saas": "SaaS",
"seo": "SEO",
"shopify": "Shopify",
"slack": "Slack",
"slo": "SLO",
"sre": "SRE",
"sql": "SQL",
"sso": "SSO",
"stripe": "Stripe",
"svg": "SVG",
"swiftui": "SwiftUI",
"tailwind": "Tailwind",
"tdd": "TDD",
"ts": "TS",
"tsx": "TSX",
"ui": "UI",
"ux": "UX",
"uv": "uv",
"webgl": "WebGL",
"xcode": "Xcode",
"xml": "XML",
"yaml": "YAML",
"zod": "Zod",
}
def _read_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def _write_json(path: Path, payload: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def _write_text(path: Path, content: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8", newline="\n")
def _clean_group_label(group: str) -> str:
return re.sub(r"^[^A-Za-z0-9]+", "", group).strip()
def _bundle_plugin_name(bundle_id: str) -> str:
return f"antigravity-bundle-{bundle_id}"
def _humanize_skill_label(skill_id: str) -> str:
tokens = re.split(r"[-_]+", skill_id.split("/")[-1])
words = [ACRONYM_TOKENS.get(token.lower(), token.capitalize()) for token in tokens if token]
return " ".join(words)
def _bundle_codex_long_description(bundle: dict[str, Any]) -> str:
audience = bundle.get("audience") or bundle["description"]
highlights = [
_humanize_skill_label(skill["id"])
for skill in bundle["skills"][:2]
if skill.get("id")
]
remaining = len(bundle["skills"]) - len(highlights)
if not highlights:
return f'{audience} Includes {len(bundle["skills"])} curated skills from Antigravity Awesome Skills.'
if remaining > 0:
return f"{audience} Covers {', '.join(highlights)}, and {remaining} more skills."
if len(highlights) == 1:
return f"{audience} Covers {highlights[0]}."
return f"{audience} Covers {' and '.join(highlights)}."
def _validate_bundle_skill_id(skill_id: str) -> None:
if not SAFE_SKILL_ID_RE.fullmatch(skill_id):
raise ValueError(f"Invalid skill id in editorial bundles manifest: {skill_id!r}")
def _validate_editorial_bundles(root: Path, payload: dict[str, Any]) -> list[dict[str, Any]]:
bundles = payload.get("bundles")
if not isinstance(bundles, list) or not bundles:
raise ValueError("data/editorial-bundles.json must contain a non-empty 'bundles' array.")
seen_bundle_ids: set[str] = set()
seen_bundle_names: set[str] = set()
skills_root = root / "skills"
for bundle in bundles:
if not isinstance(bundle, dict):
raise ValueError("Each editorial bundle must be an object.")
bundle_id = str(bundle.get("id", "")).strip()
bundle_name = str(bundle.get("name", "")).strip()
if not bundle_id or not bundle_name:
raise ValueError("Each editorial bundle requires non-empty 'id' and 'name'.")
if bundle_id in seen_bundle_ids:
raise ValueError(f"Duplicate editorial bundle id: {bundle_id}")
if bundle_name in seen_bundle_names:
raise ValueError(f"Duplicate editorial bundle name: {bundle_name}")
seen_bundle_ids.add(bundle_id)
seen_bundle_names.add(bundle_name)
plugin_name = _bundle_plugin_name(bundle_id)
if len(plugin_name) > 64:
raise ValueError(f"Bundle plugin name exceeds 64 characters: {plugin_name}")
for key in ("group", "emoji", "tagline", "audience", "description"):
if not str(bundle.get(key, "")).strip():
raise ValueError(f"Editorial bundle '{bundle_id}' is missing required field '{key}'.")
skills = bundle.get("skills")
if not isinstance(skills, list) or not skills:
raise ValueError(f"Editorial bundle '{bundle_id}' must include a non-empty 'skills' array.")
seen_skill_ids: set[str] = set()
for skill in skills:
if not isinstance(skill, dict):
raise ValueError(f"Editorial bundle '{bundle_id}' contains a non-object skill entry.")
skill_id = str(skill.get("id", "")).strip()
summary = str(skill.get("summary", "")).strip()
_validate_bundle_skill_id(skill_id)
if skill_id in seen_skill_ids:
raise ValueError(f"Editorial bundle '{bundle_id}' contains duplicate skill '{skill_id}'.")
if not summary:
raise ValueError(f"Editorial bundle '{bundle_id}' skill '{skill_id}' is missing summary.")
skill_path = (skills_root / skill_id).resolve(strict=False)
if not skill_path.exists():
raise ValueError(f"Editorial bundle '{bundle_id}' references missing skill '{skill_id}'.")
seen_skill_ids.add(skill_id)
return bundles
def _render_bundle_sections(bundles: list[dict[str, Any]]) -> str:
lines: list[str] = []
current_group: str | None = None
for bundle in bundles:
group = bundle["group"]
if group != current_group:
if lines:
lines.extend(["", "---", ""])
lines.append(f"## {group}")
lines.append("")
current_group = group
lines.append(f'### {bundle["emoji"]} {bundle["tagline"]}')
lines.append("")
lines.append(f'_{bundle["audience"]}_')
lines.append("")
for skill in bundle["skills"]:
lines.append(
f'- [`{skill["id"]}`](../../skills/{skill["id"]}/): {skill["summary"]}'
)
lines.append("")
return "\n".join(lines).strip() + "\n"
def render_bundles_doc(root: Path, metadata: dict[str, Any], bundles: list[dict[str, Any]]) -> str:
template = (root / EDITORIAL_TEMPLATE_PATH).read_text(encoding="utf-8")
return (
template.replace("{{bundle_sections}}", _render_bundle_sections(bundles).rstrip())
.replace("{{total_skills_label}}", metadata["total_skills_label"])
.replace("{{bundle_count}}", str(len(bundles)))
)
def _copy_file_contents(src: Path, dest: Path, allowed_root: Path) -> None:
resolved_src = src.resolve(strict=True)
resolved_src.relative_to(allowed_root.resolve())
if resolved_src.is_dir():
dest.mkdir(parents=True, exist_ok=True)
for child in resolved_src.iterdir():
_copy_file_contents(child, dest / child.name, allowed_root)
return
dest.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(resolved_src, dest)
def _copy_skill_directory(root: Path, skill_id: str, destination_root: Path) -> None:
skills_root = root / "skills"
source = (skills_root / skill_id).resolve(strict=True)
source.relative_to(skills_root.resolve())
if not source.is_dir():
raise ValueError(f"Editorial bundle skill '{skill_id}' is not a directory.")
skill_dest = destination_root / skill_id
if skill_dest.exists():
shutil.rmtree(skill_dest)
for child in source.iterdir():
_copy_file_contents(child, skill_dest / child.name, skills_root)
if not (skill_dest / "SKILL.md").is_file():
raise ValueError(f"Copied bundle skill '{skill_id}' is missing SKILL.md in {skill_dest}")
def _root_claude_plugin_manifest(metadata: dict[str, Any]) -> dict[str, Any]:
return {
"name": ROOT_CLAUDE_PLUGIN_NAME,
"version": metadata["version"],
"description": (
f"Universal agentic skill library for Claude Code with "
f"{metadata['total_skills_label']} reusable skills across coding, security, "
"design, product, and operations workflows."
),
"author": AUTHOR,
"homepage": REPO_URL,
"repository": REPO_URL,
"license": "MIT",
"keywords": [
"claude-code",
"skills",
"agentic-skills",
"ai-coding",
"productivity",
],
}
def _root_codex_plugin_manifest(metadata: dict[str, Any]) -> dict[str, Any]:
return {
"name": ROOT_CODEX_PLUGIN_NAME,
"version": metadata["version"],
"description": "Repository-backed Codex plugin for the Antigravity Awesome Skills library.",
"author": AUTHOR,
"homepage": REPO_URL,
"repository": REPO_URL,
"license": "MIT",
"keywords": [
"codex",
"skills",
"agentic-skills",
"developer-tools",
"productivity",
],
"skills": "./skills/",
"interface": {
"displayName": "Antigravity Awesome Skills",
"shortDescription": (
f'{metadata["total_skills_label"]} reusable skills for coding, security, '
"product, and ops workflows."
),
"longDescription": (
"Install the Antigravity Awesome Skills catalog as a Codex plugin and expose "
"the repository's curated skills library through a single marketplace entry."
),
"developerName": AUTHOR["name"],
"category": "Productivity",
"capabilities": ["Interactive", "Write"],
"websiteURL": REPO_URL,
"defaultPrompt": [
"Use @brainstorming to plan a new feature.",
"Use @test-driven-development to fix a bug safely.",
"Use @lint-and-validate to verify this branch.",
],
"brandColor": "#111827",
},
}
def _bundle_claude_plugin_manifest(metadata: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any]:
return {
"name": _bundle_plugin_name(bundle["id"]),
"version": metadata["version"],
"description": (
f'Editorial "{bundle["name"]}" bundle for Claude Code from Antigravity Awesome Skills.'
),
"author": AUTHOR,
"homepage": REPO_URL,
"repository": REPO_URL,
"license": "MIT",
"keywords": [
"claude-code",
"skills",
"bundle",
bundle["id"],
"antigravity-awesome-skills",
],
}
def _bundle_codex_plugin_manifest(metadata: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any]:
category = _clean_group_label(bundle["group"])
plugin_name = _bundle_plugin_name(bundle["id"])
skill_count = len(bundle["skills"])
return {
"name": plugin_name,
"version": metadata["version"],
"description": (
f'Install the "{bundle["name"]}" editorial skill bundle from Antigravity Awesome Skills.'
),
"author": AUTHOR,
"homepage": REPO_URL,
"repository": REPO_URL,
"license": "MIT",
"keywords": [
"codex",
"skills",
"bundle",
bundle["id"],
"productivity",
],
"skills": "./skills/",
"interface": {
"displayName": bundle["name"],
"shortDescription": f"{category} · {skill_count} curated skills",
"longDescription": _bundle_codex_long_description(bundle),
"developerName": AUTHOR["name"],
"category": category,
"capabilities": ["Interactive", "Write"],
"websiteURL": REPO_URL,
"brandColor": "#111827",
},
}
def _root_codex_plugin_manifest(metadata: dict[str, Any]) -> dict[str, Any]:
return {
"name": ROOT_CODEX_PLUGIN_NAME,
"version": metadata["version"],
"description": "Repository-backed Codex plugin for the Antigravity Awesome Skills library.",
"author": AUTHOR,
"homepage": REPO_URL,
"repository": REPO_URL,
"license": "MIT",
"keywords": [
"codex",
"skills",
"agentic-skills",
"developer-tools",
"productivity",
],
"skills": "./skills/",
"interface": {
"displayName": "Antigravity Awesome Skills",
"shortDescription": (
f'{metadata["total_skills_label"]} reusable skills for coding, security, product, and ops workflows.'
),
"longDescription": (
"Install the Antigravity Awesome Skills catalog as a Codex plugin and expose "
"the repository's curated skills library through a single marketplace entry."
),
"developerName": AUTHOR["name"],
"category": "Productivity",
"capabilities": ["Interactive", "Write"],
"websiteURL": REPO_URL,
"defaultPrompt": [
"Use @brainstorming to plan a new feature.",
"Use @test-driven-development to fix a bug safely.",
"Use @lint-and-validate to verify this branch.",
],
"brandColor": "#111827",
},
}
def _bundle_claude_marketplace_entry(metadata: dict[str, Any], bundle: dict[str, Any]) -> dict[str, Any]:
plugin_name = _bundle_plugin_name(bundle["id"])
return {
"name": plugin_name,
"version": metadata["version"],
"description": (
f'Install the "{bundle["name"]}" editorial skill bundle for Claude Code.'
),
"author": AUTHOR,
"homepage": REPO_URL,
"repository": REPO_URL,
"license": "MIT",
"keywords": [
"claude-code",
"skills",
"bundle",
bundle["id"],
"marketplace",
],
"source": f"./plugins/{plugin_name}",
}
def _render_claude_marketplace(metadata: dict[str, Any], bundles: list[dict[str, Any]]) -> dict[str, Any]:
plugins = [
{
"name": ROOT_CLAUDE_PLUGIN_NAME,
"version": metadata["version"],
"description": (
"Expose the full repository `skills/` tree to Claude Code through a "
"single marketplace entry."
),
"author": AUTHOR,
"homepage": REPO_URL,
"repository": REPO_URL,
"license": "MIT",
"keywords": [
"claude-code",
"skills",
"agentic-skills",
"plugin",
"marketplace",
],
"source": "./",
}
]
plugins.extend(_bundle_claude_marketplace_entry(metadata, bundle) for bundle in bundles)
return {
"name": ROOT_CLAUDE_PLUGIN_NAME,
"owner": AUTHOR,
"metadata": {
"description": (
"Claude Code marketplace entries for the full Antigravity Awesome Skills "
"library and its editorial bundles."
),
"version": metadata["version"],
},
"plugins": plugins,
}
def _render_codex_marketplace(bundles: list[dict[str, Any]]) -> dict[str, Any]:
plugins: list[dict[str, Any]] = [
{
"name": ROOT_CODEX_PLUGIN_NAME,
"source": {
"source": "local",
"path": f"./plugins/{ROOT_CODEX_PLUGIN_NAME}",
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
},
"category": "Productivity",
}
]
for bundle in bundles:
plugins.append(
{
"name": _bundle_plugin_name(bundle["id"]),
"source": {
"source": "local",
"path": f'./plugins/{_bundle_plugin_name(bundle["id"])}',
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL",
},
"category": _clean_group_label(bundle["group"]),
}
)
return {
"name": ROOT_CODEX_PLUGIN_NAME,
"interface": {
"displayName": "Antigravity Awesome Skills",
},
"plugins": plugins,
}
def _sync_bundle_plugin_directory(root: Path, metadata: dict[str, Any], bundle: dict[str, Any]) -> None:
plugin_name = _bundle_plugin_name(bundle["id"])
plugin_root = root / "plugins" / plugin_name
if plugin_root.exists():
shutil.rmtree(plugin_root)
bundle_skills_root = plugin_root / "skills"
bundle_skills_root.mkdir(parents=True, exist_ok=True)
for skill in bundle["skills"]:
_copy_skill_directory(root, skill["id"], bundle_skills_root)
_write_json(
plugin_root / ".claude-plugin" / "plugin.json",
_bundle_claude_plugin_manifest(metadata, bundle),
)
_write_json(
plugin_root / ".codex-plugin" / "plugin.json",
_bundle_codex_plugin_manifest(metadata, bundle),
)
def sync_editorial_bundle_plugins(root: Path, metadata: dict[str, Any], bundles: list[dict[str, Any]]) -> None:
plugins_root = root / "plugins"
for candidate in plugins_root.glob("antigravity-bundle-*"):
if candidate.is_dir():
shutil.rmtree(candidate)
for bundle in bundles:
_sync_bundle_plugin_directory(root, metadata, bundle)
def load_editorial_bundles(root: Path) -> list[dict[str, Any]]:
root = Path(root)
payload = _read_json(root / EDITORIAL_BUNDLES_PATH)
return _validate_editorial_bundles(root, payload)
def sync_editorial_bundles(root: Path) -> None:
metadata = load_metadata(str(root))
bundles = load_editorial_bundles(root)
_write_text(root / "docs" / "users" / "bundles.md", render_bundles_doc(root, metadata, bundles))
_write_json(root / CLAUDE_PLUGIN_PATH, _root_claude_plugin_manifest(metadata))
_write_json(root / CLAUDE_MARKETPLACE_PATH, _render_claude_marketplace(metadata, bundles))
_write_json(root / CODEX_ROOT_PLUGIN_PATH, _root_codex_plugin_manifest(metadata))
_write_json(root / CODEX_MARKETPLACE_PATH, _render_codex_marketplace(bundles))
sync_editorial_bundle_plugins(root, metadata, bundles)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Sync editorial bundle docs and plugin marketplaces.")
parser.add_argument(
"--check",
action="store_true",
help="Validate the editorial bundles manifest and exit without writing files.",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
root = find_repo_root(__file__)
if args.check:
metadata = load_metadata(str(root))
bundles = load_editorial_bundles(root)
expected_doc = render_bundles_doc(root, metadata, bundles)
current_doc = (root / "docs" / "users" / "bundles.md").read_text(encoding="utf-8")
if current_doc != expected_doc:
raise SystemExit("docs/users/bundles.md is out of sync with data/editorial-bundles.json")
print("✅ Editorial bundles manifest and generated doc are in sync.")
return 0
sync_editorial_bundles(root)
print("✅ Editorial bundles synced.")
return 0
if __name__ == "__main__":
configure_utf8_output()
raise SystemExit(main())

View File

@@ -7,6 +7,7 @@ import subprocess
import sys
from pathlib import Path
from sync_editorial_bundles import load_editorial_bundles, render_bundles_doc
from update_readme import configure_utf8_output, find_repo_root, load_metadata, update_readme
@@ -181,7 +182,14 @@ def sync_getting_started(content: str, metadata: dict) -> str:
return content
def sync_bundles_doc(content: str, metadata: dict) -> str:
def sync_bundles_doc(content: str, metadata: dict, base_dir: str | Path | None = None) -> str:
root = Path(base_dir) if base_dir is not None else Path(find_repo_root(__file__))
manifest_path = root / "data" / "editorial-bundles.json"
template_path = root / "tools" / "templates" / "editorial-bundles.md.tmpl"
if manifest_path.is_file() and template_path.is_file():
bundles = load_editorial_bundles(root)
return render_bundles_doc(root, metadata, bundles)
bundle_count = count_documented_bundles(content)
if bundle_count == 0:
bundle_count = 36
@@ -298,7 +306,14 @@ def sync_curated_docs(base_dir: str, metadata: dict, dry_run: bool) -> int:
updated_files = 0
updated_files += int(update_text_file(root / "README.md", sync_readme_copy, metadata, dry_run))
updated_files += int(update_text_file(root / "docs" / "users" / "getting-started.md", sync_getting_started, metadata, dry_run))
updated_files += int(update_text_file(root / "docs" / "users" / "bundles.md", sync_bundles_doc, metadata, dry_run))
updated_files += int(
update_text_file(
root / "docs" / "users" / "bundles.md",
lambda content, current_metadata: sync_bundles_doc(content, current_metadata, root),
metadata,
dry_run,
)
)
updated_files += int(update_text_file(root / "docs" / "integrations" / "jetski-cortex.md", sync_jetski_cortex, metadata, dry_run))
for path, replacements in regex_text_replacements:

View File

@@ -5,10 +5,25 @@ const { findProjectRoot } = require("../../lib/project-root");
const projectRoot = findProjectRoot(__dirname);
const marketplacePath = path.join(projectRoot, ".claude-plugin", "marketplace.json");
const editorialBundlesPath = path.join(projectRoot, "data", "editorial-bundles.json");
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
const editorialBundles = JSON.parse(fs.readFileSync(editorialBundlesPath, "utf8")).bundles || [];
assert.ok(Array.isArray(marketplace.plugins), "marketplace.json must define a plugins array");
assert.ok(marketplace.plugins.length > 0, "marketplace.json must contain at least one plugin");
assert.strictEqual(
marketplace.plugins[0]?.name,
"antigravity-awesome-skills",
"full library Claude plugin should remain the first marketplace entry",
);
const expectedBundlePluginNames = editorialBundles.map((bundle) => `antigravity-bundle-${bundle.id}`);
for (const pluginName of expectedBundlePluginNames) {
assert.ok(
marketplace.plugins.some((plugin) => plugin.name === pluginName),
`marketplace.json must contain bundle plugin ${pluginName}`,
);
}
for (const plugin of marketplace.plugins) {
assert.strictEqual(
@@ -18,7 +33,7 @@ for (const plugin of marketplace.plugins) {
);
assert.ok(
plugin.source.startsWith("./"),
`plugin ${plugin.name || "<unnamed>"} source must be a relative path starting with ./`,
`plugin ${plugin.name || "<unnamed>"} source must be a repo-relative path starting with ./`,
);
}

View File

@@ -5,7 +5,9 @@ const { findProjectRoot } = require("../../lib/project-root");
const projectRoot = findProjectRoot(__dirname);
const marketplacePath = path.join(projectRoot, ".agents", "plugins", "marketplace.json");
const editorialBundlesPath = path.join(projectRoot, "data", "editorial-bundles.json");
const marketplace = JSON.parse(fs.readFileSync(marketplacePath, "utf8"));
const editorialBundles = JSON.parse(fs.readFileSync(editorialBundlesPath, "utf8")).bundles || [];
assert.strictEqual(
marketplace.name,
@@ -19,6 +21,11 @@ assert.strictEqual(
);
assert.ok(Array.isArray(marketplace.plugins), "marketplace.json must define a plugins array");
assert.ok(marketplace.plugins.length > 0, "marketplace.json must contain at least one plugin");
assert.strictEqual(
marketplace.plugins[0]?.name,
"antigravity-awesome-skills",
"full library Codex plugin should remain the first marketplace entry",
);
const pluginEntry = marketplace.plugins.find((plugin) => plugin.name === "antigravity-awesome-skills");
assert.ok(pluginEntry, "marketplace.json must include the antigravity-awesome-skills plugin entry");
@@ -58,4 +65,23 @@ const pluginSkillsPath = path.join(pluginRoot, "skills");
assert.ok(fs.existsSync(pluginSkillsPath), "Codex plugin skills path must exist");
assert.ok(fs.statSync(pluginSkillsPath).isDirectory(), "Codex plugin skills path must be a directory");
for (const bundle of editorialBundles) {
const bundlePluginName = `antigravity-bundle-${bundle.id}`;
const bundleEntry = marketplace.plugins.find((plugin) => plugin.name === bundlePluginName);
assert.ok(bundleEntry, `marketplace.json must include bundle plugin ${bundlePluginName}`);
assert.deepStrictEqual(
bundleEntry.source,
{
source: "local",
path: `./plugins/${bundlePluginName}`,
},
`bundle plugin ${bundlePluginName} should resolve to the expected repo-local directory`,
);
assert.strictEqual(
bundleEntry.category,
bundle.group.replace(/^[^A-Za-z0-9]+/, "").trim(),
`bundle plugin ${bundlePluginName} should derive its category from the bundle group`,
);
}
console.log("ok");

View File

@@ -15,6 +15,7 @@ const LOCAL_TEST_COMMANDS = [
[path.join(TOOL_TESTS, "build_catalog_bundles.test.js")],
[path.join(TOOL_TESTS, "claude_plugin_marketplace.test.js")],
[path.join(TOOL_TESTS, "codex_plugin_marketplace.test.js")],
[path.join(TOOL_SCRIPTS, "run-python.js"), path.join(TOOL_TESTS, "test_editorial_bundles.py")],
[path.join(TOOL_TESTS, "installer_antigravity_guidance.test.js")],
[path.join(TOOL_TESTS, "jetski_gemini_loader.test.cjs")],
[path.join(TOOL_TESTS, "npm_package_contents.test.js")],

View File

@@ -1,4 +1,5 @@
import importlib.util
import json
import pathlib
import sys
import tempfile
@@ -27,23 +28,31 @@ class BundleActivationSecurityTests(unittest.TestCase):
formatted = get_bundle_skills.format_skills_for_batch([
"safe-skill",
"nested.skill_2",
"game-development/game-design",
"unsafe&calc",
"another|bad",
])
self.assertEqual(formatted, "safe-skill\nnested.skill_2\n")
self.assertEqual(formatted, "safe-skill\nnested.skill_2\ngame-development/game-design\n")
def test_get_bundle_skills_rejects_unsafe_bundle_entries(self):
def test_get_bundle_skills_rejects_unsafe_bundle_entries_from_manifest(self):
with tempfile.TemporaryDirectory() as temp_dir:
bundles_path = pathlib.Path(temp_dir) / "bundles.md"
bundles_path = pathlib.Path(temp_dir) / "editorial-bundles.json"
bundles_path.write_text(
"\n".join(
[
"### Essentials",
"- [`safe-skill`](../../skills/safe-skill/)",
"- [`unsafe&calc`](../../skills/unsafe/)",
"- [`safe_two`](../../skills/safe_two/)",
]
json.dumps(
{
"bundles": [
{
"id": "essentials",
"name": "Essentials",
"skills": [
{"id": "safe-skill"},
{"id": "unsafe&calc"},
{"id": "safe_two"},
],
}
]
}
),
encoding="utf-8",
)
@@ -55,6 +64,11 @@ class BundleActivationSecurityTests(unittest.TestCase):
self.assertEqual(skills, ["safe-skill", "safe_two"])
def test_nested_skill_ids_are_allowed_when_safe(self):
self.assertTrue(get_bundle_skills.is_safe_skill_id("game-development/game-design"))
self.assertFalse(get_bundle_skills.is_safe_skill_id("../escape"))
self.assertFalse(get_bundle_skills.is_safe_skill_id("game-development/../escape"))
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,104 @@
import importlib.util
import pathlib
import sys
import unittest
REPO_ROOT = pathlib.Path(__file__).resolve().parents[3]
TOOLS_SCRIPTS = REPO_ROOT / "tools" / "scripts"
def load_module(module_path: pathlib.Path, module_name: str):
sys.path.insert(0, str(module_path.parent))
spec = importlib.util.spec_from_file_location(module_name, module_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
editorial_bundles = load_module(
TOOLS_SCRIPTS / "sync_editorial_bundles.py",
"sync_editorial_bundles",
)
get_bundle_skills = load_module(
TOOLS_SCRIPTS / "get-bundle-skills.py",
"get_bundle_skills_json",
)
class EditorialBundlesTests(unittest.TestCase):
def setUp(self):
self.manifest_bundles = editorial_bundles.load_editorial_bundles(REPO_ROOT)
def test_manifest_has_unique_ids_and_existing_skills(self):
bundle_ids = [bundle["id"] for bundle in self.manifest_bundles]
self.assertEqual(len(bundle_ids), len(set(bundle_ids)))
for bundle in self.manifest_bundles:
self.assertEqual(bundle["id"], get_bundle_skills._normalize_bundle_query(bundle["name"]))
self.assertTrue(bundle["skills"], f'bundle "{bundle["id"]}" should not be empty')
for skill in bundle["skills"]:
self.assertTrue((REPO_ROOT / "skills" / skill["id"]).exists())
def test_bundles_doc_matches_renderer(self):
metadata = editorial_bundles.load_metadata(str(REPO_ROOT))
expected = editorial_bundles.render_bundles_doc(REPO_ROOT, metadata, self.manifest_bundles)
actual = (REPO_ROOT / "docs" / "users" / "bundles.md").read_text(encoding="utf-8")
self.assertEqual(actual, expected)
def test_get_bundle_skills_reads_json_manifest_by_name_and_id(self):
expected = ["concise-planning", "git-pushing", "kaizen", "lint-and-validate", "systematic-debugging"]
self.assertEqual(get_bundle_skills.get_bundle_skills(["Essentials"]), expected)
self.assertEqual(get_bundle_skills.get_bundle_skills(["essentials"]), expected)
web_wizard_skills = get_bundle_skills.get_bundle_skills(["web-wizard"])
self.assertIn("form-cro", web_wizard_skills)
self.assertIn("react-best-practices", web_wizard_skills)
self.assertIn(
"game-development/game-design",
get_bundle_skills.get_bundle_skills(["indie-game-dev"]),
)
def test_generated_bundle_plugin_contains_expected_skills(self):
essentials_plugin = REPO_ROOT / "plugins" / "antigravity-bundle-essentials" / "skills"
expected_ids = {
skill["id"]
for skill in next(bundle for bundle in self.manifest_bundles if bundle["id"] == "essentials")["skills"]
}
actual_ids = {
str(path.relative_to(essentials_plugin))
for path in essentials_plugin.rglob("SKILL.md")
}
self.assertEqual(actual_ids, {f"{skill_id}/SKILL.md" for skill_id in expected_ids})
sample_skill_dir = essentials_plugin / "concise-planning"
self.assertTrue((sample_skill_dir / "SKILL.md").is_file())
def test_generated_plugin_count_matches_manifest(self):
generated_plugins = sorted(path.name for path in (REPO_ROOT / "plugins").iterdir() if path.is_dir() and path.name.startswith("antigravity-bundle-"))
expected_plugins = sorted(f'antigravity-bundle-{bundle["id"]}' for bundle in self.manifest_bundles)
self.assertEqual(generated_plugins, expected_plugins)
def test_sample_bundle_copy_matches_source_file_inventory(self):
sample_bundle = next(bundle for bundle in self.manifest_bundles if bundle["id"] == "documents-presentations")
plugin_skills_root = REPO_ROOT / "plugins" / "antigravity-bundle-documents-presentations" / "skills"
for skill in sample_bundle["skills"]:
source_dir = REPO_ROOT / "skills" / skill["id"]
copied_dir = plugin_skills_root / skill["id"]
self.assertTrue(copied_dir.is_dir(), f'copied skill dir missing for {skill["id"]}')
source_files = sorted(
str(path.relative_to(source_dir))
for path in source_dir.rglob("*")
if path.is_file()
)
copied_files = sorted(
str(path.relative_to(copied_dir))
for path in copied_dir.rglob("*")
if path.is_file()
)
self.assertEqual(copied_files, source_files, f'copied bundle skill should match source inventory for {skill["id"]}')
if __name__ == "__main__":
unittest.main()

View File

@@ -0,0 +1,128 @@
# Antigravity Skill Bundles
> **Curated collections of skills organized by role and expertise level.** Don't know where to start? Pick a bundle below to get a curated set of skills for your role.
> These packs are curated starter recommendations for humans. Generated bundle ids in `data/bundles.json` are broader catalog/workflow groupings and do not need to map 1:1 to the editorial packs below.
> **Important:** bundles are installable plugin subsets and activation presets, not invokable mega-skills such as `@web-wizard` or `/essentials-bundle`. Use the individual skills listed in the pack, install the bundle as a dedicated marketplace plugin, or use the activation scripts if you want only that bundle's skills active in your live Antigravity directory.
## Quick Start
1. **Install the repository or bundle plugin:**
```bash
npx antigravity-awesome-skills
# or clone manually
git clone https://github.com/sickn33/antigravity-awesome-skills.git .agent/skills
```
2. **Choose your bundle** from the list below based on your role or interests.
3. **Use bundle plugins or individual skills** in your AI assistant:
- Claude Code: install the matching marketplace bundle plugin, or invoke `>> /skill-name help me...`
- Codex CLI / Codex app: install the matching bundle plugin where plugin marketplaces are available, or invoke `Use skill-name...`
- Cursor: `@skill-name` in chat
- Gemini CLI: `Use skill-name...`
If you want a bundle to behave like a focused active subset instead of a full install, use:
- macOS/Linux: `./scripts/activate-skills.sh --clear Essentials`
- macOS/Linux: `./scripts/activate-skills.sh --clear "Web Wizard"`
- Windows: `.\scripts\activate-skills.bat --clear Essentials`
---
{{bundle_sections}}
## 📚 How to Use Bundles
### 1) Pick by immediate goal
- Need to ship a feature now: `Essentials` + one domain pack (`Web Wizard`, `Python Pro`, `DevOps & Cloud`).
- Need reliability and hardening: add `QA & Testing` + `Security Developer`.
- Need product growth: add `Startup Founder` or `Marketing & Growth`.
### 2) Start with 3-5 skills, not 20
Pick the minimum set for your current milestone. Expand only when you hit a real gap.
### 3) Invoke skills consistently
- **Claude Code**: install a bundle plugin or use `>> /skill-name help me...`
- **Codex CLI**: install a bundle plugin where marketplaces are available, or use `Use skill-name...`
- **Cursor**: `@skill-name` in chat
- **Gemini CLI**: `Use skill-name...`
### 4) Build your personal shortlist
Keep a small list of high-frequency skills and reuse it across tasks to reduce context switching.
## 🧩 Recommended Bundle Combos
### Ship a SaaS MVP (2 weeks)
`Essentials` + `Full-Stack Developer` + `QA & Testing` + `Startup Founder`
### Harden an existing production app
`Essentials` + `Security Developer` + `DevOps & Cloud` + `Observability & Monitoring`
### Build an AI product
`Essentials` + `Agent Architect` + `LLM Application Developer` + `Data Engineering`
### Grow traffic and conversions
`Web Wizard` + `Marketing & Growth` + `Data & Analytics`
### Launch and maintain open source
`Essentials` + `OSS Maintainer` + `Architecture & Design`
---
## Learning Paths
### Beginner → Intermediate → Advanced
**Web Development:**
1. Start: `Essentials` → `Web Wizard`
2. Grow: `Full-Stack Developer` → `Architecture & Design`
3. Master: `Observability & Monitoring` → `Security Developer`
**AI/ML:**
1. Start: `Essentials` → `Agent Architect`
2. Grow: `LLM Application Developer` → `Data Engineering`
3. Master: Advanced RAG and agent orchestration
**Security:**
1. Start: `Essentials` → `Security Developer`
2. Grow: `Security Engineer` → Advanced pentesting
3. Master: Red team tactics and threat modeling
**Open Source Maintenance:**
1. Start: `Essentials` → `OSS Maintainer`
2. Grow: `Architecture & Design` → `QA & Testing`
3. Master: `Skill Author` + release automation workflows
---
## Contributing
Found a skill that should be in a bundle? Or want to create a new bundle? [Open an issue](https://github.com/sickn33/antigravity-awesome-skills/issues) or submit a PR!
---
## Related Documentation
- [Getting Started Guide](getting-started.md)
- [Full Skill Catalog](../../CATALOG.md)
- [Contributing Guide](../../CONTRIBUTING.md)
---
_Last updated: March 2026 | Total Skills: {{total_skills_label}} | Total Bundles: {{bundle_count}}_