Compare commits

...

5 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
IPGO Developer
e2bdffb41e v1.9.40: add Codex integration — skill setup, deploy, GUI buttons
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-07 07:28:11 +00:00
chrome-storm-c442
ddd6951610 v1.9.43: replace Memory quick button with Goroutines (available on all Go targets)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 10:37:52 -05:00
31 changed files with 2334 additions and 317 deletions

View File

@@ -0,0 +1,88 @@
---
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.
metadata:
short-description: Safe remote ops through ServerManager aliases
---
# 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/codex-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/.codex/skills/server-manager/scripts/server-manager-doctor.sh
```
On Windows, use:
```bat
%USERPROFILE%\.codex\skills\server-manager\scripts\server-manager-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/codex-ssh ...
```
It delegates to the installed `ssh.py` backend without requiring a `python` alias.
Safe discovery commands:
```bash
$HOME/.server-connections/codex-ssh --list
$HOME/.server-connections/codex-ssh --info ALIAS
$HOME/.server-connections/codex-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 Claude/Codex
- `tools/skill-ssh.md`: current Claude `/ssh` instructions
- `core/claude_setup.py`: Claude installer logic
- `build.py`: auto-deploys shared CLI files after build
- `README.md` and `CLAUDE.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/codex-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/codex-ssh --list
$HOME/.server-connections/codex-ssh --info ALIAS
$HOME/.server-connections/codex-ssh --status
$HOME/.server-connections/codex-ssh --set-note ALIAS "description"
```
## SSH And Telnet
```bash
$HOME/.server-connections/codex-ssh ALIAS "command"
$HOME/.server-connections/codex-ssh ALIAS --no-sudo "command"
$HOME/.server-connections/codex-ssh ALIAS --upload "local" //remote/path
$HOME/.server-connections/codex-ssh ALIAS --download //remote/path "local"
$HOME/.server-connections/codex-ssh ALIAS --ping
```
Use double slashes for remote SSH/SFTP paths when working from Git Bash style environments.
## SQL
```bash
$HOME/.server-connections/codex-ssh --sql ALIAS "SELECT * FROM table LIMIT 10"
$HOME/.server-connections/codex-ssh --sql-databases ALIAS
$HOME/.server-connections/codex-ssh --sql-tables ALIAS [database]
```
## Redis
```bash
$HOME/.server-connections/codex-ssh --redis ALIAS "GET key"
$HOME/.server-connections/codex-ssh --redis-info ALIAS
$HOME/.server-connections/codex-ssh --redis-keys ALIAS "pattern:*"
```
## S3 / MinIO
Before modifying objects:
```bash
$HOME/.server-connections/codex-ssh --s3-buckets ALIAS
$HOME/.server-connections/codex-ssh --s3-ls ALIAS bucket/prefix/
```
Then act:
```bash
$HOME/.server-connections/codex-ssh --s3-upload ALIAS "local" bucket/key
$HOME/.server-connections/codex-ssh --s3-download ALIAS bucket/key "local"
$HOME/.server-connections/codex-ssh --s3-delete ALIAS bucket/key
$HOME/.server-connections/codex-ssh --s3-url ALIAS bucket/key [seconds]
$HOME/.server-connections/codex-ssh --s3-create-bucket ALIAS bucket-name
```
Do not treat S3 as a shell filesystem.
## Grafana / Prometheus / WinRM
```bash
$HOME/.server-connections/codex-ssh --grafana-dashboards ALIAS
$HOME/.server-connections/codex-ssh --grafana-alerts ALIAS
$HOME/.server-connections/codex-ssh --prom-query ALIAS "up"
$HOME/.server-connections/codex-ssh --prom-targets ALIAS
$HOME/.server-connections/codex-ssh --prom-alerts ALIAS
$HOME/.server-connections/codex-ssh --ps ALIAS "Get-Process"
$HOME/.server-connections/codex-ssh --cmd ALIAS "dir"
```

View File

