v1.9.40: add Codex integration — skill setup, deploy, GUI buttons

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
IPGO Developer
2026-03-07 07:27:22 +00:00
parent ddd6951610
commit e2bdffb41e
14 changed files with 1067 additions and 130 deletions

View File

@@ -1,12 +1,15 @@
"""
Claude Code integration setup.
Installs ssh.py, encryption.py, /ssh skill, SSH key — everything needed
for Claude Code to manage servers via the shared servers.json.
Local AI agent integration setup.
Installs the shared ssh.py/encryption.py backend, Claude /ssh command,
Codex skill package, platform-specific wrappers, and SSH key material.
"""
import os
import sys
import re
import shutil
import subprocess
import sys
from core.logger import log
SHARED_DIR = os.path.expanduser("~/.server-connections")
@@ -19,13 +22,24 @@ else:
SSH_SCRIPT_SRC = os.path.join(_BASE_DIR, "tools", "ssh.py")
ENCRYPTION_SRC = os.path.join(_BASE_DIR, "core", "encryption.py")
SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md")
CLAUDE_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")
CLAUDE_SKILL_DST_DIR = os.path.expanduser("~/.claude/commands")
CLAUDE_SKILL_DST = os.path.join(CLAUDE_SKILL_DST_DIR, "ssh.md")
SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519")
GLOBAL_CLAUDE_MD = os.path.expanduser("~/.claude/CLAUDE.md")
CODEX_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".codex", "skills", "server-manager")
CODEX_SKILL_DST_ROOT = os.path.expanduser("~/.codex/skills")
CODEX_SKILL_DST_DIR = os.path.join(CODEX_SKILL_DST_ROOT, "server-manager")
CODEX_SKILL_ENTRY = os.path.join(CODEX_SKILL_DST_DIR, "SKILL.md")
CODEX_WRAPPER_SRC_SH = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.sh")
CODEX_WRAPPER_SRC_CMD = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.cmd")
CODEX_WRAPPER_DST = os.path.join(
SHARED_DIR,
"codex-ssh.cmd" if sys.platform == "win32" else "codex-ssh",
)
_BLOCK_START = "<!-- server-manager:start -->"
_BLOCK_END = "<!-- server-manager:end -->"
@@ -67,6 +81,27 @@ python ~/.server-connections/ssh.py --status # online/offline
"""
def _ensure_executable(path: str):
if sys.platform == "win32" or not os.path.exists(path):
return
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o755)
def _copy_file(src: str, dst: str, executable: bool = False) -> str:
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
if executable:
_ensure_executable(dst)
return dst
def _copy_tree(src: str, dst: str) -> str:
os.makedirs(dst, exist_ok=True)
shutil.copytree(src, dst, dirs_exist_ok=True)
return dst
def check_status() -> dict:
"""Check what's installed and what's missing."""
return {
@@ -74,7 +109,9 @@ def check_status() -> dict:
"servers_json": os.path.exists(os.path.join(SHARED_DIR, "servers.json")),
"ssh_script": os.path.exists(os.path.join(SHARED_DIR, "ssh.py")),
"encryption": os.path.exists(os.path.join(SHARED_DIR, "encryption.py")),
"skill_installed": os.path.exists(SKILL_DST),
"claude_skill_installed": os.path.exists(CLAUDE_SKILL_DST),
"codex_skill_installed": os.path.exists(CODEX_SKILL_ENTRY),
"codex_wrapper_installed": os.path.exists(CODEX_WRAPPER_DST),
"ssh_key_exists": os.path.exists(SSH_KEY_PATH),
"ssh_key_pub": os.path.exists(SSH_KEY_PATH + ".pub"),
}
@@ -85,10 +122,9 @@ def install_ssh_script() -> str:
os.makedirs(SHARED_DIR, exist_ok=True)
results = []
# Copy ssh.py
dst = os.path.join(SHARED_DIR, "ssh.py")
if os.path.exists(SSH_SCRIPT_SRC):
shutil.copy2(SSH_SCRIPT_SRC, dst)
_copy_file(SSH_SCRIPT_SRC, dst, executable=True)
log.info(f"ssh.py installed: {dst}")
results.append(f"ssh.py installed: {dst}")
elif os.path.exists(dst):
@@ -96,10 +132,9 @@ def install_ssh_script() -> str:
else:
results.append("ERROR: ssh.py source not found")
# Copy encryption.py
enc_dst = os.path.join(SHARED_DIR, "encryption.py")
if os.path.exists(ENCRYPTION_SRC):
shutil.copy2(ENCRYPTION_SRC, enc_dst)
_copy_file(ENCRYPTION_SRC, enc_dst)
log.info(f"encryption.py installed: {enc_dst}")
results.append(f"encryption.py installed: {enc_dst}")
elif os.path.exists(enc_dst):
@@ -110,22 +145,59 @@ def install_ssh_script() -> str:
return "\n".join(results)
def install_skill() -> str:
def install_claude_skill() -> str:
"""Install /ssh skill for Claude Code."""
os.makedirs(SKILL_DST_DIR, exist_ok=True)
if os.path.exists(SKILL_SRC):
shutil.copy2(SKILL_SRC, SKILL_DST)
log.info(f"Skill installed: {SKILL_DST}")
return f"Skill installed: {SKILL_DST}"
# Fallback: check existing
if os.path.exists(SKILL_DST):
return f"Skill already exists: {SKILL_DST}"
# Generate minimal skill
os.makedirs(CLAUDE_SKILL_DST_DIR, exist_ok=True)
if os.path.exists(CLAUDE_SKILL_SRC):
_copy_file(CLAUDE_SKILL_SRC, CLAUDE_SKILL_DST)
log.info(f"Claude skill installed: {CLAUDE_SKILL_DST}")
return f"Claude skill installed: {CLAUDE_SKILL_DST}"
if os.path.exists(CLAUDE_SKILL_DST):
return f"Claude skill already exists: {CLAUDE_SKILL_DST}"
skill_content = _generate_skill_content()
with open(SKILL_DST, "w", encoding="utf-8") as f:
with open(CLAUDE_SKILL_DST, "w", encoding="utf-8") as f:
f.write(skill_content)
log.info(f"Skill generated: {SKILL_DST}")
return f"Skill generated: {SKILL_DST}"
log.info(f"Claude skill generated: {CLAUDE_SKILL_DST}")
return f"Claude skill generated: {CLAUDE_SKILL_DST}"
def install_codex_skill() -> str:
"""Install ServerManager skill package for Codex and the local wrapper."""
results = []
if os.path.isdir(CODEX_SKILL_SRC_DIR):
_copy_tree(CODEX_SKILL_SRC_DIR, CODEX_SKILL_DST_DIR)
for rel_path in [
os.path.join("scripts", "server-manager-doctor.sh"),
os.path.join("scripts", "server-manager-doctor.cmd"),
os.path.join("scripts", "codex-ssh-wrapper.sh"),
os.path.join("scripts", "codex-ssh-wrapper.cmd"),
]:
_ensure_executable(os.path.join(CODEX_SKILL_DST_DIR, rel_path))
log.info(f"Codex skill installed: {CODEX_SKILL_DST_DIR}")
results.append(f"Codex skill installed: {CODEX_SKILL_DST_DIR}")
elif os.path.exists(CODEX_SKILL_ENTRY):
results.append(f"Codex skill already exists: {CODEX_SKILL_DST_DIR}")
else:
results.append("ERROR: Codex skill source not found")
wrapper_src = CODEX_WRAPPER_SRC_CMD if sys.platform == "win32" else CODEX_WRAPPER_SRC_SH
if os.path.exists(wrapper_src):
_copy_file(wrapper_src, CODEX_WRAPPER_DST, executable=(sys.platform != "win32"))
log.info(f"Codex wrapper installed: {CODEX_WRAPPER_DST}")
results.append(f"Codex wrapper installed: {CODEX_WRAPPER_DST}")
elif os.path.exists(CODEX_WRAPPER_DST):
results.append(f"Codex wrapper already exists: {CODEX_WRAPPER_DST}")
else:
results.append("ERROR: Codex wrapper source not found")
return "\n".join(results)
def install_skill() -> str:
"""Backward-compatible alias for the Claude /ssh skill installer."""
return install_claude_skill()
def generate_ssh_key() -> str:
@@ -135,7 +207,6 @@ def generate_ssh_key() -> str:
os.makedirs(os.path.dirname(SSH_KEY_PATH), exist_ok=True)
import subprocess
try:
subprocess.run(
["ssh-keygen", "-t", "ed25519", "-f", SSH_KEY_PATH,
@@ -156,11 +227,7 @@ def generate_ssh_key() -> str:
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
"""Add/update server manager section in global ~/.claude/CLAUDE.md."""
os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True)
existing = ""
@@ -174,29 +241,28 @@ def install_global_claude_md() -> str:
)
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}"
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."""
"""Full setup — install everything for Claude Code and Codex."""
results = []
steps = [
("ssh_script", install_ssh_script),
("skill", install_skill),
("claude_skill", install_claude_skill),
("codex_skill", install_codex_skill),
("ssh_key", generate_ssh_key),
("global_claude_md", install_global_claude_md),
]