f3cd56b78e
Use ~/.pouch, the pouch CLI, and .pouch.yaml as the SSOT container. Keep the inner skills/ packages, and store ACK project state in .pouch/ack instead of docs/ack.
107 lines
3.5 KiB
Python
107 lines
3.5 KiB
Python
"""项目级 .pouch.yaml 管理。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from pouch import yaml_io
|
|
from pouch.paths import project_manifest
|
|
from pouch.skills import normalize_source, resolve_skill_source
|
|
|
|
|
|
def load_manifest(path: Path | None = None) -> tuple[Path, dict[str, Any]]:
|
|
path = path or project_manifest(Path.cwd())
|
|
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".pouch.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".pouch.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 = project_manifest(path)
|
|
|
|
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 path.is_dir() or not path.is_file():
|
|
path = project_manifest(path if path.is_dir() else path.parent)
|
|
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)
|