"""自研 skill 发现与解析。""" from __future__ import annotations import re 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 read_skill_meta(skill_dir: Path) -> dict[str, str]: skill_md = skill_dir / "SKILL.md" if not skill_md.is_file(): return {} text = skill_md.read_text(encoding="utf-8") if not text.startswith("---"): return {} end = text.find("\n---", 3) if end == -1: return {} frontmatter = text[3:end] meta: dict[str, str] = {} name_match = re.search(r"^name:\s*(.+)$", frontmatter, re.MULTILINE) if name_match: meta["name"] = name_match.group(1).strip().strip("\"'") desc_match = re.search( r"^description:\s*(?:>-|>\||>|-)?\s*\n((?:[ \t].+\n?)+)", frontmatter, re.MULTILINE, ) if desc_match: lines = [line.strip() for line in desc_match.group(1).splitlines() if line.strip()] meta["description"] = " ".join(lines) else: inline = re.search(r"^description:\s*(.+)$", frontmatter, re.MULTILINE) if inline: meta["description"] = inline.group(1).strip().strip("\"'") return meta def skill_description(name: str) -> str | None: meta = read_skill_meta(SKILLS_DIR / name) desc = meta.get("description") return desc.strip() if desc else None 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")