feat: add Gemini skill integration and multi-user AI setup

This commit is contained in:
Codex
2026-03-11 19:30:27 +00:00
parent daa11ca440
commit 9da3125c34
18 changed files with 1239 additions and 250 deletions

View File

@@ -1,6 +1,6 @@
---
name: server-manager
description: Use ServerManager's shared local server inventory and ssh.py utility to manage configured SSH, Telnet, SQL, Redis, S3/MinIO, Grafana, Prometheus, and WinRM endpoints by alias without exposing credentials. Use when the user asks to operate on servers managed by ServerManager or when editing ServerManager's Claude/Codex integration.
description: Use ServerManager's shared local server inventory and ssh.py utility to manage configured SSH, Telnet, SQL, Redis, S3/MinIO, Grafana, Prometheus, and WinRM endpoints by alias without exposing credentials. Use when the user asks to operate on servers managed by ServerManager or when editing ServerManager's Claude/Codex/Gemini integration.
metadata:
short-description: Safe remote ops through ServerManager aliases
---
@@ -10,7 +10,7 @@ metadata:
Use this skill for two cases:
1. The user wants work done on a server or service already configured in ServerManager.
2. The user wants to modify ServerManager's CLI/integration layer so Claude/Codex can use it safely.
2. The user wants to modify ServerManager's CLI/integration layer so Claude/Codex/Gemini can use it safely.
## First Step

View File

@@ -1,6 +1,6 @@
# Project Notes
This skill is based on `/home/code/CODING/server-manager`.
This skill is based on `/home/code/Desktop/CODING/server-manager`.
## What ServerManager Is
@@ -31,8 +31,8 @@ The AI never needs raw credentials. It only uses aliases and the local CLI.
- `CLAUDE.md`: project rules, architecture, security, workflow
- `tools/ssh.py`: CLI entry point used by AI tools
- `tools/skill-ssh.md`: current Claude `/ssh` instructions
- `core/claude_setup.py`: installer for shared CLI files, Claude command, and Codex skill
- `build.py`: auto-deploys `ssh.py`, `encryption.py`, Claude skill, and Codex skill after builds
- `core/claude_setup.py`: installer for shared CLI files plus Claude/Codex/Gemini skills
- `build.py`: auto-deploys `ssh.py`, `encryption.py`, Claude skill, Codex skill, and Gemini skill after builds
## Architectural Shape

8
.gemini/settings.json Normal file
View File

@@ -0,0 +1,8 @@
{
"context": {
"fileName": "GEMINI.md"
},
"experimental": {
"enableAgents": true
}
}

View File

@@ -0,0 +1,84 @@
---
name: server-manager
description: Use ServerManager's shared local server inventory and ssh.py utility to manage configured SSH, Telnet, SQL, Redis, S3/MinIO, Grafana, Prometheus, and WinRM endpoints by alias without exposing credentials. Use when the user asks to operate on servers managed by ServerManager or when editing ServerManager's Claude/Codex/Gemini integration.
---
# Server Manager
Use this skill for two cases:
1. The user wants work done on a server or service already configured in ServerManager.
2. The user wants to modify ServerManager's CLI/integration layer so Claude/Codex/Gemini can use it safely.
## First Step
Before any server operation:
```bash
$HOME/.server-connections/gemini-ssh --list
```
Read the `Type` column before choosing commands. Do not guess the server type.
If the wrapper is missing, run the doctor script for your platform:
```bash
$HOME/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.sh
```
On Windows, use:
```bat
%USERPROFILE%\.gemini\skills\server-manager\scripts\server-manager-gemini-doctor.cmd
```
## Hard Rules
- Never read `~/.server-connections/servers.json`, `settings.json`, or `encryption.py` directly.
- Never use `--list-full`.
- Never use raw `ssh`, `scp`, `rsync`, `redis-cli`, `mysql`, `psql`, `mc`, `aws s3`, or similar tools unless the user explicitly asks to bypass ServerManager.
- Maximum one connection attempt per action. If it times out or fails, report it and stop.
- `ALIAS "command"` is only for `ssh` and `telnet`.
- `rdp` and `vnc` are GUI-only. Do not invent CLI access.
- For S3/MinIO, list buckets and paths before upload, delete, or URL generation.
- Ask for confirmation before destructive actions if the user's intent is not explicit.
## Preferred Entry Points
Use the shared wrapper:
```bash
$HOME/.server-connections/gemini-ssh ...
```
Safe discovery commands:
```bash
$HOME/.server-connections/gemini-ssh --list
$HOME/.server-connections/gemini-ssh --info ALIAS
$HOME/.server-connections/gemini-ssh --status
```
Read [references/command-matrix.md](references/command-matrix.md) when you need the per-type command matrix.
## Server Operation Workflow
1. Run `--list`.
2. Match the alias using notes/type, not credentials.
3. Pick commands strictly from the server type.
4. Execute exactly one action.
5. Report the result without exposing IPs, logins, passwords, ports, or secrets.
## Working On ServerManager Itself
Read [references/project.md](references/project.md) before changing integration code.
Source-of-truth files:
- `tools/ssh.py`: local CLI used by AI tools
- `tools/skill-ssh.md`: current Claude `/ssh` instructions
- `core/claude_setup.py`: installer for shared CLI files and AI skills
- `build.py`: auto-deploys `ssh.py`, `encryption.py`, Claude/Codex/Gemini skills after builds
- `README.md`, `CLAUDE.md`, and `GEMINI.md`: project-level rules and architecture
If you change command semantics in `tools/ssh.py`, update the user-facing instructions alongside it.

View File

@@ -0,0 +1,91 @@
# Command Matrix
Always identify the server type first with:
```bash
$HOME/.server-connections/gemini-ssh --list
```
## Type To Command Map
| Type | Use | Do Not Use |
| --- | --- | --- |
| `ssh` | `ALIAS "command"`, `--upload`, `--download`, `--ping`, `--install-key` | n/a |
| `telnet` | `ALIAS "command"` | `--upload`, `--download`, `--install-key` |
| `mariadb`, `mssql`, `postgresql` | `--sql`, `--sql-databases`, `--sql-tables` | `ALIAS "command"` |
| `redis` | `--redis`, `--redis-info`, `--redis-keys` | `ALIAS "command"` |
| `s3` | `--s3-buckets`, `--s3-ls`, `--s3-upload`, `--s3-download`, `--s3-delete`, `--s3-url`, `--s3-create-bucket` | `ALIAS "command"`, SSH/SFTP commands |
| `grafana` | `--grafana-dashboards`, `--grafana-alerts` | `ALIAS "command"` |
| `prometheus` | `--prom-query`, `--prom-targets`, `--prom-alerts` | `ALIAS "command"` |
| `winrm` | `--ps`, `--cmd` | `ALIAS "command"` |
| `rdp`, `vnc` | GUI only | all CLI actions |
## Common Safe Commands
```bash
$HOME/.server-connections/gemini-ssh --list
$HOME/.server-connections/gemini-ssh --info ALIAS
$HOME/.server-connections/gemini-ssh --status
$HOME/.server-connections/gemini-ssh --set-note ALIAS "description"
```
## SSH And Telnet
```bash
$HOME/.server-connections/gemini-ssh ALIAS "command"
$HOME/.server-connections/gemini-ssh ALIAS --no-sudo "command"
$HOME/.server-connections/gemini-ssh ALIAS --upload "local" //remote/path
$HOME/.server-connections/gemini-ssh ALIAS --download //remote/path "local"
$HOME/.server-connections/gemini-ssh ALIAS --ping
```
Use double slashes for remote SSH/SFTP paths when working from Git Bash style environments.
## SQL
```bash
$HOME/.server-connections/gemini-ssh --sql ALIAS "SELECT * FROM table LIMIT 10"
$HOME/.server-connections/gemini-ssh --sql-databases ALIAS
$HOME/.server-connections/gemini-ssh --sql-tables ALIAS [database]
```
## Redis
```bash
$HOME/.server-connections/gemini-ssh --redis ALIAS "GET key"
$HOME/.server-connections/gemini-ssh --redis-info ALIAS
$HOME/.server-connections/gemini-ssh --redis-keys ALIAS "pattern:*"
```
## S3 / MinIO
Before modifying objects:
```bash
$HOME/.server-connections/gemini-ssh --s3-buckets ALIAS
$HOME/.server-connections/gemini-ssh --s3-ls ALIAS bucket/prefix/
```
Then act:
```bash
$HOME/.server-connections/gemini-ssh --s3-upload ALIAS "local" bucket/key
$HOME/.server-connections/gemini-ssh --s3-download ALIAS bucket/key "local"
$HOME/.server-connections/gemini-ssh --s3-delete ALIAS bucket/key
$HOME/.server-connections/gemini-ssh --s3-url ALIAS bucket/key [seconds]
$HOME/.server-connections/gemini-ssh --s3-create-bucket ALIAS bucket-name
```
Do not treat S3 as a shell filesystem.
## Grafana / Prometheus / WinRM
```bash
$HOME/.server-connections/gemini-ssh --grafana-dashboards ALIAS
$HOME/.server-connections/gemini-ssh --grafana-alerts ALIAS
$HOME/.server-connections/gemini-ssh --prom-query ALIAS "up"
$HOME/.server-connections/gemini-ssh --prom-targets ALIAS
$HOME/.server-connections/gemini-ssh --prom-alerts ALIAS
$HOME/.server-connections/gemini-ssh --ps ALIAS "Get-Process"
$HOME/.server-connections/gemini-ssh --cmd ALIAS "dir"
```

