Files
.pouch/skiff/project.py
T
2026-07-04 10:50:03 +08:00

102 lines
3.1 KiB
Python

"""项目级 .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)