feat: update readme
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
#!/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())
|
||||
@@ -0,0 +1,144 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Suggest pouch skills from well-known files in a project root."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
INIT = {
|
||||
"ack": "说「/ack 初始化」或 `pouch init ack`;不要静默初始化",
|
||||
"builder": "说「用 builder 初始化」;不要 `pouch init builder`",
|
||||
"deployer": "说「用 deployer 初始化」;不要 `pouch init deployer`",
|
||||
}
|
||||
|
||||
FILE_SIGNALS: dict[str, tuple[str, ...]] = {
|
||||
"ack": (".pouch/ack",),
|
||||
"builder": (
|
||||
"makefile.builder",
|
||||
"debian",
|
||||
"Dockerfile",
|
||||
"Containerfile",
|
||||
".env.builder",
|
||||
),
|
||||
"deployer": (
|
||||
".pouch/deployer",
|
||||
"compose.yaml",
|
||||
"compose.yml",
|
||||
"docker-compose.yaml",
|
||||
"docker-compose.yml",
|
||||
"argocd.yaml",
|
||||
"k8s",
|
||||
"helm",
|
||||
),
|
||||
"manage-release": ("CHANGELOG.md", "CHANGELOG", "VERSION"),
|
||||
"declarative-openspec-loop": ("openspec", ".openspec"),
|
||||
"discussion-notes": ("000inbox",),
|
||||
}
|
||||
|
||||
INITED_MARKERS = {
|
||||
"ack": (".pouch/ack",),
|
||||
"builder": ("makefile.builder",),
|
||||
"deployer": (".pouch/deployer",),
|
||||
}
|
||||
|
||||
|
||||
def project_root(start: Path) -> Path:
|
||||
cur = start.resolve()
|
||||
if cur.is_file():
|
||||
cur = cur.parent
|
||||
for candidate in (cur, *cur.parents):
|
||||
if (candidate / ".git").exists():
|
||||
return candidate
|
||||
return cur
|
||||
|
||||
|
||||
def is_pouch_repo(root: Path) -> bool:
|
||||
return (
|
||||
(root / "install.sh").is_file()
|
||||
and (root / "bin" / "pouch").is_file()
|
||||
and (root / "skills" / "pouch" / "SKILL.md").is_file()
|
||||
)
|
||||
|
||||
|
||||
def existing(root: Path, rel: str) -> bool:
|
||||
return (root / rel).exists()
|
||||
|
||||
|
||||
def hits(root: Path, names: tuple[str, ...]) -> list[str]:
|
||||
return [name for name in names if existing(root, name)]
|
||||
|
||||
|
||||
def suggest(root: Path) -> dict[str, object]:
|
||||
suggested: list[tuple[str, list[str]]] = []
|
||||
already: list[tuple[str, list[str]]] = []
|
||||
init: list[tuple[str, str]] = []
|
||||
for name, names in FILE_SIGNALS.items():
|
||||
found = hits(root, names)
|
||||
if not found:
|
||||
continue
|
||||
inited = hits(root, INITED_MARKERS.get(name, ()))
|
||||
if inited:
|
||||
already.append((name, inited))
|
||||
else:
|
||||
suggested.append((name, found))
|
||||
if name in INIT and not inited:
|
||||
init.append((name, INIT[name]))
|
||||
return {
|
||||
"root": str(root),
|
||||
"this_is_pouch_repo": is_pouch_repo(root),
|
||||
"suggested": suggested,
|
||||
"already": already,
|
||||
"init": init,
|
||||
}
|
||||
|
||||
|
||||
def format_report(data: dict[str, object]) -> str:
|
||||
lines = [f"project: {data['root']}"]
|
||||
if data["this_is_pouch_repo"]:
|
||||
lines.append("this_is_pouch_repo: true")
|
||||
lines.append("不要按业务项目接入。维护 skill 走 pouch 的创建或回流模式。")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
lines.append("this_is_pouch_repo: false")
|
||||
suggested = data["suggested"]
|
||||
already = data["already"]
|
||||
init = data["init"]
|
||||
if suggested:
|
||||
lines.append("suggested:")
|
||||
for name, why in suggested:
|
||||
lines.append(f" {name}\twhy: {', '.join(why)}")
|
||||
else:
|
||||
lines.append("suggested: (none)")
|
||||
if already:
|
||||
lines.append("already:")
|
||||
for name, why in already:
|
||||
lines.append(f" {name}\t{', '.join(why)}")
|
||||
else:
|
||||
lines.append("already: (none)")
|
||||
if init:
|
||||
lines.append("init:")
|
||||
for name, how in init:
|
||||
lines.append(f" {name}\t{how}")
|
||||
else:
|
||||
lines.append("init: (none)")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="根据仓库信号建议 pouch skill")
|
||||
parser.add_argument(
|
||||
"path",
|
||||
nargs="?",
|
||||
default=".",
|
||||
help="项目目录(默认 cwd;向上找 .git)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
root = project_root(Path(args.path))
|
||||
sys.stdout.write(format_report(suggest(root)))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user