#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────── # ServerManager AI Integration Installer for Linux/macOS (headless / no-GUI) # # 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/ # # 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 --source-dir /path/to/server-manager --target-home /root # bash install.sh --all-users --source-dir /path/to/server-manager # ───────────────────────────────────────────────────────────────────── set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*"; } step() { echo -e "\n${CYAN}━━━ $* ━━━${NC}"; } usage() { cat </dev/null; 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 fi fi done if [[ -z "$PYTHON" ]]; then error "Python 3.8+ не найден" exit 1 fi PIP="" for cmd in pip3 pip; do if command -v "$cmd" &>/dev/null; then PIP="$cmd" break fi done if [[ -z "$PIP" ]]; then if $PYTHON -m pip --version &>/dev/null; then PIP="$PYTHON -m pip" else error "pip не найден" exit 1 fi fi ok "pip найден: $($PIP --version 2>&1 | head -1)" 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" "pymysql>=1.1.0" "psycopg2-binary>=2.9.9" "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 ok "$pkg уже установлен" else info "Устанавливаю $dep..." if $PIP install "$dep" --quiet 2>/dev/null; then ok "$dep установлен" else warn "Не удалось установить $dep (попробуйте: $PIP install $dep)" fi fi done copy_or_download() { local src_relative="$1" local dst="$2" local perms="$3" local desc="$4" mkdir -p "$(dirname "$dst")" if [[ -n "$SRC_DIR" && -f "$SRC_DIR/$src_relative" ]]; then cp "$SRC_DIR/$src_relative" "$dst" chmod "$perms" "$dst" 2>/dev/null || true ok "$desc (из $SRC_DIR)" return 0 fi local url="$GITEA_RAW/$src_relative" if command -v curl &>/dev/null; then if curl -fsSL -o "$dst" "$url" 2>/dev/null; then if [[ -s "$dst" ]] && ! head -1 "$dst" | grep -qi '/dev/null || true ok "$desc (скачан с Gitea)" return 0 fi rm -f "$dst" 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 rm -f "$dst" fi fi warn "$desc — не удалось скачать. Скопируйте вручную." return 1 } 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 } discover_homes() { local homes=() local uname_s uname_s="$(uname -s 2>/dev/null || echo Linux)" 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 homes+=("$TARGET_HOME") fi printf '%s\n' "${homes[@]}" | awk 'NF && !seen[$0]++' } step "3/5 Подготовка директорий" TARGET_HOMES=() while IFS= read -r home; do [[ -n "$home" ]] || continue TARGET_HOMES+=("$home") ok "target home: $home" done < <(discover_homes) if [[ "${#TARGET_HOMES[@]}" -eq 0 ]]; then error "Не удалось определить target home" exit 1 fi step "4/5 Установка файлов" CODEX_SKILL_FILES=( "SKILL.md" "references/command-matrix.md" "references/project.md" "scripts/codex-ssh-wrapper.sh" "scripts/codex-ssh-wrapper.cmd" "scripts/server-manager-doctor.sh" "scripts/server-manager-doctor.cmd" ) GEMINI_SKILL_FILES=( "SKILL.md" "references/command-matrix.md" "references/project.md" "scripts/gemini-ssh-wrapper.sh" "scripts/gemini-ssh-wrapper.cmd" "scripts/server-manager-gemini-doctor.sh" "scripts/server-manager-gemini-doctor.cmd" ) for HOME_DIR in "${TARGET_HOMES[@]}"; do CONN_DIR="$HOME_DIR/.server-connections" CLAUDE_DIR="$HOME_DIR/.claude" COMMANDS_DIR="$CLAUDE_DIR/commands" CODEX_DIR="$HOME_DIR/.codex/skills/server-manager" GEMINI_DIR="$HOME_DIR/.gemini" GEMINI_SKILL_DIR="$GEMINI_DIR/skills/server-manager" AGENTS_DIR="$HOME_DIR/.agents/skills/server-manager" mkdir -p "$CONN_DIR" "$COMMANDS_DIR" "$CODEX_DIR" "$GEMINI_SKILL_DIR" chmod 700 "$CONN_DIR" 2>/dev/null || true info "Устанавливаю в $HOME_DIR" copy_or_download "tools/ssh.py" "$CONN_DIR/ssh.py" 755 "ssh.py" copy_or_download "core/encryption.py" "$CONN_DIR/encryption.py" 644 "encryption.py" copy_or_download "tools/skill-ssh.md" "$COMMANDS_DIR/ssh.md" 644 "ssh.md (скилл /ssh)" if [[ -n "$SRC_DIR" && -f "$SRC_DIR/CLAUDE.md" ]]; then cp "$SRC_DIR/CLAUDE.md" "$CLAUDE_DIR/CLAUDE.md" 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 "" if $ALL_OK; then echo -e "${GREEN}Установка завершена успешно!${NC}" else echo -e "${YELLOW}Установка завершена с предупреждениями.${NC}" fi echo "" echo "Установлено для home:" printf ' - %s\n' "${TARGET_HOMES[@]}" echo "" echo "Использование:" echo " python3 ~/.server-connections/ssh.py --list" 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