"""项目级 .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 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": "owned"} name = entry.get("name") if not name: raise SystemExit(f".skills.yaml 条目缺少 name: {entry}") source = entry.get("source", "owned") 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]: if entry.get("source", "owned") == "owned" and set(entry.keys()) <= {"name", "source"}: return entry["name"] return entry def add_skill_to_manifest( manifest_path: Path, name: str, *, source: str = "owned", 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", [])] if any(e["name"] == name for e in entries): return item: dict[str, Any] = {"name": name, "source": source} if extra: item.update(extra) entries.append(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 = entry.get("source", "owned") return resolve_skill_source(name, source=source)