diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml index fc037ed..b62e35d 100644 --- a/.gitea/workflows/test.yml +++ b/.gitea/workflows/test.yml @@ -86,436 +86,38 @@ jobs: echo "========================================" apt-get update - apt-get install -y bats cmake clang-format python3-pip + apt-get install -y python3-pip python3 -m pip install --upgrade pip - python3 -m pip install toml tomli jsonschema yamllint + python3 -m pip install yamllint echo "" - echo "✓ bats 版本: $(bats --version)" echo "✓ Python 版本: $(python3 --version)" echo "========================================" - name: 🧪 运行全量测试并生成报告 shell: bash run: | - set +e - set -o pipefail - - overall_fail=0 - scripts_status="success" - templates_status="success" - integration_status="success" - docs_status="success" + set -euo pipefail echo "========================================" - echo "🐚 Shell 脚本测试" + echo "🧪 Python CLI 测试" echo "========================================" - cd "$REPO_DIR/tests/scripts" - - run_bats() { - local name="$1" - local file="$2" - local output="${name}_test_results.tap" - - if [ ! -f "$file" ]; then - echo "⚠️ 未找到测试文件: $file" - scripts_status="failure" - overall_fail=1 - return - fi - - bats --formatter tap "$file" | tee "$output" - if [ $? -ne 0 ]; then - echo "❌ $name 测试失败" - scripts_status="failure" - overall_fail=1 - else - echo "✅ $name 测试通过" - fi - } - - run_bats "sync_standards" "test_sync_standards.bats" - run_bats "sync_templates" "test_sync_templates.bats" - run_bats "vendor_playbook" "test_vendor_playbook.bats" - run_bats "install_codex_skills" "test_install_codex_skills.bats" - run_bats "windows_script_lints" "test_windows_script_lints.bats" + cd "$REPO_DIR" + python3 -m unittest discover -s tests/cli -v echo "========================================" echo "📄 模板验证测试" echo "========================================" - cd "$REPO_DIR/tests/templates" - - run_validator() { - local name="$1" - local script="$2" - - if [ ! -f "$script" ]; then - echo "⚠️ 未找到验证脚本: $script" - templates_status="failure" - overall_fail=1 - return - fi - - chmod +x "$script" - "./$script" - if [ $? -ne 0 ]; then - echo "❌ $name 模板验证失败" - templates_status="failure" - overall_fail=1 - else - echo "✅ $name 模板验证通过" - fi - } - - run_validator "python" "validate_python_templates.sh" - run_validator "cpp" "validate_cpp_templates.sh" - run_validator "ci" "validate_ci_templates.sh" - run_validator "project_templates" "validate_project_templates.sh" + sh tests/templates/validate_python_templates.sh + sh tests/templates/validate_cpp_templates.sh + sh tests/templates/validate_ci_templates.sh + sh tests/templates/validate_project_templates.sh echo "========================================" - echo "🔗 集成测试" + echo "🔗 文档链接检查" echo "========================================" - mkdir -p "${TEST_WORKSPACE}" - cd "${TEST_WORKSPACE}" - - # 创建测试项目目录 - mkdir -p test-project-tsl - mkdir -p test-project-cpp - mkdir -p test-project-multi - - echo "========================================" - echo "🧪 测试场景1: TSL 项目标准同步" - echo "========================================" - - cd "${TEST_WORKSPACE}/test-project-tsl" - - # 初始化 git 仓库 - git init - git config user.name "Test User" - git config user.email "test@example.com" - - # 模拟 subtree add(包含 rulesets 等点目录,排除 .git) - mkdir -p docs/standards/playbook - tar -C "$REPO_DIR" --exclude .git -cf - . | tar -C docs/standards/playbook -xf - - - # 运行同步脚本 - echo "▶ 运行 sync_standards.sh -langs tsl" - sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl - - # 验证结果 - if [ -d ".agents/tsl" ] && [ -f ".agents/tsl/index.md" ]; then - echo "✅ TSL 规则集同步成功" - else - echo "❌ TSL 规则集同步失败" - integration_status="failure" - overall_fail=1 - fi - - if grep -q "# BEGIN playbook .gitattributes" .gitattributes 2>/dev/null \ - || grep -q "# Added from playbook .gitattributes" .gitattributes 2>/dev/null \ - || grep -q "^\\* text=auto eol=lf" .gitattributes 2>/dev/null; then - echo "✅ .gitattributes 更新成功" - else - echo "❌ .gitattributes 更新失败" - integration_status="failure" - overall_fail=1 - fi - - echo "========================================" - echo "🧪 测试场景2: C++ 项目标准同步" - echo "========================================" - - cd "${TEST_WORKSPACE}/test-project-cpp" - - git init - git config user.name "Test User" - git config user.email "test@example.com" - - mkdir -p docs/standards/playbook - tar -C "$REPO_DIR" --exclude .git -cf - . | tar -C docs/standards/playbook -xf - - - echo "▶ 运行 sync_standards.sh -langs cpp" - sh docs/standards/playbook/scripts/sync_standards.sh -langs cpp - - if [ -d ".agents/cpp" ] && [ -f ".agents/cpp/index.md" ]; then - echo "✅ C++ 规则集同步成功" - else - echo "❌ C++ 规则集同步失败" - integration_status="failure" - overall_fail=1 - fi - - echo "========================================" - echo "🧪 测试场景3: 多语言项目标准同步" - echo "========================================" - - cd "${TEST_WORKSPACE}/test-project-multi" - - git init - git config user.name "Test User" - git config user.email "test@example.com" - - mkdir -p docs/standards/playbook - tar -C "$REPO_DIR" --exclude .git -cf - . | tar -C docs/standards/playbook -xf - - - echo "▶ 运行 sync_standards.sh -langs tsl,cpp" - sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp - - if [ -d ".agents/tsl" ] && [ -d ".agents/cpp" ] && [ -f ".agents/index.md" ]; then - echo "✅ 多语言规则集同步成功" - else - echo "❌ 多语言规则集同步失败" - integration_status="failure" - overall_fail=1 - fi - - echo "========================================" - echo "🧪 测试场景4: vendor_playbook 脚本" - echo "========================================" - - cd "${TEST_WORKSPACE}" - mkdir -p test-project-vendor - cd test-project-vendor - - git init - git config user.name "Test User" - git config user.email "test@example.com" - - echo "▶ 运行 vendor_playbook.sh" - sh "$REPO_DIR/scripts/vendor_playbook.sh" -project-root . -langs tsl - - if [ -d "docs/standards/playbook" ] && [ -d "docs/standards/playbook/rulesets/tsl" ] && [ -d ".agents/tsl" ]; then - echo "✅ vendor_playbook 脚本执行成功" - else - echo "❌ vendor_playbook 脚本执行失败" - integration_status="failure" - overall_fail=1 - fi - - echo "========================================" - echo "🧹 清理测试环境..." - chmod -R u+w "${TEST_WORKSPACE}" 2>/dev/null || true - rm -rf "${TEST_WORKSPACE}" - echo "✓ 清理完成" - - echo "========================================" - echo "📚 文档一致性检查" - echo "========================================" - - cd "$REPO_DIR/tests/integration" - - if [ -f "check_doc_links.sh" ]; then - chmod +x check_doc_links.sh - ./check_doc_links.sh - - if [ $? -eq 0 ]; then - echo "✅ 文档链接检查通过" - else - echo "❌ 发现无效链接" - docs_status="failure" - overall_fail=1 - fi - else - echo "⚠️ 未找到链接检查脚本,跳过" - fi - - echo "========================================" - echo "🔍 检查代理规则一致性(三层架构)" - echo "========================================" - - cd "$REPO_DIR" - - # 检查 rulesets/ 三层架构完整性 - python3 << 'EOF' - import sys - from pathlib import Path - - errors = [] - warnings = [] - - print("检查 Layer 1: rulesets/ (极简铁律)") - print("────────────────────────────────────────") - - # 检查各语言的 rulesets/ 目录(只需 index.md) - agents_base = Path("rulesets") - - # 检查 rulesets/index.md - agents_index = agents_base / "index.md" - if not agents_index.exists(): - errors.append(f"❌ 缺少文件: {agents_index}") - elif agents_index.stat().st_size == 0: - errors.append(f"❌ 文件为空: {agents_index}") - else: - print(f"✅ {agents_index}") - - for lang_dir in ["tsl", "cpp", "python"]: - agents_lang = agents_base / lang_dir - if not agents_lang.exists(): - errors.append(f"❌ 缺少目录: {agents_lang}") - continue - - # 只检查 index.md(≤50 行) - index_file = agents_lang / "index.md" - if not index_file.exists(): - errors.append(f"❌ 缺少文件: {index_file}") - elif index_file.stat().st_size == 0: - errors.append(f"❌ 文件为空: {index_file}") - else: - # 检查规模(≤50 行) - line_count = len(index_file.read_text(encoding='utf-8').splitlines()) - if line_count > 50: - warnings.append(f"⚠️ {index_file}: {line_count} 行 (目标: ≤50)") - else: - print(f"✅ {index_file} ({line_count} 行)") - - # 检查是否有残留的旧文件 - old_files = ["auth.md", "code_quality.md", "performance.md", "testing.md"] - for old_file in old_files: - old_path = agents_lang / old_file - if old_path.exists(): - warnings.append(f"⚠️ 残留旧文件: {old_path} (应删除)") - - print("") - print("检查 Layer 2: codex/skills/ (按需加载)") - print("────────────────────────────────────────") - - # 检查关键 skills - skills_base = Path("codex/skills") - required_skills = [ - ("tsl-guide", ["SKILL.md", "references/primer.md", "references/advanced.md"]), - ("commit-message", ["SKILL.md"]), - ("style-cleanup", ["SKILL.md"]), - ("bulk-refactor-workflow", ["SKILL.md"]), - ] - - for skill_name, required_files in required_skills: - skill_dir = skills_base / skill_name - if not skill_dir.exists(): - errors.append(f"❌ 缺少 skill: {skill_dir}") - continue - - for req_file in required_files: - file_path = skill_dir / req_file - if not file_path.exists(): - errors.append(f"❌ 缺少文件: {file_path}") - elif file_path.stat().st_size == 0: - errors.append(f"❌ 文件为空: {file_path}") - else: - print(f"✅ {file_path}") - - print("") - print("检查 Layer 3: docs/ (权威文档)") - print("────────────────────────────────────────") - - # 检查关键文档路径 - docs_paths = [ - "docs/tsl/syntax_book/index.md", - "docs/tsl/code_style.md", - "docs/tsl/naming.md", - "docs/cpp/code_style.md", - "docs/python/style_guide.md", - ] - - for doc_path in docs_paths: - path = Path(doc_path) - if not path.exists(): - errors.append(f"❌ 缺少文档: {doc_path}") - else: - print(f"✅ {doc_path}") - - print("") - print("检查架构文档") - print("────────────────────────────────────────") - - # 检查新增的架构文档 - arch_docs = ["AGENTS.md", "SKILLS.md", "README.md"] - for doc in arch_docs: - path = Path(doc) - if not path.exists(): - errors.append(f"❌ 缺少文档: {doc}") - else: - print(f"✅ {doc}") - - print("") - print("────────────────────────────────────────") - - if warnings: - print("\n⚠️ 警告:") - for warning in warnings: - print(f" {warning}") - print("") - - if errors: - print("\n❌ 发现错误:") - for error in errors: - print(f" {error}") - sys.exit(1) - else: - print("✅ 三层架构完整性检查通过") - EOF - - if [ $? -ne 0 ]; then - docs_status="failure" - overall_fail=1 - fi - - echo "========================================" - echo "📊 生成测试综合报告" - echo "========================================" - - format_status() { - case "$1" in - success) - echo "✅ 通过" - ;; - failure) - echo "❌ 失败" - ;; - *) - echo "❔ 未知" - ;; - esac - } - - cat >> $GITHUB_STEP_SUMMARY << EOFSUMMARY - # 🧪 Playbook 测试报告 - - ## 📋 测试执行摘要 - - | 测试类型 | 状态 | - |---------|------| - | 🐚 Shell 脚本测试 | $(format_status "$scripts_status") | - | 📄 模板验证测试 | $(format_status "$templates_status") | - | 🔗 集成测试 | $(format_status "$integration_status") | - | 📚 文档一致性检查 | $(format_status "$docs_status") | - - --- - - ## 🔗 相关链接 - - - 📝 [测试文档](tests/README.md) - - 🐛 [问题反馈](../../issues) - - 📖 [开发指南](docs/index.md) - - --- - -
- - *🤖 由 [Gitea Actions](../../actions) 自动生成* - - EOFSUMMARY - - echo "*📅 生成时间: $(date -u '+%Y-%m-%d %H:%M:%S UTC')*" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - - echo "========================================" - if [ "$overall_fail" -ne 0 ]; then - echo "❌ 测试失败" - exit 1 - fi - echo "✅ 全量测试通过" + sh tests/integration/check_doc_links.sh diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..d0de8ec --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +codex/skills/** diff --git a/AGENTS.md b/AGENTS.md index 30ba4d6..9c4c30e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ > > - 在 playbook 仓库中:规则集模板位于 `rulesets/` > - 在目标项目中:同步后规则集位于 `.agents/` -> - AI 代理读取目标项目根目录的 `.agents/`(由 sync_standards.sh 生成) +> - AI 代理读取目标项目根目录的 `.agents/`(由 playbook.py 的 `[sync_standards]` 生成) > > 本文适用于目标项目。playbook 仓库自身没有源代码,不需要 AI 代理规则。 @@ -89,11 +89,11 @@ Token 消耗:~8,000 tokens ## 性能指标 -| 指标 | 之前 | 现在 | 改善 | -| --------------- | -------- | -------- | ---- | -| .agents 规模 | ~500 行 | 168 行 | -66% | -| 持久化 tokens | ~12,500 | ~4,200 | -66% | -| 场景平均 tokens | ~12,500 | ~10,500 | -16% | +| 指标 | 之前 | 现在 | 改善 | +| --------------- | ------- | ------- | ---- | +| .agents 规模 | ~500 行 | 168 行 | -66% | +| 持久化 tokens | ~12,500 | ~4,200 | -66% | +| 场景平均 tokens | ~12,500 | ~10,500 | -16% | --- diff --git a/README.md b/README.md index 4b889f5..f0b209b 100644 --- a/README.md +++ b/README.md @@ -52,25 +52,29 @@ Playbook:TSL(`.tsl`/`.tsf`)+ C++ + Python + Markdown(代码格式化) ### 快速部署 -使用 `sync_templates` 脚本一键部署项目架构: +统一入口(配置驱动,示例见 `playbook.toml.example`): ```bash -# Linux/macOS -sh scripts/sync_templates.sh -project-root /path/to/project +python scripts/playbook.py -config playbook.toml +``` -# PowerShell -.\scripts\sync_templates.ps1 -ProjectRoot C:\path\to\project +示例配置(部署项目架构模板): -# Windows CMD -scripts\sync_templates.bat -project-root C:\path\to\project +```toml +[playbook] +project_root = "/path/to/project" + +[sync_templates] +project_name = "MyProject" +full = false ``` **部署行为**: - **新项目**:创建完整的 `AGENTS.md`、`AGENT_RULES.md`、`memory-bank/`、`docs/prompts/` - **已有 AGENTS.md**:追加路由链接(使用 `` 标记) -- **-full 参数**:追加完整框架(规则优先级 + 新会话开始时)到已有 AGENTS.md -- **其他文件**:如果已存在则跳过(使用 `-force` 覆盖) +- **full = true**:追加完整框架(规则优先级 + 新会话开始时)到已有 AGENTS.md +- **其他文件**:如果已存在则跳过(使用 `force = true` 覆盖) 详见:`templates/README.md` @@ -81,7 +85,7 @@ scripts\sync_templates.bat -project-root C:\path\to\project > Playbook 本身不包含源代码,因此不需要 AI 代理遵循规则。`rulesets/` 存在的目的是: > > 1. 作为**模板源**,供其他项目复制 -> 2. 通过 `sync_standards.sh` 部署到目标项目的 `.agents/` +> 2. 通过 playbook.py 的 `[sync_standards]` 部署到目标项目的 `.agents/` > 3. 目标项目的 AI 代理读取**项目根目录的 `.agents/`**(从模板生成) `rulesets/` 是 AI 代理规则集模板(三层架构设计): @@ -132,7 +136,7 @@ Layer 3: docs/ (权威静态文档) ## 在其他项目中使用本 Playbook -由于本仓库需要内部权限访问,其他项目**不能仅用外链引用**;推荐把 Playbook 规范 vendoring 到项目内。 +由于本仓库需要内部权限访问,其他项目**不能仅用外链引用**;推荐把 Playbook 规范 vendoring 到项目内,并用统一入口执行。 ### 快速决策:我应该用哪种方式? @@ -140,35 +144,36 @@ Layer 3: docs/ (权威静态文档) | ---------------------------------- | ------------------------------- | ------------------------------- | | 新项目,需要持续同步更新 | 方式一:git subtree | 可随时拉取最新标准,版本可追溯 | | 只需要一次性引入,不常更新 | 方式二:手动复制快照 | 简单直接,无需 git subtree 知识 | -| 只需要部分语言(且希望快照也裁剪) | 方式三:脚本裁剪复制 | 快照只包含所需语言(更小) | +| 只需要部分语言(且希望快照也裁剪) | 方式三:CLI 裁剪复制(vendor) | 快照只包含所需语言(更小) | | **不确定?** | **方式一:git subtree(推荐)** | 最灵活,后续可随时同步更新 | -**大部分情况推荐使用方式一(git subtree)。** -说明:方式一可选择同步哪些语言规则到 `.agents/`,但 `docs/standards/playbook/` 快照仍是全量;方式三会裁剪快照本身。 - --- ### TL;DR - 30 秒快速开始 -大部分项目只需运行以下命令即可完成落地(以 TSL 为例): +以 TSL 为例: ```bash # 1. 引入标准快照 -git subtree add --prefix docs/standards/playbook \ - https://git.mytsl.cn/csh/playbook.git main --squash +git subtree add --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbook.git main --squash -# 2. 同步规则到项目根目录 -sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl +# 2. 在项目根创建配置(示例见 docs/standards/playbook/playbook.toml.example) +cat <<'EOF' > playbook.toml +[playbook] +project_root = "." -# 3. 提交 +[sync_standards] +langs = ["tsl"] +EOF + +# 3. 执行统一入口 +python docs/standards/playbook/scripts/playbook.py -config playbook.toml + +# 4. 提交 git add . git commit -m ":package: deps(playbook): add tsl standards" ``` -**完成!** 后续同步更新只需重复步骤 1(把 `add` 改为 `pull`)和步骤 2。 - -详细说明和其他方式见下文 ↓ - --- ### 方式一:git subtree 同步(推荐) @@ -176,213 +181,71 @@ git commit -m ":package: deps(playbook): add tsl standards" 1. 在目标项目中首次引入: ```bash - git subtree add \ - --prefix docs/standards/playbook \ - https://git.mytsl.cn/csh/playbook.git \ - main --squash + git subtree add --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbook.git main --squash ``` 2. 后续同步更新: ```bash - git subtree pull \ - --prefix docs/standards/playbook \ - https://git.mytsl.cn/csh/playbook.git \ - main --squash + git subtree pull --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbook.git main --squash ``` -#### 快速落地(最小 4 步) +3. 在项目根配置并执行: -在目标项目中按以下顺序执行即可完成落地(推荐固定使用 -`--prefix docs/standards/playbook`): + ```toml + # playbook.toml + [playbook] + project_root = "." -1. **引入标准快照**(见上文 `git subtree add`) -2. **同步到项目根目录**(生成/更新 `.agents//`、更新 `.gitattributes`): + [sync_standards] + langs = ["tsl", "cpp"] + + [sync_templates] + project_name = "MyProject" + ``` ```bash - sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl + python docs/standards/playbook/scripts/playbook.py -config playbook.toml ``` - 同步 C++ 规则集(同一份快照,不同规则集): +配置参数说明见 `docs/standards/playbook/playbook.toml.example`。 - ```bash - sh docs/standards/playbook/scripts/sync_standards.sh -langs cpp - ``` - - 一次同步多个规则集(推荐,减少重复备份 `.gitattributes`): - - ```bash - sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp - ``` - - > 说明:若项目根目录没有 `AGENTS.md`,`sync_standards.*` - > 会自动生成最小版;已存在则不会覆盖。 - -3. **验收**(任意满足其一即可): - - - 目录存在:`.agents/tsl/` - - 规则入口可读:`.agents/tsl/index.md` - - (可选)C++ 规则入口可读:`.agents/cpp/index.md` - - 标准文档可读:`docs/standards/playbook/docs/index.md` - - `.gitattributes` 包含追加块头:`# Added from playbook .gitattributes` - -4. **将同步产物纳入版本控制**(目标项目建议提交): - - `docs/standards/playbook/`(标准快照) - - `.agents/tsl/`(落地规则集) - - `.gitattributes`(追加缺失规则) - - `AGENTS.md`(若本次自动生成) - -#### 新项目 / 旧项目(命令示例) - -**新项目**(无 `.agents/` 与 `AGENTS.md`): - -```bash -git subtree add --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbook.git main --squash -sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl -``` - -**旧项目**(已有 `AGENTS.md`): - -```bash -git subtree pull --prefix docs/standards/playbook https://git.mytsl.cn/csh/playbook.git main --squash -sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl -``` - -旧项目的 `AGENTS.md` 不会被覆盖;如需指向 `.agents/`,请手动对齐内容。 - -#### 可选:项目包装脚本(多 playbook 串联) - -多语言项目建议在目标项目创建一个包装脚本(便于一键同步多个规则集): - -```sh -#!/usr/bin/env sh -set -eu - -sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp -# sh docs/standards/python/scripts/sync_standards.sh -``` - -也可以直接一次同步多个规则集: - -```sh -sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp -``` - -#### 目录约定(建议) - -目标项目推荐采用以下结构(Playbook 快照与项目文档分离): - -```txt -. -├── .agents/ -│ ├── index.md # 多语言代理规则集索引(缺省时由脚本创建) -│ ├── tsl/ # 从 Playbook 同步(仅覆盖该子目录) -│ └── cpp/ # 从 Playbook 同步(仅覆盖该子目录,按需) -├── .gitattributes # 从 Playbook 同步 -├── docs/ -│ ├── standards/ -│ │ └── tsl/ # git subtree 快照(只读) -│ │ ├── docs/ # common/ + tsl/ + cpp/ -│ │ ├── .agents/ # 标准代理规则快照 -│ │ ├── .gitattributes -│ │ └── SOURCE.md # 记录来源版本/commit(项目自行维护) -│ └── project/ # 目标项目自己的文档(非语言标准:架构/运行/ADR/使用说明/业务约定等) -└── README.md # 说明遵循 standards -``` - -根目录的 `.agents//` 与 `.gitattributes` 通过同步脚本获得: - -- **说明**:在 **本 playbook 仓库**内脚本位于 `scripts/`;在 **目标项目**里通过 - `git subtree` 引入到 `docs/standards/playbook/` 后,脚本路径变为 - `docs/standards/playbook/scripts/`。 -- **在目标项目里直接运行 Playbook 提供的脚本**(子树快照里自带): - - `docs/standards/playbook/scripts/sync_standards.sh`(推荐,支持多语言参数) - - `docs/standards/playbook/scripts/sync_standards.ps1`(推荐,支持多语言参数) - - `docs/standards/playbook/scripts/sync_standards.bat`(推荐,支持多语言参数) -- 脚本会从快照目录同步到项目根目录,并先备份旧文件(`.bak.*`) - -建议固定使用 `--prefix docs/standards/playbook`,因为同步后的 `.agents/*/` -会引用该路径下的标准快照文档(`docs/standards/playbook/docs/...`)。同步时需显式指定 -语言参数(`-langs`),如需同步 C++ 规则集,推荐直接运行: -`sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp`。 - -这样 clone 任意项目时都能直接读取规范文件,不依赖外部访问权限。 - -**同步脚本行为**(目标项目内的最终落地内容): - -- 覆盖/更新:`.agents//`(由 `-langs` 或 `AGENTS_NS` 指定) -- 更新 `.gitattributes`:默认追加缺失规则(可用 - `SYNC_GITATTR_MODE=append|block|overwrite|skip` 控制) -- 缺省创建:`.agents/index.md` -- 覆盖前备份:写入同目录的 `*.bak.*`(或 Windows 下随机后缀) -- 不修改:`.gitignore`(项目自行维护) - -
- 高级选项:环境变量配置(点击展开) - -#### 环境变量(可选) - -同步脚本支持以下可选环境变量(默认值可满足大多数项目): - -| 变量名 | 默认值 | 说明 | -| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | -| `AGENTS_NS` | `tsl` | 同步的规则集名/落地目录名:`.agents//`(例如 `tsl`、`cpp`) | -| `SYNC_GITATTR_MODE` | `append` | `.gitattributes` 同步模式:`append` 仅追加缺失规则(忽略注释/空行,比对后按块追加);`block` 仅维护 managed 区块;`overwrite` 全量覆盖;`skip` 不更新 | - -
+--- ### 方式二:手动复制快照 -如果不使用 -`git subtree`,也可以由有权限的人手动复制 Playbook 到目标项目中(适合规范不频繁更新或项目数量较少的情况)。 +如果不使用 git subtree,也可手动复制快照到目标项目: -**步骤**: +1. 创建目录:`docs/standards/playbook/`。 +2. 复制 Playbook 快照内容(建议使用方式三生成裁剪快照)。 +3. 在项目根执行统一入口: -1. 在目标项目创建目录:`docs/standards/playbook/`。 -2. 从本仓库复制以下内容到目标项目: - - `docs/` → `docs/standards/playbook/docs/`(包含 - `docs/common/`、`docs/tsl/`、`docs/cpp/`、`docs/python/`) - - `.agents/` → `docs/standards/playbook/.agents/` - - `.gitattributes` → `docs/standards/playbook/.gitattributes` - - `scripts/` → `docs/standards/playbook/scripts/` -3. 在目标项目根目录运行同步脚本,把 `.agents/tsl/` 与 `.gitattributes` - 落到根目录(见上文脚本路径)。 -4. 在 `docs/standards/playbook/SOURCE.md` - 记录本次复制的来源版本/日期(建议写 Playbook 的 commit hash)。 + ```bash + python docs/standards/playbook/scripts/playbook.py -config playbook.toml + ``` -该方式没有自动同步能力,后续更新需重复上述复制流程。 +--- -### 方式三:脚本裁剪复制(按语言,离线) +### 方式三:CLI 裁剪复制(按语言,离线) -当你希望“只 vendoring 需要的语言规范”(例如只需要 `tsl` + -`cpp`)时,可直接运行本仓库提供的裁剪脚本: +当你希望只 vendoring 需要的语言规范(例如只需要 `tsl` + `cpp`)时: -- macOS/Linux: +```toml +# playbook.toml +[playbook] +project_root = "/path/to/target-project" - ```bash - sh /scripts/vendor_playbook.sh -project-root -langs tsl,cpp - ``` +[vendor] +langs = ["tsl", "cpp"] +``` -- PowerShell: +```bash +python scripts/playbook.py -config playbook.toml +``` - ```powershell - powershell -File \\scripts\\vendor_playbook.ps1 -ProjectRoot -Langs tsl,cpp - ``` +该动作仅生成裁剪快照,不会隐式同步 `.agents/` 或 `.gitattributes`;后续请用 `sync_standards` 明确落地。 -- Windows bat: - - ```bat - \\scripts\\vendor_playbook.bat -project-root -langs tsl,cpp - ``` - -**脚本会**: - -- 生成裁剪快照到 `docs/standards/playbook/`(包含 - `docs/common/` + 选定语言目录 + 对应 `.agents//` + `scripts/` + - `.gitattributes` + 通用 `templates/ci/` + 相关 `templates//`) -- 自动执行 `docs/standards/playbook/scripts/sync_standards.*`,把 - `.agents//` 与 `.gitattributes` 落地到目标项目根目录 -- 生成 `docs/standards/playbook/SOURCE.md` 记录来源与版本信息 +--- ### 多语言项目落地(TSL + C++/其他语言) @@ -393,8 +256,7 @@ sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp - 行尾与文本规范:`.gitattributes` - 代理最低要求:`.agents/*`(工作原则、质量底线、安全边界) 2. **语言级(Language-specific)规范**:只对某个语言成立的风格与工具。 - - 例如 TSL 的命名/文件顶层声明限制、C++ 的 - `.clang-format/.clang-tidy`、Python 的 `ruff` 等。 + - 例如 TSL 的命名/文件顶层声明限制、C++ 的 `.clang-format/.clang-tidy`、Python 的 `ruff` 等。 **建议**:仓库级规则尽量少且稳定;语言级规则各自独立,避免互相"污染"。 @@ -408,19 +270,17 @@ sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp ```txt . ├── .agents/ -│ ├── index.md # 多语言索引(缺省时由脚本创建) +│ ├── index.md # 多语言索引(缺省时由 playbook 生成) │ ├── tsl/ # 由本 Playbook 同步(适用于 .tsl/.tsf) │ ├── cpp/ # 由本 Playbook 同步(适用于 C++23/Modules) │ ├── python/ # Python 规则集(同上) │ └── markdown/ # Markdown 规则集(仅代码格式化) -├── .gitattributes # 行尾/文本规范(可由某个 playbook 同步) +├── .gitattributes # 行尾/文本规范 ├── docs/ │ ├── standards/ -│ │ ├── tsl/ # 本 Playbook 快照(git subtree/vendoring;包含 common/tsl/cpp) -│ │ └── python/ # Python playbook 快照(可选) +│ │ └── playbook/ # 本 Playbook 快照(git subtree/vendoring) │ └── project/ # 项目自有文档(架构、ADR、运行方式等) -├── scripts/ -│ └── sync_standards.sh # 项目包装脚本:依次调用各 playbook 的 sync +├── playbook.toml # 统一入口配置 └── src/ # 源码目录(按项目实际情况) ``` @@ -429,72 +289,3 @@ sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp - 同一项目内多个规则集并行放在 `.agents//`,不要互相覆盖 - 若某个子目录需要更具体规则(模块/子系统差异),在更靠近代码的目录放置更具体规则(例如 `src/foo/.agents/`),并以"离代码更近者优先"为准 - -
- 高级选项:`.agents` 覆盖/合并策略(点击展开) - -#### `.agents` 的覆盖/合并策略(可执行流程) - -同步脚本会同步到项目根目录的 `.agents/tsl/`(并不会覆盖 `.agents/` -下的其他语言目录)。若项目需要追加 C++ 等语言/模块专属规则,建议二选一: - -1. **推荐:子目录规则覆盖(无需改同步脚本)** - - 让本 Playbook 的规则集固定落在 `.agents/tsl/`,由同步脚本维护。 - - 在其他语言/模块目录下新增更具体规则,例如 - `.agents/cpp/`、`cpp/.agents/`、`src/.agents/`。 -2. **Overlay 合并:项目维护叠加层并在同步后覆盖回去** - - 约定项目自定义规则放在 `docs/project/agents_overlay/`(不叫 - `.agents`,避免被同步覆盖)。 - - 每次运行 `sync_standards.*` 后,再把 overlay 覆盖回 - `.agents/tsl/`(建议封装成项目脚本)。 - -macOS/Linux 示例(目标项目的 `scripts/sync_standards.sh`): - -```sh -#!/usr/bin/env sh -set -eu - -sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl,cpp - -OVERLAY="docs/project/agents_overlay" -if [ -d "$OVERLAY" ]; then - cp -R "$OVERLAY"/. ".agents/tsl/" - echo "Applied agents overlay." -fi -``` - -PowerShell 示例(目标项目的 `scripts/sync_standards.ps1`): - -```powershell -& "docs/standards/playbook/scripts/sync_standards.ps1" -Langs tsl,cpp - -$overlay = "docs/project/agents_overlay" -if (Test-Path $overlay) { - Copy-Item "$overlay\\*" ".agents\\tsl" -Recurse -Force - Write-Host "Applied agents overlay." -} -``` - -
- -#### 扩展新语言(模板) - -当目标项目需要新增一门语言(例如 C++),建议按以下模板扩展: - -- **文档**: - - 若使用本 Playbook 自带的 C++ 规范:无需额外 subtree,直接使用 - `docs/standards/playbook/docs/cpp/`,并在项目 `README.md`/`docs/index.md` - 链接入口。 - - 若新增"本 Playbook 未覆盖的语言":再引入对应语言的标准仓库(subtree/vendoring 到 - `docs/standards//`) -- **代理规则**: - - C++:运行 `sh docs/standards/playbook/scripts/sync_standards.sh -langs cpp`(或 - `& "docs/standards/playbook/scripts/sync_standards.ps1" -Langs cpp`),落地到 - `.agents/cpp/`(与 `.agents/tsl/` 并行)。 - - 其他语言:在目标项目增加 `.agents//`(与 `.agents/tsl/` - 并行),只写该语言专属要求与工具链约束 -- **同步策略**:每个规则集只同步到对应子目录(例如 `.agents/cpp/`),避免覆盖整个 - `.agents/` -- **CI/工具**:按文件类型分别执行格式化、lint、测试(不要让 TSL 规则去约束 C++ 代码,反之亦然) - - C++ 补全:建议在项目根目录提供 `.clangd` 并指向正确的 - `CompilationDatabase`(模板见 `templates/cpp/.clangd`) diff --git a/SKILLS.md b/SKILLS.md index 952fde6..0543995 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -46,41 +46,36 @@ $CODEX_HOME/skills//SKILL.md ## 3. 安装到本机(推荐) -本仓库已提供跨平台安装脚本(会把 `codex/skills/*` 复制到 -`$CODEX_HOME/skills/`): +使用统一入口 `playbook.py` 安装 skills(会把 `codex/skills/*` 复制到 `$CODEX_HOME/skills/`): -- macOS/Linux:`sh scripts/install_codex_skills.sh -all` -- PowerShell:`powershell -File scripts/install_codex_skills.ps1 -All` -- Windows bat:`scripts/install_codex_skills.bat -all` +```toml +# playbook.toml +[playbook] +project_root = "." -用法示例: +[install_skills] +mode = "all" # list|all +codex_home = "~/.codex" +``` ```bash -# 安装全部 skills -sh scripts/install_codex_skills.sh -all - -# 只安装指定 skills -sh scripts/install_codex_skills.sh -skills style-cleanup,commit-message +python scripts/playbook.py -config playbook.toml ``` -如果希望“项目内本地安装”(不污染全局),可用以下方式: +仅安装指定 skills: -```bash -# 安装到当前目录的 .codex/skills/ -sh scripts/install_codex_skills.sh -local -all - -# 或手动指定 CODEX_HOME -CODEX_HOME="$(pwd)/.codex" sh scripts/install_codex_skills.sh -all +```toml +[install_skills] +mode = "list" +skills = ["style-cleanup", "commit-message"] ``` -PowerShell / Windows: +如果希望“项目内本地安装”(不污染全局): -```powershell -powershell -File scripts/install_codex_skills.ps1 -Local -All -``` - -```bat -scripts\install_codex_skills.bat -local -all +```toml +[install_skills] +mode = "all" +codex_home = "./.codex" ``` > 注意:Codex 只会从 `CODEX_HOME` 加载 skills;使用本地安装时,启动 Codex 需设置同样的 `CODEX_HOME`。 @@ -89,7 +84,7 @@ scripts\install_codex_skills.bat -local -all `docs/standards/playbook`),则在目标项目里执行: ```bash -sh docs/standards/playbook/scripts/install_codex_skills.sh -all +python docs/standards/playbook/scripts/playbook.py -config playbook.toml ``` 安装后重启 `codex`,即可在运行时看到 `## Skills` 列表。 diff --git a/docs/plans/2026-01-23-unified-playbook-cli-design.md b/docs/plans/2026-01-23-unified-playbook-cli-design.md new file mode 100644 index 0000000..7348519 --- /dev/null +++ b/docs/plans/2026-01-23-unified-playbook-cli-design.md @@ -0,0 +1,55 @@ +# Unified Playbook CLI Design + +## 目标 + +- 提供单一入口 `scripts/playbook.py`,以 TOML 配置驱动所有动作。 +- 取消旧的 sh/ps1/bat 脚本与参数兼容,减少心智负担。 +- 保持功能覆盖:vendoring、同步模板、同步标准、安装 skills、格式化 Markdown。 + +## 非目标 + +- 不保留旧脚本的参数兼容层。 +- 不引入新的依赖(Markdown 格式化仅使用已有 Prettier)。 + +## CLI 设计 + +- 入口:`python scripts/playbook.py -config `。 +- 仅支持两个参数:`-config`(必填)与 `-h/-help`。 +- `project_root` 默认取配置文件所在目录。 + +## TOML 结构 + +- 通过 section 是否存在决定是否执行: + - `[vendor]` + - `[sync_templates]` + - `[sync_standards]` + - `[install_skills]` + - `[format_md]` +- 固定执行顺序(不支持 step list): + `vendor → sync_templates → sync_standards → install_skills → format_md`。 + +## 行为要点 + +- `vendor` 仅生成快照,不再隐式触发 `sync_standards`。 +- `sync_standards` 负责 `.agents/`、`AGENTS.md` 区块与 `.gitattributes`。 +- `sync_templates` 负责 memory-bank、docs/prompts、AGENTS/AGENT_RULES 模板。 +- `install_skills` 将 `codex/skills` 复制到目标 `~/.codex` 或指定路径。 +- `format_md` 仅调用已有 Prettier(可通过 globs 指定范围)。 + +## 预期输出 + +- 新增:`scripts/playbook.py`、`playbook.toml.example`。 +- 删除:旧的 sh/ps1/bat 脚本与对应测试。 +- 更新:README/模板说明/CI/test.yml。 + +## 测试策略 + +- 用 Python `unittest` 覆盖核心路径: + - TOML 解析与动作触发顺序。 + - vendor/sync 模拟执行与关键输出文件检查。 +- 保留现有模板验证与文档链接检查。 + +## 风险 + +- 旧脚本移除会影响现有用户;通过 README 与示例配置降低迁移成本。 +- Windows 环境权限可能影响 `npm install` 与符号链接;测试不依赖 npm。 diff --git a/docs/plans/2026-01-23-unified-playbook-cli.md b/docs/plans/2026-01-23-unified-playbook-cli.md new file mode 100644 index 0000000..d1795ad --- /dev/null +++ b/docs/plans/2026-01-23-unified-playbook-cli.md @@ -0,0 +1,408 @@ +# Unified Playbook CLI Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Replace legacy sh/ps1/bat scripts with a single Python CLI driven by TOML config, and update docs/tests/CI accordingly. + +**Architecture:** `scripts/playbook.py` reads `playbook.toml`, validates config, then executes actions in a fixed order based on section presence. Actions are implemented as pure-Python helpers to keep cross-platform behavior. + +**Tech Stack:** Python 3.11 (`tomllib`), standard library only; Prettier via local install if available. + +--- + +### Task 1: Create CLI test harness and basic argument handling + +**Files:** + +- Create: `tests/cli/test_playbook_cli.py` +- Modify: `tests/README.md` + +**Step 1: Write failing tests for CLI usage/exit codes** + +```python +import subprocess +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts" / "playbook.py" + + +def run(*args): + return subprocess.run([sys.executable, str(SCRIPT), *args], capture_output=True, text=True) + + +def test_help_shows_usage(): + result = run("-h") + assert result.returncode == 0 + assert "Usage:" in result.stderr or "Usage:" in result.stdout + + +def test_missing_config_is_error(): + result = run() + assert result.returncode != 0 + assert "-config" in (result.stderr + result.stdout) +``` + +**Step 2: Run tests to verify failure** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: FAIL (script missing) + +**Step 3: Implement minimal CLI skeleton** + +```python +# scripts/playbook.py +import sys + +def usage(): + return "Usage:\n python scripts/playbook.py -config \n python scripts/playbook.py -h" + + +def main(argv): + 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 + return 0 + +if __name__ == "__main__": + raise SystemExit(main(sys.argv[1:])) +``` + +**Step 4: Run tests to verify pass** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add scripts/playbook.py tests/cli/test_playbook_cli.py tests/README.md +git commit -m ":white_check_mark: test(cli): add basic playbook cli tests" +``` + +--- + +### Task 2: Parse TOML config and enforce dispatch order + +**Files:** + +- Modify: `scripts/playbook.py` +- Create: `playbook.toml.example` +- Modify: `README.md` + +**Step 1: Write failing test for TOML parsing + action order** + +```python +def test_action_order(tmp_path): + config = tmp_path / "playbook.toml" + config.write_text(""" +[playbook] +project_root = "." + +[format_md] + +[sync_standards] +langs = ["tsl"] +""") + result = run("-config", str(config)) + assert result.returncode == 0 + # Expect order: vendor -> sync_templates -> sync_standards -> install_skills -> format_md + # Only sections present should log as executed. + assert "sync_standards" in result.stdout + assert "format_md" in result.stdout +``` + +**Step 2: Run test to verify failure** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: FAIL (no TOML parser/action logs) + +**Step 3: Implement TOML parser + order dispatch** + +```python +import tomllib +from pathlib import Path + +ORDER = ["vendor", "sync_templates", "sync_standards", "install_skills", "format_md"] + + +def load_config(path: Path) -> dict: + return tomllib.loads(path.read_text(encoding="utf-8")) + + +def main(argv): + # parse -config value + # load config + # for section in ORDER: if section in config, call action + # print "[action] ..." for visibility +``` + +**Step 4: Run test to verify pass** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add scripts/playbook.py playbook.toml.example README.md +git commit -m ":sparkles: feat(cli): add toml config and dispatch order" +``` + +--- + +### Task 3: Implement `vendor` action (snapshot only) + +**Files:** + +- Modify: `scripts/playbook.py` +- Modify: `playbook.toml.example` +- Modify: `README.md` + +**Step 1: Write failing test for vendoring output** + +```python +def test_vendor_creates_snapshot(tmp_path): + config = tmp_path / "playbook.toml" + config.write_text(""" +[playbook] +project_root = "{root}" + +[vendor] +langs = ["tsl"] +""".format(root=tmp_path)) + result = run("-config", str(config)) + assert result.returncode == 0 + assert (tmp_path / "docs/standards/playbook/SOURCE.md").is_file() +``` + +**Step 2: Run test to verify failure** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: FAIL (vendor not implemented) + +**Step 3: Implement vendor snapshot copy** + +```python +from shutil import copy2, copytree + +# Copy: scripts/, codex/, rulesets/, docs/common, docs/, templates/ci, templates/ +# Generate docs/index.md, README.md, SOURCE.md +# Backup existing snapshot with timestamp +``` + +**Step 4: Run test to verify pass** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add scripts/playbook.py playbook.toml.example README.md tests/cli/test_playbook_cli.py +git commit -m ":sparkles: feat(vendor): add playbook snapshot generation" +``` + +--- + +### Task 4: Implement `sync_templates` and `sync_standards` + +**Files:** + +- Modify: `scripts/playbook.py` +- Modify: `playbook.toml.example` +- Modify: `templates/README.md` + +**Step 1: Add failing tests for template sync + standards sync** + +```python +def test_sync_templates_creates_memory_bank(tmp_path): + config = tmp_path / "playbook.toml" + config.write_text(""" +[playbook] +project_root = "{root}" + +[sync_templates] +project_name = "Demo" +""".format(root=tmp_path)) + result = run("-config", str(config)) + assert result.returncode == 0 + assert (tmp_path / "memory-bank" / "project-brief.md").is_file() + + +def test_sync_standards_creates_agents(tmp_path): + config = tmp_path / "playbook.toml" + config.write_text(""" +[playbook] +project_root = "{root}" + +[sync_standards] +langs = ["tsl"] +""".format(root=tmp_path)) + result = run("-config", str(config)) + assert result.returncode == 0 + assert (tmp_path / ".agents" / "tsl" / "index.md").is_file() +``` + +**Step 2: Run tests to verify failure** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: FAIL + +**Step 3: Implement `sync_templates`** + +```python +# Copy templates/memory-bank -> /memory-bank (rename *.template.md) +# Copy templates/prompts -> /docs/prompts (rename *.template.md) +# Copy AGENTS.template.md -> AGENTS.md (merge/append section) +# Copy AGENT_RULES.template.md -> AGENT_RULES.md (backup optional) +``` + +**Step 4: Implement `sync_standards`** + +```python +# Copy rulesets/ -> /.agents/ +# Update AGENTS.md playbook block (robust to blank lines) +# Update .gitattributes according to gitattr_mode +``` + +**Step 5: Run tests to verify pass** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: PASS + +**Step 6: Commit** + +```bash +git add scripts/playbook.py playbook.toml.example templates/README.md tests/cli/test_playbook_cli.py +git commit -m ":sparkles: feat(sync): add templates and standards actions" +``` + +--- + +### Task 5: Implement `install_skills` and `format_md` + +**Files:** + +- Modify: `scripts/playbook.py` +- Modify: `playbook.toml.example` +- Modify: `README.md` + +**Step 1: Add failing tests for skills install + md format** + +```python +def test_install_skills(tmp_path): + config = tmp_path / "playbook.toml" + target = tmp_path / "codex" + config.write_text(f""" +[playbook] +project_root = "{tmp_path}" + +[install_skills] +codex_home = "{target}" +mode = "list" +skills = ["brainstorming"] +""") + result = run("-config", str(config)) + assert result.returncode == 0 + assert (target / "skills" / "brainstorming" / "SKILL.md").is_file() +``` + +**Step 2: Run tests to verify failure** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: FAIL + +**Step 3: Implement `install_skills` and `format_md`** + +```python +# install_skills: copy codex/skills/ -> /skills/ +# format_md: call `prettier` with configured globs if available +``` + +**Step 4: Run tests to verify pass** + +Run: `python -m unittest tests/cli/test_playbook_cli.py -v` +Expected: PASS + +**Step 5: Commit** + +```bash +git add scripts/playbook.py playbook.toml.example tests/cli/test_playbook_cli.py README.md +git commit -m ":sparkles: feat(actions): add install_skills and format_md" +``` + +--- + +### Task 6: Remove legacy scripts and update CI/test docs + +**Files:** + +- Delete: `scripts/*.sh`, `scripts/*.ps1`, `scripts/*.bat` +- Delete: `tests/scripts/*.bats` +- Modify: `.gitea/workflows/test.yml` +- Modify: `tests/README.md` +- Modify: `README.md`, `templates/README.md` + +**Step 1: Remove legacy scripts and obsolete tests** + +```bash +rm scripts/*.sh scripts/*.ps1 scripts/*.bat +rm tests/scripts/*.bats +``` + +**Step 2: Update CI to run new Python tests + existing template/link checks** + +```yaml +- name: Run CLI tests + run: python -m unittest discover -s tests/cli -v +- name: Validate templates + run: | + sh tests/templates/validate_python_templates.sh + sh tests/templates/validate_cpp_templates.sh + sh tests/templates/validate_ci_templates.sh + sh tests/templates/validate_project_templates.sh +- name: Check doc links + run: sh tests/integration/check_doc_links.sh +``` + +**Step 3: Update docs to new CLI + TOML** + +- Replace old script usage with `python scripts/playbook.py -config playbook.toml`. +- Document config sections and example file name. + +**Step 4: Run full test suite** + +Run: `python -m unittest discover -s tests/cli -v && sh tests/templates/validate_project_templates.sh && sh tests/integration/check_doc_links.sh` +Expected: PASS + +**Step 5: Commit** + +```bash +git add . +git commit -m ":wastebasket: remove(legacy): drop old scripts and tests" +``` + +--- + +### Task 7: Final cleanup and formatting + +**Files:** + +- Modify: `README.md`, `templates/README.md`, `tests/README.md` + +**Step 1: Run Markdown format (exclude third-party skills)** + +Run: `npm run format:md -- --ignore-path .prettierignore` +Expected: No unexpected diffs + +**Step 2: Commit formatting-only changes (if any)** + +```bash +git add README.md templates/README.md tests/README.md +git commit -m ":art: style(docs): format markdown" +``` diff --git a/playbook.toml.example b/playbook.toml.example new file mode 100644 index 0000000..f8274b7 --- /dev/null +++ b/playbook.toml.example @@ -0,0 +1,32 @@ +# playbook.toml (example) +# 配置文件所在目录默认作为 project_root。 + +[playbook] +# project_root = "." # 可选:覆盖目标项目根目录 +# verbose = false # 可选:输出更详细日志 +# dry_run = false # 可选:仅预览,不写入 + +[vendor] +# 将 playbook 裁剪快照写入 /docs/standards/playbook +# langs = ["tsl"] # 可选:默认仅 tsl +# target_dir = "docs/standards/playbook" + +[sync_templates] +# project_name = "MyProject" # 可选:替换 {{PROJECT_NAME}} +# date = "2026-01-23" # 可选:替换 {{DATE}},默认今天 +# force = false # 可选:覆盖已有目录 +# no_backup = false # 可选:跳过备份 +# full = false # 可选:写入 framework 区块 + +[sync_standards] +# langs = ["tsl", "cpp"] # 必填:要同步的语言 +# gitattr_mode = "append" # append|overwrite|block|skip + +[install_skills] +# mode = "list" # list|all +# skills = ["brainstorming"] # mode=list 时必填 +# codex_home = "~/.codex" # 可选:默认 ~/.codex + +[format_md] +# tool = "prettier" # 仅支持 prettier +# globs = ["**/*.md"] # 可选:默认全量 Markdown diff --git a/scripts/install_codex_skills.bat b/scripts/install_codex_skills.bat deleted file mode 100644 index 699d0f2..0000000 --- a/scripts/install_codex_skills.bat +++ /dev/null @@ -1,135 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -rem Install Codex skills from this Playbook snapshot into CODEX_HOME. -rem - Source: \codex\skills\\ -rem - Dest: %CODEX_HOME%\skills\\ (default CODEX_HOME=%USERPROFILE%\.codex) -rem -rem Usage: -rem install_codex_skills.bat -all -rem install_codex_skills.bat -skills style-cleanup,commit-message -rem install_codex_skills.bat -local -all -rem -rem Notes: -rem - Codex loads skills at startup; restart `codex` after installation. -rem - Existing destination skill dirs are backed up with a random suffix. - -set "SCRIPT_DIR=%~dp0" -for %%I in ("%SCRIPT_DIR%..") do set "SRC=%%~fI" -set "SKILLS_SRC_ROOT=%SRC%\\codex\\skills" - -set "LOCAL_MODE=0" -set "INSTALL_ALL=0" -set "SKILLS=" -:parse_opts -if "%~1"=="" goto opts_done -if /I "%~1"=="-help" goto show_help -if /I "%~1"=="-h" goto show_help -if /I "%~1"=="-local" ( - set "LOCAL_MODE=1" - shift - goto parse_opts -) -if /I "%~1"=="-l" ( - set "LOCAL_MODE=1" - shift - goto parse_opts -) -if /I "%~1"=="-all" ( - set "INSTALL_ALL=1" - shift - goto parse_opts -) -if /I "%~1"=="-skills" ( - if "%~2"=="" ( - echo ERROR: -skills requires a value. - exit /b 1 - ) - set "SKILLS=%~2" - shift - shift - goto parse_opts -) -echo ERROR: Unknown option: %~1 -exit /b 1 -goto opts_done - -:opts_done -set "CODEX_HOME=%CODEX_HOME%" -if "%LOCAL_MODE%"=="1" if "%CODEX_HOME%"=="" set "CODEX_HOME=%CD%\\.codex" -if "%CODEX_HOME%"=="" set "CODEX_HOME=%USERPROFILE%\\.codex" -set "SKILLS_DST_ROOT=%CODEX_HOME%\\skills" - -if not exist "%SKILLS_SRC_ROOT%" ( - echo ERROR: skills source dir not found: "%SKILLS_SRC_ROOT%" - exit /b 1 -) - -if not exist "%SKILLS_DST_ROOT%" mkdir "%SKILLS_DST_ROOT%" - -if "%INSTALL_ALL%"=="1" if not "%SKILLS%"=="" ( - echo ERROR: use either -all or -skills, not both. - exit /b 1 -) -if "%INSTALL_ALL%"=="0" if "%SKILLS%"=="" ( - echo ERROR: -all or -skills is required. - exit /b 1 -) - -if "%INSTALL_ALL%"=="1" ( - for /d %%D in ("%SKILLS_SRC_ROOT%\\*") do ( - set "NAME=%%~nD" - if not "!NAME!"=="" if not "!NAME:~0,1!"=="." call :InstallOne "!NAME!" - ) -) else ( - set "SKILLS=%SKILLS:,= %" - for %%S in (%SKILLS%) do call :InstallOne "%%~S" -) - -:Done -echo Done. Skills installed to: "%SKILLS_DST_ROOT%" -endlocal -exit /b 0 - -:show_help -echo Usage: -echo install_codex_skills.bat -all -echo install_codex_skills.bat -skills style-cleanup,commit-message -echo. -echo Options: -echo -local, -l Install to .\\.codex ^(or CODEX_HOME if set^) -echo -skills LIST Comma/space-separated skill names -echo -all Install all skills -echo -help, -h Show this help -echo. -echo Env: -echo CODEX_HOME Target Codex home ^(default: %%USERPROFILE%%\\.codex^) -exit /b 0 - -:InstallOne -set "NAME=%~1" -set "SRC_DIR=%SKILLS_SRC_ROOT%\\%NAME%" -set "DST_DIR=%SKILLS_DST_ROOT%\\%NAME%" - -if not exist "%SRC_DIR%" ( - echo ERROR: skill not found: %NAME% "%SRC_DIR%" - exit /b 1 -) - -if exist "%DST_DIR%" ( - set "RAND=%RANDOM%" - pushd "%SKILLS_DST_ROOT%" - ren "%NAME%" "%NAME%.bak.!RAND!" - popd - echo Backed up existing skill: %NAME% -> %NAME%.bak.!RAND! -) - -xcopy "%SRC_DIR%\\*" "%DST_DIR%\\" /e /i /y >nul -if errorlevel 1 ( - echo ERROR: failed to copy skill: %NAME% - exit /b 1 -) - -echo Installed: %NAME% -exit /b 0 - diff --git a/scripts/install_codex_skills.ps1 b/scripts/install_codex_skills.ps1 deleted file mode 100644 index 03cafe9..0000000 --- a/scripts/install_codex_skills.ps1 +++ /dev/null @@ -1,105 +0,0 @@ -# Install Codex skills from this Playbook snapshot into CODEX_HOME. -# - Source: \codex\skills\\ -# - Dest: $env:CODEX_HOME\skills\\ (default CODEX_HOME=$HOME\.codex) -# -# Usage: -# powershell -File scripts/install_codex_skills.ps1 -# powershell -File scripts/install_codex_skills.ps1 style-cleanup commit-message -# powershell -File scripts/install_codex_skills.ps1 -Local -# -# Notes: -# - Codex loads skills at startup; restart `codex` after installation. -# - Existing destination skill dirs are backed up with a timestamp suffix. - -[CmdletBinding()] -param( - [Alias('h', '?')] - [switch]$Help, - - [switch]$Local, - [switch]$All, - [Parameter(Mandatory = $false)] - [string[]]$Skills -) - -$ErrorActionPreference = "Stop" - -if ($Help) { - Write-Host "Usage:" - Write-Host " powershell -File scripts/install_codex_skills.ps1 -All" - Write-Host " powershell -File scripts/install_codex_skills.ps1 -Skills style-cleanup,commit-message" - Write-Host "" - Write-Host "Options:" - Write-Host " -Local Install to ./.codex (or CODEX_HOME if set)." - Write-Host " -Skills Comma/space-separated skill names." - Write-Host " -All Install all skills." - Write-Host " -Help Show this help." - Write-Host "" - Write-Host "Env:" - Write-Host " CODEX_HOME Target Codex home (default: ~/.codex)." - exit 0 -} - -if ($All -and $Skills -and $Skills.Count -gt 0) { - throw "Use either -All or -Skills, not both." -} -if (-not $All -and (-not $Skills -or $Skills.Count -eq 0)) { - throw "Missing -All or -Skills. Use -Help for usage." -} -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$Src = (Resolve-Path (Join-Path $ScriptDir "..")).Path -$SkillsSrcRoot = Join-Path $Src "codex/skills" - -if (-not (Test-Path $SkillsSrcRoot)) { - throw "Skills source dir not found: $SkillsSrcRoot" -} - -$CodexHome = $env:CODEX_HOME -if ($Local) { - $localHome = Join-Path (Get-Location) ".codex" - if (-not $CodexHome) { $CodexHome = $localHome } -} -if (-not $CodexHome) { - $homeDir = $HOME - if (-not $homeDir) { $homeDir = $env:USERPROFILE } - $CodexHome = (Join-Path $homeDir ".codex") -} -$SkillsDstRoot = Join-Path $CodexHome "skills" -New-Item -ItemType Directory -Path $SkillsDstRoot -Force | Out-Null - -$timestamp = Get-Date -Format "yyyyMMddHHmmss" - -function Install-One([string]$Name) { - $srcDir = Join-Path $SkillsSrcRoot $Name - $dstDir = Join-Path $SkillsDstRoot $Name - - if (-not (Test-Path $srcDir)) { - throw "Skill not found: $Name ($srcDir)" - } - - if (Test-Path $dstDir) { - $bak = Join-Path $SkillsDstRoot "$Name.bak.$timestamp" - Move-Item $dstDir $bak - Write-Host "Backed up existing skill: $Name -> $(Split-Path -Leaf $bak)" - } - - Copy-Item $srcDir $dstDir -Recurse -Force - Write-Host "Installed: $Name" -} - -if ($All) { - foreach ($dir in (Get-ChildItem -Path $SkillsSrcRoot -Directory)) { - if ($dir.Name.StartsWith(".")) { continue } - Install-One $dir.Name - } -} else { - foreach ($item in $Skills) { - if (-not $item) { continue } - foreach ($part in $item.Split(@(',', ' '), [System.StringSplitOptions]::RemoveEmptyEntries)) { - if (-not $part) { continue } - Install-One $part - } - } -} - -Write-Host "Done. Skills installed to: $SkillsDstRoot" diff --git a/scripts/install_codex_skills.sh b/scripts/install_codex_skills.sh deleted file mode 100644 index d4364c5..0000000 --- a/scripts/install_codex_skills.sh +++ /dev/null @@ -1,142 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Install Codex skills from this Playbook snapshot into CODEX_HOME. -# - Source: /codex/skills// -# - Dest: $CODEX_HOME/skills// (default CODEX_HOME=~/.codex) -# -# Usage: -# sh scripts/install_codex_skills.sh -all -# sh scripts/install_codex_skills.sh -skills style-cleanup,commit-message -# sh scripts/install_codex_skills.sh -local -all # install to /.codex -# -# Notes: -# - Codex loads skills at startup; restart `codex` after installation. -# - Existing destination skill dirs are backed up with a timestamp suffix. - -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" -SRC="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)" -SKILLS_SRC_ROOT="$SRC/codex/skills" - -usage() { - cat <<'EOF' >&2 -Usage: - sh scripts/install_codex_skills.sh [options] - sh scripts/install_codex_skills.sh -skills style-cleanup,commit-message - sh scripts/install_codex_skills.sh -all - -Options: - -local, -l Install to ./.codex (or CODEX_HOME if set). - -skills LIST Comma/space-separated skill names. - -all Install all skills. - -h, -help Show this help. - -Env: - CODEX_HOME Target Codex home (default: ~/.codex). -EOF -} - -LOCAL_MODE=0 -INSTALL_ALL=0 -SKILLS="" -while [ $# -gt 0 ]; do - case "$1" in - -local|-l) - LOCAL_MODE=1 - shift - ;; - -skills) - if [ $# -lt 2 ] || [ -z "${2:-}" ]; then - echo "ERROR: -skills requires a value." >&2 - usage - exit 1 - fi - SKILLS="$2" - shift 2 - ;; - -all) - INSTALL_ALL=1 - shift - ;; - -h|-help) - usage - exit 0 - ;; - -*) - echo "ERROR: Unknown option: $1" >&2 - usage - exit 1 - ;; - *) - echo "ERROR: positional args are not supported; use -skills/-all." >&2 - usage - exit 1 - ;; - esac -done - -if [ "$INSTALL_ALL" -eq 1 ] && [ -n "$SKILLS" ]; then - echo "ERROR: use either -all or -skills, not both." >&2 - usage - exit 1 -fi -if [ "$INSTALL_ALL" -eq 0 ] && [ -z "$SKILLS" ]; then - echo "ERROR: -all or -skills is required." >&2 - usage - exit 1 -fi - -if [ "$LOCAL_MODE" -eq 1 ]; then - LOCAL_CODEX_HOME="$(pwd -P)/.codex" - CODEX_HOME="${CODEX_HOME:-$LOCAL_CODEX_HOME}" -fi - -CODEX_HOME="${CODEX_HOME:-$HOME/.codex}" -SKILLS_DST_ROOT="$CODEX_HOME/skills" - -if [ ! -d "$SKILLS_SRC_ROOT" ]; then - echo "ERROR: skills source dir not found: $SKILLS_SRC_ROOT" >&2 - exit 1 -fi - -mkdir -p "$SKILLS_DST_ROOT" -timestamp="$(date +%Y%m%d%H%M%S 2>/dev/null || echo bak)" - -install_one() { - name="$1" - src_dir="$SKILLS_SRC_ROOT/$name" - dst_dir="$SKILLS_DST_ROOT/$name" - - if [ ! -d "$src_dir" ]; then - echo "ERROR: skill not found: $name ($src_dir)" >&2 - exit 1 - fi - if [ -e "$dst_dir" ]; then - mv "$dst_dir" "$SKILLS_DST_ROOT/$name.bak.$timestamp" - echo "Backed up existing skill: $name -> $name.bak.$timestamp" - fi - cp -R "$src_dir" "$dst_dir" - echo "Installed: $name" -} - -if [ "$INSTALL_ALL" -eq 1 ]; then - for dir in "$SKILLS_SRC_ROOT"/*; do - [ -d "$dir" ] || continue - name="$(basename -- "$dir")" - case "$name" in - ""|.*) continue ;; - esac - install_one "$name" - done -else - old_ifs="${IFS}" - IFS=', ' - set -- $SKILLS - IFS="${old_ifs}" - for name in "$@"; do - [ -n "$name" ] || continue - install_one "$name" - done -fi - -echo "Done. Skills installed to: $SKILLS_DST_ROOT" diff --git a/scripts/playbook.py b/scripts/playbook.py new file mode 100644 index 0000000..bc04219 --- /dev/null +++ b/scripts/playbook.py @@ -0,0 +1,805 @@ +#!/usr/bin/env python3 +import sys +from datetime import datetime, timezone +from pathlib import Path +from shutil import copy2, copytree, which +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 \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 replace_placeholders(text: str, project_name: str | None, date_value: str) -> str: + result = text.replace("{{DATE}}", date_value) + if project_name: + result = result.replace("{{PROJECT_NAME}}", project_name) + return result + + +def backup_path(path: Path, no_backup: bool) -> None: + if not path.exists() or no_backup: + return + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + backup = path.with_name(f"{path.name}.bak.{timestamp}") + path.rename(backup) + log(f"Backed up: {path} -> {backup}") + + +def rename_template_files(root: Path) -> None: + for template in root.rglob("*.template.md"): + target = template.with_name(template.name.replace(".template.md", ".md")) + template.rename(target) + + +def replace_placeholders_in_dir(root: Path, project_name: str | None, date_value: str) -> None: + for file_path in root.rglob("*.md"): + text = file_path.read_text(encoding="utf-8") + updated = replace_placeholders(text, project_name, date_value) + if updated != text: + file_path.write_text(updated, encoding="utf-8") + + +def extract_block_lines(text: str, start: str, end: str) -> list[str]: + lines = text.splitlines() + block: list[str] = [] + in_block = False + for line in lines: + if line.strip() == start: + in_block = True + if in_block: + block.append(line) + if in_block and line.strip() == end: + break + if not block or block[-1].strip() != end: + return [] + return block + + +def update_agents_section( + agents_path: Path, + template_path: Path, + start_marker: str, + end_marker: str, + project_name: str | None, + date_value: str, +) -> None: + template_text = template_path.read_text(encoding="utf-8") + template_text = replace_placeholders(template_text, project_name, date_value) + block = extract_block_lines(template_text, start_marker, end_marker) + if not block: + log("Skip: markers not found in template") + return + + if not agents_path.exists(): + agents_path.write_text(template_text + "\n", encoding="utf-8") + log("Created: AGENTS.md") + return + + agents_text = agents_path.read_text(encoding="utf-8") + if start_marker in agents_text: + lines = agents_text.splitlines() + updated: list[str] = [] + in_block = False + replaced = False + for line in lines: + if not replaced and line.strip() == start_marker: + updated.extend(block) + in_block = True + replaced = True + continue + if in_block: + if line.strip() == end_marker: + in_block = False + continue + updated.append(line) + agents_path.write_text("\n".join(updated) + "\n", encoding="utf-8") + log("Updated: AGENTS.md (section)") + else: + if ".agents/index.md" in agents_text: + log("Skip: AGENTS.md already references .agents/index.md") + return + updated = agents_text.rstrip("\n") + "\n\n" + "\n".join(block) + "\n" + agents_path.write_text(updated, encoding="utf-8") + log("Appended: AGENTS.md (section)") + + +def sync_templates_action(config: dict, context: dict) -> int: + project_root: Path = context["project_root"] + if project_root.resolve() == PLAYBOOK_ROOT.resolve(): + log("Skip: playbook root equals project root.") + return 0 + + templates_dir = PLAYBOOK_ROOT / "templates" + if not templates_dir.is_dir(): + print(f"ERROR: templates not found: {templates_dir}", file=sys.stderr) + return 2 + + project_name = config.get("project_name") + date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d") + force = bool(config.get("force", False)) + no_backup = bool(config.get("no_backup", False)) + full = bool(config.get("full", False)) + + memory_src = templates_dir / "memory-bank" + prompts_src = templates_dir / "prompts" + agents_src = templates_dir / "AGENTS.template.md" + rules_src = templates_dir / "AGENT_RULES.template.md" + + if memory_src.is_dir(): + memory_dst = project_root / "memory-bank" + if memory_dst.exists() and not force: + log("memory-bank/ already exists. Use force to overwrite.") + else: + backup_path(memory_dst, no_backup) + copytree(memory_src, memory_dst) + rename_template_files(memory_dst) + replace_placeholders_in_dir(memory_dst, project_name, date_value) + log("Synced: memory-bank/") + + if prompts_src.is_dir(): + prompts_dst = project_root / "docs/prompts" + if prompts_dst.exists() and not force: + log("docs/prompts/ already exists. Use force to overwrite.") + else: + backup_path(prompts_dst, no_backup) + ensure_dir(prompts_dst.parent) + copytree(prompts_src, prompts_dst) + rename_template_files(prompts_dst) + replace_placeholders_in_dir(prompts_dst, project_name, date_value) + log("Synced: docs/prompts/") + + if agents_src.is_file(): + agents_dst = project_root / "AGENTS.md" + if full: + start_marker = "" + end_marker = "" + else: + start_marker = "" + end_marker = "" + update_agents_section( + agents_dst, agents_src, start_marker, end_marker, project_name, date_value + ) + + if rules_src.is_file(): + rules_dst = project_root / "AGENT_RULES.md" + if rules_dst.exists() and not force: + log("AGENT_RULES.md already exists. Use force to overwrite.") + else: + backup_path(rules_dst, no_backup) + text = rules_src.read_text(encoding="utf-8") + text = replace_placeholders(text, project_name, date_value) + rules_dst.write_text(text + "\n", encoding="utf-8") + log("Synced: AGENT_RULES.md") + + return 0 + + +def render_agents_block(langs: list[str]) -> list[str]: + entries = [f"`.agents/{lang}/index.md`" for lang in langs] + langs_line = "、".join(entries) if entries else "" + lines = [ + "", + "请以 `.agents/` 下的规则为准:", + "- 入口:`.agents/index.md`", + f"- 语言规则:{langs_line}" if langs_line else "- 语言规则:", + "", + ] + return lines + + +def update_agents_block(agents_md: Path, block_lines: list[str]) -> None: + start = "" + end = "" + if not agents_md.exists(): + content = "# Agent Instructions\n\n" + "\n".join(block_lines) + "\n" + agents_md.write_text(content, encoding="utf-8") + log("Created AGENTS.md") + return + + text = agents_md.read_text(encoding="utf-8") + if start in text: + lines = text.splitlines() + updated: list[str] = [] + in_block = False + replaced = False + for line in lines: + if not replaced and line.strip() == start: + updated.extend(block_lines) + in_block = True + replaced = True + continue + if in_block: + if line.strip() == end: + in_block = False + continue + updated.append(line) + agents_md.write_text("\n".join(updated) + "\n", encoding="utf-8") + log("Updated AGENTS.md (playbook block).") + else: + if ".agents/index.md" in text: + log("Skip: AGENTS.md already references .agents/index.md") + return + updated = text.rstrip("\n") + "\n\n" + "\n".join(block_lines) + "\n" + agents_md.write_text(updated, encoding="utf-8") + log("Appended playbook block to AGENTS.md") + + +def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | None) -> None: + agents_index = agents_root / "index.md" + if agents_index.exists(): + return + lines = [ + "# .agents(多语言)", + "", + "本目录用于存放仓库级/语言级的代理规则集。", + "", + "建议约定:", + "", + "- `.agents/tsl/`:TSL 相关规则集(由 playbook 同步;适用于 `.tsl`/`.tsf`)", + "- `.agents/cpp/`:C++ 相关规则集(由 playbook 同步;适用于 C++23/Modules)", + "- `.agents/python/`:Python 相关规则集(由 playbook 同步)", + "- `.agents/markdown/`:Markdown 相关规则集(仅代码格式化)", + "", + "规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。", + "", + "入口建议从:", + "", + ] + for lang in langs: + lines.append(f"- `.agents/{lang}/index.md`") + lines += [ + "", + "标准快照文档入口:", + "", + f"- {docs_prefix or 'docs/standards/playbook/docs/'}", + ] + agents_index.write_text("\n".join(lines) + "\n", encoding="utf-8") + log("Created .agents/index.md") + + +def rewrite_agents_docs_links(agents_dir: Path, docs_prefix: str) -> None: + replacements = { + "`docs/tsl/": f"`{docs_prefix}/tsl/", + "`docs/cpp/": f"`{docs_prefix}/cpp/", + "`docs/python/": f"`{docs_prefix}/python/", + "`docs/markdown/": f"`{docs_prefix}/markdown/", + "`docs/common/": f"`{docs_prefix}/common/", + } + for md_path in agents_dir.glob("*.md"): + if not md_path.is_file(): + continue + text = md_path.read_text(encoding="utf-8") + updated = text + for old, new in replacements.items(): + updated = updated.replace(old, new) + if updated != text: + md_path.write_text(updated, encoding="utf-8") + + +def read_gitattributes_entries(path: Path) -> list[str]: + entries: list[str] = [] + for line in path.read_text(encoding="utf-8").splitlines(): + stripped = line.strip() + if not stripped or stripped.startswith("#"): + continue + entries.append(stripped) + return entries + + +def sync_gitattributes_overwrite(src: Path, dst: Path) -> None: + if src.resolve() == dst.resolve(): + log("Skip: .gitattributes source equals destination.") + return + backup_path(dst, False) + copy2(src, dst) + log("Synced .gitattributes from standards (overwrite).") + + +def sync_gitattributes_append(src: Path, dst: Path, source_note: str) -> None: + src_entries = read_gitattributes_entries(src) + dst_entries: list[str] = [] + if dst.exists(): + dst_entries = read_gitattributes_entries(dst) + missing = [line for line in src_entries if line not in set(dst_entries)] + if not missing: + log("No missing .gitattributes rules to append.") + return + + original = dst.read_text(encoding="utf-8") if dst.exists() else "" + backup_path(dst, False) + header = f"# Added from playbook .gitattributes (source: {source_note})" + content = original.rstrip("\n") + if content: + content += "\n\n" + content += header + "\n" + "\n".join(missing) + "\n" + dst.write_text(content, encoding="utf-8") + log("Appended missing .gitattributes rules from standards.") + + +def sync_gitattributes_block(src: Path, dst: Path) -> None: + begin = "# BEGIN playbook .gitattributes" + end = "# END playbook .gitattributes" + begin_old = "# BEGIN tsl-playbook .gitattributes" + end_old = "# END tsl-playbook .gitattributes" + + src_lines = src.read_text(encoding="utf-8").splitlines() + block_lines = [begin] + src_lines + [end] + + if dst.exists(): + original = dst.read_text(encoding="utf-8").splitlines() + updated: list[str] = [] + in_block = False + replaced = False + for line in original: + if line == begin or line == begin_old: + if not replaced: + updated.extend(block_lines) + replaced = True + in_block = True + continue + if in_block: + if line == end or line == end_old: + in_block = False + continue + updated.append(line) + if not replaced: + if updated and updated[-1].strip(): + updated.append("") + updated.extend(block_lines) + backup_path(dst, False) + dst.write_text("\n".join(updated) + "\n", encoding="utf-8") + else: + dst.write_text("\n".join(block_lines) + "\n", encoding="utf-8") + log("Synced .gitattributes from standards (block).") + + +def sync_standards_action(config: dict, context: dict) -> int: + if "langs" not in config: + print("ERROR: langs is required for sync_standards", file=sys.stderr) + return 2 + try: + langs = normalize_langs(config.get("langs")) + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + + project_root: Path = context["project_root"] + agents_root = project_root / ".agents" + ensure_dir(agents_root) + + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + for lang in langs: + src = PLAYBOOK_ROOT / "rulesets" / lang + if not src.is_dir(): + print(f"ERROR: agents ruleset not found: {src}", file=sys.stderr) + return 2 + dst = agents_root / lang + if dst.exists(): + backup = agents_root / f"{lang}.bak.{timestamp}" + dst.rename(backup) + log(f"Backed up existing {lang} agents -> {backup.name}") + copytree(src, dst) + log(f"Synced .agents/{lang} from standards.") + + docs_prefix = None + try: + rel_snapshot = PLAYBOOK_ROOT.resolve().relative_to(project_root.resolve()) + if str(rel_snapshot) != ".": + docs_prefix = f"{rel_snapshot.as_posix()}/docs" + except ValueError: + docs_prefix = None + + if docs_prefix: + for lang in langs: + rewrite_agents_docs_links(agents_root / lang, docs_prefix) + + agents_md = project_root / "AGENTS.md" + block_lines = render_agents_block(langs) + update_agents_block(agents_md, block_lines) + + create_agents_index(agents_root, langs, docs_prefix) + + gitattributes_src = PLAYBOOK_ROOT / ".gitattributes" + if gitattributes_src.is_file(): + mode = str(config.get("gitattr_mode", "append")).lower() + gitattributes_dst = project_root / ".gitattributes" + source_note = str(gitattributes_src) + try: + source_note = str(gitattributes_src.resolve().relative_to(project_root.resolve())) + except ValueError: + source_note = str(gitattributes_src) + + if mode == "skip": + log("Skip: .gitattributes sync (mode=skip).") + elif mode == "overwrite": + sync_gitattributes_overwrite(gitattributes_src, gitattributes_dst) + elif mode == "block": + sync_gitattributes_block(gitattributes_src, gitattributes_dst) + else: + sync_gitattributes_append(gitattributes_src, gitattributes_dst, source_note) + + return 0 + + +def normalize_names(raw: object, label: str) -> list[str]: + if raw is None: + raise ValueError(f"{label} is required") + if isinstance(raw, str): + items = [raw] + else: + items = list(raw) + cleaned: list[str] = [] + for item in items: + name = str(item).strip() + if not name: + continue + if "/" in name or "\\" in name or ".." in name: + raise ValueError(f"invalid {label}: {name}") + cleaned.append(name) + if not cleaned: + raise ValueError(f"{label} is empty") + return cleaned + + +def normalize_globs(raw: object) -> list[str]: + if raw is None: + return ["**/*.md"] + if isinstance(raw, str): + items = [raw] + else: + items = list(raw) + cleaned = [str(item).strip() for item in items if str(item).strip()] + return cleaned or ["**/*.md"] + + +def install_skills_action(config: dict, context: dict) -> int: + mode = str(config.get("mode", "list")).lower() + codex_home = Path(config.get("codex_home", "~/.codex")).expanduser() + if not codex_home.is_absolute(): + codex_home = (context["project_root"] / codex_home).resolve() + + skills_src_root = PLAYBOOK_ROOT / "codex/skills" + if not skills_src_root.is_dir(): + print(f"ERROR: skills source not found: {skills_src_root}", file=sys.stderr) + return 2 + + skills_dst_root = codex_home / "skills" + ensure_dir(skills_dst_root) + + if mode == "all": + skills = [ + path.name + for path in skills_src_root.iterdir() + if path.is_dir() and not path.name.startswith(".") + ] + elif mode == "list": + try: + skills = normalize_names(config.get("skills"), "skills") + except ValueError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + return 2 + else: + print("ERROR: mode must be list or all", file=sys.stderr) + return 2 + + timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + for name in skills: + src = skills_src_root / name + if not src.is_dir(): + print(f"ERROR: skill not found: {name}", file=sys.stderr) + return 2 + dst = skills_dst_root / name + if dst.exists(): + backup = skills_dst_root / f"{name}.bak.{timestamp}" + dst.rename(backup) + log(f"Backed up existing skill: {name} -> {backup.name}") + copytree(src, dst) + log(f"Installed: {name}") + + return 0 + + +def format_md_action(config: dict, context: dict) -> int: + tool = str(config.get("tool", "prettier")).lower() + if tool != "prettier": + print("ERROR: format_md.tool only supports prettier", file=sys.stderr) + return 2 + + project_root: Path = context["project_root"] + prettier = project_root / "node_modules/.bin/prettier" + if not prettier.is_file(): + prettier = PLAYBOOK_ROOT / "node_modules/.bin/prettier" + if not prettier.is_file(): + resolved = which("prettier") + if resolved: + prettier = Path(resolved) + else: + log("Skip: prettier not found.") + return 0 + + globs_raw = config.get("globs", ["**/*.md"]) + globs = normalize_globs(globs_raw) + result = subprocess.run( + [str(prettier), "-w", *globs], + cwd=project_root, + capture_output=True, + text=True, + ) + if result.returncode != 0: + sys.stderr.write(result.stderr) + return result.returncode + + +def run_action(name: str, config: dict, context: dict) -> int: + print(f"[action] {name}") + if name == "vendor": + return vendor_action(config, context) + if name == "sync_templates": + return sync_templates_action(config, context) + if name == "sync_standards": + return sync_standards_action(config, context) + if name == "install_skills": + return install_skills_action(config, context) + if name == "format_md": + return format_md_action(config, context) + 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() + if not root.is_absolute(): + root = (config_path.parent / root).resolve() + 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:])) diff --git a/scripts/sync_standards.bat b/scripts/sync_standards.bat deleted file mode 100644 index eef5294..0000000 --- a/scripts/sync_standards.bat +++ /dev/null @@ -1,420 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -rem Sync standards snapshot to project root. -rem - Copies \rulesets\ -> \.agents\ -rem - Updates \.gitattributes (append missing rules by default) -rem Existing targets are backed up before overwrite. -rem -rem Multi rulesets: -rem sync_standards.bat -langs tsl,cpp -rem Notes: -rem - When syncing multiple rulesets, .gitattributes is synced only once (first ruleset). - -if /I "%~1"=="-h" goto show_help -if /I "%~1"=="-help" goto show_help - -set "SCRIPT_DIR=%~dp0" -set "ROOT=%SYNC_ROOT%" -if "%ROOT%"=="" for /f "delims=" %%R in ('git -C "%SCRIPT_DIR%" rev-parse --show-toplevel 2^>nul') do set "ROOT=%%R" -if "%ROOT%"=="" set "ROOT=%cd%" -for %%I in ("%ROOT%") do set "ROOT=%%~fI" - -for %%I in ("%SCRIPT_DIR%..") do set "SRC=%%~fI" - -set "AGENTS_SRC_ROOT=%SRC%\rulesets" -set "GITATTR_SRC=%SRC%\.gitattributes" - -if not exist "%AGENTS_SRC_ROOT%" ( - echo ERROR: Standards snapshot not found at %AGENTS_SRC_ROOT% >&2 - echo Run: git subtree add --prefix docs/standards/playbook ^ ^ --squash >&2 - exit /b 1 -) -set "AGENTS_NS=%AGENTS_NS%" -set "GITATTR_DST=%ROOT%\.gitattributes" -set "SYNC_GITATTR_MODE=%SYNC_GITATTR_MODE%" -if "%SYNC_GITATTR_MODE%"=="" set "SYNC_GITATTR_MODE=append" - -set "LANG_LIST=" -:parse_args -if "%~1"=="" goto args_done -if /I "%~1"=="-h" goto show_help -if /I "%~1"=="-help" goto show_help -if /I "%~1"=="-langs" ( - if "%~2"=="" goto missing_langs - set "LANG_LIST=%~2" - shift /1 - shift /1 - goto parse_args -) -echo ERROR: Unknown option: %~1 -exit /b 1 - -:missing_langs -echo ERROR: -langs requires a value. -exit /b 1 - -:args_done -if not "%LANG_LIST%"=="" set "LANG_LIST=%LANG_LIST:,= %" - -rem Multi rulesets: only on outer invocation. -if "%SYNC_STANDARDS_INNER%"=="" ( - if "%LANG_LIST%"=="" ( - if "%AGENTS_NS%"=="" ( - echo ERROR: -langs is required. - exit /b 1 - ) - ) - if not "%LANG_LIST%"=="" ( - set "FIRST=1" - set "SYNC_FIRST=%SYNC_GITATTR_MODE%" - for %%L in (!LANG_LIST!) do ( - if "!FIRST!"=="1" ( - set "FIRST=0" - set "SYNC_STANDARDS_INNER=1" - set "AGENTS_NS=%%~L" - set "SYNC_GITATTR_MODE=!SYNC_FIRST!" - call "%~f0" -langs %%~L - ) else ( - set "SYNC_STANDARDS_INNER=1" - set "AGENTS_NS=%%~L" - set "SYNC_GITATTR_MODE=skip" - call "%~f0" -langs %%~L - ) - ) - exit /b 0 - ) -) - -if "%AGENTS_NS%"=="" set "AGENTS_NS=tsl" -echo %AGENTS_NS%| findstr /r "[\\/]" >nul && ( - echo ERROR: invalid AGENTS_NS=%AGENTS_NS% - exit /b 1 -) -echo %AGENTS_NS%| findstr /c:".." >nul && ( - echo ERROR: invalid AGENTS_NS=%AGENTS_NS% - exit /b 1 -) -set "AGENTS_ROOT=%ROOT%\.agents" -set "AGENTS_DST=%AGENTS_ROOT%\%AGENTS_NS%" - -set "AGENTS_SRC=%AGENTS_SRC_ROOT%\%AGENTS_NS%" -if not exist "%AGENTS_SRC%" ( - rem Backward-compatible fallback: older snapshots used ^\.agents\* directly. - if exist "%AGENTS_SRC_ROOT%\index.md" if exist "%AGENTS_SRC_ROOT%\auth.md" ( - set "AGENTS_SRC=%AGENTS_SRC_ROOT%" - ) else ( - echo ERROR: Standards snapshot not found at "%AGENTS_SRC%". - echo Hint: set AGENTS_NS to one of the subdirs under "%AGENTS_SRC_ROOT%" ^(e.g. tsl/cpp^). - exit /b 1 - ) -) -if not exist "%AGENTS_SRC%" ( - echo ERROR: Standards snapshot not found at "%AGENTS_SRC%". - echo Run: git subtree add --prefix docs/standards/playbook ^ ^ --squash - exit /b 1 -) - -if /I "%SRC%"=="%ROOT%" ( - echo Skip: snapshot root equals project root. - goto AfterGitAttr -) - -if not exist "%AGENTS_ROOT%" mkdir "%AGENTS_ROOT%" - -if exist "%AGENTS_DST%" ( - set "RAND=%RANDOM%" - pushd "%AGENTS_ROOT%" - ren "%AGENTS_NS%" "%AGENTS_NS%.bak.!RAND!" - popd - echo Backed up existing %AGENTS_NS% agents -> %AGENTS_NS%.bak.!RAND! -) - -xcopy "%AGENTS_SRC%\\*" "%AGENTS_DST%\\" /e /i /y >nul -if errorlevel 1 ( - echo ERROR: failed to copy .agents - exit /b 1 -) - -echo Synced .agents\%AGENTS_NS% from standards. - -set "REL_SNAPSHOT=%SRC:%ROOT%\=%" -if /I not "%REL_SNAPSHOT%"=="%SRC%" ( - set "DOCS_PREFIX=%REL_SNAPSHOT%\docs" - set "DOCS_PREFIX=%DOCS_PREFIX:\=/%" - for %%F in ("%AGENTS_DST%\*.md") do ( - powershell -NoProfile -Command "$p='%%~fF'; $c=Get-Content -Raw $p; $c=$c.Replace('`docs/tsl/','`%DOCS_PREFIX%/tsl/'); $c=$c.Replace('`docs/cpp/','`%DOCS_PREFIX%/cpp/'); $c=$c.Replace('`docs/python/','`%DOCS_PREFIX%/python/'); $c=$c.Replace('`docs/markdown/','`%DOCS_PREFIX%/markdown/'); $c=$c.Replace('`docs/common/','`%DOCS_PREFIX%/common/'); Set-Content -Path $p -Value $c -Encoding UTF8" - ) -) - -if not exist "%AGENTS_ROOT%\index.md" ( - > "%AGENTS_ROOT%\index.md" echo # .agents(多语言) - >> "%AGENTS_ROOT%\index.md" echo. - >> "%AGENTS_ROOT%\index.md" echo 本目录用于存放仓库级/语言级的代理规则集。 - >> "%AGENTS_ROOT%\index.md" echo. - >> "%AGENTS_ROOT%\index.md" echo 建议约定: - >> "%AGENTS_ROOT%\index.md" echo. - >> "%AGENTS_ROOT%\index.md" echo - `.agents/tsl/`:TSL 相关规则集(由 `sync_standards.*` 同步;适用于 `.tsl`/`.tsf`) - >> "%AGENTS_ROOT%\index.md" echo - `.agents/cpp/`:C++ 相关规则集(由 `sync_standards.*` 同步;适用于 C++23/Modules) - >> "%AGENTS_ROOT%\index.md" echo - `.agents/python/`:Python 相关规则集(由 `sync_standards.*` 同步) - >> "%AGENTS_ROOT%\index.md" echo - `.agents/markdown/`:Markdown 相关规则集(仅代码格式化) - >> "%AGENTS_ROOT%\index.md" echo. - >> "%AGENTS_ROOT%\index.md" echo 规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。 - >> "%AGENTS_ROOT%\index.md" echo. - >> "%AGENTS_ROOT%\index.md" echo 入口建议从: - >> "%AGENTS_ROOT%\index.md" echo. - >> "%AGENTS_ROOT%\index.md" echo - `.agents/tsl/index.md`(TSL 规则集入口) - >> "%AGENTS_ROOT%\index.md" echo - `.agents/cpp/index.md`(C++ 规则集入口) - >> "%AGENTS_ROOT%\index.md" echo - `.agents/markdown/index.md`(Markdown 规则集入口) - >> "%AGENTS_ROOT%\index.md" echo - `docs/standards/playbook/docs/`(人类开发规范快照:`tsl/`、`cpp/`、`python/`、`common/`) - echo Created .agents\index.md -) - -set "AGENTS_LANGS=" -for /d %%D in ("%AGENTS_ROOT%\*") do ( - set "NAME=%%~nxD" - if /I not "!NAME:~0,1!"=="." ( - echo "!NAME!" | findstr /I /C:".bak." >nul - if errorlevel 1 ( - if exist "%%D\index.md" ( - if defined AGENTS_LANGS ( - set "AGENTS_LANGS=!AGENTS_LANGS!、`.agents/!NAME!/index.md`" - ) else ( - set "AGENTS_LANGS=`.agents/!NAME!/index.md`" - ) - ) - ) - ) -) -if not defined AGENTS_LANGS set "AGENTS_LANGS=`.agents/%AGENTS_NS%/index.md`" - -set "AGENTS_BLOCK_START=" -set "AGENTS_BLOCK_END=" -set "AGENTS_BLOCK_FILE=%ROOT%\.agents_block.!RANDOM!.tmp" -> "%AGENTS_BLOCK_FILE%" echo %AGENTS_BLOCK_START% ->> "%AGENTS_BLOCK_FILE%" echo. ->> "%AGENTS_BLOCK_FILE%" echo 请以 `.agents/` 下的规则为准: ->> "%AGENTS_BLOCK_FILE%" echo. ->> "%AGENTS_BLOCK_FILE%" echo - 入口:`.agents/index.md` ->> "%AGENTS_BLOCK_FILE%" echo - 语言规则:%AGENTS_LANGS% ->> "%AGENTS_BLOCK_FILE%" echo %AGENTS_BLOCK_END% - -set "AGENTS_MD=%ROOT%\AGENTS.md" -if not exist "%AGENTS_MD%" ( - > "%AGENTS_MD%" echo # Agent Instructions - >> "%AGENTS_MD%" echo. - type "%AGENTS_BLOCK_FILE%" >> "%AGENTS_MD%" - echo Created AGENTS.md -) else ( - findstr /C:"%AGENTS_BLOCK_START%" "%AGENTS_MD%" >nul - if not errorlevel 1 ( - powershell -NoProfile -Command "$file='%AGENTS_MD%'; $block=Get-Content -Raw '%AGENTS_BLOCK_FILE%'; $start='%AGENTS_BLOCK_START%'; $end='%AGENTS_BLOCK_END%'; $pattern=[regex]::Escape($start)+'.*?'+[regex]::Escape($end); $regex=New-Object System.Text.RegularExpressions.Regex($pattern,[System.Text.RegularExpressions.RegexOptions]::Singleline); $content=Get-Content -Raw $file; $new=$regex.Replace($content,$block,1); Set-Content -Path $file -Value $new -Encoding UTF8" - echo Updated AGENTS.md (playbook block). - ) else ( - findstr /C:".agents/index.md" "%AGENTS_MD%" >nul - if not errorlevel 1 ( - echo Skip: AGENTS.md already references .agents/index.md - ) else ( - >> "%AGENTS_MD%" echo. - type "%AGENTS_BLOCK_FILE%" >> "%AGENTS_MD%" - >> "%AGENTS_MD%" echo. - echo Appended playbook block to AGENTS.md - ) - ) -) -del "%AGENTS_BLOCK_FILE%" >nul 2>&1 - -:SyncGitAttr -if exist "%GITATTR_SRC%" ( - if /I "%SYNC_GITATTR_MODE%"=="skip" ( - echo Skip: .gitattributes sync ^(SYNC_GITATTR_MODE=skip^). - goto AfterGitAttr - ) - - if /I "%SYNC_GITATTR_MODE%"=="overwrite" ( - for %%I in ("%GITATTR_SRC%") do set "GITATTR_SRC_F=%%~fI" - for %%I in ("%GITATTR_DST%") do set "GITATTR_DST_F=%%~fI" - if /I "!GITATTR_SRC_F!"=="!GITATTR_DST_F!" ( - echo Skip: .gitattributes source equals destination. - goto AfterGitAttr - ) - - if exist "%GITATTR_DST%" ( - set "RAND=%RANDOM%" - set "BAK_NAME=.gitattributes.bak.!RAND!" - ren "%GITATTR_DST%" "!BAK_NAME!" - echo Backed up existing .gitattributes -> !BAK_NAME! - ) - copy /y "%GITATTR_SRC%" "%GITATTR_DST%" >nul - echo Synced .gitattributes from standards ^(overwrite^). - goto AfterGitAttr - ) - - if /I "%SYNC_GITATTR_MODE%"=="append" ( - for %%I in ("%GITATTR_SRC%") do set "GITATTR_SRC_F=%%~fI" - for %%I in ("%GITATTR_DST%") do set "GITATTR_DST_F=%%~fI" - if /I "!GITATTR_SRC_F!"=="!GITATTR_DST_F!" ( - echo Skip: .gitattributes source equals destination. - goto AfterGitAttr - ) - - set "TMP_DST=%TEMP%\\gitattributes.dst.%RANDOM%.tmp" - set "TMP_MISS=%TEMP%\\gitattributes.missing.%RANDOM%.tmp" - if exist "!TMP_DST!" del /q "!TMP_DST!" >nul 2>nul - if exist "!TMP_MISS!" del /q "!TMP_MISS!" >nul 2>nul - type nul > "!TMP_DST!" - type nul > "!TMP_MISS!" - - if exist "%GITATTR_DST%" ( - for /f "usebackq delims=" %%L in ("%GITATTR_DST%") do ( - set "LINE=%%L" - for /f "tokens=* delims= " %%A in ("!LINE!") do set "LINE=%%A" - if not "!LINE!"=="" ( - if /I not "!LINE:~0,1!"=="#" ( - echo(!LINE!>>"!TMP_DST!" - ) - ) - ) - ) - - for /f "usebackq delims=" %%L in ("%GITATTR_SRC%") do ( - set "LINE=%%L" - for /f "tokens=* delims= " %%A in ("!LINE!") do set "LINE=%%A" - if not "!LINE!"=="" ( - if /I not "!LINE:~0,1!"=="#" ( - findstr /x /l /c:"!LINE!" "!TMP_DST!" >nul || ( - findstr /x /l /c:"!LINE!" "!TMP_MISS!" >nul || echo(!LINE!>>"!TMP_MISS!" - ) - ) - ) - ) - - set "MISS_SIZE=0" - if exist "!TMP_MISS!" for %%S in ("!TMP_MISS!") do set "MISS_SIZE=%%~zS" - if "!MISS_SIZE!"=="0" ( - del /q "!TMP_DST!" "!TMP_MISS!" >nul 2>nul - echo No missing .gitattributes rules to append. - goto AfterGitAttr - ) - - if exist "%GITATTR_DST%" ( - set "RAND=%RANDOM%" - set "BAK_NAME=.gitattributes.bak.!RAND!" - ren "%GITATTR_DST%" "!BAK_NAME!" - echo Backed up existing .gitattributes -> !BAK_NAME! - set "DST_IN=%ROOT%\\!BAK_NAME!" - ) else ( - set "DST_IN=" - ) - - set "TMP_OUT=%TEMP%\\gitattributes.out.%RANDOM%.tmp" - if exist "!TMP_OUT!" del /q "!TMP_OUT!" >nul 2>nul - - if not "!DST_IN!"=="" ( - type "!DST_IN!" > "!TMP_OUT!" - for %%S in ("!DST_IN!") do set "DST_SIZE=%%~zS" - if not "!DST_SIZE!"=="0" echo.>>"!TMP_OUT!" - ) - - set "SOURCE_NOTE=%GITATTR_SRC%" - >>"!TMP_OUT!" echo # Added from playbook .gitattributes ^(source: !SOURCE_NOTE!^) - type "!TMP_MISS!" >> "!TMP_OUT!" - - copy /y "!TMP_OUT!" "%GITATTR_DST%" >nul - del /q "!TMP_DST!" "!TMP_MISS!" "!TMP_OUT!" >nul 2>nul - echo Appended missing .gitattributes rules from standards. - goto AfterGitAttr - ) - - if /I not "%SYNC_GITATTR_MODE%"=="block" ( - echo ERROR: invalid SYNC_GITATTR_MODE=%SYNC_GITATTR_MODE% ^(use block^|overwrite^|append^|skip^) - exit /b 1 - ) - - rem block mode: maintain a managed block inside the destination file - set "BEGIN=# BEGIN playbook .gitattributes" - set "END=# END playbook .gitattributes" - set "BEGIN_OLD=# BEGIN tsl-playbook .gitattributes" - set "END_OLD=# END tsl-playbook .gitattributes" - set "TMP_FILE=%TEMP%\\gitattributes.%RANDOM%.tmp" - - if exist "%GITATTR_DST%" ( - set "RAND=%RANDOM%" - set "BAK_NAME=.gitattributes.bak.!RAND!" - ren "%GITATTR_DST%" "!BAK_NAME!" - echo Backed up existing .gitattributes -> !BAK_NAME! - set "DST_IN=%ROOT%\\!BAK_NAME!" - ) else ( - set "DST_IN=" - ) - - set "IN_BLOCK=0" - set "DONE=0" - - if not "%DST_IN%"=="" ( - > "!TMP_FILE!" ( - for /f "usebackq delims=" %%L in ("!DST_IN!") do ( - set "LINE=%%L" - if "!LINE!"=="%BEGIN%" ( - if "!DONE!"=="0" ( - echo %BEGIN% - type "%GITATTR_SRC%" - echo %END% - set "DONE=1" - ) - set "IN_BLOCK=1" - ) else if "!LINE!"=="%BEGIN_OLD%" ( - if "!DONE!"=="0" ( - echo %BEGIN% - type "%GITATTR_SRC%" - echo %END% - set "DONE=1" - ) - set "IN_BLOCK=1" - ) else if "!LINE!"=="%END%" ( - set "IN_BLOCK=0" - ) else if "!LINE!"=="%END_OLD%" ( - set "IN_BLOCK=0" - ) else ( - if "!IN_BLOCK!"=="0" echo(!LINE! - ) - ) - if "!DONE!"=="0" ( - echo. - echo %BEGIN% - type "%GITATTR_SRC%" - echo %END% - ) - ) - ) else ( - > "!TMP_FILE!" ( - echo %BEGIN% - type "%GITATTR_SRC%" - echo %END% - ) - ) - - copy /y "!TMP_FILE!" "%GITATTR_DST%" >nul - del /q "!TMP_FILE!" >nul 2>nul - echo Updated .gitattributes from standards ^(managed block^). -) - -:AfterGitAttr -echo Done. -endlocal -exit /b 0 - -:show_help -echo Usage: -echo sync_standards.bat -echo sync_standards.bat -langs tsl,cpp -echo. -echo Options: -echo -langs Comma/space-separated list of languages ^(required^). -echo -h, -help Show this help. -echo. -echo Env: -echo SYNC_ROOT Target project root ^(default: git root^). -echo AGENTS_NS Single ruleset name ^(default: tsl^). -echo SYNC_GITATTR_MODE append^|overwrite^|block^|skip ^(default: append^). -exit /b 0 diff --git a/scripts/sync_standards.ps1 b/scripts/sync_standards.ps1 deleted file mode 100644 index f682d99..0000000 --- a/scripts/sync_standards.ps1 +++ /dev/null @@ -1,338 +0,0 @@ -# Sync standards snapshot to project root. -# - Copies /rulesets/ -> /.agents/ -# - Updates /.gitattributes (append missing rules by default) -# Existing targets are backed up before overwrite. -[CmdletBinding()] -param( - [Parameter(Mandatory = $false)] - [Alias('h', '?')] - [switch]$Help, - - # Sync multiple rulesets in one run: - # -Langs tsl,cpp - # -Langs @("tsl","cpp") - [Parameter(Mandatory = $false)] - [string[]]$Langs -) - -$ErrorActionPreference = "Stop" - -if ($Help) { - Write-Host "Usage:" - Write-Host " powershell -File scripts/sync_standards.ps1 -Langs tsl,cpp" - Write-Host "" - Write-Host "Options:" - Write-Host " -Langs Comma/space-separated list or array (required)." - Write-Host " -Help Show this help." - Write-Host "" - Write-Host "Env:" - Write-Host " SYNC_ROOT Target project root (default: git root)." - Write-Host " AGENTS_NS Single ruleset name (default: tsl)." - Write-Host " SYNC_GITATTR_MODE append|overwrite|block|skip (default: append)." - exit 0 -} -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$Src = (Resolve-Path (Join-Path $ScriptDir "..")).Path - -$Root = $env:SYNC_ROOT -if (-not $Root) { - $Root = (git -C $ScriptDir rev-parse --show-toplevel 2>$null) - if (-not $Root) { $Root = (Get-Location).Path } -} -$Root = (Resolve-Path $Root).Path - -$AgentsSrcRoot = Join-Path $Src "rulesets" -$GitAttrSrc = Join-Path $Src ".gitattributes" - -if (-not (Test-Path $AgentsSrcRoot)) { - throw "Standards snapshot not found at $AgentsSrcRoot. Run: git subtree add --prefix docs/standards/playbook --squash" -} - -$timestamp = Get-Date -Format "yyyyMMddHHmmss" - -# Require explicit -Langs on outer invocation unless AGENTS_NS is provided. -if (-not $env:SYNC_STANDARDS_INNER -and (-not $Langs -or $Langs.Count -eq 0) -and -not $env:AGENTS_NS) { - throw "Missing -Langs. Use -Help for usage." -} - -# Multi rulesets: only on the outer invocation. -if (-not $env:SYNC_STANDARDS_INNER -and $Langs -and $Langs.Count -gt 0) { - $oldInner = $env:SYNC_STANDARDS_INNER - $oldAgentsNs = $env:AGENTS_NS - $oldMode = $env:SYNC_GITATTR_MODE - - $syncModeFirst = $env:SYNC_GITATTR_MODE - if (-not $syncModeFirst) { $syncModeFirst = "append" } - - $first = $true - foreach ($ns in $Langs) { - if (-not $ns) { continue } - - $env:SYNC_STANDARDS_INNER = "1" - $env:AGENTS_NS = $ns - if ($first) { - $first = $false - $env:SYNC_GITATTR_MODE = $syncModeFirst - } else { - $env:SYNC_GITATTR_MODE = "skip" - } - - & $MyInvocation.MyCommand.Path - } - - $env:SYNC_STANDARDS_INNER = $oldInner - $env:AGENTS_NS = $oldAgentsNs - $env:SYNC_GITATTR_MODE = $oldMode - exit 0 -} - -$AgentsNs = $env:AGENTS_NS -if (-not $AgentsNs) { $AgentsNs = "tsl" } -if ($AgentsNs -match '[\\/]' -or $AgentsNs -match '\.\.') { - throw "Invalid AGENTS_NS=$AgentsNs" -} -$AgentsSrc = Join-Path $AgentsSrcRoot $AgentsNs -if (-not (Test-Path $AgentsSrc)) { - # Backward-compatible fallback: older snapshots used /.agents/* directly. - if ((Test-Path (Join-Path $AgentsSrcRoot "index.md")) -and (Test-Path (Join-Path $AgentsSrcRoot "auth.md"))) { - $AgentsSrc = $AgentsSrcRoot - } else { - throw "Agents ruleset not found: $AgentsSrc (set AGENTS_NS to one of the subdirs under $AgentsSrcRoot, e.g. tsl/cpp)." - } -} -$AgentsRoot = Join-Path $Root ".agents" -$AgentsDst = Join-Path $AgentsRoot $AgentsNs - -if ($Src -ieq $Root) { - Write-Host "Skip: snapshot root equals project root." - Write-Host "Done." - exit 0 -} - -New-Item -ItemType Directory -Path $AgentsRoot -Force | Out-Null - -if (Test-Path $AgentsDst) { - $bak = (Join-Path $AgentsRoot "$AgentsNs.bak.$timestamp") - Move-Item $AgentsDst $bak - Write-Host "Backed up existing $AgentsNs agents -> $(Split-Path -Leaf $bak)" -} - -New-Item -ItemType Directory -Path $AgentsDst -Force | Out-Null -Copy-Item (Join-Path $AgentsSrc "*") $AgentsDst -Recurse -Force -Write-Host "Synced .agents/$AgentsNs from standards." - -# Rewrite docs/* references to the snapshot docs path. -$relSnapshot = $null -$rootPrefix = $Root.TrimEnd('\', '/') -$rootPrefixWithSep = $rootPrefix + [System.IO.Path]::DirectorySeparatorChar -if ($Src.ToLowerInvariant().StartsWith($rootPrefixWithSep.ToLowerInvariant())) { - $relSnapshot = $Src.Substring($rootPrefixWithSep.Length) -} -if ($relSnapshot) { - $docsPrefix = (Join-Path $relSnapshot "docs") -replace "\\", "/" - Get-ChildItem -Path $AgentsDst -Filter *.md -File | ForEach-Object { - $content = Get-Content -Raw -Path $_.FullName - $content = $content.Replace("``docs/tsl/", "``$docsPrefix/tsl/") - $content = $content.Replace("``docs/cpp/", "``$docsPrefix/cpp/") - $content = $content.Replace("``docs/python/", "``$docsPrefix/python/") - $content = $content.Replace("``docs/markdown/", "``$docsPrefix/markdown/") - $content = $content.Replace("``docs/common/", "``$docsPrefix/common/") - Set-Content -Path $_.FullName -Value $content -Encoding UTF8 - } -} - -$AgentsIndex = Join-Path $AgentsRoot "index.md" -if (-not (Test-Path $AgentsIndex)) { - $agentsIndexContent = @' -# .agents(多语言) - -本目录用于存放仓库级/语言级的代理规则集。 - -建议约定: - -- `.agents/tsl/`:TSL 相关规则集(由 `sync_standards.*` 同步;适用于 `.tsl`/`.tsf`) -- `.agents/cpp/`:C++ 相关规则集(由 `sync_standards.*` 同步;适用于 C++23/Modules) -- `.agents/python/`:Python 相关规则集(由 `sync_standards.*` 同步) -- `.agents/markdown/`:Markdown 相关规则集(仅代码格式化) - -规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。 - -入口建议从: - -- `.agents/tsl/index.md`(TSL 规则集入口) -- `.agents/cpp/index.md`(C++ 规则集入口) -- `.agents/markdown/index.md`(Markdown 规则集入口) -- `docs/standards/playbook/docs/`(人类开发规范快照:`tsl/`、`cpp/`、`python/`、`common/`) -'@ - Set-Content -Path $AgentsIndex -Encoding UTF8 -Value $agentsIndexContent - Write-Host "Created .agents/index.md" -} - -$AgentsMd = Join-Path $Root "AGENTS.md" -$AgentsBlockStart = "" -$AgentsBlockEnd = "" -$agentsLangs = @() -if (Test-Path $AgentsRoot) { - Get-ChildItem -Path $AgentsRoot -Directory | ForEach-Object { - $name = $_.Name - if ($name -and -not $name.StartsWith(".") -and -not ($name -match "\.bak\.") -and (Test-Path (Join-Path $_.FullName "index.md"))) { - $agentsLangs += $name - } - } -} -if ($agentsLangs.Count -eq 0) { $agentsLangs = @($AgentsNs) } -$langsLine = ($agentsLangs | ForEach-Object { "`.agents/$_/index.md`" }) -join "、" -$agentsBlock = @" - - -请以 `.agents/` 下的规则为准: - -- 入口:`.agents/index.md` -- 语言规则:$langsLine - -"@ - -if (-not (Test-Path $AgentsMd)) { - $agentsMdContent = @" -# Agent Instructions - -$agentsBlock -"@ - Set-Content -Path $AgentsMd -Encoding UTF8 -Value $agentsMdContent - Write-Host "Created AGENTS.md" -} else { - $content = Get-Content -Raw -Path $AgentsMd - if ($content.Contains($AgentsBlockStart)) { - $pattern = [regex]::Escape($AgentsBlockStart) + ".*?" + [regex]::Escape($AgentsBlockEnd) - $regex = New-Object System.Text.RegularExpressions.Regex($pattern, [System.Text.RegularExpressions.RegexOptions]::Singleline) - $newContent = $regex.Replace($content, $agentsBlock, 1) - Set-Content -Path $AgentsMd -Value $newContent -Encoding UTF8 - Write-Host "Updated AGENTS.md (playbook block)." - } elseif ($content.Contains(".agents/index.md")) { - Write-Host "Skip: AGENTS.md already references .agents/index.md" - } else { - Add-Content -Path $AgentsMd -Value "" -Encoding UTF8 - Add-Content -Path $AgentsMd -Value $agentsBlock -Encoding UTF8 - Add-Content -Path $AgentsMd -Value "" -Encoding UTF8 - Write-Host "Appended playbook block to AGENTS.md" - } -} - -$GitAttrDst = Join-Path $Root ".gitattributes" -if (Test-Path $GitAttrSrc) { - $mode = $env:SYNC_GITATTR_MODE - if (-not $mode) { $mode = "append" } - switch ($mode.ToLowerInvariant()) { - "skip" { - Write-Host "Skip: .gitattributes sync (SYNC_GITATTR_MODE=skip)." - break - } - "overwrite" { - if ($GitAttrSrc -ieq $GitAttrDst) { - Write-Host "Skip: .gitattributes source equals destination." - break - } - if (Test-Path $GitAttrDst) { - $bak = "$GitAttrDst.bak.$timestamp" - Move-Item $GitAttrDst $bak - Write-Host "Backed up existing .gitattributes -> $bak" - } - Copy-Item $GitAttrSrc $GitAttrDst -Force - Write-Host "Synced .gitattributes from standards (overwrite)." - break - } - "append" { - if ($GitAttrSrc -ieq $GitAttrDst) { - Write-Host "Skip: .gitattributes source equals destination." - break - } - - $dstLines = @{} - if (Test-Path $GitAttrDst) { - Get-Content $GitAttrDst | ForEach-Object { - $line = $_.Trim() - if ($line -eq "" -or $line.StartsWith("#")) { return } - $dstLines[$line] = $true - } - } - - $missing = New-Object System.Collections.Generic.List[string] - Get-Content $GitAttrSrc | ForEach-Object { - $line = $_.Trim() - if ($line -eq "" -or $line.StartsWith("#")) { return } - if (-not $dstLines.ContainsKey($line)) { - $dstLines[$line] = $true - $missing.Add($line) - } - } - - if ($missing.Count -eq 0) { - Write-Host "No missing .gitattributes rules to append." - break - } - - $bak = $null - if (Test-Path $GitAttrDst) { - $bak = "$GitAttrDst.bak.$timestamp" - Move-Item $GitAttrDst $bak -Force - Write-Host "Backed up existing .gitattributes -> $bak" - } - - $sourceNote = $GitAttrSrc - $rootPrefix = "$Root\" - if ($sourceNote.StartsWith($rootPrefix)) { - $sourceNote = $sourceNote.Substring($rootPrefix.Length) - } - $header = "# Added from playbook .gitattributes (source: $sourceNote)" - - $content = @() - if ($bak -and (Test-Path $bak)) { - $existing = Get-Content $bak - if ($existing.Count -gt 0) { - $content += $existing - $content += "" - } - } - $content += $header - $content += $missing - $content | Set-Content -Path $GitAttrDst -Encoding UTF8 - - Write-Host "Appended missing .gitattributes rules from standards." - break - } - "block" { - $begin = "# BEGIN playbook .gitattributes" - $end = "# END playbook .gitattributes" - $beginOld = "# BEGIN tsl-playbook .gitattributes" - $endOld = "# END tsl-playbook .gitattributes" - $src = Get-Content -Path $GitAttrSrc -Raw - $block = $begin + "`r`n" + $src.TrimEnd() + "`r`n" + $end + "`r`n" - - $dst = "" - if (Test-Path $GitAttrDst) { - $bak = "$GitAttrDst.bak.$timestamp" - Move-Item $GitAttrDst $bak - Write-Host "Backed up existing .gitattributes -> $bak" - $dst = Get-Content -Path $bak -Raw - } - - $pattern = "(?ms)^(" + [regex]::Escape($begin) + "|" + [regex]::Escape($beginOld) + ")\\R.*?^(" + [regex]::Escape($end) + "|" + [regex]::Escape($endOld) + ")\\R?" - if ($dst -and ($dst -match $pattern)) { - $new = [regex]::Replace($dst, $pattern, $block) - } elseif ($dst) { - $new = $dst.TrimEnd() + "`r`n`r`n" + $block - } else { - $new = $block - } - - $new | Set-Content -Path $GitAttrDst -Encoding UTF8 - Write-Host "Updated .gitattributes from standards (managed block)." - break - } - default { - throw "Invalid SYNC_GITATTR_MODE=$mode (use block|overwrite|append|skip)" - } - } -} - -Write-Host "Done." diff --git a/scripts/sync_standards.sh b/scripts/sync_standards.sh deleted file mode 100644 index 7f90667..0000000 --- a/scripts/sync_standards.sh +++ /dev/null @@ -1,428 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Sync standards snapshot to project root. -# - Copies /rulesets/ -> /.agents/ -# - Updates /.gitattributes (append missing rules by default) -# Existing targets are backed up before overwrite. -# -# Multi rulesets: -# sh .../sync_standards.sh -langs tsl,cpp -# Notes: -# - When syncing multiple rulesets, .gitattributes is synced only once (first ruleset). - -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" -SRC="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)" -if [ -n "${SYNC_ROOT:-}" ]; then - ROOT="$SYNC_ROOT" -else - ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)" -fi -ROOT="$(CDPATH= cd -- "$ROOT" && pwd -P)" - -usage() { - cat <<'EOF' >&2 -Usage: - sh scripts/sync_standards.sh -langs tsl - sh scripts/sync_standards.sh -langs tsl,cpp - -Options: - -langs L1,L2 Comma/space-separated list of languages (required). - -h, -help Show this help. - -Env: - SYNC_ROOT Target project root (default: git root). - AGENTS_NS Single ruleset name (default: tsl). - SYNC_GITATTR_MODE append|overwrite|block|skip (default: append). -EOF -} - -if [ "${1:-}" = "-h" ] || [ "${1:-}" = "-help" ]; then - usage - exit 0 -fi - -langs="" -while [ $# -gt 0 ]; do - case "$1" in - -langs) - if [ $# -lt 2 ] || [ -z "${2:-}" ]; then - echo "ERROR: -langs requires a value." >&2 - usage - exit 1 - fi - langs="$2" - shift 2 - ;; - -*) - echo "ERROR: Unknown option: $1" >&2 - usage - exit 1 - ;; - *) - echo "ERROR: positional args are not supported; use -langs." >&2 - usage - exit 1 - ;; - esac -done - -AGENTS_SRC_ROOT="$SRC/rulesets" -GITATTR_SRC="$SRC/.gitattributes" - -if [ ! -d "$AGENTS_SRC_ROOT" ]; then - echo "ERROR: Standards snapshot not found at $AGENTS_SRC_ROOT" >&2 - echo "Run: git subtree add --prefix docs/standards/playbook --squash" >&2 - exit 1 -fi - -timestamp="$(date +%Y%m%d%H%M%S 2>/dev/null || echo bak)" - -if [ "$SRC" = "$ROOT" ]; then - echo "Skip: snapshot root equals project root." - echo "Done." - exit 0 -fi - -# Parse multi rulesets only on the outer invocation. -if [ "${SYNC_STANDARDS_INNER:-}" != "1" ]; then - if [ -z "${langs:-}" ] && [ -z "${AGENTS_NS:-}" ]; then - echo "ERROR: -langs is required." >&2 - usage - exit 1 - fi - if [ -n "${langs:-}" ]; then - sync_mode_first="${SYNC_GITATTR_MODE:-append}" - - first=1 - old_ifs="${IFS}" - IFS=', ' - set -- $langs - IFS="${old_ifs}" - - for ns in "$@"; do - [ -n "$ns" ] || continue - if [ "$first" -eq 1 ]; then - first=0 - SYNC_STANDARDS_INNER=1 AGENTS_NS="$ns" SYNC_GITATTR_MODE="$sync_mode_first" sh "$0" -langs "$ns" - else - SYNC_STANDARDS_INNER=1 AGENTS_NS="$ns" SYNC_GITATTR_MODE=skip sh "$0" -langs "$ns" - fi - done - exit 0 - fi -fi - -: "${AGENTS_NS:=tsl}" -case "$AGENTS_NS" in - ""|*/*|*\\*|*..*) - echo "ERROR: invalid AGENTS_NS=$AGENTS_NS" >&2 - exit 1 - ;; -esac - -AGENTS_SRC="$AGENTS_SRC_ROOT/$AGENTS_NS" -if [ ! -d "$AGENTS_SRC" ]; then - # Backward-compatible fallback: older snapshots used /.agents/* directly. - if [ -f "$AGENTS_SRC_ROOT/index.md" ] && [ -f "$AGENTS_SRC_ROOT/auth.md" ]; then - AGENTS_SRC="$AGENTS_SRC_ROOT" - else - echo "ERROR: agents ruleset not found: $AGENTS_SRC" >&2 - echo "Hint: set AGENTS_NS to one of the subdirs under $AGENTS_SRC_ROOT (e.g. tsl/cpp)." >&2 - exit 1 - fi -fi - -AGENTS_ROOT="$ROOT/.agents" -AGENTS_DST="$AGENTS_ROOT/$AGENTS_NS" -mkdir -p "$AGENTS_ROOT" - -if [ -e "$AGENTS_DST" ]; then - mv "$AGENTS_DST" "$AGENTS_ROOT/$AGENTS_NS.bak.$timestamp" - echo "Backed up existing $AGENTS_NS agents -> $AGENTS_NS.bak.$timestamp" -fi - -cp -R "$AGENTS_SRC" "$AGENTS_DST" -echo "Synced .agents/$AGENTS_NS from standards." - -# Rewrite docs/* references to the snapshot docs path. -REL_SNAPSHOT="" -case "$SRC" in - "$ROOT"/*) REL_SNAPSHOT="${SRC#$ROOT/}" ;; -esac -if [ -n "$REL_SNAPSHOT" ]; then - DOCS_PREFIX="$REL_SNAPSHOT/docs" - for md in "$AGENTS_DST"/*.md; do - [ -f "$md" ] || continue - tmp="$(mktemp 2>/dev/null || echo "$AGENTS_DST/.rewrite.$(basename "$md").$timestamp")" - sed \ - -e 's#`docs/tsl/#`'"$DOCS_PREFIX"'/tsl/#g' \ - -e 's#`docs/cpp/#`'"$DOCS_PREFIX"'/cpp/#g' \ - -e 's#`docs/python/#`'"$DOCS_PREFIX"'/python/#g' \ - -e 's#`docs/markdown/#`'"$DOCS_PREFIX"'/markdown/#g' \ - -e 's#`docs/common/#`'"$DOCS_PREFIX"'/common/#g' \ - "$md" >"$tmp" - mv "$tmp" "$md" - done -fi - -AGENTS_INDEX="$AGENTS_ROOT/index.md" -if [ ! -f "$AGENTS_INDEX" ]; then - cat >"$AGENTS_INDEX" <<'EOF' -# .agents(多语言) - -本目录用于存放仓库级/语言级的代理规则集。 - -建议约定: - -- `.agents/tsl/`:TSL 相关规则集(由 `sync_standards.*` 同步;适用于 `.tsl`/`.tsf`) -- `.agents/cpp/`:C++ 相关规则集(由 `sync_standards.*` 同步;适用于 C++23/Modules) -- `.agents/python/`:Python 相关规则集(由 `sync_standards.*` 同步) -- `.agents/markdown/`:Markdown 相关规则集(仅代码格式化) - -规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。 - -入口建议从: - -- `.agents/tsl/index.md`(TSL 规则集入口) -- `.agents/cpp/index.md`(C++ 规则集入口) -- `.agents/markdown/index.md`(Markdown 规则集入口) -- `docs/standards/playbook/docs/`(人类开发规范快照:`tsl/`、`cpp/`、`python/`、`common/`) -EOF - echo "Created .agents/index.md" -fi - -AGENTS_MD="$ROOT/AGENTS.md" -AGENTS_BLOCK_START="" -AGENTS_BLOCK_END="" -AGENTS_BLOCK_TMP="$(mktemp 2>/dev/null || echo "$ROOT/.agents_block.$timestamp")" -agents_langs="" -if [ -d "$AGENTS_ROOT" ]; then - for dir in "$AGENTS_ROOT"/*; do - [ -d "$dir" ] || continue - name="$(basename "$dir")" - case "$name" in - ""|.*|*.bak.*) continue ;; - esac - if [ -f "$dir/index.md" ]; then - agents_langs="${agents_langs:+$agents_langs }$name" - fi - done -fi -if [ -z "$agents_langs" ]; then - agents_langs="$AGENTS_NS" -fi - -langs_line="" -for name in $agents_langs; do - entry='`.agents/'"$name"'/index.md`' - if [ -z "$langs_line" ]; then - langs_line="$entry" - else - langs_line="$langs_line、$entry" - fi -done - -{ - printf "%s\n\n" "$AGENTS_BLOCK_START" - printf "%s\n\n" '请以 `.agents/` 下的规则为准:' - printf "%s\n" '- 入口:`.agents/index.md`' - if [ -n "$langs_line" ]; then - printf "%s\n" "- 语言规则:$langs_line" - else - printf "%s\n" "- 语言规则:" - fi - printf "%s\n" "$AGENTS_BLOCK_END" -} >"$AGENTS_BLOCK_TMP" - -if [ ! -f "$AGENTS_MD" ]; then - { - printf "%s\n\n" "# Agent Instructions" - cat "$AGENTS_BLOCK_TMP" - } >"$AGENTS_MD" - echo "Created AGENTS.md" -else - if grep -Fq "$AGENTS_BLOCK_START" "$AGENTS_MD"; then - tmp="$(mktemp 2>/dev/null || echo "$ROOT/.agents_md.$timestamp")" - awk -v start="$AGENTS_BLOCK_START" -v end="$AGENTS_BLOCK_END" -v block_file="$AGENTS_BLOCK_TMP" ' - BEGIN { - while ((getline line < block_file) > 0) { block[++n] = line } - close(block_file) - inblock=0 - replaced=0 - } - { - if (!replaced && $0 == start) { - for (i=1; i<=n; i++) print block[i] - inblock=1 - replaced=1 - next - } - if (inblock) { - if ($0 == end) { inblock=0 } - next - } - print - } - ' "$AGENTS_MD" >"$tmp" - mv "$tmp" "$AGENTS_MD" - echo "Updated AGENTS.md (playbook block)." - else - if grep -Fq ".agents/index.md" "$AGENTS_MD"; then - echo "Skip: AGENTS.md already references .agents/index.md" - else - printf "\n" >>"$AGENTS_MD" - cat "$AGENTS_BLOCK_TMP" >>"$AGENTS_MD" - printf "\n" >>"$AGENTS_MD" - echo "Appended playbook block to AGENTS.md" - fi - fi -fi -rm -f "$AGENTS_BLOCK_TMP" - -echo "Synced agents ruleset to $AGENTS_DST." - -GITATTR_DST="$ROOT/.gitattributes" -if [ -f "$GITATTR_SRC" ]; then - : "${SYNC_GITATTR_MODE:=append}" - case "$SYNC_GITATTR_MODE" in - skip) - echo "Skip: .gitattributes sync (SYNC_GITATTR_MODE=skip)." - ;; - overwrite) - if [ "$(CDPATH= cd -- "$(dirname -- "$GITATTR_SRC")" && pwd -P)/$(basename -- "$GITATTR_SRC")" = "$GITATTR_DST" ]; then - echo "Skip: .gitattributes source equals destination." - else - if [ -e "$GITATTR_DST" ]; then - mv "$GITATTR_DST" "$ROOT/.gitattributes.bak.$timestamp" - echo "Backed up existing .gitattributes -> .gitattributes.bak.$timestamp" - fi - cp "$GITATTR_SRC" "$GITATTR_DST" - echo "Synced .gitattributes from standards (overwrite)." - fi - ;; - append) - if [ "$(CDPATH= cd -- "$(dirname -- "$GITATTR_SRC")" && pwd -P)/$(basename -- "$GITATTR_SRC")" = "$GITATTR_DST" ]; then - echo "Skip: .gitattributes source equals destination." - else - missing_tmp="$(mktemp 2>/dev/null || echo "$ROOT/.gitattributes.missing.$timestamp")" - if [ -f "$GITATTR_DST" ]; then - awk ' - function norm(line) { - gsub(/^[ \t]+|[ \t]+$/, "", line) - return line - } - FNR==NR { - line=norm($0) - if (line == "" || line ~ /^#/) next - seen[line]=1 - next - } - { - line=norm($0) - if (line == "" || line ~ /^#/) next - if (!seen[line] && !out[line]++) print line - } - ' "$GITATTR_DST" "$GITATTR_SRC" >"$missing_tmp" - else - awk ' - function norm(line) { - gsub(/^[ \t]+|[ \t]+$/, "", line) - return line - } - { - line=norm($0) - if (line == "" || line ~ /^#/) next - if (!out[line]++) print line - } - ' "$GITATTR_SRC" >"$missing_tmp" - fi - - if [ ! -s "$missing_tmp" ]; then - rm -f "$missing_tmp" - echo "No missing .gitattributes rules to append." - echo "Done." - exit 0 - fi - - if [ -e "$GITATTR_DST" ]; then - mv "$GITATTR_DST" "$ROOT/.gitattributes.bak.$timestamp" - echo "Backed up existing .gitattributes -> .gitattributes.bak.$timestamp" - fi - - source_note="$GITATTR_SRC" - case "$GITATTR_SRC" in - "$ROOT"/*) source_note="${GITATTR_SRC#$ROOT/}" ;; - esac - header="# Added from playbook .gitattributes (source: $source_note)" - - { - if [ -f "$ROOT/.gitattributes.bak.$timestamp" ]; then - cat "$ROOT/.gitattributes.bak.$timestamp" - if [ -s "$ROOT/.gitattributes.bak.$timestamp" ]; then - printf "\n" - fi - fi - printf "%s\n" "$header" - cat "$missing_tmp" - } >"$GITATTR_DST" - rm -f "$missing_tmp" - echo "Appended missing .gitattributes rules from standards." - fi - ;; - block) - begin="# BEGIN playbook .gitattributes" - end="# END playbook .gitattributes" - begin_old="# BEGIN tsl-playbook .gitattributes" - end_old="# END tsl-playbook .gitattributes" - - if [ -e "$GITATTR_DST" ]; then - mv "$GITATTR_DST" "$ROOT/.gitattributes.bak.$timestamp" - echo "Backed up existing .gitattributes -> .gitattributes.bak.$timestamp" - fi - - tmp="${GITATTR_DST}.tmp.${timestamp}" - if [ -f "$ROOT/.gitattributes.bak.$timestamp" ]; then - src_dst="$ROOT/.gitattributes.bak.$timestamp" - else - src_dst="" - fi - - if [ -n "$src_dst" ]; then - awk -v begin="$begin" -v end="$end" -v begin_old="$begin_old" -v end_old="$end_old" -v src="$GITATTR_SRC" ' - function emit_src() { - print begin - while ((getline line < src) > 0) print line - close(src) - print end - } - BEGIN { in_block=0; done=0 } - $0 == begin || $0 == begin_old { in_block=1; if (!done) { emit_src(); done=1 } ; next } - $0 == end || $0 == end_old { in_block=0; next } - !in_block { print } - END { - if (!done) { - if (NR > 0) print "" - emit_src() - } - } - ' "$src_dst" >"$tmp" - else - { - printf "%s\n" "$begin" - cat "$GITATTR_SRC" - printf "\n%s\n" "$end" - } >"$tmp" - fi - - mv "$tmp" "$GITATTR_DST" - echo "Updated .gitattributes from standards (managed block)." - ;; - *) - echo "ERROR: invalid SYNC_GITATTR_MODE=$SYNC_GITATTR_MODE (use block|overwrite|append|skip)" >&2 - exit 1 - ;; - esac -fi - -echo "Done." diff --git a/scripts/sync_templates.bat b/scripts/sync_templates.bat deleted file mode 100644 index cd93eee..0000000 --- a/scripts/sync_templates.bat +++ /dev/null @@ -1,233 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -rem Sync project templates to target project. -rem - Copies templates/memory-bank/ -> /memory-bank/ -rem - Copies templates/prompts/ -> /docs/prompts/ -rem - Copies templates/AGENTS.template.md -> /AGENTS.md -rem - Copies templates/AGENT_RULES.template.md -> /AGENT_RULES.md -rem Existing targets are NOT overwritten (skip if exists). -rem -rem Usage: -rem sync_templates.bat # sync to current git root -rem sync_templates.bat -project-root # sync to specified project -rem sync_templates.bat -force # overwrite existing files -rem sync_templates.bat -full # append full framework to existing AGENTS.md - -set "SCRIPT_DIR=%~dp0" -for %%I in ("%SCRIPT_DIR%..") do set "SRC=%%~fI" - -set "FORCE=0" -set "FULL=0" -set "PROJECT_ROOT=" - -:parse_args -if "%~1"=="" goto args_done -if /I "%~1"=="-force" ( - set "FORCE=1" - shift - goto parse_args -) -if /I "%~1"=="-full" ( - set "FULL=1" - shift - goto parse_args -) -if /I "%~1"=="-project-root" ( - if "%~2"=="" ( - echo ERROR: -project-root requires a path. - exit /b 1 - ) - set "PROJECT_ROOT=%~2" - shift - shift - goto parse_args -) -if /I "%~1"=="-h" goto show_help -if /I "%~1"=="-help" goto show_help -echo ERROR: positional args are not supported. Use -project-root. -exit /b 1 - -:show_help -echo Usage: -echo sync_templates.bat [options] -echo sync_templates.bat -project-root ^ -echo. -echo Options: -echo -project-root PATH Target project root ^(default: git root^) -echo -force Overwrite existing files -echo -full Append full framework (规则优先级 + 新会话开始时) to existing AGENTS.md -echo -h, -help Show this help -exit /b 0 - -:args_done - -rem Determine project root -if "%PROJECT_ROOT%"=="" ( - for /f "delims=" %%R in ('git -C "%SCRIPT_DIR%" rev-parse --show-toplevel 2^>nul') do set "PROJECT_ROOT=%%R" -) -if "%PROJECT_ROOT%"=="" set "PROJECT_ROOT=%cd%" -for %%I in ("%PROJECT_ROOT%") do set "PROJECT_ROOT=%%~fI" - -rem Source directories -set "TEMPLATES_DIR=%SRC%\templates" -set "MEMORY_BANK_SRC=%TEMPLATES_DIR%\memory-bank" -set "PROMPTS_SRC=%TEMPLATES_DIR%\prompts" -set "AGENTS_SRC=%TEMPLATES_DIR%\AGENTS.template.md" -set "AGENT_RULES_SRC=%TEMPLATES_DIR%\AGENT_RULES.template.md" - -rem Check source exists -if not exist "%TEMPLATES_DIR%" ( - echo ERROR: Templates directory not found: %TEMPLATES_DIR% - exit /b 1 -) - -rem Skip if source equals destination -if /I "%SRC%"=="%PROJECT_ROOT%" ( - echo Skip: playbook root equals project root. - echo Done. - exit /b 0 -) - -for /f "usebackq delims=" %%D in (`powershell -NoProfile -Command "Get-Date -Format 'yyyy-MM-dd'"`) do set "SYNC_DATE=%%D" -if "%SYNC_DATE%"=="" set "SYNC_DATE=%date%" - -echo Syncing templates to: %PROJECT_ROOT% -echo. - -rem 1. Sync memory-bank/ -set "MEMORY_BANK_DST=%PROJECT_ROOT%\memory-bank" -if exist "%MEMORY_BANK_SRC%" ( - if exist "%MEMORY_BANK_DST%" ( - if "%FORCE%"=="0" ( - echo memory-bank/ already exists. Skip. Use -force to overwrite. - goto sync_prompts - ) - ) - if not exist "%MEMORY_BANK_DST%" mkdir "%MEMORY_BANK_DST%" - xcopy "%MEMORY_BANK_SRC%\*" "%MEMORY_BANK_DST%\" /e /i /y >nul 2>nul - - rem Rename .template.md to .md - for %%F in ("%MEMORY_BANK_DST%\*.template.md") do ( - set "OLDNAME=%%~nxF" - set "NEWNAME=!OLDNAME:.template.md=.md!" - ren "%%F" "!NEWNAME!" - ) - - rem Replace {{DATE}} placeholder - for %%F in ("%MEMORY_BANK_DST%\*.md") do ( - powershell -NoProfile -Command "$f='%%~fF'; $c=Get-Content -Raw $f; $c=$c.Replace('{{DATE}}','%SYNC_DATE%'); Set-Content -Path $f -Value $c -Encoding UTF8 -NoNewline" - ) - echo Synced: memory-bank/ -) else ( - echo Skip: memory-bank/ templates not found -) - -:sync_prompts -rem 2. Sync docs/prompts/ -set "PROMPTS_DST=%PROJECT_ROOT%\docs\prompts" -if exist "%PROMPTS_SRC%" ( - if exist "%PROMPTS_DST%" ( - if "%FORCE%"=="0" ( - echo docs/prompts/ already exists. Skip. Use -force to overwrite. - goto sync_agents - ) - ) - if not exist "%PROJECT_ROOT%\docs" mkdir "%PROJECT_ROOT%\docs" - if not exist "%PROMPTS_DST%" mkdir "%PROMPTS_DST%" - xcopy "%PROMPTS_SRC%\*" "%PROMPTS_DST%\" /e /i /y >nul 2>nul - - rem Rename .template.md to .md recursively - for /r "%PROMPTS_DST%" %%F in (*.template.md) do ( - set "OLDNAME=%%~nxF" - set "NEWNAME=!OLDNAME:.template.md=.md!" - ren "%%F" "!NEWNAME!" - ) - - rem Replace {{DATE}} placeholder - for /r "%PROMPTS_DST%" %%F in (*.md) do ( - powershell -NoProfile -Command "$f='%%~fF'; $c=Get-Content -Raw $f; $c=$c.Replace('{{DATE}}','%SYNC_DATE%'); Set-Content -Path $f -Value $c -Encoding UTF8 -NoNewline" - ) - echo Synced: docs/prompts/ -) else ( - echo Skip: prompts/ templates not found -) - -:sync_agents -rem 3. Sync AGENTS.md -set "AGENTS_DST=%PROJECT_ROOT%\AGENTS.md" - -rem Choose markers based on -full flag -if "%FULL%"=="1" ( - set "MARKER_START=" - set "MARKER_END=" - set "SECTION_NAME=framework" -) else ( - set "MARKER_START=" - set "MARKER_END=" - set "SECTION_NAME=templates" -) - -if exist "%AGENTS_SRC%" ( - if not exist "%AGENTS_DST%" ( - rem AGENTS.md doesn't exist: create from full template - copy /y "%AGENTS_SRC%" "%AGENTS_DST%" >nul - powershell -NoProfile -Command "$f='%AGENTS_DST%'; $c=Get-Content -Raw $f; $c=$c.Replace('{{DATE}}','%SYNC_DATE%'); Set-Content -Path $f -Value $c -Encoding UTF8 -NoNewline" - echo Created: AGENTS.md - ) else ( - rem AGENTS.md exists: update or append section (extract from template) - powershell -NoProfile -Command ^ - "$src='%AGENTS_SRC%'; $dst='%AGENTS_DST%'; $date='%SYNC_DATE%'; " ^ - "$markerStart='!MARKER_START!'; $markerEnd='!MARKER_END!'; $sectionName='!SECTION_NAME!'; " ^ - "$templateContent = Get-Content -Raw $src; " ^ - "$extractPattern = '(?s)(' + [regex]::Escape($markerStart) + '.*?' + [regex]::Escape($markerEnd) + ')'; " ^ - "if ($templateContent -match $extractPattern) { " ^ - " $snippetContent = $Matches[1]; " ^ - " $content = Get-Content -Raw $dst; " ^ - " if ($content -match [regex]::Escape($markerStart)) { " ^ - " $replacePattern = '(?s)' + [regex]::Escape($markerStart) + '.*?' + [regex]::Escape($markerEnd); " ^ - " $newContent = $content -replace $replacePattern, $snippetContent; " ^ - " $newContent = $newContent.Replace('{{DATE}}', $date); " ^ - " Set-Content -Path $dst -Value $newContent -Encoding UTF8 -NoNewline; " ^ - " Write-Host \"Updated: AGENTS.md ($sectionName section)\"; " ^ - " } else { " ^ - " $newContent = $content.TrimEnd() + \"`n`n\" + $snippetContent; " ^ - " $newContent = $newContent.Replace('{{DATE}}', $date); " ^ - " Set-Content -Path $dst -Value $newContent -Encoding UTF8 -NoNewline; " ^ - " Write-Host \"Appended: AGENTS.md ($sectionName section)\"; " ^ - " } " ^ - "} else { " ^ - " Write-Host 'Skip: markers not found in template'; " ^ - "}" - ) -) else ( - echo Skip: AGENTS.template.md not found -) - -:sync_agent_rules -rem 4. Sync AGENT_RULES.md -set "AGENT_RULES_DST=%PROJECT_ROOT%\AGENT_RULES.md" -if exist "%AGENT_RULES_SRC%" ( - if exist "%AGENT_RULES_DST%" ( - if "%FORCE%"=="0" ( - echo AGENT_RULES.md already exists. Skip. Use -force to overwrite. - goto sync_done - ) - ) - copy /y "%AGENT_RULES_SRC%" "%AGENT_RULES_DST%" >nul - powershell -NoProfile -Command "$f='%AGENT_RULES_DST%'; $c=Get-Content -Raw $f; $c=$c.Replace('{{DATE}}','%SYNC_DATE%'); Set-Content -Path $f -Value $c -Encoding UTF8 -NoNewline" - echo Synced: AGENT_RULES.md -) else ( - echo Skip: AGENT_RULES.template.md not found -) - -:sync_done -echo. -echo Done. -echo. -echo Next steps: -echo 1. Edit memory-bank\*.md to fill in project-specific content -echo 2. Replace remaining {{PLACEHOLDER}} values -echo 3. Run sync_standards.bat to sync .agents\ rules - -endlocal diff --git a/scripts/sync_templates.ps1 b/scripts/sync_templates.ps1 deleted file mode 100644 index 62c09d9..0000000 --- a/scripts/sync_templates.ps1 +++ /dev/null @@ -1,248 +0,0 @@ -# Sync project templates to target project. -# - Copies templates/memory-bank/ -> /memory-bank/ -# - Copies templates/prompts/ -> /docs/prompts/ -# - Copies templates/AGENTS.template.md -> /AGENTS.md -# - Copies templates/AGENT_RULES.template.md -> /AGENT_RULES.md -# Existing targets are backed up before overwrite. -[CmdletBinding()] -param( - [Parameter(Mandatory = $false)] - [Alias('h', '?')] - [switch]$Help, - - [Parameter(Mandatory = $false)] - [string]$ProjectRoot, - - [Parameter(Mandatory = $false)] - [string]$ProjectName, - - [Parameter(Mandatory = $false)] - [string]$Date, - - [Parameter(Mandatory = $false)] - [switch]$NoBackup, - - [Parameter(Mandatory = $false)] - [switch]$Force, - - [Parameter(Mandatory = $false)] - [switch]$Full -) - -$ErrorActionPreference = "Stop" - -if ($Help) { - Write-Host "Usage:" - Write-Host " powershell -File scripts/sync_templates.ps1 [options]" - Write-Host " powershell -File scripts/sync_templates.ps1 -ProjectRoot C:\\path\\to\\project" - Write-Host "" - Write-Host "Options:" - Write-Host " -ProjectRoot PATH Target project root (default: git root)." - Write-Host " -ProjectName NAME Replace {{PROJECT_NAME}} placeholder." - Write-Host " -Date DATE Replace {{DATE}} placeholder (default: today)." - Write-Host " -NoBackup Skip backup of existing files." - Write-Host " -Force Overwrite without prompting." - Write-Host " -Full Append full framework section to AGENTS.md." - Write-Host " -Help Show this help." - exit 0 -} -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$Src = (Resolve-Path (Join-Path $ScriptDir "..")).Path - -# Defaults -if (-not $Date) { - $Date = Get-Date -Format "yyyy-MM-dd" -} - -# Determine project root -if (-not $ProjectRoot) { - $ProjectRoot = (git -C $ScriptDir rev-parse --show-toplevel 2>$null) - if (-not $ProjectRoot) { $ProjectRoot = (Get-Location).Path } -} -$ProjectRoot = (Resolve-Path $ProjectRoot).Path - -# Source directories -$TemplatesDir = Join-Path $Src "templates" -$MemoryBankSrc = Join-Path $TemplatesDir "memory-bank" -$PromptsSrc = Join-Path $TemplatesDir "prompts" -$AgentsSrc = Join-Path $TemplatesDir "AGENTS.template.md" -$AgentRulesSrc = Join-Path $TemplatesDir "AGENT_RULES.template.md" - -# Check source exists -if (-not (Test-Path $TemplatesDir)) { - throw "Templates directory not found: $TemplatesDir" -} - -# Skip if source equals destination -if ($Src -ieq $ProjectRoot) { - Write-Host "Skip: playbook root equals project root." - Write-Host "Done." - exit 0 -} - -$timestamp = Get-Date -Format "yyyyMMddHHmmss" - -# Function: backup file/directory -function Backup-IfExists { - param([string]$Target) - - if ((Test-Path $Target) -and -not $NoBackup) { - $backup = "$Target.bak.$timestamp" - Move-Item $Target $backup - Write-Host "Backed up: $(Split-Path -Leaf $Target) -> $(Split-Path -Leaf $backup)" - } -} - -# Function: replace placeholders in file -function Replace-Placeholders { - param([string]$File) - - if (-not (Test-Path $File)) { return } - - $content = Get-Content -Raw -Path $File - if ($ProjectName) { - $content = $content.Replace("{{PROJECT_NAME}}", $ProjectName) - } - $content = $content.Replace("{{DATE}}", $Date) - Set-Content -Path $File -Value $content -Encoding UTF8 -NoNewline -} - -# Function: replace placeholders in directory -function Replace-PlaceholdersDir { - param([string]$Dir) - - if (-not (Test-Path $Dir)) { return } - - Get-ChildItem -Path $Dir -Filter "*.md" -Recurse -File | ForEach-Object { - Replace-Placeholders -File $_.FullName - } -} - -Write-Host "Syncing templates to: $ProjectRoot" -Write-Host "" - -# 1. Sync memory-bank/ -if (Test-Path $MemoryBankSrc) { - $MemoryBankDst = Join-Path $ProjectRoot "memory-bank" - - if ((Test-Path $MemoryBankDst) -and -not $Force) { - Write-Host "memory-bank/ already exists. Use -Force to overwrite." - } else { - Backup-IfExists -Target $MemoryBankDst - New-Item -ItemType Directory -Path $MemoryBankDst -Force | Out-Null - Copy-Item -Path (Join-Path $MemoryBankSrc "*") -Destination $MemoryBankDst -Recurse -Force - - # Rename .template.md to .md - Get-ChildItem -Path $MemoryBankDst -Filter "*.template.md" -File | ForEach-Object { - $newName = $_.Name -replace "\.template\.md$", ".md" - Rename-Item -Path $_.FullName -NewName $newName - } - - Replace-PlaceholdersDir -Dir $MemoryBankDst - Write-Host "Synced: memory-bank/" - } -} else { - Write-Host "Skip: memory-bank/ templates not found" -} - -# 2. Sync docs/prompts/ -if (Test-Path $PromptsSrc) { - $PromptsDst = Join-Path $ProjectRoot "docs\prompts" - - if ((Test-Path $PromptsDst) -and -not $Force) { - Write-Host "docs/prompts/ already exists. Use -Force to overwrite." - } else { - Backup-IfExists -Target $PromptsDst - $DocsDir = Join-Path $ProjectRoot "docs" - New-Item -ItemType Directory -Path $DocsDir -Force | Out-Null - New-Item -ItemType Directory -Path $PromptsDst -Force | Out-Null - Copy-Item -Path (Join-Path $PromptsSrc "*") -Destination $PromptsDst -Recurse -Force - - # Rename .template.md to .md recursively - Get-ChildItem -Path $PromptsDst -Filter "*.template.md" -Recurse -File | ForEach-Object { - $newName = $_.Name -replace "\.template\.md$", ".md" - Rename-Item -Path $_.FullName -NewName $newName - } - - Replace-PlaceholdersDir -Dir $PromptsDst - Write-Host "Synced: docs/prompts/" - } -} else { - Write-Host "Skip: prompts/ templates not found" -} - -# 3. Sync AGENTS.md -# Choose markers based on -Full flag -if ($Full) { - $MarkerStart = "" - $MarkerEnd = "" - $SectionName = "framework" -} else { - $MarkerStart = "" - $MarkerEnd = "" - $SectionName = "templates" -} - -if (Test-Path $AgentsSrc) { - $AgentsDst = Join-Path $ProjectRoot "AGENTS.md" - - if (-not (Test-Path $AgentsDst)) { - # AGENTS.md doesn't exist: create from full template - Copy-Item -Path $AgentsSrc -Destination $AgentsDst -Force - Replace-Placeholders -File $AgentsDst - Write-Host "Created: AGENTS.md" - } else { - # AGENTS.md exists: update or append section - # Extract snippet from template - $templateContent = Get-Content -Raw -Path $AgentsSrc - $extractPattern = "(?s)(" + [regex]::Escape($MarkerStart) + ".*?" + [regex]::Escape($MarkerEnd) + ")" - if ($templateContent -match $extractPattern) { - $snippetContent = $Matches[1] - - $content = Get-Content -Raw -Path $AgentsDst - - if ($content -match [regex]::Escape($MarkerStart)) { - # Has markers: replace content between markers - $replacePattern = "(?s)" + [regex]::Escape($MarkerStart) + ".*?" + [regex]::Escape($MarkerEnd) - $newContent = $content -replace $replacePattern, $snippetContent - Set-Content -Path $AgentsDst -Value $newContent -Encoding UTF8 -NoNewline - Replace-Placeholders -File $AgentsDst - Write-Host "Updated: AGENTS.md ($SectionName section)" - } else { - # No markers: append snippet at the end - $newContent = $content.TrimEnd() + "`n`n" + $snippetContent - Set-Content -Path $AgentsDst -Value $newContent -Encoding UTF8 -NoNewline - Replace-Placeholders -File $AgentsDst - Write-Host "Appended: AGENTS.md ($SectionName section)" - } - } else { - Write-Host "Skip: markers not found in template" - } - } -} else { - Write-Host "Skip: AGENTS.template.md not found" -} - -# 4. Sync AGENT_RULES.md -if (Test-Path $AgentRulesSrc) { - $AgentRulesDst = Join-Path $ProjectRoot "AGENT_RULES.md" - - if ((Test-Path $AgentRulesDst) -and -not $Force) { - Write-Host "AGENT_RULES.md already exists. Use -Force to overwrite." - } else { - Backup-IfExists -Target $AgentRulesDst - Copy-Item -Path $AgentRulesSrc -Destination $AgentRulesDst -Force - Replace-Placeholders -File $AgentRulesDst - Write-Host "Synced: AGENT_RULES.md" - } -} else { - Write-Host "Skip: AGENT_RULES.template.md not found" -} - -Write-Host "" -Write-Host "Done." -Write-Host "" -Write-Host "Next steps:" -Write-Host " 1. Edit memory-bank/*.md to fill in project-specific content" -Write-Host " 2. Replace remaining {{PLACEHOLDER}} values" -Write-Host " 3. Run sync_standards.ps1 to sync .agents/ rules" diff --git a/scripts/sync_templates.sh b/scripts/sync_templates.sh deleted file mode 100644 index b233489..0000000 --- a/scripts/sync_templates.sh +++ /dev/null @@ -1,323 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Sync project templates to target project. -# - Copies templates/memory-bank/ -> /memory-bank/ -# - Copies templates/prompts/ -> /docs/prompts/ -# - Copies templates/AGENTS.template.md -> /AGENTS.md -# - Copies templates/AGENT_RULES.template.md -> /AGENT_RULES.md -# Existing targets are backed up before overwrite. -# -# Usage: -# sh scripts/sync_templates.sh # sync to current git root -# sh scripts/sync_templates.sh -project-root /path/to/project -# sh scripts/sync_templates.sh -project-root /path/to/project -project-name "MyProject" -date "2026-01-20" -# -# Options: -# -project-root PATH Target project root (default: git root) -# -project-name NAME Replace {{PROJECT_NAME}} placeholder -# -date DATE Replace {{DATE}} placeholder (default: today) -# -no-backup Skip backup of existing files -# -force Overwrite without prompting -# -full Append full framework (规则优先级 + 新会话开始时) to existing AGENTS.md - -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" -SRC="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)" - -# Defaults -PROJECT_NAME="" -SYNC_DATE="$(date +%Y-%m-%d 2>/dev/null || echo "{{DATE}}")" -NO_BACKUP=0 -FORCE=0 -FULL=0 -PROJECT_ROOT="" - -# Parse arguments -while [ $# -gt 0 ]; do - case "$1" in - -project-root) - if [ $# -lt 2 ] || [ -z "${2:-}" ]; then - echo "ERROR: -project-root requires a path." >&2 - exit 1 - fi - PROJECT_ROOT="$2" - shift 2 - ;; - -project-name) - if [ $# -lt 2 ] || [ -z "${2:-}" ]; then - echo "ERROR: -project-name requires a value." >&2 - exit 1 - fi - PROJECT_NAME="$2" - shift 2 - ;; - -date) - if [ $# -lt 2 ] || [ -z "${2:-}" ]; then - echo "ERROR: -date requires a value." >&2 - exit 1 - fi - SYNC_DATE="$2" - shift 2 - ;; - -no-backup) - NO_BACKUP=1 - shift - ;; - -force) - FORCE=1 - shift - ;; - -full) - FULL=1 - shift - ;; - -h|-help) - cat <<'EOF' -Usage: - sh scripts/sync_templates.sh [options] - sh scripts/sync_templates.sh -project-root /path/to/project - -Options: - -project-root PATH Target project root (default: git root) - -project-name NAME Replace {{PROJECT_NAME}} placeholder - -date DATE Replace {{DATE}} placeholder (default: today) - -no-backup Skip backup of existing files - -force Overwrite without prompting - -full Append full framework (规则优先级 + 新会话开始时) to existing AGENTS.md - -h, -help Show this help - -Examples: - sh scripts/sync_templates.sh - sh scripts/sync_templates.sh -project-root /path/to/project - sh scripts/sync_templates.sh -project-root /path/to/project -full -EOF - exit 0 - ;; - -*) - echo "ERROR: Unknown option: $1" >&2 - exit 1 - ;; - *) - echo "ERROR: positional args are not supported; use -project-root." >&2 - exit 1 - ;; - esac -done - -# Determine project root -if [ -z "$PROJECT_ROOT" ]; then - PROJECT_ROOT="$(git -C "$SCRIPT_DIR" rev-parse --show-toplevel 2>/dev/null || pwd)" -fi -PROJECT_ROOT="$(CDPATH= cd -- "$PROJECT_ROOT" && pwd -P)" - -# Source directories -TEMPLATES_DIR="$SRC/templates" -MEMORY_BANK_SRC="$TEMPLATES_DIR/memory-bank" -PROMPTS_SRC="$TEMPLATES_DIR/prompts" -AGENTS_SRC="$TEMPLATES_DIR/AGENTS.template.md" -AGENT_RULES_SRC="$TEMPLATES_DIR/AGENT_RULES.template.md" - -# Check source exists -if [ ! -d "$TEMPLATES_DIR" ]; then - echo "ERROR: Templates directory not found: $TEMPLATES_DIR" >&2 - exit 1 -fi - -# Skip if source equals destination (running from playbook repo itself) -if [ "$SRC" = "$PROJECT_ROOT" ]; then - echo "Skip: playbook root equals project root." - echo "Done." - exit 0 -fi - -timestamp="$(date +%Y%m%d%H%M%S 2>/dev/null || echo bak)" - -# Function: backup file/directory -backup_if_exists() { - target="$1" - if [ -e "$target" ] && [ "$NO_BACKUP" -eq 0 ]; then - backup="${target}.bak.$timestamp" - mv "$target" "$backup" - echo "Backed up: $(basename "$target") -> $(basename "$backup")" - fi -} - -escape_sed_replacement() { - printf '%s' "$1" | sed 's/[&/|\\]/\\&/g' -} - -# Function: replace placeholders in file -replace_placeholders() { - file="$1" - [ -f "$file" ] || return 0 - - tmp="$(mktemp 2>/dev/null || echo "$file.tmp.$timestamp")" - date_repl="$(escape_sed_replacement "$SYNC_DATE")" - if [ -n "$PROJECT_NAME" ]; then - project_repl="$(escape_sed_replacement "$PROJECT_NAME")" - sed -e "s/{{PROJECT_NAME}}/$project_repl/g" -e "s/{{DATE}}/$date_repl/g" "$file" > "$tmp" - else - sed -e "s/{{DATE}}/$date_repl/g" "$file" > "$tmp" - fi - mv "$tmp" "$file" -} - -# Function: replace placeholders in directory -replace_placeholders_dir() { - dir="$1" - [ -d "$dir" ] || return 0 - - find "$dir" -type f -name '*.md' -print | while IFS= read -r file; do - replace_placeholders "$file" - done -} - -echo "Syncing templates to: $PROJECT_ROOT" -echo "" - -# 1. Sync memory-bank/ -if [ -d "$MEMORY_BANK_SRC" ]; then - MEMORY_BANK_DST="$PROJECT_ROOT/memory-bank" - - if [ -e "$MEMORY_BANK_DST" ] && [ "$FORCE" -eq 0 ]; then - echo "memory-bank/ already exists. Use -force to overwrite." - else - backup_if_exists "$MEMORY_BANK_DST" - mkdir -p "$MEMORY_BANK_DST" - cp -R "$MEMORY_BANK_SRC"/* "$MEMORY_BANK_DST/" 2>/dev/null || true - - # Rename .template.md to .md - for f in "$MEMORY_BANK_DST"/*.template.md; do - [ -f "$f" ] || continue - newname="$(echo "$f" | sed 's/\.template\.md$/.md/')" - mv "$f" "$newname" - done - - replace_placeholders_dir "$MEMORY_BANK_DST" - echo "Synced: memory-bank/" - fi -else - echo "Skip: memory-bank/ templates not found" -fi - -# 2. Sync docs/prompts/ -if [ -d "$PROMPTS_SRC" ]; then - PROMPTS_DST="$PROJECT_ROOT/docs/prompts" - - if [ -e "$PROMPTS_DST" ] && [ "$FORCE" -eq 0 ]; then - echo "docs/prompts/ already exists. Use -force to overwrite." - else - backup_if_exists "$PROMPTS_DST" - mkdir -p "$PROJECT_ROOT/docs" - mkdir -p "$PROMPTS_DST" - cp -R "$PROMPTS_SRC"/* "$PROMPTS_DST/" 2>/dev/null || true - - # Rename .template.md to .md (recursive) - find "$PROMPTS_DST" -type f -name '*.template.md' -print | while IFS= read -r f; do - newname="$(echo "$f" | sed 's/\.template\.md$/.md/')" - mv "$f" "$newname" - done - - replace_placeholders_dir "$PROMPTS_DST" - echo "Synced: docs/prompts/" - fi -else - echo "Skip: prompts/ templates not found" -fi - -# 3. Sync AGENTS.md -# Choose markers based on -full flag -if [ "$FULL" -eq 1 ]; then - MARKER_START="" - MARKER_END="" - SECTION_NAME="framework" -else - MARKER_START="" - MARKER_END="" - SECTION_NAME="templates" -fi - -if [ -f "$AGENTS_SRC" ]; then - AGENTS_DST="$PROJECT_ROOT/AGENTS.md" - - if [ ! -e "$AGENTS_DST" ]; then - # AGENTS.md doesn't exist: create from full template - cp "$AGENTS_SRC" "$AGENTS_DST" - replace_placeholders "$AGENTS_DST" - echo "Created: AGENTS.md" - else - # AGENTS.md exists: update or append section - # Extract snippet from template - snippet_content="$(awk -v start="$MARKER_START" -v end="$MARKER_END" ' - $0 ~ start { found=1 } - found { print } - $0 ~ end { found=0 } - ' "$AGENTS_SRC")" - - if [ -z "$snippet_content" ]; then - echo "Skip: markers not found in template" - elif grep -q "$MARKER_START" "$AGENTS_DST"; then - # Has markers: replace content between markers in place - snippet_tmp="$(mktemp 2>/dev/null || echo "$AGENTS_DST.snippet.$timestamp")" - printf "%s\n" "$snippet_content" > "$snippet_tmp" - tmp="$(mktemp 2>/dev/null || echo "$AGENTS_DST.tmp.$timestamp")" - awk -v start="$MARKER_START" -v end="$MARKER_END" -v snippet="$snippet_tmp" ' - BEGIN { - while ((getline line < snippet) > 0) { block[++n] = line } - close(snippet) - inblock = 0 - replaced = 0 - } - { - if (!replaced && $0 ~ start) { - for (i=1; i<=n; i++) print block[i] - inblock = 1 - replaced = 1 - next - } - if (inblock) { - if ($0 ~ end) { inblock = 0 } - next - } - print - } - ' "$AGENTS_DST" > "$tmp" - mv "$tmp" "$AGENTS_DST" - rm -f "$snippet_tmp" - replace_placeholders "$AGENTS_DST" - echo "Updated: AGENTS.md ($SECTION_NAME section)" - else - # No markers: append snippet at the end - echo "" >> "$AGENTS_DST" - echo "$snippet_content" >> "$AGENTS_DST" - replace_placeholders "$AGENTS_DST" - echo "Appended: AGENTS.md ($SECTION_NAME section)" - fi - fi -else - echo "Skip: AGENTS.template.md not found" -fi - -# 4. Sync AGENT_RULES.md -if [ -f "$AGENT_RULES_SRC" ]; then - AGENT_RULES_DST="$PROJECT_ROOT/AGENT_RULES.md" - - if [ -e "$AGENT_RULES_DST" ] && [ "$FORCE" -eq 0 ]; then - echo "AGENT_RULES.md already exists. Use -force to overwrite." - else - backup_if_exists "$AGENT_RULES_DST" - cp "$AGENT_RULES_SRC" "$AGENT_RULES_DST" - replace_placeholders "$AGENT_RULES_DST" - echo "Synced: AGENT_RULES.md" - fi -else - echo "Skip: AGENT_RULES.template.md not found" -fi - -echo "" -echo "Done." -echo "" -echo "Next steps:" -echo " 1. Edit memory-bank/*.md to fill in project-specific content" -echo " 2. Replace remaining {{PLACEHOLDER}} values" -echo " 3. Run sync_standards.sh -langs to sync .agents/ rules" diff --git a/scripts/vendor_playbook.bat b/scripts/vendor_playbook.bat deleted file mode 100644 index 8a24c1f..0000000 --- a/scripts/vendor_playbook.bat +++ /dev/null @@ -1,379 +0,0 @@ -@echo off -setlocal enabledelayedexpansion - -rem Vendor a trimmed Playbook snapshot into a target project (offline copy), -rem then run sync_standards to materialize .agents\\ and .gitattributes in -rem the target project root. -rem -rem Usage: -rem scripts\vendor_playbook.bat -project-root (default: tsl) -rem scripts\vendor_playbook.bat -project-root -langs tsl,cpp -rem scripts\vendor_playbook.bat -project-root -langs tsl,cpp -apply-templates -rem -rem Options: -rem -project-root Target project root (required) -rem -apply-templates Apply CI/lang templates to project root (skip if exists) -rem -rem Notes: -rem - Snapshot is written to: \docs\standards\playbook\ -rem - Existing snapshot is backed up before overwrite. -rem - With -apply-templates, CI and lang templates are copied to project root. - -set "SCRIPT_DIR=%~dp0" -for %%I in ("%SCRIPT_DIR%..") do set "SRC=%%~fI" - -if "%~1"=="" goto Usage -if "%~1"=="-h" goto Usage -if "%~1"=="-help" goto Usage - -set "DEST_ROOT=" -set "LANGS=" -set "APPLY_TEMPLATES=0" - -rem Parse arguments -:parse_args -if "%~1"=="" goto args_done -if "%~1"=="-project-root" ( - if "%~2"=="" ( - echo ERROR: -project-root requires a path. - exit /b 1 - ) - set "DEST_ROOT=%~2" - shift /1 - shift /1 - goto parse_args -) -if "%~1"=="-langs" ( - if "%~2"=="" ( - echo ERROR: -langs requires a value. - exit /b 1 - ) - set "LANGS=%~2" - shift /1 - shift /1 - goto parse_args -) -if "%~1"=="-apply-templates" ( - set "APPLY_TEMPLATES=1" - shift /1 - goto parse_args -) -echo ERROR: positional args are not supported. Use -project-root/-langs. -exit /b 1 -:args_done - -if "%DEST_ROOT%"=="" ( - echo ERROR: -project-root is required. - exit /b 1 -) - -if "%LANGS%"=="" set "LANGS=tsl" -set "LANGS=%LANGS:,= %" - -if not exist "%DEST_ROOT%" mkdir "%DEST_ROOT%" -for %%I in ("%DEST_ROOT%") do set "DEST_ROOT_ABS=%%~fI" - -set "STANDARDS_DIR=%DEST_ROOT_ABS%\\docs\\standards" -set "DEST_PREFIX=%STANDARDS_DIR%\\playbook" - -if not exist "%STANDARDS_DIR%" mkdir "%STANDARDS_DIR%" - -if exist "%DEST_PREFIX%" ( - set "RAND=%RANDOM%" - pushd "%STANDARDS_DIR%" - ren "playbook" "playbook.bak.!RAND!" - popd - echo Backed up existing snapshot -^> docs\\standards\\playbook.bak.!RAND! -) - -if not exist "%DEST_PREFIX%" mkdir "%DEST_PREFIX%" - -copy /y "%SRC%\\.gitattributes" "%DEST_PREFIX%\\.gitattributes" >nul -copy /y "%SRC%\\SKILLS.md" "%DEST_PREFIX%\\SKILLS.md" >nul - -xcopy "%SRC%\\scripts\\*" "%DEST_PREFIX%\\scripts\\" /e /i /y >nul -if errorlevel 1 ( - echo ERROR: failed to copy scripts - exit /b 1 -) - -xcopy "%SRC%\\codex\\*" "%DEST_PREFIX%\\codex\\" /e /i /y >nul -if errorlevel 1 ( - echo ERROR: failed to copy codex - exit /b 1 -) - -xcopy "%SRC%\\docs\\common\\*" "%DEST_PREFIX%\\docs\\common\\" /e /i /y >nul -if errorlevel 1 ( - echo ERROR: failed to copy docs\\common - exit /b 1 -) - -if not exist "%DEST_PREFIX%\\rulesets" mkdir "%DEST_PREFIX%\\rulesets" -copy /y "%SRC%\\rulesets\\index.md" "%DEST_PREFIX%\\rulesets\\index.md" >nul - -if not exist "%DEST_PREFIX%\\templates" mkdir "%DEST_PREFIX%\\templates" - -if exist "%SRC%\\templates\\ci" ( - xcopy "%SRC%\\templates\\ci\\*" "%DEST_PREFIX%\\templates\\ci\\" /e /i /y >nul - if errorlevel 1 ( - echo ERROR: failed to copy templates\\ci - exit /b 1 - ) -) - -set "LANGS_CSV=" -for %%L in (%LANGS%) do ( - echo %%~L| findstr /r "[\\/]" >nul && ( - echo ERROR: invalid lang=%%~L - exit /b 1 - ) - echo %%~L| findstr /c:".." >nul && ( - echo ERROR: invalid lang=%%~L - exit /b 1 - ) - - if not exist "%SRC%\\docs\\%%~L" ( - echo ERROR: docs not found for lang=%%~L "%SRC%\\docs\\%%~L" - exit /b 1 - ) - if not exist "%SRC%\\rulesets\\%%~L" ( - echo ERROR: agents ruleset not found for lang=%%~L "%SRC%\\rulesets\\%%~L" - exit /b 1 - ) - - xcopy "%SRC%\\docs\\%%~L\\*" "%DEST_PREFIX%\\docs\\%%~L\\" /e /i /y >nul - if errorlevel 1 ( - echo ERROR: failed to copy docs for lang=%%~L - exit /b 1 - ) - - xcopy "%SRC%\\rulesets\\%%~L\\*" "%DEST_PREFIX%\\rulesets\\%%~L\\" /e /i /y >nul - if errorlevel 1 ( - echo ERROR: failed to copy agents for lang=%%~L - exit /b 1 - ) - - if exist "%SRC%\\templates\\%%~L" ( - xcopy "%SRC%\\templates\\%%~L\\*" "%DEST_PREFIX%\\templates\\%%~L\\" /e /i /y >nul - if errorlevel 1 ( - echo ERROR: failed to copy templates for lang=%%~L - exit /b 1 - ) - ) - - if "!LANGS_CSV!"=="" ( - set "LANGS_CSV=%%~L" - ) else ( - set "LANGS_CSV=!LANGS_CSV!,%%~L" - ) -) - -set "DOC_INDEX=%DEST_PREFIX%\\docs\\index.md" -> "%DOC_INDEX%" echo # 文档导航(Docs Index) ->> "%DOC_INDEX%" echo. ->> "%DOC_INDEX%" echo 本快照为裁剪版 Playbook(langs: %LANGS_CSV%)。 ->> "%DOC_INDEX%" echo. ->> "%DOC_INDEX%" echo ## 跨语言(common) ->> "%DOC_INDEX%" echo. ->> "%DOC_INDEX%" echo - 提交信息与版本号:`common/commit_message.md` - -for %%L in (%LANGS%) do call :AppendDocsSection "%%~L" - -set "COMMIT=" -for /f "delims=" %%H in ('git -C "%SRC%" rev-parse HEAD 2^>nul') do set "COMMIT=%%H" -if "%COMMIT%"=="" set "COMMIT=N/A" - -set "README=%DEST_PREFIX%\\README.md" -> "%README%" echo # Playbook(裁剪快照) ->> "%README%" echo. ->> "%README%" echo 本目录为从 Playbook vendoring 的裁剪快照(langs: %LANGS_CSV%)。 ->> "%README%" echo. ->> "%README%" echo ## 使用 ->> "%README%" echo. ->> "%README%" echo 在目标项目根目录执行(多语言一次同步): ->> "%README%" echo. ->> "%README%" echo ```sh ->> "%README%" echo sh docs/standards/playbook/scripts/sync_standards.sh -langs %LANGS_CSV% ->> "%README%" echo ``` ->> "%README%" echo. ->> "%README%" echo 查看规范入口: ->> "%README%" echo. ->> "%README%" echo - `docs/standards/playbook/docs/index.md` ->> "%README%" echo - `.agents/index.md` ->> "%README%" echo. ->> "%README%" echo ## Codex skills(可选) ->> "%README%" echo. ->> "%README%" echo 安装到本机(需要先在 `~/.codex/config.toml` 启用 skills;见 `docs/standards/playbook/SKILLS.md`): ->> "%README%" echo. ->> "%README%" echo ```sh ->> "%README%" echo sh docs/standards/playbook/scripts/install_codex_skills.sh -all ->> "%README%" echo ``` ->> "%README%" echo. ->> "%README%" echo ## CI templates(可选) ->> "%README%" echo. ->> "%README%" echo 目标项目可复制启用的 CI 示例模板(如 Gitea Actions):`templates/ci/`。 - -set "SOURCE=%DEST_PREFIX%\\SOURCE.md" -> "%SOURCE%" echo # SOURCE ->> "%SOURCE%" echo. ->> "%SOURCE%" echo - Source: %SRC% ->> "%SOURCE%" echo - Commit: %COMMIT% ->> "%SOURCE%" echo - Date: %DATE% %TIME% ->> "%SOURCE%" echo - Langs: %LANGS_CSV% ->> "%SOURCE%" echo - Generated-by: scripts/vendor_playbook.bat - -echo Vendored snapshot -^> %DEST_PREFIX% - -set "PROJECT_AGENTS_ROOT=%DEST_ROOT_ABS%\\.agents" -set "PROJECT_AGENTS_INDEX=%PROJECT_AGENTS_ROOT%\\index.md" -if not exist "%PROJECT_AGENTS_ROOT%" mkdir "%PROJECT_AGENTS_ROOT%" -if not exist "%PROJECT_AGENTS_INDEX%" ( - > "%PROJECT_AGENTS_INDEX%" echo # .agents(多语言) - >> "%PROJECT_AGENTS_INDEX%" echo. - >> "%PROJECT_AGENTS_INDEX%" echo 本目录用于存放仓库级/语言级的代理规则集。 - >> "%PROJECT_AGENTS_INDEX%" echo. - >> "%PROJECT_AGENTS_INDEX%" echo 本项目已启用的规则集: - for %%L in (%LANGS%) do ( - if /I "%%~L"=="tsl" >> "%PROJECT_AGENTS_INDEX%" echo - .agents/tsl/:TSL 相关规则集(适用于 .tsl/.tsf) - if /I "%%~L"=="cpp" >> "%PROJECT_AGENTS_INDEX%" echo - .agents/cpp/:C++ 相关规则集(C++23,含 Modules) - if /I "%%~L"=="python" >> "%PROJECT_AGENTS_INDEX%" echo - .agents/python/:Python 相关规则集 - if /I "%%~L"=="markdown" >> "%PROJECT_AGENTS_INDEX%" echo - .agents/markdown/:Markdown 相关规则集(仅代码格式化) - ) - >> "%PROJECT_AGENTS_INDEX%" echo. - >> "%PROJECT_AGENTS_INDEX%" echo 入口建议从: - for %%L in (%LANGS%) do >> "%PROJECT_AGENTS_INDEX%" echo - .agents/%%~L/index.md - >> "%PROJECT_AGENTS_INDEX%" echo. - >> "%PROJECT_AGENTS_INDEX%" echo 标准快照文档入口: - >> "%PROJECT_AGENTS_INDEX%" echo. - >> "%PROJECT_AGENTS_INDEX%" echo - docs/standards/playbook/docs/index.md -) - -set "OLD_SYNC_ROOT=%SYNC_ROOT%" -set "SYNC_ROOT=%DEST_ROOT_ABS%" -pushd "%DEST_ROOT_ABS%" -call "%DEST_PREFIX%\\scripts\\sync_standards.bat" -langs %LANGS_CSV% -popd -set "SYNC_ROOT=%OLD_SYNC_ROOT%" - -rem Apply templates to project root if requested -if "%APPLY_TEMPLATES%"=="1" ( - echo. - echo Applying templates to project root... - - rem Apply CI templates ^(Gitea workflows^) - set "CI_SRC=%DEST_PREFIX%\templates\ci\gitea\.gitea" - if exist "!CI_SRC!" ( - if exist "%DEST_ROOT_ABS%\.gitea" ( - echo Skip ^(exists^): .gitea\ - ) else ( - xcopy "!CI_SRC!\*" "%DEST_ROOT_ABS%\.gitea\" /e /i /y >nul 2>nul - echo Applied: .gitea\ - ) - ) - - rem Apply lang-specific templates - for %%L in (%LANGS%) do ( - set "LANG_SRC=%DEST_PREFIX%\templates\%%~L" - if exist "!LANG_SRC!" ( - if /I "%%~L"=="cpp" ( - call :CopyIfNotExists "!LANG_SRC!\.clang-format" "%DEST_ROOT_ABS%\.clang-format" - call :CopyIfNotExists "!LANG_SRC!\.clangd" "%DEST_ROOT_ABS%\.clangd" - call :CopyIfNotExists "!LANG_SRC!\CMakeLists.txt" "%DEST_ROOT_ABS%\CMakeLists.txt" - call :CopyIfNotExists "!LANG_SRC!\CMakeUserPresets.json" "%DEST_ROOT_ABS%\CMakeUserPresets.json" - call :CopyIfNotExists "!LANG_SRC!\conanfile.txt" "%DEST_ROOT_ABS%\conanfile.txt" - if exist "!LANG_SRC!\conan" ( - if exist "%DEST_ROOT_ABS%\conan" ( - echo Skip ^(exists^): conan\ - ) else ( - xcopy "!LANG_SRC!\conan\*" "%DEST_ROOT_ABS%\conan\" /e /i /y >nul 2>nul - echo Applied: conan\ - ) - ) - ) - if /I "%%~L"=="python" ( - call :CopyIfNotExists "!LANG_SRC!\.editorconfig" "%DEST_ROOT_ABS%\.editorconfig" - call :CopyIfNotExists "!LANG_SRC!\.flake8" "%DEST_ROOT_ABS%\.flake8" - call :CopyIfNotExists "!LANG_SRC!\.pre-commit-config.yaml" "%DEST_ROOT_ABS%\.pre-commit-config.yaml" - call :CopyIfNotExists "!LANG_SRC!\.pylintrc" "%DEST_ROOT_ABS%\.pylintrc" - call :CopyIfNotExists "!LANG_SRC!\pyproject.toml" "%DEST_ROOT_ABS%\pyproject.toml" - if exist "!LANG_SRC!\.vscode" ( - if exist "%DEST_ROOT_ABS%\.vscode" ( - echo Skip ^(exists^): .vscode\ - ) else ( - xcopy "!LANG_SRC!\.vscode\*" "%DEST_ROOT_ABS%\.vscode\" /e /i /y >nul 2>nul - echo Applied: .vscode\ - ) - ) - ) - ) - ) - - echo Templates applied. -) - -echo Done. -endlocal -exit /b 0 - -:AppendDocsSection -set "LANG=%~1" -if /I "%LANG%"=="tsl" ( - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo ## TSL(tsl) - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo - 代码风格:`tsl/code_style.md` - >> "%DOC_INDEX%" echo - 命名规范:`tsl/naming.md` - >> "%DOC_INDEX%" echo - 语法手册:`tsl/syntax_book/index.md` - >> "%DOC_INDEX%" echo - 工具链与验证命令(模板):`tsl/toolchain.md` -) -if /I "%LANG%"=="cpp" ( - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo ## C++(cpp) - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo - 代码风格:`cpp/code_style.md` - >> "%DOC_INDEX%" echo - 命名规范:`cpp/naming.md` - >> "%DOC_INDEX%" echo - 工具链与验证命令(模板):`cpp/toolchain.md` - >> "%DOC_INDEX%" echo - 第三方依赖(Conan):`cpp/dependencies_conan.md` - >> "%DOC_INDEX%" echo - clangd 配置:`cpp/clangd.md` -) -if /I "%LANG%"=="python" ( - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo ## Python(python) - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo - 代码风格:`python/style_guide.md` - >> "%DOC_INDEX%" echo - 工具链:`python/tooling.md` - >> "%DOC_INDEX%" echo - 配置清单:`python/configuration.md` -) -if /I "%LANG%"=="markdown" ( - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo ## Markdown(markdown) - >> "%DOC_INDEX%" echo. - >> "%DOC_INDEX%" echo - 代码块与行内代码格式:`markdown/index.md` -) -exit /b 0 - -:CopyIfNotExists -set "SRC_FILE=%~1" -set "DST_FILE=%~2" -if exist "%SRC_FILE%" ( - if exist "%DST_FILE%" ( - for %%F in ("%DST_FILE%") do echo Skip ^(exists^): %%~nxF - ) else ( - copy /y "%SRC_FILE%" "%DST_FILE%" >nul - for %%F in ("%DST_FILE%") do echo Applied: %%~nxF - ) -) -exit /b 0 - -:Usage -echo Usage: -echo scripts\vendor_playbook.bat -project-root ^ ^(default: tsl^) -echo scripts\vendor_playbook.bat -project-root ^ -langs tsl,cpp -echo scripts\vendor_playbook.bat -project-root ^ -langs tsl,cpp -apply-templates -echo. -echo Options: -echo -project-root Target project root ^(required^) -echo -langs Comma/space-separated list of languages ^(default: tsl^) -echo -apply-templates Apply CI/lang templates to project root ^(skip if exists^) -exit /b 1 diff --git a/scripts/vendor_playbook.ps1 b/scripts/vendor_playbook.ps1 deleted file mode 100644 index c90029d..0000000 --- a/scripts/vendor_playbook.ps1 +++ /dev/null @@ -1,353 +0,0 @@ -# Vendor a trimmed Playbook snapshot into a target project (offline copy), -# then run sync_standards to materialize rulesets\\ and .gitattributes in -# the target project root. -# -# Usage: -# powershell -File scripts/vendor_playbook.ps1 -ProjectRoot -# powershell -File scripts/vendor_playbook.ps1 -ProjectRoot -Langs tsl,cpp -# powershell -File scripts/vendor_playbook.ps1 -ProjectRoot -Langs @("tsl","cpp") -ApplyTemplates -# -# Options: -# -ApplyTemplates Apply CI/lang templates to project root (skip if exists) -# -# Notes: -# - Snapshot is written to: \docs\standards\playbook\ -# - Existing snapshot is backed up before overwrite. -# - With -ApplyTemplates, CI and lang templates are copied to project root. - -[CmdletBinding()] -param( - [Parameter(Mandatory = $false)] - [Alias('h', '?')] - [switch]$Help, - - [Parameter(Mandatory = $false)] - [string]$ProjectRoot, - - [Parameter(Mandatory = $false)] - [string[]]$Langs, - - [Parameter(Mandatory = $false)] - [switch]$ApplyTemplates -) - -$ErrorActionPreference = "Stop" - -if ($Help) { - Write-Host "Usage:" - Write-Host " powershell -File scripts/vendor_playbook.ps1 -ProjectRoot " - Write-Host " powershell -File scripts/vendor_playbook.ps1 -ProjectRoot -Langs tsl,cpp" - Write-Host " powershell -File scripts/vendor_playbook.ps1 -ProjectRoot -Langs @('tsl','cpp') -ApplyTemplates" - Write-Host "" - Write-Host "Options:" - Write-Host " -ProjectRoot Target project root (required)." - Write-Host " -Langs Comma/space-separated list or array (default: tsl)." - Write-Host " -ApplyTemplates Apply CI/lang templates to project root." - Write-Host " -Help Show this help." - exit 0 -} - -if (-not $ProjectRoot) { - throw "ProjectRoot is required. Use -Help for usage." -} - -function Normalize-Langs([string[]]$InputLangs) { - if (-not $InputLangs -or $InputLangs.Count -eq 0) { return @("tsl") } - - $result = New-Object System.Collections.Generic.List[string] - foreach ($item in $InputLangs) { - if (-not $item) { continue } - foreach ($part in $item.Split(@(',', ' '), [System.StringSplitOptions]::RemoveEmptyEntries)) { - if (-not $part) { continue } - $result.Add($part) - } - } - if ($result.Count -eq 0) { return @("tsl") } - return $result.ToArray() -} - -$Langs = Normalize-Langs $Langs - -foreach ($lang in $Langs) { - if ($lang -match '[\\/]' -or $lang -match '\.\.') { - throw "Invalid lang=$lang" - } -} - -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path -$Src = (Resolve-Path (Join-Path $ScriptDir "..")).Path - -New-Item -ItemType Directory -Path $ProjectRoot -Force | Out-Null -$DestRootAbs = (Resolve-Path $ProjectRoot).Path - -$StandardsDir = Join-Path $DestRootAbs "docs/standards" -$DestPrefix = Join-Path $StandardsDir "playbook" - -New-Item -ItemType Directory -Path $StandardsDir -Force | Out-Null - -$timestamp = Get-Date -Format "yyyyMMddHHmmss" -if (Test-Path $DestPrefix) { - $bak = Join-Path $StandardsDir "playbook.bak.$timestamp" - Move-Item $DestPrefix $bak - Write-Host "Backed up existing snapshot -> docs\\standards\\$(Split-Path -Leaf $bak)" -} - -New-Item -ItemType Directory -Path $DestPrefix -Force | Out-Null - -Copy-Item (Join-Path $Src ".gitattributes") (Join-Path $DestPrefix ".gitattributes") -Force -Copy-Item (Join-Path $Src "scripts") $DestPrefix -Recurse -Force -Copy-Item (Join-Path $Src "codex") $DestPrefix -Recurse -Force -Copy-Item (Join-Path $Src "SKILLS.md") (Join-Path $DestPrefix "SKILLS.md") -Force - -$DocsDir = Join-Path $DestPrefix "docs" -New-Item -ItemType Directory -Path $DocsDir -Force | Out-Null -Copy-Item (Join-Path $Src "docs/common") $DocsDir -Recurse -Force - -$AgentsDir = Join-Path $DestPrefix "rulesets" -New-Item -ItemType Directory -Path $AgentsDir -Force | Out-Null -Copy-Item (Join-Path $Src "rulesets/index.md") (Join-Path $AgentsDir "index.md") -Force - -$TemplatesDir = Join-Path $DestPrefix "templates" -New-Item -ItemType Directory -Path $TemplatesDir -Force | Out-Null - -$ciTplSrc = Join-Path (Join-Path $Src "templates") "ci" -if (Test-Path $ciTplSrc) { - Copy-Item $ciTplSrc $TemplatesDir -Recurse -Force -} - -foreach ($lang in $Langs) { - $docsSrc = Join-Path (Join-Path $Src "docs") $lang - if (-not (Test-Path $docsSrc)) { throw "Docs not found for lang=$lang ($docsSrc)" } - Copy-Item $docsSrc $DocsDir -Recurse -Force - - $agentsSrc = Join-Path (Join-Path $Src "rulesets") $lang - if (-not (Test-Path $agentsSrc)) { throw "Agents ruleset not found for lang=$lang ($agentsSrc)" } - Copy-Item $agentsSrc $AgentsDir -Recurse -Force - - $tplSrc = Join-Path (Join-Path $Src "templates") $lang - if (Test-Path $tplSrc) { - Copy-Item $tplSrc $TemplatesDir -Recurse -Force - } -} - -$langsCsv = ($Langs -join ",") - -$docLines = New-Object System.Collections.Generic.List[string] -$docLines.Add("# 文档导航(Docs Index)") -$docLines.Add("") -$docLines.Add("本快照为裁剪版 Playbook(langs: $langsCsv)。") -$docLines.Add("") -$docLines.Add("## 跨语言(common)") -$docLines.Add("") -$docLines.Add('- 提交信息与版本号:`common/commit_message.md`') - -function Append-DocsSection([string]$Lang) { - switch ($Lang) { - "tsl" { - $docLines.Add("") - $docLines.Add("## TSL(tsl)") - $docLines.Add("") - $docLines.Add('- 代码风格:`tsl/code_style.md`') - $docLines.Add('- 命名规范:`tsl/naming.md`') - $docLines.Add('- 语法手册:`tsl/syntax_book/index.md`') - $docLines.Add('- 工具链与验证命令(模板):`tsl/toolchain.md`') - break - } - "cpp" { - $docLines.Add("") - $docLines.Add("## C++(cpp)") - $docLines.Add("") - $docLines.Add('- 代码风格:`cpp/code_style.md`') - $docLines.Add('- 命名规范:`cpp/naming.md`') - $docLines.Add('- 工具链与验证命令(模板):`cpp/toolchain.md`') - $docLines.Add('- 第三方依赖(Conan):`cpp/dependencies_conan.md`') - $docLines.Add('- clangd 配置:`cpp/clangd.md`') - break - } - "python" { - $docLines.Add("") - $docLines.Add("## Python(python)") - $docLines.Add("") - $docLines.Add('- 代码风格:`python/style_guide.md`') - $docLines.Add('- 工具链:`python/tooling.md`') - $docLines.Add('- 配置清单:`python/configuration.md`') - break - } - "markdown" { - $docLines.Add("") - $docLines.Add("## Markdown(markdown)") - $docLines.Add("") - $docLines.Add('- 代码块与行内代码格式:`markdown/index.md`') - break - } - } -} - -foreach ($lang in $Langs) { - Append-DocsSection $lang -} - -($docLines -join "`n") | Set-Content -Path (Join-Path $DocsDir "index.md") -Encoding UTF8 - -$commit = "" -try { - $commit = (git -C $Src rev-parse HEAD 2>$null) -} catch { - $commit = "" -} -if (-not $commit) { $commit = "N/A" } - -@" -# Playbook(裁剪快照) - -本目录为从 Playbook vendoring 的裁剪快照(langs: $langsCsv)。 - -## 使用 - -在目标项目根目录执行(多语言一次同步): - -```sh -sh docs/standards/playbook/scripts/sync_standards.sh -langs $langsCsv -``` - -查看规范入口: - -- `docs/standards/playbook/docs/index.md` -- `.agents/index.md` - -## Codex skills(可选) - -安装到本机(需要先在 `~/.codex/config.toml` 启用 skills;见 `docs/standards/playbook/SKILLS.md`): - -```sh -sh docs/standards/playbook/scripts/install_codex_skills.sh -all -``` - -## CI templates(可选) - -目标项目可复制启用的 CI 示例模板(如 Gitea Actions):`templates/ci/`。 -"@ | Set-Content -Path (Join-Path $DestPrefix "README.md") -Encoding UTF8 - -@" -# SOURCE - -- Source: $Src -- Commit: $commit -- Date: $(Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") -- Langs: $langsCsv -- Generated-by: scripts/vendor_playbook.ps1 -"@ | Set-Content -Path (Join-Path $DestPrefix "SOURCE.md") -Encoding UTF8 - -Write-Host "Vendored snapshot -> $DestPrefix" - -$ProjectAgentsRoot = Join-Path $DestRootAbs ".agents" -$ProjectAgentsIndex = Join-Path $ProjectAgentsRoot "index.md" -New-Item -ItemType Directory -Path $ProjectAgentsRoot -Force | Out-Null -if (-not (Test-Path $ProjectAgentsIndex)) { - $agentLines = New-Object System.Collections.Generic.List[string] - $agentLines.Add("# .agents(多语言)") - $agentLines.Add("") - $agentLines.Add("本目录用于存放仓库级/语言级的代理规则集。") - $agentLines.Add("") - $agentLines.Add("本项目已启用的规则集:") - foreach ($lang in $Langs) { - switch ($lang) { - "tsl" { $agentLines.Add("- .agents/tsl/:TSL 相关规则集(适用于 .tsl/.tsf)"); break } - "cpp" { $agentLines.Add("- .agents/cpp/:C++ 相关规则集(C++23,含 Modules)"); break } - "python" { $agentLines.Add("- .agents/python/:Python 相关规则集"); break } - "markdown" { $agentLines.Add("- .agents/markdown/:Markdown 相关规则集(仅代码格式化)"); break } - } - } - $agentLines.Add("") - $agentLines.Add("入口建议从:") - foreach ($lang in $Langs) { $agentLines.Add("- .agents/$lang/index.md") } - $agentLines.Add("") - $agentLines.Add("标准快照文档入口:") - $agentLines.Add("") - $agentLines.Add("- docs/standards/playbook/docs/index.md") - ($agentLines -join "`n") | Set-Content -Path $ProjectAgentsIndex -Encoding UTF8 -} - -$oldSyncRoot = $env:SYNC_ROOT -$env:SYNC_ROOT = $DestRootAbs -try { - & (Join-Path $DestPrefix "scripts/sync_standards.ps1") -Langs $Langs -} finally { - $env:SYNC_ROOT = $oldSyncRoot -} - -# Apply templates to project root if requested -if ($ApplyTemplates) { - Write-Host "" - Write-Host "Applying templates to project root..." - - # Helper function: copy file if not exists - function Copy-IfNotExists { - param([string]$SrcFile, [string]$DstFile) - if (Test-Path $SrcFile) { - if (Test-Path $DstFile) { - Write-Host " Skip (exists): $(Split-Path -Leaf $DstFile)" - } else { - Copy-Item $SrcFile $DstFile -Force - Write-Host " Applied: $(Split-Path -Leaf $DstFile)" - } - } - } - - # Apply CI templates (Gitea workflows) - $ciSrc = Join-Path $DestPrefix "templates/ci/gitea/.gitea" - if (Test-Path $ciSrc) { - $ciDst = Join-Path $DestRootAbs ".gitea" - if (Test-Path $ciDst) { - Write-Host " Skip (exists): .gitea/" - } else { - Copy-Item $ciSrc $ciDst -Recurse -Force - Write-Host " Applied: .gitea/" - } - } - - # Apply lang-specific templates - foreach ($lang in $Langs) { - $langSrc = Join-Path $DestPrefix "templates/$lang" - if (-not (Test-Path $langSrc)) { continue } - - switch ($lang) { - "cpp" { - Copy-IfNotExists (Join-Path $langSrc ".clang-format") (Join-Path $DestRootAbs ".clang-format") - Copy-IfNotExists (Join-Path $langSrc ".clangd") (Join-Path $DestRootAbs ".clangd") - Copy-IfNotExists (Join-Path $langSrc "CMakeLists.txt") (Join-Path $DestRootAbs "CMakeLists.txt") - Copy-IfNotExists (Join-Path $langSrc "CMakeUserPresets.json") (Join-Path $DestRootAbs "CMakeUserPresets.json") - Copy-IfNotExists (Join-Path $langSrc "conanfile.txt") (Join-Path $DestRootAbs "conanfile.txt") - $conanSrc = Join-Path $langSrc "conan" - $conanDst = Join-Path $DestRootAbs "conan" - if ((Test-Path $conanSrc) -and -not (Test-Path $conanDst)) { - Copy-Item $conanSrc $conanDst -Recurse -Force - Write-Host " Applied: conan/" - } elseif (Test-Path $conanDst) { - Write-Host " Skip (exists): conan/" - } - break - } - "python" { - Copy-IfNotExists (Join-Path $langSrc ".editorconfig") (Join-Path $DestRootAbs ".editorconfig") - Copy-IfNotExists (Join-Path $langSrc ".flake8") (Join-Path $DestRootAbs ".flake8") - Copy-IfNotExists (Join-Path $langSrc ".pre-commit-config.yaml") (Join-Path $DestRootAbs ".pre-commit-config.yaml") - Copy-IfNotExists (Join-Path $langSrc ".pylintrc") (Join-Path $DestRootAbs ".pylintrc") - Copy-IfNotExists (Join-Path $langSrc "pyproject.toml") (Join-Path $DestRootAbs "pyproject.toml") - $vscodeSrc = Join-Path $langSrc ".vscode" - $vscodeDst = Join-Path $DestRootAbs ".vscode" - if ((Test-Path $vscodeSrc) -and -not (Test-Path $vscodeDst)) { - Copy-Item $vscodeSrc $vscodeDst -Recurse -Force - Write-Host " Applied: .vscode/" - } elseif (Test-Path $vscodeDst) { - Write-Host " Skip (exists): .vscode/" - } - break - } - } - } - - Write-Host "Templates applied." -} - -Write-Host "Done." diff --git a/scripts/vendor_playbook.sh b/scripts/vendor_playbook.sh deleted file mode 100644 index e2b3ee0..0000000 --- a/scripts/vendor_playbook.sh +++ /dev/null @@ -1,392 +0,0 @@ -#!/usr/bin/env sh -set -eu - -# Vendor a trimmed Playbook snapshot into a target project (offline copy), -# then run sync_standards to materialize .agents// and .gitattributes in -# the target project root. -# -# Usage: -# sh scripts/vendor_playbook.sh -project-root # default: tsl -# sh scripts/vendor_playbook.sh -project-root -langs tsl,cpp -# sh scripts/vendor_playbook.sh -project-root -langs tsl,cpp -apply-templates -# -# Options: -# -project-root PATH Target project root (required) -# -langs L1,L2 Comma/space-separated list of languages (default: tsl) -# -apply-templates Apply CI/lang templates to project root (skip if exists) -# -# Notes: -# - Snapshot is written to: /docs/standards/playbook/ -# - Existing snapshot is backed up before overwrite. -# - Ruleset templates from rulesets/ will be copied to snapshot for sync_standards use. -# - With -apply-templates, CI and lang templates are copied to project root. - -SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)" -SRC="$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd -P)" - -usage() { - cat <<'EOF' >&2 -Usage: - sh scripts/vendor_playbook.sh -project-root # default: tsl - sh scripts/vendor_playbook.sh -project-root -langs tsl,cpp - sh scripts/vendor_playbook.sh -project-root -langs tsl,cpp -apply-templates - -Options: - -project-root PATH Target project root (required) - -langs L1,L2 Comma/space-separated list of languages (default: tsl) - -apply-templates Apply CI/lang templates to project root (skip if exists) - -h, -help Show this help -EOF -} - -if [ "${1:-}" = "-h" ] || [ "${1:-}" = "-help" ]; then - usage - exit 0 -fi - -PROJECT_ROOT="" -langs="" -APPLY_TEMPLATES=0 - -# Parse arguments -while [ $# -gt 0 ]; do - case "$1" in - -project-root) - if [ $# -lt 2 ] || [ -z "${2:-}" ]; then - echo "ERROR: -project-root requires a path." >&2 - usage - exit 1 - fi - PROJECT_ROOT="$2" - shift 2 - ;; - -langs) - if [ $# -lt 2 ] || [ -z "${2:-}" ]; then - echo "ERROR: -langs requires a value." >&2 - usage - exit 1 - fi - langs="$2" - shift 2 - ;; - -apply-templates) - APPLY_TEMPLATES=1 - shift - ;; - -*) - echo "ERROR: Unknown option: $1" >&2 - exit 1 - ;; - *) - echo "ERROR: positional args are not supported; use -project-root/-langs." >&2 - exit 1 - ;; - esac -done - -if [ -z "$PROJECT_ROOT" ]; then - echo "ERROR: -project-root is required." >&2 - usage - exit 1 -fi - -if [ -z "${langs:-}" ]; then - langs="tsl" -fi - -timestamp="$(date +%Y%m%d%H%M%S 2>/dev/null || echo bak)" - -if [ ! -d "$PROJECT_ROOT" ]; then - echo "ERROR: project root does not exist: $PROJECT_ROOT" >&2 - exit 1 -fi -PROJECT_ROOT_ABS="$(CDPATH= cd -- "$PROJECT_ROOT" && pwd -P)" -DEST_PREFIX="$PROJECT_ROOT_ABS/docs/standards/playbook" -DEST_STANDARDS="$PROJECT_ROOT_ABS/docs/standards" - -mkdir -p "$DEST_STANDARDS" - -if [ -e "$DEST_PREFIX" ]; then - mv "$DEST_PREFIX" "$DEST_STANDARDS/playbook.bak.$timestamp" - echo "Backed up existing snapshot -> docs/standards/playbook.bak.$timestamp" -fi - -mkdir -p "$DEST_PREFIX" - -# Always include: scripts + gitattributes + docs/common + codex/skills -cp "$SRC/.gitattributes" "$DEST_PREFIX/.gitattributes" -cp -R "$SRC/scripts" "$DEST_PREFIX/" -cp -R "$SRC/codex" "$DEST_PREFIX/" -cp "$SRC/SKILLS.md" "$DEST_PREFIX/SKILLS.md" - -mkdir -p "$DEST_PREFIX/docs" -cp -R "$SRC/docs/common" "$DEST_PREFIX/docs/" - -# Copy rulesets -mkdir -p "$DEST_PREFIX/rulesets" -cp "$SRC/rulesets/index.md" "$DEST_PREFIX/rulesets/index.md" - -mkdir -p "$DEST_PREFIX/templates" -if [ -d "$SRC/templates/ci" ]; then - cp -R "$SRC/templates/ci" "$DEST_PREFIX/templates/" -fi - -old_ifs="${IFS}" -IFS=', ' -set -- $langs -IFS="${old_ifs}" - -langs_csv="" -for lang in "$@"; do - [ -n "$lang" ] || continue - - case "$lang" in - ""|*/*|*\\*|*..*) - echo "ERROR: invalid lang=$lang" >&2 - exit 1 - ;; - esac - - if [ ! -d "$SRC/docs/$lang" ]; then - echo "ERROR: docs not found for lang=$lang ($SRC/docs/$lang)" >&2 - exit 1 - fi - - if [ ! -d "$SRC/rulesets/$lang" ]; then - echo "ERROR: rulesets not found for lang=$lang ($SRC/rulesets/$lang)" >&2 - exit 1 - fi - - cp -R "$SRC/docs/$lang" "$DEST_PREFIX/docs/" - cp -R "$SRC/rulesets/$lang" "$DEST_PREFIX/rulesets/" - if [ -d "$SRC/templates/$lang" ]; then - cp -R "$SRC/templates/$lang" "$DEST_PREFIX/templates/" - fi - - if [ -n "$langs_csv" ]; then - langs_csv="$langs_csv,$lang" - else - langs_csv="$lang" - fi -done - -cat >"$DEST_PREFIX/docs/index.md" <>"$DEST_PREFIX/docs/index.md" <<'EOF' - -## TSL(tsl) - -- 代码风格:`tsl/code_style.md` -- 命名规范:`tsl/naming.md` -- 语法手册:`tsl/syntax_book/index.md` -- 工具链与验证命令(模板):`tsl/toolchain.md` -EOF - ;; - cpp) - cat >>"$DEST_PREFIX/docs/index.md" <<'EOF' - -## C++(cpp) - -- 代码风格:`cpp/code_style.md` -- 命名规范:`cpp/naming.md` -- 工具链与验证命令(模板):`cpp/toolchain.md` -- 第三方依赖(Conan):`cpp/dependencies_conan.md` -- clangd 配置:`cpp/clangd.md` -EOF - ;; - python) - cat >>"$DEST_PREFIX/docs/index.md" <<'EOF' - -## Python(python) - -- 代码风格:`python/style_guide.md` -- 工具链:`python/tooling.md` -- 配置清单:`python/configuration.md` -EOF - ;; - markdown) - cat >>"$DEST_PREFIX/docs/index.md" <<'EOF' - -## Markdown(markdown) - -- 代码块与行内代码格式:`markdown/index.md` -EOF - ;; - esac -} - -for lang in "$@"; do - [ -n "$lang" ] || continue - append_docs_section "$lang" -done - -commit="" -if command -v git >/dev/null 2>&1; then - commit="$(git -C "$SRC" rev-parse HEAD 2>/dev/null || true)" -fi - -cat >"$DEST_PREFIX/README.md" <"$DEST_PREFIX/SOURCE.md" </dev/null || date) -- Langs: ${langs_csv} -- Generated-by: scripts/vendor_playbook.sh -EOF - -echo "Vendored snapshot -> $DEST_PREFIX" - -PROJECT_AGENTS_ROOT="$PROJECT_ROOT_ABS/.agents" -PROJECT_AGENTS_INDEX="$PROJECT_AGENTS_ROOT/index.md" -mkdir -p "$PROJECT_AGENTS_ROOT" -if [ ! -f "$PROJECT_AGENTS_INDEX" ]; then - cat >"$PROJECT_AGENTS_INDEX" <>"$PROJECT_AGENTS_INDEX" ;; - cpp) printf '%s\n' "- .agents/cpp/:C++ 相关规则集(C++23,含 Modules)" >>"$PROJECT_AGENTS_INDEX" ;; - python) printf '%s\n' "- .agents/python/:Python 相关规则集" >>"$PROJECT_AGENTS_INDEX" ;; - markdown) printf '%s\n' "- .agents/markdown/:Markdown 相关规则集(仅代码格式化)" >>"$PROJECT_AGENTS_INDEX" ;; - esac - done - - cat >>"$PROJECT_AGENTS_INDEX" <<'EOF' - -入口建议从: -EOF - - for lang in "$@"; do - printf '%s\n' "- .agents/$lang/index.md" >>"$PROJECT_AGENTS_INDEX" - done - - cat >>"$PROJECT_AGENTS_INDEX" <<'EOF' - -标准快照文档入口: - -- docs/standards/playbook/docs/index.md -EOF -fi - -SYNC_ROOT="$PROJECT_ROOT_ABS" sh "$DEST_PREFIX/scripts/sync_standards.sh" -langs "$langs_csv" - -# Apply templates to project root if requested -if [ "$APPLY_TEMPLATES" -eq 1 ]; then - echo "" - echo "Applying templates to project root..." - - # Helper function: copy file if not exists - copy_if_not_exists() { - src_file="$1" - dst_file="$2" - if [ -f "$src_file" ]; then - if [ -f "$dst_file" ]; then - echo " Skip (exists): $(basename "$dst_file")" - else - cp "$src_file" "$dst_file" - echo " Applied: $(basename "$dst_file")" - fi - fi - } - - # Apply CI templates (Gitea workflows) - CI_SRC="$DEST_PREFIX/templates/ci/gitea" - if [ -d "$CI_SRC/.gitea" ]; then - if [ -d "$PROJECT_ROOT_ABS/.gitea" ]; then - echo " Skip (exists): .gitea/" - else - cp -R "$CI_SRC/.gitea" "$PROJECT_ROOT_ABS/" - echo " Applied: .gitea/" - fi - fi - - # Apply lang-specific templates - for lang in "$@"; do - [ -n "$lang" ] || continue - LANG_SRC="$DEST_PREFIX/templates/$lang" - [ -d "$LANG_SRC" ] || continue - - case "$lang" in - cpp) - copy_if_not_exists "$LANG_SRC/.clang-format" "$PROJECT_ROOT_ABS/.clang-format" - copy_if_not_exists "$LANG_SRC/.clangd" "$PROJECT_ROOT_ABS/.clangd" - copy_if_not_exists "$LANG_SRC/CMakeLists.txt" "$PROJECT_ROOT_ABS/CMakeLists.txt" - copy_if_not_exists "$LANG_SRC/CMakeUserPresets.json" "$PROJECT_ROOT_ABS/CMakeUserPresets.json" - copy_if_not_exists "$LANG_SRC/conanfile.txt" "$PROJECT_ROOT_ABS/conanfile.txt" - if [ -d "$LANG_SRC/conan" ] && [ ! -d "$PROJECT_ROOT_ABS/conan" ]; then - cp -R "$LANG_SRC/conan" "$PROJECT_ROOT_ABS/" - echo " Applied: conan/" - elif [ -d "$PROJECT_ROOT_ABS/conan" ]; then - echo " Skip (exists): conan/" - fi - ;; - python) - copy_if_not_exists "$LANG_SRC/.editorconfig" "$PROJECT_ROOT_ABS/.editorconfig" - copy_if_not_exists "$LANG_SRC/.flake8" "$PROJECT_ROOT_ABS/.flake8" - copy_if_not_exists "$LANG_SRC/.pre-commit-config.yaml" "$PROJECT_ROOT_ABS/.pre-commit-config.yaml" - copy_if_not_exists "$LANG_SRC/.pylintrc" "$PROJECT_ROOT_ABS/.pylintrc" - copy_if_not_exists "$LANG_SRC/pyproject.toml" "$PROJECT_ROOT_ABS/pyproject.toml" - if [ -d "$LANG_SRC/.vscode" ] && [ ! -d "$PROJECT_ROOT_ABS/.vscode" ]; then - cp -R "$LANG_SRC/.vscode" "$PROJECT_ROOT_ABS/" - echo " Applied: .vscode/" - elif [ -d "$PROJECT_ROOT_ABS/.vscode" ]; then - echo " Skip (exists): .vscode/" - fi - ;; - esac - done - - echo "Templates applied." -fi - -echo "Done." diff --git a/templates/README.md b/templates/README.md index 8a1a4e8..eb6488d 100644 --- a/templates/README.md +++ b/templates/README.md @@ -37,42 +37,31 @@ templates/ ## 快速部署 -使用 `sync_templates` 脚本一键部署: +使用统一入口 `playbook.py`: -**Linux/macOS**: +```toml +# playbook.toml +[playbook] +project_root = "/path/to/project" + +[sync_templates] +project_name = "MyProject" +full = false +``` ```bash -# 基础部署 -sh scripts/sync_templates.sh -project-root /path/to/project - -# 追加完整框架到已有 AGENTS.md -sh scripts/sync_templates.sh -project-root /path/to/project -full +python docs/standards/playbook/scripts/playbook.py -config playbook.toml ``` -**Windows PowerShell**: - -```powershell -# 基础部署 -.\scripts\sync_templates.ps1 -ProjectRoot C:\path\to\project - -# 追加完整框架 -.\scripts\sync_templates.ps1 -ProjectRoot C:\path\to\project -Full -``` - -**Windows CMD**: - -```cmd -scripts\sync_templates.bat -project-root C:\path\to\project -scripts\sync_templates.bat -project-root C:\path\to\project -full -``` +参数说明见 `docs/standards/playbook/playbook.toml.example`。 ### 部署行为 - **新项目**:创建完整的 AGENTS.md、AGENT_RULES.md、memory-bank/、docs/prompts/ - **已有 AGENTS.md**: - 默认:追加路由链接(``) - - `-full`:追加完整框架(规则优先级 + 路由 + 新会话开始时) -- **其他文件**:如果已存在则跳过(使用 `-force` 覆盖) + - `full = true`:追加完整框架(规则优先级 + 路由 + 新会话开始时) +- **其他文件**:如果已存在则跳过(使用 `force = true` 覆盖) - **占位符替换**:自动替换 `{{DATE}}` 为当前日期 ### 部署后的目录结构 @@ -115,13 +104,13 @@ project/ 项目上下文文档,用于让 AI 快速理解项目: -| 文件 | 用途 | -| --------------------------------- | -------------------- | -| `project-brief.template.md` | 项目定位、边界、约束 | -| `tech-stack.template.md` | 技术栈、工具链、环境 | -| `architecture.template.md` | 架构设计、模块职责 | -| `progress.template.md` | 开发进度追踪 | -| `decisions.template.md` | 架构决策记录(ADR) | +| 文件 | 用途 | +| --------------------------- | -------------------- | +| `project-brief.template.md` | 项目定位、边界、约束 | +| `tech-stack.template.md` | 技术栈、工具链、环境 | +| `architecture.template.md` | 架构设计、模块职责 | +| `progress.template.md` | 开发进度追踪 | +| `decisions.template.md` | 架构决策记录(ADR) | ### prompts/ @@ -173,15 +162,15 @@ project/ **playbook 标记**(用于自动更新): -| 标记 | 用途 | 管理脚本 | -| --------------------------------------- | ---------------------- | -------------- | -| `` | 语言规则链接 | sync_standards | -| `` | 路由链接(默认追加) | sync_templates | -| `` | 完整框架(-full 追加) | sync_templates | +| 标记 | 用途 | 管理入口 | +| --------------------------------------- | --------------------- | ------------------------------ | +| `` | 语言规则链接 | playbook.py `[sync_standards]` | +| `` | 路由链接(默认追加) | playbook.py `[sync_templates]` | +| `` | 完整框架(full 追加) | playbook.py `[sync_templates]` | ### ci/、cpp/、python/ -语言和 CI 配置模板。通过 `vendor_playbook -apply-templates` 部署: +语言和 CI 配置模板。通过 playbook.py 的 `[sync_templates]` 部署: | 目录 | 内容 | 部署位置 | | ----------- | ----------------------------------------- | ---------- | @@ -191,9 +180,17 @@ project/ **使用方式**: +```toml +# playbook.toml +[playbook] +project_root = "/path/to/project" + +[sync_templates] +project_name = "MyProject" +``` + ```bash -# vendor_playbook 时一并部署 -sh scripts/vendor_playbook.sh -project-root /path/to/project -langs tsl,cpp -apply-templates +python docs/standards/playbook/scripts/playbook.py -config playbook.toml ``` ## 与 playbook 其他部分的关系 @@ -205,22 +202,18 @@ playbook/ ├── docs/ # 权威静态文档 ├── templates/ # 本目录:项目架构模板 → 部署到 memory-bank/ 等 └── scripts/ - ├── sync_standards.* # 同步 .agents/ 和 .gitattributes - └── sync_templates.* # 同步 memory-bank/、docs/prompts/、AGENT_RULES.md + └── playbook.py # 统一入口:vendor/sync_templates/sync_standards/... ``` ## 完整部署流程 ```bash -# 1. 部署项目架构模板 -sh scripts/sync_templates.sh -project-root /path/to/project +# 1. 准备配置并执行统一入口 +python docs/standards/playbook/scripts/playbook.py -config playbook.toml -# 2. 部署语言规则 -sh scripts/sync_standards.sh -langs tsl # 或其他语言 +# 2. 编辑 memory-bank/*.md 填写项目信息 -# 3. 编辑 memory-bank/*.md 填写项目信息 - -# 4. 替换剩余的 {{PLACEHOLDER}} 占位符 +# 3. 替换剩余的 {{PLACEHOLDER}} 占位符 ``` --- diff --git a/tests/README.md b/tests/README.md index cebfbc1..0238ba1 100644 --- a/tests/README.md +++ b/tests/README.md @@ -1,18 +1,14 @@ # 🧪 Playbook 测试套件 -本目录包含 Playbook 项目的完整测试框架,用于验证脚本、模板和文档的正确性。 +本目录包含 Playbook 项目的测试,用于验证 CLI、模板与文档链接。 ## 📋 目录结构 ```txt tests/ ├── README.md # 本文件:测试文档 -├── scripts/ # Shell 脚本测试(bats) -│ ├── test_sync_standards.bats # sync_standards.sh 测试 -│ ├── test_sync_templates.bats # sync_templates.sh 测试 -│ ├── test_vendor_playbook.bats # vendor_playbook.sh 测试 -│ ├── test_install_codex_skills.bats # install_codex_skills.sh 测试 -│ └── test_windows_script_lints.bats # Windows 脚本 lint 测试 +├── cli/ # Python CLI 测试(unittest) +│ └── test_playbook_cli.py # playbook.py 基础功能测试 ├── templates/ # 模板验证测试 │ ├── validate_python_templates.sh # Python 模板验证 │ ├── validate_cpp_templates.sh # C++ 模板验证 @@ -24,33 +20,24 @@ tests/ ## 🚀 快速开始 -### 本地运行所有测试 - ```bash # 进入 playbook 根目录 cd /path/to/playbook -# 1. 运行 Shell 脚本测试(需要 bats) -sudo apt-get install bats # Ubuntu/Debian -cd tests/scripts -bats test_sync_standards.bats -bats test_sync_templates.bats -bats test_vendor_playbook.bats -bats test_install_codex_skills.bats +# 1. 运行 Python CLI 测试 +python -m unittest discover -s tests/cli -v # 2. 运行模板验证测试 -cd tests/templates -sh validate_python_templates.sh -sh validate_cpp_templates.sh -sh validate_ci_templates.sh -sh validate_project_templates.sh +sh tests/templates/validate_python_templates.sh +sh tests/templates/validate_cpp_templates.sh +sh tests/templates/validate_ci_templates.sh +sh tests/templates/validate_project_templates.sh -# 3. 运行集成测试 -cd tests/integration -sh check_doc_links.sh +# 3. 运行文档链接检查 +sh tests/integration/check_doc_links.sh ``` -### CI 自动化测试 +## 🧭 CI 自动化测试 测试套件通过 Gitea Actions 自动运行(见 `.gitea/workflows/test.yml`): @@ -59,426 +46,21 @@ sh check_doc_links.sh - Pull Request 到 `main` 分支 - 手动触发(workflow_dispatch) - **运行平台**:ubuntu-22.04 -- **并行策略**:使用 matrix 策略并行运行多个测试组 ## 📚 测试详解 -### 1. Shell 脚本测试 (scripts/) +### 1. Python CLI 测试 (cli/) -使用 [bats-core](https://github.com/bats-core/bats-core) 框架测试 shell 脚本。 +使用 `unittest` 运行,覆盖 `scripts/playbook.py` 的核心行为: -#### test_sync_standards.bats - -测试 `scripts/sync_standards.sh` 脚本的功能: - -- **基础功能**: - - 脚本存在且可执行 - - 单语言同步(`-langs tsl`/`-langs cpp`) - - 多语言同步(`-langs tsl,cpp`) -- **.gitattributes 同步**: - - 默认模式追加缺失规则 - - 保留现有内容 - - 更新已存在的 managed block -- **AGENTS.md 处理**: - - 不存在时自动创建 - - 已存在时不覆盖 -- **备份功能**: - - 更新前创建备份 -- **错误处理**: - - 未找到 playbook 快照时报错 - - 无效语言参数时报错 -- **环境变量**: - - `SYNC_GITATTR_MODE` 配置 -- **幂等性**: - - 多次执行结果一致 - -#### test_sync_templates.bats - -测试 `scripts/sync_templates.sh` 脚本的功能: - -- **基础同步**: - - 同步 memory-bank/ 与 docs/prompts/ - - 创建 AGENTS.md / AGENT_RULES.md -- **占位符替换**: - - `{{PROJECT_NAME}}` 与 `{{DATE}}` -- **目录覆盖策略**: - - 无 `-force` 时不覆盖已有目录 - - `-force` 时覆盖并备份 -- **AGENTS.md 更新**: - - `-full` 更新 framework 区块 - -#### test_vendor_playbook.bats - -测试 `scripts/vendor_playbook.sh` 脚本的功能: - -- **基础功能**: - - 单语言 vendoring(`-langs tsl`) - - 多语言 vendoring(`-langs tsl,cpp`) -- **自动同步**: - - 自动执行 sync_standards -- **SOURCE.md 生成**: - - 包含来源信息 - - 包含 commit hash - - 包含时间戳 -- **裁剪功能**: - - 仅包含指定语言 - - 始终包含 common 目录 - - 包含对应模板文件 -- **目标目录处理**: - - 已存在时覆盖更新 - - 创建必要的父目录 -- **错误处理**: - - 目标目录不存在时报错 - - 无效语言参数时报错 -- **完整性验证**: - - 所有必要文件已复制 - - 脚本可执行 -- **幂等性**: - - 多次 vendor 结果一致 - -#### test_install_codex_skills.bats - -测试 `scripts/install_codex_skills.sh` 脚本的功能: - -- **基础功能**: - - 脚本存在且可执行 -- **安装功能**: - - 创建 skills 目录 - - 复制 skill 目录(包含 `SKILL.md`) - - 支持 `-skills` 指定单个 skill 安装 - - 支持 `-all` 安装全部 skills - - 同名目录安装前创建备份 -- **错误处理**: - - 指定不存在的 skill 报错 - - 未传 `-all/-skills` 报错 -- **幂等性**: - - 多次安装结果一致 - -#### test_windows_script_lints.bats - -测试 Windows 脚本的基础 lint 规则: - -- **PowerShell**: - - here-string 终止符不与管道同一行 - - Help 参数别名不与参数名冲突 -- **Batch**: - - `:show_help` 标签不应阻断参数解析 +- CLI 参数解析与帮助信息 +- TOML 配置解析与动作顺序 +- vendor/sync/install 等基础动作落地 ### 2. 模板验证测试 (templates/) -验证项目模板文件的正确性和完整性。 +通过脚本检查各类模板文件结构与关键字段(如占位符、关键配置项)。 -#### validate_python_templates.sh +### 3. 文档链接检查 (integration/) -验证 `templates/python/` 目录下的 Python 模板: - -- **pyproject.toml**: - - 文件存在 - - TOML 语法正确 - - 包含必要配置节(tool.black, tool.isort, tool.pytest.ini_options) - - black line-length 配置正确 - - isort profile 配置正确 -- **.flake8**: - - 文件存在 - - 包含 [flake8] 配置 - - 配置了 max-line-length - - 配置了错误忽略规则 -- **.pylintrc**: - - 文件存在 - - 包含必要配置节(MASTER, MESSAGES CONTROL, FORMAT) - - 配置了 max-line-length -- **.pre-commit-config.yaml**: - - 文件存在 - - YAML 语法正确 - - 包含 repos 配置 - - 配置了常用 hooks(black, isort, flake8) -- **.editorconfig**: - - 文件存在 - - 包含 root = true - - 包含 Python 文件配置 -- **.vscode/settings.json**: - - 文件存在 - - JSON 语法正确 - - 包含 Python 配置 - -#### validate_cpp_templates.sh - -验证 `templates/cpp/` 目录下的 C++ 模板: - -- **CMakeLists.txt**: - - 文件存在 - - CMake 基础语法正确 - - 配置了 C++23 标准 - - 启用了 C++ Modules 扫描(可选) - - 启用了 import std; 支持(可选) - - 启用了编译命令导出(用于 clangd) -- **.clang-format**: - - 文件存在 - - YAML 语法正确 - - 配置了 Language: Cpp - - 基于某个风格(BasedOnStyle) - - 配置了 C++ 标准 - - 配置了缩进宽度 -- **.clangd**: - - 文件存在 - - YAML 语法正确 - - 包含 CompileFlags 配置 - - 配置了 -std=c++23 - - 配置了 CompilationDatabase 路径 - - 配置了 Index 选项 -- **conanfile.txt**: - - 文件存在 - - 包含 [requires] 配置(可选) - - 包含 [generators] 配置 - - 配置了 CMakeDeps 生成器 - - 配置了 CMakeToolchain 生成器 - - 包含 [options] 配置 -- **CMakeUserPresets.json**: - - 文件存在 - - JSON 语法正确 - - 包含 version 字段 - - 包含 configurePresets - - 包含 buildPresets - -#### validate_ci_templates.sh - -验证 `templates/ci/` 目录下的 CI 模板: - -- **Gitea/GitHub workflow 文件**: - - 文件存在 - - YAML 语法正确 - - 包含必要字段(name, on, jobs, runs-on, steps) - - 包含中文注释(符合项目风格) - - 包含配置区域标记 - - 包含环境变量配置 -- **特定 workflow 模板**: - - standards-check.yml:包含格式化检查和 lint 检查 - - test.yml:包含测试步骤和测试矩阵 -- **文档**: - - 包含 README 说明文档 - - 包含使用说明 - -#### validate_project_templates.sh - -验证项目通用模板: - -- **核心模板**: - - `templates/AGENTS.template.md` - - `templates/AGENT_RULES.template.md` - - `templates/README.md` -- **memory-bank 模板**: - - 项目定位/技术栈/架构/进度/决策/实施计划 -- **prompts 模板**: - - `prompts/README.md` - - `prompts/system/agent-behavior.template.md` - - `prompts/coding/clarify.template.md` - - `prompts/coding/verify.template.md` - -### 3. 集成测试 (integration/) - -端到端测试,验证整体功能。 - -#### check_doc_links.sh - -检查所有 Markdown 文档中的链接有效性: - -- **扫描范围**: - - 所有 `*.md` 文件(排除 `*.template.md`) - - 排除 node_modules, .git, build, dist 等目录 -- **链接类型**: - - Markdown 链接:`[text](link)` - - 引用链接:`[text]: link` -- **验证逻辑**: - - 检查相对路径链接是否指向存在的文件 - - 跳过外部链接(http/https) - - 跳过 mailto 链接 - - 跳过代码块与行内代码中的链接样式文本 - - 支持锚点(但只验证文件存在性) -- **报告内容**: - - 总链接数 - - 有效链接数 - - 跳过链接数(外部/mailto) - - 断开链接详情(文件、行号、链接、目标路径) - -## 🔧 本地开发 - -### 安装测试依赖 - -**Ubuntu/Debian**: - -```bash -# bats-core (Shell 脚本测试) -sudo apt-get update -sudo apt-get install bats - -# Python 工具(模板验证) -pip install toml tomli jsonschema yamllint - -# C++ 工具(模板验证,可选) -sudo apt-get install cmake clang-format - -# YAML 验证 -pip install yamllint -``` - -**macOS**: - -```bash -# bats-core -brew install bats-core - -# Python 工具 -pip3 install toml tomli jsonschema yamllint - -# C++ 工具(可选) -brew install cmake clang-format - -# YAML 验证 -pip3 install yamllint -``` - -### 运行单个测试 - -```bash -# Shell 脚本测试 -cd tests/scripts -bats test_sync_standards.bats --tap # TAP 格式输出 -bats test_sync_templates.bats -bats test_vendor_playbook.bats --formatter junit # JUnit 格式 - -# 模板验证测试 -cd tests/templates -sh validate_python_templates.sh -sh validate_cpp_templates.sh -sh validate_ci_templates.sh -sh validate_project_templates.sh - -# 集成测试 -cd tests/integration -sh check_doc_links.sh -``` - -### 添加新测试 - -#### 添加新的 bats 测试 - -在 `tests/scripts/` 创建新文件 `test_.bats`: - -```bash -#!/usr/bin/env bats - -setup() { - # 测试前准备 - export TEST_DIR="$(mktemp -d)" -} - -teardown() { - # 测试后清理 - rm -rf "$TEST_DIR" -} - -@test "描述测试内容" { - # 测试代码 - [ -f "some_file" ] -} -``` - -#### 添加新的模板验证 - -在 `tests/templates/` 创建新文件 `validate__templates.sh`,参考现有脚本结构。 - -#### 添加新的集成测试 - -在 `tests/integration/` 创建新脚本,确保: - -1. 使用 `set -eu` 启用错误检测 -2. 输出清晰的测试进度 -3. 生成详细的报告文件 -4. 返回正确的退出码(0 = 成功,非 0 = 失败) - -## 📊 测试覆盖率目标 - -- **Shell 脚本测试**:目标覆盖率 ≥ 80% -- **模板验证测试**:目标通过率 = 100% -- **集成测试**:目标通过率 = 100% -- **文档链接有效性**:目标有效率 = 100% - -## 🐛 故障排查 - -### bats 测试失败 - -```bash -# 使用 --verbose 查看详细输出 -bats --verbose test_sync_standards.bats - -# 使用 --trace 查看执行跟踪 -bats --trace test_sync_standards.bats -``` - -### 模板验证失败 - -验证脚本会生成详细报告文件: - -- `tests/templates/python_validation_report.txt` -- `tests/templates/cpp_validation_report.txt` -- `tests/templates/ci_validation_report.txt` - -### 文档链接检查失败 - -查看详细报告: - -```bash -cat /tmp/doc_links_report.txt -``` - -## 🤝 贡献指南 - -添加新功能时,请同步更新相应的测试: - -1. **修改脚本**(`scripts/`)→ 更新对应的 `.bats` 测试 -2. **修改模板**(`templates/`)→ 更新对应的验证脚本 -3. **修改文档**(`docs/`, `.agents/`)→ 运行文档链接检查 -4. **修改 CI workflow**(`.gitea/workflows/`)→ 验证 YAML 语法 - -## 📖 相关文档 - -- [开发规范索引](../docs/index.md) -- [提交信息规范](../docs/common/commit_message.md) -- [Gitea Actions 文档](https://docs.gitea.com/usage/actions/overview) -- [bats-core 文档](https://bats-core.readthedocs.io/) - -## ❓ 常见问题 - -### Q: 为什么测试在 CI 通过,但本地失败? - -A: 可能原因: - -- 环境差异(工具版本、路径) -- 权限问题 -- Git 配置差异 - -建议使用 Docker 容器复现 CI 环境。 - -### Q: 如何跳过某些测试? - -A: bats 支持 `skip` 命令: - -```bash -@test "某个测试" { - skip "原因说明" - # 测试代码 -} -``` - -### Q: 测试运行很慢,如何加速? - -A: 建议: - -1. 使用 bats 的 `--jobs` 参数并行运行 -2. 只运行变更相关的测试 -3. 使用 CI 的缓存机制 - ---- - -**测试套件维护者**: Playbook 团队 -**最后更新**: 2026-01-07 +扫描 `docs/` 与模板文件中的本地链接,确保引用路径有效。 diff --git a/tests/cli/test_playbook_cli.py b/tests/cli/test_playbook_cli.py new file mode 100644 index 0000000..eec82c0 --- /dev/null +++ b/tests/cli/test_playbook_cli.py @@ -0,0 +1,126 @@ +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +SCRIPT = ROOT / "scripts" / "playbook.py" + + +def run_cli(*args): + return subprocess.run( + [sys.executable, str(SCRIPT), *args], + capture_output=True, + text=True, + ) + + +class PlaybookCliTests(unittest.TestCase): + def test_help_shows_usage(self): + result = run_cli("-h") + self.assertEqual(result.returncode, 0) + self.assertIn("Usage:", result.stdout + result.stderr) + + def test_missing_config_is_error(self): + result = run_cli() + self.assertNotEqual(result.returncode, 0) + self.assertIn("-config", result.stdout + result.stderr) + + def test_action_order(self): + config_body = """ +[playbook] +project_root = "." + +[format_md] + +[sync_standards] +langs = ["tsl"] +""" + with tempfile.TemporaryDirectory() as tmp_dir: + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + result = run_cli("-config", str(config_path)) + + self.assertEqual(result.returncode, 0) + output = result.stdout + result.stderr + self.assertIn("sync_standards", output) + self.assertIn("format_md", output) + + def test_vendor_creates_snapshot(self): + with tempfile.TemporaryDirectory() as tmp_dir: + config_body = f""" +[playbook] +project_root = "{tmp_dir}" + +[vendor] +langs = ["tsl"] +""" + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + + result = run_cli("-config", str(config_path)) + + snapshot = Path(tmp_dir) / "docs/standards/playbook/SOURCE.md" + self.assertEqual(result.returncode, 0) + self.assertTrue(snapshot.is_file()) + + def test_sync_templates_creates_memory_bank(self): + with tempfile.TemporaryDirectory() as tmp_dir: + config_body = f""" +[playbook] +project_root = "{tmp_dir}" + +[sync_templates] +project_name = "Demo" +""" + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + + result = run_cli("-config", str(config_path)) + + memory_bank = Path(tmp_dir) / "memory-bank/project-brief.md" + self.assertEqual(result.returncode, 0) + self.assertTrue(memory_bank.is_file()) + + def test_sync_standards_creates_agents(self): + with tempfile.TemporaryDirectory() as tmp_dir: + config_body = f""" +[playbook] +project_root = "{tmp_dir}" + +[sync_standards] +langs = ["tsl"] +""" + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + + result = run_cli("-config", str(config_path)) + + agents_index = Path(tmp_dir) / ".agents/tsl/index.md" + self.assertEqual(result.returncode, 0) + self.assertTrue(agents_index.is_file()) + + def test_install_skills(self): + with tempfile.TemporaryDirectory() as tmp_dir: + target = Path(tmp_dir) / "codex" + config_body = f""" +[playbook] +project_root = "{tmp_dir}" + +[install_skills] +codex_home = "{target}" +mode = "list" +skills = ["brainstorming"] +""" + config_path = Path(tmp_dir) / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + + result = run_cli("-config", str(config_path)) + + skill_file = target / "skills/brainstorming/SKILL.md" + self.assertEqual(result.returncode, 0) + self.assertTrue(skill_file.is_file()) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/scripts/test_install_codex_skills.bats b/tests/scripts/test_install_codex_skills.bats deleted file mode 100644 index 934d4ee..0000000 --- a/tests/scripts/test_install_codex_skills.bats +++ /dev/null @@ -1,116 +0,0 @@ -#!/usr/bin/env bats -# install_codex_skills.sh 测试套件 - -setup() { - export TEST_DIR="$(mktemp -d)" - export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - export SCRIPT_PATH="$PLAYBOOK_ROOT/scripts/install_codex_skills.sh" - - # 创建临时 HOME 避免污染真实环境 - export ORIGINAL_HOME="$HOME" - export HOME="$TEST_DIR/home" - mkdir -p "$HOME" - - export CODEX_HOME="$HOME/.codex" - export SKILLS_DST_ROOT="$CODEX_HOME/skills" - - cd "$TEST_DIR" -} - -teardown() { - export HOME="$ORIGINAL_HOME" - - if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then - chmod -R u+w "$TEST_DIR" 2>/dev/null || true - rm -rf "$TEST_DIR" - fi -} - -skip_if_no_skills() { - if [ ! -d "$PLAYBOOK_ROOT/codex/skills" ]; then - skip "No codex/skills directory found" - fi -} - -first_skill_name() { - ls -1 "$PLAYBOOK_ROOT/codex/skills" 2>/dev/null | head -n 1 -} - -# ============================================== -# 基础功能测试 -# ============================================== - -@test "install_codex_skills.sh 脚本存在且可执行" { - [ -f "$SCRIPT_PATH" ] -} - -@test "安装 - 创建 skills 目录" { - skip_if_no_skills - - [ ! -d "$SKILLS_DST_ROOT" ] - - sh "$SCRIPT_PATH" -all - - [ -d "$SKILLS_DST_ROOT" ] -} - -@test "安装 - 复制 skill 目录" { - skip_if_no_skills - - sh "$SCRIPT_PATH" -all - - SKILL_DIRS=$(find "$SKILLS_DST_ROOT" -mindepth 1 -maxdepth 1 -type d) - [ -n "$SKILL_DIRS" ] - - for dir in $SKILL_DIRS; do - [ -f "$dir/SKILL.md" ] - done -} - -@test "安装 - 指定单个 skill" { - skip_if_no_skills - - SKILL_NAME="$(first_skill_name)" - [ -n "$SKILL_NAME" ] - - sh "$SCRIPT_PATH" -skills "$SKILL_NAME" - - [ -d "$SKILLS_DST_ROOT/$SKILL_NAME" ] - COUNT=$(find "$SKILLS_DST_ROOT" -mindepth 1 -maxdepth 1 -type d | wc -l) - [ "$COUNT" -eq 1 ] -} - -@test "备份 - 同名 skill 目录会创建备份" { - skip_if_no_skills - - SKILL_NAME="$(first_skill_name)" - [ -n "$SKILL_NAME" ] - - mkdir -p "$SKILLS_DST_ROOT/$SKILL_NAME" - echo "marker" > "$SKILLS_DST_ROOT/$SKILL_NAME/marker.txt" - - sh "$SCRIPT_PATH" -skills "$SKILL_NAME" - - BACKUP_DIRS=$(find "$SKILLS_DST_ROOT" -maxdepth 1 -type d -name "$SKILL_NAME.bak.*") - [ -n "$BACKUP_DIRS" ] -} - -@test "错误处理 - 指定不存在 skill 报错" { - skip_if_no_skills - - run sh "$SCRIPT_PATH" -skills nonexistent-skill - - [ "$status" -ne 0 ] -} - -@test "幂等性 - 多次安装结果一致" { - skip_if_no_skills - - sh "$SCRIPT_PATH" -all - CHECKSUM1=$(find "$SKILLS_DST_ROOT" -type f -name "SKILL.md" ! -path "$SKILLS_DST_ROOT"'/*.bak.*/*' -exec md5sum {} \; | sort | md5sum) - - sh "$SCRIPT_PATH" -all - CHECKSUM2=$(find "$SKILLS_DST_ROOT" -type f -name "SKILL.md" ! -path "$SKILLS_DST_ROOT"'/*.bak.*/*' -exec md5sum {} \; | sort | md5sum) - - [ "$CHECKSUM1" = "$CHECKSUM2" ] -} diff --git a/tests/scripts/test_sync_standards.bats b/tests/scripts/test_sync_standards.bats deleted file mode 100644 index 5d05bc3..0000000 --- a/tests/scripts/test_sync_standards.bats +++ /dev/null @@ -1,274 +0,0 @@ -#!/usr/bin/env bats -# sync_standards.sh 测试套件 - -# 测试辅助函数 -setup() { - # 创建临时测试目录 - export TEST_DIR="$(mktemp -d)" - export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - # 初始化测试项目 - cd "$TEST_DIR" - git init -b main - git config user.name "Test User" - git config user.email "test@example.com" - - # 模拟 playbook 快照目录 - mkdir -p docs/standards/playbook - cp -r "$PLAYBOOK_ROOT"/{rulesets,.gitattributes,docs,scripts} docs/standards/playbook/ 2>/dev/null || true - - export SCRIPT_PATH="$TEST_DIR/docs/standards/playbook/scripts/sync_standards.sh" -} - -teardown() { - # 清理测试目录 - if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then - chmod -R u+rwX "$TEST_DIR" 2>/dev/null || true - rm -rf "$TEST_DIR" - fi -} - -# ============================================== -# 基础功能测试 -# ============================================== - -@test "sync_standards.sh 脚本存在且可执行" { - [ -f "$SCRIPT_PATH" ] -} - -@test "sync_standards.sh - 未传 -langs 报错" { - cd "$TEST_DIR" - run sh "$SCRIPT_PATH" - - [ "$status" -ne 0 ] -} - -@test "sync_standards.sh tsl - 同步 TSL 规则集" { - cd "$TEST_DIR" - sh "$SCRIPT_PATH" -langs tsl - - # 验证必须文件 - [ -d ".agents/tsl" ] - [ -f ".agents/tsl/index.md" ] - [ ! -f ".agents/tsl/auth.md" ] - [ ! -f ".agents/tsl/code_quality.md" ] - [ ! -f ".agents/tsl/performance.md" ] - [ ! -f ".agents/tsl/testing.md" ] -} - -@test "sync_standards.sh cpp - 同步 C++ 规则集" { - cd "$TEST_DIR" - sh "$SCRIPT_PATH" -langs cpp - - # 验证必须文件 - [ -d ".agents/cpp" ] - [ -f ".agents/cpp/index.md" ] - [ ! -f ".agents/cpp/auth.md" ] - [ ! -f ".agents/cpp/code_quality.md" ] - [ ! -f ".agents/cpp/performance.md" ] - [ ! -f ".agents/cpp/testing.md" ] -} - -@test "sync_standards.sh tsl cpp - 同步多个规则集" { - cd "$TEST_DIR" - sh "$SCRIPT_PATH" -langs tsl,cpp - - # 验证两个规则集都存在 - [ -d ".agents/tsl" ] - [ -d ".agents/cpp" ] - [ -f ".agents/index.md" ] -} - -# ============================================== -# .gitattributes 同步测试 -# ============================================== - -@test ".gitattributes - 默认模式追加缺失规则" { - cd "$TEST_DIR" - [ ! -f ".gitattributes" ] - - sh "$SCRIPT_PATH" -langs tsl - - [ -f ".gitattributes" ] - grep -q "Added from playbook .gitattributes" .gitattributes - grep -q "\\*.tsl" .gitattributes -} - -@test ".gitattributes - 保留现有内容" { - cd "$TEST_DIR" - echo "# My custom rules" > .gitattributes - echo "*.custom binary" >> .gitattributes - - sh "$SCRIPT_PATH" -langs tsl - - grep -q "# My custom rules" .gitattributes - grep -q "*.custom binary" .gitattributes -} - -@test ".gitattributes - 更新已存在的 managed block" { - cd "$TEST_DIR" - cat > .gitattributes << 'EOF' -# BEGIN playbook .gitattributes -# Old content -# END playbook .gitattributes -EOF - - export SYNC_GITATTR_MODE=block - sh "$SCRIPT_PATH" -langs tsl - - # 验证 block 已更新(不再包含 "Old content") - ! grep -q "Old content" .gitattributes -} - -# ============================================== -# AGENTS.md 自动生成测试 -# ============================================== - -@test "AGENTS.md - 不存在时自动创建" { - cd "$TEST_DIR" - [ ! -f "AGENTS.md" ] - - sh "$SCRIPT_PATH" -langs tsl - - [ -f "AGENTS.md" ] - grep -q ".agents/" AGENTS.md -} - -@test "AGENTS.md - 已存在时不覆盖" { - cd "$TEST_DIR" - echo "# My custom AGENTS.md" > AGENTS.md - - sh "$SCRIPT_PATH" -langs tsl - - grep -q "# My custom AGENTS.md" AGENTS.md -} - -# ============================================== -# 备份功能测试 -# ============================================== - -@test "备份 - .gitattributes 更新前创建备份" { - cd "$TEST_DIR" - echo "# Original content" > .gitattributes - - sh "$SCRIPT_PATH" -langs tsl - - # 验证备份文件存在 - [ -f ".gitattributes.bak."* ] || [ -f ".gitattributes.bak" ] -} - -@test "备份 - rulesets/ 更新前创建备份" { - cd "$TEST_DIR" - mkdir -p .agents/tsl - echo "# Old index" > .agents/tsl/index.md - - sh "$SCRIPT_PATH" -langs tsl - - # 验证备份目录存在 - [ -d ".agents/tsl.bak."* ] || [ -d ".agents/tsl.bak" ] -} - -# ============================================== -# 多语言项目测试 -# ============================================== - -@test "多语言 - TSL + C++ + Python 规则集共存" { - cd "$TEST_DIR" - - # 复制 Python 规则集(如果存在) - if [ -d "$PLAYBOOK_ROOT/rulesets/python" ]; then - cp -r "$PLAYBOOK_ROOT/rulesets/python" docs/standards/playbook/rulesets/ - fi - - sh "$SCRIPT_PATH" -langs tsl,cpp - - # 验证规则集不互相覆盖 - [ -d ".agents/tsl" ] - [ -d ".agents/cpp" ] - - # 验证索引文件正确引用 - [ -f ".agents/index.md" ] -} - -# ============================================== -# 错误处理测试 -# ============================================== - -@test "错误处理 - 未找到 playbook 快照时报错" { - cd "$TEST_DIR" - rm -rf docs/standards/playbook/rulesets - - run sh "$SCRIPT_PATH" -langs tsl - - [ "$status" -ne 0 ] -} - -@test "错误处理 - 无效语言参数时报错" { - cd "$TEST_DIR" - - run sh "$SCRIPT_PATH" -langs invalid_lang - - [ "$status" -ne 0 ] -} - -# ============================================== -# 环境变量配置测试 -# ============================================== - -@test "环境变量 - SYNC_GITATTR_MODE=skip 跳过 .gitattributes" { - cd "$TEST_DIR" - export SYNC_GITATTR_MODE=skip - - sh "$SCRIPT_PATH" -langs tsl - - [ ! -f ".gitattributes" ] -} - -@test "环境变量 - SYNC_GITATTR_MODE=overwrite 覆盖 .gitattributes" { - cd "$TEST_DIR" - echo "# Custom content" > .gitattributes - export SYNC_GITATTR_MODE=overwrite - - sh "$SCRIPT_PATH" -langs tsl - - # 验证自定义内容被覆盖 - ! grep -q "# Custom content" .gitattributes -} - -@test "环境变量 - SYNC_GITATTR_MODE=append 追加缺失规则" { - cd "$TEST_DIR" - echo "# Custom rules only" > .gitattributes - export SYNC_GITATTR_MODE=append - - sh "$SCRIPT_PATH" -langs tsl - - grep -q "Added from playbook .gitattributes" .gitattributes - grep -q "\\*.tsl" .gitattributes -} - -@test "环境变量 - SYNC_GITATTR_MODE=append 无缺失规则不追加" { - cd "$TEST_DIR" - cp "$PLAYBOOK_ROOT/.gitattributes" .gitattributes - export SYNC_GITATTR_MODE=append - - sh "$SCRIPT_PATH" -langs tsl - - ! grep -q "Added from playbook .gitattributes" .gitattributes -} - -# ============================================== -# 幂等性测试 -# ============================================== - -@test "幂等性 - 多次执行结果一致" { - cd "$TEST_DIR" - - # 第一次同步 - sh "$SCRIPT_PATH" -langs tsl - CHECKSUM1=$(find rulesets/tsl -type f -exec md5sum {} \; | sort | md5sum) - - # 第二次同步 - sh "$SCRIPT_PATH" -langs tsl - CHECKSUM2=$(find rulesets/tsl -type f -exec md5sum {} \; | sort | md5sum) - - [ "$CHECKSUM1" = "$CHECKSUM2" ] -} diff --git a/tests/scripts/test_sync_templates.bats b/tests/scripts/test_sync_templates.bats deleted file mode 100644 index d84e73f..0000000 --- a/tests/scripts/test_sync_templates.bats +++ /dev/null @@ -1,84 +0,0 @@ -#!/usr/bin/env bats -# sync_templates.sh 测试套件 - -setup() { - export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - export SCRIPT_PATH="$PLAYBOOK_ROOT/scripts/sync_templates.sh" - export TARGET_DIR="$(mktemp -d)" -} - -teardown() { - if [ -n "$TARGET_DIR" ] && [ -d "$TARGET_DIR" ]; then - chmod -R u+w "$TARGET_DIR" 2>/dev/null || true - rm -rf "$TARGET_DIR" - fi -} - -# ============================================== -# 基础功能测试 -# ============================================== - -@test "sync_templates.sh 脚本存在且可执行" { - [ -f "$SCRIPT_PATH" ] -} - -@test "sync_templates.sh - 基础同步与占位符替换" { - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -project-name "DemoProject" -date "2026-02-03" - - [ -d "$TARGET_DIR/memory-bank" ] - [ -f "$TARGET_DIR/memory-bank/project-brief.md" ] - [ -f "$TARGET_DIR/docs/prompts/coding/clarify.md" ] - [ -f "$TARGET_DIR/AGENTS.md" ] - [ -f "$TARGET_DIR/AGENT_RULES.md" ] - - grep -q "DemoProject" "$TARGET_DIR/memory-bank/project-brief.md" - ! grep -q "{{DATE}}" "$TARGET_DIR/AGENT_RULES.md" - - [ -z "$(find "$TARGET_DIR" -name '*.template.md' -print -quit)" ] -} - -@test "sync_templates.sh - 已存在目录不覆盖 (无 -force)" { - mkdir -p "$TARGET_DIR/memory-bank" - mkdir -p "$TARGET_DIR/docs/prompts" - echo "keep" > "$TARGET_DIR/memory-bank/keep.md" - echo "keep" > "$TARGET_DIR/docs/prompts/keep.md" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" - - [ -f "$TARGET_DIR/memory-bank/keep.md" ] - [ ! -f "$TARGET_DIR/memory-bank/project-brief.md" ] - [ -f "$TARGET_DIR/docs/prompts/keep.md" ] - [ ! -f "$TARGET_DIR/docs/prompts/README.md" ] -} - -@test "sync_templates.sh - -force 覆盖并备份" { - mkdir -p "$TARGET_DIR/memory-bank" - echo "marker" > "$TARGET_DIR/memory-bank/marker.txt" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -force - - [ -f "$TARGET_DIR/memory-bank/project-brief.md" ] - [ ! -f "$TARGET_DIR/memory-bank/marker.txt" ] - - backup_dir="$(ls -d "$TARGET_DIR"/memory-bank.bak.* 2>/dev/null | head -n 1)" - [ -n "$backup_dir" ] - [ -f "$backup_dir/marker.txt" ] -} - -@test "sync_templates.sh - -full 更新 framework 区块" { - cat > "$TARGET_DIR/AGENTS.md" << 'EOF' -# Agent Instructions - - -OLD_FRAMEWORK - - -Footer -EOF - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -full - - ! grep -q "OLD_FRAMEWORK" "$TARGET_DIR/AGENTS.md" - grep -q "" "$TARGET_DIR/AGENTS.md" - grep -q "Footer" "$TARGET_DIR/AGENTS.md" -} diff --git a/tests/scripts/test_vendor_playbook.bats b/tests/scripts/test_vendor_playbook.bats deleted file mode 100644 index 8615f90..0000000 --- a/tests/scripts/test_vendor_playbook.bats +++ /dev/null @@ -1,301 +0,0 @@ -#!/usr/bin/env bats -# vendor_playbook.sh 测试套件 - -# 测试辅助函数 -setup() { - # 创建临时测试目录 - export TEST_DIR="$(mktemp -d)" - export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" - export SCRIPT_PATH="$PLAYBOOK_ROOT/scripts/vendor_playbook.sh" - - # 创建目标项目目录 - export TARGET_DIR="$(mktemp -d)" - cd "$TARGET_DIR" - git init -b main - git config user.name "Test User" - git config user.email "test@example.com" -} - -teardown() { - # 清理测试目录 - if [ -n "$TEST_DIR" ] && [ -d "$TEST_DIR" ]; then - chmod -R u+w "$TEST_DIR" 2>/dev/null || true - rm -rf "$TEST_DIR" - fi - if [ -n "$TARGET_DIR" ] && [ -d "$TARGET_DIR" ]; then - chmod -R u+w "$TARGET_DIR" 2>/dev/null || true - rm -rf "$TARGET_DIR" - fi -} - -# ============================================== -# 基础功能测试 -# ============================================== - -@test "vendor_playbook.sh 脚本存在且可执行" { - [ -f "$SCRIPT_PATH" ] -} - -@test "vendor_playbook.sh - 无参数时显示用法" { - cd "$TARGET_DIR" - - run sh "$SCRIPT_PATH" - - [ "$status" -ne 0 ] -} - -@test "vendor_playbook.sh - 单语言 vendoring (tsl)" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证快照目录结构 - [ -d "docs/standards/playbook" ] - [ -d "docs/standards/playbook/docs/common" ] - [ -d "docs/standards/playbook/docs/tsl" ] - [ -d "docs/standards/playbook/rulesets/tsl" ] - [ -f "docs/standards/playbook/scripts/sync_standards.sh" ] -} - -@test "vendor_playbook.sh - 多语言 vendoring (tsl cpp)" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl,cpp - - # 验证包含两种语言的文档 - [ -d "docs/standards/playbook/docs/tsl" ] - [ -d "docs/standards/playbook/docs/cpp" ] - [ -d "docs/standards/playbook/rulesets/tsl" ] - [ -d "docs/standards/playbook/rulesets/cpp" ] -} - -# ============================================== -# 自动同步测试 -# ============================================== - -@test "vendor_playbook.sh - 自动执行 sync_standards" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证根目录已同步规则集 - [ -d ".agents/tsl" ] - [ -f ".agents/tsl/index.md" ] - [ -f ".gitattributes" ] -} - -@test "vendor_playbook.sh - 多语言自动同步" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl,cpp - - # 验证两个规则集都已同步 - [ -d ".agents/tsl" ] - [ -d ".agents/cpp" ] - [ -f ".agents/index.md" ] -} - -# ============================================== -# SOURCE.md 生成测试 -# ============================================== - -@test "vendor_playbook.sh - 生成 SOURCE.md" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - [ -f "docs/standards/playbook/SOURCE.md" ] - grep -q "Source:" docs/standards/playbook/SOURCE.md - grep -q "Commit:" docs/standards/playbook/SOURCE.md -} - -@test "vendor_playbook.sh - SOURCE.md 包含 commit hash" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证包含 commit hash(40个十六进制字符) - grep -E "[0-9a-f]{40}" docs/standards/playbook/SOURCE.md -} - -@test "vendor_playbook.sh - SOURCE.md 包含时间戳" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证包含日期格式 YYYY-MM-DD - grep -E "[0-9]{4}-[0-9]{2}-[0-9]{2}" docs/standards/playbook/SOURCE.md -} - -# ============================================== -# 裁剪功能测试 -# ============================================== - -@test "裁剪 - 仅包含指定语言的文档" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证包含 TSL 文档 - [ -d "docs/standards/playbook/docs/tsl" ] - - # 验证不包含其他语言(如果原本有 Python) - if [ -d "$PLAYBOOK_ROOT/docs/python" ]; then - [ ! -d "docs/standards/playbook/docs/python" ] - fi -} - -@test "裁剪 - 始终包含 common 目录" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - [ -d "docs/standards/playbook/docs/common" ] - [ -f "docs/standards/playbook/docs/common/commit_message.md" ] -} - -@test "裁剪 - 包含对应的模板文件" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs cpp - - # 验证包含 C++ 模板 - [ -d "docs/standards/playbook/templates/cpp" ] - - # 验证不包含 Python 模板(如果指定了 cpp) - if [ -d "$PLAYBOOK_ROOT/templates/python" ]; then - [ ! -d "docs/standards/playbook/templates/python" ] - fi -} - -@test "裁剪 - 包含通用 CI 模板" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - [ -d "docs/standards/playbook/templates/ci" ] -} - -@test "vendor_playbook.sh - -apply-templates 应用模板到项目根目录" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs cpp -apply-templates - - if [ -f "$PLAYBOOK_ROOT/templates/cpp/.clang-format" ]; then - [ -f ".clang-format" ] - fi - - if [ -f "$PLAYBOOK_ROOT/templates/ci/gitea/.gitea/workflows/standards-check.yml" ]; then - [ -f ".gitea/workflows/standards-check.yml" ] - fi -} - -# ============================================== -# 目标目录处理测试 -# ============================================== - -@test "目标目录 - 已存在时覆盖更新" { - cd "$TARGET_DIR" - - # 首次 vendor - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 在快照中添加标记文件 - touch docs/standards/playbook/OLD_MARKER - - # 再次 vendor - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证旧标记不存在(已被覆盖) - [ ! -f "docs/standards/playbook/OLD_MARKER" ] -} - -@test "目标目录 - 创建必要的父目录" { - cd "$TARGET_DIR" - - # 确保 docs/standards 不存在 - [ ! -d "docs/standards" ] - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - [ -d "docs/standards/playbook" ] -} - -# ============================================== -# 错误处理测试 -# ============================================== - -@test "错误处理 - 目标目录不存在时报错" { - cd "$TARGET_DIR" - - missing_dir="$TEST_DIR/missing-project" - rm -rf "$missing_dir" - - run sh "$SCRIPT_PATH" -project-root "$missing_dir" -langs tsl - - [ "$status" -ne 0 ] -} - -@test "错误处理 - 无效语言参数时报错" { - cd "$TARGET_DIR" - - run sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs invalid_lang - - [ "$status" -ne 0 ] -} - -@test "错误处理 - 目标目录不是 git 仓库时警告" { - cd "$TARGET_DIR" - rm -rf .git - - run sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 应该给出警告但不失败 - [ "$status" -eq 0 ] -} - -# ============================================== -# 完整性测试 -# ============================================== - -@test "完整性 - 验证所有必要文件已复制" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证关键文件 - [ -f "docs/standards/playbook/README.md" ] - [ -f "docs/standards/playbook/docs/index.md" ] - [ -f "docs/standards/playbook/.gitattributes" ] - [ -f "docs/standards/playbook/scripts/sync_standards.sh" ] - [ -f "docs/standards/playbook/SOURCE.md" ] -} - -@test "完整性 - 验证脚本可执行性" { - cd "$TARGET_DIR" - - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - - # 验证同步脚本可执行 - run sh docs/standards/playbook/scripts/sync_standards.sh -langs tsl - - [ "$status" -eq 0 ] -} - -# ============================================== -# 多次执行测试 -# ============================================== - -@test "幂等性 - 多次 vendor 结果一致" { - cd "$TARGET_DIR" - - # 第一次 vendor - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - CHECKSUM1=$(find docs/standards/playbook -type f -name "*.md" ! -name "SOURCE.md" -exec md5sum {} \; | sort | md5sum) - - # 第二次 vendor - sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -langs tsl - CHECKSUM2=$(find docs/standards/playbook -type f -name "*.md" ! -name "SOURCE.md" -exec md5sum {} \; | sort | md5sum) - - [ "$CHECKSUM1" = "$CHECKSUM2" ] -} diff --git a/tests/scripts/test_windows_script_lints.bats b/tests/scripts/test_windows_script_lints.bats deleted file mode 100644 index 6436b30..0000000 --- a/tests/scripts/test_windows_script_lints.bats +++ /dev/null @@ -1,44 +0,0 @@ -#!/usr/bin/env bats -# Windows script lint tests (PowerShell/Batch) - -setup() { - export PLAYBOOK_ROOT="$(cd "$BATS_TEST_DIRNAME/../.." && pwd)" -} - -@test "sync_standards.ps1 - here-string terminator not piped" { - run grep -nE "^[[:space:]]*['\\\"]@\\s*\\|" "$PLAYBOOK_ROOT/scripts/sync_standards.ps1" - [ "$status" -ne 0 ] -} - -@test "sync_standards.ps1 - Help alias does not shadow parameter name" { - run grep -niE "Alias\\([^)]*['\"]help['\"]" "$PLAYBOOK_ROOT/scripts/sync_standards.ps1" - [ "$status" -ne 0 ] -} - -@test "install_codex_skills.ps1 - Help alias does not shadow parameter name" { - run grep -niE "Alias\\([^)]*['\"]help['\"]" "$PLAYBOOK_ROOT/scripts/install_codex_skills.ps1" - [ "$status" -ne 0 ] -} - -@test "sync_templates.ps1 - Help alias does not shadow parameter name" { - run grep -niE "Alias\\([^)]*['\"]help['\"]" "$PLAYBOOK_ROOT/scripts/sync_templates.ps1" - [ "$status" -ne 0 ] -} - -@test "vendor_playbook.ps1 - Help alias does not shadow parameter name" { - run grep -niE "Alias\\([^)]*['\"]help['\"]" "$PLAYBOOK_ROOT/scripts/vendor_playbook.ps1" - [ "$status" -ne 0 ] -} - -@test "sync_standards.bat - show_help label follows parse_args" { - local file="$PLAYBOOK_ROOT/scripts/sync_standards.bat" - local show_line - local parse_line - - show_line=$(grep -n "^:show_help" "$file" | head -n 1 | cut -d: -f1) - parse_line=$(grep -n "^:parse_args" "$file" | head -n 1 | cut -d: -f1) - - [ -n "$show_line" ] - [ -n "$parse_line" ] - [ "$show_line" -gt "$parse_line" ] -}