Compare commits
13 Commits
eb75036b64
...
ab0dd11afc
| Author | SHA1 | Date |
|---|---|---|
|
|
ab0dd11afc | |
|
|
398696c9c2 | |
|
|
d959f80de7 | |
|
|
b4f712acb4 | |
|
|
0c4cd0e037 | |
|
|
3d1582ce9e | |
|
|
49bbfa13e4 | |
|
|
8cfcc25f98 | |
|
|
05903c33ae | |
|
|
65d216ef52 | |
|
|
f0bcf546b2 | |
|
|
08853097d9 | |
|
|
3483d8a48b |
|
|
@ -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)
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
*🤖 由 [Gitea Actions](../../actions) 自动生成*
|
||||
|
||||
EOFSUMMARY
|
||||
|
||||
echo "*📅 生成时间: $(date -u '+%Y-%m-%d %H:%M:%S UTC')*" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "</div>" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
echo "========================================"
|
||||
if [ "$overall_fail" -ne 0 ]; then
|
||||
echo "❌ 测试失败"
|
||||
exit 1
|
||||
fi
|
||||
echo "✅ 全量测试通过"
|
||||
sh tests/integration/check_doc_links.sh
|
||||
|
|
|
|||
|
|
@ -20,4 +20,4 @@ tags
|
|||
[._]*.un~
|
||||
|
||||
reports/
|
||||
|
||||
.worktrees/
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
codex/skills/**
|
||||
12
AGENTS.md
12
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% |
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
355
README.md
355
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**:追加路由链接(使用 `<!-- playbook:templates:start/end -->` 标记)
|
||||
- **-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/<lang>/`、更新 `.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/<lang>/` 与 `.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/<AGENTS_NS>/`(由 `-langs` 或 `AGENTS_NS` 指定)
|
||||
- 更新 `.gitattributes`:默认追加缺失规则(可用
|
||||
`SYNC_GITATTR_MODE=append|block|overwrite|skip` 控制)
|
||||
- 缺省创建:`.agents/index.md`
|
||||
- 覆盖前备份:写入同目录的 `*.bak.*`(或 Windows 下随机后缀)
|
||||
- 不修改:`.gitignore`(项目自行维护)
|
||||
|
||||
<details>
|
||||
<summary> 高级选项:环境变量配置(点击展开)</summary>
|
||||
|
||||
#### 环境变量(可选)
|
||||
|
||||
同步脚本支持以下可选环境变量(默认值可满足大多数项目):
|
||||
|
||||
| 变量名 | 默认值 | 说明 |
|
||||
| ------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `AGENTS_NS` | `tsl` | 同步的规则集名/落地目录名:`.agents/<AGENTS_NS>/`(例如 `tsl`、`cpp`) |
|
||||
| `SYNC_GITATTR_MODE` | `append` | `.gitattributes` 同步模式:`append` 仅追加缺失规则(忽略注释/空行,比对后按块追加);`block` 仅维护 managed 区块;`overwrite` 全量覆盖;`skip` 不更新 |
|
||||
|
||||
</details>
|
||||
---
|
||||
|
||||
### 方式二:手动复制快照
|
||||
|
||||
如果不使用
|
||||
`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 <PLAYBOOK_ROOT>/scripts/vendor_playbook.sh -project-root <target-project-root> -langs tsl,cpp
|
||||
```
|
||||
[vendor]
|
||||
langs = ["tsl", "cpp"]
|
||||
```
|
||||
|
||||
- PowerShell:
|
||||
```bash
|
||||
python scripts/playbook.py -config playbook.toml
|
||||
```
|
||||
|
||||
```powershell
|
||||
powershell -File <PLAYBOOK_ROOT>\\scripts\\vendor_playbook.ps1 -ProjectRoot <target-project-root> -Langs tsl,cpp
|
||||
```
|
||||
该动作仅生成裁剪快照,不会隐式同步 `.agents/` 或 `.gitattributes`;后续请用 `sync_standards` 明确落地。
|
||||
|
||||
- Windows bat:
|
||||
|
||||
```bat
|
||||
<PLAYBOOK_ROOT>\\scripts\\vendor_playbook.bat -project-root <target-project-root> -langs tsl,cpp
|
||||
```
|
||||
|
||||
**脚本会**:
|
||||
|
||||
- 生成裁剪快照到 `docs/standards/playbook/`(包含
|
||||
`docs/common/` + 选定语言目录 + 对应 `.agents/<lang>/` + `scripts/` +
|
||||
`.gitattributes` + 通用 `templates/ci/` + 相关 `templates/<lang>/`)
|
||||
- 自动执行 `docs/standards/playbook/scripts/sync_standards.*`,把
|
||||
`.agents/<lang>/` 与 `.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/<lang>/`,不要互相覆盖
|
||||
- 若某个子目录需要更具体规则(模块/子系统差异),在更靠近代码的目录放置更具体规则(例如
|
||||
`src/foo/.agents/`),并以"离代码更近者优先"为准
|
||||
|
||||
<details>
|
||||
<summary> 高级选项:`.agents` 覆盖/合并策略(点击展开)</summary>
|
||||
|
||||
#### `.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."
|
||||
}
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
#### 扩展新语言(模板)
|
||||
|
||||
当目标项目需要新增一门语言(例如 C++),建议按以下模板扩展:
|
||||
|
||||
- **文档**:
|
||||
- 若使用本 Playbook 自带的 C++ 规范:无需额外 subtree,直接使用
|
||||
`docs/standards/playbook/docs/cpp/`,并在项目 `README.md`/`docs/index.md`
|
||||
链接入口。
|
||||
- 若新增"本 Playbook 未覆盖的语言":再引入对应语言的标准仓库(subtree/vendoring 到
|
||||
`docs/standards/<lang>/`)
|
||||
- **代理规则**:
|
||||
- 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/<lang>/`(与 `.agents/tsl/`
|
||||
并行),只写该语言专属要求与工具链约束
|
||||
- **同步策略**:每个规则集只同步到对应子目录(例如 `.agents/cpp/`),避免覆盖整个
|
||||
`.agents/`
|
||||
- **CI/工具**:按文件类型分别执行格式化、lint、测试(不要让 TSL 规则去约束 C++ 代码,反之亦然)
|
||||
- C++ 补全:建议在项目根目录提供 `.clangd` 并指向正确的
|
||||
`CompilationDatabase`(模板见 `templates/cpp/.clangd`)
|
||||
|
|
|
|||
47
SKILLS.md
47
SKILLS.md
|
|
@ -46,41 +46,36 @@ $CODEX_HOME/skills/<skill-name>/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` 列表。
|
||||
|
|
|
|||
|
|
@ -0,0 +1,32 @@
|
|||
# playbook.toml (example)
|
||||
# 配置文件所在目录默认作为 project_root。
|
||||
|
||||
[playbook]
|
||||
# project_root = "." # 可选:覆盖目标项目根目录
|
||||
# verbose = false # 可选:输出更详细日志
|
||||
# dry_run = false # 可选:仅预览,不写入
|
||||
|
||||
[vendor]
|
||||
# 将 playbook 裁剪快照写入 <project_root>/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
|
||||
|
|
@ -1,135 +0,0 @@
|
|||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
rem Install Codex skills from this Playbook snapshot into CODEX_HOME.
|
||||
rem - Source: <snapshot>\codex\skills\<skill-name>\
|
||||
rem - Dest: %CODEX_HOME%\skills\<skill-name>\ (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
|
||||
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
# Install Codex skills from this Playbook snapshot into CODEX_HOME.
|
||||
# - Source: <snapshot>\codex\skills\<skill-name>\
|
||||
# - Dest: $env:CODEX_HOME\skills\<skill-name>\ (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"
|
||||
|
|
@ -1,142 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Install Codex skills from this Playbook snapshot into CODEX_HOME.
|
||||
# - Source: <snapshot>/codex/skills/<skill-name>/
|
||||
# - Dest: $CODEX_HOME/skills/<skill-name>/ (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 <cwd>/.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"
|
||||
|
|
@ -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 <path>\n python scripts/playbook.py -h"
|
||||
|
||||
|
||||
def load_config(path: Path) -> dict:
|
||||
return tomllib.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
def log(message: str) -> None:
|
||||
print(message)
|
||||
|
||||
|
||||
def ensure_dir(path: Path) -> None:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def normalize_langs(raw: object) -> list[str]:
|
||||
if raw is None:
|
||||
return ["tsl"]
|
||||
if isinstance(raw, str):
|
||||
langs = [raw]
|
||||
else:
|
||||
langs = list(raw)
|
||||
cleaned: list[str] = []
|
||||
for lang in langs:
|
||||
item = str(lang).strip()
|
||||
if not item:
|
||||
continue
|
||||
if "/" in item or "\\" in item or ".." in item:
|
||||
raise ValueError(f"invalid lang: {item}")
|
||||
cleaned.append(item)
|
||||
if not cleaned:
|
||||
raise ValueError("langs is empty")
|
||||
return cleaned
|
||||
|
||||
|
||||
def read_git_commit(root: Path) -> str:
|
||||
try:
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), "rev-parse", "HEAD"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=True,
|
||||
)
|
||||
except (OSError, subprocess.CalledProcessError):
|
||||
return "N/A"
|
||||
return result.stdout.strip() or "N/A"
|
||||
|
||||
|
||||
def write_docs_index(dest_prefix: Path, langs: list[str]) -> None:
|
||||
lines = [
|
||||
"# 文档导航(Docs Index)",
|
||||
"",
|
||||
f"本快照为裁剪版 Playbook(langs: {','.join(langs)})。",
|
||||
"",
|
||||
"## 跨语言(common)",
|
||||
"",
|
||||
"- 提交信息与版本号:`common/commit_message.md`",
|
||||
]
|
||||
for lang in langs:
|
||||
if lang == "tsl":
|
||||
lines += [
|
||||
"",
|
||||
"## TSL(tsl)",
|
||||
"",
|
||||
"- 代码风格:`tsl/code_style.md`",
|
||||
"- 命名规范:`tsl/naming.md`",
|
||||
"- 语法手册:`tsl/syntax_book/index.md`",
|
||||
"- 工具链与验证命令(模板):`tsl/toolchain.md`",
|
||||
]
|
||||
elif lang == "cpp":
|
||||
lines += [
|
||||
"",
|
||||
"## C++(cpp)",
|
||||
"",
|
||||
"- 代码风格:`cpp/code_style.md`",
|
||||
"- 命名规范:`cpp/naming.md`",
|
||||
"- 工具链与验证命令(模板):`cpp/toolchain.md`",
|
||||
"- 第三方依赖(Conan):`cpp/dependencies_conan.md`",
|
||||
"- clangd 配置:`cpp/clangd.md`",
|
||||
]
|
||||
elif lang == "python":
|
||||
lines += [
|
||||
"",
|
||||
"## Python(python)",
|
||||
"",
|
||||
"- 代码风格:`python/style_guide.md`",
|
||||
"- 工具链:`python/tooling.md`",
|
||||
"- 配置清单:`python/configuration.md`",
|
||||
]
|
||||
elif lang == "markdown":
|
||||
lines += [
|
||||
"",
|
||||
"## Markdown(markdown)",
|
||||
"",
|
||||
"- 代码块与行内代码格式:`markdown/index.md`",
|
||||
]
|
||||
docs_index = dest_prefix / "docs/index.md"
|
||||
ensure_dir(docs_index.parent)
|
||||
docs_index.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def write_snapshot_readme(dest_prefix: Path, langs: list[str]) -> None:
|
||||
lines = [
|
||||
"# Playbook(裁剪快照)",
|
||||
"",
|
||||
f"本目录为从 Playbook vendoring 的裁剪快照(langs: {','.join(langs)})。",
|
||||
"",
|
||||
"## 使用",
|
||||
"",
|
||||
"在目标项目根目录执行:",
|
||||
"",
|
||||
"```sh",
|
||||
"python docs/standards/playbook/scripts/playbook.py -config playbook.toml",
|
||||
"```",
|
||||
"",
|
||||
"配置示例:`docs/standards/playbook/playbook.toml.example`",
|
||||
"",
|
||||
"文档入口:",
|
||||
"",
|
||||
"- `docs/standards/playbook/docs/index.md`",
|
||||
"- `.agents/index.md`",
|
||||
]
|
||||
(dest_prefix / "README.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def write_source_file(dest_prefix: Path, langs: list[str]) -> None:
|
||||
commit = read_git_commit(PLAYBOOK_ROOT)
|
||||
timestamp = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
|
||||
lines = [
|
||||
"# SOURCE",
|
||||
"",
|
||||
f"- Source: {PLAYBOOK_ROOT}",
|
||||
f"- Commit: {commit}",
|
||||
f"- Date: {timestamp}",
|
||||
f"- Langs: {','.join(langs)}",
|
||||
"- Generated-by: scripts/playbook.py",
|
||||
]
|
||||
(dest_prefix / "SOURCE.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def vendor_action(config: dict, context: dict) -> int:
|
||||
try:
|
||||
langs = normalize_langs(config.get("langs"))
|
||||
except ValueError as exc:
|
||||
print(f"ERROR: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
target_dir = config.get("target_dir", "docs/standards/playbook")
|
||||
target_path = Path(target_dir)
|
||||
if target_path.is_absolute() or ".." in target_path.parts:
|
||||
print(f"ERROR: invalid target_dir: {target_dir}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
project_root: Path = context["project_root"]
|
||||
dest_prefix = project_root / target_path
|
||||
dest_standards = dest_prefix.parent
|
||||
|
||||
ensure_dir(dest_standards)
|
||||
|
||||
if dest_prefix.exists():
|
||||
timestamp = datetime.now().strftime("%Y%m%d%H%M%S")
|
||||
backup = dest_standards / f"{dest_prefix.name}.bak.{timestamp}"
|
||||
dest_prefix.rename(backup)
|
||||
log(f"Backed up existing snapshot -> {backup}")
|
||||
|
||||
ensure_dir(dest_prefix)
|
||||
|
||||
gitattributes_src = PLAYBOOK_ROOT / ".gitattributes"
|
||||
if gitattributes_src.is_file():
|
||||
copy2(gitattributes_src, dest_prefix / ".gitattributes")
|
||||
|
||||
copytree(PLAYBOOK_ROOT / "scripts", dest_prefix / "scripts")
|
||||
copytree(PLAYBOOK_ROOT / "codex", dest_prefix / "codex")
|
||||
copy2(PLAYBOOK_ROOT / "SKILLS.md", dest_prefix / "SKILLS.md")
|
||||
|
||||
common_docs = PLAYBOOK_ROOT / "docs/common"
|
||||
if common_docs.is_dir():
|
||||
copytree(common_docs, dest_prefix / "docs/common")
|
||||
|
||||
rulesets_root = PLAYBOOK_ROOT / "rulesets"
|
||||
ensure_dir(dest_prefix / "rulesets")
|
||||
if (rulesets_root / "index.md").is_file():
|
||||
copy2(rulesets_root / "index.md", dest_prefix / "rulesets/index.md")
|
||||
|
||||
templates_ci = PLAYBOOK_ROOT / "templates/ci"
|
||||
if templates_ci.is_dir():
|
||||
copytree(templates_ci, dest_prefix / "templates/ci")
|
||||
|
||||
for lang in langs:
|
||||
docs_src = PLAYBOOK_ROOT / "docs" / lang
|
||||
rules_src = PLAYBOOK_ROOT / "rulesets" / lang
|
||||
if not docs_src.is_dir():
|
||||
print(f"ERROR: docs not found for lang={lang}", file=sys.stderr)
|
||||
return 2
|
||||
if not rules_src.is_dir():
|
||||
print(f"ERROR: rulesets not found for lang={lang}", file=sys.stderr)
|
||||
return 2
|
||||
copytree(docs_src, dest_prefix / "docs" / lang)
|
||||
copytree(rules_src, dest_prefix / "rulesets" / lang)
|
||||
templates_src = PLAYBOOK_ROOT / "templates" / lang
|
||||
if templates_src.is_dir():
|
||||
copytree(templates_src, dest_prefix / "templates" / lang)
|
||||
|
||||
example_config = PLAYBOOK_ROOT / "playbook.toml.example"
|
||||
if example_config.is_file():
|
||||
copy2(example_config, dest_prefix / "playbook.toml.example")
|
||||
|
||||
write_docs_index(dest_prefix, langs)
|
||||
write_snapshot_readme(dest_prefix, langs)
|
||||
write_source_file(dest_prefix, langs)
|
||||
|
||||
log(f"Vendored snapshot -> {dest_prefix}")
|
||||
return 0
|
||||
|
||||
|
||||
def 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 = "<!-- playbook:framework:start -->"
|
||||
end_marker = "<!-- playbook:framework:end -->"
|
||||
else:
|
||||
start_marker = "<!-- playbook:templates:start -->"
|
||||
end_marker = "<!-- playbook:templates:end -->"
|
||||
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 = [
|
||||
"<!-- playbook:agents:start -->",
|
||||
"请以 `.agents/` 下的规则为准:",
|
||||
"- 入口:`.agents/index.md`",
|
||||
f"- 语言规则:{langs_line}" if langs_line else "- 语言规则:",
|
||||
"<!-- playbook:agents:end -->",
|
||||
]
|
||||
return lines
|
||||
|
||||
|
||||
def update_agents_block(agents_md: Path, block_lines: list[str]) -> None:
|
||||
start = "<!-- playbook:agents:start -->"
|
||||
end = "<!-- playbook:agents: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:]))
|
||||
|
|
@ -1,420 +0,0 @@
|
|||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
rem Sync standards snapshot to project root.
|
||||
rem - Copies <snapshot>\rulesets\<AGENTS_NS> -> <project-root>\.agents\<AGENTS_NS>
|
||||
rem - Updates <project-root>\.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 ^<url^> ^<branch^> --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 ^<snapshot^>\.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 ^<standards-url^> ^<branch^> --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=<!-- playbook:agents:start -->"
|
||||
set "AGENTS_BLOCK_END=<!-- playbook:agents: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
|
||||
|
|
@ -1,338 +0,0 @@
|
|||
# Sync standards snapshot to project root.
|
||||
# - Copies <snapshot>/rulesets/<AGENTS_NS> -> <project-root>/.agents/<AGENTS_NS>
|
||||
# - Updates <project-root>/.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 <list> 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 <url> <branch> --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 <snapshot>/.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 = "<!-- playbook:agents:start -->"
|
||||
$AgentsBlockEnd = "<!-- playbook:agents:end -->"
|
||||
$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 = @"
|
||||
<!-- playbook:agents:start -->
|
||||
|
||||
请以 `.agents/` 下的规则为准:
|
||||
|
||||
- 入口:`.agents/index.md`
|
||||
- 语言规则:$langsLine
|
||||
<!-- playbook:agents:end -->
|
||||
"@
|
||||
|
||||
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."
|
||||
|
|
@ -1,428 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Sync standards snapshot to project root.
|
||||
# - Copies <snapshot>/rulesets/<AGENTS_NS> -> <project-root>/.agents/<AGENTS_NS>
|
||||
# - Updates <project-root>/.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 <standards-url> <branch> --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 <snapshot>/.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="<!-- playbook:agents:start -->"
|
||||
AGENTS_BLOCK_END="<!-- playbook:agents: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."
|
||||
|
|
@ -1,233 +0,0 @@
|
|||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
rem Sync project templates to target project.
|
||||
rem - Copies templates/memory-bank/ -> <project-root>/memory-bank/
|
||||
rem - Copies templates/prompts/ -> <project-root>/docs/prompts/
|
||||
rem - Copies templates/AGENTS.template.md -> <project-root>/AGENTS.md
|
||||
rem - Copies templates/AGENT_RULES.template.md -> <project-root>/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 <path> # 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 ^<path^>
|
||||
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=<!-- playbook:framework:start -->"
|
||||
set "MARKER_END=<!-- playbook:framework:end -->"
|
||||
set "SECTION_NAME=framework"
|
||||
) else (
|
||||
set "MARKER_START=<!-- playbook:templates:start -->"
|
||||
set "MARKER_END=<!-- playbook:templates: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
|
||||
|
|
@ -1,248 +0,0 @@
|
|||
# Sync project templates to target project.
|
||||
# - Copies templates/memory-bank/ -> <project-root>/memory-bank/
|
||||
# - Copies templates/prompts/ -> <project-root>/docs/prompts/
|
||||
# - Copies templates/AGENTS.template.md -> <project-root>/AGENTS.md
|
||||
# - Copies templates/AGENT_RULES.template.md -> <project-root>/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 = "<!-- playbook:framework:start -->"
|
||||
$MarkerEnd = "<!-- playbook:framework:end -->"
|
||||
$SectionName = "framework"
|
||||
} else {
|
||||
$MarkerStart = "<!-- playbook:templates:start -->"
|
||||
$MarkerEnd = "<!-- playbook:templates:end -->"
|
||||
$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"
|
||||
|
|
@ -1,323 +0,0 @@
|
|||
#!/usr/bin/env sh
|
||||
set -eu
|
||||
|
||||
# Sync project templates to target project.
|
||||
# - Copies templates/memory-bank/ -> <project-root>/memory-bank/
|
||||
# - Copies templates/prompts/ -> <project-root>/docs/prompts/
|
||||
# - Copies templates/AGENTS.template.md -> <project-root>/AGENTS.md
|
||||
# - Copies templates/AGENT_RULES.template.md -> <project-root>/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="<!-- playbook:framework:start -->"
|
||||
MARKER_END="<!-- playbook:framework:end -->"
|
||||
SECTION_NAME="framework"
|
||||
else
|
||||
MARKER_START="<!-- playbook:templates:start -->"
|
||||
MARKER_END="<!-- playbook:templates: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 <lang> to sync .agents/ rules"
|
||||
|
|
@ -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\<lang>\ and .gitattributes in
|
||||
rem the target project root.
|
||||
rem
|
||||
rem Usage:
|
||||
rem scripts\vendor_playbook.bat -project-root <path> (default: tsl)
|
||||
rem scripts\vendor_playbook.bat -project-root <path> -langs tsl,cpp
|
||||
rem scripts\vendor_playbook.bat -project-root <path> -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: <project-root>\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 ^<path^> ^(default: tsl^)
|
||||
echo scripts\vendor_playbook.bat -project-root ^<path^> -langs tsl,cpp
|
||||
echo scripts\vendor_playbook.bat -project-root ^<path^> -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
|
||||
|
|
@ -1,353 +0,0 @@
|
|||
# Vendor a trimmed Playbook snapshot into a target project (offline copy),
|
||||
# then run sync_standards to materialize rulesets\<lang>\ and .gitattributes in
|
||||
# the target project root.
|
||||
#
|
||||
# Usage:
|
||||
# powershell -File scripts/vendor_playbook.ps1 -ProjectRoot <path>
|
||||
# powershell -File scripts/vendor_playbook.ps1 -ProjectRoot <path> -Langs tsl,cpp
|
||||
# powershell -File scripts/vendor_playbook.ps1 -ProjectRoot <path> -Langs @("tsl","cpp") -ApplyTemplates
|
||||
#
|
||||
# Options:
|
||||
# -ApplyTemplates Apply CI/lang templates to project root (skip if exists)
|
||||
#
|
||||
# Notes:
|
||||
# - Snapshot is written to: <project-root>\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 <path>"
|
||||
Write-Host " powershell -File scripts/vendor_playbook.ps1 -ProjectRoot <path> -Langs tsl,cpp"
|
||||
Write-Host " powershell -File scripts/vendor_playbook.ps1 -ProjectRoot <path> -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."
|
||||
|
|
@ -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/<lang>/ and .gitattributes in
|
||||
# the target project root.
|
||||
#
|
||||
# Usage:
|
||||
# sh scripts/vendor_playbook.sh -project-root <path> # default: tsl
|
||||
# sh scripts/vendor_playbook.sh -project-root <path> -langs tsl,cpp
|
||||
# sh scripts/vendor_playbook.sh -project-root <path> -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: <project-root>/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 <path> # default: tsl
|
||||
sh scripts/vendor_playbook.sh -project-root <path> -langs tsl,cpp
|
||||
sh scripts/vendor_playbook.sh -project-root <path> -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" <<EOF
|
||||
# 文档导航(Docs Index)
|
||||
|
||||
本快照为裁剪版 Playbook(langs: ${langs_csv})。
|
||||
|
||||
## 跨语言(common)
|
||||
|
||||
- 提交信息与版本号:\`common/commit_message.md\`
|
||||
EOF
|
||||
|
||||
append_docs_section() {
|
||||
lang="$1"
|
||||
case "$lang" in
|
||||
tsl)
|
||||
cat >>"$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" <<EOF
|
||||
# Playbook(裁剪快照)
|
||||
|
||||
本目录为从 Playbook vendoring 的裁剪快照(langs: ${langs_csv})。
|
||||
|
||||
## 使用
|
||||
|
||||
在目标项目根目录执行(多语言一次同步):
|
||||
|
||||
\`\`\`sh
|
||||
sh docs/standards/playbook/scripts/sync_standards.sh -langs ${langs_csv}
|
||||
\`\`\`
|
||||
|
||||
查看规范入口:
|
||||
|
||||
- \`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/\`。
|
||||
EOF
|
||||
|
||||
cat >"$DEST_PREFIX/SOURCE.md" <<EOF
|
||||
# SOURCE
|
||||
|
||||
- Source: $SRC
|
||||
- Commit: ${commit:-N/A}
|
||||
- Date: $(date -u +%Y-%m-%dT%H:%M:%SZ 2>/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" <<EOF
|
||||
# .agents(多语言)
|
||||
|
||||
本目录用于存放仓库级/语言级的代理规则集。
|
||||
|
||||
本项目已启用的规则集:
|
||||
EOF
|
||||
|
||||
for lang in "$@"; do
|
||||
case "$lang" in
|
||||
tsl) printf '%s\n' "- .agents/tsl/:TSL 相关规则集(适用于 .tsl/.tsf)" >>"$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."
|
||||
|
|
@ -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**:
|
||||
- 默认:追加路由链接(`<!-- playbook:templates:start/end -->`)
|
||||
- `-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 标记**(用于自动更新):
|
||||
|
||||
| 标记 | 用途 | 管理脚本 |
|
||||
| --------------------------------------- | ---------------------- | -------------- |
|
||||
| `<!-- playbook:agents:start/end -->` | 语言规则链接 | sync_standards |
|
||||
| `<!-- playbook:templates:start/end -->` | 路由链接(默认追加) | sync_templates |
|
||||
| `<!-- playbook:framework:start/end -->` | 完整框架(-full 追加) | sync_templates |
|
||||
| 标记 | 用途 | 管理入口 |
|
||||
| --------------------------------------- | --------------------- | ------------------------------ |
|
||||
| `<!-- playbook:agents:start/end -->` | 语言规则链接 | playbook.py `[sync_standards]` |
|
||||
| `<!-- playbook:templates:start/end -->` | 路由链接(默认追加) | playbook.py `[sync_templates]` |
|
||||
| `<!-- playbook:framework:start/end -->` | 完整框架(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}} 占位符
|
||||
```
|
||||
|
||||
---
|
||||
|
|
|
|||
458
tests/README.md
458
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_<script_name>.bats`:
|
||||
|
||||
```bash
|
||||
#!/usr/bin/env bats
|
||||
|
||||
setup() {
|
||||
# 测试前准备
|
||||
export TEST_DIR="$(mktemp -d)"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
# 测试后清理
|
||||
rm -rf "$TEST_DIR"
|
||||
}
|
||||
|
||||
@test "描述测试内容" {
|
||||
# 测试代码
|
||||
[ -f "some_file" ]
|
||||
}
|
||||
```
|
||||
|
||||
#### 添加新的模板验证
|
||||
|
||||
在 `tests/templates/` 创建新文件 `validate_<template_type>_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/` 与模板文件中的本地链接,确保引用路径有效。
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -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" ]
|
||||
}
|
||||
|
|
@ -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" ]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
||||
<!-- playbook:framework:start -->
|
||||
OLD_FRAMEWORK
|
||||
<!-- playbook:framework:end -->
|
||||
|
||||
Footer
|
||||
EOF
|
||||
|
||||
sh "$SCRIPT_PATH" -project-root "$TARGET_DIR" -full
|
||||
|
||||
! grep -q "OLD_FRAMEWORK" "$TARGET_DIR/AGENTS.md"
|
||||
grep -q "<!-- playbook:framework:start -->" "$TARGET_DIR/AGENTS.md"
|
||||
grep -q "Footer" "$TARGET_DIR/AGENTS.md"
|
||||
}
|
||||
|
|
@ -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" ]
|
||||
}
|
||||
|
|
@ -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" ]
|
||||
}
|
||||
Loading…
Reference in New Issue