"""自研 skill 发现与解析。""" from __future__ import annotations from pathlib import Path from skiff.paths import SKILLS_DIR, TEMPLATE_DIR, ensure_skills_home from skiff.registry import external_skill_path, load_registry def list_owned_skills() -> list[str]: ensure_skills_home() if not SKILLS_DIR.is_dir(): return [] names: list[str] = [] for entry in sorted(SKILLS_DIR.iterdir()): if not entry.is_dir(): continue if entry.name.startswith("_"): continue if (entry / "SKILL.md").is_file(): names.append(entry.name) return names def owned_skill_path(name: str) -> Path: path = SKILLS_DIR / name if not (path / "SKILL.md").is_file(): raise SystemExit(f"自研 skill 不存在: {name}") return path def resolve_skill_source(name: str, *, source: str | None = None) -> tuple[Path, str]: """返回 (skill_path, kind),kind 为 owned 或 external。""" ensure_skills_home() owned = SKILLS_DIR / name if source in (None, "owned") and (owned / "SKILL.md").is_file(): return owned, "owned" registry = load_registry() if source in (None, "registry") and name in registry: path = external_skill_path(name, registry[name]) if not path.exists(): raise SystemExit( f"外部 skill {name!r} 尚未 fetch。请先运行: skiff fetch {name}" ) return path, "external" if (owned / "SKILL.md").is_file(): return owned, "owned" raise SystemExit(f"找不到 skill: {name}") def validate_skill_name(name: str) -> None: import re if not re.fullmatch(r"[a-z0-9]+(?:-[a-z0-9]+)*", name): raise SystemExit( f"skill 名称无效: {name!r}(小写 + 连字符,如 security-review)" ) if name == "_template": raise SystemExit("不能使用保留名 _template")