View File

@@ -0,0 +1,73 @@
# Project Notes
This skill is based on `/home/code/Desktop/CODING/server-manager`.
## What ServerManager Is
ServerManager is a cross-platform desktop GUI built with CustomTkinter. It manages multiple remote endpoint types through one local encrypted inventory:
- SSH / Telnet
- MariaDB / MSSQL / PostgreSQL
- Redis
- S3 / MinIO
- Grafana
- Prometheus
- WinRM
- RDP / VNC launchers
## Core Integration Model
The GUI and CLI share one local backend:
```text
ServerManager GUI <-> ~/.server-connections/servers.json <-> ~/.server-connections/ssh.py
```
The AI never needs raw credentials. It only uses aliases and the local CLI.
## Important Files
- `README.md`: product overview and install flow
- `CLAUDE.md`: project rules, architecture, security, workflow
- `GEMINI.md`: Gemini-native project contract
- `tools/ssh.py`: CLI entry point used by AI tools
- `tools/skill-ssh.md`: current Claude `/ssh` instructions
- `core/claude_setup.py`: installer for shared CLI files plus Claude/Codex/Gemini skill deployment
- `build.py`: auto-deploys `ssh.py`, `encryption.py`, Claude skill, Codex skill, and Gemini skill after builds
## Architectural Shape
- `core/server_store.py`: encrypted storage, CRUD, observers, backups
- `core/connection_factory.py`: type-to-client factory with lazy imports
- `core/*_client.py`: protocol-specific backends
- `gui/app.py`: tab registry, conditional tabs by server type
- `gui/tabs/`: protocol-specific GUI surfaces
## Existing Local Agent Integration
Current setup installs:
- `~/.server-connections/ssh.py`
- `~/.server-connections/encryption.py`
- `~/.claude/commands/ssh.md`
- `~/.codex/skills/server-manager/`
- `~/.gemini/skills/server-manager/`
- `~/.agents/skills/server-manager/` (cross-tool mirror)
- `~/.server-connections/codex-ssh` or `codex-ssh.cmd`
- `~/.server-connections/gemini-ssh` or `gemini-ssh.cmd`
- a `~/.claude/CLAUDE.md` guidance block
- a `~/.gemini/GEMINI.md` guidance block
The Gemini skill mirrors the same safety model:
- use aliases only
- use the shared local CLI
- never read credentials directly
- choose commands by server type
## Local Findings
- `ssh.py` is executable and uses a `python3` shebang, so Gemini does not need a `python` alias.
- `ssh.py` has no `--help`; use `--list`, `--info`, and `--status` for safe discovery.
- The Unix wrapper path covers both Linux and macOS through `gemini-ssh-wrapper.sh`.
- Windows-native Gemini wrapper support exists through `gemini-ssh-wrapper.cmd`.

View File

@@ -0,0 +1,11 @@
@echo off
setlocal
set SHARED_DIR=%SERVER_MANAGER_SHARED_DIR%
if "%SHARED_DIR%"=="" set SHARED_DIR=%USERPROFILE%\.server-connections
set SSH_SCRIPT=%SHARED_DIR%\ssh.py
if not exist "%SSH_SCRIPT%" (
echo error: missing executable ssh.py at %SSH_SCRIPT% 1>&2
echo hint: install ServerManager's shared CLI files first 1>&2
exit /b 1
)
"%SSH_SCRIPT%" %*

View File

@@ -0,0 +1,13 @@
#!/usr/bin/env bash
set -euo pipefail
shared_dir="${SERVER_MANAGER_SHARED_DIR:-$HOME/.server-connections}"
ssh_script="${shared_dir}/ssh.py"
if [[ ! -x "$ssh_script" ]]; then
echo "error: missing executable ssh.py at ${ssh_script}" >&2
echo "hint: install ServerManager's shared CLI files first" >&2
exit 1
fi
exec "$ssh_script" "$@"

View File

@@ -0,0 +1,39 @@
@echo off
setlocal
set SHARED_DIR=%SERVER_MANAGER_SHARED_DIR%
if "%SHARED_DIR%"=="" set SHARED_DIR=%USERPROFILE%\.server-connections
set SSH_SCRIPT=%SHARED_DIR%\ssh.py
set ENCRYPTION=%SHARED_DIR%\encryption.py
set WRAPPER=%SHARED_DIR%\gemini-ssh.cmd
set SKILL=%USERPROFILE%\.gemini\skills\server-manager\SKILL.md
set STATUS=0
if exist "%ENCRYPTION%" (
echo [ok] file %ENCRYPTION%
) else (
echo [missing] file %ENCRYPTION% 1>&2
set STATUS=1
)
if exist "%SSH_SCRIPT%" (
echo [ok] file %SSH_SCRIPT%
) else (
echo [missing] file %SSH_SCRIPT% 1>&2
set STATUS=1
)
if exist "%WRAPPER%" (
echo [ok] file %WRAPPER%
) else (
echo [missing] file %WRAPPER% 1>&2
set STATUS=1
)
if exist "%SKILL%" (
echo [ok] file %SKILL%
) else (
echo [missing] file %SKILL% 1>&2
set STATUS=1
)
exit /b %STATUS%

View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -euo pipefail
shared_dir="${SERVER_MANAGER_SHARED_DIR:-$HOME/.server-connections}"
ssh_script="${shared_dir}/ssh.py"
encryption_module="${shared_dir}/encryption.py"
wrapper="${shared_dir}/gemini-ssh"
skill_dir="$HOME/.gemini/skills/server-manager"
status=0
check_file() {
local path="$1"
if [[ -f "$path" ]]; then
printf '[ok] file %s\n' "$path"
else
printf '[missing] file %s\n' "$path" >&2
status=1
fi
}
check_exec() {
local path="$1"
if [[ -x "$path" ]]; then
printf '[ok] executable %s\n' "$path"
else
printf '[missing] executable %s\n' "$path" >&2
status=1
fi
}
check_file "$encryption_module"
check_exec "$ssh_script"
check_exec "$wrapper"
check_file "$skill_dir/SKILL.md"
exit "$status"

52
GEMINI.md Normal file
View File

@@ -0,0 +1,52 @@
# Gemini Project Contract
This repository is **ServerManager** — a cross-platform desktop GUI for managing remote servers and services through one encrypted local inventory.
Use this file as the native Gemini contract for sessions started inside this repository.
## First Read
Read these files first when relevant:
- `README.md`
- `CLAUDE.md`
- `CHANGELOG.md`
- `core/claude_setup.py`
- `tools/ssh.py`
## Default Role
- Gemini is a secondary implementation and review helper, not the owner of the project state.
- Prefer minimal safe changes that preserve Claude and Codex integration behavior.
- When changing AI integration code, keep Claude `/ssh`, Codex `server-manager`, and Gemini `server-manager` behavior aligned.
## Project-Specific Rules
- Never read or print secrets from `~/.server-connections/servers.json`, `settings.json`, or `encryption.py`.
- Treat `tools/ssh.py` as the shared transport layer for Claude, Codex, and Gemini.
- Keep cross-platform behavior explicit for Linux, macOS, and Windows.
- Prefer shared installer logic in `core/claude_setup.py` over duplicated per-tool logic.
- If command semantics change in `tools/ssh.py`, update all relevant user-facing skill docs.
## Native Gemini Entry Points
- Project contract: `GEMINI.md`
- Gemini settings: `.gemini/settings.json`
- Workspace skill: `.gemini/skills/server-manager/`
## Safe Server Workflow
When the user asks to operate on a server already configured in ServerManager:
1. Use the installed ServerManager Gemini skill.
2. First enumerate aliases safely.
3. Determine endpoint type before choosing a command.
4. Use the shared CLI wrapper, not raw credentials.
Preferred discovery commands:
```bash
$HOME/.server-connections/gemini-ssh --list
$HOME/.server-connections/gemini-ssh --info ALIAS
$HOME/.server-connections/gemini-ssh --status
```

View File

