CtrlK
BlogDocsLog inGet started
Tessl Logo

punkdev/cc2oc

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

1.25x
Quality

92%

Does it follow best practices?

Impact

97%

1.25x

Average score across 2 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

validate_skills.pyskills/migrate-plugin/scripts/

#!/usr/bin/env python3
"""Validate Agent Skill files for OpenCode-compatible sharing."""

from __future__ import annotations

import argparse
from pathlib import Path

from _common import SKILL_NAME_RE, iter_skill_files, parse_frontmatter, print_result, rel


DISCOVERY_PREFIXES = (
    "skills/",
    ".claude/skills/",
    ".agents/skills/",
    ".opencode/skills/",
    "opencode-plugin/skills/",
)


def is_discoverable_or_packaged(relative_path: str) -> bool:
    return any(relative_path.startswith(prefix) for prefix in DISCOVERY_PREFIXES)


def validate_skill_file(path: Path, root: Path) -> tuple[list[dict[str, str]], list[str], list[str]]:
    checks: list[dict[str, str]] = []
    warnings: list[str] = []
    errors: list[str] = []
    relative = rel(path, root)
    frontmatter, parse_errors = parse_frontmatter(path)
    for error in parse_errors:
        errors.append(f"{relative}: {error}")

    name = frontmatter.get("name", "")
    description = frontmatter.get("description", "")
    expected_name = path.parent.name

    def add_check(text: str, passed: bool, evidence: str) -> None:
        checks.append({"path": relative, "text": text, "passed": str(passed).lower(), "evidence": evidence})

    add_check("frontmatter has name", bool(name), name or "missing")
    add_check("frontmatter has description", bool(description), f"{len(description)} chars" if description else "missing")
    add_check("name matches OpenCode skill-name format", bool(name and SKILL_NAME_RE.match(name)), name or "missing")
    add_check("name matches containing directory", name == expected_name, f"name={name or '<missing>'}, directory={expected_name}")
    add_check("description length is 1-1024 characters", 1 <= len(description) <= 1024, f"{len(description)} chars")
    add_check("path is discoverable or package-local", is_discoverable_or_packaged(relative), relative)

    if description and len(description) > 1024:
        errors.append(f"{relative}: description exceeds 1024 characters")
    if name and not SKILL_NAME_RE.match(name):
        errors.append(f"{relative}: name does not match ^[a-z0-9]+(-[a-z0-9]+)*$")
    if name and name != expected_name:
        errors.append(f"{relative}: name does not match containing directory {expected_name}")
    if not is_discoverable_or_packaged(relative):
        warnings.append(f"{relative}: path is not a known OpenCode/Claude/agent discovery path or opencode-plugin packaged skill path")

    return checks, warnings, errors


def validate_skills(root: Path) -> dict:
    root = root.resolve()
    checks: list[dict[str, str]] = []
    warnings: list[str] = []
    errors: list[str] = []
    skill_files = iter_skill_files(root)

    if not skill_files:
        warnings.append("No SKILL.md files found")

    for skill_file in skill_files:
        file_checks, file_warnings, file_errors = validate_skill_file(skill_file, root)
        checks.extend(file_checks)
        warnings.extend(file_warnings)
        errors.extend(file_errors)

    passed = sum(1 for check in checks if check["passed"] == "true")
    return {
        "title": "Agent Skill Validation",
        "summary": {
            "target": str(root),
            "skill_count": len(skill_files),
            "checks_passed": passed,
            "checks_total": len(checks),
            "status": "fail" if errors else "pass",
        },
        "checks": checks,
        "warnings": warnings,
        "errors": errors,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("target", nargs="?", default=".", help="Repo to validate")
    parser.add_argument("--format", choices=("json", "markdown"), default="json")
    args = parser.parse_args()

    result = validate_skills(Path(args.target))
    print_result(result, args.format)
    return 1 if result["errors"] else 0


if __name__ == "__main__":
    raise SystemExit(main())

README.md

tile.json