#!/usr/bin/env python3 """检查 CraftKit Skill 的基础结构、引用和敏感内容。""" from __future__ import annotations import argparse import json import re from pathlib import Path NAME_RE = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$") LINK_RE = re.compile(r"\[[^]]+]\((?!https?://|#)([^)]+)\)") SECRET_RE = re.compile( r"(?i)(api[_-]?key|password|private[_-]?key|access[_-]?token)\s*[:=]\s*[^\s]+" ) def frontmatter(text: str) -> dict[str, str]: """解析当前校验所需的简单顶层 frontmatter 字段。""" if not text.startswith("---\n"): return {} end = text.find("\n---", 4) if end < 0: return {} values: dict[str, str] = {} for line in text[4:end].splitlines(): if ":" in line and not line.startswith((" ", "\t")): key, value = line.split(":", 1) values[key.strip()] = value.strip().strip("'\"") return values def check_skill(root: Path, forbidden: list[str]) -> dict: """返回结构化问题列表,不修改被检查目录。""" issues: list[dict[str, str]] = [] skill_md = root / "SKILL.md" if not skill_md.is_file(): return {"skill": root.name, "issues": [{"code": "missing-skill-md", "path": "SKILL.md"}]} text = skill_md.read_text(encoding="utf-8-sig") metadata = frontmatter(text) name = metadata.get("name", "") if not NAME_RE.fullmatch(name): issues.append({"code": "invalid-name", "path": "SKILL.md"}) if name != root.name: issues.append({"code": "name-path-mismatch", "path": "SKILL.md"}) if not metadata.get("description"): issues.append({"code": "missing-description", "path": "SKILL.md"}) for path in sorted(item for item in root.rglob("*") if item.is_file()): relative = path.relative_to(root).as_posix() content = path.read_text(encoding="utf-8-sig", errors="replace") if "[TODO:" in content: issues.append({"code": "todo-placeholder", "path": relative}) if SECRET_RE.search(content): issues.append({"code": "possible-secret", "path": relative}) lowered = content.casefold() for term in forbidden: if term.casefold() in lowered: issues.append({"code": f"forbidden:{term}", "path": relative}) if path.suffix.lower() == ".md": for link in LINK_RE.findall(content): target = link.split("#", 1)[0] if target and not (path.parent / target).resolve().is_file(): issues.append({"code": "broken-link", "path": f"{relative}:{link}"}) return {"skill": root.name, "issues": issues} def main() -> None: parser = argparse.ArgumentParser(description="检查 CraftKit Skill") parser.add_argument("skill", nargs="+", type=Path) parser.add_argument("--forbid", action="append", default=[]) args = parser.parse_args() results = [check_skill(path.resolve(), args.forbid) for path in args.skill] print(json.dumps({"results": results}, ensure_ascii=False, indent=2)) raise SystemExit(1 if any(item["issues"] for item in results) else 0) if __name__ == "__main__": main()