Compare commits

...

3 Commits

Author SHA1 Message Date
Codex
136d1605c0 docs: refresh Claude Codex and Gemini integration guides 2026-03-11 19:32:25 +00:00
Codex
9da3125c34 feat: add Gemini skill integration and multi-user AI setup 2026-03-11 19:32:25 +00:00
chrome-storm-c442
daa11ca440 v1.9.44: sync with Codex integration from Linux
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 02:35:10 -05:00
23 changed files with 1364 additions and 284 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"

View File

@@ -24,13 +24,13 @@
В репозитории исходники skill лежат здесь:
- [SKILL.md](/home/code/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
- [command-matrix.md](/home/code/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
- [project.md](/home/code/CODING/server-manager/.codex/skills/server-manager/references/project.md)
- [server-manager-doctor.sh](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.sh)
- [server-manager-doctor.cmd](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.cmd)
- [codex-ssh-wrapper.sh](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.sh)
- [codex-ssh-wrapper.cmd](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.cmd)
- [SKILL.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
- [command-matrix.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
- [project.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/references/project.md)
- [server-manager-doctor.sh](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.sh)
- [server-manager-doctor.cmd](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.cmd)
- [codex-ssh-wrapper.sh](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.sh)
- [codex-ssh-wrapper.cmd](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.cmd)
## Как это работает
@@ -200,12 +200,12 @@ $HOME/.server-connections/codex-ssh --status
Если меняется поведение интеграции, проверять нужно в таком порядке:
1. [tools/ssh.py](/home/code/CODING/server-manager/tools/ssh.py)
2. [tools/skill-ssh.md](/home/code/CODING/server-manager/tools/skill-ssh.md)
3. [core/claude_setup.py](/home/code/CODING/server-manager/core/claude_setup.py)
4. [build.py](/home/code/CODING/server-manager/build.py)
5. [SKILL.md](/home/code/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
6. [command-matrix.md](/home/code/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
1. [tools/ssh.py](/home/code/Desktop/CODING/server-manager/tools/ssh.py)
2. [tools/skill-ssh.md](/home/code/Desktop/CODING/server-manager/tools/skill-ssh.md)
3. [core/claude_setup.py](/home/code/Desktop/CODING/server-manager/core/claude_setup.py)
4. [build.py](/home/code/Desktop/CODING/server-manager/build.py)
5. [SKILL.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
6. [command-matrix.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
Если меняется семантика `ssh.py`, нужно обновлять и Claude skill, и Codex skill.

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
```

86
GEMINI_SKILL_SETUP.md Normal file
View File

@@ -0,0 +1,86 @@
# Развёртывание Gemini Skill Для ServerManager
Этот документ описывает, как ServerManager интегрируется с Gemini CLI.
## Что устанавливается
Для каждого target home устанавливаются:
1. Общий backend:
- `~/.server-connections/ssh.py`
- `~/.server-connections/encryption.py`
2. Gemini skill package:
- `~/.gemini/skills/server-manager/`
3. Безопасный runtime wrapper:
- `~/.server-connections/gemini-ssh`
4. Глобальный Gemini context:
- `~/.gemini/GEMINI.md`
## Skill workflow
Gemini должен начинать discovery так:
```bash
$HOME/.server-connections/gemini-ssh --list
```
Далее:
- определить `Type`
- выбрать команду строго по типу
- выполнить ровно одно действие
- не раскрывать IP, логины, пароли, порты
## Рекомендуемая установка
### Через GUI
Вкладка `Setup` теперь умеет ставить:
- Claude skill
- Codex skill
- Gemini skill
- shared backend и wrappers
### Через Python installer
```bash
python3 tools/install_ai_integrations.py
python3 tools/install_ai_integrations.py --target-home /root
python3 tools/install_ai_integrations.py --all-users
```
### Через shell installer (Linux/macOS)
```bash
bash tools/install.sh --source-dir /path/to/server-manager
bash tools/install.sh --source-dir /path/to/server-manager --target-home /root
bash tools/install.sh --source-dir /path/to/server-manager --all-users
```
## Проверка
### 1. Проверить skill discovery
```bash
gemini skills list
```
### 2. Проверить wrapper
```bash
$HOME/.server-connections/gemini-ssh --list
```
### 3. Проверить doctor script
```bash
$HOME/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.sh
```
## Важные замечания
- `servers.json` не размножается автоматически в `--all-users` режиме — это сделано намеренно, чтобы не копировать credentials между пользователями.
- Для root / service accounts используйте отдельную установку в нужный `target home`.
- Gemini skill source в репозитории лежит в `.gemini/skills/server-manager/`.
- При необходимости можно дополнительно ставить mirror в `~/.agents/skills/server-manager/`, но по умолчанию это отключено, чтобы Gemini не ругался на duplicate skill conflict.

View File

@@ -2,7 +2,7 @@
<p align="center">
<strong>Desktop GUI for managing remote servers</strong><br>
CustomTkinter + Paramiko | Dark Theme | Claude Code + Codex Integration
CustomTkinter + Paramiko | Dark Theme | Claude Code + Codex + Gemini Integration
</p>
<p align="center">
@@ -22,7 +22,7 @@
- **SFTP Transfer** — upload/download files with progress bar
- **SSH Keys** — generate ed25519, install on server, copy to clipboard
- **Status Monitor** — background check every 60 sec (online/offline badges)
- **Claude Code + Codex Integration** — one-click setup, shared config with `/ssh` skill and Codex skill
- **Claude Code + Codex + Gemini Integration** — one-click setup, shared config with `/ssh`, Codex skill, and Gemini skill
- **TOTP / 2FA** — Google Authenticator compatible codes with live countdown, one-click copy
- **Encryption** — servers.json encrypted with Fernet (passwords never stored in plaintext)
- **Backups** — manual and automatic backups with one-click restore
@@ -62,10 +62,10 @@ Output goes to `releases/ServerManager-vX.Y.Z-{platform}.exe`
3. **Terminal** — select server → Terminal tab → type command → Run
4. **Files** — select server → Files tab → set paths → Upload/Download
5. **Keys** — Keys tab → Generate Key → Install on Server
6. **Setup** — Setup tab → "Install Everything" → Claude Code and Codex ready
6. **Setup** — Setup tab → "Install Everything" → Claude Code, Codex, and Gemini ready
7. Status badges update automatically (green = online, red = offline)
### Claude Code + Codex Integration
### Claude Code + Codex + Gemini Integration
ServerManager, Claude Code, and Codex share the same config file: `~/.server-connections/servers.json`
@@ -75,11 +75,11 @@ For Codex deployment and operational edge cases, see [`CODEX_SKILL_SETUP.md`](CO
```
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py backend
↕ ↕
Add/edit/delete Claude /ssh + Codex skill
Add/edit/delete Claude /ssh + Codex + Gemini skill
servers in GUI execute commands
```
- Add a server in GUI → Claude Code and Codex see it immediately
- Add a server in GUI → Claude Code, Codex, and Gemini see it immediately
- Both agents use the same `ssh.py` + `servers.json`
- Passwords **never** pass through the AI API
@@ -95,14 +95,17 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py ba
**Setup on a new machine:**
1. Install ServerManager (clone repo or download binary)
2. Open Setup tab → click "Install Everything"
3. Done. Claude Code now has `/ssh`, and Codex now has the `server-manager` skill with access to your servers
3. Done. Claude Code now has `/ssh`, Codex now has the `server-manager` skill, and Gemini now has the `server-manager` skill with access to your servers
The Setup tab installs:
- `ssh.py``~/.server-connections/` (SSH utility)
- `encryption.py``~/.server-connections/` (encryption module for CLI)
- `/ssh` skill → `~/.claude/commands/ssh.md` (Claude Code skill)
- `server-manager` skill → `~/.codex/skills/server-manager/` (Codex skill package)
- `server-manager` skill → `~/.gemini/skills/server-manager/` (Gemini skill package)
- optional mirror → `~/.agents/skills/server-manager/` (off by default to avoid Gemini duplicate-skill warnings)
- `codex-ssh` wrapper → `~/.server-connections/` (safe Codex entry point)
- `gemini-ssh` wrapper → `~/.server-connections/` (safe Gemini entry point)
- SSH key (ed25519) — if not exists
- Checks for duplicates — safe to run multiple times
@@ -158,7 +161,7 @@ ServerManager/
│ ├── server_store.py # CRUD + encrypted JSON + observer + backups
│ ├── encryption.py # Fernet encryption module
│ ├── ssh_client.py # Paramiko SSH/SFTP wrapper
│ ├── claude_setup.py # Claude Code + Codex integration installer
│ ├── claude_setup.py # Claude Code + Codex + Gemini integration installer
│ ├── status_checker.py # Background monitoring
│ ├── totp.py # TOTP/2FA module (pyotp)
│ ├── logger.py # Rotating file logger
@@ -205,7 +208,7 @@ python main.py
- **SFTP** — загрузка и скачивание файлов с прогресс-баром
- **SSH-ключи** — генерация ed25519, установка на сервер, копирование
- **Мониторинг** — фоновая проверка каждые 60 сек (бейджи online/offline)
- **Интеграция с Claude Code + Codex** — установка в один клик, общий конфиг со скиллом `/ssh` и Codex skill
- **Интеграция с Claude Code + Codex + Gemini** — установка в один клик, общий конфиг со скиллом `/ssh`, Codex skill и Gemini skill
- **TOTP / 2FA** — коды Google Authenticator с обратным отсчётом, копирование в один клик
- **Шифрование** — servers.json зашифрован Fernet (пароли не хранятся в открытом виде)
- **Бэкапы** — ручные и автоматические с восстановлением в один клик
@@ -245,22 +248,22 @@ python build.py
3. **Терминал** — выберите сервер → вкладка Terminal → введите команду → Run
4. **Файлы** — выберите сервер → вкладка Files → укажите пути → Upload/Download
5. **Ключи** — вкладка Keys → Generate Key → Install on Server
6. **Настройка** — вкладка Setup → "Install Everything" → Claude Code и Codex готовы
6. **Настройка** — вкладка Setup → "Install Everything" → Claude Code, Codex и Gemini готовы
7. Бейджи статуса обновляются автоматически (зелёный = online, красный = offline)
### Интеграция с Claude Code + Codex
### Интеграция с Claude Code + Codex + Gemini
ServerManager, Claude Code и Codex используют **один и тот же файл конфигурации**: `~/.server-connections/servers.json`
ServerManager, Claude Code, Codex и Gemini используют **один и тот же файл конфигурации**: `~/.server-connections/servers.json`
**Как это работает:**
```
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ backend ssh.py
↕ ↕
Добавил/изменил Claude /ssh + Codex skill
Добавил/изменил Claude /ssh + Codex + Gemini skill
серверы в GUI выполняют команды
```
- Добавил сервер в GUI → Claude Code и Codex сразу видят его
- Добавил сервер в GUI → Claude Code, Codex и Gemini сразу видят его
- Оба агента используют один `ssh.py` + `servers.json`
- Пароли **никогда** не проходят через API нейронки
@@ -276,14 +279,16 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ backend s
**Настройка на новой машине:**
1. Установить ServerManager (клонировать репо или скачать бинарник)
2. Открыть вкладку Setup → нажать "Install Everything"
3. Готово. Claude Code получает скилл `/ssh`, а Codex получает skill `server-manager` и доступ к серверам
3. Готово. Claude Code получает скилл `/ssh`, а Codex и Gemini получают skill `server-manager` и доступ к серверам
Вкладка Setup устанавливает:
- `ssh.py``~/.server-connections/` (SSH-утилита)
- `encryption.py``~/.server-connections/` (модуль шифрования для CLI)
- скилл `/ssh``~/.claude/commands/ssh.md` (скилл Claude Code)
- skill `server-manager``~/.codex/skills/server-manager/` (скилл Codex)
- skill `server-manager``~/.gemini/skills/server-manager/` (скилл Gemini)
- wrapper `codex-ssh``~/.server-connections/` (безопасная точка входа для Codex)
- wrapper `gemini-ssh``~/.server-connections/` (безопасная точка входа для Gemini)
- SSH-ключ (ed25519) — если ещё не создан
- Проверяет дубли — безопасно запускать повторно
@@ -342,7 +347,7 @@ pip install -r requirements.txt
python main.py
# → Вкладка Setup → Install Everything
# → Добавить серверы через + Add
# → Готово! GUI, Claude Code и Codex работают с одним конфигом
# → Готово! GUI, Claude Code, Codex и Gemini работают с одним конфигом
```
---
@@ -356,7 +361,7 @@ python main.py
- **SFTP传输** — 带进度条的文件上传/下载
- **SSH密钥** — 生成ed25519、安装到服务器、复制到剪贴板
- **状态监控** — 每60秒后台检查在线/离线徽标)
- **Claude Code + Codex 集成** — 一键设置,与 `/ssh` 技能Codex skill 共享配置
- **Claude Code + Codex + Gemini 集成** — 一键设置,与 `/ssh` 技能Codex skill 和 Gemini skill 共享配置
- **TOTP / 2FA** — 兼容Google Authenticator的验证码实时倒计时一键复制
- **加密** — servers.json使用Fernet加密密码不再以明文存储
- **备份** — 手动和自动备份,一键恢复
@@ -399,7 +404,7 @@ python build.py
6. **设置** — Setup标签 → "Install Everything" → Claude Code 和 Codex 就绪
7. 状态徽标自动更新(绿色 = 在线,红色 = 离线)
### Claude Code + Codex 集成
### Claude Code + Codex + Gemini 集成
ServerManager、Claude Code 和 Codex 共享**同一个配置文件**`~/.server-connections/servers.json`
@@ -407,12 +412,12 @@ ServerManager、Claude Code 和 Codex 共享**同一个配置文件**`~/.serv
```
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py 后端
↕ ↕
在GUI中添加/编辑 Claude /ssh + Codex skill
在GUI中添加/编辑 Claude /ssh + Codex + Gemini skill
服务器 执行命令
```
- 在GUI中添加服务器 → Claude Code 和 Codex 都会立即看到
- 两个代理都使用同一个 `ssh.py` + `servers.json`
- Claude Code、Codex 和 Gemini 都使用同一个 `ssh.py` + `servers.json`
- 密码**绝不**通过AI API传递
**在新机器上设置:**

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,41 +457,96 @@ 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),
]
for name, func in steps:
try:
log.info(f"install_all: running {name}")
result = func()
results.append(result)
except Exception as e:
msg = f"ERROR ({name}): {e}"
log.error(msg)
results.append(msg)
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}")
result = func()
results.append(result)
except Exception as e:
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
cp "$SRC_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
chmod 644 "$CLAUDE_DIR/CLAUDE.md"
ok "CLAUDE.md"
fi
# servers.json — only copy if exists locally, never download (contains encrypted creds)
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/"
fi
# settings.json
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
echo '{"language":"en","check_interval":60}' > "$CONN_DIR/settings.json"
chmod 600 "$CONN_DIR/settings.json"
ok "settings.json (создан по умолчанию)"
fi
# ── Step 5: Verify ──
step "5/5 Проверка установки"
ALL_OK=true
if [ -f "$CONN_DIR/ssh.py" ] && [ -x "$CONN_DIR/ssh.py" ]; then
ok "ssh.py — исполняемый"
else
error "ssh.py — не найден или не исполняемый"
ALL_OK=false
fi
if [ -f "$CONN_DIR/encryption.py" ]; then
ok "encryption.py"
else
error "encryption.py — не найден"
ALL_OK=false
fi
if [ -f "$COMMANDS_DIR/ssh.md" ]; then
ok "ssh.md скилл"
else
warn "ssh.md скилл — не найден"
fi
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)"
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
warn "ssh.py вернул ошибку — проверьте зависимости"
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
# ── Summary ──
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
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 не найден для $HOME_DIR — скопируйте вручную"
fi
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
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/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"
info "Проверка $HOME_DIR"
[[ -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; }
done
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())

View File

@@ -1,6 +1,6 @@
"""Version info for ServerManager."""
__version__ = "1.9.43"
__version__ = "1.9.44"
__app_name__ = "ServerManager"
__author__ = "aibot777"
__description__ = "Desktop GUI for managing remote servers"