Files
basicmachines-co-basic-memory/scripts/validate_claude_plugin.py
T
2026-05-30 14:16:04 -05:00

79 lines
3.0 KiB
Python

#!/usr/bin/env python3
"""Validate the Basic Memory Claude Code plugin layout."""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from validate_skills import parse_frontmatter
ROOT = Path(__file__).resolve().parents[1]
def read_json(path: Path) -> dict:
return json.loads(path.read_text())
def validate_claude_plugin(plugin_dir: Path) -> None:
plugin_dir = plugin_dir.resolve()
plugin_json = plugin_dir / ".claude-plugin/plugin.json"
local_marketplace_json = plugin_dir / ".claude-plugin/marketplace.json"
root_marketplace_json = ROOT / ".claude-plugin/marketplace.json"
for path in [plugin_json, local_marketplace_json, root_marketplace_json]:
if not path.exists():
raise SystemExit(f"Missing Claude plugin manifest: {path}")
plugin = read_json(plugin_json)
if plugin.get("name") != "basic-memory":
raise SystemExit(f"{plugin_json}: expected name=basic-memory")
if not str(plugin.get("repository", "")).endswith(
"/basic-memory/tree/main/plugins/claude-code"
):
raise SystemExit(f"{plugin_json}: repository must point at the monorepo subdirectory")
root_marketplace = read_json(root_marketplace_json)
root_plugin = root_marketplace["plugins"][0]
expected_source = "./plugins/claude-code"
if root_plugin.get("name") != "basic-memory" or root_plugin.get("source") != expected_source:
raise SystemExit(f"{root_marketplace_json}: expected basic-memory source {expected_source}")
local_marketplace = read_json(local_marketplace_json)
local_plugin = local_marketplace["plugins"][0]
if local_plugin.get("name") != "basic-memory" or local_plugin.get("source") != "./":
raise SystemExit(f"{local_marketplace_json}: expected plugin-local source ./")
hooks = plugin_dir / "hooks/hooks.json"
hooks_json = read_json(hooks)
if "PreToolUse" not in hooks_json.get("hooks", {}):
raise SystemExit(f"{hooks}: missing PreToolUse hook")
agent = plugin_dir / "agents/basic-memory-manager.md"
agent_frontmatter = parse_frontmatter(agent)
if agent_frontmatter.get("name") != "basic-memory-manager":
raise SystemExit(f"{agent}: missing basic-memory-manager frontmatter")
skill_dirs = sorted(path for path in (plugin_dir / "skills").iterdir() if path.is_dir())
if not skill_dirs:
raise SystemExit(f"{plugin_dir / 'skills'}: no bundled Claude Code skills")
for skill_dir in skill_dirs:
frontmatter = parse_frontmatter(skill_dir / "SKILL.md")
if frontmatter.get("name") != skill_dir.name:
raise SystemExit(f"{skill_dir}: skill name must match directory")
print(f"validated Claude Code plugin in {plugin_dir}")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("plugin_dir", nargs="?", default="plugins/claude-code")
args = parser.parse_args()
validate_claude_plugin(Path.cwd() / args.plugin_dir)
if __name__ == "__main__":
main()