feat: add skiff
This commit is contained in:
@@ -0,0 +1,101 @@
|
||||
"""项目级 .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)
|
||||
Reference in New Issue
Block a user