#!/usr/bin/env python3 """Validate a registry tool submission. Usage: python scripts/validate_tool.py path/to/tool """ from __future__ import annotations import re import sys from pathlib import Path from typing import List import yaml TOOL_NAME_RE = re.compile(r"^[A-Za-z0-9-]{1,64}$") SEMVER_RE = re.compile(r"^(\d+)\.(\d+)\.(\d+)(?:-[0-9A-Za-z.-]+)?(?:\+.+)?$") REQUIRED_README_SECTIONS = ["## Usage", "## Examples"] def find_repo_root(start: Path) -> Path | None: current = start.resolve() while current != current.parent: if (current / "categories" / "categories.yaml").exists(): return current current = current.parent if (current / "categories" / "categories.yaml").exists(): return current return None def load_categories(repo_root: Path) -> List[str]: categories_path = repo_root / "categories" / "categories.yaml" data = yaml.safe_load(categories_path.read_text(encoding="utf-8")) or {} categories = data.get("categories", []) return [c.get("name") for c in categories if c.get("name")] def validate_tool(tool_path: Path) -> List[str]: errors: List[str] = [] if tool_path.is_dir(): config_path = tool_path / "config.yaml" readme_path = tool_path / "README.md" else: config_path = tool_path readme_path = tool_path.parent / "README.md" if not config_path.exists(): return [f"Missing config.yaml at {config_path}"] try: config_text = config_path.read_text(encoding="utf-8") data = yaml.safe_load(config_text) or {} except Exception as exc: return [f"Invalid YAML in config.yaml: {exc}"] name = (data.get("name") or "").strip() version = (data.get("version") or "").strip() description = (data.get("description") or "").strip() category = (data.get("category") or "").strip() if not name: errors.append("Missing required field: name") elif not TOOL_NAME_RE.match(name): errors.append("Tool name must match ^[A-Za-z0-9-]{1,64}$") if not version: errors.append("Missing required field: version") elif not SEMVER_RE.match(version): errors.append("Version must be valid semver (MAJOR.MINOR.PATCH)") if not description: errors.append("Missing required field: description") repo_root = find_repo_root(tool_path) if repo_root: categories = load_categories(repo_root) if category and category not in categories: errors.append(f"Unknown category '{category}' (not in categories.yaml)") else: if category: errors.append("Cannot validate category (categories.yaml not found)") if not readme_path.exists(): errors.append(f"Missing README.md at {readme_path}") else: readme_text = readme_path.read_text(encoding="utf-8") for section in REQUIRED_README_SECTIONS: if section not in readme_text: errors.append(f"README.md missing section: {section}") return errors def main() -> int: if len(sys.argv) < 2: print("Usage: python scripts/validate_tool.py path/to/tool") return 1 tool_path = Path(sys.argv[1]) errors = validate_tool(tool_path) if errors: print("Validation failed:") for err in errors: print(f"- {err}") return 1 print("Validation passed") return 0 if __name__ == "__main__": raise SystemExit(main())