275 lines
8.7 KiB
Python
275 lines
8.7 KiB
Python
#!/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"本快照为裁剪版 Playbook(langs: {','.join(langs)})。",
|
||
"",
|
||
"## 跨语言(common)",
|
||
"",
|
||
"- 提交信息与版本号:`common/commit_message.md`",
|
||
]
|
||
for lang in langs:
|
||
if lang == "tsl":
|
||
lines += [
|
||
"",
|
||
"## TSL(tsl)",
|
||
"",
|
||
"- 代码风格:`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 += [
|
||
"",
|
||
"## Python(python)",
|
||
"",
|
||
"- 代码风格:`python/style_guide.md`",
|
||
"- 工具链:`python/tooling.md`",
|
||
"- 配置清单:`python/configuration.md`",
|
||
]
|
||
elif lang == "markdown":
|
||
lines += [
|
||
"",
|
||
"## Markdown(markdown)",
|
||
"",
|
||
"- 代码块与行内代码格式:`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:]))
|