@@ -114,8 +114,12 @@ def build():
"--add-data", f"config/servers.example.json{os.pathsep}config",
"--add-data", f"tools/ssh.py{os.pathsep}tools",
"--add-data", f"tools/skill-ssh.md{os.pathsep}tools",
"--add-data", f"tools/install_ai_integrations.py{os.pathsep}tools",
"--add-data", f"core/encryption.py{os.pathsep}core",
"--add-data", f".codex/skills/server-manager{os.pathsep}.codex/skills/server-manager",
"--add-data", f".gemini/skills/server-manager{os.pathsep}.gemini/skills/server-manager",
"--add-data", f".gemini/settings.json{os.pathsep}.gemini",
"--add-data", f"GEMINI.md{os.pathsep}.",
]
# PNG icons for GUI (Material Design)
@@ -355,6 +359,7 @@ def deploy_shared_files():
from core.claude_setup import (
install_claude_skill,
install_codex_skill,
install_gemini_skill,
install_ssh_script,
)
@@ -362,6 +367,7 @@ def deploy_shared_files():
install_ssh_script,
install_claude_skill,
install_codex_skill,
install_gemini_skill,
]
deployed = []

View File

@@ -1,7 +1,7 @@
"""
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.
Codex/Gemini skill packages, platform-specific wrappers, and SSH key material.
"""
import os
@@ -12,8 +12,6 @@ import sys
from core.logger import log
SHARED_DIR = os.path.expanduser("~/.server-connections")
# PyInstaller: bundled data is in sys._MEIPASS; otherwise use project dir
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
_BASE_DIR = sys._MEIPASS
@@ -23,25 +21,19 @@ else:
SSH_SCRIPT_SRC = os.path.join(_BASE_DIR, "tools", "ssh.py")
ENCRYPTION_SRC = os.path.join(_BASE_DIR, "core", "encryption.py")
CLAUDE_SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-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")
GEMINI_CONTRACT_SRC = os.path.join(_BASE_DIR, "GEMINI.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",
)
GEMINI_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".gemini", "skills", "server-manager")
GEMINI_WRAPPER_SRC_SH = os.path.join(GEMINI_SKILL_SRC_DIR, "scripts", "gemini-ssh-wrapper.sh")
GEMINI_WRAPPER_SRC_CMD = os.path.join(GEMINI_SKILL_SRC_DIR, "scripts", "gemini-ssh-wrapper.cmd")
_BLOCK_START = "<!-- server-manager:start -->"
_BLOCK_END = "<!-- server-manager:end -->"
_GEMINI_BLOCK_START = "<!-- server-manager-gemini:start -->"
_GEMINI_BLOCK_END = "<!-- server-manager-gemini:end -->"
GLOBAL_CLAUDE_MD_BLOCK = f"""{_BLOCK_START}
## Серверы — ТОЛЬКО через /ssh
@@ -80,6 +72,110 @@ python ~/.server-connections/ssh.py --status # online/offline
{_BLOCK_END}
"""
GLOBAL_GEMINI_MD_BLOCK = f"""{_GEMINI_BLOCK_START}
## ServerManager — use the installed skill
When a user asks about a server managed by ServerManager, use the installed `server-manager` skill first.
Preferred discovery commands:
```bash
$HOME/.server-connections/gemini-ssh --list
$HOME/.server-connections/gemini-ssh --info ALIAS
$HOME/.server-connections/gemini-ssh --status
```
Rules:
- Never read `~/.server-connections/servers.json`, `settings.json`, or `encryption.py` directly.
- Never use `--list-full`.
- Never use raw `ssh`, `scp`, `redis-cli`, `psql`, `mysql`, `mc`, or cloud CLIs unless the user explicitly asks to bypass ServerManager.
- Choose commands strictly by the endpoint type reported by `--list`.
- Use exactly one connection attempt per action and stop on timeout/failure.
{_GEMINI_BLOCK_END}
"""
def _target_home() -> str:
override = os.environ.get("SERVER_MANAGER_TARGET_HOME", "").strip()
if override:
return os.path.abspath(os.path.expanduser(override))
return os.path.expanduser("~")
def _shared_dir() -> str:
return os.path.join(_target_home(), ".server-connections")
def _gemini_dir() -> str:
return os.path.join(_target_home(), ".gemini")
def _claude_skill_dst_dir() -> str:
return os.path.join(_target_home(), ".claude", "commands")
def _claude_skill_dst() -> str:
return os.path.join(_claude_skill_dst_dir(), "ssh.md")
def _ssh_key_path() -> str:
return os.path.join(_target_home(), ".ssh", "id_ed25519")
def _global_claude_md() -> str:
return os.path.join(_target_home(), ".claude", "CLAUDE.md")
def _global_gemini_md() -> str:
return os.path.join(_gemini_dir(), "GEMINI.md")
def _codex_skill_dst_root() -> str:
return os.path.join(_target_home(), ".codex", "skills")
def _codex_skill_dst_dir() -> str:
return os.path.join(_codex_skill_dst_root(), "server-manager")
def _codex_skill_entry() -> str:
return os.path.join(_codex_skill_dst_dir(), "SKILL.md")
def _codex_wrapper_dst() -> str:
return os.path.join(
_shared_dir(),
"codex-ssh.cmd" if sys.platform == "win32" else "codex-ssh",
)
def _gemini_skill_dst_root() -> str:
return os.path.join(_gemini_dir(), "skills")
def _gemini_skill_dst_dir() -> str:
return os.path.join(_gemini_skill_dst_root(), "server-manager")
def _gemini_skill_entry() -> str:
return os.path.join(_gemini_skill_dst_dir(), "SKILL.md")
def _agents_skill_dst_root() -> str:
return os.path.join(_target_home(), ".agents", "skills")
def _agents_skill_dst_dir() -> str:
return os.path.join(_agents_skill_dst_root(), "server-manager")
def _gemini_wrapper_dst() -> str:
return os.path.join(
_shared_dir(),
"gemini-ssh.cmd" if sys.platform == "win32" else "gemini-ssh",
)
def _ensure_executable(path: str):
if sys.platform == "win32" or not os.path.exists(path):
@@ -102,27 +198,96 @@ def _copy_tree(src: str, dst: str) -> str:
return dst
def _install_wrapper(src: str, dst: str) -> str:
return _copy_file(src, dst, executable=(sys.platform != "win32"))
def _skill_script_names() -> list[str]:
if sys.platform == "win32":
return [
os.path.join("scripts", "server-manager-doctor.cmd"),
os.path.join("scripts", "codex-ssh-wrapper.cmd"),
os.path.join("scripts", "server-manager-gemini-doctor.cmd"),
os.path.join("scripts", "gemini-ssh-wrapper.cmd"),
]
return [
os.path.join("scripts", "server-manager-doctor.sh"),
os.path.join("scripts", "codex-ssh-wrapper.sh"),
os.path.join("scripts", "server-manager-gemini-doctor.sh"),
os.path.join("scripts", "gemini-ssh-wrapper.sh"),
]
def _ensure_skill_scripts(skill_dir: str):
for rel_path in _skill_script_names():
_ensure_executable(os.path.join(skill_dir, rel_path))
def _iter_all_user_homes() -> list[str]:
homes: list[str] = []
def add(path: str):
expanded = os.path.abspath(os.path.expanduser(path))
if os.path.isdir(expanded) and expanded not in homes:
homes.append(expanded)
add(_target_home())
if sys.platform == "win32":
users_root = os.path.join(os.environ.get("SystemDrive", "C:"), "Users")
skip = {"public", "default", "default user", "all users"}
if os.path.isdir(users_root):
for name in sorted(os.listdir(users_root)):
if name.lower() in skip:
continue
add(os.path.join(users_root, name))
elif sys.platform == "darwin":
add("/var/root")
users_root = "/Users"
if os.path.isdir(users_root):
for name in sorted(os.listdir(users_root)):
if name.startswith("."):
continue
add(os.path.join(users_root, name))
else:
add("/root")
users_root = "/home"
if os.path.isdir(users_root):
for name in sorted(os.listdir(users_root)):
if name.startswith("."):
continue
add(os.path.join(users_root, name))
return homes
def check_status() -> dict:
"""Check what's installed and what's missing."""
shared_dir = _shared_dir()
ssh_key_path = _ssh_key_path()
return {
"shared_dir": os.path.exists(SHARED_DIR),
"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")),
"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"),
"target_home": _target_home(),
"shared_dir": os.path.exists(shared_dir),
"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")),
"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()),
"gemini_skill_installed": os.path.exists(_gemini_skill_entry()),
"gemini_wrapper_installed": os.path.exists(_gemini_wrapper_dst()),
"ssh_key_exists": os.path.exists(ssh_key_path),
"ssh_key_pub": os.path.exists(ssh_key_path + ".pub"),
}
def install_ssh_script() -> str:
"""Copy ssh.py and encryption.py to shared dir."""
os.makedirs(SHARED_DIR, exist_ok=True)
shared_dir = _shared_dir()
os.makedirs(shared_dir, exist_ok=True)
results = []
dst = os.path.join(SHARED_DIR, "ssh.py")
dst = os.path.join(shared_dir, "ssh.py")
if os.path.exists(SSH_SCRIPT_SRC):
_copy_file(SSH_SCRIPT_SRC, dst, executable=True)
log.info(f"ssh.py installed: {dst}")
@@ -132,7 +297,7 @@ def install_ssh_script() -> str:
else:
results.append("ERROR: ssh.py source not found")
enc_dst = os.path.join(SHARED_DIR, "encryption.py")
enc_dst = os.path.join(shared_dir, "encryption.py")
if os.path.exists(ENCRYPTION_SRC):
_copy_file(ENCRYPTION_SRC, enc_dst)
log.info(f"encryption.py installed: {enc_dst}")
@@ -147,54 +312,102 @@ def install_ssh_script() -> str:
def install_claude_skill() -> str:
"""Install /ssh skill for Claude Code."""
os.makedirs(CLAUDE_SKILL_DST_DIR, exist_ok=True)
claude_skill_dst_dir = _claude_skill_dst_dir()
claude_skill_dst = _claude_skill_dst()
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}"
_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(CLAUDE_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"Claude skill generated: {CLAUDE_SKILL_DST}")
return f"Claude skill generated: {CLAUDE_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 = []
codex_skill_dst_dir = _codex_skill_dst_dir()
codex_skill_entry = _codex_skill_entry()
codex_wrapper_dst = _codex_wrapper_dst()
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}")
_copy_tree(CODEX_SKILL_SRC_DIR, codex_skill_dst_dir)
_ensure_skill_scripts(codex_skill_dst_dir)
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}")
_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_gemini_skill() -> str:
"""Install ServerManager skill package for Gemini."""
results = []
gemini_skill_dst_dir = _gemini_skill_dst_dir()
gemini_skill_entry = _gemini_skill_entry()
agents_skill_dst_dir = _agents_skill_dst_dir()
gemini_wrapper_dst = _gemini_wrapper_dst()
install_generic_mirror = os.environ.get(
"SERVER_MANAGER_INSTALL_GENERIC_SKILL_MIRROR", ""
).strip() == "1"
if os.path.isdir(GEMINI_SKILL_SRC_DIR):
_copy_tree(GEMINI_SKILL_SRC_DIR, gemini_skill_dst_dir)
_ensure_skill_scripts(gemini_skill_dst_dir)
log.info(f"Gemini skill installed: {gemini_skill_dst_dir}")
results.append(f"Gemini skill installed: {gemini_skill_dst_dir}")
if install_generic_mirror:
_copy_tree(GEMINI_SKILL_SRC_DIR, agents_skill_dst_dir)
_ensure_skill_scripts(agents_skill_dst_dir)
log.info(f"Generic agents skill mirror installed: {agents_skill_dst_dir}")
results.append(f"Generic agents skill mirror installed: {agents_skill_dst_dir}")
elif os.path.exists(agents_skill_dst_dir):
shutil.rmtree(agents_skill_dst_dir, ignore_errors=True)
log.info(f"Removed generic agents skill mirror to avoid Gemini conflicts: {agents_skill_dst_dir}")
results.append(
f"Removed generic agents skill mirror to avoid Gemini conflicts: {agents_skill_dst_dir}"
)
elif os.path.exists(gemini_skill_entry):
results.append(f"Gemini skill already exists: {gemini_skill_dst_dir}")
else:
results.append("ERROR: Gemini skill source not found")
wrapper_src = GEMINI_WRAPPER_SRC_CMD if sys.platform == "win32" else GEMINI_WRAPPER_SRC_SH
if not os.path.exists(wrapper_src):
wrapper_src = CODEX_WRAPPER_SRC_CMD if sys.platform == "win32" else CODEX_WRAPPER_SRC_SH
if os.path.exists(wrapper_src):
_install_wrapper(wrapper_src, gemini_wrapper_dst)
log.info(f"Gemini wrapper installed: {gemini_wrapper_dst}")
results.append(f"Gemini wrapper installed: {gemini_wrapper_dst}")
elif os.path.exists(gemini_wrapper_dst):
results.append(f"Gemini wrapper already exists: {gemini_wrapper_dst}")
else:
results.append("ERROR: Gemini 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()
@@ -202,19 +415,20 @@ def install_skill() -> str:
def generate_ssh_key() -> str:
"""Generate ed25519 SSH key if not exists."""
if os.path.exists(SSH_KEY_PATH):
return f"Key already exists: {SSH_KEY_PATH}"
ssh_key_path = _ssh_key_path()
if os.path.exists(ssh_key_path):
return f"Key already exists: {ssh_key_path}"
os.makedirs(os.path.dirname(SSH_KEY_PATH), exist_ok=True)
os.makedirs(os.path.dirname(ssh_key_path), exist_ok=True)
try:
subprocess.run(
["ssh-keygen", "-t", "ed25519", "-f", SSH_KEY_PATH,
["ssh-keygen", "-t", "ed25519", "-f", ssh_key_path,
"-N", "", "-C", "server-manager"],
check=True, capture_output=True, timeout=15
)
log.info(f"SSH key generated: {SSH_KEY_PATH}")
return f"Key generated: {SSH_KEY_PATH}"
log.info(f"SSH key generated: {ssh_key_path}")
return f"Key generated: {ssh_key_path}"
except FileNotFoundError:
hint = "enable OpenSSH optional feature" if sys.platform == "win32" else "install openssh-client"
msg = f"ERROR: ssh-keygen not found — {hint}"
@@ -228,11 +442,12 @@ def generate_ssh_key() -> str:
def install_global_claude_md() -> str:
"""Add/update server manager section in global ~/.claude/CLAUDE.md."""
os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True)
global_claude_md = _global_claude_md()
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:
if os.path.exists(global_claude_md):
with open(global_claude_md, encoding="utf-8") as f:
existing = f.read()
pattern = re.compile(
@@ -242,31 +457,64 @@ 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:
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}"
log.info(f"Global CLAUDE.md block updated: {global_claude_md}")
return f"Global CLAUDE.md block updated: {global_claude_md}"
with open(GLOBAL_CLAUDE_MD, "a", encoding="utf-8") as f:
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}"
log.info(f"Global CLAUDE.md block added: {global_claude_md}")
return f"Global CLAUDE.md block added: {global_claude_md}"
def install_global_gemini_md() -> str:
"""Add/update server manager section in global ~/.gemini/GEMINI.md."""
global_gemini_md = _global_gemini_md()
os.makedirs(os.path.dirname(global_gemini_md), exist_ok=True)
existing = ""
if os.path.exists(global_gemini_md):
with open(global_gemini_md, encoding="utf-8") as f:
existing = f.read()
pattern = re.compile(
re.escape(_GEMINI_BLOCK_START) + r".*?" + re.escape(_GEMINI_BLOCK_END),
re.DOTALL
)
if pattern.search(existing):
updated = pattern.sub(GLOBAL_GEMINI_MD_BLOCK.strip(), existing)
with open(global_gemini_md, "w", encoding="utf-8") as f:
f.write(updated)
log.info(f"Global GEMINI.md block updated: {global_gemini_md}")
return f"Global GEMINI.md block updated: {global_gemini_md}"
with open(global_gemini_md, "a", encoding="utf-8") as f:
if existing and not existing.endswith("\n"):
f.write("\n")
f.write("\n" + GLOBAL_GEMINI_MD_BLOCK)
log.info(f"Global GEMINI.md block added: {global_gemini_md}")
return f"Global GEMINI.md block added: {global_gemini_md}"
def install_all() -> list[str]:
"""Full setup — install everything for Claude Code and Codex."""
results = []
steps = [
"""Full setup — install everything for Claude Code, Codex, and Gemini."""
all_users = os.environ.get("SERVER_MANAGER_INSTALL_ALL_USERS", "").strip() == "1"
base_steps = [
("ssh_script", install_ssh_script),
("claude_skill", install_claude_skill),
("codex_skill", install_codex_skill),
("ssh_key", generate_ssh_key),
("gemini_skill", install_gemini_skill),
("global_claude_md", install_global_claude_md),
("global_gemini_md", install_global_gemini_md),
]
if not all_users:
steps = base_steps[:3] + [("ssh_key", generate_ssh_key)] + base_steps[3:]
results = []
for name, func in steps:
try:
log.info(f"install_all: running {name}")
@@ -276,7 +524,29 @@ def install_all() -> list[str]:
msg = f"ERROR ({name}): {e}"
log.error(msg)
results.append(msg)
return results
results = []
original_target = os.environ.get("SERVER_MANAGER_TARGET_HOME")
for home in _iter_all_user_homes():
os.environ["SERVER_MANAGER_TARGET_HOME"] = home
results.append(f"[target_home] {home}")
for name, func in base_steps:
try:
log.info(f"install_all(all_users): running {name} for {home}")
result = func()
results.append(result)
except Exception as e:
msg = f"ERROR ({name}, {home}): {e}"
log.error(msg)
results.append(msg)
if original_target is None:
os.environ.pop("SERVER_MANAGER_TARGET_HOME", None)
else:
os.environ["SERVER_MANAGER_TARGET_HOME"] = original_target
results.append("INFO: SSH key generation skipped in SERVER_MANAGER_INSTALL_ALL_USERS=1 mode")
return results

View File

@@ -159,9 +159,9 @@ _EN = {
# Setup
"agent_integration": "AI Agent Integration",
"agent_desc": (
"Setup everything so Claude Code and Codex can manage your servers via shared local skills.\n"
"ServerManager, Claude Code, and Codex share the same servers.json — add a server here,\n"
"both agents see it immediately."
"Setup everything so Claude Code, Codex, and Gemini can manage your servers via shared local skills.\n"
"ServerManager, Claude Code, Codex, and Gemini share the same servers.json — add a server here,\n"
"all agents see it immediately."
),
"claude_integration": "Claude Code Integration",
"claude_desc": (
@@ -178,6 +178,8 @@ _EN = {
"status_claude_skill": "/ssh skill for Claude Code",
"status_codex_skill": "ServerManager skill for Codex",
"status_codex_wrapper": "Codex wrapper (codex-ssh)",
"status_gemini_skill": "ServerManager skill for Gemini",
"status_gemini_wrapper": "Gemini wrapper (gemini-ssh)",
"status_ssh_key": "SSH key (ed25519)",
"install_everything": "Install Everything",
"installing_all": "Installing...",
@@ -185,6 +187,7 @@ _EN = {
"install_skill": "/ssh skill",
"install_claude_skill": "Claude skill",
"install_codex_skill": "Codex skill",
"install_gemini_skill": "Gemini skill",
"install_ssh_key": "SSH key",
"refresh": "Refresh",
"configuration": "Configuration",
@@ -194,7 +197,7 @@ _EN = {
"select_backup": "Select backup...",
"no_backups": "No backups",
"restore": "Restore",
"install_done": "Done! Claude Code and Codex can now use ServerManager to manage your servers.",
"install_done": "Done! Claude Code, Codex, and Gemini can now use ServerManager to manage your servers.",
"config_changed": "Config path changed: {path}",
"backup_created": "Backup created: {name}",
"backup_failed": "Backup failed: {e}",
@@ -726,17 +729,17 @@ _RU = {
# Setup
"agent_integration": "Интеграция AI-агентов",
"agent_desc": (
"Настройте всё, чтобы Claude Code и Codex могли управлять серверами через локальные skills.\n"
"ServerManager, Claude Code и Codex используют один и тот же servers.json — добавьте сервер здесь,\n"
"и оба агента увидят его сразу."
),
"claude_integration": "Интеграция с Claude Code",
"claude_desc": (
"Настройте всё, чтобы Claude Code мог управлять серверами через скилл /ssh.\n"
"GUI и Claude Code используют один servers.json — добавьте сервер здесь,\n"
"Claude увидит его сразу."
),
"agent_desc": (
"Настройте всё, чтобы Claude Code, Codex и Gemini могли управлять серверами через локальные skills.\n"
"ServerManager, Claude Code, Codex и Gemini используют один и тот же servers.json — добавьте сервер здесь,\n"
"все агенты увидят его сразу."
),
"status": "Статус",
"status_shared_dir": "Общий каталог (~/.server-connections)",
"status_servers_json": "servers.json",
@@ -746,6 +749,8 @@ _RU = {
"status_claude_skill": "Скилл /ssh для Claude Code",
"status_codex_skill": "Скилл ServerManager для Codex",
"status_codex_wrapper": "Обёртка Codex (codex-ssh)",
"status_gemini_skill": "Скилл ServerManager для Gemini",
"status_gemini_wrapper": "Обёртка Gemini (gemini-ssh)",
"status_ssh_key": "SSH-ключ (ed25519)",
"install_everything": "Установить всё",
"installing_all": "Установка...",
@@ -753,6 +758,7 @@ _RU = {
"install_skill": "Скилл /ssh",
"install_claude_skill": "Скилл Claude",
"install_codex_skill": "Скилл Codex",
"install_gemini_skill": "Скилл Gemini",
"install_ssh_key": "SSH-ключ",
"refresh": "Обновить",
"configuration": "Конфигурация",
@@ -762,7 +768,7 @@ _RU = {
"select_backup": "Выберите бэкап...",
"no_backups": "Нет бэкапов",
"restore": "Восстановить",
"install_done": "Готово! Claude Code и Codex теперь могут использовать ServerManager для управления серверами.",
"install_done": "Готово! Claude Code, Codex и Gemini теперь могут использовать ServerManager для управления серверами.",
"config_changed": "Путь конфига изменён: {path}",
"backup_created": "Бэкап создан: {name}",
"backup_failed": "Ошибка бэкапа: {e}",
@@ -1294,11 +1300,6 @@ _ZH = {
# Setup
"agent_integration": "AI代理集成",
"agent_desc": (
"完成设置后Claude Code 和 Codex 都可以通过共享的本地技能来管理您的服务器。\n"
"ServerManager、Claude Code 和 Codex 共用同一个 servers.json — 在此添加服务器后,\n"
"两个代理都会立即看到。"
),
"claude_integration": "Claude Code集成",
"claude_desc": (
"设置一切以便Claude Code通过/ssh技能管理您的服务器。\n"
@@ -1310,10 +1311,17 @@ _ZH = {
"status_servers_json": "servers.json",
"status_ssh_script": "ssh.pyCLI工具",
"status_encryption": "加密模块",
"agent_desc": (
"完成设置后Claude Code、Codex 和 Gemini 都可以通过共享的本地 skills 管理您的服务器。\n"
"ServerManager、Claude Code、Codex 和 Gemini 共用同一个 servers.json — 在此添加服务器后,\n"
"所有代理都会立即看到。"
),
"status_skill": "Claude Code的/ssh技能",
"status_claude_skill": "Claude Code 的 /ssh 技能",
"status_codex_skill": "Codex 的 ServerManager 技能",
"status_codex_wrapper": "Codex 包装器codex-ssh",
"status_gemini_skill": "Gemini 的 ServerManager 技能",
"status_gemini_wrapper": "Gemini 包装器gemini-ssh",
"status_ssh_key": "SSH密钥ed25519",
"install_everything": "全部安装",
"installing_all": "安装中...",
@@ -1321,6 +1329,7 @@ _ZH = {
"install_skill": "/ssh技能",
"install_claude_skill": "Claude 技能",
"install_codex_skill": "Codex 技能",
"install_gemini_skill": "Gemini 技能",
"install_ssh_key": "SSH密钥",
"refresh": "刷新",
"configuration": "配置",
@@ -1330,7 +1339,7 @@ _ZH = {
"select_backup": "选择备份...",
"no_backups": "无备份",
"restore": "恢复",
"install_done": "完成Claude CodeCodex 现在都可以使用 ServerManager 来管理您的服务器。",
"install_done": "完成Claude CodeCodex 和 Gemini 现在都可以使用 ServerManager 来管理您的服务器。",
"config_changed": "配置路径已更改:{path}",
"backup_created": "备份已创建:{name}",
"backup_failed": "备份失败:{e}",

View File

@@ -14,6 +14,7 @@ from core.claude_setup import (
install_all,
install_claude_skill,
install_codex_skill,
install_gemini_skill,
install_ssh_script,
)
from core.i18n import t
@@ -63,6 +64,8 @@ class SetupTab(ctk.CTkFrame):
("claude_skill_installed", "status_claude_skill"),
("codex_skill_installed", "status_codex_skill"),
("codex_wrapper_installed", "status_codex_wrapper"),
("gemini_skill_installed", "status_gemini_skill"),
("gemini_wrapper_installed", "status_gemini_wrapper"),
("ssh_key_exists", "status_ssh_key"),
]
for key, i18n_key in status_items:
@@ -112,6 +115,12 @@ class SetupTab(ctk.CTkFrame):
)
self.codex_skill_btn.pack(side="left", padx=5)
self.gemini_skill_btn = make_icon_button(
top_btn_row, "confirm", t("install_gemini_skill"), width=130, fg_color="#6b7280",
command=self._install_gemini_skill
)
self.gemini_skill_btn.pack(side="left", padx=5)
bottom_btn_row = ctk.CTkFrame(ind_frame, fg_color="transparent")
bottom_btn_row.pack(fill="x")
@@ -370,6 +379,11 @@ class SetupTab(ctk.CTkFrame):
self._log(msg)
self._refresh_status()
def _install_gemini_skill(self):
msg = install_gemini_skill()
self._log(msg)
self._refresh_status()
def _install_skill(self):
msg = install_claude_skill()
self._log(msg)

93
test_ai_setup.py Normal file
View File

@@ -0,0 +1,93 @@
import os
import tempfile
import unittest
from pathlib import Path
from core import claude_setup as cs
class AISetupTests(unittest.TestCase):
def setUp(self):
self._old_target = os.environ.get("SERVER_MANAGER_TARGET_HOME")
self._old_all = os.environ.get("SERVER_MANAGER_INSTALL_ALL_USERS")
self._old_iter = cs._iter_all_user_homes
self._old_platform = cs.sys.platform
def tearDown(self):
if self._old_target is None:
os.environ.pop("SERVER_MANAGER_TARGET_HOME", None)
else:
os.environ["SERVER_MANAGER_TARGET_HOME"] = self._old_target
if self._old_all is None:
os.environ.pop("SERVER_MANAGER_INSTALL_ALL_USERS", None)
else:
os.environ["SERVER_MANAGER_INSTALL_ALL_USERS"] = self._old_all
cs._iter_all_user_homes = self._old_iter
cs.sys.platform = self._old_platform
def test_single_target_installers_create_expected_files(self):
with tempfile.TemporaryDirectory() as tmp:
os.environ["SERVER_MANAGER_TARGET_HOME"] = tmp
cs.install_ssh_script()
cs.install_claude_skill()
cs.install_codex_skill()
cs.install_gemini_skill()
cs.install_global_claude_md()
cs.install_global_gemini_md()
self.assertTrue(Path(tmp, ".server-connections", "ssh.py").exists())
self.assertTrue(Path(tmp, ".server-connections", "encryption.py").exists())
self.assertTrue(Path(tmp, ".claude", "commands", "ssh.md").exists())
self.assertTrue(Path(tmp, ".codex", "skills", "server-manager", "SKILL.md").exists())
self.assertTrue(Path(tmp, ".gemini", "skills", "server-manager", "SKILL.md").exists())
self.assertTrue(Path(tmp, ".server-connections", "codex-ssh").exists())
self.assertTrue(Path(tmp, ".server-connections", "gemini-ssh").exists())
self.assertTrue(Path(tmp, ".claude", "CLAUDE.md").exists())
self.assertTrue(Path(tmp, ".gemini", "GEMINI.md").exists())
self.assertFalse(Path(tmp, ".agents", "skills", "server-manager").exists())
status = cs.check_status()
self.assertTrue(status["claude_skill_installed"])
self.assertTrue(status["codex_skill_installed"])
self.assertTrue(status["gemini_skill_installed"])
self.assertTrue(status["codex_wrapper_installed"])
self.assertTrue(status["gemini_wrapper_installed"])
def test_install_all_users_mode_installs_into_each_home_and_skips_ssh_key(self):
with tempfile.TemporaryDirectory() as base:
home1 = Path(base, "user1")
home2 = Path(base, "user2")
home1.mkdir()
home2.mkdir()
os.environ["SERVER_MANAGER_INSTALL_ALL_USERS"] = "1"
cs._iter_all_user_homes = lambda: [str(home1), str(home2)]
results = cs.install_all()
self.assertIn("INFO: SSH key generation skipped", "\n".join(results))
self.assertTrue(Path(home1, ".codex", "skills", "server-manager", "SKILL.md").exists())
self.assertTrue(Path(home1, ".gemini", "skills", "server-manager", "SKILL.md").exists())
self.assertTrue(Path(home2, ".codex", "skills", "server-manager", "SKILL.md").exists())
self.assertTrue(Path(home2, ".gemini", "skills", "server-manager", "SKILL.md").exists())
self.assertFalse(Path(home1, ".ssh", "id_ed25519").exists())
self.assertFalse(Path(home2, ".ssh", "id_ed25519").exists())
def test_windows_wrapper_names_are_generated_with_cmd_suffix(self):
with tempfile.TemporaryDirectory() as tmp:
os.environ["SERVER_MANAGER_TARGET_HOME"] = tmp
cs.sys.platform = "win32"
cs.install_ssh_script()
cs.install_codex_skill()
cs.install_gemini_skill()
self.assertTrue(Path(tmp, ".server-connections", "codex-ssh.cmd").exists())
self.assertTrue(Path(tmp, ".server-connections", "gemini-ssh.cmd").exists())
if __name__ == "__main__":
unittest.main()

View File

@@ -1,30 +1,40 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────
# ServerManager CLI Installer for Linux (headless / no-GUI)
# ServerManager AI Integration Installer for Linux/macOS (headless / no-GUI)
#
# Устанавливает:
# - ssh.py + encryption.py ~/.server-connections/
# - servers.json + settings.json → ~/.server-connections/ (если есть)
# - CLAUDE.md → ~/.claude/
# - ssh.md (скилл) → ~/.claude/commands/
# - Python-зависимости для CLI (paramiko, cryptography, etc.)
# Installs for each target home:
# - ssh.py + encryption.py -> ~/.server-connections/
# - Claude /ssh skill -> ~/.claude/commands/
# - Codex server-manager skill -> ~/.codex/skills/server-manager/
# - Gemini server-manager skill -> ~/.gemini/skills/server-manager/
# - codex-ssh / gemini-ssh wrappers -> ~/.server-connections/
# - CLAUDE.md / GEMINI.md (if available) -> ~/.claude/ / ~/.gemini/
#
# Запуск:
# curl -sSL https://git.sensey24.ru/aibot777/server-manager/raw/branch/master/tools/install.sh | bash
# или:
# Optional per-target local config copy:
# - servers.json + settings.json -> ~/.server-connections/
#
# Notes:
# - servers.json is NEVER downloaded remotely.
# - --all-users installs code/skills/wrappers for discovered homes, but skips
# copying servers.json to avoid replicating credentials between users.
# - Gemini also supports ~/.agents/skills, but this installer avoids placing
# the same skill in both ~/.gemini/skills and ~/.agents/skills by default
# because Gemini reports that as a duplicate-skill conflict.
#
# Usage:
# bash install.sh
# или с указанием источника файлов:
# bash install.sh /path/to/server-manager/
# bash install.sh /path/to/server-manager
# bash install.sh --source-dir /path/to/server-manager --target-home /root
# bash install.sh --all-users --source-dir /path/to/server-manager
# ─────────────────────────────────────────────────────────────────────
set -euo pipefail
# ── Colors ──
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
NC='\033[0m' # No Color
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
@@ -32,33 +42,80 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; }
step() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; }
# ── Config ──
CONN_DIR="$HOME/.server-connections"
CLAUDE_DIR="$HOME/.claude"
COMMANDS_DIR="$CLAUDE_DIR/commands"
usage() {
cat <<USAGE
ServerManager AI integration installer
Options:
--source-dir PATH Use local repo as source of files
--target-home PATH Install into a specific user's home
--all-users Install into all discovered user homes on this machine
--install-agents-mirror Also mirror Gemini skill into ~/.agents/skills
-h, --help Show this help
Positional compatibility:
install.sh /path/to/server-manager # same as --source-dir
USAGE
}
GITEA_RAW="https://git.sensey24.ru/aibot777/server-manager/raw/branch/master"
SRC_DIR=""
TARGET_HOME="${SERVER_MANAGER_TARGET_HOME:-${TARGET_HOME:-$HOME}}"
INSTALL_ALL_USERS=0
INSTALL_AGENTS_MIRROR=0
# Source directory (optional argument)
SRC_DIR="${1:-}"
while [[ $# -gt 0 ]]; do
case "$1" in
--source-dir)
SRC_DIR="$2"
shift 2
;;
--target-home)
TARGET_HOME="$2"
shift 2
;;
--all-users)
INSTALL_ALL_USERS=1
shift
;;
--install-agents-mirror)
INSTALL_AGENTS_MIRROR=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
if [[ -z "$SRC_DIR" ]]; then
SRC_DIR="$1"
shift
else
error "Неизвестный аргумент: $1"
usage
exit 2
fi
;;
esac
done
# ── Banner ──
echo -e "${CYAN}"
echo "╔══════════════════════════════════════════════╗"
echo "║ ServerManager CLI Installer for Linux ║"
echo "║ github: git.sensey24.ru/aibot777 ║"
echo "╚══════════════════════════════════════════════╝"
echo "╔══════════════════════════════════════════════════════╗"
echo "║ ServerManager AI Integration Installer (headless) ║"
echo "║ Claude + Codex + Gemini ║"
echo "╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"
# ── Step 1: Check Python ──
step "1/5 Проверка Python"
PYTHON=""
for cmd in python3 python; do
if command -v "$cmd" &>/dev/null; then
ver=$("$cmd" --version 2>&1 | grep -oP '\d+\.\d+')
major=$(echo "$ver" | cut -d. -f1)
minor=$(echo "$ver" | cut -d. -f2)
if [ "$major" -ge 3 ] && [ "$minor" -ge 8 ]; then
if "$cmd" - <<'PY' &>/dev/null
import sys
raise SystemExit(0 if sys.version_info >= (3, 8) else 1)
PY
then
PYTHON="$cmd"
ok "Python найден: $($cmd --version)"
break
@@ -66,14 +123,11 @@ for cmd in python3 python; do
fi
done
if [ -z "$PYTHON" ]; then
error "Python 3.8+ не найден!"
echo " Установите: sudo apt install python3 python3-pip"
echo " или: sudo yum install python3 python3-pip"
if [[ -z "$PYTHON" ]]; then
error "Python 3.8+ не найден"
exit 1
fi
# Check pip
PIP=""
for cmd in pip3 pip; do
if command -v "$cmd" &>/dev/null; then
@@ -81,22 +135,26 @@ for cmd in pip3 pip; do
break
fi
done
if [ -z "$PIP" ]; then
# Try python -m pip
if [[ -z "$PIP" ]]; then
if $PYTHON -m pip --version &>/dev/null; then
PIP="$PYTHON -m pip"
else
error "pip не найден!"
echo " Установите: sudo apt install python3-pip"
error "pip не найден"
exit 1
fi
fi
ok "pip найден: $($PIP --version 2>&1 | head -1)"
# ── Step 2: Install Python dependencies ──
step "2/5 Установка Python-зависимостей"
resolve_home() {
"$PYTHON" - "$1" <<'PY'
import os, sys
print(os.path.abspath(os.path.expanduser(sys.argv[1])))
PY
}
TARGET_HOME="$(resolve_home "$TARGET_HOME")"
step "2/5 Установка Python-зависимостей"
CLI_DEPS=(
"paramiko>=3.4.0"
"cryptography>=41.0.0"
@@ -105,7 +163,6 @@ CLI_DEPS=(
"redis>=5.0.0"
"requests>=2.31.0"
)
for dep in "${CLI_DEPS[@]}"; do
pkg=$(echo "$dep" | sed 's/[>=<].*//')
if $PYTHON -c "import $pkg" 2>/dev/null; then
@@ -120,38 +177,26 @@ for dep in "${CLI_DEPS[@]}"; do
fi
done
# ── Step 3: Create directories ──
step "3/5 Создание директорий"
mkdir -p "$CONN_DIR" "$COMMANDS_DIR"
chmod 700 "$CONN_DIR" 2>/dev/null || true
ok "$CONN_DIR"
ok "$COMMANDS_DIR"
# ── Step 4: Copy/Download files ──
step "4/5 Установка файлов"
copy_or_download() {
local src_relative="$1"
local dst="$2"
local perms="$3"
local desc="$4"
# Try local source first
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/$src_relative" ]; then
mkdir -p "$(dirname "$dst")"
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/$src_relative" ]]; then
cp "$SRC_DIR/$src_relative" "$dst"
chmod "$perms" "$dst"
chmod "$perms" "$dst" 2>/dev/null || true
ok "$desc (из $SRC_DIR)"
return 0
fi
# Try download from Gitea
local url="$GITEA_RAW/$src_relative"
if command -v curl &>/dev/null; then
if curl -sSL -o "$dst" "$url" 2>/dev/null; then
# Verify not empty and not HTML error page
if [ -s "$dst" ] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
chmod "$perms" "$dst"
if curl -fsSL -o "$dst" "$url" 2>/dev/null; then
if [[ -s "$dst" ]] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
chmod "$perms" "$dst" 2>/dev/null || true
ok "$desc (скачан с Gitea)"
return 0
fi
@@ -159,8 +204,8 @@ copy_or_download() {
fi
elif command -v wget &>/dev/null; then
if wget -q -O "$dst" "$url" 2>/dev/null; then
if [ -s "$dst" ] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
chmod "$perms" "$dst"
if [[ -s "$dst" ]] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
chmod "$perms" "$dst" 2>/dev/null || true
ok "$desc (скачан с Gitea)"
return 0
fi
@@ -172,86 +217,180 @@ copy_or_download() {
return 1
}
# Core files (always install)
copy_or_download "tools/ssh.py" "$CONN_DIR/ssh.py" "755" "ssh.py"
copy_or_download "core/encryption.py" "$CONN_DIR/encryption.py" "644" "encryption.py"
install_skill_tree() {
local prefix="$1"
local dst_root="$2"
shift 2
mkdir -p "$dst_root"
local rel
for rel in "$@"; do
copy_or_download "$prefix/$rel" "$dst_root/$rel" 644 "$prefix/$rel" || true
done
find "$dst_root/scripts" -type f -name '*.sh' -exec chmod 755 {} + 2>/dev/null || true
find "$dst_root/scripts" -type f -name '*.cmd' -exec chmod 644 {} + 2>/dev/null || true
}
# Claude Code skill
copy_or_download "tools/skill-ssh.md" "$COMMANDS_DIR/ssh.md" "644" "ssh.md (скилл /ssh)"
discover_homes() {
local homes=()
local uname_s
uname_s="$(uname -s 2>/dev/null || echo Linux)"
# CLAUDE.md
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/CLAUDE.md" ]; then
if [[ "$INSTALL_ALL_USERS" -eq 1 ]]; then
if [[ "$uname_s" == "Darwin" ]]; then
[[ -d /var/root ]] && homes+=("/var/root")
if [[ -d /Users ]]; then
while IFS= read -r -d '' d; do homes+=("$d"); done < <(find /Users -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
fi
else
[[ -d /root ]] && homes+=("/root")
if [[ -d /home ]]; then
while IFS= read -r -d '' d; do homes+=("$d"); done < <(find /home -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
fi
fi
else
homes+=("$TARGET_HOME")
fi
printf '%s\n' "${homes[@]}" | awk 'NF && !seen[$0]++'
}
step "3/5 Подготовка директорий"
TARGET_HOMES=()
while IFS= read -r home; do
[[ -n "$home" ]] || continue
TARGET_HOMES+=("$home")
ok "target home: $home"
done < <(discover_homes)
if [[ "${#TARGET_HOMES[@]}" -eq 0 ]]; then
error "Не удалось определить target home"
exit 1
fi
step "4/5 Установка файлов"
CODEX_SKILL_FILES=(
"SKILL.md"
"references/command-matrix.md"
"references/project.md"
"scripts/codex-ssh-wrapper.sh"
"scripts/codex-ssh-wrapper.cmd"
"scripts/server-manager-doctor.sh"
"scripts/server-manager-doctor.cmd"
)
GEMINI_SKILL_FILES=(
"SKILL.md"
"references/command-matrix.md"
"references/project.md"
"scripts/gemini-ssh-wrapper.sh"
"scripts/gemini-ssh-wrapper.cmd"
"scripts/server-manager-gemini-doctor.sh"
"scripts/server-manager-gemini-doctor.cmd"
)
for HOME_DIR in "${TARGET_HOMES[@]}"; do
CONN_DIR="$HOME_DIR/.server-connections"
CLAUDE_DIR="$HOME_DIR/.claude"
COMMANDS_DIR="$CLAUDE_DIR/commands"
CODEX_DIR="$HOME_DIR/.codex/skills/server-manager"
GEMINI_DIR="$HOME_DIR/.gemini"
GEMINI_SKILL_DIR="$GEMINI_DIR/skills/server-manager"
AGENTS_DIR="$HOME_DIR/.agents/skills/server-manager"
mkdir -p "$CONN_DIR" "$COMMANDS_DIR" "$CODEX_DIR" "$GEMINI_SKILL_DIR"
chmod 700 "$CONN_DIR" 2>/dev/null || true
info "Устанавливаю в $HOME_DIR"
copy_or_download "tools/ssh.py" "$CONN_DIR/ssh.py" 755 "ssh.py"
copy_or_download "core/encryption.py" "$CONN_DIR/encryption.py" 644 "encryption.py"
copy_or_download "tools/skill-ssh.md" "$COMMANDS_DIR/ssh.md" 644 "ssh.md (скилл /ssh)"
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/CLAUDE.md" ]]; then
cp "$SRC_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
chmod 644 "$CLAUDE_DIR/CLAUDE.md"
ok "CLAUDE.md"
elif [[ ! -f "$CLAUDE_DIR/CLAUDE.md" ]]; then
copy_or_download "CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" 644 "CLAUDE.md" || true
fi
# servers.json — only copy if exists locally, never download (contains encrypted creds)
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/servers.json" ]; then
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/GEMINI.md" ]]; then
cp "$SRC_DIR/GEMINI.md" "$GEMINI_DIR/GEMINI.md"
chmod 644 "$GEMINI_DIR/GEMINI.md"
ok "GEMINI.md"
elif [[ ! -f "$GEMINI_DIR/GEMINI.md" ]]; then
copy_or_download "GEMINI.md" "$GEMINI_DIR/GEMINI.md" 644 "GEMINI.md" || true
fi
install_skill_tree ".codex/skills/server-manager" "$CODEX_DIR" "${CODEX_SKILL_FILES[@]}"
install_skill_tree ".gemini/skills/server-manager" "$GEMINI_SKILL_DIR" "${GEMINI_SKILL_FILES[@]}"
if [[ "$INSTALL_AGENTS_MIRROR" -eq 1 ]]; then
mkdir -p "$AGENTS_DIR"
install_skill_tree ".gemini/skills/server-manager" "$AGENTS_DIR" "${GEMINI_SKILL_FILES[@]}"
ok "agents skill mirror"
elif [[ -d "$AGENTS_DIR" ]]; then
rm -rf "$AGENTS_DIR"
ok "removed stale agents skill mirror to avoid Gemini conflict"
fi
if [[ -f "$CODEX_DIR/scripts/codex-ssh-wrapper.sh" ]]; then
cp "$CODEX_DIR/scripts/codex-ssh-wrapper.sh" "$CONN_DIR/codex-ssh"
chmod 755 "$CONN_DIR/codex-ssh"
ok "codex-ssh wrapper"
else
copy_or_download ".codex/skills/server-manager/scripts/codex-ssh-wrapper.sh" "$CONN_DIR/codex-ssh" 755 "codex-ssh wrapper" || true
fi
if [[ -f "$GEMINI_SKILL_DIR/scripts/gemini-ssh-wrapper.sh" ]]; then
cp "$GEMINI_SKILL_DIR/scripts/gemini-ssh-wrapper.sh" "$CONN_DIR/gemini-ssh"
chmod 755 "$CONN_DIR/gemini-ssh"
ok "gemini-ssh wrapper"
else
copy_or_download ".gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh" "$CONN_DIR/gemini-ssh" 755 "gemini-ssh wrapper" || true
fi
if [[ "$INSTALL_ALL_USERS" -eq 0 ]]; then
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/servers.json" ]]; then
cp "$SRC_DIR/servers.json" "$CONN_DIR/servers.json"
chmod 600 "$CONN_DIR/servers.json"
ok "servers.json (зашифрованный)"
elif [ ! -f "$CONN_DIR/servers.json" ]; then
warn "servers.json не найден — скопируйте с основной машины:"
echo " scp user@main:~/.server-connections/servers.json $CONN_DIR/"
elif [[ ! -f "$CONN_DIR/servers.json" ]]; then
warn "servers.json не найден для $HOME_DIR — скопируйте вручную"
fi
# settings.json
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/settings.json" ]; then
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/settings.json" ]]; then
cp "$SRC_DIR/settings.json" "$CONN_DIR/settings.json"
chmod 600 "$CONN_DIR/settings.json"
ok "settings.json"
elif [ ! -f "$CONN_DIR/settings.json" ]; then
# Create minimal settings
elif [[ ! -f "$CONN_DIR/settings.json" ]]; then
echo '{"language":"en","check_interval":60}' > "$CONN_DIR/settings.json"
chmod 600 "$CONN_DIR/settings.json"
ok "settings.json (создан по умолчанию)"
fi
else
warn "all-users mode: servers.json/settings.json не копируются автоматически для $HOME_DIR"
fi
done
# ── Step 5: Verify ──
step "5/5 Проверка установки"
ALL_OK=true
for HOME_DIR in "${TARGET_HOMES[@]}"; do
CONN_DIR="$HOME_DIR/.server-connections"
COMMANDS_DIR="$HOME_DIR/.claude/commands"
CODEX_DIR="$HOME_DIR/.codex/skills/server-manager"
GEMINI_SKILL_DIR="$HOME_DIR/.gemini/skills/server-manager"
if [ -f "$CONN_DIR/ssh.py" ] && [ -x "$CONN_DIR/ssh.py" ]; then
ok "ssh.py — исполняемый"
else
error "ssh.py — не найден или не исполняемый"
ALL_OK=false
fi
info "Проверка $HOME_DIR"
if [ -f "$CONN_DIR/encryption.py" ]; then
ok "encryption.py"
else
error "encryption.py — не найден"
ALL_OK=false
fi
[[ -x "$CONN_DIR/ssh.py" ]] && ok "ssh.py — исполняемый" || { error "ssh.py — не найден или не исполняемый"; ALL_OK=false; }
[[ -f "$CONN_DIR/encryption.py" ]] && ok "encryption.py" || { error "encryption.py — не найден"; ALL_OK=false; }
[[ -f "$COMMANDS_DIR/ssh.md" ]] && ok "Claude /ssh skill" || warn "Claude /ssh skill — не найден"
[[ -f "$CODEX_DIR/SKILL.md" ]] && ok "Codex skill" || { warn "Codex skill — не найден"; ALL_OK=false; }
[[ -x "$CONN_DIR/codex-ssh" ]] && ok "codex-ssh wrapper" || { warn "codex-ssh wrapper — не найден"; ALL_OK=false; }
[[ -f "$GEMINI_SKILL_DIR/SKILL.md" ]] && ok "Gemini skill" || { warn "Gemini skill — не найден"; ALL_OK=false; }
[[ -x "$CONN_DIR/gemini-ssh" ]] && ok "gemini-ssh wrapper" || { warn "gemini-ssh wrapper — не найден"; ALL_OK=false; }
if [ -f "$COMMANDS_DIR/ssh.md" ]; then
ok "ssh.md скилл"
else
warn "ssh.md скилл — не найден"
fi
done
if [ -f "$CONN_DIR/servers.json" ]; then
ok "servers.json"
else
warn "servers.json — отсутствует (нужно скопировать вручную)"
fi
# Test ssh.py
info "Тест ssh.py..."
if $PYTHON "$CONN_DIR/ssh.py" --list &>/dev/null; then
ok "ssh.py --list работает"
else
if [ ! -f "$CONN_DIR/servers.json" ]; then
warn "ssh.py не может запуститься (нет servers.json)"
else
warn "ssh.py вернул ошибку — проверьте зависимости"
fi
fi
# ── Summary ──
echo ""
echo -e "${CYAN}━━━ Готово ━━━${NC}"
echo ""
@@ -260,17 +399,19 @@ if $ALL_OK; then
else
echo -e "${YELLOW}Установка завершена с предупреждениями.${NC}"
fi
echo ""
echo "Файлы:"
echo " $CONN_DIR/ssh.py — CLI-утилита"
echo " $CONN_DIR/encryption.py — модуль шифрования"
echo " $CONN_DIR/servers.json — серверы (зашифрованные)"
echo " $COMMANDS_DIR/ssh.md — скилл /ssh для Claude Code"
echo "Установлено для home:"
printf ' - %s\n' "${TARGET_HOMES[@]}"
echo ""
echo "Использование:"
echo " python3 ~/.server-connections/ssh.py --list"
echo " python3 ~/.server-connections/ssh.py --info ALIAS"
echo " python3 ~/.server-connections/ssh.py ALIAS \"command\""
echo ""
echo "Claude Code скилл: /ssh"
echo " ~/.server-connections/codex-ssh --list"
echo " ~/.server-connections/gemini-ssh --list"
echo ""
echo "Claude skill: ~/.claude/commands/ssh.md"
echo "Codex skill: ~/.codex/skills/server-manager/"
echo "Gemini skill: ~/.gemini/skills/server-manager/"
if [[ "$INSTALL_AGENTS_MIRROR" -eq 1 ]]; then
echo "Mirror skill: ~/.agents/skills/server-manager/"
fi

View File

@@ -0,0 +1,48 @@
#!/usr/bin/env python3
"""Cross-platform installer for ServerManager AI integrations.
Supports Claude (/ssh), Codex (server-manager), and Gemini (server-manager)
for the current user, a target home, or all discovered user homes.
"""
from __future__ import annotations
import argparse
import os
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from core.claude_setup import install_all # noqa: E402
def parse_args() -> argparse.Namespace:
p = argparse.ArgumentParser(description="Install ServerManager AI integrations")
p.add_argument("--target-home", help="Install into this home directory instead of the current user")
p.add_argument("--all-users", action="store_true", help="Install to all discovered user homes on this system")
return p.parse_args()
def main() -> int:
args = parse_args()
if args.target_home and args.all_users:
print("error: --target-home and --all-users are mutually exclusive", file=sys.stderr)
return 2
if args.target_home:
os.environ["SERVER_MANAGER_TARGET_HOME"] = os.path.abspath(os.path.expanduser(args.target_home))
if args.all_users:
os.environ["SERVER_MANAGER_INSTALL_ALL_USERS"] = "1"
for line in install_all():
print(line)
return 0
if __name__ == "__main__":
raise SystemExit(main())