From 9da3125c3441589c82a011ac3498c1dcfd78ea5a Mon Sep 17 00:00:00 2001 From: Codex Date: Wed, 11 Mar 2026 19:30:27 +0000 Subject: [PATCH] feat: add Gemini skill integration and multi-user AI setup --- .codex/skills/server-manager/SKILL.md | 4 +- .../server-manager/references/project.md | 6 +- .gemini/settings.json | 8 + .gemini/skills/server-manager/SKILL.md | 84 ++++ .../references/command-matrix.md | 91 ++++ .../server-manager/references/project.md | 73 +++ .../scripts/gemini-ssh-wrapper.cmd | 11 + .../scripts/gemini-ssh-wrapper.sh | 13 + .../scripts/server-manager-gemini-doctor.cmd | 39 ++ .../scripts/server-manager-gemini-doctor.sh | 37 ++ GEMINI.md | 52 +++ build.py | 6 + core/claude_setup.py | 434 +++++++++++++---- core/i18n.py | 41 +- gui/tabs/setup_tab.py | 14 + test_ai_setup.py | 93 ++++ tools/install.sh | 435 ++++++++++++------ tools/install_ai_integrations.py | 48 ++ 18 files changed, 1239 insertions(+), 250 deletions(-) create mode 100644 .gemini/settings.json create mode 100644 .gemini/skills/server-manager/SKILL.md create mode 100644 .gemini/skills/server-manager/references/command-matrix.md create mode 100644 .gemini/skills/server-manager/references/project.md create mode 100644 .gemini/skills/server-manager/scripts/gemini-ssh-wrapper.cmd create mode 100644 .gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh create mode 100644 .gemini/skills/server-manager/scripts/server-manager-gemini-doctor.cmd create mode 100644 .gemini/skills/server-manager/scripts/server-manager-gemini-doctor.sh create mode 100644 GEMINI.md create mode 100644 test_ai_setup.py create mode 100644 tools/install_ai_integrations.py diff --git a/.codex/skills/server-manager/SKILL.md b/.codex/skills/server-manager/SKILL.md index d93d448..fbc9c11 100644 --- a/.codex/skills/server-manager/SKILL.md +++ b/.codex/skills/server-manager/SKILL.md @@ -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 diff --git a/.codex/skills/server-manager/references/project.md b/.codex/skills/server-manager/references/project.md index d1ae527..543361d 100644 --- a/.codex/skills/server-manager/references/project.md +++ b/.codex/skills/server-manager/references/project.md @@ -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 diff --git a/.gemini/settings.json b/.gemini/settings.json new file mode 100644 index 0000000..ba45186 --- /dev/null +++ b/.gemini/settings.json @@ -0,0 +1,8 @@ +{ + "context": { + "fileName": "GEMINI.md" + }, + "experimental": { + "enableAgents": true + } +} diff --git a/.gemini/skills/server-manager/SKILL.md b/.gemini/skills/server-manager/SKILL.md new file mode 100644 index 0000000..9f78ab4 --- /dev/null +++ b/.gemini/skills/server-manager/SKILL.md @@ -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. diff --git a/.gemini/skills/server-manager/references/command-matrix.md b/.gemini/skills/server-manager/references/command-matrix.md new file mode 100644 index 0000000..458f421 --- /dev/null +++ b/.gemini/skills/server-manager/references/command-matrix.md @@ -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" +``` diff --git a/.gemini/skills/server-manager/references/project.md b/.gemini/skills/server-manager/references/project.md new file mode 100644 index 0000000..cc24280 --- /dev/null +++ b/.gemini/skills/server-manager/references/project.md @@ -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`. diff --git a/.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.cmd b/.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.cmd new file mode 100644 index 0000000..3082043 --- /dev/null +++ b/.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.cmd @@ -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%" %* diff --git a/.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh b/.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh new file mode 100644 index 0000000..723f665 --- /dev/null +++ b/.gemini/skills/server-manager/scripts/gemini-ssh-wrapper.sh @@ -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" "$@" diff --git a/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.cmd b/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.cmd new file mode 100644 index 0000000..30e3557 --- /dev/null +++ b/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.cmd @@ -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% diff --git a/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.sh b/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.sh new file mode 100644 index 0000000..1ae9758 --- /dev/null +++ b/.gemini/skills/server-manager/scripts/server-manager-gemini-doctor.sh @@ -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" diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..0f766fd --- /dev/null +++ b/GEMINI.md @@ -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 +``` diff --git a/build.py b/build.py index c050a92..73effd5 100644 --- a/build.py +++ b/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 = [] diff --git a/core/claude_setup.py b/core/claude_setup.py index d05cf5f..a7153fa 100644 --- a/core/claude_setup.py +++ b/core/claude_setup.py @@ -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 = "" _BLOCK_END = "" +_GEMINI_BLOCK_START = "" +_GEMINI_BLOCK_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 diff --git a/core/i18n.py b/core/i18n.py index 02ed086..8527dd1 100644 --- a/core/i18n.py +++ b/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}", diff --git a/gui/tabs/setup_tab.py b/gui/tabs/setup_tab.py index 0828186..ff3e2ba 100644 --- a/gui/tabs/setup_tab.py +++ b/gui/tabs/setup_tab.py @@ -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) diff --git a/test_ai_setup.py b/test_ai_setup.py new file mode 100644 index 0000000..8b4754c --- /dev/null +++ b/test_ai_setup.py @@ -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() diff --git a/tools/install.sh b/tools/install.sh index 13bd77b..1130583 100644 --- a/tools/install.sh +++ b/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 </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 '/dev/null; then + if [[ -s "$dst" ]] && ! head -1 "$dst" | grep -qi '/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 '/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 diff --git a/tools/install_ai_integrations.py b/tools/install_ai_integrations.py new file mode 100644 index 0000000..18d3d0d --- /dev/null +++ b/tools/install_ai_integrations.py @@ -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())