diff --git a/README.md b/README.md index 68b888f..b7d220b 100644 --- a/README.md +++ b/README.md @@ -175,6 +175,8 @@ TSL 相关问题直接查阅 `rulesets/tsl/index.md` 与 `docs/tsl/`。 **安装与使用**:详见 `SKILLS.md` +如果你通过 `[install_skills]` 更新已经安装过的 skill,默认会先把旧目录备份为 `*.bak.`;如果你明确希望“删除旧版本后直接重装”,可在 `playbook.toml` 的 `[install_skills]` 下设置 `no_backup = true`。 + ## 在其他项目中使用本 Playbook 由于本仓库需要内部权限访问,其他项目**不能仅用外链引用**;推荐把 Playbook 规范部署到项目内,并用统一入口执行。 diff --git a/SKILLS.md b/SKILLS.md index c3edf8c..e749fff 100644 --- a/SKILLS.md +++ b/SKILLS.md @@ -75,10 +75,23 @@ skills = ["style-cleanup", "commit-message"] [install_skills] mode = "all" agents_home = "./.agents" +# no_backup = true ``` > 注意:Codex 默认从 `~/.agents/skills` 加载 skills;使用本地安装时,需要确保 Codex 能发现该路径。 +`[install_skills]` 默认会先把已存在的 skill 目录重命名为 `*.bak.`,再复制新版本,便于手动回退。 +如果你希望安装过程不保留备份,而是“先删除旧目录,再复制新目录”,可显式设置: + +```toml +[install_skills] +mode = "all" +agents_home = "~/.agents" +no_backup = true +``` + +`no_backup = true` 适合 CI 或你已经用 Git 管理变更、只想要确定性覆盖安装的场景。 + 如果你的项目已经把本 Playbook 部署到项目内(无论来自 `git subtree`,还是外部 clone 后部署到自定义根目录),则在目标项目里执行: ```bash diff --git a/data/tsl_reference_catalog_source/index.md b/data/tsl_reference_catalog_source/index.md index fae62e6..c9d61f0 100644 --- a/data/tsl_reference_catalog_source/index.md +++ b/data/tsl_reference_catalog_source/index.md @@ -1,6 +1,6 @@ # TSL函数 -> 本文档从 [docs/tsl/syntax_book/function/index.md](../../docs/tsl/syntax_book/function/index.md) 拆分而来 +> 本文档对应的正式检索入口见 [docs/tsl/reference/catalog/index.md](../../docs/tsl/reference/catalog/index.md) TSL函数包含数学、系统、基础、图形等通用函数,适用于各种TSL脚本开发场景。 @@ -80,4 +80,4 @@ TSL函数包含数学、系统、基础、图形等通用函数,适用于各 --- -**返回**: [docs/tsl/syntax_book/function/index.md](../../docs/tsl/syntax_book/function/index.md) +**返回**: [docs/tsl/reference/catalog/index.md](../../docs/tsl/reference/catalog/index.md) diff --git a/playbook.toml.example b/playbook.toml.example index c1cfe8e..495f6f5 100644 --- a/playbook.toml.example +++ b/playbook.toml.example @@ -15,7 +15,6 @@ # force = false # 可选:覆盖已有文件 # no_backup = false # 可选:跳过备份 # date = "2026-04-22" # 可选:替换 {{DATE}} -# main_language = "tsl" # 可选:覆盖 {{MAIN_LANGUAGE}} [sync_memory_bank] # 同步 memory-bank/(配置节存在即启用) @@ -25,7 +24,6 @@ # force = false # 可选:覆盖已有文件(会先备份) # no_backup = false # 可选:跳过备份 # date = "2026-04-22" # 可选:替换 {{DATE}} -# main_language = "tsl" # 可选:覆盖 {{MAIN_LANGUAGE}} [sync_prompts] # 同步 docs/prompts/(配置节存在即启用) @@ -33,7 +31,6 @@ # force = false # 可选:覆盖已有文件(会先备份) # no_backup = false # 可选:跳过备份 # date = "2026-04-22" # 可选:替换 {{DATE}} -# main_language = "tsl" # 可选:覆盖 {{MAIN_LANGUAGE}} [sync_standards] # langs = ["tsl", "cpp", "typescript"] # 必填:要同步的语言 @@ -44,6 +41,7 @@ # mode = "list" # list|all # skills = ["brainstorming"] # mode=list 时必填 # agents_home = "~/.agents" # 可选:默认 ~/.agents +# no_backup = false # 可选:跳过备份,直接删除旧 skill 后重装 [format_md] # tool = "prettier" # 仅支持 prettier diff --git a/scripts/playbook.py b/scripts/playbook.py index bcdd6a4..a54a8aa 100644 --- a/scripts/playbook.py +++ b/scripts/playbook.py @@ -320,29 +320,7 @@ def resolve_docs_prefix(context: dict) -> str: return join_deploy_subpath(resolve_deploy_root(context), "docs") -def resolve_main_language(config: dict, context: dict) -> str: - raw = config.get("main_language") - if raw is not None and str(raw).strip(): - return str(raw).strip() - - full_config = context.get("config", {}) - if isinstance(full_config, dict): - sync_conf = full_config.get("sync_standards") - if isinstance(sync_conf, dict): - langs_raw = sync_conf.get("langs") - if langs_raw is not None: - try: - langs = normalize_langs(langs_raw) - except ValueError: - langs = [] - if langs: - return langs[0] - - return "tsl" - - -def resolve_playbook_scripts(project_root: Path, context: dict) -> str: - _ = project_root +def resolve_playbook_scripts(context: dict) -> str: return join_deploy_subpath(resolve_deploy_root(context), "scripts") @@ -541,14 +519,11 @@ def replace_placeholders( text: str, project_name: str | None, date_value: str, - main_language: str | None, playbook_scripts: str | None, ) -> str: result = text.replace("{{DATE}}", date_value) if project_name: result = result.replace("{{PROJECT_NAME}}", project_name) - if main_language: - result = result.replace("{{MAIN_LANGUAGE}}", main_language) if playbook_scripts: result = result.replace("{{PLAYBOOK_SCRIPTS}}", playbook_scripts) return result @@ -563,41 +538,16 @@ def backup_path(path: Path, no_backup: bool) -> None: 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, - main_language: str | None, - playbook_scripts: str | None, -) -> None: - for file_path in root.rglob("*.md"): - text = file_path.read_text(encoding="utf-8") - updated = replace_placeholders( - text, project_name, date_value, main_language, playbook_scripts - ) - if updated != text: - file_path.write_text(updated, encoding="utf-8") - - def replace_placeholders_in_file( file_path: Path, project_name: str | None, date_value: str, - main_language: str | None, playbook_scripts: str | None, ) -> None: if file_path.suffix != ".md": return text = file_path.read_text(encoding="utf-8") - updated = replace_placeholders( - text, project_name, date_value, main_language, playbook_scripts - ) + updated = replace_placeholders(text, project_name, date_value, playbook_scripts) if updated != text: file_path.write_text(updated, encoding="utf-8") @@ -616,7 +566,6 @@ def sync_directory( target_dir: Path, project_name: str | None, date_value: str, - main_language: str | None, playbook_scripts: str | None, force: bool, no_backup: bool, @@ -637,7 +586,6 @@ def sync_directory( target_file, project_name, date_value, - main_language, playbook_scripts, ) @@ -665,12 +613,11 @@ def update_agents_section( end_marker: str, project_name: str | None, date_value: str, - main_language: str | None, playbook_scripts: str | None, ) -> None: template_text = template_path.read_text(encoding="utf-8") template_text = replace_placeholders( - template_text, project_name, date_value, main_language, playbook_scripts + template_text, project_name, date_value, playbook_scripts ) block = extract_block_lines(template_text, start_marker, end_marker) if not block: @@ -746,8 +693,7 @@ def sync_agents_template(context: dict) -> int: return 0 project_name = resolve_project_name(context) - main_language = resolve_main_language({}, context) - playbook_scripts = resolve_playbook_scripts(project_root, context) + playbook_scripts = resolve_playbook_scripts(context) date_value = resolve_template_date(context) agents_dst = project_root / "AGENTS.md" @@ -773,7 +719,6 @@ def sync_agents_template(context: dict) -> int: end_marker, project_name, date_value, - main_language, playbook_scripts, ) return 0 @@ -805,17 +750,14 @@ def sync_rules_action(config: dict, context: dict) -> int: return 0 project_name = resolve_project_name(context) - main_language = resolve_main_language(config, context) - playbook_scripts = resolve_playbook_scripts(project_root, context) + playbook_scripts = resolve_playbook_scripts(context) date_value = config.get("date") or datetime.now().strftime("%Y-%m-%d") no_backup = bool(config.get("no_backup", False)) backup_path(rules_dst, no_backup) text = rules_src.read_text(encoding="utf-8") - text = replace_placeholders( - text, project_name, date_value, main_language, playbook_scripts - ) - rules_dst.write_text(text + "\n", encoding="utf-8") + text = replace_placeholders(text, project_name, date_value, playbook_scripts) + rules_dst.write_text(text.rstrip("\n") + "\n", encoding="utf-8") log("Synced: AGENT_RULES.md") return 0 @@ -833,8 +775,7 @@ def sync_memory_bank_action(config: dict, context: dict) -> int: return 2 project_name = config.get("project_name") - main_language = resolve_main_language(config, context) - playbook_scripts = resolve_playbook_scripts(project_root, context) + playbook_scripts = resolve_playbook_scripts(context) 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)) @@ -846,7 +787,6 @@ def sync_memory_bank_action(config: dict, context: dict) -> int: memory_dst, project_name, date_value, - main_language, playbook_scripts, force, no_backup, @@ -868,8 +808,7 @@ def sync_prompts_action(config: dict, context: dict) -> int: return 2 project_name = resolve_project_name(context) - main_language = resolve_main_language(config, context) - playbook_scripts = resolve_playbook_scripts(project_root, context) + playbook_scripts = resolve_playbook_scripts(context) 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)) @@ -882,7 +821,6 @@ def sync_prompts_action(config: dict, context: dict) -> int: prompts_dst, project_name, date_value, - main_language, playbook_scripts, force, no_backup, @@ -945,6 +883,13 @@ def update_agents_block(agents_md: Path, block_lines: list[str]) -> None: def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | None) -> None: agents_index = agents_root / "index.md" + lang_descriptions = { + "tsl": "TSL 相关规则集(由 playbook 同步;适用于 `.tsl`/`.tsf`)", + "cpp": "C++ 相关规则集(由 playbook 同步;适用于 C++23/Modules)", + "python": "Python 相关规则集(由 playbook 同步)", + "typescript": "TypeScript/JavaScript 相关规则集(由 playbook 同步)", + "markdown": "Markdown 相关规则集(仅代码格式化)", + } lines = [ "# .agents(多语言)", "", @@ -952,11 +897,11 @@ def create_agents_index(agents_root: Path, langs: list[str], docs_prefix: str | "", "建议约定:", "", - "- `.agents/tsl/`:TSL 相关规则集(由 playbook 同步;适用于 `.tsl`/`.tsf`)", - "- `.agents/cpp/`:C++ 相关规则集(由 playbook 同步;适用于 C++23/Modules)", - "- `.agents/python/`:Python 相关规则集(由 playbook 同步)", - "- `.agents/typescript/`:TypeScript/JavaScript 相关规则集(由 playbook 同步)", - "- `.agents/markdown/`:Markdown 相关规则集(仅代码格式化)", + ] + for lang in langs: + description = lang_descriptions.get(lang, "相关规则集(由 playbook 同步)") + lines.append(f"- `.agents/{lang}/`:{description}") + lines += [ "", "规则发生冲突时,建议以“更靠近代码的目录规则更具体”为准。", "", @@ -1220,6 +1165,7 @@ def install_skills_action(config: dict, context: dict) -> int: return 2 timestamp = datetime.now().strftime("%Y%m%d%H%M%S") + no_backup = bool(config.get("no_backup", False)) for name in skills: src = skills_src_root / name if not src.is_dir(): @@ -1227,9 +1173,12 @@ def install_skills_action(config: dict, context: dict) -> int: 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}") + if no_backup: + rmtree(dst) + else: + backup = skills_dst_root / f"{name}.bak.{timestamp}" + dst.rename(backup) + log(f"Backed up existing skill: {name} -> {backup.name}") copytree(src, dst) rewrite_skill_docs_links(dst, resolve_docs_prefix(context)) log(f"Installed: {name}") diff --git a/templates/AGENTS.template.md b/templates/AGENTS.template.md index b43ba3c..ef99cac 100644 --- a/templates/AGENTS.template.md +++ b/templates/AGENTS.template.md @@ -9,7 +9,7 @@ 请以 `.agents/` 下的规则为准: - 入口:`.agents/index.md` -- 语言规则:`.agents/{{MAIN_LANGUAGE}}/index.md` +- 语言规则:见 `.agents/index.md` 与对应语言子目录 diff --git a/templates/README.md b/templates/README.md index 2620ed6..56000c5 100644 --- a/templates/README.md +++ b/templates/README.md @@ -186,12 +186,10 @@ project/ | `{{PROJECT_NAME}}` | 项目名称 | ✅ 可选 | | `{{PROJECT_GOAL}}` | 项目目标 | ❌ 手动 | | `{{PROJECT_DESCRIPTION}}` | 项目描述 | ❌ 手动 | -| `{{MAIN_LANGUAGE}}` | 主语言 | ✅ 可选 | | `{{PLAYBOOK_SCRIPTS}}` | 脚本路径 | ✅ 是 | | 其他 `{{...}}` | 项目特定内容 | ❌ 手动 | `{{PROJECT_NAME}}` 可通过 `sync_memory_bank.project_name` 自动替换;未配置时保持原样。 -`{{MAIN_LANGUAGE}}` 可通过 `sync_standards.langs[0]` 自动替换;未配置时默认 `tsl`。 `{{PLAYBOOK_SCRIPTS}}` 自动替换为 Playbook 脚本路径(默认 `docs/standards/playbook/scripts`,也可按项目配置改成 `custom/playbook/scripts` 等)。 ## 模板说明 diff --git a/templates/memory-bank/tech-stack.template.md b/templates/memory-bank/tech-stack.template.md index 9566356..b794559 100644 --- a/templates/memory-bank/tech-stack.template.md +++ b/templates/memory-bank/tech-stack.template.md @@ -11,8 +11,6 @@ -**主语言**:{{MAIN_LANGUAGE}} - **文件类型**:{{FILE_TYPES}} ## 项目结构 diff --git a/tests/cli/test_playbook_cli.py b/tests/cli/test_playbook_cli.py index 26377cb..a44a0d9 100644 --- a/tests/cli/test_playbook_cli.py +++ b/tests/cli/test_playbook_cli.py @@ -237,6 +237,29 @@ no_backup = true self.assertIn("`.agents/tsl/index.md`", agents_index) self.assertIn("`.agents/cpp/index.md`", agents_index) + def test_sync_standards_agents_index_only_lists_configured_langs(self): + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + config_body = f""" +[playbook] +project_root = "{tmp_dir}" +deploy_root = "{CUSTOM_DEPLOY_ROOT}" + +[sync_standards] +langs = ["tsl", "markdown"] +""" + config_path = write_config(root, "playbook.toml", config_body) + + result = run_cli("-config", str(config_path)) + self.assertEqual(result.returncode, 0, msg=result.stdout + result.stderr) + + agents_index = (root / ".agents" / "index.md").read_text(encoding="utf-8") + self.assertIn("`.agents/tsl/`:TSL 相关规则集", agents_index) + self.assertIn("`.agents/markdown/`:Markdown 相关规则集", agents_index) + self.assertNotIn("`.agents/cpp/`", agents_index) + self.assertNotIn("`.agents/python/`", agents_index) + self.assertNotIn("`.agents/typescript/`", agents_index) + def test_sync_standards_agents_block_has_blank_lines(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" diff --git a/tests/test_no_backup_flags.py b/tests/test_no_backup_flags.py index 352296a..6c24628 100644 --- a/tests/test_no_backup_flags.py +++ b/tests/test_no_backup_flags.py @@ -74,6 +74,36 @@ no_backup = true git_backups = list(root.glob(".gitattributes.bak.*")) self.assertEqual(git_backups, []) + def test_install_skills_no_backup_replaces_existing_skill_without_backup(self): + with tempfile.TemporaryDirectory() as tmp_dir: + root = Path(tmp_dir) + skills_root = root / "agents" / "skills" + existing = skills_root / "brainstorming" + existing.mkdir(parents=True) + (existing / "stale.txt").write_text("old", encoding="utf-8") + + config_body = f""" +[playbook] +project_root = "{tmp_dir}" +deploy_root = "{DEFAULT_DEPLOY_ROOT}" + +[install_skills] +agents_home = "{root / 'agents'}" +mode = "list" +skills = ["brainstorming"] +no_backup = true +""" + config_path = root / "playbook.toml" + config_path.write_text(config_body, encoding="utf-8") + + result = run_cli("-config", str(config_path)) + self.assertEqual(result.returncode, 0, msg=result.stderr) + + backups = list(skills_root.glob("brainstorming.bak.*")) + self.assertEqual(backups, []) + self.assertFalse((existing / "stale.txt").exists()) + self.assertTrue((existing / "SKILL.md").is_file()) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_sync_templates_placeholders.py b/tests/test_sync_templates_placeholders.py index 0750d4c..1c9e33a 100644 --- a/tests/test_sync_templates_placeholders.py +++ b/tests/test_sync_templates_placeholders.py @@ -27,7 +27,29 @@ def run_script(script_path: Path, *args, cwd: Path | None = None): class SyncTemplatesPlaceholdersTests(unittest.TestCase): - def test_main_language_placeholder_replaced(self): + def test_templates_no_longer_expose_main_language_placeholder(self): + example_text = (ROOT / "playbook.toml.example").read_text(encoding="utf-8") + self.assertNotIn("main_language", example_text) + + templates_readme = (ROOT / "templates" / "README.md").read_text( + encoding="utf-8" + ) + self.assertNotIn("{{MAIN_LANGUAGE}}", templates_readme) + self.assertNotIn("{{LANGUAGE_1}}", templates_readme) + + agents_template = (ROOT / "templates" / "AGENTS.template.md").read_text( + encoding="utf-8" + ) + self.assertNotIn("{{MAIN_LANGUAGE}}", agents_template) + + tech_stack_template = ( + ROOT / "templates" / "memory-bank" / "tech-stack.template.md" + ).read_text(encoding="utf-8") + self.assertNotIn("{{MAIN_LANGUAGE}}", tech_stack_template) + self.assertNotIn("{{LANGUAGE_1}}", tech_stack_template) + self.assertNotIn("**主要语言**", tech_stack_template) + + def test_sync_templates_replaces_playbook_scripts_without_main_language_support(self): with tempfile.TemporaryDirectory() as tmp_dir: config_body = f""" [playbook] @@ -36,6 +58,8 @@ deploy_root = "{DEFAULT_DEPLOY_ROOT}" [sync_rules] +[sync_memory_bank] + [sync_standards] langs = [\"cpp\", \"tsl\"] """ @@ -50,6 +74,12 @@ langs = [\"cpp\", \"tsl\"] self.assertIn(".agents/cpp/index.md", text) self.assertNotIn("{{MAIN_LANGUAGE}}", text) + tech_stack = Path(tmp_dir) / "memory-bank" / "tech-stack.md" + tech_stack_text = tech_stack.read_text(encoding="utf-8") + self.assertNotIn("{{LANGUAGE_1}}", tech_stack_text) + self.assertNotIn("{{MAIN_LANGUAGE}}", tech_stack_text) + self.assertNotIn("**主要语言**", tech_stack_text) + rules_md = Path(tmp_dir) / "AGENT_RULES.md" rules_text = rules_md.read_text(encoding="utf-8") self.assertIn("docs/standards/playbook/scripts/main_loop.py claim", rules_text) @@ -57,6 +87,7 @@ langs = [\"cpp\", \"tsl\"] self.assertIn("不得直接使用 `$executing-plans`", rules_text) self.assertIn("不得直接使用 `$subagent-driven-development`", rules_text) self.assertNotIn("{{PLAYBOOK_SCRIPTS}}", rules_text) + self.assertFalse(rules_text.endswith("\n\n")) def test_sync_standards_rewrites_typescript_docs_prefix_for_vendored_playbook(self): with tempfile.TemporaryDirectory() as tmp_dir: diff --git a/tests/test_tsl_entrypoints_consistency.py b/tests/test_tsl_entrypoints_consistency.py index d9b9cc0..2b1115c 100644 --- a/tests/test_tsl_entrypoints_consistency.py +++ b/tests/test_tsl_entrypoints_consistency.py @@ -8,6 +8,7 @@ PLAYBOOK_EXAMPLE = ROOT / "playbook.toml.example" SKILLS_DOC = ROOT / "SKILLS.md" TEMPLATES_CI_README = ROOT / "templates" / "ci" / "README.md" REMOVED_TSL_GUIDE = ROOT / "codex" / "skills" / "tsl-guide" +REFERENCE_CATALOG_SOURCE_INDEX = ROOT / "data" / "tsl_reference_catalog_source" / "index.md" class TslEntrypointsConsistencyTests(unittest.TestCase): @@ -51,6 +52,11 @@ class TslEntrypointsConsistencyTests(unittest.TestCase): self.assertNotIn("tsl-guide", SKILLS_DOC.read_text(encoding="utf-8")) self.assertNotIn("$tsl-guide", RULESET_TSL.read_text(encoding="utf-8")) + def test_reference_catalog_source_index_uses_canonical_reference_entrypoint(self): + text = REFERENCE_CATALOG_SOURCE_INDEX.read_text(encoding="utf-8") + self.assertIn("docs/tsl/reference/catalog/index.md", text) + self.assertNotIn("docs/tsl/syntax_book/function/index.md", text) + if __name__ == "__main__": unittest.main()