From 006ca2bf9f62ed8710b1b36045fab6e0fac8804f Mon Sep 17 00:00:00 2001 From: delta-cloud-208e Date: Fri, 27 Feb 2026 07:11:39 +0000 Subject: [PATCH] feat: add idempotent global CLAUDE.md install with start/end markers Uses markers to safely add or replace the server-manager section. Re-running install_all() updates the block without creating duplicates. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- core/claude_setup.py | 55 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/core/claude_setup.py b/core/claude_setup.py index 781bdd7..d08364a 100644 --- a/core/claude_setup.py +++ b/core/claude_setup.py @@ -24,6 +24,25 @@ SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md") SKILL_DST_DIR = os.path.expanduser("~/.claude/commands") SKILL_DST = os.path.join(SKILL_DST_DIR, "ssh.md") SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519") +GLOBAL_CLAUDE_MD = os.path.expanduser("~/.claude/CLAUDE.md") + +_BLOCK_START = "" +_BLOCK_END = "" + +GLOBAL_CLAUDE_MD_BLOCK = f"""{_BLOCK_START} +## Server Manager — управление серверами + +**ВСЕГДА** используй server manager для подключения к серверам. Никогда не используй `ssh`, `sshpass` или прямые подключения. + +- Скилл: `/ssh ALIAS "command"` — выполнить команду на сервере +- Список серверов: `python3 ~/.server-connections/ssh.py --list` +- Документация: `~/.claude/commands/ssh.md` +- Memory bank: проект `global-infrastructure` → `techContext.md` +- Инфраструктура: https://git.sensey24.ru/aibot777/infrastructure-docs + +**Запрещено:** использовать `ssh`, `sshpass`, читать `~/.server-connections/` напрямую, раскрывать IP/пароли/порты. +{_BLOCK_END} +""" def check_status() -> dict: @@ -112,6 +131,41 @@ def generate_ssh_key() -> str: return f"Key generated: {SSH_KEY_PATH}" +def install_global_claude_md() -> str: + """Add/update server manager section in global ~/.claude/CLAUDE.md. + + Uses start/end markers to safely replace existing block without duplication. + """ + import re + os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True) + + existing = "" + if os.path.exists(GLOBAL_CLAUDE_MD): + with open(GLOBAL_CLAUDE_MD, encoding="utf-8") as f: + existing = f.read() + + pattern = re.compile( + re.escape(_BLOCK_START) + r".*?" + re.escape(_BLOCK_END), + re.DOTALL + ) + + if pattern.search(existing): + # Блок уже есть — заменяем на актуальную версию + updated = pattern.sub(GLOBAL_CLAUDE_MD_BLOCK.strip(), existing) + with open(GLOBAL_CLAUDE_MD, "w", encoding="utf-8") as f: + f.write(updated) + log.info(f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}") + return f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}" + else: + # Блока нет — добавляем в конец + with open(GLOBAL_CLAUDE_MD, "a", encoding="utf-8") as f: + if existing and not existing.endswith("\n"): + f.write("\n") + f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK) + log.info(f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}") + return f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}" + + def install_all() -> list[str]: """Full setup — install everything.""" results = [] @@ -120,6 +174,7 @@ def install_all() -> list[str]: ("ssh_script", install_ssh_script), ("skill", install_skill), ("ssh_key", generate_ssh_key), + ("global_claude_md", install_global_claude_md), ] for name, func in steps: