"""预置 Skill catalog 的配置、发现与本地 checkout 路径。""" from __future__ import annotations import hashlib from pathlib import Path from typing import Any from pouch import yaml_io from pouch.paths import CATALOG_FILE, LEGACY_REGISTRY_FILE def load_catalog(path: Path | None = None) -> dict[str, dict[str, Any]]: path = path or ( CATALOG_FILE if CATALOG_FILE.is_file() else LEGACY_REGISTRY_FILE ) if not path.is_file(): return {} data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {} if not isinstance(data, dict): raise SystemExit(f"catalog 格式错误: {path}") return {k: v for k, v in data.items() if isinstance(v, dict) and not k.startswith("#")} def save_catalog(data: dict[str, dict[str, Any]], path: Path | None = None) -> None: path = path or CATALOG_FILE path.parent.mkdir(parents=True, exist_ok=True) path.write_text(yaml_io.safe_dump(data, allow_unicode=True, sort_keys=False), encoding="utf-8") def catalog_repo(entry: dict[str, Any]) -> str: """Expand a local home-relative repo while leaving remote URLs unchanged.""" repo = str(entry.get("repo", "")) return str(Path(repo).expanduser()) if repo.startswith("~") else repo def catalog_repo_path(entry: dict[str, Any]) -> Path: """Return the shared checkout path for a repo/ref pair.""" from pouch.paths import CATALOG_CACHE_DIR repo = str(entry.get("repo", "")) ref = str(entry.get("ref", "main")) digest = hashlib.sha256(f"{repo}\0{ref}".encode()).hexdigest()[:16] return CATALOG_CACHE_DIR / "_repos" / digest def catalog_checkout_path(name: str, entry: dict[str, Any]) -> Path: """Use a local repo directly, otherwise return its external checkout.""" from pouch.paths import CATALOG_CACHE_DIR configured_repo = str(entry.get("repo", "")) local_repo = Path(catalog_repo(entry)) if configured_repo.startswith("~") and local_repo.is_dir(): return local_repo.resolve() legacy = CATALOG_CACHE_DIR / name return legacy if legacy.is_dir() else catalog_repo_path(entry) def catalog_skill_path(name: str, entry: dict[str, Any] | None = None) -> Path: entry = entry or load_catalog().get(name, {}) subpath = entry.get("path", ".") or "." checkout = catalog_checkout_path(name, entry).resolve() skill_path = (checkout / subpath).resolve() try: skill_path.relative_to(checkout) except ValueError as exc: raise SystemExit( f"catalog 条目 {name!r} 的 path 超出来源仓库: {subpath!r}" ) from exc return skill_path def discover_catalog_skills( name: str, entry: dict[str, Any] | None = None, ) -> dict[str, Path]: """Discover a catalog provider containing one or more skills.""" entry = entry or load_catalog().get(name, {}) root = catalog_skill_path(name, entry) if (root / "SKILL.md").is_file(): return {name: root} if not root.is_dir(): return {} resolved_root = root.resolve() skills: dict[str, Path] = {} for item in sorted(root.iterdir()): skill_md = item / "SKILL.md" if ( item.name.startswith("_") or item.is_symlink() or not item.is_dir() or skill_md.is_symlink() or not skill_md.is_file() ): continue resolved_item = item.resolve() resolved_skill_md = skill_md.resolve() try: resolved_item.relative_to(resolved_root) resolved_skill_md.relative_to(resolved_item) except ValueError: continue skills[item.name] = item return skills