#!/usr/bin/env python3 """Print a human-facing builtin skill list: one-liner + copy-paste example. Reads each skill's README.md. Do not substitute SKILL.md descriptions. """ from __future__ import annotations import argparse import re import sys from pathlib import Path _HEADING_RE = re.compile(r"^#{1,6}\s+") _FENCE_RE = re.compile(r"^```(\w+)?\s*$") def skills_dir() -> Path: here = Path(__file__).resolve() repo_skills = here.parents[2] if (repo_skills / "pouch" / "SKILL.md").is_file(): return repo_skills return Path.home() / ".pouch" / "skills" def _strip_md(text: str) -> str: text = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", text) text = re.sub(r"`([^`]+)`", r"\1", text) return " ".join(text.split()) def _first_sentence(text: str) -> str: text = _strip_md(text).strip() for sep in ("。", "!", "?"): idx = text.find(sep) if idx != -1: return text[: idx + 1] return text.rstrip("::") + ("。" if text else "") def _lede(readme: str) -> str: lines = readme.splitlines() i = 0 while i < len(lines) and ( not lines[i].strip() or _HEADING_RE.match(lines[i]) ): i += 1 chunk: list[str] = [] while i < len(lines): raw = lines[i] stripped = raw.strip() if not stripped: break if _HEADING_RE.match(raw) or stripped.startswith(("- ", "* ", "> ")): break chunk.append(stripped) i += 1 return _first_sentence(" ".join(chunk)) def _section_body(readme: str, titles: tuple[str, ...]) -> str | None: lines = readme.splitlines() start = None for i, line in enumerate(lines): if line.startswith("## ") and line[3:].strip() in titles: start = i + 1 break if start is None: return None end = len(lines) for j in range(start, len(lines)): if lines[j].startswith("## "): end = j break return "\n".join(lines[start:end]) def _first_example(readme: str) -> str: body = _section_body(readme, ("使用示例", "常见用法")) search = body if body is not None else readme lines = search.splitlines() i = 0 while i < len(lines): fence = _FENCE_RE.match(lines[i].strip()) if not fence: i += 1 continue i += 1 block: list[str] = [] while i < len(lines) and not lines[i].strip().startswith("```"): block.append(lines[i]) i += 1 for raw in block: stripped = raw.strip() if not stripped or stripped.startswith("#"): continue return stripped i += 1 return "" def iter_skills(root: Path) -> list[Path]: skills: list[Path] = [] if not root.is_dir(): return skills for entry in sorted(root.iterdir()): if not entry.is_dir() or entry.name.startswith("_"): continue if (entry / "SKILL.md").is_file(): skills.append(entry) return skills def catalog_rows(root: Path) -> list[tuple[str, str, str]]: rows: list[tuple[str, str, str]] = [] errors: list[str] = [] for skill in iter_skills(root): readme_path = skill / "README.md" if not readme_path.is_file(): errors.append(f"{skill.name}: 缺少 README.md") continue text = readme_path.read_text(encoding="utf-8") lede = _lede(text) example = _first_example(text) if not lede: errors.append(f"{skill.name}: README 缺少一句话说明") if not example: errors.append(f"{skill.name}: README 缺少可复制的使用示例") if lede and example: rows.append((skill.name, lede, example)) if errors: raise SystemExit("catalog 失败:\n " + "\n ".join(errors)) return rows def format_table(rows: list[tuple[str, str, str]]) -> str: lines = [ "| Skill | 做什么 | 可以这么说 |", "| --- | --- | --- |", ] for name, lede, example in rows: example = example.replace("|", "\\|") lede = lede.replace("|", "\\|") lines.append(f"| {name} | {lede} | `{example}` |") return "\n".join(lines) + "\n" def main() -> int: parser = argparse.ArgumentParser( description="列出 builtin skill 的一句话说明和可复制示例" ) parser.add_argument( "--skills-dir", type=Path, default=None, help="skill 根目录(默认本仓库 skills/ 或 ~/.pouch/skills)", ) args = parser.parse_args() root = args.skills_dir.resolve() if args.skills_dir else skills_dir() if not root.is_dir(): print(f"找不到 skills 目录: {root}", file=sys.stderr) return 2 sys.stdout.write(format_table(catalog_rows(root))) return 0 if __name__ == "__main__": raise SystemExit(main())