#!/usr/bin/env python3 """Validate the distribution repository and its plugin manifest.""" from __future__ import annotations import json import sys from pathlib import Path from typing import Any REPO_ROOT = Path(__file__).resolve().parents[1] SOURCES_PATH = REPO_ROOT / ".dist-sync" / "sources.json" VERSIONS_PATH = REPO_ROOT / ".dist-sync" / "versions.json" MARKETPLACE_PATH = REPO_ROOT / ".claude-plugin" / "marketplace.json" class ValidationError(RuntimeError): pass def read_json(path: Path) -> Any: try: return json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as error: raise ValidationError(f"无法读取 JSON:{path}: {error}") from error def frontmatter_name(skill_file: Path) -> str: try: lines = skill_file.read_text(encoding="utf-8-sig").splitlines() except (OSError, UnicodeDecodeError) as error: raise ValidationError(f"无法读取 {skill_file}: {error}") from error if not lines or lines[0].strip() != "---": raise ValidationError(f"SKILL.md 缺少 YAML frontmatter:{skill_file}") for line in lines[1:]: if line.strip() == "---": break if line.startswith("name:"): return line.split(":", 1)[1].strip().strip("'\"") raise ValidationError(f"SKILL.md frontmatter 缺少 name:{skill_file}") def main() -> int: sources_doc = read_json(SOURCES_PATH) sources = sources_doc.get("sources") if isinstance(sources_doc, dict) else None if not isinstance(sources, list) or not sources: raise ValidationError("sources.json 必须包含非空 sources 数组") allowlisted: set[str] = set() for source in sources: if not isinstance(source, dict) or not all( isinstance(source.get(field), str) and source[field] for field in ("name", "repository", "asset") ): raise ValidationError("sources.json 包含无效来源") name = source["name"] if name in allowlisted: raise ValidationError(f"sources.json 包含重复名称:{name}") allowlisted.add(name) versions = read_json(VERSIONS_PATH) if not isinstance(versions, dict) or not all( isinstance(name, str) and isinstance(tag, str) and tag for name, tag in versions.items() ): raise ValidationError("versions.json 必须是非空 tag 字符串映射") unknown_versions = set(versions) - allowlisted if unknown_versions: raise ValidationError(f"versions.json 包含未列入同步白名单的 skill:{sorted(unknown_versions)}") marketplace = read_json(MARKETPLACE_PATH) plugins = marketplace.get("plugins") if isinstance(marketplace, dict) else None if not isinstance(plugins, list) or not plugins: raise ValidationError("marketplace.json 必须包含非空 plugins 数组") manifest_names: list[str] = [] manifest_paths: set[Path] = set() for plugin in plugins: skills = plugin.get("skills") if isinstance(plugin, dict) else None if not isinstance(skills, list) or not skills: raise ValidationError("marketplace.json 的每个 plugin 都必须列出 skills") for raw_path in skills: if not isinstance(raw_path, str) or not raw_path.startswith("./skills/"): raise ValidationError(f"无效 marketplace skill 路径:{raw_path!r}") relative = Path(raw_path[2:]) skill_dir = (REPO_ROOT / relative).resolve() skills_root = (REPO_ROOT / "skills").resolve() if skill_dir.parent != skills_root: raise ValidationError(f"skill 必须直接位于 skills/:{raw_path}") if skill_dir in manifest_paths: raise ValidationError(f"marketplace.json 重复列出路径:{raw_path}") manifest_paths.add(skill_dir) skill_file = skill_dir / "SKILL.md" if not skill_file.is_file(): raise ValidationError(f"marketplace 路径缺少 SKILL.md:{raw_path}") actual_name = frontmatter_name(skill_file) if actual_name != skill_dir.name: raise ValidationError( f"{raw_path}/SKILL.md 的 name 为 {actual_name!r},目录名为 {skill_dir.name!r}" ) manifest_names.append(actual_name) if len(manifest_names) != len(set(manifest_names)): raise ValidationError("marketplace.json 中存在重复的 skill name") disk_dirs = { path.resolve() for path in (REPO_ROOT / "skills").iterdir() if path.is_dir() and not path.name.startswith(".") } unlisted = disk_dirs - manifest_paths missing = manifest_paths - disk_dirs if unlisted: raise ValidationError(f"skills/ 中存在未列入 marketplace 的目录:{sorted(path.name for path in unlisted)}") if missing: raise ValidationError(f"marketplace 指向不存在的目录:{sorted(path.name for path in missing)}") for skill_dir in sorted(manifest_paths): for path in skill_dir.rglob("*"): if not path.is_file() or path.stat().st_size > 1024: continue if path.read_bytes().startswith(b"version https://git-lfs.github.com/spec/v1"): raise ValidationError(f"发现 Git LFS pointer,分发副本缺少真实文件:{path.relative_to(REPO_ROOT)}") print(f"分发仓校验通过:{len(manifest_names)} 个 skill,{len(allowlisted)} 个 Release 同步来源") return 0 if __name__ == "__main__": try: raise SystemExit(main()) except ValidationError as error: print(f"校验失败:{error}", file=sys.stderr) raise SystemExit(1)