Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136d1605c0 | ||
|
|
9da3125c34 | ||
|
|
daa11ca440 | ||
|
|
e2bdffb41e | ||
|
|
ddd6951610 |
88
.codex/skills/server-manager/SKILL.md
Normal file
88
.codex/skills/server-manager/SKILL.md
Normal 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.
|
||||
91
.codex/skills/server-manager/references/command-matrix.md
Normal file
91
.codex/skills/server-manager/references/command-matrix.md
Normal 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"
|
||||
```
|
||||
68
.codex/skills/server-manager/references/project.md
Normal file
68
.codex/skills/server-manager/references/project.md
Normal 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`.
|
||||
28
.codex/skills/server-manager/scripts/codex-ssh-wrapper.cmd
Normal file
28
.codex/skills/server-manager/scripts/codex-ssh-wrapper.cmd
Normal 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
|
||||
13
.codex/skills/server-manager/scripts/codex-ssh-wrapper.sh
Normal file
13
.codex/skills/server-manager/scripts/codex-ssh-wrapper.sh
Normal 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" "$@"
|
||||
@@ -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
|
||||
@@ -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
8
.gemini/settings.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"context": {
|
||||
"fileName": "GEMINI.md"
|
||||
},
|
||||
"experimental": {
|
||||
"enableAgents": true
|
||||
}
|
||||
}
|
||||
84
.gemini/skills/server-manager/SKILL.md
Normal file
84
.gemini/skills/server-manager/SKILL.md
Normal 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.
|
||||
91
.gemini/skills/server-manager/references/command-matrix.md
Normal file
91
.gemini/skills/server-manager/references/command-matrix.md
Normal 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"
|
||||
```
|
||||
73
.gemini/skills/server-manager/references/project.md
Normal file
73
.gemini/skills/server-manager/references/project.md
Normal 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`.
|
||||
11
.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.cmd
Normal file
11
.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.cmd
Normal 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%" %*
|
||||
13
.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh
Normal file
13
.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh
Normal 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" "$@"
|
||||
@@ -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%
|
||||
@@ -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"
|
||||
10
CHANGELOG.md
10
CHANGELOG.md
@@ -1,5 +1,15 @@
|
||||
# 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
|
||||
|
||||
|
||||
409
CODEX_SKILL_SETUP.md
Normal file
409
CODEX_SKILL_SETUP.md
Normal 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
52
GEMINI.md
Normal 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
86
GEMINI_SKILL_SETUP.md
Normal 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.
|
||||
89
README.md
89
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<p align="center">
|
||||
<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 align="center">
|
||||
@@ -22,7 +22,7 @@
|
||||
- **SFTP Transfer** — upload/download files with progress bar
|
||||
- **SSH Keys** — generate ed25519, install on server, copy to clipboard
|
||||
- **Status Monitor** — background check every 60 sec (online/offline badges)
|
||||
- **Claude Code 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
|
||||
- **Encryption** — servers.json encrypted with Fernet (passwords never stored in plaintext)
|
||||
- **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
|
||||
4. **Files** — select server → Files tab → set paths → Upload/Download
|
||||
5. **Keys** — Keys tab → Generate Key → Install on Server
|
||||
6. **Setup** — Setup tab → "Install Everything" → Claude Code ready
|
||||
6. **Setup** — Setup tab → "Install Everything" → Claude Code, Codex, and Gemini ready
|
||||
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:**
|
||||
```
|
||||
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (Claude Code)
|
||||
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py backend
|
||||
↕ ↕
|
||||
Add/edit/delete /ssh skill
|
||||
servers in GUI executes commands
|
||||
Add/edit/delete Claude /ssh + Codex + Gemini skill
|
||||
servers in GUI execute commands
|
||||
```
|
||||
|
||||
- Add a server in GUI → Claude Code sees it immediately via `/ssh list`
|
||||
- Both use the same `ssh.py` + `servers.json`
|
||||
- Add a server in GUI → Claude Code, Codex, and Gemini see it immediately
|
||||
- Both agents use the same `ssh.py` + `servers.json`
|
||||
- Passwords **never** pass through the AI API
|
||||
|
||||
**New SSH Commands:**
|
||||
@@ -93,12 +95,17 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (C
|
||||
**Setup on a new machine:**
|
||||
1. Install ServerManager (clone repo or download binary)
|
||||
2. Open Setup tab → click "Install Everything"
|
||||
3. Done. Claude Code now has `/ssh` 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:
|
||||
- `ssh.py` → `~/.server-connections/` (SSH utility)
|
||||
- `encryption.py` → `~/.server-connections/` (encryption module for CLI)
|
||||
- `/ssh` skill → `~/.claude/commands/ssh.md` (Claude Code skill)
|
||||
- `server-manager` skill → `~/.codex/skills/server-manager/` (Codex skill package)
|
||||
- `server-manager` skill → `~/.gemini/skills/server-manager/` (Gemini skill package)
|
||||
- optional mirror → `~/.agents/skills/server-manager/` (off by default to avoid Gemini duplicate-skill warnings)
|
||||
- `codex-ssh` wrapper → `~/.server-connections/` (safe Codex entry point)
|
||||
- `gemini-ssh` wrapper → `~/.server-connections/` (safe Gemini entry point)
|
||||
- SSH key (ed25519) — if not exists
|
||||
- Checks for duplicates — safe to run multiple times
|
||||
|
||||
@@ -140,7 +147,7 @@ App executes: sudo -S -p '' bash -c 'systemctl restart nginx'
|
||||
- Passwords stored locally only, **never sent to any AI/API**
|
||||
- SSH keys (ed25519) — recommended auth method
|
||||
- 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
|
||||
|
||||
### Project Structure
|
||||
@@ -154,7 +161,7 @@ ServerManager/
|
||||
│ ├── server_store.py # CRUD + encrypted JSON + observer + backups
|
||||
│ ├── encryption.py # Fernet encryption module
|
||||
│ ├── ssh_client.py # Paramiko SSH/SFTP wrapper
|
||||
│ ├── claude_setup.py # Claude Code integration installer
|
||||
│ ├── claude_setup.py # Claude Code + Codex + Gemini integration installer
|
||||
│ ├── status_checker.py # Background monitoring
|
||||
│ ├── totp.py # TOTP/2FA module (pyotp)
|
||||
│ ├── logger.py # Rotating file logger
|
||||
@@ -166,7 +173,7 @@ ServerManager/
|
||||
│ ├── tabs/ # Terminal, Files, Info, Keys, Setup
|
||||
│ └── widgets/ # StatusBadge
|
||||
├── 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
|
||||
├── config/ # Example configs
|
||||
├── releases/ # Built executables
|
||||
@@ -187,7 +194,7 @@ pip install -r requirements.txt
|
||||
python main.py
|
||||
# → Setup tab → Install Everything
|
||||
# → 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** — загрузка и скачивание файлов с прогресс-баром
|
||||
- **SSH-ключи** — генерация ed25519, установка на сервер, копирование
|
||||
- **Мониторинг** — фоновая проверка каждые 60 сек (бейджи online/offline)
|
||||
- **Интеграция с Claude Code** — установка в один клик, общий конфиг со скиллом `/ssh`
|
||||
- **Интеграция с Claude Code + Codex + Gemini** — установка в один клик, общий конфиг со скиллом `/ssh`, Codex skill и Gemini skill
|
||||
- **TOTP / 2FA** — коды Google Authenticator с обратным отсчётом, копирование в один клик
|
||||
- **Шифрование** — servers.json зашифрован Fernet (пароли не хранятся в открытом виде)
|
||||
- **Бэкапы** — ручные и автоматические с восстановлением в один клик
|
||||
@@ -241,23 +248,23 @@ python build.py
|
||||
3. **Терминал** — выберите сервер → вкладка Terminal → введите команду → Run
|
||||
4. **Файлы** — выберите сервер → вкладка Files → укажите пути → Upload/Download
|
||||
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)
|
||||
|
||||
### Интеграция с 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
|
||||
сервер в GUI выполняет команды
|
||||
Добавил/изменил Claude /ssh + Codex + Gemini skill
|
||||
серверы в GUI выполняют команды
|
||||
```
|
||||
|
||||
- Добавил сервер в GUI → Claude Code сразу видит его через `/ssh list`
|
||||
- Оба используют один `ssh.py` + `servers.json`
|
||||
- Добавил сервер в GUI → Claude Code, Codex и Gemini сразу видят его
|
||||
- Оба агента используют один `ssh.py` + `servers.json`
|
||||
- Пароли **никогда** не проходят через API нейронки
|
||||
|
||||
**Новые SSH команды:**
|
||||
@@ -272,12 +279,16 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (C
|
||||
**Настройка на новой машине:**
|
||||
1. Установить ServerManager (клонировать репо или скачать бинарник)
|
||||
2. Открыть вкладку Setup → нажать "Install Everything"
|
||||
3. Готово. Claude Code теперь имеет скилл `/ssh` и доступ к серверам
|
||||
3. Готово. Claude Code получает скилл `/ssh`, а Codex и Gemini получают skill `server-manager` и доступ к серверам
|
||||
|
||||
Вкладка Setup устанавливает:
|
||||
- `ssh.py` → `~/.server-connections/` (SSH-утилита)
|
||||
- `encryption.py` → `~/.server-connections/` (модуль шифрования для CLI)
|
||||
- скилл `/ssh` → `~/.claude/commands/ssh.md` (скилл Claude Code)
|
||||
- skill `server-manager` → `~/.codex/skills/server-manager/` (скилл Codex)
|
||||
- skill `server-manager` → `~/.gemini/skills/server-manager/` (скилл Gemini)
|
||||
- wrapper `codex-ssh` → `~/.server-connections/` (безопасная точка входа для Codex)
|
||||
- wrapper `gemini-ssh` → `~/.server-connections/` (безопасная точка входа для Gemini)
|
||||
- SSH-ключ (ed25519) — если ещё не создан
|
||||
- Проверяет дубли — безопасно запускать повторно
|
||||
|
||||
@@ -319,7 +330,7 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py (C
|
||||
- Пароли хранятся только локально, **никогда не передаются в AI/API**
|
||||
- SSH-ключи (ed25519) — рекомендуемый метод аутентификации
|
||||
- sudo-пароль передаётся через stdin (не виден в списке процессов)
|
||||
- При использовании с Claude Code: через API нейронки проходят только alias + команда, пароли остаются в зашифрованном локальном файле
|
||||
- При использовании с Claude Code или Codex: через API нейронки проходят только alias + команда, пароли остаются в зашифрованном локальном файле
|
||||
- Автоматический пред-шифровальный бэкап при первой миграции
|
||||
|
||||
### Развёртывание на новой машине
|
||||
@@ -336,7 +347,7 @@ pip install -r requirements.txt
|
||||
python main.py
|
||||
# → Вкладка Setup → Install Everything
|
||||
# → Добавить серверы через + Add
|
||||
# → Готово! GUI и Claude Code работают с одним конфигом
|
||||
# → Готово! GUI, Claude Code, Codex и Gemini работают с одним конфигом
|
||||
```
|
||||
|
||||
---
|
||||
@@ -350,7 +361,7 @@ python main.py
|
||||
- **SFTP传输** — 带进度条的文件上传/下载
|
||||
- **SSH密钥** — 生成ed25519、安装到服务器、复制到剪贴板
|
||||
- **状态监控** — 每60秒后台检查(在线/离线徽标)
|
||||
- **Claude Code集成** — 一键设置,与`/ssh`技能共享配置
|
||||
- **Claude Code + Codex + Gemini 集成** — 一键设置,与 `/ssh` 技能、Codex skill 和 Gemini skill 共享配置
|
||||
- **TOTP / 2FA** — 兼容Google Authenticator的验证码,实时倒计时,一键复制
|
||||
- **加密** — servers.json使用Fernet加密(密码不再以明文存储)
|
||||
- **备份** — 手动和自动备份,一键恢复
|
||||
@@ -390,34 +401,36 @@ python build.py
|
||||
3. **终端** — 选择服务器 → Terminal标签 → 输入命令 → Run
|
||||
4. **文件** — 选择服务器 → Files标签 → 设置路径 → Upload/Download
|
||||
5. **密钥** — Keys标签 → Generate Key → Install on Server
|
||||
6. **设置Claude** — Setup标签 → "Install Everything" → Claude Code就绪
|
||||
6. **设置** — Setup标签 → "Install Everything" → Claude Code 和 Codex 就绪
|
||||
7. 状态徽标自动更新(绿色 = 在线,红色 = 离线)
|
||||
|
||||
### Claude Code集成
|
||||
### Claude Code + Codex + Gemini 集成
|
||||
|
||||
ServerManager和Claude Code共享**同一个配置文件**:`~/.server-connections/servers.json`
|
||||
ServerManager、Claude 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` 看到
|
||||
- 两者使用相同的 `ssh.py` + `servers.json`
|
||||
- 在GUI中添加服务器 → Claude Code 和 Codex 都会立即看到
|
||||
- Claude Code、Codex 和 Gemini 都使用同一个 `ssh.py` + `servers.json`
|
||||
- 密码**绝不**通过AI API传递
|
||||
|
||||
**在新机器上设置:**
|
||||
1. 安装ServerManager(克隆仓库或下载二进制文件)
|
||||
2. 打开Setup标签 → 点击 "Install Everything"
|
||||
3. 完成。Claude Code现在拥有 `/ssh` 技能并可访问您的服务器
|
||||
3. 完成。Claude Code 现在拥有 `/ssh` 技能,Codex 现在拥有 `server-manager` 技能并可访问您的服务器
|
||||
|
||||
Setup标签安装:
|
||||
- `ssh.py` → `~/.server-connections/`(SSH工具)
|
||||
- `encryption.py` → `~/.server-connections/`(CLI加密模块)
|
||||
- `/ssh` 技能 → `~/.claude/commands/ssh.md`(Claude Code技能)
|
||||
- `server-manager` 技能 → `~/.codex/skills/server-manager/`(Codex技能包)
|
||||
- `codex-ssh` 包装器 → `~/.server-connections/`(Codex安全入口)
|
||||
- SSH密钥(ed25519)— 如果不存在
|
||||
- 检查重复 — 可安全重复运行
|
||||
|
||||
@@ -459,7 +472,7 @@ Setup标签安装:
|
||||
- 密码仅存储在本地,**绝不发送到任何AI/API**
|
||||
- SSH密钥(ed25519)— 推荐的认证方式
|
||||
- sudo密码通过stdin传递(在进程列表中不可见)
|
||||
- 与Claude Code配合使用时:只有别名和命令通过AI API传递,密码保留在本地加密文件中
|
||||
- 与 Claude Code 或 Codex 配合使用时:只有别名和命令通过 AI API 传递,密码保留在本地加密文件中
|
||||
- 首次迁移时自动创建加密前备份
|
||||
|
||||
### 在新机器上部署
|
||||
@@ -476,7 +489,7 @@ pip install -r requirements.txt
|
||||
python main.py
|
||||
# → Setup标签 → Install Everything
|
||||
# → 通过 + Add 添加服务器
|
||||
# → 完成!GUI和Claude Code使用同一个配置
|
||||
# → 完成!GUI、Claude Code 和 Codex 使用同一个配置
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
49
build.py
49
build.py
@@ -114,7 +114,12 @@ def build():
|
||||
"--add-data", f"config/servers.example.json{os.pathsep}config",
|
||||
"--add-data", f"tools/ssh.py{os.pathsep}tools",
|
||||
"--add-data", f"tools/skill-ssh.md{os.pathsep}tools",
|
||||
"--add-data", f"tools/install_ai_integrations.py{os.pathsep}tools",
|
||||
"--add-data", f"core/encryption.py{os.pathsep}core",
|
||||
"--add-data", f".codex/skills/server-manager{os.pathsep}.codex/skills/server-manager",
|
||||
"--add-data", f".gemini/skills/server-manager{os.pathsep}.gemini/skills/server-manager",
|
||||
"--add-data", f".gemini/settings.json{os.pathsep}.gemini",
|
||||
"--add-data", f"GEMINI.md{os.pathsep}.",
|
||||
]
|
||||
|
||||
# PNG icons for GUI (Material Design)
|
||||
@@ -350,34 +355,34 @@ def cleanup_old_releases():
|
||||
|
||||
|
||||
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.
|
||||
Without this, editing tools/ssh.py updates the exe but NOT the live
|
||||
~/.server-connections/ssh.py that Claude Code actually calls.
|
||||
"""
|
||||
shared_dir = os.path.expanduser("~/.server-connections")
|
||||
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")),
|
||||
deploy_steps = [
|
||||
install_ssh_script,
|
||||
install_claude_skill,
|
||||
install_codex_skill,
|
||||
install_gemini_skill,
|
||||
]
|
||||
|
||||
deployed = []
|
||||
for src, dst in deploy_map:
|
||||
if not os.path.exists(src):
|
||||
continue
|
||||
os.makedirs(os.path.dirname(dst), exist_ok=True)
|
||||
shutil.copy2(src, dst)
|
||||
deployed.append(os.path.basename(dst))
|
||||
for step in deploy_steps:
|
||||
try:
|
||||
result = step()
|
||||
if result:
|
||||
deployed.append(result.replace("\n", "; "))
|
||||
except Exception as exc:
|
||||
print(f"WARNING: auto-deploy step failed ({step.__name__}): {exc}")
|
||||
|
||||
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__":
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"""
|
||||
Claude Code integration setup.
|
||||
Installs ssh.py, encryption.py, /ssh skill, SSH key — everything needed
|
||||
for Claude Code to manage servers via the shared servers.json.
|
||||
Local AI agent integration setup.
|
||||
Installs the shared ssh.py/encryption.py backend, Claude /ssh command,
|
||||
Codex/Gemini skill packages, platform-specific wrappers, and SSH key material.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import re
|
||||
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
|
||||
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")
|
||||
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")
|
||||
SKILL_DST = os.path.join(SKILL_DST_DIR, "ssh.md")
|
||||
SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519")
|
||||
GLOBAL_CLAUDE_MD = os.path.expanduser("~/.claude/CLAUDE.md")
|
||||
CODEX_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".codex", "skills", "server-manager")
|
||||
CODEX_WRAPPER_SRC_SH = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.sh")
|
||||
CODEX_WRAPPER_SRC_CMD = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.cmd")
|
||||
GEMINI_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".gemini", "skills", "server-manager")
|
||||
GEMINI_WRAPPER_SRC_SH = os.path.join(GEMINI_SKILL_SRC_DIR, "scripts", "gemini-ssh-wrapper.sh")
|
||||
GEMINI_WRAPPER_SRC_CMD = os.path.join(GEMINI_SKILL_SRC_DIR, "scripts", "gemini-ssh-wrapper.cmd")
|
||||
|
||||
_BLOCK_START = "<!-- server-manager:start -->"
|
||||
_BLOCK_END = "<!-- server-manager:end -->"
|
||||
_GEMINI_BLOCK_START = "<!-- server-manager-gemini:start -->"
|
||||
_GEMINI_BLOCK_END = "<!-- server-manager-gemini:end -->"
|
||||
|
||||
GLOBAL_CLAUDE_MD_BLOCK = f"""{_BLOCK_START}
|
||||
## Серверы — ТОЛЬКО через /ssh
|
||||
@@ -66,29 +72,224 @@ python ~/.server-connections/ssh.py --status # online/offline
|
||||
{_BLOCK_END}
|
||||
"""
|
||||
|
||||
GLOBAL_GEMINI_MD_BLOCK = f"""{_GEMINI_BLOCK_START}
|
||||
## ServerManager — use the installed skill
|
||||
|
||||
When a user asks about a server managed by ServerManager, use the installed `server-manager` skill first.
|
||||
|
||||
Preferred discovery commands:
|
||||
|
||||
```bash
|
||||
$HOME/.server-connections/gemini-ssh --list
|
||||
$HOME/.server-connections/gemini-ssh --info ALIAS
|
||||
$HOME/.server-connections/gemini-ssh --status
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Never read `~/.server-connections/servers.json`, `settings.json`, or `encryption.py` directly.
|
||||
- Never use `--list-full`.
|
||||
- Never use raw `ssh`, `scp`, `redis-cli`, `psql`, `mysql`, `mc`, or cloud CLIs unless the user explicitly asks to bypass ServerManager.
|
||||
- Choose commands strictly by the endpoint type reported by `--list`.
|
||||
- Use exactly one connection attempt per action and stop on timeout/failure.
|
||||
{_GEMINI_BLOCK_END}
|
||||
"""
|
||||
|
||||
|
||||
def _target_home() -> str:
|
||||
override = os.environ.get("SERVER_MANAGER_TARGET_HOME", "").strip()
|
||||
if override:
|
||||
return os.path.abspath(os.path.expanduser(override))
|
||||
return os.path.expanduser("~")
|
||||
|
||||
|
||||
def _shared_dir() -> str:
|
||||
return os.path.join(_target_home(), ".server-connections")
|
||||
|
||||
|
||||
def _gemini_dir() -> str:
|
||||
return os.path.join(_target_home(), ".gemini")
|
||||
|
||||
|
||||
def _claude_skill_dst_dir() -> str:
|
||||
return os.path.join(_target_home(), ".claude", "commands")
|
||||
|
||||
|
||||
def _claude_skill_dst() -> str:
|
||||
return os.path.join(_claude_skill_dst_dir(), "ssh.md")
|
||||
|
||||
|
||||
def _ssh_key_path() -> str:
|
||||
return os.path.join(_target_home(), ".ssh", "id_ed25519")
|
||||
|
||||
|
||||
def _global_claude_md() -> str:
|
||||
return os.path.join(_target_home(), ".claude", "CLAUDE.md")
|
||||
|
||||
|
||||
def _global_gemini_md() -> str:
|
||||
return os.path.join(_gemini_dir(), "GEMINI.md")
|
||||
|
||||
|
||||
def _codex_skill_dst_root() -> str:
|
||||
return os.path.join(_target_home(), ".codex", "skills")
|
||||
|
||||
|
||||
def _codex_skill_dst_dir() -> str:
|
||||
return os.path.join(_codex_skill_dst_root(), "server-manager")
|
||||
|
||||
|
||||
def _codex_skill_entry() -> str:
|
||||
return os.path.join(_codex_skill_dst_dir(), "SKILL.md")
|
||||
|
||||
|
||||
def _codex_wrapper_dst() -> str:
|
||||
return os.path.join(
|
||||
_shared_dir(),
|
||||
"codex-ssh.cmd" if sys.platform == "win32" else "codex-ssh",
|
||||
)
|
||||
|
||||
|
||||
def _gemini_skill_dst_root() -> str:
|
||||
return os.path.join(_gemini_dir(), "skills")
|
||||
|
||||
|
||||
def _gemini_skill_dst_dir() -> str:
|
||||
return os.path.join(_gemini_skill_dst_root(), "server-manager")
|
||||
|
||||
|
||||
def _gemini_skill_entry() -> str:
|
||||
return os.path.join(_gemini_skill_dst_dir(), "SKILL.md")
|
||||
|
||||
|
||||
def _agents_skill_dst_root() -> str:
|
||||
return os.path.join(_target_home(), ".agents", "skills")
|
||||
|
||||
|
||||
def _agents_skill_dst_dir() -> str:
|
||||
return os.path.join(_agents_skill_dst_root(), "server-manager")
|
||||
|
||||
|
||||
def _gemini_wrapper_dst() -> str:
|
||||
return os.path.join(
|
||||
_shared_dir(),
|
||||
"gemini-ssh.cmd" if sys.platform == "win32" else "gemini-ssh",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_executable(path: str):
|
||||
if sys.platform == "win32" or not os.path.exists(path):
|
||||
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:
|
||||
"""Check what's installed and what's missing."""
|
||||
shared_dir = _shared_dir()
|
||||
ssh_key_path = _ssh_key_path()
|
||||
return {
|
||||
"shared_dir": os.path.exists(SHARED_DIR),
|
||||
"servers_json": os.path.exists(os.path.join(SHARED_DIR, "servers.json")),
|
||||
"ssh_script": os.path.exists(os.path.join(SHARED_DIR, "ssh.py")),
|
||||
"encryption": os.path.exists(os.path.join(SHARED_DIR, "encryption.py")),
|
||||
"skill_installed": os.path.exists(SKILL_DST),
|
||||
"ssh_key_exists": os.path.exists(SSH_KEY_PATH),
|
||||
"ssh_key_pub": os.path.exists(SSH_KEY_PATH + ".pub"),
|
||||
"target_home": _target_home(),
|
||||
"shared_dir": os.path.exists(shared_dir),
|
||||
"servers_json": os.path.exists(os.path.join(shared_dir, "servers.json")),
|
||||
"ssh_script": os.path.exists(os.path.join(shared_dir, "ssh.py")),
|
||||
"encryption": os.path.exists(os.path.join(shared_dir, "encryption.py")),
|
||||
"claude_skill_installed": os.path.exists(_claude_skill_dst()),
|
||||
"codex_skill_installed": os.path.exists(_codex_skill_entry()),
|
||||
"codex_wrapper_installed": os.path.exists(_codex_wrapper_dst()),
|
||||
"gemini_skill_installed": os.path.exists(_gemini_skill_entry()),
|
||||
"gemini_wrapper_installed": os.path.exists(_gemini_wrapper_dst()),
|
||||
"ssh_key_exists": os.path.exists(ssh_key_path),
|
||||
"ssh_key_pub": os.path.exists(ssh_key_path + ".pub"),
|
||||
}
|
||||
|
||||
|
||||
def install_ssh_script() -> str:
|
||||
"""Copy ssh.py and encryption.py to shared dir."""
|
||||
os.makedirs(SHARED_DIR, exist_ok=True)
|
||||
shared_dir = _shared_dir()
|
||||
os.makedirs(shared_dir, exist_ok=True)
|
||||
results = []
|
||||
|
||||
# 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):
|
||||
shutil.copy2(SSH_SCRIPT_SRC, dst)
|
||||
_copy_file(SSH_SCRIPT_SRC, dst, executable=True)
|
||||
log.info(f"ssh.py installed: {dst}")
|
||||
results.append(f"ssh.py installed: {dst}")
|
||||
elif os.path.exists(dst):
|
||||
@@ -96,10 +297,9 @@ def install_ssh_script() -> str:
|
||||
else:
|
||||
results.append("ERROR: ssh.py source not found")
|
||||
|
||||
# Copy encryption.py
|
||||
enc_dst = os.path.join(SHARED_DIR, "encryption.py")
|
||||
enc_dst = os.path.join(shared_dir, "encryption.py")
|
||||
if os.path.exists(ENCRYPTION_SRC):
|
||||
shutil.copy2(ENCRYPTION_SRC, enc_dst)
|
||||
_copy_file(ENCRYPTION_SRC, enc_dst)
|
||||
log.info(f"encryption.py installed: {enc_dst}")
|
||||
results.append(f"encryption.py installed: {enc_dst}")
|
||||
elif os.path.exists(enc_dst):
|
||||
@@ -110,40 +310,125 @@ def install_ssh_script() -> str:
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
def install_skill() -> str:
|
||||
def install_claude_skill() -> str:
|
||||
"""Install /ssh skill for Claude Code."""
|
||||
os.makedirs(SKILL_DST_DIR, exist_ok=True)
|
||||
if os.path.exists(SKILL_SRC):
|
||||
shutil.copy2(SKILL_SRC, SKILL_DST)
|
||||
log.info(f"Skill installed: {SKILL_DST}")
|
||||
return f"Skill installed: {SKILL_DST}"
|
||||
# Fallback: check existing
|
||||
if os.path.exists(SKILL_DST):
|
||||
return f"Skill already exists: {SKILL_DST}"
|
||||
# Generate minimal skill
|
||||
claude_skill_dst_dir = _claude_skill_dst_dir()
|
||||
claude_skill_dst = _claude_skill_dst()
|
||||
os.makedirs(claude_skill_dst_dir, exist_ok=True)
|
||||
if os.path.exists(CLAUDE_SKILL_SRC):
|
||||
_copy_file(CLAUDE_SKILL_SRC, claude_skill_dst)
|
||||
log.info(f"Claude skill installed: {claude_skill_dst}")
|
||||
return f"Claude skill installed: {claude_skill_dst}"
|
||||
if os.path.exists(claude_skill_dst):
|
||||
return f"Claude skill already exists: {claude_skill_dst}"
|
||||
|
||||
skill_content = _generate_skill_content()
|
||||
with open(SKILL_DST, "w", encoding="utf-8") as f:
|
||||
with open(claude_skill_dst, "w", encoding="utf-8") as f:
|
||||
f.write(skill_content)
|
||||
log.info(f"Skill generated: {SKILL_DST}")
|
||||
return f"Skill generated: {SKILL_DST}"
|
||||
log.info(f"Claude skill generated: {claude_skill_dst}")
|
||||
return f"Claude skill generated: {claude_skill_dst}"
|
||||
|
||||
|
||||
def install_codex_skill() -> str:
|
||||
"""Install ServerManager skill package for Codex and the local wrapper."""
|
||||
results = []
|
||||
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:
|
||||
"""Generate ed25519 SSH key if not exists."""
|
||||
if os.path.exists(SSH_KEY_PATH):
|
||||
return f"Key already exists: {SSH_KEY_PATH}"
|
||||
ssh_key_path = _ssh_key_path()
|
||||
if os.path.exists(ssh_key_path):
|
||||
return f"Key already exists: {ssh_key_path}"
|
||||
|
||||
os.makedirs(os.path.dirname(SSH_KEY_PATH), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(ssh_key_path), exist_ok=True)
|
||||
|
||||
import subprocess
|
||||
try:
|
||||
subprocess.run(
|
||||
["ssh-keygen", "-t", "ed25519", "-f", SSH_KEY_PATH,
|
||||
["ssh-keygen", "-t", "ed25519", "-f", ssh_key_path,
|
||||
"-N", "", "-C", "server-manager"],
|
||||
check=True, capture_output=True, timeout=15
|
||||
)
|
||||
log.info(f"SSH key generated: {SSH_KEY_PATH}")
|
||||
return f"Key generated: {SSH_KEY_PATH}"
|
||||
log.info(f"SSH key generated: {ssh_key_path}")
|
||||
return f"Key generated: {ssh_key_path}"
|
||||
except FileNotFoundError:
|
||||
hint = "enable OpenSSH optional feature" if sys.platform == "win32" else "install openssh-client"
|
||||
msg = f"ERROR: ssh-keygen not found — {hint}"
|
||||
@@ -156,16 +441,13 @@ def generate_ssh_key() -> str:
|
||||
|
||||
|
||||
def install_global_claude_md() -> str:
|
||||
"""Add/update server manager section in global ~/.claude/CLAUDE.md.
|
||||
|
||||
Uses start/end markers to safely replace existing block without duplication.
|
||||
"""
|
||||
import re
|
||||
os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True)
|
||||
"""Add/update server manager section in global ~/.claude/CLAUDE.md."""
|
||||
global_claude_md = _global_claude_md()
|
||||
os.makedirs(os.path.dirname(global_claude_md), exist_ok=True)
|
||||
|
||||
existing = ""
|
||||
if os.path.exists(GLOBAL_CLAUDE_MD):
|
||||
with open(GLOBAL_CLAUDE_MD, encoding="utf-8") as f:
|
||||
if os.path.exists(global_claude_md):
|
||||
with open(global_claude_md, encoding="utf-8") as f:
|
||||
existing = f.read()
|
||||
|
||||
pattern = re.compile(
|
||||
@@ -174,43 +456,97 @@ def install_global_claude_md() -> str:
|
||||
)
|
||||
|
||||
if pattern.search(existing):
|
||||
# Блок уже есть — заменяем на актуальную версию
|
||||
updated = pattern.sub(GLOBAL_CLAUDE_MD_BLOCK.strip(), existing)
|
||||
with open(GLOBAL_CLAUDE_MD, "w", encoding="utf-8") as f:
|
||||
with open(global_claude_md, "w", encoding="utf-8") as f:
|
||||
f.write(updated)
|
||||
log.info(f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}")
|
||||
return f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}"
|
||||
else:
|
||||
# Блока нет — добавляем в конец
|
||||
with open(GLOBAL_CLAUDE_MD, "a", encoding="utf-8") as f:
|
||||
if existing and not existing.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK)
|
||||
log.info(f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}")
|
||||
return f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}"
|
||||
log.info(f"Global CLAUDE.md block updated: {global_claude_md}")
|
||||
return f"Global CLAUDE.md block updated: {global_claude_md}"
|
||||
|
||||
with open(global_claude_md, "a", encoding="utf-8") as f:
|
||||
if existing and not existing.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK)
|
||||
log.info(f"Global CLAUDE.md block added: {global_claude_md}")
|
||||
return f"Global CLAUDE.md block added: {global_claude_md}"
|
||||
|
||||
|
||||
def install_global_gemini_md() -> str:
|
||||
"""Add/update server manager section in global ~/.gemini/GEMINI.md."""
|
||||
global_gemini_md = _global_gemini_md()
|
||||
os.makedirs(os.path.dirname(global_gemini_md), exist_ok=True)
|
||||
|
||||
existing = ""
|
||||
if os.path.exists(global_gemini_md):
|
||||
with open(global_gemini_md, encoding="utf-8") as f:
|
||||
existing = f.read()
|
||||
|
||||
pattern = re.compile(
|
||||
re.escape(_GEMINI_BLOCK_START) + r".*?" + re.escape(_GEMINI_BLOCK_END),
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
if pattern.search(existing):
|
||||
updated = pattern.sub(GLOBAL_GEMINI_MD_BLOCK.strip(), existing)
|
||||
with open(global_gemini_md, "w", encoding="utf-8") as f:
|
||||
f.write(updated)
|
||||
log.info(f"Global GEMINI.md block updated: {global_gemini_md}")
|
||||
return f"Global GEMINI.md block updated: {global_gemini_md}"
|
||||
|
||||
with open(global_gemini_md, "a", encoding="utf-8") as f:
|
||||
if existing and not existing.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n" + GLOBAL_GEMINI_MD_BLOCK)
|
||||
log.info(f"Global GEMINI.md block added: {global_gemini_md}")
|
||||
return f"Global GEMINI.md block added: {global_gemini_md}"
|
||||
|
||||
|
||||
def install_all() -> list[str]:
|
||||
"""Full setup — install everything."""
|
||||
results = []
|
||||
|
||||
steps = [
|
||||
"""Full setup — install everything for Claude Code, Codex, and Gemini."""
|
||||
all_users = os.environ.get("SERVER_MANAGER_INSTALL_ALL_USERS", "").strip() == "1"
|
||||
base_steps = [
|
||||
("ssh_script", install_ssh_script),
|
||||
("skill", install_skill),
|
||||
("ssh_key", generate_ssh_key),
|
||||
("claude_skill", install_claude_skill),
|
||||
("codex_skill", install_codex_skill),
|
||||
("gemini_skill", install_gemini_skill),
|
||||
("global_claude_md", install_global_claude_md),
|
||||
("global_gemini_md", install_global_gemini_md),
|
||||
]
|
||||
|
||||
for name, func in steps:
|
||||
try:
|
||||
log.info(f"install_all: running {name}")
|
||||
result = func()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
msg = f"ERROR ({name}): {e}"
|
||||
log.error(msg)
|
||||
results.append(msg)
|
||||
if not all_users:
|
||||
steps = base_steps[:3] + [("ssh_key", generate_ssh_key)] + base_steps[3:]
|
||||
results = []
|
||||
for name, func in steps:
|
||||
try:
|
||||
log.info(f"install_all: running {name}")
|
||||
result = func()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
msg = f"ERROR ({name}): {e}"
|
||||
log.error(msg)
|
||||
results.append(msg)
|
||||
return results
|
||||
|
||||
results = []
|
||||
original_target = os.environ.get("SERVER_MANAGER_TARGET_HOME")
|
||||
for home in _iter_all_user_homes():
|
||||
os.environ["SERVER_MANAGER_TARGET_HOME"] = home
|
||||
results.append(f"[target_home] {home}")
|
||||
for name, func in base_steps:
|
||||
try:
|
||||
log.info(f"install_all(all_users): running {name} for {home}")
|
||||
result = func()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
msg = f"ERROR ({name}, {home}): {e}"
|
||||
log.error(msg)
|
||||
results.append(msg)
|
||||
|
||||
if original_target is None:
|
||||
os.environ.pop("SERVER_MANAGER_TARGET_HOME", None)
|
||||
else:
|
||||
os.environ["SERVER_MANAGER_TARGET_HOME"] = original_target
|
||||
|
||||
results.append("INFO: SSH key generation skipped in SERVER_MANAGER_INSTALL_ALL_USERS=1 mode")
|
||||
return results
|
||||
|
||||
|
||||
|
||||
66
core/i18n.py
66
core/i18n.py
@@ -46,7 +46,7 @@ _EN = {
|
||||
"about_desc": (
|
||||
"Desktop application for managing remote servers.\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": (
|
||||
@@ -56,13 +56,13 @@ _EN = {
|
||||
"• TOTP / 2FA (Google Authenticator)\n"
|
||||
"• Encrypted credentials (Fernet)\n"
|
||||
"• Automatic backups\n"
|
||||
"• Claude Code integration"
|
||||
"• Claude Code and Codex integration"
|
||||
),
|
||||
"about_howto_title": "🚀 Quick Start",
|
||||
"about_howto": (
|
||||
"1. Click \"+ Add\" to add a server\n"
|
||||
"2. Select server → Terminal / Files\n"
|
||||
"3. Setup tab → Claude Code integration"
|
||||
"3. Setup tab → Claude Code / Codex integration"
|
||||
),
|
||||
"version": "Version",
|
||||
"author": "Author",
|
||||
@@ -157,6 +157,12 @@ _EN = {
|
||||
"no_public_key": "[!] No public key to copy",
|
||||
|
||||
# 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_desc": (
|
||||
"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_encryption": "Encryption module",
|
||||
"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)",
|
||||
"install_everything": "Install Everything",
|
||||
"installing_all": "Installing...",
|
||||
"install_ssh_py": "ssh.py",
|
||||
"install_skill": "/ssh skill",
|
||||
"install_claude_skill": "Claude skill",
|
||||
"install_codex_skill": "Codex skill",
|
||||
"install_gemini_skill": "Gemini skill",
|
||||
"install_ssh_key": "SSH key",
|
||||
"refresh": "Refresh",
|
||||
"configuration": "Configuration",
|
||||
@@ -183,7 +197,7 @@ _EN = {
|
||||
"select_backup": "Select backup...",
|
||||
"no_backups": "No backups",
|
||||
"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}",
|
||||
"backup_created": "Backup created: {name}",
|
||||
"backup_failed": "Backup failed: {e}",
|
||||
@@ -603,7 +617,7 @@ _RU = {
|
||||
"about_desc": (
|
||||
"Настольное приложение для управления удалёнными серверами.\n"
|
||||
"SSH-терминал, SFTP-передача файлов, управление ключами,\n"
|
||||
"шифрование паролей и интеграция с Claude Code."
|
||||
"шифрование паролей и интеграция с Claude Code / Codex."
|
||||
),
|
||||
"about_features_title": "⚡ Возможности",
|
||||
"about_features": (
|
||||
@@ -613,13 +627,13 @@ _RU = {
|
||||
"• TOTP / 2FA (Google Authenticator)\n"
|
||||
"• Шифрование паролей (Fernet)\n"
|
||||
"• Автоматические бэкапы\n"
|
||||
"• Интеграция с Claude Code"
|
||||
"• Интеграция с Claude Code и Codex"
|
||||
),
|
||||
"about_howto_title": "🚀 Быстрый старт",
|
||||
"about_howto": (
|
||||
"1. Нажмите \"+ Добавить\" для добавления сервера\n"
|
||||
"2. Выберите сервер → Терминал / Файлы\n"
|
||||
"3. Вкладка Настройка → интеграция Claude Code"
|
||||
"3. Вкладка Настройка → интеграция Claude Code / Codex"
|
||||
),
|
||||
"version": "Версия",
|
||||
"author": "Автор",
|
||||
@@ -714,23 +728,37 @@ _RU = {
|
||||
"no_public_key": "[!] Нет публичного ключа",
|
||||
|
||||
# Setup
|
||||
"agent_integration": "Интеграция AI-агентов",
|
||||
"claude_integration": "Интеграция с Claude Code",
|
||||
"claude_desc": (
|
||||
"Настройте всё, чтобы Claude Code мог управлять серверами через скилл /ssh.\n"
|
||||
"GUI и Claude Code используют один servers.json — добавьте сервер здесь,\n"
|
||||
"Claude увидит его сразу."
|
||||
),
|
||||
"agent_desc": (
|
||||
"Настройте всё, чтобы Claude Code, Codex и Gemini могли управлять серверами через локальные skills.\n"
|
||||
"ServerManager, Claude Code, Codex и Gemini используют один и тот же servers.json — добавьте сервер здесь,\n"
|
||||
"все агенты увидят его сразу."
|
||||
),
|
||||
"status": "Статус",
|
||||
"status_shared_dir": "Общий каталог (~/.server-connections)",
|
||||
"status_servers_json": "servers.json",
|
||||
"status_ssh_script": "ssh.py (CLI-утилита)",
|
||||
"status_encryption": "Модуль шифрования",
|
||||
"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)",
|
||||
"install_everything": "Установить всё",
|
||||
"installing_all": "Установка...",
|
||||
"install_ssh_py": "ssh.py",
|
||||
"install_skill": "Скилл /ssh",
|
||||
"install_claude_skill": "Скилл Claude",
|
||||
"install_codex_skill": "Скилл Codex",
|
||||
"install_gemini_skill": "Скилл Gemini",
|
||||
"install_ssh_key": "SSH-ключ",
|
||||
"refresh": "Обновить",
|
||||
"configuration": "Конфигурация",
|
||||
@@ -740,7 +768,7 @@ _RU = {
|
||||
"select_backup": "Выберите бэкап...",
|
||||
"no_backups": "Нет бэкапов",
|
||||
"restore": "Восстановить",
|
||||
"install_done": "Готово! Claude Code теперь может использовать /ssh для управления серверами.",
|
||||
"install_done": "Готово! Claude Code, Codex и Gemini теперь могут использовать ServerManager для управления серверами.",
|
||||
"config_changed": "Путь конфига изменён: {path}",
|
||||
"backup_created": "Бэкап создан: {name}",
|
||||
"backup_failed": "Ошибка бэкапа: {e}",
|
||||
@@ -1160,7 +1188,7 @@ _ZH = {
|
||||
"about_desc": (
|
||||
"用于管理远程服务器的桌面应用程序。\n"
|
||||
"SSH终端、SFTP文件传输、密钥管理、\n"
|
||||
"凭据加密以及Claude Code集成。"
|
||||
"凭据加密以及Claude Code / Codex集成。"
|
||||
),
|
||||
"about_features_title": "⚡ 功能特点",
|
||||
"about_features": (
|
||||
@@ -1170,13 +1198,13 @@ _ZH = {
|
||||
"• TOTP / 2FA(Google Authenticator)\n"
|
||||
"• 凭据加密(Fernet)\n"
|
||||
"• 自动备份\n"
|
||||
"• Claude Code集成"
|
||||
"• Claude Code 和 Codex 集成"
|
||||
),
|
||||
"about_howto_title": "🚀 快速开始",
|
||||
"about_howto": (
|
||||
"1. 点击\"+ 添加\"来添加服务器\n"
|
||||
"2. 选择服务器 → 终端 / 文件\n"
|
||||
"3. 设置标签 → Claude Code集成"
|
||||
"3. 设置标签 → Claude Code / Codex 集成"
|
||||
),
|
||||
"version": "版本",
|
||||
"author": "作者",
|
||||
@@ -1271,6 +1299,7 @@ _ZH = {
|
||||
"no_public_key": "[!] 没有公钥可复制",
|
||||
|
||||
# Setup
|
||||
"agent_integration": "AI代理集成",
|
||||
"claude_integration": "Claude Code集成",
|
||||
"claude_desc": (
|
||||
"设置一切以便Claude Code通过/ssh技能管理您的服务器。\n"
|
||||
@@ -1282,12 +1311,25 @@ _ZH = {
|
||||
"status_servers_json": "servers.json",
|
||||
"status_ssh_script": "ssh.py(CLI工具)",
|
||||
"status_encryption": "加密模块",
|
||||
"agent_desc": (
|
||||
"完成设置后,Claude Code、Codex 和 Gemini 都可以通过共享的本地 skills 管理您的服务器。\n"
|
||||
"ServerManager、Claude Code、Codex 和 Gemini 共用同一个 servers.json — 在此添加服务器后,\n"
|
||||
"所有代理都会立即看到。"
|
||||
),
|
||||
"status_skill": "Claude Code的/ssh技能",
|
||||
"status_claude_skill": "Claude Code 的 /ssh 技能",
|
||||
"status_codex_skill": "Codex 的 ServerManager 技能",
|
||||
"status_codex_wrapper": "Codex 包装器(codex-ssh)",
|
||||
"status_gemini_skill": "Gemini 的 ServerManager 技能",
|
||||
"status_gemini_wrapper": "Gemini 包装器(gemini-ssh)",
|
||||
"status_ssh_key": "SSH密钥(ed25519)",
|
||||
"install_everything": "全部安装",
|
||||
"installing_all": "安装中...",
|
||||
"install_ssh_py": "ssh.py",
|
||||
"install_skill": "/ssh技能",
|
||||
"install_claude_skill": "Claude 技能",
|
||||
"install_codex_skill": "Codex 技能",
|
||||
"install_gemini_skill": "Gemini 技能",
|
||||
"install_ssh_key": "SSH密钥",
|
||||
"refresh": "刷新",
|
||||
"configuration": "配置",
|
||||
@@ -1297,7 +1339,7 @@ _ZH = {
|
||||
"select_backup": "选择备份...",
|
||||
"no_backups": "无备份",
|
||||
"restore": "恢复",
|
||||
"install_done": "完成!Claude Code现在可以使用/ssh来管理您的服务器。",
|
||||
"install_done": "完成!Claude Code、Codex 和 Gemini 现在都可以使用 ServerManager 来管理您的服务器。",
|
||||
"config_changed": "配置路径已更改:{path}",
|
||||
"backup_created": "备份已创建:{name}",
|
||||
"backup_failed": "备份失败:{e}",
|
||||
|
||||
@@ -46,7 +46,7 @@ class PrometheusTab(ctk.CTkFrame):
|
||||
quick_frame.pack(fill="x", padx=15, pady=(0, 5))
|
||||
|
||||
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,
|
||||
fg_color="#6b7280", hover_color="#4b5563",
|
||||
command=lambda q=query: self._run_quick(q))
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -8,7 +8,15 @@ import threading
|
||||
from datetime import datetime
|
||||
from tkinter import filedialog, messagebox
|
||||
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.icons import icon_text, make_icon_button
|
||||
from core.logger import log
|
||||
@@ -25,13 +33,13 @@ class SetupTab(ctk.CTkFrame):
|
||||
|
||||
# Header
|
||||
self.header_label = ctk.CTkLabel(
|
||||
self._scroll, text=t("claude_integration"),
|
||||
self._scroll, text=t("agent_integration"),
|
||||
font=ctk.CTkFont(size=20, weight="bold")
|
||||
)
|
||||
self.header_label.pack(padx=20, pady=(20, 5))
|
||||
|
||||
self.desc_label = ctk.CTkLabel(
|
||||
self._scroll, text=t("claude_desc"),
|
||||
self._scroll, text=t("agent_desc"),
|
||||
text_color="#9ca3af", justify="center"
|
||||
)
|
||||
self.desc_label.pack(padx=20, pady=(0, 15))
|
||||
@@ -53,7 +61,11 @@ class SetupTab(ctk.CTkFrame):
|
||||
("servers_json", "status_servers_json"),
|
||||
("ssh_script", "status_ssh_script"),
|
||||
("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"),
|
||||
]
|
||||
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.pack(fill="x")
|
||||
|
||||
self.ssh_py_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_py"), width=110, fg_color="#6b7280",
|
||||
command=self._install_script)
|
||||
top_btn_row = ctk.CTkFrame(ind_frame, fg_color="transparent")
|
||||
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.skill_btn = make_icon_button(ind_frame, "confirm", t("install_skill"), width=110, fg_color="#6b7280",
|
||||
command=self._install_skill)
|
||||
self.skill_btn.pack(side="left", padx=5)
|
||||
self.ssh_key_btn = make_icon_button(ind_frame, "confirm", t("install_ssh_key"), width=110, fg_color="#6b7280",
|
||||
command=self._gen_key)
|
||||
self.ssh_key_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.claude_skill_btn = make_icon_button(
|
||||
top_btn_row, "confirm", t("install_claude_skill"), width=130, fg_color="#6b7280",
|
||||
command=self._install_claude_skill
|
||||
)
|
||||
self.claude_skill_btn.pack(side="left", padx=5)
|
||||
|
||||
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")
|
||||
|
||||
# ── Monitoring section ─────────────────────────
|
||||
@@ -328,8 +369,23 @@ class SetupTab(ctk.CTkFrame):
|
||||
self._log(msg)
|
||||
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):
|
||||
msg = install_skill()
|
||||
msg = install_claude_skill()
|
||||
self._log(msg)
|
||||
self._refresh_status()
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
93
test_ai_setup.py
Normal file
93
test_ai_setup.py
Normal 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()
|
||||
435
tools/install.sh
435
tools/install.sh
@@ -1,30 +1,40 @@
|
||||
#!/usr/bin/env bash
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
# ServerManager CLI Installer for Linux (headless / no-GUI)
|
||||
# ServerManager AI Integration Installer for Linux/macOS (headless / no-GUI)
|
||||
#
|
||||
# Устанавливает:
|
||||
# - ssh.py + encryption.py → ~/.server-connections/
|
||||
# - servers.json + settings.json → ~/.server-connections/ (если есть)
|
||||
# - CLAUDE.md → ~/.claude/
|
||||
# - ssh.md (скилл) → ~/.claude/commands/
|
||||
# - Python-зависимости для CLI (paramiko, cryptography, etc.)
|
||||
# Installs for each target home:
|
||||
# - ssh.py + encryption.py -> ~/.server-connections/
|
||||
# - Claude /ssh skill -> ~/.claude/commands/
|
||||
# - Codex server-manager skill -> ~/.codex/skills/server-manager/
|
||||
# - Gemini server-manager skill -> ~/.gemini/skills/server-manager/
|
||||
# - codex-ssh / gemini-ssh wrappers -> ~/.server-connections/
|
||||
# - CLAUDE.md / GEMINI.md (if available) -> ~/.claude/ / ~/.gemini/
|
||||
#
|
||||
# Запуск:
|
||||
# curl -sSL https://git.sensey24.ru/aibot777/server-manager/raw/branch/master/tools/install.sh | bash
|
||||
# или:
|
||||
# Optional per-target local config copy:
|
||||
# - servers.json + settings.json -> ~/.server-connections/
|
||||
#
|
||||
# Notes:
|
||||
# - servers.json is NEVER downloaded remotely.
|
||||
# - --all-users installs code/skills/wrappers for discovered homes, but skips
|
||||
# copying servers.json to avoid replicating credentials between users.
|
||||
# - Gemini also supports ~/.agents/skills, but this installer avoids placing
|
||||
# the same skill in both ~/.gemini/skills and ~/.agents/skills by default
|
||||
# because Gemini reports that as a duplicate-skill conflict.
|
||||
#
|
||||
# Usage:
|
||||
# bash install.sh
|
||||
# или с указанием источника файлов:
|
||||
# bash install.sh /path/to/server-manager/
|
||||
# bash install.sh /path/to/server-manager
|
||||
# bash install.sh --source-dir /path/to/server-manager --target-home /root
|
||||
# bash install.sh --all-users --source-dir /path/to/server-manager
|
||||
# ─────────────────────────────────────────────────────────────────────
|
||||
set -euo pipefail
|
||||
|
||||
# ── Colors ──
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[0;34m'
|
||||
CYAN='\033[0;36m'
|
||||
NC='\033[0m' # No Color
|
||||
NC='\033[0m'
|
||||
|
||||
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
|
||||
ok() { echo -e "${GREEN}[OK]${NC} $*"; }
|
||||
@@ -32,33 +42,80 @@ warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
|
||||
error() { echo -e "${RED}[ERROR]${NC} $*"; }
|
||||
step() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; }
|
||||
|
||||
# ── Config ──
|
||||
CONN_DIR="$HOME/.server-connections"
|
||||
CLAUDE_DIR="$HOME/.claude"
|
||||
COMMANDS_DIR="$CLAUDE_DIR/commands"
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
ServerManager AI integration installer
|
||||
|
||||
Options:
|
||||
--source-dir PATH Use local repo as source of files
|
||||
--target-home PATH Install into a specific user's home
|
||||
--all-users Install into all discovered user homes on this machine
|
||||
--install-agents-mirror Also mirror Gemini skill into ~/.agents/skills
|
||||
-h, --help Show this help
|
||||
|
||||
Positional compatibility:
|
||||
install.sh /path/to/server-manager # same as --source-dir
|
||||
USAGE
|
||||
}
|
||||
|
||||
GITEA_RAW="https://git.sensey24.ru/aibot777/server-manager/raw/branch/master"
|
||||
SRC_DIR=""
|
||||
TARGET_HOME="${SERVER_MANAGER_TARGET_HOME:-${TARGET_HOME:-$HOME}}"
|
||||
INSTALL_ALL_USERS=0
|
||||
INSTALL_AGENTS_MIRROR=0
|
||||
|
||||
# Source directory (optional argument)
|
||||
SRC_DIR="${1:-}"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--source-dir)
|
||||
SRC_DIR="$2"
|
||||
shift 2
|
||||
;;
|
||||
--target-home)
|
||||
TARGET_HOME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--all-users)
|
||||
INSTALL_ALL_USERS=1
|
||||
shift
|
||||
;;
|
||||
--install-agents-mirror)
|
||||
INSTALL_AGENTS_MIRROR=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$SRC_DIR" ]]; then
|
||||
SRC_DIR="$1"
|
||||
shift
|
||||
else
|
||||
error "Неизвестный аргумент: $1"
|
||||
usage
|
||||
exit 2
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Banner ──
|
||||
echo -e "${CYAN}"
|
||||
echo "╔══════════════════════════════════════════════╗"
|
||||
echo "║ ServerManager CLI Installer for Linux ║"
|
||||
echo "║ github: git.sensey24.ru/aibot777 ║"
|
||||
echo "╚══════════════════════════════════════════════╝"
|
||||
echo "╔══════════════════════════════════════════════════════╗"
|
||||
echo "║ ServerManager AI Integration Installer (headless) ║"
|
||||
echo "║ Claude + Codex + Gemini ║"
|
||||
echo "╚══════════════════════════════════════════════════════╝"
|
||||
echo -e "${NC}"
|
||||
|
||||
# ── Step 1: Check Python ──
|
||||
step "1/5 Проверка Python"
|
||||
|
||||
PYTHON=""
|
||||
for cmd in python3 python; do
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
ver=$("$cmd" --version 2>&1 | grep -oP '\d+\.\d+')
|
||||
major=$(echo "$ver" | cut -d. -f1)
|
||||
minor=$(echo "$ver" | cut -d. -f2)
|
||||
if [ "$major" -ge 3 ] && [ "$minor" -ge 8 ]; then
|
||||
if "$cmd" - <<'PY' &>/dev/null
|
||||
import sys
|
||||
raise SystemExit(0 if sys.version_info >= (3, 8) else 1)
|
||||
PY
|
||||
then
|
||||
PYTHON="$cmd"
|
||||
ok "Python найден: $($cmd --version)"
|
||||
break
|
||||
@@ -66,14 +123,11 @@ for cmd in python3 python; do
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON" ]; then
|
||||
error "Python 3.8+ не найден!"
|
||||
echo " Установите: sudo apt install python3 python3-pip"
|
||||
echo " или: sudo yum install python3 python3-pip"
|
||||
if [[ -z "$PYTHON" ]]; then
|
||||
error "Python 3.8+ не найден"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check pip
|
||||
PIP=""
|
||||
for cmd in pip3 pip; do
|
||||
if command -v "$cmd" &>/dev/null; then
|
||||
@@ -81,22 +135,26 @@ for cmd in pip3 pip; do
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PIP" ]; then
|
||||
# Try python -m pip
|
||||
if [[ -z "$PIP" ]]; then
|
||||
if $PYTHON -m pip --version &>/dev/null; then
|
||||
PIP="$PYTHON -m pip"
|
||||
else
|
||||
error "pip не найден!"
|
||||
echo " Установите: sudo apt install python3-pip"
|
||||
error "pip не найден"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
ok "pip найден: $($PIP --version 2>&1 | head -1)"
|
||||
|
||||
# ── Step 2: Install Python dependencies ──
|
||||
step "2/5 Установка Python-зависимостей"
|
||||
resolve_home() {
|
||||
"$PYTHON" - "$1" <<'PY'
|
||||
import os, sys
|
||||
print(os.path.abspath(os.path.expanduser(sys.argv[1])))
|
||||
PY
|
||||
}
|
||||
|
||||
TARGET_HOME="$(resolve_home "$TARGET_HOME")"
|
||||
|
||||
step "2/5 Установка Python-зависимостей"
|
||||
CLI_DEPS=(
|
||||
"paramiko>=3.4.0"
|
||||
"cryptography>=41.0.0"
|
||||
@@ -105,7 +163,6 @@ CLI_DEPS=(
|
||||
"redis>=5.0.0"
|
||||
"requests>=2.31.0"
|
||||
)
|
||||
|
||||
for dep in "${CLI_DEPS[@]}"; do
|
||||
pkg=$(echo "$dep" | sed 's/[>=<].*//')
|
||||
if $PYTHON -c "import $pkg" 2>/dev/null; then
|
||||
@@ -120,38 +177,26 @@ for dep in "${CLI_DEPS[@]}"; do
|
||||
fi
|
||||
done
|
||||
|
||||
# ── Step 3: Create directories ──
|
||||
step "3/5 Создание директорий"
|
||||
|
||||
mkdir -p "$CONN_DIR" "$COMMANDS_DIR"
|
||||
chmod 700 "$CONN_DIR" 2>/dev/null || true
|
||||
ok "$CONN_DIR"
|
||||
ok "$COMMANDS_DIR"
|
||||
|
||||
# ── Step 4: Copy/Download files ──
|
||||
step "4/5 Установка файлов"
|
||||
|
||||
copy_or_download() {
|
||||
local src_relative="$1"
|
||||
local dst="$2"
|
||||
local perms="$3"
|
||||
local desc="$4"
|
||||
|
||||
# Try local source first
|
||||
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/$src_relative" ]; then
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
|
||||
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/$src_relative" ]]; then
|
||||
cp "$SRC_DIR/$src_relative" "$dst"
|
||||
chmod "$perms" "$dst"
|
||||
chmod "$perms" "$dst" 2>/dev/null || true
|
||||
ok "$desc (из $SRC_DIR)"
|
||||
return 0
|
||||
fi
|
||||
|
||||
# Try download from Gitea
|
||||
local url="$GITEA_RAW/$src_relative"
|
||||
if command -v curl &>/dev/null; then
|
||||
if curl -sSL -o "$dst" "$url" 2>/dev/null; then
|
||||
# Verify not empty and not HTML error page
|
||||
if [ -s "$dst" ] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
|
||||
chmod "$perms" "$dst"
|
||||
if curl -fsSL -o "$dst" "$url" 2>/dev/null; then
|
||||
if [[ -s "$dst" ]] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
|
||||
chmod "$perms" "$dst" 2>/dev/null || true
|
||||
ok "$desc (скачан с Gitea)"
|
||||
return 0
|
||||
fi
|
||||
@@ -159,8 +204,8 @@ copy_or_download() {
|
||||
fi
|
||||
elif command -v wget &>/dev/null; then
|
||||
if wget -q -O "$dst" "$url" 2>/dev/null; then
|
||||
if [ -s "$dst" ] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
|
||||
chmod "$perms" "$dst"
|
||||
if [[ -s "$dst" ]] && ! head -1 "$dst" | grep -qi '<!doctype\|<html'; then
|
||||
chmod "$perms" "$dst" 2>/dev/null || true
|
||||
ok "$desc (скачан с Gitea)"
|
||||
return 0
|
||||
fi
|
||||
@@ -172,86 +217,180 @@ copy_or_download() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Core files (always install)
|
||||
copy_or_download "tools/ssh.py" "$CONN_DIR/ssh.py" "755" "ssh.py"
|
||||
copy_or_download "core/encryption.py" "$CONN_DIR/encryption.py" "644" "encryption.py"
|
||||
install_skill_tree() {
|
||||
local prefix="$1"
|
||||
local dst_root="$2"
|
||||
shift 2
|
||||
mkdir -p "$dst_root"
|
||||
local rel
|
||||
for rel in "$@"; do
|
||||
copy_or_download "$prefix/$rel" "$dst_root/$rel" 644 "$prefix/$rel" || true
|
||||
done
|
||||
find "$dst_root/scripts" -type f -name '*.sh' -exec chmod 755 {} + 2>/dev/null || true
|
||||
find "$dst_root/scripts" -type f -name '*.cmd' -exec chmod 644 {} + 2>/dev/null || true
|
||||
}
|
||||
|
||||
# Claude Code skill
|
||||
copy_or_download "tools/skill-ssh.md" "$COMMANDS_DIR/ssh.md" "644" "ssh.md (скилл /ssh)"
|
||||
discover_homes() {
|
||||
local homes=()
|
||||
local uname_s
|
||||
uname_s="$(uname -s 2>/dev/null || echo Linux)"
|
||||
|
||||
# CLAUDE.md
|
||||
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/CLAUDE.md" ]; then
|
||||
cp "$SRC_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
|
||||
chmod 644 "$CLAUDE_DIR/CLAUDE.md"
|
||||
ok "CLAUDE.md"
|
||||
fi
|
||||
|
||||
# servers.json — only copy if exists locally, never download (contains encrypted creds)
|
||||
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/servers.json" ]; then
|
||||
cp "$SRC_DIR/servers.json" "$CONN_DIR/servers.json"
|
||||
chmod 600 "$CONN_DIR/servers.json"
|
||||
ok "servers.json (зашифрованный)"
|
||||
elif [ ! -f "$CONN_DIR/servers.json" ]; then
|
||||
warn "servers.json не найден — скопируйте с основной машины:"
|
||||
echo " scp user@main:~/.server-connections/servers.json $CONN_DIR/"
|
||||
fi
|
||||
|
||||
# settings.json
|
||||
if [ -n "$SRC_DIR" ] && [ -f "$SRC_DIR/settings.json" ]; then
|
||||
cp "$SRC_DIR/settings.json" "$CONN_DIR/settings.json"
|
||||
chmod 600 "$CONN_DIR/settings.json"
|
||||
ok "settings.json"
|
||||
elif [ ! -f "$CONN_DIR/settings.json" ]; then
|
||||
# Create minimal settings
|
||||
echo '{"language":"en","check_interval":60}' > "$CONN_DIR/settings.json"
|
||||
chmod 600 "$CONN_DIR/settings.json"
|
||||
ok "settings.json (создан по умолчанию)"
|
||||
fi
|
||||
|
||||
# ── Step 5: Verify ──
|
||||
step "5/5 Проверка установки"
|
||||
|
||||
ALL_OK=true
|
||||
|
||||
if [ -f "$CONN_DIR/ssh.py" ] && [ -x "$CONN_DIR/ssh.py" ]; then
|
||||
ok "ssh.py — исполняемый"
|
||||
else
|
||||
error "ssh.py — не найден или не исполняемый"
|
||||
ALL_OK=false
|
||||
fi
|
||||
|
||||
if [ -f "$CONN_DIR/encryption.py" ]; then
|
||||
ok "encryption.py"
|
||||
else
|
||||
error "encryption.py — не найден"
|
||||
ALL_OK=false
|
||||
fi
|
||||
|
||||
if [ -f "$COMMANDS_DIR/ssh.md" ]; then
|
||||
ok "ssh.md скилл"
|
||||
else
|
||||
warn "ssh.md скилл — не найден"
|
||||
fi
|
||||
|
||||
if [ -f "$CONN_DIR/servers.json" ]; then
|
||||
ok "servers.json"
|
||||
else
|
||||
warn "servers.json — отсутствует (нужно скопировать вручную)"
|
||||
fi
|
||||
|
||||
# Test ssh.py
|
||||
info "Тест ssh.py..."
|
||||
if $PYTHON "$CONN_DIR/ssh.py" --list &>/dev/null; then
|
||||
ok "ssh.py --list работает"
|
||||
else
|
||||
if [ ! -f "$CONN_DIR/servers.json" ]; then
|
||||
warn "ssh.py не может запуститься (нет servers.json)"
|
||||
if [[ "$INSTALL_ALL_USERS" -eq 1 ]]; then
|
||||
if [[ "$uname_s" == "Darwin" ]]; then
|
||||
[[ -d /var/root ]] && homes+=("/var/root")
|
||||
if [[ -d /Users ]]; then
|
||||
while IFS= read -r -d '' d; do homes+=("$d"); done < <(find /Users -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
|
||||
fi
|
||||
else
|
||||
[[ -d /root ]] && homes+=("/root")
|
||||
if [[ -d /home ]]; then
|
||||
while IFS= read -r -d '' d; do homes+=("$d"); done < <(find /home -mindepth 1 -maxdepth 1 -type d -print0 2>/dev/null)
|
||||
fi
|
||||
fi
|
||||
else
|
||||
warn "ssh.py вернул ошибку — проверьте зависимости"
|
||||
homes+=("$TARGET_HOME")
|
||||
fi
|
||||
|
||||
printf '%s\n' "${homes[@]}" | awk 'NF && !seen[$0]++'
|
||||
}
|
||||
|
||||
step "3/5 Подготовка директорий"
|
||||
TARGET_HOMES=()
|
||||
while IFS= read -r home; do
|
||||
[[ -n "$home" ]] || continue
|
||||
TARGET_HOMES+=("$home")
|
||||
ok "target home: $home"
|
||||
done < <(discover_homes)
|
||||
|
||||
if [[ "${#TARGET_HOMES[@]}" -eq 0 ]]; then
|
||||
error "Не удалось определить target home"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Summary ──
|
||||
step "4/5 Установка файлов"
|
||||
CODEX_SKILL_FILES=(
|
||||
"SKILL.md"
|
||||
"references/command-matrix.md"
|
||||
"references/project.md"
|
||||
"scripts/codex-ssh-wrapper.sh"
|
||||
"scripts/codex-ssh-wrapper.cmd"
|
||||
"scripts/server-manager-doctor.sh"
|
||||
"scripts/server-manager-doctor.cmd"
|
||||
)
|
||||
GEMINI_SKILL_FILES=(
|
||||
"SKILL.md"
|
||||
"references/command-matrix.md"
|
||||
"references/project.md"
|
||||
"scripts/gemini-ssh-wrapper.sh"
|
||||
"scripts/gemini-ssh-wrapper.cmd"
|
||||
"scripts/server-manager-gemini-doctor.sh"
|
||||
"scripts/server-manager-gemini-doctor.cmd"
|
||||
)
|
||||
|
||||
for HOME_DIR in "${TARGET_HOMES[@]}"; do
|
||||
CONN_DIR="$HOME_DIR/.server-connections"
|
||||
CLAUDE_DIR="$HOME_DIR/.claude"
|
||||
COMMANDS_DIR="$CLAUDE_DIR/commands"
|
||||
CODEX_DIR="$HOME_DIR/.codex/skills/server-manager"
|
||||
GEMINI_DIR="$HOME_DIR/.gemini"
|
||||
GEMINI_SKILL_DIR="$GEMINI_DIR/skills/server-manager"
|
||||
AGENTS_DIR="$HOME_DIR/.agents/skills/server-manager"
|
||||
|
||||
mkdir -p "$CONN_DIR" "$COMMANDS_DIR" "$CODEX_DIR" "$GEMINI_SKILL_DIR"
|
||||
chmod 700 "$CONN_DIR" 2>/dev/null || true
|
||||
|
||||
info "Устанавливаю в $HOME_DIR"
|
||||
|
||||
copy_or_download "tools/ssh.py" "$CONN_DIR/ssh.py" 755 "ssh.py"
|
||||
copy_or_download "core/encryption.py" "$CONN_DIR/encryption.py" 644 "encryption.py"
|
||||
copy_or_download "tools/skill-ssh.md" "$COMMANDS_DIR/ssh.md" 644 "ssh.md (скилл /ssh)"
|
||||
|
||||
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/CLAUDE.md" ]]; then
|
||||
cp "$SRC_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md"
|
||||
chmod 644 "$CLAUDE_DIR/CLAUDE.md"
|
||||
ok "CLAUDE.md"
|
||||
elif [[ ! -f "$CLAUDE_DIR/CLAUDE.md" ]]; then
|
||||
copy_or_download "CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" 644 "CLAUDE.md" || true
|
||||
fi
|
||||
|
||||
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/GEMINI.md" ]]; then
|
||||
cp "$SRC_DIR/GEMINI.md" "$GEMINI_DIR/GEMINI.md"
|
||||
chmod 644 "$GEMINI_DIR/GEMINI.md"
|
||||
ok "GEMINI.md"
|
||||
elif [[ ! -f "$GEMINI_DIR/GEMINI.md" ]]; then
|
||||
copy_or_download "GEMINI.md" "$GEMINI_DIR/GEMINI.md" 644 "GEMINI.md" || true
|
||||
fi
|
||||
|
||||
install_skill_tree ".codex/skills/server-manager" "$CODEX_DIR" "${CODEX_SKILL_FILES[@]}"
|
||||
install_skill_tree ".gemini/skills/server-manager" "$GEMINI_SKILL_DIR" "${GEMINI_SKILL_FILES[@]}"
|
||||
if [[ "$INSTALL_AGENTS_MIRROR" -eq 1 ]]; then
|
||||
mkdir -p "$AGENTS_DIR"
|
||||
install_skill_tree ".gemini/skills/server-manager" "$AGENTS_DIR" "${GEMINI_SKILL_FILES[@]}"
|
||||
ok "agents skill mirror"
|
||||
elif [[ -d "$AGENTS_DIR" ]]; then
|
||||
rm -rf "$AGENTS_DIR"
|
||||
ok "removed stale agents skill mirror to avoid Gemini conflict"
|
||||
fi
|
||||
|
||||
if [[ -f "$CODEX_DIR/scripts/codex-ssh-wrapper.sh" ]]; then
|
||||
cp "$CODEX_DIR/scripts/codex-ssh-wrapper.sh" "$CONN_DIR/codex-ssh"
|
||||
chmod 755 "$CONN_DIR/codex-ssh"
|
||||
ok "codex-ssh wrapper"
|
||||
else
|
||||
copy_or_download ".codex/skills/server-manager/scripts/codex-ssh-wrapper.sh" "$CONN_DIR/codex-ssh" 755 "codex-ssh wrapper" || true
|
||||
fi
|
||||
|
||||
if [[ -f "$GEMINI_SKILL_DIR/scripts/gemini-ssh-wrapper.sh" ]]; then
|
||||
cp "$GEMINI_SKILL_DIR/scripts/gemini-ssh-wrapper.sh" "$CONN_DIR/gemini-ssh"
|
||||
chmod 755 "$CONN_DIR/gemini-ssh"
|
||||
ok "gemini-ssh wrapper"
|
||||
else
|
||||
copy_or_download ".gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh" "$CONN_DIR/gemini-ssh" 755 "gemini-ssh wrapper" || true
|
||||
fi
|
||||
|
||||
if [[ "$INSTALL_ALL_USERS" -eq 0 ]]; then
|
||||
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/servers.json" ]]; then
|
||||
cp "$SRC_DIR/servers.json" "$CONN_DIR/servers.json"
|
||||
chmod 600 "$CONN_DIR/servers.json"
|
||||
ok "servers.json (зашифрованный)"
|
||||
elif [[ ! -f "$CONN_DIR/servers.json" ]]; then
|
||||
warn "servers.json не найден для $HOME_DIR — скопируйте вручную"
|
||||
fi
|
||||
|
||||
if [[ -n "$SRC_DIR" && -f "$SRC_DIR/settings.json" ]]; then
|
||||
cp "$SRC_DIR/settings.json" "$CONN_DIR/settings.json"
|
||||
chmod 600 "$CONN_DIR/settings.json"
|
||||
ok "settings.json"
|
||||
elif [[ ! -f "$CONN_DIR/settings.json" ]]; then
|
||||
echo '{"language":"en","check_interval":60}' > "$CONN_DIR/settings.json"
|
||||
chmod 600 "$CONN_DIR/settings.json"
|
||||
ok "settings.json (создан по умолчанию)"
|
||||
fi
|
||||
else
|
||||
warn "all-users mode: servers.json/settings.json не копируются автоматически для $HOME_DIR"
|
||||
fi
|
||||
done
|
||||
|
||||
step "5/5 Проверка установки"
|
||||
ALL_OK=true
|
||||
for HOME_DIR in "${TARGET_HOMES[@]}"; do
|
||||
CONN_DIR="$HOME_DIR/.server-connections"
|
||||
COMMANDS_DIR="$HOME_DIR/.claude/commands"
|
||||
CODEX_DIR="$HOME_DIR/.codex/skills/server-manager"
|
||||
GEMINI_SKILL_DIR="$HOME_DIR/.gemini/skills/server-manager"
|
||||
|
||||
info "Проверка $HOME_DIR"
|
||||
|
||||
[[ -x "$CONN_DIR/ssh.py" ]] && ok "ssh.py — исполняемый" || { error "ssh.py — не найден или не исполняемый"; ALL_OK=false; }
|
||||
[[ -f "$CONN_DIR/encryption.py" ]] && ok "encryption.py" || { error "encryption.py — не найден"; ALL_OK=false; }
|
||||
[[ -f "$COMMANDS_DIR/ssh.md" ]] && ok "Claude /ssh skill" || warn "Claude /ssh skill — не найден"
|
||||
[[ -f "$CODEX_DIR/SKILL.md" ]] && ok "Codex skill" || { warn "Codex skill — не найден"; ALL_OK=false; }
|
||||
[[ -x "$CONN_DIR/codex-ssh" ]] && ok "codex-ssh wrapper" || { warn "codex-ssh wrapper — не найден"; ALL_OK=false; }
|
||||
[[ -f "$GEMINI_SKILL_DIR/SKILL.md" ]] && ok "Gemini skill" || { warn "Gemini skill — не найден"; ALL_OK=false; }
|
||||
[[ -x "$CONN_DIR/gemini-ssh" ]] && ok "gemini-ssh wrapper" || { warn "gemini-ssh wrapper — не найден"; ALL_OK=false; }
|
||||
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo -e "${CYAN}━━━ Готово ━━━${NC}"
|
||||
echo ""
|
||||
@@ -260,17 +399,19 @@ if $ALL_OK; then
|
||||
else
|
||||
echo -e "${YELLOW}Установка завершена с предупреждениями.${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Файлы:"
|
||||
echo " $CONN_DIR/ssh.py — CLI-утилита"
|
||||
echo " $CONN_DIR/encryption.py — модуль шифрования"
|
||||
echo " $CONN_DIR/servers.json — серверы (зашифрованные)"
|
||||
echo " $COMMANDS_DIR/ssh.md — скилл /ssh для Claude Code"
|
||||
echo "Установлено для home:"
|
||||
printf ' - %s\n' "${TARGET_HOMES[@]}"
|
||||
echo ""
|
||||
echo "Использование:"
|
||||
echo " python3 ~/.server-connections/ssh.py --list"
|
||||
echo " python3 ~/.server-connections/ssh.py --info ALIAS"
|
||||
echo " python3 ~/.server-connections/ssh.py ALIAS \"command\""
|
||||
echo ""
|
||||
echo "Claude Code скилл: /ssh"
|
||||
echo " ~/.server-connections/codex-ssh --list"
|
||||
echo " ~/.server-connections/gemini-ssh --list"
|
||||
echo ""
|
||||
echo "Claude skill: ~/.claude/commands/ssh.md"
|
||||
echo "Codex skill: ~/.codex/skills/server-manager/"
|
||||
echo "Gemini skill: ~/.gemini/skills/server-manager/"
|
||||
if [[ "$INSTALL_AGENTS_MIRROR" -eq 1 ]]; then
|
||||
echo "Mirror skill: ~/.agents/skills/server-manager/"
|
||||
fi
|
||||
|
||||
48
tools/install_ai_integrations.py
Normal file
48
tools/install_ai_integrations.py
Normal 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())
|
||||
@@ -1,6 +1,6 @@
|
||||
"""Version info for ServerManager."""
|
||||
|
||||
__version__ = "1.9.42"
|
||||
__version__ = "1.9.44"
|
||||
__app_name__ = "ServerManager"
|
||||
__author__ = "aibot777"
|
||||
__description__ = "Desktop GUI for managing remote servers"
|
||||
|
||||
Reference in New Issue
Block a user