418 lines
15 KiB
Bash
418 lines
15 KiB
Bash
#!/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 <<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
|
||
|
||
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
|
||
|
||
echo -e "${CYAN}"
|
||
echo "╔══════════════════════════════════════════════════════╗"
|
||
echo "║ ServerManager AI Integration Installer (headless) ║"
|
||
echo "║ Claude + Codex + Gemini ║"
|
||
echo "╚══════════════════════════════════════════════════════╝"
|
||
echo -e "${NC}"
|
||
|
||
step "1/5 Проверка Python"
|
||
|
||
PYTHON=""
|
||
for cmd in python3 python; do
|
||
if command -v "$cmd" &>/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 '<!doctype\|<html'; then
|
||
chmod "$perms" "$dst" 2>/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 '<!doctype\|<html'; then
|
||
chmod "$perms" "$dst" 2>/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
|