@@ -0,0 +1,68 @@
# 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
- `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 skills
- `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/`
- `~/.server-connections/codex-ssh` or `codex-ssh.cmd`
- a `~/.claude/CLAUDE.md` guidance block
The Codex 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 Codex 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 `codex-ssh-wrapper.sh`.
- Windows-native Codex wrapper support exists through `codex-ssh-wrapper.cmd`.

View File

@@ -0,0 +1,28 @@
@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 ssh.py at %SSH_SCRIPT% 1>&2
echo hint: install ServerManager shared CLI files first 1>&2
exit /b 1
)
where py >nul 2>&1
if not errorlevel 1 (
py -3 "%SSH_SCRIPT%" %*
exit /b %errorlevel%
)
where python >nul 2>&1
if not errorlevel 1 (
python "%SSH_SCRIPT%" %*
exit /b %errorlevel%
)
echo error: neither py nor python is available in PATH 1>&2
echo hint: install Python launcher or use ServerManager Setup on a machine with Python present 1>&2
exit /b 1

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,41 @@
@echo off
setlocal
set "SHARED_DIR=%SERVER_MANAGER_SHARED_DIR%"
if "%SHARED_DIR%"=="" set "SHARED_DIR=%USERPROFILE%\.server-connections"
set "CODEX_HOME=%USERPROFILE%\.codex\skills\server-manager"
set "FAILED=0"
if exist "%SHARED_DIR%\ssh.py" (
echo ok: found %SHARED_DIR%\ssh.py
) else (
echo error: missing %SHARED_DIR%\ssh.py 1>&2
set "FAILED=1"
)
if exist "%SHARED_DIR%\encryption.py" (
echo ok: found %SHARED_DIR%\encryption.py
) else (
echo error: missing %SHARED_DIR%\encryption.py 1>&2
set "FAILED=1"
)
if exist "%SHARED_DIR%\codex-ssh.cmd" (
echo ok: found %SHARED_DIR%\codex-ssh.cmd
) else (
echo error: missing %SHARED_DIR%\codex-ssh.cmd 1>&2
set "FAILED=1"
)
if exist "%CODEX_HOME%\SKILL.md" (
echo ok: found %CODEX_HOME%\SKILL.md
) else (
echo error: missing %CODEX_HOME%\SKILL.md 1>&2
set "FAILED=1"
)
if "%FAILED%"=="1" exit /b 1
echo ok: ServerManager Codex integration looks installed
exit /b 0

View File

@@ -0,0 +1,41 @@
#!/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}/codex-ssh"
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"
if [[ -d "/home/code/CODING/server-manager" ]]; then
printf '[ok] source repo /home/code/CODING/server-manager\n'
else
printf '[warn] source repo /home/code/CODING/server-manager not found\n'
fi
exit "$status"

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

@@ -1,5 +1,15 @@
# Changelog # Changelog
## [Unreleased]
### Added
- Add Codex integration to Setup tab with dedicated install buttons and status rows
- Add packaged Codex skill deployment and local `codex-ssh` wrapper installation
- Add Windows `.cmd` wrappers for Codex skill installation/runtime
### Changed
- Extend `build.py` auto-deploy to sync Codex artifacts alongside Claude artifacts
- Update docs and i18n strings to describe Claude Code + Codex setup flow
## [1.8.24] - 2026-02-24 ## [1.8.24] - 2026-02-24

409
CODEX_SKILL_SETUP.md Normal file
View File

@@ -0,0 +1,409 @@
# Развёртывание Codex Skill Для ServerManager
Этот документ описывает текущее состояние интеграции `ServerManager -> Codex`, автоматическое и ручное развёртывание, проверку и все известные edge cases.
Поддерживаемый deployment target для этой интеграции:
- Linux
- macOS
- Windows
## Что именно разворачивается
Интеграция для Codex состоит из трёх слоёв:
1. Общий локальный backend:
- `~/.server-connections/ssh.py`
- `~/.server-connections/encryption.py`
- `~/.server-connections/servers.json`
2. Codex skill package:
- `~/.codex/skills/server-manager/`
3. Безопасный wrapper для вызова backend из Codex:
- `~/.server-connections/codex-ssh` на Linux/macOS
- `~/.server-connections/codex-ssh.cmd` на Windows
В репозитории исходники skill лежат здесь:
- [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)
## Как это работает
Модель безопасности та же, что и у Claude integration:
```text
Codex skill -> ~/.server-connections/codex-ssh -> ~/.server-connections/ssh.py -> encrypted servers.json
```
Ключевая идея:
- Codex видит только алиасы и безопасные результаты команд.
- `ssh.py` сам читает credentials из локального зашифрованного хранилища.
- Codex не должен читать `servers.json`, `settings.json` или `encryption.py` напрямую.
## Что уже автоматизировано
Теперь Codex integration встроена в продуктовый setup flow:
- `core/claude_setup.py` ставит `ssh.py`, `encryption.py`, `~/.claude/commands/ssh.md`, `~/.codex/skills/server-manager/`, wrapper `~/.server-connections/codex-ssh` и блок в `~/.claude/CLAUDE.md`
- вкладка `Setup` в GUI показывает отдельные статусы для Claude skill, Codex skill и Codex wrapper
- `build.py` после сборки автоматически синхронизирует Claude- и Codex-артефакты в локальный runtime
Платформенный split такой:
- Linux/macOS: используются `codex-ssh-wrapper.sh` и `server-manager-doctor.sh`
- Windows: используются `codex-ssh-wrapper.cmd` и `server-manager-doctor.cmd`
Ручная установка всё ещё полезна как fallback path, если нужен точечный repair или offline debugging.
## Предварительные условия
Перед установкой Codex skill должны уже существовать или быть установлены через `Setup`:
1. `~/.server-connections/ssh.py`
2. `~/.server-connections/encryption.py`
3. `~/.server-connections/servers.json`
4. `codex` CLI
Проверка:
```bash
ls -la ~/.server-connections
codex --help
```
Если `~/.server-connections/ssh.py` отсутствует:
1. Открыть ServerManager GUI
2. Перейти в `Setup`
3. Нажать `Install Everything`
Это поставит backend, Claude skill и Codex skill целиком.
## Рекомендуемый путь: установка через GUI
1. Открыть ServerManager
2. Перейти в `Setup`
3. Нажать `Install Everything`
4. Проверить, что зелёные статусы появились у:
- `ssh.py`
- `Encryption module`
- `Claude /ssh skill`
- `Codex skill`
- `Codex wrapper`
- `SSH key`
Для точечного ремонта можно использовать отдельные кнопки `Claude skill` и `Codex skill` в той же вкладке.
## Ручная установка Codex Skill
### 1. Скопировать skill package в глобальный Codex home
```bash
mkdir -p ~/.codex/skills
cp -R .codex/skills/server-manager ~/.codex/skills/server-manager
```
### 2. Установить wrapper в shared runtime directory
Linux/macOS:
```bash
install -m 755 .codex/skills/server-manager/scripts/codex-ssh-wrapper.sh ~/.server-connections/codex-ssh
```
Windows:
```bat
copy .codex\skills\server-manager\scripts\codex-ssh-wrapper.cmd %USERPROFILE%\.server-connections\codex-ssh.cmd
```
### 3. Проверить doctor script
Linux/macOS:
```bash
~/.codex/skills/server-manager/scripts/server-manager-doctor.sh
```
Windows:
```bat
%USERPROFILE%\.codex\skills\server-manager\scripts\server-manager-doctor.cmd
```
Ожидается:
- `ssh.py` найден
- `encryption.py` найден
- `codex-ssh` executable
### 4. Проверить wrapper без раскрытия credentials
Linux/macOS:
```bash
~/.server-connections/codex-ssh --list
```
Windows:
```bat
%USERPROFILE%\.server-connections\codex-ssh.cmd --list
```
Это безопасная базовая проверка. Она должна вывести список алиасов и типов серверов.
### 5. Перезапустить Codex
Если у вас уже была открыта интерактивная Codex session, её нужно перезапустить. Новый skill обычно подхватывается новым процессом Codex, а не уже живой сессией.
## Как проверить, что Codex реально видит skill
Самый надёжный способ:
```bash
codex exec --skip-git-repo-check -s read-only -C /tmp \
"A user asks: Using the locally installed ServerManager integration, what is the safest first command to enumerate configured servers? Reply with only the command."
```
Если skill подхватился корректно, Codex должен сам прочитать `~/.codex/skills/server-manager/SKILL.md` и ответить:
```bash
$HOME/.server-connections/codex-ssh --list
```
## Что Codex должен делать через skill
Правильный workflow для любой server operation:
1. Сначала `--list`
2. Прочитать колонку `Type`
3. Выбрать команду строго по типу сервера
4. Выполнить ровно одно подключение/одно действие
5. Вернуть результат без IP/логинов/паролей/портов
Безопасные discovery-команды:
```bash
$HOME/.server-connections/codex-ssh --list
$HOME/.server-connections/codex-ssh --info ALIAS
$HOME/.server-connections/codex-ssh --status
```
## Источники истины по интеграции
Если меняется поведение интеграции, проверять нужно в таком порядке:
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.
## Edge Cases
### 1. `python` alias отсутствует
В этой среде `python` отсутствует, но `ssh.py` имеет shebang `#!/usr/bin/env python3` и executable bit.
Поэтому wrapper вызывает `ssh.py` напрямую:
```bash
~/.server-connections/codex-ssh --list
```
Это намеренно лучше, чем завязка на `python ~/.server-connections/ssh.py`.
### 2. `ssh.py --help` не поддерживается
`ssh.py` не имеет полноценного `--help`. Попытка вызвать `--help` возвращает список доступных alias'ов, а не usage.
Поэтому для безопасной проверки используются:
- `--list`
- `--info ALIAS`
- `--status`
### 3. Skill установлен в repo, но не установлен глобально
Наличие `.codex/skills/server-manager/` внутри репозитория полезно как source of truth, но новый Codex процесс по умолчанию ищет глобальные skills в `~/.codex/skills`.
Если skill есть только в repo:
- документация в проекте будет на месте
- глобальный Codex может его не увидеть
Для надёжности нужен именно глобальный install в `~/.codex/skills/server-manager`.
### 4. Wrapper отсутствует, а skill уже установлен
В этом случае Codex прочитает skill, но не сможет выполнить рекомендуемую команду `$HOME/.server-connections/codex-ssh ...`.
Проверка:
```bash
~/.codex/skills/server-manager/scripts/server-manager-doctor.sh
```
Исправление:
```bash
install -m 755 .codex/skills/server-manager/scripts/codex-ssh-wrapper.sh ~/.server-connections/codex-ssh
```
### 5. Backend отсутствует
Если нет `~/.server-connections/ssh.py` или `encryption.py`, skill бесполезен: он знает workflow, но не имеет локального transport layer.
Исправление:
1. Запустить ServerManager
2. `Setup -> Install Everything`
### 6. Интерактивный Codex уже был запущен до установки skill
Новая интерактивная сессия обычно увидит skill, старая может не увидеть.
Исправление:
- закрыть старую Codex session
- запустить новый процесс `codex`
### 7. `codex exec` не может проверить skill из-за sandbox/network policy
Во время non-interactive проверки Codex может упереться не в skill, а в сетевую политику среды:
- websocket backend заблокирован
- sandbox запрещает соединение
Симптом:
```text
failed to connect to websocket ... Operation not permitted
```
Это не означает, что skill неверный. Это означает, что проверка упёрлась в runtime policy Codex backend.
### 8. Повторная установка поверх существующего skill
GUI installer и `install_codex_skill()` синхронизируют дерево skill поверх существующей директории без полного удаления. Это безопасно для обычных обновлений.
Но при ручном `cp -R` возможен stale state, если какие-то файлы были удалены из repo, а старая глобальная копия осталась.
Полная ручная пересинхронизация нужна только если вы осознанно хотите очистить старые файлы:
1. удалить старую копию осознанно
2. снова скопировать skill целиком
Пример:
```bash
rm -rf ~/.codex/skills/server-manager
cp -R .codex/skills/server-manager ~/.codex/skills/server-manager
```
Делать это только если вы уверены, что хотите полностью пересобрать глобальную копию.
### 9. Изменился `tools/ssh.py`, но глобальная установка осталась старой
Это самый вероятный operational drift.
Что может устареть:
- `~/.server-connections/ssh.py`
- `~/.codex/skills/server-manager/*`
- `~/.server-connections/codex-ssh`
После изменения `tools/ssh.py` или skill docs нужно заново синхронизировать:
```bash
cp tools/ssh.py ~/.server-connections/ssh.py
cp core/encryption.py ~/.server-connections/encryption.py
rm -rf ~/.codex/skills/server-manager
cp -R .codex/skills/server-manager ~/.codex/skills/server-manager
install -m 755 .codex/skills/server-manager/scripts/codex-ssh-wrapper.sh ~/.server-connections/codex-ssh
```
### 10. Windows / macOS / Linux split
Сейчас runtime path intentionally разделён по платформам:
- Linux/macOS: shell wrapper `codex-ssh-wrapper.sh`
- Windows: native wrapper `codex-ssh-wrapper.cmd`
Installer на Windows кладёт wrapper как:
- `~/.server-connections/codex-ssh.cmd`
Installer на Linux/macOS кладёт wrapper как:
- `~/.server-connections/codex-ssh`
Что это закрывает:
- Linux/macOS path без platform-specific разветвления в skill
- запуск через `cmd.exe`
- запуск из PowerShell
- отсутствие bash-зависимости для стандартного Windows deployment path
Ограничение остаётся одно: в текущей среде я прогнал end-to-end smoke только на Linux. macOS и Windows path подготовлены в installer/docs, но не smoke-tested здесь из-за отсутствия соответствующих runner'ов.
### 11. `ssh.py` intentionally не должен читать secrets в контекст AI
Это не баг. Даже если кажется проще открыть `servers.json`, делать этого нельзя.
Skill намеренно запрещает:
- `cat ~/.server-connections/servers.json`
- `cat ~/.server-connections/settings.json`
- `python -c "...read servers.json..."`
- `--list-full`
### 12. fail2ban / anti-bruteforce edge case
Повторные неудачные подключения опасны. Поэтому skill зафиксирован как:
- максимум 1 попытка на действие
- при timeout/ошибке остановиться и сообщить пользователю
Это обязательное правило, а не рекомендация.
## Рекомендуемый Update Workflow
После любых изменений, затрагивающих Codex integration:
1. Обновить исходники в repo:
- `tools/ssh.py`
- `.codex/skills/server-manager/*`
- этот документ
2. Синхронизировать глобальную установку
3. Прогнать doctor
4. Прогнать `~/.server-connections/codex-ssh --list`
5. Прогнать свежий `codex exec` smoke test
## Минимальный Smoke Test
```bash
~/.codex/skills/server-manager/scripts/server-manager-doctor.sh
~/.server-connections/codex-ssh --list
codex exec --skip-git-repo-check -s read-only -C /tmp \
"A user asks: Using the locally installed ServerManager integration, what is the safest first command to enumerate configured servers? Reply with only the command."
```
## Что ещё желательно автоматизировать позже
Чтобы интеграция стала production-complete, в проект ещё полезно добавить:
1. отдельный smoke test script внутри репозитория для проверки именно Codex integration
2. e2e smoke test на Windows runner
3. e2e smoke test на macOS runner
4. optional PowerShell-native wrapper, если понадобится richer Windows logging

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"> <p align="center">
<strong>Desktop GUI for managing remote servers</strong><br> <strong>Desktop GUI for managing remote servers</strong><br>
CustomTkinter + Paramiko | Dark Theme | Claude Code Integration CustomTkinter + Paramiko | Dark Theme | Claude Code + Codex + Gemini Integration
</p> </p>
<p align="center"> <p align="center">
@@ -22,7 +22,7 @@
- **SFTP Transfer** — upload/download files with progress bar - **SFTP Transfer** — upload/download files with progress bar
- **SSH Keys** — generate ed25519, install on server, copy to clipboard - **SSH Keys** — generate ed25519, install on server, copy to clipboard
- **Status Monitor** — background check every 60 sec (online/offline badges) - **Status Monitor** — background check every 60 sec (online/offline badges)
- **Claude Code Integration** — one-click setup, shared config with `/ssh` 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 - **TOTP / 2FA** — Google Authenticator compatible codes with live countdown, one-click copy
- **Encryption** — servers.json encrypted with Fernet (passwords never stored in plaintext) - **Encryption** — servers.json encrypted with Fernet (passwords never stored in plaintext)
- **Backups** — manual and automatic backups with one-click restore - **Backups** — manual and automatic backups with one-click restore
@@ -62,23 +62,25 @@ Output goes to `releases/ServerManager-vX.Y.Z-{platform}.exe`
3. **Terminal** — select server → Terminal tab → type command → Run 3. **Terminal** — select server → Terminal tab → type command → Run
4. **Files** — select server → Files tab → set paths → Upload/Download 4. **Files** — select server → Files tab → set paths → Upload/Download
5. **Keys** — Keys tab → Generate Key → Install on Server 5. **Keys** — Keys tab → Generate Key → Install on Server
6. **Setup** — Setup tab → "Install Everything" → Claude Code ready 6. **Setup** — Setup tab → "Install Everything" → Claude Code, Codex, and Gemini ready
7. Status badges update automatically (green = online, red = offline) 7. Status badges update automatically (green = online, red = offline)
### Claude Code Integration ### Claude Code + Codex + Gemini Integration
ServerManager and Claude Code share the same config file: `~/.server-connections/servers.json` ServerManager, Claude Code, and Codex share the same config file: `~/.server-connections/servers.json`
For Codex deployment and operational edge cases, see [`CODEX_SKILL_SETUP.md`](CODEX_SKILL_SETUP.md).
**How it works:** **How it works:**
``` ```
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (Claude Code) ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py backend
↕ ↕ ↕ ↕
Add/edit/delete /ssh skill Add/edit/delete Claude /ssh + Codex + Gemini skill
servers in GUI executes commands servers in GUI execute commands
``` ```
- Add a server in GUI → Claude Code sees it immediately via `/ssh list` - Add a server in GUI → Claude Code, Codex, and Gemini see it immediately
- Both use the same `ssh.py` + `servers.json` - Both agents use the same `ssh.py` + `servers.json`
- Passwords **never** pass through the AI API - Passwords **never** pass through the AI API
**New SSH Commands:** **New SSH Commands:**
@@ -93,12 +95,17 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (C
**Setup on a new machine:** **Setup on a new machine:**
1. Install ServerManager (clone repo or download binary) 1. Install ServerManager (clone repo or download binary)
2. Open Setup tab → click "Install Everything" 2. Open Setup tab → click "Install Everything"
3. Done. Claude Code now has `/ssh` skill and 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: The Setup tab installs:
- `ssh.py``~/.server-connections/` (SSH utility) - `ssh.py``~/.server-connections/` (SSH utility)
- `encryption.py``~/.server-connections/` (encryption module for CLI) - `encryption.py``~/.server-connections/` (encryption module for CLI)
- `/ssh` skill → `~/.claude/commands/ssh.md` (Claude Code skill) - `/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 - SSH key (ed25519) — if not exists
- Checks for duplicates — safe to run multiple times - Checks for duplicates — safe to run multiple times
@@ -140,7 +147,7 @@ App executes: sudo -S -p '' bash -c 'systemctl restart nginx'
- Passwords stored locally only, **never sent to any AI/API** - Passwords stored locally only, **never sent to any AI/API**
- SSH keys (ed25519) — recommended auth method - SSH keys (ed25519) — recommended auth method
- sudo password sent via stdin (not visible in process list) - sudo password sent via stdin (not visible in process list)
- When used with Claude Code: only alias + command are passed through the AI API, passwords stay in the local encrypted file - When used with Claude Code or Codex: only alias + command are passed through the AI API, passwords stay in the local encrypted file
- Automatic pre-encryption backup on first migration - Automatic pre-encryption backup on first migration
### Project Structure ### Project Structure
@@ -154,7 +161,7 @@ ServerManager/
│ ├── server_store.py # CRUD + encrypted JSON + observer + backups │ ├── server_store.py # CRUD + encrypted JSON + observer + backups
│ ├── encryption.py # Fernet encryption module │ ├── encryption.py # Fernet encryption module
│ ├── ssh_client.py # Paramiko SSH/SFTP wrapper │ ├── ssh_client.py # Paramiko SSH/SFTP wrapper
│ ├── claude_setup.py # Claude Code integration installer │ ├── claude_setup.py # Claude Code + Codex + Gemini integration installer
│ ├── status_checker.py # Background monitoring │ ├── status_checker.py # Background monitoring
│ ├── totp.py # TOTP/2FA module (pyotp) │ ├── totp.py # TOTP/2FA module (pyotp)
│ ├── logger.py # Rotating file logger │ ├── logger.py # Rotating file logger
@@ -166,7 +173,7 @@ ServerManager/
│ ├── tabs/ # Terminal, Files, Info, Keys, Setup │ ├── tabs/ # Terminal, Files, Info, Keys, Setup
│ └── widgets/ # StatusBadge │ └── widgets/ # StatusBadge
├── tools/ # CLI tools (installed to ~/.server-connections/) ├── tools/ # CLI tools (installed to ~/.server-connections/)
│ ├── ssh.py # SSH utility for Claude Code │ ├── ssh.py # SSH utility for Claude Code / Codex
│ └── skill-ssh.md # /ssh skill template │ └── skill-ssh.md # /ssh skill template
├── config/ # Example configs ├── config/ # Example configs
├── releases/ # Built executables ├── releases/ # Built executables
@@ -187,7 +194,7 @@ pip install -r requirements.txt
python main.py python main.py
# → Setup tab → Install Everything # → Setup tab → Install Everything
# → Add your servers via + Add # → Add your servers via + Add
# → Done! Both GUI and Claude Code are ready # → Done! GUI, Claude Code, and Codex are ready
``` ```
--- ---
@@ -201,7 +208,7 @@ python main.py
- **SFTP** — загрузка и скачивание файлов с прогресс-баром - **SFTP** — загрузка и скачивание файлов с прогресс-баром
- **SSH-ключи** — генерация ed25519, установка на сервер, копирование - **SSH-ключи** — генерация ed25519, установка на сервер, копирование
- **Мониторинг** — фоновая проверка каждые 60 сек (бейджи online/offline) - **Мониторинг** — фоновая проверка каждые 60 сек (бейджи online/offline)
- **Интеграция с Claude Code** — установка в один клик, общий конфиг со скиллом `/ssh` - **Интеграция с Claude Code + Codex + Gemini** — установка в один клик, общий конфиг со скиллом `/ssh`, Codex skill и Gemini skill
- **TOTP / 2FA** — коды Google Authenticator с обратным отсчётом, копирование в один клик - **TOTP / 2FA** — коды Google Authenticator с обратным отсчётом, копирование в один клик
- **Шифрование** — servers.json зашифрован Fernet (пароли не хранятся в открытом виде) - **Шифрование** — servers.json зашифрован Fernet (пароли не хранятся в открытом виде)
- **Бэкапы** — ручные и автоматические с восстановлением в один клик - **Бэкапы** — ручные и автоматические с восстановлением в один клик
@@ -241,23 +248,23 @@ python build.py
3. **Терминал** — выберите сервер → вкладка Terminal → введите команду → Run 3. **Терминал** — выберите сервер → вкладка Terminal → введите команду → Run
4. **Файлы** — выберите сервер → вкладка Files → укажите пути → Upload/Download 4. **Файлы** — выберите сервер → вкладка Files → укажите пути → Upload/Download
5. **Ключи** — вкладка Keys → Generate Key → Install on Server 5. **Ключи** — вкладка Keys → Generate Key → Install on Server
6. **Настройка Claude** — вкладка Setup → "Install Everything" → Claude Code готов 6. **Настройка** — вкладка Setup → "Install Everything" → Claude Code, Codex и Gemini готовы
7. Бейджи статуса обновляются автоматически (зелёный = online, красный = offline) 7. Бейджи статуса обновляются автоматически (зелёный = online, красный = offline)
### Интеграция с Claude Code ### Интеграция с Claude Code + Codex + Gemini
ServerManager и Claude Code используют **один и тот же файл конфигурации**: `~/.server-connections/servers.json` ServerManager, Claude Code, Codex и Gemini используют **один и тот же файл конфигурации**: `~/.server-connections/servers.json`
**Как это работает:** **Как это работает:**
``` ```
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (Claude Code) ServerManager GUI ←→ ~/.server-connections/servers.json ←→ backend ssh.py
↕ ↕ ↕ ↕
Добавил/изменил скилл /ssh Добавил/изменил Claude /ssh + Codex + Gemini skill
сервер в GUI выполняет команды серверы в GUI выполняют команды
``` ```
- Добавил сервер в GUI → Claude Code сразу видит его через `/ssh list` - Добавил сервер в GUI → Claude Code, Codex и Gemini сразу видят его
- Оба используют один `ssh.py` + `servers.json` - Оба агента используют один `ssh.py` + `servers.json`
- Пароли **никогда** не проходят через API нейронки - Пароли **никогда** не проходят через API нейронки
**Новые SSH команды:** **Новые SSH команды:**
@@ -272,12 +279,16 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (C
**Настройка на новой машине:** **Настройка на новой машине:**
1. Установить ServerManager (клонировать репо или скачать бинарник) 1. Установить ServerManager (клонировать репо или скачать бинарник)
2. Открыть вкладку Setup → нажать "Install Everything" 2. Открыть вкладку Setup → нажать "Install Everything"
3. Готово. Claude Code теперь имеет скилл `/ssh` и доступ к серверам 3. Готово. Claude Code получает скилл `/ssh`, а Codex и Gemini получают skill `server-manager` и доступ к серверам
Вкладка Setup устанавливает: Вкладка Setup устанавливает:
- `ssh.py``~/.server-connections/` (SSH-утилита) - `ssh.py``~/.server-connections/` (SSH-утилита)
- `encryption.py``~/.server-connections/` (модуль шифрования для CLI) - `encryption.py``~/.server-connections/` (модуль шифрования для CLI)
- скилл `/ssh``~/.claude/commands/ssh.md` (скилл Claude Code) - скилл `/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) — если ещё не создан - SSH-ключ (ed25519) — если ещё не создан
- Проверяет дубли — безопасно запускать повторно - Проверяет дубли — безопасно запускать повторно
@@ -319,7 +330,7 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (C
- Пароли хранятся только локально, **никогда не передаются в AI/API** - Пароли хранятся только локально, **никогда не передаются в AI/API**
- SSH-ключи (ed25519) — рекомендуемый метод аутентификации - SSH-ключи (ed25519) — рекомендуемый метод аутентификации
- sudo-пароль передаётся через stdin (не виден в списке процессов) - sudo-пароль передаётся через stdin (не виден в списке процессов)
- При использовании с Claude Code: через API нейронки проходят только alias + команда, пароли остаются в зашифрованном локальном файле - При использовании с Claude Code или Codex: через API нейронки проходят только alias + команда, пароли остаются в зашифрованном локальном файле
- Автоматический пред-шифровальный бэкап при первой миграции - Автоматический пред-шифровальный бэкап при первой миграции
### Развёртывание на новой машине ### Развёртывание на новой машине
@@ -336,7 +347,7 @@ pip install -r requirements.txt
python main.py python main.py
# → Вкладка Setup → Install Everything # → Вкладка Setup → Install Everything
# → Добавить серверы через + Add # → Добавить серверы через + Add
# → Готово! GUI и Claude Code работают с одним конфигом # → Готово! GUI, Claude Code, Codex и Gemini работают с одним конфигом
``` ```
--- ---
@@ -350,7 +361,7 @@ python main.py
- **SFTP传输** — 带进度条的文件上传/下载 - **SFTP传输** — 带进度条的文件上传/下载
- **SSH密钥** — 生成ed25519、安装到服务器、复制到剪贴板 - **SSH密钥** — 生成ed25519、安装到服务器、复制到剪贴板
- **状态监控** — 每60秒后台检查在线/离线徽标) - **状态监控** — 每60秒后台检查在线/离线徽标)
- **Claude Code集成** — 一键设置,与`/ssh`技能共享配置 - **Claude Code + Codex + Gemini 集成** — 一键设置,与 `/ssh` 技能、Codex skill 和 Gemini skill 共享配置
- **TOTP / 2FA** — 兼容Google Authenticator的验证码实时倒计时一键复制 - **TOTP / 2FA** — 兼容Google Authenticator的验证码实时倒计时一键复制
- **加密** — servers.json使用Fernet加密密码不再以明文存储 - **加密** — servers.json使用Fernet加密密码不再以明文存储
- **备份** — 手动和自动备份,一键恢复 - **备份** — 手动和自动备份,一键恢复
@@ -390,34 +401,36 @@ python build.py
3. **终端** — 选择服务器 → Terminal标签 → 输入命令 → Run 3. **终端** — 选择服务器 → Terminal标签 → 输入命令 → Run
4. **文件** — 选择服务器 → Files标签 → 设置路径 → Upload/Download 4. **文件** — 选择服务器 → Files标签 → 设置路径 → Upload/Download
5. **密钥** — Keys标签 → Generate Key → Install on Server 5. **密钥** — Keys标签 → Generate Key → Install on Server
6. **设置Claude** — Setup标签 → "Install Everything" → Claude Code就绪 6. **设置** — Setup标签 → "Install Everything" → Claude Code 和 Codex 就绪
7. 状态徽标自动更新(绿色 = 在线,红色 = 离线) 7. 状态徽标自动更新(绿色 = 在线,红色 = 离线)
### Claude Code集成 ### Claude Code + Codex + Gemini 集成
ServerManagerClaude Code共享**同一个配置文件**`~/.server-connections/servers.json` ServerManagerClaude Code 和 Codex 共享**同一个配置文件**`~/.server-connections/servers.json`
**工作原理:** **工作原理:**
``` ```
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (Claude Code) ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py 后端
↕ ↕ ↕ ↕
在GUI中添加/编辑 /ssh技能 在GUI中添加/编辑 Claude /ssh + Codex + Gemini skill
服务器 执行命令 服务器 执行命令
``` ```
- 在GUI中添加服务器 → Claude Code立即通过 `/ssh list` 看到 - 在GUI中添加服务器 → Claude Code 和 Codex 都会立即看到
- 两者使用相同的 `ssh.py` + `servers.json` - Claude Code、Codex 和 Gemini 都使用同一个 `ssh.py` + `servers.json`
- 密码**绝不**通过AI API传递 - 密码**绝不**通过AI API传递
**在新机器上设置:** **在新机器上设置:**
1. 安装ServerManager克隆仓库或下载二进制文件 1. 安装ServerManager克隆仓库或下载二进制文件
2. 打开Setup标签 → 点击 "Install Everything" 2. 打开Setup标签 → 点击 "Install Everything"
3. 完成。Claude Code现在拥有 `/ssh` 技能并可访问您的服务器 3. 完成。Claude Code 现在拥有 `/ssh` 技能Codex 现在拥有 `server-manager` 技能并可访问您的服务器
Setup标签安装 Setup标签安装
- `ssh.py``~/.server-connections/`SSH工具 - `ssh.py``~/.server-connections/`SSH工具
- `encryption.py``~/.server-connections/`CLI加密模块 - `encryption.py``~/.server-connections/`CLI加密模块
- `/ssh` 技能 → `~/.claude/commands/ssh.md`Claude Code技能 - `/ssh` 技能 → `~/.claude/commands/ssh.md`Claude Code技能
- `server-manager` 技能 → `~/.codex/skills/server-manager/`Codex技能包
- `codex-ssh` 包装器 → `~/.server-connections/`Codex安全入口
- SSH密钥ed25519— 如果不存在 - SSH密钥ed25519— 如果不存在
- 检查重复 — 可安全重复运行 - 检查重复 — 可安全重复运行
@@ -459,7 +472,7 @@ Setup标签安装
- 密码仅存储在本地,**绝不发送到任何AI/API** - 密码仅存储在本地,**绝不发送到任何AI/API**
- SSH密钥ed25519— 推荐的认证方式 - SSH密钥ed25519— 推荐的认证方式
- sudo密码通过stdin传递在进程列表中不可见 - sudo密码通过stdin传递在进程列表中不可见
- 与Claude Code配合使用时只有别名和命令通过AI API传递密码保留在本地加密文件中 - Claude Code 或 Codex 配合使用时:只有别名和命令通过 AI API 传递,密码保留在本地加密文件中
- 首次迁移时自动创建加密前备份 - 首次迁移时自动创建加密前备份
### 在新机器上部署 ### 在新机器上部署
@@ -476,7 +489,7 @@ pip install -r requirements.txt
python main.py python main.py
# → Setup标签 → Install Everything # → Setup标签 → Install Everything
# → 通过 + Add 添加服务器 # → 通过 + Add 添加服务器
# → 完成GUIClaude Code使用同一个配置 # → 完成GUIClaude Code 和 Codex 使用同一个配置
``` ```
--- ---

