106 lines
3.6 KiB
Python
106 lines
3.6 KiB
Python
"""预置 Skill catalog 的配置、发现与本地 checkout 路径。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from skiff import yaml_io
|
|
from skiff.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 skiff.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 skiff.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
|