Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
136d1605c0 | ||
|
|
9da3125c34 | ||
|
|
daa11ca440 |
@@ -1,6 +1,6 @@
|
||||
---
|
||||
name: server-manager
|
||||
description: Use ServerManager's shared local server inventory and ssh.py utility to manage configured SSH, Telnet, SQL, Redis, S3/MinIO, Grafana, Prometheus, and WinRM endpoints by alias without exposing credentials. Use when the user asks to operate on servers managed by ServerManager or when editing ServerManager's Claude/Codex integration.
|
||||
description: Use ServerManager's shared local server inventory and ssh.py utility to manage configured SSH, Telnet, SQL, Redis, S3/MinIO, Grafana, Prometheus, and WinRM endpoints by alias without exposing credentials. Use when the user asks to operate on servers managed by ServerManager or when editing ServerManager's Claude/Codex/Gemini integration.
|
||||
metadata:
|
||||
short-description: Safe remote ops through ServerManager aliases
|
||||
---
|
||||
@@ -10,7 +10,7 @@ metadata:
|
||||
Use this skill for two cases:
|
||||
|
||||
1. The user wants work done on a server or service already configured in ServerManager.
|
||||
2. The user wants to modify ServerManager's CLI/integration layer so Claude/Codex can use it safely.
|
||||
2. The user wants to modify ServerManager's CLI/integration layer so Claude/Codex/Gemini can use it safely.
|
||||
|
||||
## First Step
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Project Notes
|
||||
|
||||
This skill is based on `/home/code/CODING/server-manager`.
|
||||
This skill is based on `/home/code/Desktop/CODING/server-manager`.
|
||||
|
||||
## What ServerManager Is
|
||||
|
||||
@@ -31,8 +31,8 @@ The AI never needs raw credentials. It only uses aliases and the local CLI.
|
||||
- `CLAUDE.md`: project rules, architecture, security, workflow
|
||||
- `tools/ssh.py`: CLI entry point used by AI tools
|
||||
- `tools/skill-ssh.md`: current Claude `/ssh` instructions
|
||||
- `core/claude_setup.py`: installer for shared CLI files, Claude command, and Codex skill
|
||||
- `build.py`: auto-deploys `ssh.py`, `encryption.py`, Claude skill, and Codex skill after builds
|
||||
- `core/claude_setup.py`: installer for shared CLI files plus Claude/Codex/Gemini skills
|
||||
- `build.py`: auto-deploys `ssh.py`, `encryption.py`, Claude skill, Codex skill, and Gemini skill after builds
|
||||
|
||||
## Architectural Shape
|
||||
|
||||
|
||||
8
.gemini/settings.json
Normal file
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"
|
||||
@@ -24,13 +24,13 @@
|
||||
|
||||
В репозитории исходники skill лежат здесь:
|
||||
|
||||
- [SKILL.md](/home/code/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
|
||||
- [command-matrix.md](/home/code/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
|
||||
- [project.md](/home/code/CODING/server-manager/.codex/skills/server-manager/references/project.md)
|
||||
- [server-manager-doctor.sh](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.sh)
|
||||
- [server-manager-doctor.cmd](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.cmd)
|
||||
- [codex-ssh-wrapper.sh](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.sh)
|
||||
- [codex-ssh-wrapper.cmd](/home/code/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.cmd)
|
||||
- [SKILL.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
|
||||
- [command-matrix.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
|
||||
- [project.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/references/project.md)
|
||||
- [server-manager-doctor.sh](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.sh)
|
||||
- [server-manager-doctor.cmd](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/server-manager-doctor.cmd)
|
||||
- [codex-ssh-wrapper.sh](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.sh)
|
||||
- [codex-ssh-wrapper.cmd](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/scripts/codex-ssh-wrapper.cmd)
|
||||
|
||||
## Как это работает
|
||||
|
||||
@@ -200,12 +200,12 @@ $HOME/.server-connections/codex-ssh --status
|
||||
|
||||
Если меняется поведение интеграции, проверять нужно в таком порядке:
|
||||
|
||||
1. [tools/ssh.py](/home/code/CODING/server-manager/tools/ssh.py)
|
||||
2. [tools/skill-ssh.md](/home/code/CODING/server-manager/tools/skill-ssh.md)
|
||||
3. [core/claude_setup.py](/home/code/CODING/server-manager/core/claude_setup.py)
|
||||
4. [build.py](/home/code/CODING/server-manager/build.py)
|
||||
5. [SKILL.md](/home/code/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
|
||||
6. [command-matrix.md](/home/code/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
|
||||
1. [tools/ssh.py](/home/code/Desktop/CODING/server-manager/tools/ssh.py)
|
||||
2. [tools/skill-ssh.md](/home/code/Desktop/CODING/server-manager/tools/skill-ssh.md)
|
||||
3. [core/claude_setup.py](/home/code/Desktop/CODING/server-manager/core/claude_setup.py)
|
||||
4. [build.py](/home/code/Desktop/CODING/server-manager/build.py)
|
||||
5. [SKILL.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/SKILL.md)
|
||||
6. [command-matrix.md](/home/code/Desktop/CODING/server-manager/.codex/skills/server-manager/references/command-matrix.md)
|
||||
|
||||
Если меняется семантика `ssh.py`, нужно обновлять и Claude skill, и Codex skill.
|
||||
|
||||
|
||||
52
GEMINI.md
Normal file
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.
|
||||
45
README.md
45
README.md
@@ -2,7 +2,7 @@
|
||||
|
||||
<p align="center">
|
||||
<strong>Desktop GUI for managing remote servers</strong><br>
|
||||
CustomTkinter + Paramiko | Dark Theme | Claude Code + Codex Integration
|
||||
CustomTkinter + Paramiko | Dark Theme | Claude Code + Codex + Gemini Integration
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -22,7 +22,7 @@
|
||||
- **SFTP Transfer** — upload/download files with progress bar
|
||||
- **SSH Keys** — generate ed25519, install on server, copy to clipboard
|
||||
- **Status Monitor** — background check every 60 sec (online/offline badges)
|
||||
- **Claude Code + Codex Integration** — one-click setup, shared config with `/ssh` skill and Codex skill
|
||||
- **Claude Code + Codex + Gemini Integration** — one-click setup, shared config with `/ssh`, Codex skill, and Gemini skill
|
||||
- **TOTP / 2FA** — Google Authenticator compatible codes with live countdown, one-click copy
|
||||
- **Encryption** — servers.json encrypted with Fernet (passwords never stored in plaintext)
|
||||
- **Backups** — manual and automatic backups with one-click restore
|
||||
@@ -62,10 +62,10 @@ Output goes to `releases/ServerManager-vX.Y.Z-{platform}.exe`
|
||||
3. **Terminal** — select server → Terminal tab → type command → Run
|
||||
4. **Files** — select server → Files tab → set paths → Upload/Download
|
||||
5. **Keys** — Keys tab → Generate Key → Install on Server
|
||||
6. **Setup** — Setup tab → "Install Everything" → Claude Code and Codex ready
|
||||
6. **Setup** — Setup tab → "Install Everything" → Claude Code, Codex, and Gemini ready
|
||||
7. Status badges update automatically (green = online, red = offline)
|
||||
|
||||
### Claude Code + Codex Integration
|
||||
### Claude Code + Codex + Gemini Integration
|
||||
|
||||
ServerManager, Claude Code, and Codex share the same config file: `~/.server-connections/servers.json`
|
||||
|
||||
@@ -75,11 +75,11 @@ For Codex deployment and operational edge cases, see [`CODEX_SKILL_SETUP.md`](CO
|
||||
```
|
||||
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py backend
|
||||
↕ ↕
|
||||
Add/edit/delete Claude /ssh + Codex skill
|
||||
Add/edit/delete Claude /ssh + Codex + Gemini skill
|
||||
servers in GUI execute commands
|
||||
```
|
||||
|
||||
- Add a server in GUI → Claude Code and Codex see it immediately
|
||||
- Add a server in GUI → Claude Code, Codex, and Gemini see it immediately
|
||||
- Both agents use the same `ssh.py` + `servers.json`
|
||||
- Passwords **never** pass through the AI API
|
||||
|
||||
@@ -95,14 +95,17 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py ba
|
||||
**Setup on a new machine:**
|
||||
1. Install ServerManager (clone repo or download binary)
|
||||
2. Open Setup tab → click "Install Everything"
|
||||
3. Done. Claude Code now has `/ssh`, and Codex now has the `server-manager` skill with access to your servers
|
||||
3. Done. Claude Code now has `/ssh`, Codex now has the `server-manager` skill, and Gemini now has the `server-manager` skill with access to your servers
|
||||
|
||||
The Setup tab installs:
|
||||
- `ssh.py` → `~/.server-connections/` (SSH utility)
|
||||
- `encryption.py` → `~/.server-connections/` (encryption module for CLI)
|
||||
- `/ssh` skill → `~/.claude/commands/ssh.md` (Claude Code skill)
|
||||
- `server-manager` skill → `~/.codex/skills/server-manager/` (Codex skill package)
|
||||
- `server-manager` skill → `~/.gemini/skills/server-manager/` (Gemini skill package)
|
||||
- optional mirror → `~/.agents/skills/server-manager/` (off by default to avoid Gemini duplicate-skill warnings)
|
||||
- `codex-ssh` wrapper → `~/.server-connections/` (safe Codex entry point)
|
||||
- `gemini-ssh` wrapper → `~/.server-connections/` (safe Gemini entry point)
|
||||
- SSH key (ed25519) — if not exists
|
||||
- Checks for duplicates — safe to run multiple times
|
||||
|
||||
@@ -158,7 +161,7 @@ ServerManager/
|
||||
│ ├── server_store.py # CRUD + encrypted JSON + observer + backups
|
||||
│ ├── encryption.py # Fernet encryption module
|
||||
│ ├── ssh_client.py # Paramiko SSH/SFTP wrapper
|
||||
│ ├── claude_setup.py # Claude Code + Codex integration installer
|
||||
│ ├── claude_setup.py # Claude Code + Codex + Gemini integration installer
|
||||
│ ├── status_checker.py # Background monitoring
|
||||
│ ├── totp.py # TOTP/2FA module (pyotp)
|
||||
│ ├── logger.py # Rotating file logger
|
||||
@@ -205,7 +208,7 @@ python main.py
|
||||
- **SFTP** — загрузка и скачивание файлов с прогресс-баром
|
||||
- **SSH-ключи** — генерация ed25519, установка на сервер, копирование
|
||||
- **Мониторинг** — фоновая проверка каждые 60 сек (бейджи online/offline)
|
||||
- **Интеграция с Claude Code + Codex** — установка в один клик, общий конфиг со скиллом `/ssh` и Codex skill
|
||||
- **Интеграция с Claude Code + Codex + Gemini** — установка в один клик, общий конфиг со скиллом `/ssh`, Codex skill и Gemini skill
|
||||
- **TOTP / 2FA** — коды Google Authenticator с обратным отсчётом, копирование в один клик
|
||||
- **Шифрование** — servers.json зашифрован Fernet (пароли не хранятся в открытом виде)
|
||||
- **Бэкапы** — ручные и автоматические с восстановлением в один клик
|
||||
@@ -245,22 +248,22 @@ python build.py
|
||||
3. **Терминал** — выберите сервер → вкладка Terminal → введите команду → Run
|
||||
4. **Файлы** — выберите сервер → вкладка Files → укажите пути → Upload/Download
|
||||
5. **Ключи** — вкладка Keys → Generate Key → Install on Server
|
||||
6. **Настройка** — вкладка Setup → "Install Everything" → Claude Code и Codex готовы
|
||||
6. **Настройка** — вкладка Setup → "Install Everything" → Claude Code, Codex и Gemini готовы
|
||||
7. Бейджи статуса обновляются автоматически (зелёный = online, красный = offline)
|
||||
|
||||
### Интеграция с Claude Code + Codex
|
||||
### Интеграция с Claude Code + Codex + Gemini
|
||||
|
||||
ServerManager, Claude Code и Codex используют **один и тот же файл конфигурации**: `~/.server-connections/servers.json`
|
||||
ServerManager, Claude Code, Codex и Gemini используют **один и тот же файл конфигурации**: `~/.server-connections/servers.json`
|
||||
|
||||
**Как это работает:**
|
||||
```
|
||||
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ backend ssh.py
|
||||
↕ ↕
|
||||
Добавил/изменил Claude /ssh + Codex skill
|
||||
Добавил/изменил Claude /ssh + Codex + Gemini skill
|
||||
серверы в GUI выполняют команды
|
||||
```
|
||||
|
||||
- Добавил сервер в GUI → Claude Code и Codex сразу видят его
|
||||
- Добавил сервер в GUI → Claude Code, Codex и Gemini сразу видят его
|
||||
- Оба агента используют один `ssh.py` + `servers.json`
|
||||
- Пароли **никогда** не проходят через API нейронки
|
||||
|
||||
@@ -276,14 +279,16 @@ ServerManager GUI ←→ ~/.server-connections/servers.json ←→ backend s
|
||||
**Настройка на новой машине:**
|
||||
1. Установить ServerManager (клонировать репо или скачать бинарник)
|
||||
2. Открыть вкладку Setup → нажать "Install Everything"
|
||||
3. Готово. Claude Code получает скилл `/ssh`, а Codex получает skill `server-manager` и доступ к серверам
|
||||
3. Готово. Claude Code получает скилл `/ssh`, а Codex и Gemini получают skill `server-manager` и доступ к серверам
|
||||
|
||||
Вкладка Setup устанавливает:
|
||||
- `ssh.py` → `~/.server-connections/` (SSH-утилита)
|
||||
- `encryption.py` → `~/.server-connections/` (модуль шифрования для CLI)
|
||||
- скилл `/ssh` → `~/.claude/commands/ssh.md` (скилл Claude Code)
|
||||
- skill `server-manager` → `~/.codex/skills/server-manager/` (скилл Codex)
|
||||
- skill `server-manager` → `~/.gemini/skills/server-manager/` (скилл Gemini)
|
||||
- wrapper `codex-ssh` → `~/.server-connections/` (безопасная точка входа для Codex)
|
||||
- wrapper `gemini-ssh` → `~/.server-connections/` (безопасная точка входа для Gemini)
|
||||
- SSH-ключ (ed25519) — если ещё не создан
|
||||
- Проверяет дубли — безопасно запускать повторно
|
||||
|
||||
@@ -342,7 +347,7 @@ pip install -r requirements.txt
|
||||
python main.py
|
||||
# → Вкладка Setup → Install Everything
|
||||
# → Добавить серверы через + Add
|
||||
# → Готово! GUI, Claude Code и Codex работают с одним конфигом
|
||||
# → Готово! GUI, Claude Code, Codex и Gemini работают с одним конфигом
|
||||
```
|
||||
|
||||
---
|
||||
@@ -356,7 +361,7 @@ python main.py
|
||||
- **SFTP传输** — 带进度条的文件上传/下载
|
||||
- **SSH密钥** — 生成ed25519、安装到服务器、复制到剪贴板
|
||||
- **状态监控** — 每60秒后台检查(在线/离线徽标)
|
||||
- **Claude Code + Codex 集成** — 一键设置,与 `/ssh` 技能和 Codex skill 共享配置
|
||||
- **Claude Code + Codex + Gemini 集成** — 一键设置,与 `/ssh` 技能、Codex skill 和 Gemini skill 共享配置
|
||||
- **TOTP / 2FA** — 兼容Google Authenticator的验证码,实时倒计时,一键复制
|
||||
- **加密** — servers.json使用Fernet加密(密码不再以明文存储)
|
||||
- **备份** — 手动和自动备份,一键恢复
|
||||
@@ -399,7 +404,7 @@ python build.py
|
||||
6. **设置** — Setup标签 → "Install Everything" → Claude Code 和 Codex 就绪
|
||||
7. 状态徽标自动更新(绿色 = 在线,红色 = 离线)
|
||||
|
||||
### Claude Code + Codex 集成
|
||||
### Claude Code + Codex + Gemini 集成
|
||||
|
||||
ServerManager、Claude Code 和 Codex 共享**同一个配置文件**:`~/.server-connections/servers.json`
|
||||
|
||||
@@ -407,12 +412,12 @@ ServerManager、Claude Code 和 Codex 共享**同一个配置文件**:`~/.serv
|
||||
```
|
||||
ServerManager GUI ←→ ~/.server-connections/servers.json ←→ ssh.py 后端
|
||||
↕ ↕
|
||||
在GUI中添加/编辑 Claude /ssh + Codex skill
|
||||
在GUI中添加/编辑 Claude /ssh + Codex + Gemini skill
|
||||
服务器 执行命令
|
||||
```
|
||||
|
||||
- 在GUI中添加服务器 → Claude Code 和 Codex 都会立即看到
|
||||
- 两个代理都使用同一个 `ssh.py` + `servers.json`
|
||||
- Claude Code、Codex 和 Gemini 都使用同一个 `ssh.py` + `servers.json`
|
||||
- 密码**绝不**通过AI API传递
|
||||
|
||||
**在新机器上设置:**
|
||||
|
||||
6
build.py
6
build.py
@@ -114,8 +114,12 @@ def build():
|
||||
"--add-data", f"config/servers.example.json{os.pathsep}config",
|
||||
"--add-data", f"tools/ssh.py{os.pathsep}tools",
|
||||
"--add-data", f"tools/skill-ssh.md{os.pathsep}tools",
|
||||
"--add-data", f"tools/install_ai_integrations.py{os.pathsep}tools",
|
||||
"--add-data", f"core/encryption.py{os.pathsep}core",
|
||||
"--add-data", f".codex/skills/server-manager{os.pathsep}.codex/skills/server-manager",
|
||||
"--add-data", f".gemini/skills/server-manager{os.pathsep}.gemini/skills/server-manager",
|
||||
"--add-data", f".gemini/settings.json{os.pathsep}.gemini",
|
||||
"--add-data", f"GEMINI.md{os.pathsep}.",
|
||||
]
|
||||
|
||||
# PNG icons for GUI (Material Design)
|
||||
@@ -355,6 +359,7 @@ def deploy_shared_files():
|
||||
from core.claude_setup import (
|
||||
install_claude_skill,
|
||||
install_codex_skill,
|
||||
install_gemini_skill,
|
||||
install_ssh_script,
|
||||
)
|
||||
|
||||
@@ -362,6 +367,7 @@ def deploy_shared_files():
|
||||
install_ssh_script,
|
||||
install_claude_skill,
|
||||
install_codex_skill,
|
||||
install_gemini_skill,
|
||||
]
|
||||
|
||||
deployed = []
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Local AI agent integration setup.
|
||||
Installs the shared ssh.py/encryption.py backend, Claude /ssh command,
|
||||
Codex skill package, platform-specific wrappers, and SSH key material.
|
||||
Codex/Gemini skill packages, platform-specific wrappers, and SSH key material.
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -12,8 +12,6 @@ import sys
|
||||
|
||||
from core.logger import log
|
||||
|
||||
SHARED_DIR = os.path.expanduser("~/.server-connections")
|
||||
|
||||
# PyInstaller: bundled data is in sys._MEIPASS; otherwise use project dir
|
||||
if getattr(sys, "frozen", False) and hasattr(sys, "_MEIPASS"):
|
||||
_BASE_DIR = sys._MEIPASS
|
||||
@@ -23,25 +21,19 @@ else:
|
||||
SSH_SCRIPT_SRC = os.path.join(_BASE_DIR, "tools", "ssh.py")
|
||||
ENCRYPTION_SRC = os.path.join(_BASE_DIR, "core", "encryption.py")
|
||||
CLAUDE_SKILL_SRC = os.path.join(_BASE_DIR, "tools", "skill-ssh.md")
|
||||
|
||||
CLAUDE_SKILL_DST_DIR = os.path.expanduser("~/.claude/commands")
|
||||
CLAUDE_SKILL_DST = os.path.join(CLAUDE_SKILL_DST_DIR, "ssh.md")
|
||||
SSH_KEY_PATH = os.path.expanduser("~/.ssh/id_ed25519")
|
||||
GLOBAL_CLAUDE_MD = os.path.expanduser("~/.claude/CLAUDE.md")
|
||||
GEMINI_CONTRACT_SRC = os.path.join(_BASE_DIR, "GEMINI.md")
|
||||
|
||||
CODEX_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".codex", "skills", "server-manager")
|
||||
CODEX_SKILL_DST_ROOT = os.path.expanduser("~/.codex/skills")
|
||||
CODEX_SKILL_DST_DIR = os.path.join(CODEX_SKILL_DST_ROOT, "server-manager")
|
||||
CODEX_SKILL_ENTRY = os.path.join(CODEX_SKILL_DST_DIR, "SKILL.md")
|
||||
CODEX_WRAPPER_SRC_SH = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.sh")
|
||||
CODEX_WRAPPER_SRC_CMD = os.path.join(CODEX_SKILL_SRC_DIR, "scripts", "codex-ssh-wrapper.cmd")
|
||||
CODEX_WRAPPER_DST = os.path.join(
|
||||
SHARED_DIR,
|
||||
"codex-ssh.cmd" if sys.platform == "win32" else "codex-ssh",
|
||||
)
|
||||
GEMINI_SKILL_SRC_DIR = os.path.join(_BASE_DIR, ".gemini", "skills", "server-manager")
|
||||
GEMINI_WRAPPER_SRC_SH = os.path.join(GEMINI_SKILL_SRC_DIR, "scripts", "gemini-ssh-wrapper.sh")
|
||||
GEMINI_WRAPPER_SRC_CMD = os.path.join(GEMINI_SKILL_SRC_DIR, "scripts", "gemini-ssh-wrapper.cmd")
|
||||
|
||||
_BLOCK_START = "<!-- server-manager:start -->"
|
||||
_BLOCK_END = "<!-- server-manager:end -->"
|
||||
_GEMINI_BLOCK_START = "<!-- server-manager-gemini:start -->"
|
||||
_GEMINI_BLOCK_END = "<!-- server-manager-gemini:end -->"
|
||||
|
||||
GLOBAL_CLAUDE_MD_BLOCK = f"""{_BLOCK_START}
|
||||
## Серверы — ТОЛЬКО через /ssh
|
||||
@@ -80,6 +72,110 @@ python ~/.server-connections/ssh.py --status # online/offline
|
||||
{_BLOCK_END}
|
||||
"""
|
||||
|
||||
GLOBAL_GEMINI_MD_BLOCK = f"""{_GEMINI_BLOCK_START}
|
||||
## ServerManager — use the installed skill
|
||||
|
||||
When a user asks about a server managed by ServerManager, use the installed `server-manager` skill first.
|
||||
|
||||
Preferred discovery commands:
|
||||
|
||||
```bash
|
||||
$HOME/.server-connections/gemini-ssh --list
|
||||
$HOME/.server-connections/gemini-ssh --info ALIAS
|
||||
$HOME/.server-connections/gemini-ssh --status
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Never read `~/.server-connections/servers.json`, `settings.json`, or `encryption.py` directly.
|
||||
- Never use `--list-full`.
|
||||
- Never use raw `ssh`, `scp`, `redis-cli`, `psql`, `mysql`, `mc`, or cloud CLIs unless the user explicitly asks to bypass ServerManager.
|
||||
- Choose commands strictly by the endpoint type reported by `--list`.
|
||||
- Use exactly one connection attempt per action and stop on timeout/failure.
|
||||
{_GEMINI_BLOCK_END}
|
||||
"""
|
||||
|
||||
|
||||
def _target_home() -> str:
|
||||
override = os.environ.get("SERVER_MANAGER_TARGET_HOME", "").strip()
|
||||
if override:
|
||||
return os.path.abspath(os.path.expanduser(override))
|
||||
return os.path.expanduser("~")
|
||||
|
||||
|
||||
def _shared_dir() -> str:
|
||||
return os.path.join(_target_home(), ".server-connections")
|
||||
|
||||
|
||||
def _gemini_dir() -> str:
|
||||
return os.path.join(_target_home(), ".gemini")
|
||||
|
||||
|
||||
def _claude_skill_dst_dir() -> str:
|
||||
return os.path.join(_target_home(), ".claude", "commands")
|
||||
|
||||
|
||||
def _claude_skill_dst() -> str:
|
||||
return os.path.join(_claude_skill_dst_dir(), "ssh.md")
|
||||
|
||||
|
||||
def _ssh_key_path() -> str:
|
||||
return os.path.join(_target_home(), ".ssh", "id_ed25519")
|
||||
|
||||
|
||||
def _global_claude_md() -> str:
|
||||
return os.path.join(_target_home(), ".claude", "CLAUDE.md")
|
||||
|
||||
|
||||
def _global_gemini_md() -> str:
|
||||
return os.path.join(_gemini_dir(), "GEMINI.md")
|
||||
|
||||
|
||||
def _codex_skill_dst_root() -> str:
|
||||
return os.path.join(_target_home(), ".codex", "skills")
|
||||
|
||||
|
||||
def _codex_skill_dst_dir() -> str:
|
||||
return os.path.join(_codex_skill_dst_root(), "server-manager")
|
||||
|
||||
|
||||
def _codex_skill_entry() -> str:
|
||||
return os.path.join(_codex_skill_dst_dir(), "SKILL.md")
|
||||
|
||||
|
||||
def _codex_wrapper_dst() -> str:
|
||||
return os.path.join(
|
||||
_shared_dir(),
|
||||
"codex-ssh.cmd" if sys.platform == "win32" else "codex-ssh",
|
||||
)
|
||||
|
||||
|
||||
def _gemini_skill_dst_root() -> str:
|
||||
return os.path.join(_gemini_dir(), "skills")
|
||||
|
||||
|
||||
def _gemini_skill_dst_dir() -> str:
|
||||
return os.path.join(_gemini_skill_dst_root(), "server-manager")
|
||||
|
||||
|
||||
def _gemini_skill_entry() -> str:
|
||||
return os.path.join(_gemini_skill_dst_dir(), "SKILL.md")
|
||||
|
||||
|
||||
def _agents_skill_dst_root() -> str:
|
||||
return os.path.join(_target_home(), ".agents", "skills")
|
||||
|
||||
|
||||
def _agents_skill_dst_dir() -> str:
|
||||
return os.path.join(_agents_skill_dst_root(), "server-manager")
|
||||
|
||||
|
||||
def _gemini_wrapper_dst() -> str:
|
||||
return os.path.join(
|
||||
_shared_dir(),
|
||||
"gemini-ssh.cmd" if sys.platform == "win32" else "gemini-ssh",
|
||||
)
|
||||
|
||||
|
||||
def _ensure_executable(path: str):
|
||||
if sys.platform == "win32" or not os.path.exists(path):
|
||||
@@ -102,27 +198,96 @@ def _copy_tree(src: str, dst: str) -> str:
|
||||
return dst
|
||||
|
||||
|
||||
def _install_wrapper(src: str, dst: str) -> str:
|
||||
return _copy_file(src, dst, executable=(sys.platform != "win32"))
|
||||
|
||||
|
||||
def _skill_script_names() -> list[str]:
|
||||
if sys.platform == "win32":
|
||||
return [
|
||||
os.path.join("scripts", "server-manager-doctor.cmd"),
|
||||
os.path.join("scripts", "codex-ssh-wrapper.cmd"),
|
||||
os.path.join("scripts", "server-manager-gemini-doctor.cmd"),
|
||||
os.path.join("scripts", "gemini-ssh-wrapper.cmd"),
|
||||
]
|
||||
return [
|
||||
os.path.join("scripts", "server-manager-doctor.sh"),
|
||||
os.path.join("scripts", "codex-ssh-wrapper.sh"),
|
||||
os.path.join("scripts", "server-manager-gemini-doctor.sh"),
|
||||
os.path.join("scripts", "gemini-ssh-wrapper.sh"),
|
||||
]
|
||||
|
||||
|
||||
def _ensure_skill_scripts(skill_dir: str):
|
||||
for rel_path in _skill_script_names():
|
||||
_ensure_executable(os.path.join(skill_dir, rel_path))
|
||||
|
||||
|
||||
def _iter_all_user_homes() -> list[str]:
|
||||
homes: list[str] = []
|
||||
|
||||
def add(path: str):
|
||||
expanded = os.path.abspath(os.path.expanduser(path))
|
||||
if os.path.isdir(expanded) and expanded not in homes:
|
||||
homes.append(expanded)
|
||||
|
||||
add(_target_home())
|
||||
|
||||
if sys.platform == "win32":
|
||||
users_root = os.path.join(os.environ.get("SystemDrive", "C:"), "Users")
|
||||
skip = {"public", "default", "default user", "all users"}
|
||||
if os.path.isdir(users_root):
|
||||
for name in sorted(os.listdir(users_root)):
|
||||
if name.lower() in skip:
|
||||
continue
|
||||
add(os.path.join(users_root, name))
|
||||
elif sys.platform == "darwin":
|
||||
add("/var/root")
|
||||
users_root = "/Users"
|
||||
if os.path.isdir(users_root):
|
||||
for name in sorted(os.listdir(users_root)):
|
||||
if name.startswith("."):
|
||||
continue
|
||||
add(os.path.join(users_root, name))
|
||||
else:
|
||||
add("/root")
|
||||
users_root = "/home"
|
||||
if os.path.isdir(users_root):
|
||||
for name in sorted(os.listdir(users_root)):
|
||||
if name.startswith("."):
|
||||
continue
|
||||
add(os.path.join(users_root, name))
|
||||
|
||||
return homes
|
||||
|
||||
|
||||
def check_status() -> dict:
|
||||
"""Check what's installed and what's missing."""
|
||||
shared_dir = _shared_dir()
|
||||
ssh_key_path = _ssh_key_path()
|
||||
return {
|
||||
"shared_dir": os.path.exists(SHARED_DIR),
|
||||
"servers_json": os.path.exists(os.path.join(SHARED_DIR, "servers.json")),
|
||||
"ssh_script": os.path.exists(os.path.join(SHARED_DIR, "ssh.py")),
|
||||
"encryption": os.path.exists(os.path.join(SHARED_DIR, "encryption.py")),
|
||||
"claude_skill_installed": os.path.exists(CLAUDE_SKILL_DST),
|
||||
"codex_skill_installed": os.path.exists(CODEX_SKILL_ENTRY),
|
||||
"codex_wrapper_installed": os.path.exists(CODEX_WRAPPER_DST),
|
||||
"ssh_key_exists": os.path.exists(SSH_KEY_PATH),
|
||||
"ssh_key_pub": os.path.exists(SSH_KEY_PATH + ".pub"),
|
||||
"target_home": _target_home(),
|
||||
"shared_dir": os.path.exists(shared_dir),
|
||||
"servers_json": os.path.exists(os.path.join(shared_dir, "servers.json")),
|
||||
"ssh_script": os.path.exists(os.path.join(shared_dir, "ssh.py")),
|
||||
"encryption": os.path.exists(os.path.join(shared_dir, "encryption.py")),
|
||||
"claude_skill_installed": os.path.exists(_claude_skill_dst()),
|
||||
"codex_skill_installed": os.path.exists(_codex_skill_entry()),
|
||||
"codex_wrapper_installed": os.path.exists(_codex_wrapper_dst()),
|
||||
"gemini_skill_installed": os.path.exists(_gemini_skill_entry()),
|
||||
"gemini_wrapper_installed": os.path.exists(_gemini_wrapper_dst()),
|
||||
"ssh_key_exists": os.path.exists(ssh_key_path),
|
||||
"ssh_key_pub": os.path.exists(ssh_key_path + ".pub"),
|
||||
}
|
||||
|
||||
|
||||
def install_ssh_script() -> str:
|
||||
"""Copy ssh.py and encryption.py to shared dir."""
|
||||
os.makedirs(SHARED_DIR, exist_ok=True)
|
||||
shared_dir = _shared_dir()
|
||||
os.makedirs(shared_dir, exist_ok=True)
|
||||
results = []
|
||||
|
||||
dst = os.path.join(SHARED_DIR, "ssh.py")
|
||||
dst = os.path.join(shared_dir, "ssh.py")
|
||||
if os.path.exists(SSH_SCRIPT_SRC):
|
||||
_copy_file(SSH_SCRIPT_SRC, dst, executable=True)
|
||||
log.info(f"ssh.py installed: {dst}")
|
||||
@@ -132,7 +297,7 @@ def install_ssh_script() -> str:
|
||||
else:
|
||||
results.append("ERROR: ssh.py source not found")
|
||||
|
||||
enc_dst = os.path.join(SHARED_DIR, "encryption.py")
|
||||
enc_dst = os.path.join(shared_dir, "encryption.py")
|
||||
if os.path.exists(ENCRYPTION_SRC):
|
||||
_copy_file(ENCRYPTION_SRC, enc_dst)
|
||||
log.info(f"encryption.py installed: {enc_dst}")
|
||||
@@ -147,54 +312,102 @@ def install_ssh_script() -> str:
|
||||
|
||||
def install_claude_skill() -> str:
|
||||
"""Install /ssh skill for Claude Code."""
|
||||
os.makedirs(CLAUDE_SKILL_DST_DIR, exist_ok=True)
|
||||
claude_skill_dst_dir = _claude_skill_dst_dir()
|
||||
claude_skill_dst = _claude_skill_dst()
|
||||
os.makedirs(claude_skill_dst_dir, exist_ok=True)
|
||||
if os.path.exists(CLAUDE_SKILL_SRC):
|
||||
_copy_file(CLAUDE_SKILL_SRC, CLAUDE_SKILL_DST)
|
||||
log.info(f"Claude skill installed: {CLAUDE_SKILL_DST}")
|
||||
return f"Claude skill installed: {CLAUDE_SKILL_DST}"
|
||||
if os.path.exists(CLAUDE_SKILL_DST):
|
||||
return f"Claude skill already exists: {CLAUDE_SKILL_DST}"
|
||||
_copy_file(CLAUDE_SKILL_SRC, claude_skill_dst)
|
||||
log.info(f"Claude skill installed: {claude_skill_dst}")
|
||||
return f"Claude skill installed: {claude_skill_dst}"
|
||||
if os.path.exists(claude_skill_dst):
|
||||
return f"Claude skill already exists: {claude_skill_dst}"
|
||||
|
||||
skill_content = _generate_skill_content()
|
||||
with open(CLAUDE_SKILL_DST, "w", encoding="utf-8") as f:
|
||||
with open(claude_skill_dst, "w", encoding="utf-8") as f:
|
||||
f.write(skill_content)
|
||||
log.info(f"Claude skill generated: {CLAUDE_SKILL_DST}")
|
||||
return f"Claude skill generated: {CLAUDE_SKILL_DST}"
|
||||
log.info(f"Claude skill generated: {claude_skill_dst}")
|
||||
return f"Claude skill generated: {claude_skill_dst}"
|
||||
|
||||
|
||||
def install_codex_skill() -> str:
|
||||
"""Install ServerManager skill package for Codex and the local wrapper."""
|
||||
results = []
|
||||
codex_skill_dst_dir = _codex_skill_dst_dir()
|
||||
codex_skill_entry = _codex_skill_entry()
|
||||
codex_wrapper_dst = _codex_wrapper_dst()
|
||||
|
||||
if os.path.isdir(CODEX_SKILL_SRC_DIR):
|
||||
_copy_tree(CODEX_SKILL_SRC_DIR, CODEX_SKILL_DST_DIR)
|
||||
for rel_path in [
|
||||
os.path.join("scripts", "server-manager-doctor.sh"),
|
||||
os.path.join("scripts", "server-manager-doctor.cmd"),
|
||||
os.path.join("scripts", "codex-ssh-wrapper.sh"),
|
||||
os.path.join("scripts", "codex-ssh-wrapper.cmd"),
|
||||
]:
|
||||
_ensure_executable(os.path.join(CODEX_SKILL_DST_DIR, rel_path))
|
||||
log.info(f"Codex skill installed: {CODEX_SKILL_DST_DIR}")
|
||||
results.append(f"Codex skill installed: {CODEX_SKILL_DST_DIR}")
|
||||
elif os.path.exists(CODEX_SKILL_ENTRY):
|
||||
results.append(f"Codex skill already exists: {CODEX_SKILL_DST_DIR}")
|
||||
_copy_tree(CODEX_SKILL_SRC_DIR, codex_skill_dst_dir)
|
||||
_ensure_skill_scripts(codex_skill_dst_dir)
|
||||
log.info(f"Codex skill installed: {codex_skill_dst_dir}")
|
||||
results.append(f"Codex skill installed: {codex_skill_dst_dir}")
|
||||
elif os.path.exists(codex_skill_entry):
|
||||
results.append(f"Codex skill already exists: {codex_skill_dst_dir}")
|
||||
else:
|
||||
results.append("ERROR: Codex skill source not found")
|
||||
|
||||
wrapper_src = CODEX_WRAPPER_SRC_CMD if sys.platform == "win32" else CODEX_WRAPPER_SRC_SH
|
||||
if os.path.exists(wrapper_src):
|
||||
_copy_file(wrapper_src, CODEX_WRAPPER_DST, executable=(sys.platform != "win32"))
|
||||
log.info(f"Codex wrapper installed: {CODEX_WRAPPER_DST}")
|
||||
results.append(f"Codex wrapper installed: {CODEX_WRAPPER_DST}")
|
||||
elif os.path.exists(CODEX_WRAPPER_DST):
|
||||
results.append(f"Codex wrapper already exists: {CODEX_WRAPPER_DST}")
|
||||
_copy_file(wrapper_src, codex_wrapper_dst, executable=(sys.platform != "win32"))
|
||||
log.info(f"Codex wrapper installed: {codex_wrapper_dst}")
|
||||
results.append(f"Codex wrapper installed: {codex_wrapper_dst}")
|
||||
elif os.path.exists(codex_wrapper_dst):
|
||||
results.append(f"Codex wrapper already exists: {codex_wrapper_dst}")
|
||||
else:
|
||||
results.append("ERROR: Codex wrapper source not found")
|
||||
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
def install_gemini_skill() -> str:
|
||||
"""Install ServerManager skill package for Gemini."""
|
||||
results = []
|
||||
gemini_skill_dst_dir = _gemini_skill_dst_dir()
|
||||
gemini_skill_entry = _gemini_skill_entry()
|
||||
agents_skill_dst_dir = _agents_skill_dst_dir()
|
||||
gemini_wrapper_dst = _gemini_wrapper_dst()
|
||||
install_generic_mirror = os.environ.get(
|
||||
"SERVER_MANAGER_INSTALL_GENERIC_SKILL_MIRROR", ""
|
||||
).strip() == "1"
|
||||
|
||||
if os.path.isdir(GEMINI_SKILL_SRC_DIR):
|
||||
_copy_tree(GEMINI_SKILL_SRC_DIR, gemini_skill_dst_dir)
|
||||
_ensure_skill_scripts(gemini_skill_dst_dir)
|
||||
log.info(f"Gemini skill installed: {gemini_skill_dst_dir}")
|
||||
results.append(f"Gemini skill installed: {gemini_skill_dst_dir}")
|
||||
|
||||
if install_generic_mirror:
|
||||
_copy_tree(GEMINI_SKILL_SRC_DIR, agents_skill_dst_dir)
|
||||
_ensure_skill_scripts(agents_skill_dst_dir)
|
||||
log.info(f"Generic agents skill mirror installed: {agents_skill_dst_dir}")
|
||||
results.append(f"Generic agents skill mirror installed: {agents_skill_dst_dir}")
|
||||
elif os.path.exists(agents_skill_dst_dir):
|
||||
shutil.rmtree(agents_skill_dst_dir, ignore_errors=True)
|
||||
log.info(f"Removed generic agents skill mirror to avoid Gemini conflicts: {agents_skill_dst_dir}")
|
||||
results.append(
|
||||
f"Removed generic agents skill mirror to avoid Gemini conflicts: {agents_skill_dst_dir}"
|
||||
)
|
||||
elif os.path.exists(gemini_skill_entry):
|
||||
results.append(f"Gemini skill already exists: {gemini_skill_dst_dir}")
|
||||
else:
|
||||
results.append("ERROR: Gemini skill source not found")
|
||||
|
||||
wrapper_src = GEMINI_WRAPPER_SRC_CMD if sys.platform == "win32" else GEMINI_WRAPPER_SRC_SH
|
||||
if not os.path.exists(wrapper_src):
|
||||
wrapper_src = CODEX_WRAPPER_SRC_CMD if sys.platform == "win32" else CODEX_WRAPPER_SRC_SH
|
||||
|
||||
if os.path.exists(wrapper_src):
|
||||
_install_wrapper(wrapper_src, gemini_wrapper_dst)
|
||||
log.info(f"Gemini wrapper installed: {gemini_wrapper_dst}")
|
||||
results.append(f"Gemini wrapper installed: {gemini_wrapper_dst}")
|
||||
elif os.path.exists(gemini_wrapper_dst):
|
||||
results.append(f"Gemini wrapper already exists: {gemini_wrapper_dst}")
|
||||
else:
|
||||
results.append("ERROR: Gemini wrapper source not found")
|
||||
|
||||
return "\n".join(results)
|
||||
|
||||
|
||||
def install_skill() -> str:
|
||||
"""Backward-compatible alias for the Claude /ssh skill installer."""
|
||||
return install_claude_skill()
|
||||
@@ -202,19 +415,20 @@ def install_skill() -> str:
|
||||
|
||||
def generate_ssh_key() -> str:
|
||||
"""Generate ed25519 SSH key if not exists."""
|
||||
if os.path.exists(SSH_KEY_PATH):
|
||||
return f"Key already exists: {SSH_KEY_PATH}"
|
||||
ssh_key_path = _ssh_key_path()
|
||||
if os.path.exists(ssh_key_path):
|
||||
return f"Key already exists: {ssh_key_path}"
|
||||
|
||||
os.makedirs(os.path.dirname(SSH_KEY_PATH), exist_ok=True)
|
||||
os.makedirs(os.path.dirname(ssh_key_path), exist_ok=True)
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["ssh-keygen", "-t", "ed25519", "-f", SSH_KEY_PATH,
|
||||
["ssh-keygen", "-t", "ed25519", "-f", ssh_key_path,
|
||||
"-N", "", "-C", "server-manager"],
|
||||
check=True, capture_output=True, timeout=15
|
||||
)
|
||||
log.info(f"SSH key generated: {SSH_KEY_PATH}")
|
||||
return f"Key generated: {SSH_KEY_PATH}"
|
||||
log.info(f"SSH key generated: {ssh_key_path}")
|
||||
return f"Key generated: {ssh_key_path}"
|
||||
except FileNotFoundError:
|
||||
hint = "enable OpenSSH optional feature" if sys.platform == "win32" else "install openssh-client"
|
||||
msg = f"ERROR: ssh-keygen not found — {hint}"
|
||||
@@ -228,11 +442,12 @@ def generate_ssh_key() -> str:
|
||||
|
||||
def install_global_claude_md() -> str:
|
||||
"""Add/update server manager section in global ~/.claude/CLAUDE.md."""
|
||||
os.makedirs(os.path.dirname(GLOBAL_CLAUDE_MD), exist_ok=True)
|
||||
global_claude_md = _global_claude_md()
|
||||
os.makedirs(os.path.dirname(global_claude_md), exist_ok=True)
|
||||
|
||||
existing = ""
|
||||
if os.path.exists(GLOBAL_CLAUDE_MD):
|
||||
with open(GLOBAL_CLAUDE_MD, encoding="utf-8") as f:
|
||||
if os.path.exists(global_claude_md):
|
||||
with open(global_claude_md, encoding="utf-8") as f:
|
||||
existing = f.read()
|
||||
|
||||
pattern = re.compile(
|
||||
@@ -242,41 +457,96 @@ def install_global_claude_md() -> str:
|
||||
|
||||
if pattern.search(existing):
|
||||
updated = pattern.sub(GLOBAL_CLAUDE_MD_BLOCK.strip(), existing)
|
||||
with open(GLOBAL_CLAUDE_MD, "w", encoding="utf-8") as f:
|
||||
with open(global_claude_md, "w", encoding="utf-8") as f:
|
||||
f.write(updated)
|
||||
log.info(f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}")
|
||||
return f"Global CLAUDE.md block updated: {GLOBAL_CLAUDE_MD}"
|
||||
log.info(f"Global CLAUDE.md block updated: {global_claude_md}")
|
||||
return f"Global CLAUDE.md block updated: {global_claude_md}"
|
||||
|
||||
with open(GLOBAL_CLAUDE_MD, "a", encoding="utf-8") as f:
|
||||
with open(global_claude_md, "a", encoding="utf-8") as f:
|
||||
if existing and not existing.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n" + GLOBAL_CLAUDE_MD_BLOCK)
|
||||
log.info(f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}")
|
||||
return f"Global CLAUDE.md block added: {GLOBAL_CLAUDE_MD}"
|
||||
log.info(f"Global CLAUDE.md block added: {global_claude_md}")
|
||||
return f"Global CLAUDE.md block added: {global_claude_md}"
|
||||
|
||||
|
||||
def install_global_gemini_md() -> str:
|
||||
"""Add/update server manager section in global ~/.gemini/GEMINI.md."""
|
||||
global_gemini_md = _global_gemini_md()
|
||||
os.makedirs(os.path.dirname(global_gemini_md), exist_ok=True)
|
||||
|
||||
existing = ""
|
||||
if os.path.exists(global_gemini_md):
|
||||
with open(global_gemini_md, encoding="utf-8") as f:
|
||||
existing = f.read()
|
||||
|
||||
pattern = re.compile(
|
||||
re.escape(_GEMINI_BLOCK_START) + r".*?" + re.escape(_GEMINI_BLOCK_END),
|
||||
re.DOTALL
|
||||
)
|
||||
|
||||
if pattern.search(existing):
|
||||
updated = pattern.sub(GLOBAL_GEMINI_MD_BLOCK.strip(), existing)
|
||||
with open(global_gemini_md, "w", encoding="utf-8") as f:
|
||||
f.write(updated)
|
||||
log.info(f"Global GEMINI.md block updated: {global_gemini_md}")
|
||||
return f"Global GEMINI.md block updated: {global_gemini_md}"
|
||||
|
||||
with open(global_gemini_md, "a", encoding="utf-8") as f:
|
||||
if existing and not existing.endswith("\n"):
|
||||
f.write("\n")
|
||||
f.write("\n" + GLOBAL_GEMINI_MD_BLOCK)
|
||||
log.info(f"Global GEMINI.md block added: {global_gemini_md}")
|
||||
return f"Global GEMINI.md block added: {global_gemini_md}"
|
||||
|
||||
|
||||
def install_all() -> list[str]:
|
||||
"""Full setup — install everything for Claude Code and Codex."""
|
||||
results = []
|
||||
|
||||
steps = [
|
||||
"""Full setup — install everything for Claude Code, Codex, and Gemini."""
|
||||
all_users = os.environ.get("SERVER_MANAGER_INSTALL_ALL_USERS", "").strip() == "1"
|
||||
base_steps = [
|
||||
("ssh_script", install_ssh_script),
|
||||
("claude_skill", install_claude_skill),
|
||||
("codex_skill", install_codex_skill),
|
||||
("ssh_key", generate_ssh_key),
|
||||
("gemini_skill", install_gemini_skill),
|
||||
("global_claude_md", install_global_claude_md),
|
||||
("global_gemini_md", install_global_gemini_md),
|
||||
]
|
||||
|
||||
for name, func in steps:
|
||||
try:
|
||||
log.info(f"install_all: running {name}")
|
||||
result = func()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
msg = f"ERROR ({name}): {e}"
|
||||
log.error(msg)
|
||||
results.append(msg)
|
||||
if not all_users:
|
||||
steps = base_steps[:3] + [("ssh_key", generate_ssh_key)] + base_steps[3:]
|
||||
results = []
|
||||
for name, func in steps:
|
||||
try:
|
||||
log.info(f"install_all: running {name}")
|
||||
result = func()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
msg = f"ERROR ({name}): {e}"
|
||||
log.error(msg)
|
||||
results.append(msg)
|
||||
return results
|
||||
|
||||
results = []
|
||||
original_target = os.environ.get("SERVER_MANAGER_TARGET_HOME")
|
||||
for home in _iter_all_user_homes():
|
||||
os.environ["SERVER_MANAGER_TARGET_HOME"] = home
|
||||
results.append(f"[target_home] {home}")
|
||||
for name, func in base_steps:
|
||||
try:
|
||||
log.info(f"install_all(all_users): running {name} for {home}")
|
||||
result = func()
|
||||
results.append(result)
|
||||
except Exception as e:
|
||||
msg = f"ERROR ({name}, {home}): {e}"
|
||||
log.error(msg)
|
||||
results.append(msg)
|
||||
|
||||
if original_target is None:
|
||||
os.environ.pop("SERVER_MANAGER_TARGET_HOME", None)
|
||||
else:
|
||||
os.environ["SERVER_MANAGER_TARGET_HOME"] = original_target
|
||||
|
||||
results.append("INFO: SSH key generation skipped in SERVER_MANAGER_INSTALL_ALL_USERS=1 mode")
|
||||
return results
|
||||
|
||||
|
||||
|
||||
41
core/i18n.py
41
core/i18n.py
@@ -159,9 +159,9 @@ _EN = {
|
||||
# Setup
|
||||
"agent_integration": "AI Agent Integration",
|
||||
"agent_desc": (
|
||||
"Setup everything so Claude Code and Codex can manage your servers via shared local skills.\n"
|
||||
"ServerManager, Claude Code, and Codex share the same servers.json — add a server here,\n"
|
||||
"both agents see it immediately."
|
||||
"Setup everything so Claude Code, Codex, and Gemini can manage your servers via shared local skills.\n"
|
||||
"ServerManager, Claude Code, Codex, and Gemini share the same servers.json — add a server here,\n"
|
||||
"all agents see it immediately."
|
||||
),
|
||||
"claude_integration": "Claude Code Integration",
|
||||
"claude_desc": (
|
||||
@@ -178,6 +178,8 @@ _EN = {
|
||||
"status_claude_skill": "/ssh skill for Claude Code",
|
||||
"status_codex_skill": "ServerManager skill for Codex",
|
||||
"status_codex_wrapper": "Codex wrapper (codex-ssh)",
|
||||
"status_gemini_skill": "ServerManager skill for Gemini",
|
||||
"status_gemini_wrapper": "Gemini wrapper (gemini-ssh)",
|
||||
"status_ssh_key": "SSH key (ed25519)",
|
||||
"install_everything": "Install Everything",
|
||||
"installing_all": "Installing...",
|
||||
@@ -185,6 +187,7 @@ _EN = {
|
||||
"install_skill": "/ssh skill",
|
||||
"install_claude_skill": "Claude skill",
|
||||
"install_codex_skill": "Codex skill",
|
||||
"install_gemini_skill": "Gemini skill",
|
||||
"install_ssh_key": "SSH key",
|
||||
"refresh": "Refresh",
|
||||
"configuration": "Configuration",
|
||||
@@ -194,7 +197,7 @@ _EN = {
|
||||
"select_backup": "Select backup...",
|
||||
"no_backups": "No backups",
|
||||
"restore": "Restore",
|
||||
"install_done": "Done! Claude Code and Codex can now use ServerManager to manage your servers.",
|
||||
"install_done": "Done! Claude Code, Codex, and Gemini can now use ServerManager to manage your servers.",
|
||||
"config_changed": "Config path changed: {path}",
|
||||
"backup_created": "Backup created: {name}",
|
||||
"backup_failed": "Backup failed: {e}",
|
||||
@@ -726,17 +729,17 @@ _RU = {
|
||||
|
||||
# Setup
|
||||
"agent_integration": "Интеграция AI-агентов",
|
||||
"agent_desc": (
|
||||
"Настройте всё, чтобы Claude Code и Codex могли управлять серверами через локальные skills.\n"
|
||||
"ServerManager, Claude Code и Codex используют один и тот же servers.json — добавьте сервер здесь,\n"
|
||||
"и оба агента увидят его сразу."
|
||||
),
|
||||
"claude_integration": "Интеграция с Claude Code",
|
||||
"claude_desc": (
|
||||
"Настройте всё, чтобы Claude Code мог управлять серверами через скилл /ssh.\n"
|
||||
"GUI и Claude Code используют один servers.json — добавьте сервер здесь,\n"
|
||||
"Claude увидит его сразу."
|
||||
),
|
||||
"agent_desc": (
|
||||
"Настройте всё, чтобы Claude Code, Codex и Gemini могли управлять серверами через локальные skills.\n"
|
||||
"ServerManager, Claude Code, Codex и Gemini используют один и тот же servers.json — добавьте сервер здесь,\n"
|
||||
"все агенты увидят его сразу."
|
||||
),
|
||||
"status": "Статус",
|
||||
"status_shared_dir": "Общий каталог (~/.server-connections)",
|
||||
"status_servers_json": "servers.json",
|
||||
@@ -746,6 +749,8 @@ _RU = {
|
||||
"status_claude_skill": "Скилл /ssh для Claude Code",
|
||||
"status_codex_skill": "Скилл ServerManager для Codex",
|
||||
"status_codex_wrapper": "Обёртка Codex (codex-ssh)",
|
||||
"status_gemini_skill": "Скилл ServerManager для Gemini",
|
||||
"status_gemini_wrapper": "Обёртка Gemini (gemini-ssh)",
|
||||
"status_ssh_key": "SSH-ключ (ed25519)",
|
||||
"install_everything": "Установить всё",
|
||||
"installing_all": "Установка...",
|
||||
@@ -753,6 +758,7 @@ _RU = {
|
||||
"install_skill": "Скилл /ssh",
|
||||
"install_claude_skill": "Скилл Claude",
|
||||
"install_codex_skill": "Скилл Codex",
|
||||
"install_gemini_skill": "Скилл Gemini",
|
||||
"install_ssh_key": "SSH-ключ",
|
||||
"refresh": "Обновить",
|
||||
"configuration": "Конфигурация",
|
||||
@@ -762,7 +768,7 @@ _RU = {
|
||||
"select_backup": "Выберите бэкап...",
|
||||
"no_backups": "Нет бэкапов",
|
||||
"restore": "Восстановить",
|
||||
"install_done": "Готово! Claude Code и Codex теперь могут использовать ServerManager для управления серверами.",
|
||||
"install_done": "Готово! Claude Code, Codex и Gemini теперь могут использовать ServerManager для управления серверами.",
|
||||
"config_changed": "Путь конфига изменён: {path}",
|
||||
"backup_created": "Бэкап создан: {name}",
|
||||
"backup_failed": "Ошибка бэкапа: {e}",
|
||||
@@ -1294,11 +1300,6 @@ _ZH = {
|
||||
|
||||
# Setup
|
||||
"agent_integration": "AI代理集成",
|
||||
"agent_desc": (
|
||||
"完成设置后,Claude Code 和 Codex 都可以通过共享的本地技能来管理您的服务器。\n"
|
||||
"ServerManager、Claude Code 和 Codex 共用同一个 servers.json — 在此添加服务器后,\n"
|
||||
"两个代理都会立即看到。"
|
||||
),
|
||||
"claude_integration": "Claude Code集成",
|
||||
"claude_desc": (
|
||||
"设置一切以便Claude Code通过/ssh技能管理您的服务器。\n"
|
||||
@@ -1310,10 +1311,17 @@ _ZH = {
|
||||
"status_servers_json": "servers.json",
|
||||
"status_ssh_script": "ssh.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": "安装中...",
|
||||
@@ -1321,6 +1329,7 @@ _ZH = {
|
||||
"install_skill": "/ssh技能",
|
||||
"install_claude_skill": "Claude 技能",
|
||||
"install_codex_skill": "Codex 技能",
|
||||
"install_gemini_skill": "Gemini 技能",
|
||||
"install_ssh_key": "SSH密钥",
|
||||
"refresh": "刷新",
|
||||
"configuration": "配置",
|
||||
@@ -1330,7 +1339,7 @@ _ZH = {
|
||||
"select_backup": "选择备份...",
|
||||
"no_backups": "无备份",
|
||||
"restore": "恢复",
|
||||
"install_done": "完成!Claude Code 和 Codex 现在都可以使用 ServerManager 来管理您的服务器。",
|
||||
"install_done": "完成!Claude Code、Codex 和 Gemini 现在都可以使用 ServerManager 来管理您的服务器。",
|
||||
"config_changed": "配置路径已更改:{path}",
|
||||
"backup_created": "备份已创建:{name}",
|
||||
"backup_failed": "备份失败:{e}",
|
||||
|
||||
@@ -14,6 +14,7 @@ from core.claude_setup import (
|
||||
install_all,
|
||||
install_claude_skill,
|
||||
install_codex_skill,
|
||||
install_gemini_skill,
|
||||
install_ssh_script,
|
||||
)
|
||||
from core.i18n import t
|
||||
@@ -63,6 +64,8 @@ class SetupTab(ctk.CTkFrame):
|
||||
("claude_skill_installed", "status_claude_skill"),
|
||||
("codex_skill_installed", "status_codex_skill"),
|
||||
("codex_wrapper_installed", "status_codex_wrapper"),
|
||||
("gemini_skill_installed", "status_gemini_skill"),
|
||||
("gemini_wrapper_installed", "status_gemini_wrapper"),
|
||||
("ssh_key_exists", "status_ssh_key"),
|
||||
]
|
||||
for key, i18n_key in status_items:
|
||||
@@ -112,6 +115,12 @@ class SetupTab(ctk.CTkFrame):
|
||||
)
|
||||
self.codex_skill_btn.pack(side="left", padx=5)
|
||||
|
||||
self.gemini_skill_btn = make_icon_button(
|
||||
top_btn_row, "confirm", t("install_gemini_skill"), width=130, fg_color="#6b7280",
|
||||
command=self._install_gemini_skill
|
||||
)
|
||||
self.gemini_skill_btn.pack(side="left", padx=5)
|
||||
|
||||
bottom_btn_row = ctk.CTkFrame(ind_frame, fg_color="transparent")
|
||||
bottom_btn_row.pack(fill="x")
|
||||
|
||||
@@ -370,6 +379,11 @@ class SetupTab(ctk.CTkFrame):
|
||||
self._log(msg)
|
||||
self._refresh_status()
|
||||
|
||||
def _install_gemini_skill(self):
|
||||
msg = install_gemini_skill()
|
||||
self._log(msg)
|
||||
self._refresh_status()
|
||||
|
||||
def _install_skill(self):
|
||||
msg = install_claude_skill()
|
||||
self._log(msg)
|
||||
|
||||
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.43"
|
||||
__version__ = "1.9.44"
|
||||
__app_name__ = "ServerManager"
|
||||
__author__ = "aibot777"
|
||||
__description__ = "Desktop GUI for managing remote servers"
|
||||
|
||||
Reference in New Issue
Block a user