View File

@@ -114,7 +114,12 @@ def build():
"--add-data", f"config/servers.example.json{os.pathsep}config", "--add-data", f"config/servers.example.json{os.pathsep}config",
"--add-data", f"tools/ssh.py{os.pathsep}tools", "--add-data", f"tools/ssh.py{os.pathsep}tools",
"--add-data", f"tools/skill-ssh.md{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"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) # PNG icons for GUI (Material Design)
@@ -350,34 +355,34 @@ def cleanup_old_releases():
def deploy_shared_files(): def deploy_shared_files():
"""Auto-deploy ssh.py, encryption.py, skill to shared dirs after build. """Auto-deploy shared CLI files and local agent integrations after build."""
from core.claude_setup import (
install_claude_skill,
install_codex_skill,
install_gemini_skill,
install_ssh_script,
)
Ensures Claude Code /ssh skill always uses the latest version. deploy_steps = [
Without this, editing tools/ssh.py updates the exe but NOT the live install_ssh_script,
~/.server-connections/ssh.py that Claude Code actually calls. install_claude_skill,
""" install_codex_skill,
shared_dir = os.path.expanduser("~/.server-connections") install_gemini_skill,
skill_dir = os.path.expanduser("~/.claude/commands")
deploy_map = [
(os.path.join(PROJECT_DIR, "tools", "ssh.py"),
os.path.join(shared_dir, "ssh.py")),
(os.path.join(PROJECT_DIR, "core", "encryption.py"),
os.path.join(shared_dir, "encryption.py")),
(os.path.join(PROJECT_DIR, "tools", "skill-ssh.md"),
os.path.join(skill_dir, "ssh.md")),
] ]
deployed = [] deployed = []
for src, dst in deploy_map: for step in deploy_steps:
if not os.path.exists(src): try:
continue result = step()
os.makedirs(os.path.dirname(dst), exist_ok=True) if result:
shutil.copy2(src, dst) deployed.append(result.replace("\n", "; "))
deployed.append(os.path.basename(dst)) except Exception as exc:
print(f"WARNING: auto-deploy step failed ({step.__name__}): {exc}")
if deployed: if deployed:
print(f"Auto-deployed to local: {', '.join(deployed)}") print("Auto-deployed to local:")
for item in deployed:
print(f"- {item}")
if __name__ == "__main__": if __name__ == "__main__":

