playbook/scripts/playbook.py

275 lines
8.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
import sys
from datetime import datetime, timezone
from pathlib import Path
from shutil import copy2, copytree
import subprocess
import tomllib
ORDER = ["vendor", "sync_templates", "sync_standards", "install_skills", "format_md"]
SCRIPT_DIR = Path(__file__).resolve().parent
PLAYBOOK_ROOT = SCRIPT_DIR.parent
def usage() -> str:
return "Usage:\n python scripts/playbook.py -config <path>\n python scripts/playbook.py -h"
def load_config(path: Path) -> dict:
return tomllib.loads(path.read_text(encoding="utf-8"))
def log(message: str) -> None:
print(message)
def ensure_dir(path: Path) -> None:
path.mkdir(parents=True, exist_ok=True)
def normalize_langs(raw: object) -> list[str]:
if raw is None:
return ["tsl"]
if isinstance(raw, str):
langs = [raw]
else:
langs = list(raw)
cleaned: list[str] = []
for lang in langs:
item = str(lang).strip()
if not item:
continue
if "/" in item or "\\" in item or ".." in item:
raise ValueError(f"invalid lang: {item}")
cleaned.append(item)
if not cleaned:
raise ValueError("langs is empty")
return cleaned
def read_git_commit(root: Path) -> str:
try:
result = subprocess.run(
["git", "-C", str(root), "rev-parse", "HEAD"],
capture_output=True,
text=True,
check=True,
)
except (OSError, subprocess.CalledProcessError):
return "N/A"
return result.stdout.strip() or "N/A"
def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
lines = [
"# 文档导航Docs Index",
"",
f"本快照为裁剪版 Playbooklangs: {','.join(langs)})。",
"",
"## 跨语言common",
"",
"- 提交信息与版本号:`common/commit_message.md`",
]
for lang in langs:
if lang == "tsl":
lines += [
"",
"## TSLtsl",
"",
"- 代码风格:`tsl/code_style.md`",
"- 命名规范:`tsl/naming.md`",
"- 语法手册:`tsl/syntax_book/index.md`",
"- 工具链与验证命令(模板):`tsl/toolchain.md`",
]
elif lang == "cpp":
lines += [
"",
"## C++cpp",
"",
"- 代码风格:`cpp/code_style.md`",
"- 命名规范:`cpp/naming.md`",
"- 工具链与验证命令(模板):`cpp/toolchain.md`",
"- 第三方依赖Conan`cpp/dependencies_conan.md`",
"- clangd 配置:`cpp/clangd.md`",
]
elif lang == "python":
lines += [
"",
"## Pythonpython",
"",
"- 代码风格:`python/style_guide.md`",
"- 工具链:`python/tooling.md`",
"- 配置清单:`python/configuration.md`",
]
elif lang == "markdown":
lines += [
"",
"## Markdownmarkdown",
"",
"- 代码块与行内代码格式:`markdown/index.md`",
]
docs_index = dest_prefix / "docs/index.md"
ensure_dir(docs_index.parent)
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_snapshot_readme(dest_prefix: Path, langs: list[str]) -> None:
lines = [
"# Playbook裁剪快照",
"",
f"本目录为从 Playbook vendoring 的裁剪快照langs: {','.join(langs)})。",
"",
"## 使用",
"",
"在目标项目根目录执行:",
"",
"```sh",
"python docs/standards/playbook/scripts/playbook.py -config playbook.toml",
"```",
"",
"配置示例:`docs/standards/playbook/playbook.toml.example`",
"",
"文档入口:",
"",
"- `docs/standards/playbook/docs/index.md`",
"- `.agents/index.md`",
]
(dest_prefix / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
def write_source_file(dest_prefix: Path, langs: list[str]) -> None:
commit = read_git_commit(PLAYBOOK_ROOT)
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
lines = [
"# SOURCE",
"",
f"- Source: {PLAYBOOK_ROOT}",
f"- Commit: {commit}",
f"- Date: {timestamp}",
f"- Langs: {','.join(langs)}",
"- Generated-by: scripts/playbook.py",
]
(dest_prefix / "SOURCE.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
def vendor_action(config: dict, context: dict) -> int:
try:
langs = normalize_langs(config.get("langs"))
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
target_dir = config.get("target_dir", "docs/standards/playbook")
target_path = Path(target_dir)
if target_path.is_absolute() or ".." in target_path.parts:
print(f"ERROR: invalid target_dir: {target_dir}", file=sys.stderr)
return 2
project_root: Path = context["project_root"]
dest_prefix = project_root / target_path
dest_standards = dest_prefix.parent
ensure_dir(dest_standards)
if dest_prefix.exists():
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
backup = dest_standards / f"{dest_prefix.name}.bak.{timestamp}"
dest_prefix.rename(backup)
log(f"Backed up existing snapshot -> {backup}")
ensure_dir(dest_prefix)
gitattributes_src = PLAYBOOK_ROOT / ".gitattributes"
if gitattributes_src.is_file():
copy2(gitattributes_src, dest_prefix / ".gitattributes")
copytree(PLAYBOOK_ROOT / "scripts", dest_prefix / "scripts")
copytree(PLAYBOOK_ROOT / "codex", dest_prefix / "codex")
copy2(PLAYBOOK_ROOT / "SKILLS.md", dest_prefix / "SKILLS.md")
common_docs = PLAYBOOK_ROOT / "docs/common"
if common_docs.is_dir():
copytree(common_docs, dest_prefix / "docs/common")
rulesets_root = PLAYBOOK_ROOT / "rulesets"
ensure_dir(dest_prefix / "rulesets")
if (rulesets_root / "index.md").is_file():
copy2(rulesets_root / "index.md", dest_prefix / "rulesets/index.md")
templates_ci = PLAYBOOK_ROOT / "templates/ci"
if templates_ci.is_dir():
copytree(templates_ci, dest_prefix / "templates/ci")
for lang in langs:
docs_src = PLAYBOOK_ROOT / "docs" / lang
rules_src = PLAYBOOK_ROOT / "rulesets" / lang
if not docs_src.is_dir():
print(f"ERROR: docs not found for lang={lang}", file=sys.stderr)
return 2
if not rules_src.is_dir():
print(f"ERROR: rulesets not found for lang={lang}", file=sys.stderr)
return 2
copytree(docs_src, dest_prefix / "docs" / lang)
copytree(rules_src, dest_prefix / "rulesets" / lang)
templates_src = PLAYBOOK_ROOT / "templates" / lang
if templates_src.is_dir():
copytree(templates_src, dest_prefix / "templates" / lang)
example_config = PLAYBOOK_ROOT / "playbook.toml.example"
if example_config.is_file():
copy2(example_config, dest_prefix / "playbook.toml.example")
write_docs_index(dest_prefix, langs)
write_snapshot_readme(dest_prefix, langs)
write_source_file(dest_prefix, langs)
log(f"Vendored snapshot -> {dest_prefix}")
return 0
def run_action(name: str, config: dict, context: dict) -> int:
if name == "vendor":
return vendor_action(config, context)
print(f"[action] {name}")
return 0
def main(argv: list[str]) -> int:
if "-h" in argv or "-help" in argv:
print(usage())
return 0
if "-config" not in argv:
print("ERROR: -config is required.\n" + usage(), file=sys.stderr)
return 2
idx = argv.index("-config")
if idx + 1 >= len(argv) or not argv[idx + 1]:
print("ERROR: -config requires a path.\n" + usage(), file=sys.stderr)
return 2
config_path = Path(argv[idx + 1]).expanduser()
if not config_path.is_file():
print(f"ERROR: config not found: {config_path}", file=sys.stderr)
return 2
config = load_config(config_path)
playbook_config = config.get("playbook", {})
project_root = playbook_config.get("project_root")
if project_root:
root = Path(project_root).expanduser()
else:
root = config_path.parent
context = {"project_root": root.resolve(), "config_path": config_path.resolve()}
for name in ORDER:
if name in config:
result = run_action(name, config[name], context)
if result != 0:
return result
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))