"""项目级 .skills.yaml 管理。""" from __future__ import annotations from pathlib import Path from typing import Any from skiff import yaml_io from skiff.paths import PROJECT_MANIFEST from skiff.skills import normalize_source, resolve_skill_source def load_manifest(path: Path | None = None) -> tuple[Path, dict[str, Any]]: path = path or Path.cwd() / PROJECT_MANIFEST if not path.is_file(): return path, {"skills": []} data = yaml_io.safe_load(path.read_text(encoding="utf-8")) or {} if not isinstance(data, dict): raise SystemExit(f".skills.yaml 格式错误: {path}") if "skills" not in data: data["skills"] = [] return path, data def save_manifest(path: Path, data: dict[str, Any]) -> None: path.write_text( yaml_io.safe_dump(data, allow_unicode=True, sort_keys=False), encoding="utf-8", ) def normalize_skill_entry(entry: str | dict[str, Any]) -> dict[str, Any]: if isinstance(entry, str): return {"name": entry, "source": "builtin"} name = entry.get("name") if not name: raise SystemExit(f".skills.yaml 条目缺少 name: {entry}") source = normalize_source(str(entry.get("source", "builtin"))) if source == "catalog" and entry.get("registry"): source = f"catalog:{entry['registry']}" return {"name": name, "source": source, **{k: v for k, v in entry.items() if k not in ("name", "source")}} def manifest_skill_names(data: dict[str, Any]) -> list[str]: return [normalize_skill_entry(e)["name"] for e in data.get("skills", [])] def _entry_to_yaml(entry: dict[str, Any]) -> str | dict[str, Any]: return entry def add_skill_to_manifest( manifest_path: Path, name: str, *, source: str = "builtin", extra: dict[str, Any] | None = None, ) -> None: path = manifest_path if path.is_dir(): path = path / PROJECT_MANIFEST file_path, data = load_manifest(path) if path.is_file() else (path, {"skills": []}) if not path.is_file(): file_path = path entries = [normalize_skill_entry(e) for e in data.get("skills", [])] item: dict[str, Any] = {"name": name, "source": source} if extra: item.update(extra) existing_index = next((i for i, entry in enumerate(entries) if entry["name"] == name), None) if existing_index is None: entries.append(item) elif entries[existing_index] == item: return else: entries[existing_index] = item data["skills"] = [_entry_to_yaml(e) for e in entries] save_manifest(file_path, data) def remove_skill_from_manifest(manifest_path: Path, name: str) -> bool: path = manifest_path if not path.is_file(): path = path / PROJECT_MANIFEST file_path, data = load_manifest(path) original = data.get("skills", []) kept = [e for e in original if normalize_skill_entry(e)["name"] != name] if len(kept) == len(original): return False data["skills"] = kept save_manifest(file_path, data) return True def iter_manifest_skills(data: dict[str, Any]) -> list[dict[str, Any]]: return [normalize_skill_entry(e) for e in data.get("skills", [])] def resolve_manifest_skill(entry: dict[str, Any]) -> tuple[Path, str]: name = entry["name"] source = normalize_source(entry.get("source", "builtin")) if source == "catalog" and entry.get("registry"): source = f"catalog:{entry['registry']}" return resolve_skill_source(name, source=source)