View File

@@ -1,15 +1,16 @@
""" """
Claude Code integration setup. Local AI agent integration setup.
Installs ssh.py, encryption.py, /ssh skill, SSH key — everything needed Installs the shared ssh.py/encryption.py backend, Claude /ssh command,
for Claude Code to manage servers via the shared servers.json. Codex/Gemini skill packages, platform-specific wrappers, and SSH key material.
""" """
import os import os
import sys import re
import shutil import shutil
from core.logger import log import subprocess
import sys
SHARED_DIR = os.path.expanduser("~/.server-connections") from core.logger import log
# PyInstaller: bundled data is in sys._MEIPASS; otherwise use project dir # PyInstaller: bundled data is in sys._MEIPASS; otherwise use project dir
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"): if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
@@ -19,15 +20,20 @@ else:
SSH_SCRIPT_SRC = os.path.join(_BASE_DIR, "tools", "ssh.py") SSH_SCRIPT_SRC = os.path.join(_BASE_DIR, "tools", "ssh.py")
ENCRYPTION_SRC = os.path.join(_BASE_DIR, "core", "encryption.py") ENCRYPTION_SRC = os.path.join(_BASE_DIR, "core", "encryption.py")
SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md") CLAUDE_SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md")
GEMINI_CONTRACT_SRC = os.path.join(_BASE_DIR, "GEMINI.md")
SKILL_DST_DIR = os.path.expanduser("~/.claude/commands") CODEX_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".codex", "skills", "server-manager")
SKILL_DST = os.path.join(SKILL_DST_DIR, "ssh.md") CODEX_WRAPPER_SRC_SH = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.sh")
SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519") CODEX_WRAPPER_SRC_CMD = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.cmd")
GLOBAL_CLAUDE_MD = os.path.expanduser("~/.claude/CLAUDE.md") 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_START = "<!-- server-manager:start -->"
_BLOCK_END = "<!-- server-manager:end -->" _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} GLOBAL_CLAUDE_MD_BLOCK = f"""{_BLOCK_START}
## Серверы — ТОЛЬКО через /ssh ## Серверы — ТОЛЬКО через /ssh
@@ -66,29 +72,224 @@ python ~/.server-connections/ssh.py --status # online/offline
{_BLOCK_END} {_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):
return
mode = os.stat(path).st_mode
os.chmod(path, mode | 0o755)
def _copy_file(src: str, dst: str, executable: bool = False) -> str:
os.makedirs(os.path.dirname(dst), exist_ok=True)
shutil.copy2(src, dst)
if executable:
_ensure_executable(dst)
return dst
def _copy_tree(src: str, dst: str) -> str:
os.makedirs(dst, exist_ok=True)
shutil.copytree(src, dst, dirs_exist_ok=True)
return dst
def _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: def check_status() -> dict:
"""Check what's installed and what's missing.""" """Check what's installed and what's missing."""
shared_dir = _shared_dir()
ssh_key_path = _ssh_key_path()
return { return {
"shared_dir": os.path.exists(SHARED_DIR), "target_home": _target_home(),
"servers_json": os.path.exists(os.path.join(SHARED_DIR, "servers.json")), "shared_dir": os.path.exists(shared_dir),
"ssh_script": os.path.exists(os.path.join(SHARED_DIR, "ssh.py")), "servers_json": os.path.exists(os.path.join(shared_dir, "servers.json")),
"encryption": os.path.exists(os.path.join(SHARED_DIR, "encryption.py")), "ssh_script": os.path.exists(os.path.join(shared_dir, "ssh.py")),
"skill_installed": os.path.exists(SKILL_DST), "encryption": os.path.exists(os.path.join(shared_dir, "encryption.py")),
"ssh_key_exists": os.path.exists(SSH_KEY_PATH), "claude_skill_installed": os.path.exists(_claude_skill_dst()),
"ssh_key_pub": os.path.exists(SSH_KEY_PATH + ".pub"), "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: def install_ssh_script() -> str:
"""Copy ssh.py and encryption.py to shared dir.""" """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 = [] results = []
# Copy ssh.py dst = os.path.join(shared_dir, "ssh.py")
dst = os.path.join(SHARED_DIR, "ssh.py")
if os.path.exists(SSH_SCRIPT_SRC): if os.path.exists(SSH_SCRIPT_SRC):
shutil.copy2(SSH_SCRIPT_SRC, dst) _copy_file(SSH_SCRIPT_SRC, dst, executable=True)
log.info(f"ssh.py installed: {dst}") log.info(f"ssh.py installed: {dst}")
results.append(f"ssh.py installed: {dst}") results.append(f"ssh.py installed: {dst}")
elif os.path.exists(dst): elif os.path.exists(dst):
@@ -96,10 +297,9 @@ def install_ssh_script() -> str:
else: else:
results.append("ERROR: ssh.py source not found") results.append("ERROR: ssh.py source not found")
# Copy encryption.py enc_dst = os.path.join(shared_dir, "encryption.py")
enc_dst = os.path.join(SHARED_DIR, "encryption.py")
if os.path.exists(ENCRYPTION_SRC): if os.path.exists(ENCRYPTION_SRC):
shutil.copy2(ENCRYPTION_SRC, enc_dst) _copy_file(ENCRYPTION_SRC, enc_dst)
log.info(f"encryption.py installed: {enc_dst}") log.info(f"encryption.py installed: {enc_dst}")
results.append(f"encryption.py installed: {enc_dst}") results.append(f"encryption.py installed: {enc_dst}")
elif os.path.exists(enc_dst): elif os.path.exists(enc_dst):
@@ -110,40 +310,125 @@ def install_ssh_script() -> str:
return "\n".join(results) return "\n".join(results)
def install_skill() -> str: def install_claude_skill() -> str:
"""Install /ssh skill for Claude Code.""" """Install /ssh skill for Claude Code."""
os.makedirs(SKILL_DST_DIR, exist_ok=True) claude_skill_dst_dir = _claude_skill_dst_dir()
if os.path.exists(SKILL_SRC): claude_skill_dst = _claude_skill_dst()
shutil.copy2(SKILL_SRC, SKILL_DST) os.makedirs(claude_skill_dst_dir, exist_ok=True)
log.info(f"Skill installed: {SKILL_DST}") if os.path.exists(CLAUDE_SKILL_SRC):
return f"Skill installed: {SKILL_DST}" _copy_file(CLAUDE_SKILL_SRC, claude_skill_dst)
# Fallback: check existing log.info(f"Claude skill installed: {claude_skill_dst}")
if os.path.exists(SKILL_DST): return f"Claude skill installed: {claude_skill_dst}"
return f"Skill already exists: {SKILL_DST}" if os.path.exists(claude_skill_dst):
# Generate minimal skill return f"Claude skill already exists: {claude_skill_dst}"
skill_content = _generate_skill_content() skill_content = _generate_skill_content()
with open(SKILL_DST, "w", encoding="utf-8") as f: with open(claude_skill_dst, "w", encoding="utf-8") as f:
f.write(skill_content) f.write(skill_content)
log.info(f"Skill generated: {SKILL_DST}") log.info(f"Claude skill generated: {claude_skill_dst}")
return f"Skill generated: {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)
_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}")
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()
def generate_ssh_key() -> str: def generate_ssh_key() -> str:
"""Generate ed25519 SSH key if not exists.""" """Generate ed25519 SSH key if not exists."""
if os.path.exists(SSH_KEY_PATH): ssh_key_path = _ssh_key_path()
return f"Key already exists: {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)
import subprocess
try: try:
subprocess.run( subprocess.run(
["ssh-keygen", "-t", "ed25519", "-f", SSH_KEY_PATH, ["ssh-keygen", "-t", "ed25519", "-f", ssh_key_path,
"-N", "", "-C", "server-manager"], "-N", "", "-C", "server-manager"],
check=True, capture_output=True, timeout=15 check=True, capture_output=True, timeout=15
) )
log.info(f"SSH key generated: {SSH_KEY_PATH}") log.info(f"SSH key generated: {ssh_key_path}")
return f"Key generated: {SSH_KEY_PATH}" return f"Key generated: {ssh_key_path}"
except FileNotFoundError: except FileNotFoundError:
hint = "enable OpenSSH optional feature" if sys.platform == "win32" else "install openssh-client" hint = "enable OpenSSH optional feature" if sys.platform == "win32" else "install openssh-client"
msg = f"ERROR: ssh-keygen not found — {hint}" msg = f"ERROR: ssh-keygen not found — {hint}"
@@ -156,16 +441,13 @@ def generate_ssh_key() -> str:
def install_global_claude_md() -> str: def install_global_claude_md() -> str:
"""Add/update server manager section in global ~/.claude/CLAUDE.md. """Add/update server manager section in global ~/.claude/CLAUDE.md."""
global_claude_md = _global_claude_md()
Uses start/end markers to safely replace existing block without duplication. os.makedirs(os.path.dirname(global_claude_md), exist_ok=True)
"""
import re
os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True)
existing = "" existing = ""
if os.path.exists(GLOBAL_CLAUDE_MD): if os.path.exists(global_claude_md):
with open(GLOBAL_CLAUDE_MD, encoding="utf-8") as f: with open(global_claude_md, encoding="utf-8") as f:
existing = f.read() existing = f.read()
pattern = re.compile( pattern = re.compile(
@@ -174,33 +456,65 @@ def install_global_claude_md() -> str:
) )
if pattern.search(existing): if pattern.search(existing):
# Блок уже есть — заменяем на актуальную версию
updated = pattern.sub(GLOBAL_CLAUDE_MD_BLOCK.strip(), 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) f.write(updated)
log.info(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}" return f"Global CLAUDE.md block updated: {global_claude_md}"
else:
# Блока нет — добавляем в конец 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"): if existing and not existing.endswith("\n"):
f.write("\n") f.write("\n")
f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK) f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK)
log.info(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}" 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]: def install_all() -> list[str]:
"""Full setup — install everything.""" """Full setup — install everything for Claude Code, Codex, and Gemini."""
results = [] all_users = os.environ.get("SERVER_MANAGER_INSTALL_ALL_USERS", "").strip() == "1"
base_steps = [
steps = [
("ssh_script", install_ssh_script), ("ssh_script", install_ssh_script),
("skill", install_skill), ("claude_skill", install_claude_skill),
("ssh_key", generate_ssh_key), ("codex_skill", install_codex_skill),
("gemini_skill", install_gemini_skill),
("global_claude_md", install_global_claude_md), ("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: for name, func in steps:
try: try:
log.info(f"install_all: running {name}") log.info(f"install_all: running {name}")
@@ -210,7 +524,29 @@ def install_all() -> list[str]:
msg = f"ERROR ({name}): {e}" msg = f"ERROR ({name}): {e}"
log.error(msg) log.error(msg)
results.append(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 return results

View File

@@ -46,7 +46,7 @@ _EN = {
"about_desc": ( "about_desc": (
"Desktop application for managing remote servers.\n" "Desktop application for managing remote servers.\n"
"SSH terminal, SFTP file transfer, key management,\n" "SSH terminal, SFTP file transfer, key management,\n"
"encrypted credentials, and Claude Code integration." "encrypted credentials, and Claude Code / Codex integration."
), ),
"about_features_title": "⚡ Features", "about_features_title": "⚡ Features",
"about_features": ( "about_features": (
@@ -56,13 +56,13 @@ _EN = {
"• TOTP / 2FA (Google Authenticator)\n" "• TOTP / 2FA (Google Authenticator)\n"
"• Encrypted credentials (Fernet)\n" "• Encrypted credentials (Fernet)\n"
"• Automatic backups\n" "• Automatic backups\n"
"• Claude Code integration" "• Claude Code and Codex integration"
), ),
"about_howto_title": "🚀 Quick Start", "about_howto_title": "🚀 Quick Start",
"about_howto": ( "about_howto": (
"1. Click \"+ Add\" to add a server\n" "1. Click \"+ Add\" to add a server\n"
"2. Select server → Terminal / Files\n" "2. Select server → Terminal / Files\n"
"3. Setup tab → Claude Code integration" "3. Setup tab → Claude Code / Codex integration"
), ),
"version": "Version", "version": "Version",
"author": "Author", "author": "Author",
@@ -157,6 +157,12 @@ _EN = {
"no_public_key": "[!] No public key to copy", "no_public_key": "[!] No public key to copy",
# Setup # Setup
"agent_integration": "AI Agent Integration",
"agent_desc": (
"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_integration": "Claude Code Integration",
"claude_desc": ( "claude_desc": (
"Setup everything so Claude Code can manage your servers via /ssh skill.\n" "Setup everything so Claude Code can manage your servers via /ssh skill.\n"
@@ -169,11 +175,19 @@ _EN = {
"status_ssh_script": "ssh.py (CLI tool)", "status_ssh_script": "ssh.py (CLI tool)",
"status_encryption": "Encryption module", "status_encryption": "Encryption module",
"status_skill": "/ssh skill for Claude Code", "status_skill": "/ssh skill for Claude Code",
"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)", "status_ssh_key": "SSH key (ed25519)",
"install_everything": "Install Everything", "install_everything": "Install Everything",
"installing_all": "Installing...", "installing_all": "Installing...",
"install_ssh_py": "ssh.py", "install_ssh_py": "ssh.py",
"install_skill": "/ssh skill", "install_skill": "/ssh skill",
"install_claude_skill": "Claude skill",
"install_codex_skill": "Codex skill",
"install_gemini_skill": "Gemini skill",
"install_ssh_key": "SSH key", "install_ssh_key": "SSH key",
"refresh": "Refresh", "refresh": "Refresh",
"configuration": "Configuration", "configuration": "Configuration",
@@ -183,7 +197,7 @@ _EN = {
"select_backup": "Select backup...", "select_backup": "Select backup...",
"no_backups": "No backups", "no_backups": "No backups",
"restore": "Restore", "restore": "Restore",
"install_done": "Done! Claude Code can now use /ssh 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}", "config_changed": "Config path changed: {path}",
"backup_created": "Backup created: {name}", "backup_created": "Backup created: {name}",
"backup_failed": "Backup failed: {e}", "backup_failed": "Backup failed: {e}",
@@ -603,7 +617,7 @@ _RU = {
"about_desc": ( "about_desc": (
"Настольное приложение для управления удалёнными серверами.\n" "Настольное приложение для управления удалёнными серверами.\n"
"SSH-терминал, SFTP-передача файлов, управление ключами,\n" "SSH-терминал, SFTP-передача файлов, управление ключами,\n"
"шифрование паролей и интеграция с Claude Code." "шифрование паролей и интеграция с Claude Code / Codex."
), ),
"about_features_title": "⚡ Возможности", "about_features_title": "⚡ Возможности",
"about_features": ( "about_features": (
@@ -613,13 +627,13 @@ _RU = {
"• TOTP / 2FA (Google Authenticator)\n" "• TOTP / 2FA (Google Authenticator)\n"
"• Шифрование паролей (Fernet)\n" "• Шифрование паролей (Fernet)\n"
"• Автоматические бэкапы\n" "• Автоматические бэкапы\n"
"• Интеграция с Claude Code" "• Интеграция с Claude Code и Codex"
), ),
"about_howto_title": "🚀 Быстрый старт", "about_howto_title": "🚀 Быстрый старт",
"about_howto": ( "about_howto": (
"1. Нажмите \"+ Добавить\" для добавления сервера\n" "1. Нажмите \"+ Добавить\" для добавления сервера\n"
"2. Выберите сервер → Терминал / Файлы\n" "2. Выберите сервер → Терминал / Файлы\n"
"3. Вкладка Настройка → интеграция Claude Code" "3. Вкладка Настройка → интеграция Claude Code / Codex"
), ),
"version": "Версия", "version": "Версия",
"author": "Автор", "author": "Автор",
@@ -714,23 +728,37 @@ _RU = {
"no_public_key": "[!] Нет публичного ключа", "no_public_key": "[!] Нет публичного ключа",
# Setup # Setup
"agent_integration": "Интеграция AI-агентов",
"claude_integration": "Интеграция с Claude Code", "claude_integration": "Интеграция с Claude Code",
"claude_desc": ( "claude_desc": (
"Настройте всё, чтобы Claude Code мог управлять серверами через скилл /ssh.\n" "Настройте всё, чтобы Claude Code мог управлять серверами через скилл /ssh.\n"
"GUI и Claude Code используют один servers.json — добавьте сервер здесь,\n" "GUI и Claude Code используют один servers.json — добавьте сервер здесь,\n"
"Claude увидит его сразу." "Claude увидит его сразу."
), ),
"agent_desc": (
"Настройте всё, чтобы Claude Code, Codex и Gemini могли управлять серверами через локальные skills.\n"
"ServerManager, Claude Code, Codex и Gemini используют один и тот же servers.json — добавьте сервер здесь,\n"
"все агенты увидят его сразу."
),
"status": "Статус", "status": "Статус",
"status_shared_dir": "Общий каталог (~/.server-connections)", "status_shared_dir": "Общий каталог (~/.server-connections)",
"status_servers_json": "servers.json", "status_servers_json": "servers.json",
"status_ssh_script": "ssh.py (CLI-утилита)", "status_ssh_script": "ssh.py (CLI-утилита)",
"status_encryption": "Модуль шифрования", "status_encryption": "Модуль шифрования",
"status_skill": "Скилл /ssh для Claude Code", "status_skill": "Скилл /ssh для Claude Code",
"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)", "status_ssh_key": "SSH-ключ (ed25519)",
"install_everything": "Установить всё", "install_everything": "Установить всё",
"installing_all": "Установка...", "installing_all": "Установка...",
"install_ssh_py": "ssh.py", "install_ssh_py": "ssh.py",
"install_skill": "Скилл /ssh", "install_skill": "Скилл /ssh",
"install_claude_skill": "Скилл Claude",
"install_codex_skill": "Скилл Codex",
"install_gemini_skill": "Скилл Gemini",
"install_ssh_key": "SSH-ключ", "install_ssh_key": "SSH-ключ",
"refresh": "Обновить", "refresh": "Обновить",
"configuration": "Конфигурация", "configuration": "Конфигурация",
@@ -740,7 +768,7 @@ _RU = {
"select_backup": "Выберите бэкап...", "select_backup": "Выберите бэкап...",
"no_backups": "Нет бэкапов", "no_backups": "Нет бэкапов",
"restore": "Восстановить", "restore": "Восстановить",
"install_done": "Готово! Claude Code теперь может использовать /ssh для управления серверами.", "install_done": "Готово! Claude Code, Codex и Gemini теперь могут использовать ServerManager для управления серверами.",
"config_changed": "Путь конфига изменён: {path}", "config_changed": "Путь конфига изменён: {path}",
"backup_created": "Бэкап создан: {name}", "backup_created": "Бэкап создан: {name}",
"backup_failed": "Ошибка бэкапа: {e}", "backup_failed": "Ошибка бэкапа: {e}",
@@ -1160,7 +1188,7 @@ _ZH = {
"about_desc": ( "about_desc": (
"用于管理远程服务器的桌面应用程序。\n" "用于管理远程服务器的桌面应用程序。\n"
"SSH终端、SFTP文件传输、密钥管理、\n" "SSH终端、SFTP文件传输、密钥管理、\n"
"凭据加密以及Claude Code集成。" "凭据加密以及Claude Code / Codex集成。"
), ),
"about_features_title": "⚡ 功能特点", "about_features_title": "⚡ 功能特点",
"about_features": ( "about_features": (
@@ -1170,13 +1198,13 @@ _ZH = {
"• TOTP / 2FAGoogle Authenticator\n" "• TOTP / 2FAGoogle Authenticator\n"
"• 凭据加密Fernet\n" "• 凭据加密Fernet\n"
"• 自动备份\n" "• 自动备份\n"
"• Claude Code集成" "• Claude Code 和 Codex 集成"
), ),
"about_howto_title": "🚀 快速开始", "about_howto_title": "🚀 快速开始",
"about_howto": ( "about_howto": (
"1. 点击\"+ 添加\"来添加服务器\n" "1. 点击\"+ 添加\"来添加服务器\n"
"2. 选择服务器 → 终端 / 文件\n" "2. 选择服务器 → 终端 / 文件\n"
"3. 设置标签 → Claude Code集成" "3. 设置标签 → Claude Code / Codex 集成"
), ),
"version": "版本", "version": "版本",
"author": "作者", "author": "作者",
@@ -1271,6 +1299,7 @@ _ZH = {
"no_public_key": "[!] 没有公钥可复制", "no_public_key": "[!] 没有公钥可复制",
# Setup # Setup
"agent_integration": "AI代理集成",
"claude_integration": "Claude Code集成", "claude_integration": "Claude Code集成",
"claude_desc": ( "claude_desc": (
"设置一切以便Claude Code通过/ssh技能管理您的服务器。\n" "设置一切以便Claude Code通过/ssh技能管理您的服务器。\n"
@@ -1282,12 +1311,25 @@ _ZH = {
"status_servers_json": "servers.json", "status_servers_json": "servers.json",
"status_ssh_script": "ssh.pyCLI工具", "status_ssh_script": "ssh.pyCLI工具",
"status_encryption": "加密模块", "status_encryption": "加密模块",
"agent_desc": (
"完成设置后Claude Code、Codex 和 Gemini 都可以通过共享的本地 skills 管理您的服务器。\n"
"ServerManager、Claude Code、Codex 和 Gemini 共用同一个 servers.json — 在此添加服务器后,\n"
"所有代理都会立即看到。"
),
"status_skill": "Claude Code的/ssh技能", "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", "status_ssh_key": "SSH密钥ed25519",
"install_everything": "全部安装", "install_everything": "全部安装",
"installing_all": "安装中...", "installing_all": "安装中...",
"install_ssh_py": "ssh.py", "install_ssh_py": "ssh.py",
"install_skill": "/ssh技能", "install_skill": "/ssh技能",
"install_claude_skill": "Claude 技能",
"install_codex_skill": "Codex 技能",
"install_gemini_skill": "Gemini 技能",
"install_ssh_key": "SSH密钥", "install_ssh_key": "SSH密钥",
"refresh": "刷新", "refresh": "刷新",
"configuration": "配置", "configuration": "配置",
@@ -1297,7 +1339,7 @@ _ZH = {
"select_backup": "选择备份...", "select_backup": "选择备份...",
"no_backups": "无备份", "no_backups": "无备份",
"restore": "恢复", "restore": "恢复",
"install_done": "完成Claude Code现在可以使用/ssh来管理您的服务器。", "install_done": "完成Claude Code、Codex 和 Gemini 现在可以使用 ServerManager 来管理您的服务器。",
"config_changed": "配置路径已更改:{path}", "config_changed": "配置路径已更改:{path}",
"backup_created": "备份已创建:{name}", "backup_created": "备份已创建:{name}",
"backup_failed": "备份失败:{e}", "backup_failed": "备份失败:{e}",

View File

@@ -46,7 +46,7 @@ class PrometheusTab(ctk.CTkFrame):
quick_frame.pack(fill="x", padx=15, pady=(0, 5)) quick_frame.pack(fill="x", padx=15, pady=(0, 5))
for label, query in [("up", "up"), ("CPU", "process_cpu_seconds_total"), for label, query in [("up", "up"), ("CPU", "process_cpu_seconds_total"),
("Memory", "node_memory_MemFree_bytes")]: ("Goroutines", "go_goroutines")]:
btn = make_icon_button(quick_frame, "metrics", label, width=80, btn = make_icon_button(quick_frame, "metrics", label, width=80,
fg_color="#6b7280", hover_color="#4b5563", fg_color="#6b7280", hover_color="#4b5563",
command=lambda q=query: self._run_quick(q)) command=lambda q=query: self._run_quick(q))

View File

@@ -1,5 +1,5 @@
""" """
Setup tab — one-click installation for Claude Code integration. Setup tab — one-click installation for local AI agent integration.
Includes configuration path management and backup/restore. Includes configuration path management and backup/restore.
""" """
@@ -8,7 +8,15 @@ import threading
from datetime import datetime from datetime import datetime
from tkinter import filedialog, messagebox from tkinter import filedialog, messagebox
import customtkinter as ctk import customtkinter as ctk
from core.claude_setup import check_status, install_all, install_ssh_script, install_skill, generate_ssh_key from core.claude_setup import (
check_status,
generate_ssh_key,
install_all,
install_claude_skill,
install_codex_skill,
install_gemini_skill,
install_ssh_script,
)
from core.i18n import t from core.i18n import t
from core.icons import icon_text, make_icon_button from core.icons import icon_text, make_icon_button
from core.logger import log from core.logger import log
@@ -25,13 +33,13 @@ class SetupTab(ctk.CTkFrame):
# Header # Header
self.header_label = ctk.CTkLabel( self.header_label = ctk.CTkLabel(
self._scroll, text=t("claude_integration"), self._scroll, text=t("agent_integration"),
font=ctk.CTkFont(size=20, weight="bold") font=ctk.CTkFont(size=20, weight="bold")
) )
self.header_label.pack(padx=20, pady=(20, 5)) self.header_label.pack(padx=20, pady=(20, 5))
self.desc_label = ctk.CTkLabel( self.desc_label = ctk.CTkLabel(
self._scroll, text=t("claude_desc"), self._scroll, text=t("agent_desc"),
text_color="#9ca3af", justify="center" text_color="#9ca3af", justify="center"
) )
self.desc_label.pack(padx=20, pady=(0, 15)) self.desc_label.pack(padx=20, pady=(0, 15))
@@ -53,7 +61,11 @@ class SetupTab(ctk.CTkFrame):
("servers_json", "status_servers_json"), ("servers_json", "status_servers_json"),
("ssh_script", "status_ssh_script"), ("ssh_script", "status_ssh_script"),
("encryption", "status_encryption"), ("encryption", "status_encryption"),
("skill_installed", "status_skill"), ("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"), ("ssh_key_exists", "status_ssh_key"),
] ]
for key, i18n_key in status_items: for key, i18n_key in status_items:
@@ -82,17 +94,46 @@ class SetupTab(ctk.CTkFrame):
ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent") ind_frame = ctk.CTkFrame(btn_frame, fg_color="transparent")
ind_frame.pack(fill="x") ind_frame.pack(fill="x")
self.ssh_py_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_py"), width=110, fg_color="#6b7280", top_btn_row = ctk.CTkFrame(ind_frame, fg_color="transparent")
command=self._install_script) top_btn_row.pack(fill="x", pady=(0, 5))
self.ssh_py_btn = make_icon_button(
top_btn_row, "confirm", t("install_ssh_py"), width=120, fg_color="#6b7280",
command=self._install_script
)
self.ssh_py_btn.pack(side="left", padx=(0, 5)) self.ssh_py_btn.pack(side="left", padx=(0, 5))
self.skill_btn = make_icon_button(ind_frame, "confirm", t("install_skill"), width=110, fg_color="#6b7280",
command=self._install_skill) self.claude_skill_btn = make_icon_button(
self.skill_btn.pack(side="left", padx=5) top_btn_row, "confirm", t("install_claude_skill"), width=130, fg_color="#6b7280",
self.ssh_key_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_key"), width=110, fg_color="#6b7280", command=self._install_claude_skill
command=self._gen_key) )
self.ssh_key_btn.pack(side="left", padx=5) self.claude_skill_btn.pack(side="left", padx=5)
self.refresh_btn = make_icon_button(ind_frame, "refresh", t("refresh"), width=90, fg_color="#3b82f6",
command=self._refresh_status) self.codex_skill_btn = make_icon_button(
top_btn_row, "confirm", t("install_codex_skill"), width=130, fg_color="#6b7280",
command=self._install_codex_skill
)
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")
self.ssh_key_btn = make_icon_button(
bottom_btn_row, "confirm", t("install_ssh_key"), width=120, fg_color="#6b7280",
command=self._gen_key
)
self.ssh_key_btn.pack(side="left", padx=(0, 5))
self.refresh_btn = make_icon_button(
bottom_btn_row, "refresh", t("refresh"), width=90, fg_color="#3b82f6",
command=self._refresh_status
)
self.refresh_btn.pack(side="right") self.refresh_btn.pack(side="right")
# ── Monitoring section ───────────────────────── # ── Monitoring section ─────────────────────────
@@ -328,8 +369,23 @@ class SetupTab(ctk.CTkFrame):
self._log(msg) self._log(msg)
self._refresh_status() self._refresh_status()
def _install_claude_skill(self):
msg = install_claude_skill()
self._log(msg)
self._refresh_status()
def _install_codex_skill(self):
msg = install_codex_skill()
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): def _install_skill(self):
msg = install_skill() msg = install_claude_skill()
self._log(msg) self._log(msg)
self._refresh_status() self._refresh_status()

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 #!/usr/bin/env bash
# ───────────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────────
# ServerManager CLI Installer for Linux (headless / no-GUI) # ServerManager AI Integration Installer for Linux/macOS (headless / no-GUI)
# #
# Устанавливает: # Installs for each target home:
# - ssh.py + encryption.py ~/.server-connections/ # - ssh.py + encryption.py -> ~/.server-connections/
# - servers.json + settings.json → ~/.server-connections/ (если есть) # - Claude /ssh skill -> ~/.claude/commands/
# - CLAUDE.md → ~/.claude/ # - Codex server-manager skill -> ~/.codex/skills/server-manager/
# - ssh.md (скилл) → ~/.claude/commands/ # - Gemini server-manager skill -> ~/.gemini/skills/server-manager/
# - Python-зависимости для CLI (paramiko, cryptography, etc.) # - codex-ssh / gemini-ssh wrappers -> ~/.server-connections/
# - CLAUDE.md / GEMINI.md (if available) -> ~/.claude/ / ~/.gemini/
# #
# Запуск: # Optional per-target local config copy:
# curl -sSL https://git.sensey24.ru/aibot777/server-manager/raw/branch/master/tools/install.sh | bash # - 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
# или с указанием источника файлов: # 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 set -euo pipefail
# ── Colors ──
RED='\033[0;31m' RED='\033[0;31m'
GREEN='\033[0;32m' GREEN='\033[0;32m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
BLUE='\033[0;34m' BLUE='\033[0;34m'
CYAN='\033[0;36m' CYAN='\033[0;36m'
NC='\033[0m' # No Color NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; } info() { echo -e "${BLUE}[INFO]${NC} $*"; }
ok() { echo -e "${GREEN}[OK]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; }
@@ -32,33 +42,80 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*"; }
step() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; } step() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; }
# ── Config ── usage() {
CONN_DIR="$HOME/.server-connections" cat <<USAGE
CLAUDE_DIR="$HOME/.claude" ServerManager AI integration installer
COMMANDS_DIR="$CLAUDE_DIR/commands"
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" 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) while [[ $# -gt 0 ]]; do
SRC_DIR="${1:-}" 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 -e "${CYAN}"
echo "╔══════════════════════════════════════════════╗" echo "╔══════════════════════════════════════════════════════╗"
echo "║ ServerManager CLI Installer for Linux ║" echo "║ ServerManager AI Integration Installer (headless) ║"
echo "║ github: git.sensey24.ru/aibot777 ║" echo "║ Claude + Codex + Gemini ║"
echo "╚══════════════════════════════════════════════╝" echo "╚══════════════════════════════════════════════════════╝"
echo -e "${NC}" echo -e "${NC}"
# ── Step 1: Check Python ──
step "1/5 Проверка Python" step "1/5 Проверка Python"
PYTHON="" PYTHON=""
for cmd in python3 python; do for cmd in python3 python; do
if command -v "$cmd" &>/dev/null; then if command -v "$cmd" &>/dev/null; then
ver=$("$cmd" --version 2>&1 | grep -oP '\d+\.\d+') if "$cmd" - <<'PY' &>/dev/null
major=$(echo "$ver" | cut -d. -f1) import sys
minor=$(echo "$ver" | cut -d. -f2) raise SystemExit(0 if sys.version_info >= (3, 8) else 1)
if [ "$major" -ge 3 ] && [ "$minor" -ge 8 ]; then PY
then
PYTHON="$cmd" PYTHON="$cmd"
ok "Python найден: $($cmd --version)" ok "Python найден: $($cmd --version)"
break break
@@ -66,14 +123,11 @@ for cmd in python3 python; do
fi fi
done done
if [ -z "$PYTHON" ]; then if [[ -z "$PYTHON" ]]; then
error "Python 3.8+ не найден!" error "Python 3.8+ не найден"
echo " Установите: sudo apt install python3 python3-pip"
echo " или: sudo yum install python3 python3-pip"
exit 1 exit 1
fi fi
# Check pip
PIP="" PIP=""
for cmd in pip3 pip; do for cmd in pip3 pip; do
if command -v "$cmd" &>/dev/null; then if command -v "$cmd" &>/dev/null; then
@@ -81,22 +135,26 @@ for cmd in pip3 pip; do
break break
fi fi
done done
if [[ -z "$PIP" ]]; then
if [ -z "$PIP" ]; then
# Try python -m pip
if $PYTHON -m pip --version &>/dev/null; then if $PYTHON -m pip --version &>/dev/null; then
PIP="$PYTHON -m pip" PIP="$PYTHON -m pip"
else else
error "pip не найден!" error "pip не найден"
echo " Установите: sudo apt install python3-pip"
exit 1 exit 1
fi fi
fi fi
ok "pip найден: $($PIP --version 2>&1 | head -1)" ok "pip найден: $($PIP --version 2>&1 | head -1)"
# ── Step 2: Install Python dependencies ── resolve_home() {
step "2/5 Установка Python-зависимостей" "$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=( CLI_DEPS=(
"paramiko>=3.4.0" "paramiko>=3.4.0"
"cryptography>=41.0.0" "cryptography>=41.0.0"
@@ -105,7 +163,6 @@ CLI_DEPS=(
"redis>=5.0.0" "redis>=5.0.0"
"requests>=2.31.0" "requests>=2.31.0"
) )
for dep in "${CLI_DEPS[@]}"; do for dep in "${CLI_DEPS[@]}"; do
pkg=$(echo "$dep" | sed 's/[>=<].*//') pkg=$(echo "$dep" | sed 's/[>=<].*//')
if $PYTHON -c "import $pkg" 2>/dev/null; then if $PYTHON -c "import $pkg" 2>/dev/null; then
@@ -120,38 +177,26 @@ for dep in "${CLI_DEPS[@]}"; do
fi fi
done 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() { copy_or_download() {
local src_relative="$1" local src_relative="$1"
local dst="$2" local dst="$2"
local perms="$3" local perms="$3"
local desc="$4" local desc="$4"
# Try local source first mkdir -p "$(dirname "$dst")"
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/$src_relative" ]; then
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/$src_relative" ]]; then
cp "$SRC_DIR/$src_relative" "$dst" cp "$SRC_DIR/$src_relative" "$dst"
chmod "$perms" "$dst" chmod "$perms" "$dst" 2>/dev/null || true
ok "$desc (из $SRC_DIR)" ok "$desc (из $SRC_DIR)"
return 0 return 0
fi fi
# Try download from Gitea
local url="$GITEA_RAW/$src_relative" local url="$GITEA_RAW/$src_relative"
if command -v curl &>/dev/null; then if command -v curl &>/dev/null; then
if curl -sSL -o "$dst" "$url" 2>/dev/null; then if curl -fsSL -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
if [ -s "$dst" ] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then chmod "$perms" "$dst" 2>/dev/null || true
chmod "$perms" "$dst"
ok "$desc (скачан с Gitea)" ok "$desc (скачан с Gitea)"
return 0 return 0
fi fi
@@ -159,8 +204,8 @@ copy_or_download() {
fi fi
elif command -v wget &>/dev/null; then elif command -v wget &>/dev/null; then
if wget -q -O "$dst" "$url" 2>/dev/null; then if wget -q -O "$dst" "$url" 2>/dev/null; then
if [ -s "$dst" ] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then if [[ -s "$dst" ]] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
chmod "$perms" "$dst" chmod "$perms" "$dst" 2>/dev/null || true
ok "$desc (скачан с Gitea)" ok "$desc (скачан с Gitea)"
return 0 return 0
fi fi
@@ -172,86 +217,180 @@ copy_or_download() {
return 1 return 1
} }
# Core files (always install) install_skill_tree() {
copy_or_download "tools/ssh.py" "$CONN_DIR/ssh.py" "755" "ssh.py" local prefix="$1"
copy_or_download "core/encryption.py" "$CONN_DIR/encryption.py" "644" "encryption.py" 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 discover_homes() {
copy_or_download "tools/skill-ssh.md" "$COMMANDS_DIR/ssh.md" "644" "ssh.md (скилл /ssh)" local homes=()
local uname_s
uname_s="$(uname -s 2>/dev/null || echo Linux)"
# CLAUDE.md if [[ "$INSTALL_ALL_USERS" -eq 1 ]]; then
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/CLAUDE.md" ]; 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" cp "$SRC_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
chmod 644 "$CLAUDE_DIR/CLAUDE.md" chmod 644 "$CLAUDE_DIR/CLAUDE.md"
ok "CLAUDE.md" ok "CLAUDE.md"
fi 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/GEMINI.md" ]]; then
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/servers.json" ]; 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" cp "$SRC_DIR/servers.json" "$CONN_DIR/servers.json"
chmod 600 "$CONN_DIR/servers.json" chmod 600 "$CONN_DIR/servers.json"
ok "servers.json (зашифрованный)" ok "servers.json (зашифрованный)"
elif [ ! -f "$CONN_DIR/servers.json" ]; then elif [[ ! -f "$CONN_DIR/servers.json" ]]; then
warn "servers.json не найден — скопируйте с основной машины:" warn "servers.json не найден для $HOME_DIR — скопируйте вручную"
echo " scp user@main:~/.server-connections/servers.json $CONN_DIR/" fi
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" cp "$SRC_DIR/settings.json" "$CONN_DIR/settings.json"
chmod 600 "$CONN_DIR/settings.json" chmod 600 "$CONN_DIR/settings.json"
ok "settings.json" ok "settings.json"
elif [ ! -f "$CONN_DIR/settings.json" ]; then elif [[ ! -f "$CONN_DIR/settings.json" ]]; then
# Create minimal settings
echo '{"language":"en","check_interval":60}' > "$CONN_DIR/settings.json" echo '{"language":"en","check_interval":60}' > "$CONN_DIR/settings.json"
chmod 600 "$CONN_DIR/settings.json" chmod 600 "$CONN_DIR/settings.json"
ok "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)"
else
warn "ssh.py вернул ошибку — проверьте зависимости"
fi fi
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
# ── Summary ──
echo "" echo ""
echo -e "${CYAN}━━━ Готово ━━━${NC}" echo -e "${CYAN}━━━ Готово ━━━${NC}"
echo "" echo ""
@@ -260,17 +399,19 @@ if $ALL_OK; then
else else
echo -e "${YELLOW}Установка завершена с предупреждениями.${NC}" echo -e "${YELLOW}Установка завершена с предупреждениями.${NC}"
fi fi
echo "" echo ""
echo "Файлы:" echo "Установлено для home:"
echo " $CONN_DIR/ssh.py — CLI-утилита" printf ' - %s\n' "${TARGET_HOMES[@]}"
echo " $CONN_DIR/encryption.py — модуль шифрования"
echo " $CONN_DIR/servers.json — серверы (зашифрованные)"
echo " $COMMANDS_DIR/ssh.md — скилл /ssh для Claude Code"
echo "" echo ""
echo "Использование:" echo "Использование:"
echo " python3 ~/.server-connections/ssh.py --list" echo " python3 ~/.server-connections/ssh.py --list"
echo " python3 ~/.server-connections/ssh.py --info ALIAS" echo " ~/.server-connections/codex-ssh --list"
echo " python3 ~/.server-connections/ssh.py ALIAS \"command\"" echo " ~/.server-connections/gemini-ssh --list"
echo ""
echo "Claude Code скилл: /ssh"
echo "" 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 info for ServerManager."""
__version__ = "1.9.42" __version__ = "1.9.44"
__app_name__ = "ServerManager" __app_name__ = "ServerManager"
__author__ = "aibot777" __author__ = "aibot777"
__description__ = "Desktop GUI for managing remote servers" __description__ = "Desktop GUI for managing remote servers"