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:
@@ -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),
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user