Files
skills/scripts/validate-dist.py
T

134 lines
5.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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/<name>:{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)