Add and ship OpenCode support for one Claude Code plugin at a time. Includes core migration to a reviewable `opencode-plugin/` adapter, maintainer-facing docs, and follow-up CI/versioning setup for package publishing or skill-copy drift checks.
92
92%
Does it follow best practices?
Impact
97%
1.25xAverage score across 2 eval scenarios
Passed
No known issues
#!/usr/bin/env python3
"""Inspect a Claude plugin or skill repo before adding OpenCode support."""
from __future__ import annotations
import argparse
from pathlib import Path
from _common import iter_files, iter_skill_files, print_result, rel
HOOK_EXTENSIONS = {".py", ".sh", ".bash", ".js", ".mjs", ".cjs", ".ts"}
def classify(root: Path, assets: list[dict[str, str]]) -> str:
has_marketplace = any(asset["kind"] == "claude-marketplace" for asset in assets)
has_plugin = any(asset["kind"] == "claude-plugin" for asset in assets)
skill_count = sum(1 for asset in assets if asset["kind"] == "skill")
hook_count = sum(1 for asset in assets if asset["kind"] == "hook-candidate")
if has_marketplace:
return "marketplace"
if hook_count >= 4:
return "hook-heavy"
if has_plugin:
return "single-plugin"
if skill_count > 0:
return "skill-only"
return "unknown"
def inspect_repo(root: Path) -> dict:
root = root.resolve()
assets: list[dict[str, str]] = []
warnings: list[str] = []
for plugin_json in sorted(root.rglob(".claude-plugin/plugin.json")):
location = "root" if plugin_json.parent.parent == root else "nested"
assets.append({"path": rel(plugin_json, root), "kind": "claude-plugin", "verdict": "claude-only", "notes": f"{location} Claude plugin metadata"})
for marketplace_json in sorted(root.rglob(".claude-plugin/marketplace.json")):
assets.append({"path": rel(marketplace_json, root), "kind": "claude-marketplace", "verdict": "scope-decision", "notes": "Decide one package per plugin or scoped family"})
for mcp_json in sorted(root.rglob(".mcp.json")):
assets.append({"path": rel(mcp_json, root), "kind": "claude-mcp", "verdict": "config-migration", "notes": "Convert mcpServers to OpenCode mcp config"})
for hooks_json in sorted(root.rglob("hooks/hooks.json")):
assets.append({"path": rel(hooks_json, root), "kind": "claude-hooks", "verdict": "wrapped-or-rewritten", "notes": "Create per-hook OpenCode parity records"})
for skill_file in iter_skill_files(root):
assets.append({"path": rel(skill_file, root), "kind": "skill", "verdict": "shared", "notes": "Agent Skill source"})
for path in iter_files(root):
relative = rel(path, root)
parts = set(Path(relative).parts)
if path.suffix in HOOK_EXTENSIONS and ("hooks" in parts or ".claude" in parts or ".claude-plugin" in parts):
assets.append({"path": relative, "kind": "hook-candidate", "verdict": "wrapped-or-rewritten", "notes": "Review for OpenCode event parity"})
opencode_package = root / "opencode-plugin" / "package.json"
if opencode_package.exists():
assets.append({"path": rel(opencode_package, root), "kind": "opencode-package", "verdict": "generated-or-existing", "notes": "Validate before reusing"})
opencode_dir = root / ".opencode"
if opencode_dir.exists():
warnings.append(".opencode/ exists; treat it as active OpenCode config, not a neutral generated-files directory")
repo_shape = classify(root, assets)
if repo_shape == "unknown":
warnings.append("No Claude plugin metadata, skills, or hook candidates found; ask the user where the Claude plugin lives")
return {
"title": "OpenCode Support Repo Inspection",
"summary": {
"target": str(root),
"repo_shape": repo_shape,
"asset_count": len(assets),
},
"assets": assets,
"warnings": warnings,
"errors": [],
}
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("target", nargs="?", default=".", help="Repo to inspect")
parser.add_argument("--format", choices=("json", "markdown"), default="json")
args = parser.parse_args()
print_result(inspect_repo(Path(args.target)), args.format)
return 0
if __name__ == "__main__":
raise SystemExit(main())