mirror of
https://github.com/pulumi/agent-skills.git
synced 2026-09-14 20:16:47 +08:00
d083ad2cc8
* Add combined pulumi plugin for one-install marketplace distribution The Claude marketplace 'pulumi' entry now points at a combined plugin defined at the repo root, whose skills field bundles the authoring, migration, and delegation skill groups. A single '/plugin install pulumi' covers all 14 end-user skills; granular plugins remain for subset installs. Also adds the official square logo asset for marketplace listings, raises pulumi-delegation to three Codex starter prompts, and updates the install docs. * Review fixes: version 2.0.0 + displayName, doc counts, missing skill listing Bump the combined plugin to 2.0.0 since it replaces the granular 'pulumi' plugin (also 1.0.0) with different contents, and add a displayName. Fix the README wording that implied pulumi-package- maintenance is part of the combined plugin. Correct stale skill counts (migration 4 to 5, pulumi 7 to 8) and list pulumi-debug-failed-operation in both docs; it was missing from README and AGENTS.md. * Reword combined plugin description to say what Pulumi is * Add HCL to the combined plugin language list * Extend the combined pulumi plugin to Codex with a root .codex-plugin manifest The Codex catalog still pointed the pulumi entry at ./pulumi (8 skills) while the Claude marketplace pointed it at ./ (14 skills), so the same plugin name installed different contents per ecosystem. Add a root .codex-plugin/plugin.json mirroring the combined Claude manifest and point the catalog at ./. Verified with codex-cli 0.146.0: the skills array registers all 14 skills under the pulumi namespace (checked via codex debug prompt-input). Manifest tests now cover the root manifests and accept both string and array skills fields.
106 lines
4.1 KiB
Python
106 lines
4.1 KiB
Python
"""
|
|
Validate plugin manifests for Claude Code and Codex.
|
|
|
|
Checks that every plugin.json parses, has the fields each ecosystem requires,
|
|
and that marketplace catalogs reference plugin directories that actually exist.
|
|
|
|
Run with:
|
|
uv run pytest tests/test_manifests.py -v
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
REPO_ROOT = Path(__file__).parent.parent
|
|
|
|
CODEX_REQUIRED_FIELDS = ("name", "version", "description", "skills")
|
|
CLAUDE_REQUIRED_FIELDS = ("name", "version", "description")
|
|
|
|
|
|
def _codex_manifests() -> list[Path]:
|
|
return sorted(REPO_ROOT.glob(".codex-plugin/plugin.json")) + sorted(
|
|
REPO_ROOT.glob("*/.codex-plugin/plugin.json")
|
|
)
|
|
|
|
|
|
def _claude_manifests() -> list[Path]:
|
|
return sorted(REPO_ROOT.glob(".claude-plugin/plugin.json")) + sorted(
|
|
REPO_ROOT.glob("*/.claude-plugin/plugin.json")
|
|
)
|
|
|
|
|
|
def _rel(path: Path) -> str:
|
|
return str(path.relative_to(REPO_ROOT))
|
|
|
|
|
|
@pytest.mark.parametrize("manifest", _codex_manifests(), ids=_rel)
|
|
def test_codex_plugin_manifest(manifest: Path) -> None:
|
|
data = json.loads(manifest.read_text())
|
|
for field in CODEX_REQUIRED_FIELDS:
|
|
assert field in data, f"{_rel(manifest)}: missing field `{field}`"
|
|
skills = data["skills"]
|
|
for skills_entry in skills if isinstance(skills, list) else [skills]:
|
|
skills_path = (manifest.parent.parent / skills_entry.lstrip("./")).resolve()
|
|
assert skills_path.is_dir(), (
|
|
f"{_rel(manifest)}: skills path `{skills_entry}` does not resolve to a directory"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize("manifest", _claude_manifests(), ids=_rel)
|
|
def test_claude_plugin_manifest(manifest: Path) -> None:
|
|
data = json.loads(manifest.read_text())
|
|
for field in CLAUDE_REQUIRED_FIELDS:
|
|
assert field in data, f"{_rel(manifest)}: missing field `{field}`"
|
|
|
|
|
|
def test_codex_marketplace() -> None:
|
|
marketplace = REPO_ROOT / ".agents" / "plugins" / "marketplace.json"
|
|
if not marketplace.exists():
|
|
pytest.skip("Codex marketplace.json not present")
|
|
data = json.loads(marketplace.read_text())
|
|
plugins = data.get("plugins", [])
|
|
assert plugins, f"{_rel(marketplace)}: no plugins listed"
|
|
for entry in plugins:
|
|
name = entry["name"]
|
|
source = entry["source"]
|
|
assert source.get("source") == "local", (
|
|
f"{_rel(marketplace)}: plugin {name} uses unsupported source `{source.get('source')}`; "
|
|
"this test only validates `local` sources"
|
|
)
|
|
plugin_dir = (REPO_ROOT / source["path"].lstrip("./")).resolve()
|
|
plugin_manifest = plugin_dir / ".codex-plugin" / "plugin.json"
|
|
assert plugin_manifest.exists(), (
|
|
f"{_rel(marketplace)}: plugin {name} points at `{source['path']}` "
|
|
f"but `{plugin_manifest.relative_to(REPO_ROOT)}` does not exist"
|
|
)
|
|
manifest_name = json.loads(plugin_manifest.read_text())["name"]
|
|
assert manifest_name == name, (
|
|
f"{_rel(marketplace)}: plugin name `{name}` does not match "
|
|
f"`{plugin_manifest.relative_to(REPO_ROOT)}` name `{manifest_name}`"
|
|
)
|
|
|
|
|
|
def test_claude_marketplace() -> None:
|
|
marketplace = REPO_ROOT / ".claude-plugin" / "marketplace.json"
|
|
if not marketplace.exists():
|
|
pytest.skip("Claude marketplace.json not present")
|
|
data = json.loads(marketplace.read_text())
|
|
plugins = data.get("plugins", [])
|
|
assert plugins, f"{_rel(marketplace)}: no plugins listed"
|
|
for entry in plugins:
|
|
name = entry["name"]
|
|
source = entry["source"]
|
|
plugin_dir = (REPO_ROOT / source.lstrip("./")).resolve()
|
|
plugin_manifest = plugin_dir / ".claude-plugin" / "plugin.json"
|
|
assert plugin_manifest.exists(), (
|
|
f"{_rel(marketplace)}: plugin {name} points at `{source}` "
|
|
f"but `{plugin_manifest.relative_to(REPO_ROOT)}` does not exist"
|
|
)
|
|
manifest_name = json.loads(plugin_manifest.read_text())["name"]
|
|
assert manifest_name == name, (
|
|
f"{_rel(marketplace)}: plugin name `{name}` does not match "
|
|
f"`{plugin_manifest.relative_to(REPO_ROOT)}` name `{manifest_name}`"
|
|
)
|