#!/usr/bin/env bash set -uo pipefail # ============================================================ # LiveServer-M1 — Universal Setup / Update / Doctor Script # ============================================================ # Usage: # ./setup.sh — интерактивное меню # ./setup.sh dev — обновить + запустить dev-сервер (auto LAN, kill old) # ./setup.sh install — первоначальная установка # ./setup.sh update — обновить проект (git pull + npm + prisma + rebuild) # ./setup.sh doctor — диагностика и автоисправление # ./setup.sh status — статус всех сервисов # ./setup.sh logs [svc] — логи сервиса (по умолчанию app) # ./setup.sh restart — перезапуск всех контейнеров # ./setup.sh reset — полный сброс (с подтверждением) # ============================================================ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" # --- Colors --- RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' CYAN='\033[0;36m' NC='\033[0m' BOLD='\033[1m' DIM='\033[2m' # --- Logging --- log_step() { echo -e "\n${BLUE}[$(date +%H:%M:%S)]${NC} ${BOLD}$1${NC}"; } log_ok() { echo -e " ${GREEN}✓${NC} $1"; } log_warn() { echo -e " ${YELLOW}⚠${NC} $1"; } log_err() { echo -e " ${RED}✗${NC} $1"; } log_info() { echo -e " ${CYAN}→${NC} $1"; } log_fix() { echo -e " ${GREEN}⚙${NC} ${BOLD}FIX:${NC} $1"; } # --- Helpers --- ask() { local prompt="$1" default="${2:-}" var_name="$3" if [[ -n "$default" ]]; then echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${default}]${NC}: " else echo -en " ${CYAN}?${NC} ${prompt}: " fi read -r input printf -v "$var_name" '%s' "${input:-$default}" } ask_secret() { local prompt="$1" default="${2:-}" var_name="$3" if [[ -n "$default" ]]; then echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${default}]${NC}: " else echo -en " ${CYAN}?${NC} ${prompt}: " fi read -rs input echo "" printf -v "$var_name" '%s' "${input:-$default}" } ask_yn() { local prompt="$1" default="${2:-y}" local hint="Y/n" [[ "$default" == "n" ]] && hint="y/N" echo -en " ${CYAN}?${NC} ${prompt} ${YELLOW}[${hint}]${NC}: " read -r input input="${input:-$default}" [[ "${input,,}" == "y" || "${input,,}" == "yes" ]] } generate_secret() { openssl rand -base64 32 2>/dev/null || head -c 32 /dev/urandom | base64 | tr -d '=/+' | head -c 32 } generate_password() { openssl rand -base64 24 2>/dev/null | tr -d '=/+' | head -c 20 || \ head -c 24 /dev/urandom | base64 | tr -d '=/+' | head -c 20 } # Прочитать переменную из .env env_get() { local key="$1" if [[ -f .env ]]; then grep -E "^${key}=" .env 2>/dev/null | head -1 | cut -d'=' -f2- | sed 's/^"//' | sed 's/"$//' fi } # Определить режим (local/prod) из .env detect_mode() { local domain domain=$(env_get DOMAIN) if [[ -n "$domain" ]]; then echo "prod" else echo "local" fi } # Docker compose wrapper (определяет prod/local) dc() { if [[ "$(detect_mode)" == "prod" ]]; then docker compose -f docker-compose.yml -f docker-compose.prod.yml "$@" else docker compose "$@" fi } # Проверить что сервис запущен и healthy check_service() { local name="$1" local state state=$(docker compose ps --format '{{.State}}' "$name" 2>/dev/null || echo "missing") local health health=$(docker compose ps --format '{{.Health}}' "$name" 2>/dev/null || echo "") if [[ "$state" == "running" ]]; then if [[ "$health" == "healthy" || -z "$health" ]]; then echo "ok" else echo "unhealthy" fi elif [[ "$state" == "missing" || -z "$state" ]]; then echo "stopped" else echo "$state" fi } # Ожидание сервиса с таймаутом wait_for_service() { local name="$1" check_cmd="$2" timeout="${3:-30}" echo -e " Ожидание ${name}..." for i in $(seq 1 "$timeout"); do if eval "$check_cmd" &>/dev/null; then log_ok "${name} готов" return 0 fi sleep 1 done log_err "${name} не ответил за ${timeout}с" return 1 } check_command() { command -v "$1" &>/dev/null } install_if_missing() { local cmd="$1" install_hint="$2" if check_command "$cmd"; then log_ok "$cmd найден: $(command -v "$cmd")" return 0 fi log_warn "$cmd не найден" if [[ "$cmd" == "node" || "$cmd" == "npm" || "$cmd" == "npx" ]]; then if check_command nvm; then echo -e " Установка Node.js через nvm..." nvm install --lts 2>&1 | tail -3 check_command "$cmd" && { log_ok "$cmd установлен через nvm"; return 0; } fi fi case "$cmd" in docker) if check_command apt-get && ask_yn "Установить Docker автоматически (apt)?" "y"; then curl -fsSL https://get.docker.com | sh 2>&1 | tail -5 sudo usermod -aG docker "$USER" 2>/dev/null || true check_command docker && { log_ok "Docker установлен"; return 0; } fi ;; node|npm|npx) if check_command apt-get && ask_yn "Установить Node.js 22 автоматически?" "y"; then curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - 2>&1 | tail -3 sudo apt-get install -y nodejs 2>&1 | tail -3 check_command "$cmd" && { log_ok "$cmd установлен"; return 0; } elif check_command brew && ask_yn "Установить Node.js через Homebrew?" "y"; then brew install node 2>&1 | tail -3 check_command "$cmd" && { log_ok "$cmd установлен"; return 0; } fi ;; esac log_err "$cmd не удалось установить автоматически" echo -e " ${YELLOW}Установи вручную:${NC} $install_hint" return 1 } # ============================================================ # Banners # ============================================================ print_banner() { echo "" echo -e "${CYAN}╔══════════════════════════════════════════════╗${NC}" echo -e "${CYAN}║${NC} ${BOLD}LiveServer-M1${NC} — Setup & Doctor ${CYAN}║${NC}" echo -e "${CYAN}║${NC} Образовательная видеоконференц-платформа ${CYAN}║${NC}" echo -e "${CYAN}╚══════════════════════════════════════════════╝${NC}" echo "" } print_usage() { echo -e " ${BOLD}Команды:${NC}" echo -e " ${CYAN}install${NC} Первоначальная установка" echo -e " ${CYAN}update${NC} Обновить проект (git pull + npm + prisma + rebuild)" echo -e " ${CYAN}dev${NC} Обновить + запустить dev-сервер (auto LAN, kill old)" echo -e " ${CYAN}doctor${NC} Диагностика и автоисправление проблем" echo -e " ${CYAN}status${NC} Статус всех сервисов" echo -e " ${CYAN}admin${NC} Создать администратора" echo -e " ${CYAN}logs${NC} [svc] Логи сервиса (по умолчанию: app)" echo -e " ${CYAN}restart${NC} Перезапуск контейнеров" echo -e " ${CYAN}reset${NC} Полный сброс (с подтверждением)" echo "" } # ============================================================ # COMMAND: status # ============================================================ cmd_status() { log_step "Статус сервисов" local services=(postgres redis pgbouncer minio app ai-agent) local all_ok=true printf " %-14s %-12s %-10s %s\n" "СЕРВИС" "СОСТОЯНИЕ" "ПОРТ" "ДЕТАЛИ" printf " %-14s %-12s %-10s %s\n" "──────" "─────────" "────" "──────" for svc in "${services[@]}"; do local state state=$(check_service "$svc") local port="" details="" case "$svc" in postgres) port="5432" ;; redis) port="6379" ;; pgbouncer) port="6432" ;; minio) port="9000/9001" ;; app) port="3000" ;; ai-agent) port="—" ;; esac local color="$RED" local icon="✗" case "$state" in ok) color="$GREEN"; icon="✓" ;; unhealthy) color="$YELLOW"; icon="⚠"; all_ok=false ;; *) color="$RED"; icon="✗"; all_ok=false ;; esac # Доп. детали if [[ "$state" == "ok" ]]; then case "$svc" in postgres) local db_size db_size=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c "SELECT pg_size_pretty(pg_database_size('liveserver'));" 2>/dev/null | tr -d ' ' || echo "?") details="size: ${db_size}" ;; redis) local keys keys=$(docker compose exec -T redis redis-cli dbsize 2>/dev/null | grep -oP '\d+' || echo "?") details="keys: ${keys}" ;; minio) details="bucket: $(env_get S3_BUCKET)" ;; esac fi printf " ${color}${icon}${NC} %-12s %-12s %-10s %s\n" "$svc" "$state" "$port" "$details" done # .env check echo "" if [[ -f .env ]]; then log_ok ".env существует" local domain domain=$(env_get DOMAIN) if [[ -n "$domain" ]]; then log_info "Режим: ${BOLD}Production${NC} (${domain})" else log_info "Режим: ${BOLD}Локальная разработка${NC}" fi else log_err ".env не найден — запусти ${CYAN}./setup.sh install${NC}" fi # node_modules check if [[ -d "node_modules" ]]; then log_ok "node_modules установлен" else log_warn "node_modules отсутствует" fi # .next check if [[ -d ".next" ]]; then log_ok ".next кеш существует" else log_info ".next кеш отсутствует (нормально до первого запуска)" fi if $all_ok; then echo "" log_ok "${GREEN}Все сервисы работают${NC}" else echo "" log_warn "Есть проблемы — запусти ${CYAN}./setup.sh doctor${NC}" fi } # ============================================================ # COMMAND: doctor # ============================================================ cmd_doctor() { log_step "Диагностика LiveServer-M1" local issues=0 local fixed=0 # --- 1. Системные зависимости --- log_step "1/8 — Системные зависимости" for cmd in docker node npm; do if check_command "$cmd"; then log_ok "$cmd: $(command -v "$cmd")" else log_err "$cmd не найден" issues=$((issues + 1)) install_if_missing "$cmd" "см. документацию" && fixed=$((fixed + 1)) || true fi done if check_command docker; then if docker compose version &>/dev/null; then log_ok "docker compose: $(docker compose version --short 2>/dev/null)" else log_err "docker compose V2 не найден" issues=$((issues + 1)) fi if docker info &>/dev/null; then log_ok "Docker daemon запущен" else log_err "Docker daemon не запущен" issues=$((issues + 1)) log_fix "Запускаю Docker..." sudo systemctl start docker 2>/dev/null && fixed=$((fixed + 1)) || log_warn "Не удалось запустить Docker" fi fi # --- 2. .env --- log_step "2/8 — Файл .env" if [[ ! -f .env ]]; then log_err ".env не найден" issues=$((issues + 1)) if ask_yn "Запустить полную установку?" "y"; then cmd_install return fi else log_ok ".env существует" # Проверить обязательные переменные local required_vars=(DATABASE_URL POSTGRES_PASSWORD REDIS_URL BETTER_AUTH_SECRET S3_ENDPOINT S3_ACCESS_KEY S3_SECRET_KEY MINIO_ROOT_USER MINIO_ROOT_PASSWORD) for var in "${required_vars[@]}"; do local val val=$(env_get "$var") if [[ -z "$val" ]]; then log_err "${var} не задан в .env" issues=$((issues + 1)) fi done # Проверить что пароли не дефолтные (для prod) if [[ "$(detect_mode)" == "prod" ]]; then local pg_pass pg_pass=$(env_get POSTGRES_PASSWORD) if [[ "$pg_pass" == "postgres" ]]; then log_warn "POSTGRES_PASSWORD = 'postgres' в production — небезопасно!" issues=$((issues + 1)) fi local minio_pass minio_pass=$(env_get MINIO_ROOT_PASSWORD) if [[ "$minio_pass" == "minioadmin" ]]; then log_warn "MINIO_ROOT_PASSWORD = 'minioadmin' в production — небезопасно!" issues=$((issues + 1)) fi fi # Проверить что .env.example не содержит новых переменных if [[ -f .env.example ]]; then local missing_in_env=0 while IFS= read -r line; do # Пропустить пустые строки и комментарии [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && continue # Извлечь ключ (до первого =) local key="${line%%=*}" # Убрать пробелы и проверить что это валидное имя переменной key=$(echo "$key" | tr -d '[:space:]') [[ -z "$key" || ! "$key" =~ ^[A-Z_][A-Z0-9_]*$ ]] && continue if ! grep -qE "^${key}=" .env 2>/dev/null; then if [[ "$missing_in_env" -eq 0 ]]; then log_warn "Новые переменные из .env.example отсутствуют в .env:" fi echo -e " ${YELLOW}${key}${NC}" missing_in_env=1 fi done < .env.example [[ "$missing_in_env" -eq 1 ]] && issues=$((issues + 1)) fi fi # --- 3. package.json --- log_step "3/8 — package.json и зависимости" if [[ -f package.json ]]; then # Проверить "type" поле — оно не должно быть if grep -q '"type"' package.json; then local type_val type_val=$(node -e "console.log(require('./package.json').type || '')" 2>/dev/null || echo "") if [[ -n "$type_val" ]]; then log_err "package.json содержит \"type\": \"${type_val}\" — вызывает ESM/CJS конфликт с Turbopack" issues=$((issues + 1)) log_fix "Удаляю поле \"type\" из package.json..." node -e " const fs = require('fs'); const pkg = JSON.parse(fs.readFileSync('package.json','utf8')); delete pkg.type; fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); " 2>/dev/null && { log_ok "\"type\" удалён"; fixed=$((fixed + 1)); } || log_warn "Не удалось автоисправить" fi else log_ok "package.json: нет конфликтного поля \"type\"" fi # node_modules if [[ ! -d "node_modules" ]]; then log_warn "node_modules отсутствует" issues=$((issues + 1)) log_fix "Запускаю npm install..." npm install 2>&1 | tail -5 log_ok "npm install завершён" fixed=$((fixed + 1)) elif [[ "package.json" -nt "node_modules/.package-lock.json" ]] 2>/dev/null; then log_warn "node_modules устарел (package.json новее)" issues=$((issues + 1)) log_fix "Запускаю npm install..." npm install 2>&1 | tail -5 log_ok "npm install обновлён" fixed=$((fixed + 1)) else log_ok "node_modules актуален" fi # Prisma client if [[ -d "node_modules/.prisma/client" ]] || [[ -d "node_modules/@prisma/client" ]]; then log_ok "Prisma Client сгенерирован" else log_warn "Prisma Client не найден" issues=$((issues + 1)) log_fix "Генерирую Prisma Client..." npx prisma generate 2>&1 | tail -2 fixed=$((fixed + 1)) fi else log_err "package.json не найден — это не корень проекта?" issues=$((issues + 1)) fi # --- 4. Docker контейнеры --- log_step "4/8 — Docker контейнеры" local infra_services=(postgres redis pgbouncer minio) local stopped_services=() for svc in "${infra_services[@]}"; do local state state=$(check_service "$svc") case "$state" in ok) log_ok "${svc}: работает" ;; unhealthy) log_warn "${svc}: unhealthy" issues=$((issues + 1)) log_fix "Перезапускаю ${svc}..." docker compose restart "$svc" 2>/dev/null sleep 3 local new_state new_state=$(check_service "$svc") if [[ "$new_state" == "ok" ]]; then log_ok "${svc}: восстановлен" fixed=$((fixed + 1)) else log_err "${svc}: всё ещё проблема после рестарта" fi ;; *) log_err "${svc}: ${state}" issues=$((issues + 1)) stopped_services+=("$svc") ;; esac done if [[ ${#stopped_services[@]} -gt 0 ]]; then log_fix "Запускаю остановленные сервисы: ${stopped_services[*]}..." dc up -d "${stopped_services[@]}" 2>&1 | tail -3 sleep 5 for svc in "${stopped_services[@]}"; do local new_state new_state=$(check_service "$svc") if [[ "$new_state" == "ok" ]]; then log_ok "${svc}: запущен" fixed=$((fixed + 1)) else log_err "${svc}: не удалось запустить" fi done fi # --- 5. Подключения к БД --- log_step "5/8 — Подключение к базе данных" if [[ "$(check_service postgres)" == "ok" ]]; then # Прямое подключение if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then log_ok "PostgreSQL: pg_isready OK" else log_err "PostgreSQL: pg_isready FAIL" issues=$((issues + 1)) fi # Проверить что БД liveserver существует if docker compose exec -T postgres psql -U postgres -d liveserver -c "SELECT 1;" &>/dev/null; then log_ok "БД 'liveserver' существует" else log_warn "БД 'liveserver' не найдена" issues=$((issues + 1)) log_fix "Создаю БД..." docker compose exec -T postgres createdb -U postgres liveserver 2>/dev/null && fixed=$((fixed + 1)) || true fi # Проверить схему (есть ли таблицы) local table_count table_count=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';" 2>/dev/null | tr -d ' ' || echo "0") if [[ "$table_count" -gt 0 ]]; then log_ok "Схема БД: ${table_count} таблиц" else log_warn "Схема БД пуста" issues=$((issues + 1)) log_fix "Применяю Prisma schema..." npx prisma db push --skip-generate --accept-data-loss 2>&1 | tail -3 fixed=$((fixed + 1)) fi # PgBouncer if [[ "$(check_service pgbouncer)" == "ok" ]]; then if docker compose exec -T pgbouncer sh -c 'echo | nc -w 2 127.0.0.1 6432' &>/dev/null; then log_ok "PgBouncer: порт 6432 отвечает" else log_warn "PgBouncer: порт 6432 не отвечает" issues=$((issues + 1)) fi fi else log_err "PostgreSQL не запущен — пропускаю проверки БД" fi # --- 6. Redis --- log_step "6/8 — Redis" if [[ "$(check_service redis)" == "ok" ]]; then if docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG; then log_ok "Redis PING → PONG" else log_err "Redis не отвечает на PING" issues=$((issues + 1)) fi local redis_mem redis_mem=$(docker compose exec -T redis redis-cli info memory 2>/dev/null | grep used_memory_human | cut -d: -f2 | tr -d '[:space:]' || echo "?") log_info "Redis memory: ${redis_mem}" local redis_keys redis_keys=$(docker compose exec -T redis redis-cli dbsize 2>/dev/null | grep -oP '\d+' || echo "?") log_info "Redis keys: ${redis_keys}" else log_err "Redis не запущен" fi # --- 7. MinIO --- log_step "7/8 — MinIO (S3 Storage)" if [[ "$(check_service minio)" == "ok" ]]; then log_ok "MinIO контейнер запущен" local minio_user minio_user=$(env_get MINIO_ROOT_USER) local minio_pass minio_pass=$(env_get MINIO_ROOT_PASSWORD) local bucket bucket=$(env_get S3_BUCKET) bucket="${bucket:-liveserver}" # Проверить healthcheck через docker exec (не зависит от пробросa портов) if docker compose exec -T minio curl -sf http://localhost:9000/minio/health/live &>/dev/null; then log_ok "MinIO health: live" else log_warn "MinIO health endpoint не отвечает" issues=$((issues + 1)) fi # Проверить/создать bucket через docker exec mc if docker compose exec -T minio mc alias set local http://localhost:9000 "$minio_user" "$minio_pass" &>/dev/null; then if docker compose exec -T minio mc ls "local/${bucket}" &>/dev/null; then log_ok "MinIO: bucket '${bucket}' существует" else log_warn "MinIO: bucket '${bucket}' не найден" issues=$((issues + 1)) log_fix "Создаю bucket..." if docker compose exec -T minio mc mb "local/${bucket}" &>/dev/null; then log_ok "Bucket '${bucket}' создан" fixed=$((fixed + 1)) else log_warn "Не удалось создать bucket" fi fi else log_warn "MinIO: mc недоступен в контейнере — проверка bucket пропущена" issues=$((issues + 1)) fi else log_err "MinIO не запущен" fi # --- 8. .next и кеш --- log_step "8/8 — Кеш и сборка" if [[ -d ".next" ]]; then local next_size next_size=$(du -sh .next 2>/dev/null | cut -f1 || echo "?") log_ok ".next кеш: ${next_size}" # Проверить что .next не битый if [[ -f ".next/BUILD_ID" ]]; then log_ok "BUILD_ID: $(cat .next/BUILD_ID)" else log_warn ".next может быть повреждён (нет BUILD_ID)" issues=$((issues + 1)) if ask_yn "Очистить .next кеш?" "y"; then rm -rf .next log_fix "Кеш очищен" fixed=$((fixed + 1)) fi fi else log_info ".next кеш отсутствует (создастся при npm run dev / npm run build)" fi # Проверить lock file конфликт if [[ -f "package-lock.json" ]] && [[ -f "yarn.lock" ]]; then log_warn "Найдены И package-lock.json И yarn.lock — выбери один менеджер" issues=$((issues + 1)) fi # --- Итого --- echo "" echo -e "${CYAN}══════════════════════════════════════════════${NC}" if [[ "$issues" -eq 0 ]]; then echo -e " ${GREEN}${BOLD}Всё в порядке! Проблем не найдено.${NC}" else local remaining=$((issues - fixed)) echo -e " Найдено проблем: ${YELLOW}${issues}${NC}" echo -e " Автоисправлено: ${GREEN}${fixed}${NC}" if [[ "$remaining" -gt 0 ]]; then echo -e " Осталось: ${RED}${remaining}${NC}" echo "" echo -e " ${YELLOW}Исправь оставшиеся проблемы вручную или запусти doctor ещё раз${NC}" else echo -e " ${GREEN}${BOLD}Все проблемы исправлены!${NC}" fi fi echo -e "${CYAN}══════════════════════════════════════════════${NC}" echo "" } # ============================================================ # COMMAND: update # ============================================================ cmd_update() { log_step "Обновление LiveServer-M1" # 1. Git pull log_step "1/5 — Обновление кода" if [[ -d .git ]]; then local branch branch=$(git branch --show-current 2>/dev/null || echo "unknown") log_info "Текущая ветка: ${branch}" # Проверить uncommitted changes local skip_pull=false if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then log_warn "Есть незакоммиченные изменения" if ask_yn "Сохранить через git stash и продолжить?" "y"; then git stash push -m "auto-stash before update $(date +%Y%m%d_%H%M%S)" 2>&1 log_ok "Изменения сохранены в stash" else log_warn "Пропускаю git pull" skip_pull=true fi fi if [[ "$skip_pull" == "false" ]]; then git pull --rebase 2>&1 | tail -5 log_ok "git pull завершён" fi else log_warn "Не git-репозиторий — пропускаю git pull" fi # 2. npm install log_step "2/5 — Зависимости" npm install 2>&1 | tail -5 log_ok "npm install завершён" # 3. Prisma log_step "3/5 — База данных" npx prisma generate 2>&1 | tail -2 log_ok "Prisma Client обновлён" # Проверить нужна ли миграция if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then log_info "Применяю schema changes..." npx prisma db push --skip-generate --accept-data-loss 2>&1 | tail -3 log_ok "Схема БД синхронизирована" else log_warn "PostgreSQL недоступен — миграция пропущена" fi # 4. Пересборка контейнеров log_step "4/5 — Пересборка контейнеров" local mode mode=$(detect_mode) if [[ "$mode" == "prod" ]]; then log_info "Production mode — пересборка app + ai-agent" dc up -d --build app ai-agent 2>&1 | tail -5 else log_info "Local mode — пересборка инфраструктуры" dc up -d postgres minio redis pgbouncer 2>&1 | tail -5 fi log_ok "Контейнеры обновлены" # 5. Очистка .next log_step "5/5 — Очистка кеша" if [[ -d ".next" ]]; then rm -rf .next log_ok ".next кеш очищен (пересоберётся при запуске)" else log_ok "Кеш чист" fi echo "" log_ok "${GREEN}${BOLD}Обновление завершено!${NC}" if [[ "$mode" == "local" ]]; then echo -e " Запусти: ${CYAN}npm run dev${NC}" else echo -e " Приложение должно быть доступно через несколько секунд" fi echo "" } # ============================================================ # COMMAND: admin — создание админа # ============================================================ cmd_admin() { log_step "Создание администратора" # Проверить что PostgreSQL доступен if ! docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then log_err "PostgreSQL не запущен. Запусти: ./setup.sh doctor" return 1 fi # Проверить что таблица users существует if ! docker compose exec -T postgres psql -U postgres -d liveserver -c "SELECT 1 FROM users LIMIT 0;" &>/dev/null; then log_err "Таблица users не найдена. Запусти: npx prisma db push" return 1 fi # Показать существующих админов local existing_admins existing_admins=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c \ "SELECT email FROM users WHERE role = 'ADMIN';" 2>/dev/null | tr -d ' ' | grep -v '^$' || true) if [[ -n "$existing_admins" ]]; then echo "" log_info "Существующие админы:" echo "$existing_admins" | while read -r admin_email; do echo -e " ${CYAN}${admin_email}${NC}" done echo "" fi # Спросить данные local ADMIN_NAME ADMIN_EMAIL ADMIN_PASS echo "" ask "Имя администратора" "Admin" ADMIN_NAME ask "Email" "" ADMIN_EMAIL ask_secret "Пароль (мин. 8 символов)" "" ADMIN_PASS # Валидация if [[ -z "$ADMIN_EMAIL" ]]; then log_err "Email обязателен" return 1 fi if [[ ${#ADMIN_PASS} -lt 8 ]]; then log_err "Пароль должен быть минимум 8 символов" return 1 fi # Проверить что email не занят local exists exists=$(docker compose exec -T postgres psql -U postgres -d liveserver -t -c \ "SELECT count(*) FROM users WHERE email = '${ADMIN_EMAIL}';" 2>/dev/null | tr -d ' ') if [[ "$exists" -gt 0 ]]; then echo "" if ask_yn "Пользователь ${ADMIN_EMAIL} уже существует. Сделать админом?" "y"; then docker compose exec -T postgres psql -U postgres -d liveserver -c \ "UPDATE users SET role = 'ADMIN' WHERE email = '${ADMIN_EMAIL}';" &>/dev/null log_ok "${ADMIN_EMAIL} теперь ADMIN" return 0 else log_info "Отменено" return 0 fi fi # Хешировать пароль через node + bcryptjs (уже в зависимостях) log_info "Хеширование пароля..." local HASH HASH=$(node -e " const bcrypt = require('bcryptjs'); const hash = bcrypt.hashSync(process.argv[1], 10); process.stdout.write(hash); " "$ADMIN_PASS" 2>/dev/null) if [[ -z "$HASH" ]]; then log_err "Не удалось хешировать пароль (bcryptjs не установлен?)" log_info "Попробуй: npm install && ./setup.sh admin" return 1 fi # Генерировать ID в стиле cuid local USER_ID USER_ID=$(node -e "process.stdout.write(require('crypto').randomBytes(12).toString('hex'))" 2>/dev/null || echo "admin_$(date +%s)") # Создать пользователя docker compose exec -T postgres psql -U postgres -d liveserver -c " INSERT INTO users (id, email, name, \"emailVerified\", role, \"createdAt\", \"updatedAt\") VALUES ('${USER_ID}', '${ADMIN_EMAIL}', '${ADMIN_NAME}', true, 'ADMIN', NOW(), NOW()); " &>/dev/null # Создать аккаунт (better-auth accounts table для email+password) local ACCOUNT_ID ACCOUNT_ID=$(node -e "process.stdout.write(require('crypto').randomBytes(12).toString('hex'))" 2>/dev/null || echo "acc_$(date +%s)") docker compose exec -T postgres psql -U postgres -d liveserver -c " INSERT INTO accounts (id, \"userId\", \"accountId\", \"providerId\", \"password\", \"createdAt\", \"updatedAt\") VALUES ('${ACCOUNT_ID}', '${USER_ID}', '${USER_ID}', 'credential', '${HASH}', NOW(), NOW()); " &>/dev/null if [[ $? -eq 0 ]]; then echo "" log_ok "${GREEN}${BOLD}Администратор создан!${NC}" echo -e " ${CYAN}Email:${NC} ${ADMIN_EMAIL}" echo -e " ${CYAN}Пароль:${NC} (тот что ввёл)" echo -e " ${CYAN}Роль:${NC} ADMIN" echo "" echo -e " Войди через: ${CYAN}http://localhost:3000/login${NC}" else log_err "Не удалось создать аккаунт" fi } # ============================================================ # COMMAND: install (оригинальный setup) # ============================================================ cmd_install() { # --- Step 1: Check & install dependencies --- log_step "Шаг 1/6 — Проверка и установка зависимостей" local MISSING=0 install_if_missing docker "https://docs.docker.com/get-docker/" || MISSING=1 check_command "docker compose" || check_command docker-compose || { log_err "docker compose не найден (нужен Docker Compose V2)" MISSING=1 } install_if_missing node "https://nodejs.org/" || MISSING=1 install_if_missing npm "https://nodejs.org/" || MISSING=1 if [[ "$MISSING" -eq 1 ]]; then log_err "Установи недостающие зависимости и запусти скрипт снова." exit 1 fi local NODE_VER NODE_VER=$(node -v) log_ok "Node.js: $NODE_VER" # --- Step 2: Choose mode --- log_step "Шаг 2/6 — Режим запуска" echo "" echo -e " ${BOLD}1)${NC} Локальная разработка (localhost, без SSL)" echo -e " ${BOLD}2)${NC} Production (домен + Traefik + Let's Encrypt SSL)" echo "" ask "Выбери режим" "1" MODE local DOMAIN="" ACME_EMAIL="" if [[ "$MODE" == "2" ]]; then echo "" ask "Домен (например live.example.com)" "" DOMAIN ask "Email для Let's Encrypt" "" ACME_EMAIL if [[ -z "$DOMAIN" || -z "$ACME_EMAIL" ]]; then log_err "Домен и email обязательны для production." exit 1 fi log_ok "Production: ${DOMAIN}" log_warn "Убедись что DNS записи указывают на этот сервер:" echo -e " ${DOMAIN} → $(curl -s ifconfig.me 2>/dev/null || echo '')" echo -e " s3.${DOMAIN} → тот же IP" echo -e " minio.${DOMAIN} → тот же IP" echo "" if ! ask_yn "DNS настроен?" "y"; then log_warn "Настрой DNS и запусти скрипт снова." exit 0 fi else log_ok "Локальная разработка" fi # --- Step 3: Configure environment --- log_step "Шаг 3/6 — Настройка окружения" local PG_PASSWORD MINIO_USER MINIO_PASS AUTH_SECRET DEV_ACCESS_KEY S3_BUCKET PG_PASSWORD=$(generate_password) MINIO_USER="minio-$(head -c 4 /dev/urandom | xxd -p)" MINIO_PASS=$(generate_password) AUTH_SECRET=$(generate_secret) DEV_ACCESS_KEY=$(generate_password | head -c 16) S3_BUCKET="liveserver" echo "" echo -e " ${GREEN}${BOLD}Пароли сгенерированы автоматически:${NC}" echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" echo -e " ${CYAN}MinIO user:${NC} ${MINIO_USER}" echo -e " ${CYAN}MinIO password:${NC} ${MINIO_PASS}" echo -e " ${CYAN}Auth secret:${NC} ${AUTH_SECRET:0:16}..." if [[ "$MODE" == "1" ]]; then echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" fi echo "" echo -e " ${YELLOW}Все пароли сохранены в .env — можешь изменить позже${NC}" if ask_yn "Хочешь задать свои пароли вместо сгенерированных?" "n"; then echo "" echo -e " ${BOLD}PostgreSQL${NC}" ask "Пароль PostgreSQL" "$PG_PASSWORD" PG_PASSWORD echo "" echo -e " ${BOLD}MinIO (S3 Storage)${NC}" ask "MinIO root user" "$MINIO_USER" MINIO_USER ask_secret "MinIO root password" "$MINIO_PASS" MINIO_PASS ask "Название S3 bucket" "$S3_BUCKET" S3_BUCKET if [[ "$MODE" == "1" ]]; then echo "" echo -e " ${BOLD}Защита локалки${NC}" ask "Ключ доступа (DEV_ACCESS_KEY)" "$DEV_ACCESS_KEY" DEV_ACCESS_KEY fi fi echo "" echo -e " ${BOLD}LiveKit${NC} (Enter чтобы пропустить, настроишь позже)" local LK_URL="" LK_KEY="" LK_SECRET="" LK_WEBHOOK="" ask "LiveKit URL (wss://...)" "" LK_URL if [[ -n "$LK_URL" ]]; then ask "LiveKit API Key" "" LK_KEY ask_secret "LiveKit API Secret" "" LK_SECRET ask_secret "LiveKit Webhook Secret (Enter = пропустить)" "" LK_WEBHOOK fi echo "" echo -e " ${BOLD}AI Agent${NC} (Enter чтобы пропустить, настроишь позже)" local DG_KEY="" OAI_KEY="" ask_secret "Deepgram API Key" "" DG_KEY ask_secret "OpenAI API Key" "" OAI_KEY local ALLOWED_IPS="" if [[ "$MODE" == "1" ]]; then if ask_yn "Ограничить доступ по IP? (кроме localhost)" "n"; then ask "Разрешённые IP (через запятую)" "" ALLOWED_IPS fi fi # --- Step 4: Generate .env --- log_step "Шаг 4/6 — Генерация .env" local APP_URL S3_ENDPOINT if [[ "$MODE" == "2" ]]; then APP_URL="https://${DOMAIN}" S3_ENDPOINT="http://minio:9000" else APP_URL="http://localhost:3000" S3_ENDPOINT="http://localhost:9000" fi if [[ -f .env ]]; then cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)" log_ok "Бэкап старого .env создан" fi cat > .env << ENVEOF # === Generated by setup.sh at $(date) === # Все пароли сгенерированы автоматически — можешь заменить на свои. # Domain & SSL DOMAIN=${DOMAIN} ACME_EMAIL=${ACME_EMAIL} # Database DATABASE_URL=postgresql://postgres:${PG_PASSWORD}@localhost:5432/liveserver POSTGRES_PASSWORD=${PG_PASSWORD} # Redis # Для Docker контейнеров: redis://redis:6379 # Для локального npm run dev: redis://localhost:6379 REDIS_URL=redis://localhost:6379 # LiveKit LIVEKIT_URL=${LK_URL} NEXT_PUBLIC_LIVEKIT_URL=${LK_URL} LIVEKIT_API_KEY=${LK_KEY} LIVEKIT_API_SECRET=${LK_SECRET} # AI Agent DEEPGRAM_API_KEY=${DG_KEY} OPENAI_API_KEY=${OAI_KEY} # Storage (MinIO/S3) S3_ENDPOINT=${S3_ENDPOINT} S3_ACCESS_KEY=${MINIO_USER} S3_SECRET_KEY=${MINIO_PASS} S3_BUCKET=${S3_BUCKET} MINIO_ROOT_USER=${MINIO_USER} MINIO_ROOT_PASSWORD=${MINIO_PASS} # Auth BETTER_AUTH_SECRET=${AUTH_SECRET} BETTER_AUTH_URL=${APP_URL} NEXT_PUBLIC_APP_URL=${APP_URL} # Local dev protection DEV_ACCESS_KEY=${DEV_ACCESS_KEY:-} ALLOWED_IPS=${ALLOWED_IPS:-} ENVEOF log_ok ".env создан" # --- Step 5: Install & setup --- log_step "Шаг 5/6 — Установка и настройка" if [[ ! -d "node_modules" ]]; then echo -e " Установка npm зависимостей..." npm install 2>&1 | tail -5 log_ok "npm install завершён" elif [[ "package.json" -nt "node_modules/.package-lock.json" ]] 2>/dev/null; then echo -e " package.json обновлён, переустановка зависимостей..." npm install 2>&1 | tail -5 log_ok "npm install завершён (обновление)" else log_ok "node_modules актуален" fi echo -e " Генерация Prisma Client..." npx prisma generate 2>&1 | tail -1 log_ok "Prisma Client сгенерирован" echo -e " Запуск Docker контейнеров..." if [[ "$MODE" == "2" ]]; then docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build 2>&1 | tail -5 else docker compose up -d postgres minio redis pgbouncer 2>&1 | tail -5 fi log_ok "Docker контейнеры запущены" wait_for_service "PostgreSQL" "docker compose exec -T postgres pg_isready -U postgres" 30 || exit 1 wait_for_service "Redis" "docker compose exec -T redis redis-cli ping 2>/dev/null | grep -q PONG" 15 || exit 1 wait_for_service "PgBouncer" "docker compose exec -T pgbouncer pg_isready -h 127.0.0.1 -p 6432" 15 || \ log_warn "PgBouncer не отвечает — продолжаем без него" echo -e " Применение миграций..." npx prisma db push --skip-generate --accept-data-loss 2>&1 | tail -3 log_ok "Схема БД синхронизирована" echo -e " Создание S3 bucket..." sleep 2 if check_command mc; then mc alias set local http://localhost:9000 "$MINIO_USER" "$MINIO_PASS" 2>/dev/null || true mc mb "local/${S3_BUCKET}" 2>/dev/null || true log_ok "Bucket '${S3_BUCKET}' создан" else if curl -sf -o /dev/null -X PUT "http://localhost:9000/${S3_BUCKET}" \ -u "${MINIO_USER}:${MINIO_PASS}" 2>/dev/null; then log_ok "Bucket '${S3_BUCKET}' создан через API" else log_warn "Не удалось создать bucket автоматически" echo -e " Создай вручную: ${CYAN}http://localhost:9001${NC}" fi fi # --- Step 6: Done --- log_step "Шаг 6/6 — Готово!" echo "" if [[ "$MODE" == "2" ]]; then echo -e " ${GREEN}${BOLD}Production запущен!${NC}" echo "" echo -e " Приложение: ${CYAN}https://${DOMAIN}${NC}" echo -e " MinIO Console: ${CYAN}https://minio.${DOMAIN}${NC}" echo -e " MinIO S3 API: ${CYAN}https://s3.${DOMAIN}${NC}" else echo -e " ${GREEN}${BOLD}Локальная среда готова!${NC}" echo "" echo -e " Запусти приложение: ${CYAN}npm run dev${NC}" echo -e " Приложение: ${CYAN}http://localhost:3000${NC}" echo -e " MinIO Console: ${CYAN}http://localhost:9001${NC}" echo -e " Prisma Studio: ${CYAN}npx prisma studio${NC}" if [[ -n "${DEV_ACCESS_KEY:-}" ]]; then echo "" echo -e " ${YELLOW}Для доступа из сети:${NC}" echo -e " ${CYAN}http://:3000?key=${DEV_ACCESS_KEY}${NC}" fi fi echo "" echo -e " ${BOLD}Сгенерированные пароли (сохранены в .env):${NC}" echo -e " ${CYAN}PostgreSQL:${NC} ${PG_PASSWORD}" echo -e " ${CYAN}MinIO:${NC} ${MINIO_USER} / ${MINIO_PASS}" if [[ "$MODE" == "1" && -n "${DEV_ACCESS_KEY:-}" ]]; then echo -e " ${CYAN}DEV_ACCESS_KEY:${NC} ${DEV_ACCESS_KEY}" fi echo -e " ${YELLOW}Изменить пароли: отредактируй .env и перезапусти docker compose${NC}" if [[ -z "$LK_URL" ]]; then echo "" log_warn "LiveKit не настроен — видеозвонки не будут работать." echo -e " Зарегистрируйся на ${CYAN}https://cloud.livekit.io${NC}" echo -e " и заполни LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET в .env" fi if [[ -z "$DG_KEY" ]]; then echo "" log_warn "Deepgram не настроен — AI транскрипция отключена." echo -e " Получи ключ: ${CYAN}https://console.deepgram.com${NC}" fi if [[ -z "$OAI_KEY" ]]; then echo "" log_warn "OpenAI не настроен — AI суммаризация отключена." echo -e " Получи ключ: ${CYAN}https://platform.openai.com/api-keys${NC}" fi echo "" echo -e " Диагностика: ${CYAN}./setup.sh doctor${NC}" echo -e " Обновление: ${CYAN}./setup.sh update${NC}" echo -e " Статус: ${CYAN}./setup.sh status${NC}" echo "" } # ============================================================ # COMMAND: logs # ============================================================ cmd_logs() { local service="${1:-app}" log_info "Логи: ${service} (Ctrl+C для выхода)" docker compose logs -f --tail 100 "$service" } # ============================================================ # COMMAND: restart # ============================================================ cmd_restart() { log_step "Перезапуск контейнеров" local mode mode=$(detect_mode) if [[ "$mode" == "prod" ]]; then dc down 2>&1 | tail -3 dc up -d --build 2>&1 | tail -5 else docker compose restart postgres redis pgbouncer minio 2>&1 | tail -3 fi log_ok "Контейнеры перезапущены" # Очистить .next на всякий случай if [[ -d ".next" ]] && ask_yn "Очистить .next кеш?" "n"; then rm -rf .next log_ok ".next очищен" fi } # ============================================================ # COMMAND: reset # ============================================================ cmd_reset() { echo "" echo -e " ${RED}${BOLD}ВНИМАНИЕ: Полный сброс удалит:${NC}" echo -e " ${RED}• Все Docker контейнеры и volumes (БД, файлы MinIO)${NC}" echo -e " ${RED}• node_modules${NC}" echo -e " ${RED}• .next кеш${NC}" echo -e " ${RED}• .env (будет создан бэкап)${NC}" echo "" if ! ask_yn "Точно сбросить всё?" "n"; then log_info "Отменено" return fi if ! ask_yn "Ты уверен? Данные БД будут УДАЛЕНЫ" "n"; then log_info "Отменено" return fi log_step "Полный сброс" # Бэкап .env if [[ -f .env ]]; then cp .env ".env.backup.$(date +%Y%m%d_%H%M%S)" log_ok "Бэкап .env создан" fi # Остановить и удалить контейнеры + volumes log_info "Остановка контейнеров..." dc down -v 2>&1 | tail -5 log_ok "Контейнеры и volumes удалены" # Очистка [[ -d "node_modules" ]] && { rm -rf node_modules; log_ok "node_modules удалён"; } [[ -d ".next" ]] && { rm -rf .next; log_ok ".next удалён"; } [[ -f ".env" ]] && { rm .env; log_ok ".env удалён"; } echo "" log_ok "${GREEN}Сброс завершён${NC}" echo -e " Для повторной установки: ${CYAN}./setup.sh install${NC}" echo "" } # ============================================================ # COMMAND: dev — обновить + запустить dev-сервер # ============================================================ cmd_dev() { log_step "Dev Server — обновление и запуск" # 1. Git pull (без интерактива — stash автоматически) log_step "1/7 — Обновление кода" if [[ -d .git ]]; then if ! git diff --quiet 2>/dev/null || ! git diff --cached --quiet 2>/dev/null; then git stash push -m "auto-stash before dev $(date +%Y%m%d_%H%M%S)" 2>&1 log_ok "Изменения сохранены в stash" fi git pull --rebase 2>&1 | tail -5 log_ok "git pull завершён" fi # 2. npm install log_step "2/7 — Зависимости" npm install 2>&1 | tail -3 log_ok "npm install завершён" # 3. Prisma log_step "3/7 — Prisma" npx prisma generate 2>&1 | tail -2 if docker compose exec -T postgres pg_isready -U postgres &>/dev/null; then npx prisma db push --skip-generate --accept-data-loss 2>&1 | tail -3 log_ok "Схема БД синхронизирована" else log_warn "PostgreSQL недоступен — миграция пропущена" fi # 4. Инфраструктурные контейнеры log_step "4/7 — Контейнеры" docker compose up -d postgres minio redis pgbouncer 2>&1 | tail -5 log_ok "Инфра-контейнеры запущены" # Ждём здоровья сервисов (макс 30 сек) log_info "Ожидаю готовности сервисов..." local waited=0 while [[ $waited -lt 30 ]]; do local all_ok=true for svc in postgres redis pgbouncer; do local st st=$(check_service "$svc") if [[ "$st" != "ok" ]]; then all_ok=false break fi done if $all_ok; then log_ok "Все сервисы healthy" break fi sleep 2 waited=$((waited + 2)) done if [[ $waited -ge 30 ]]; then log_warn "Таймаут ожидания — некоторые сервисы могут быть unhealthy" log_info "Запусти ./setup.sh doctor для диагностики" fi # 5. Убить старые процессы Next.js log_step "5/7 — Очистка старых процессов" local killed=0 for pid in $(pgrep -f "next dev" 2>/dev/null || true); do kill "$pid" 2>/dev/null && ((killed++)) || true done # Убить процесс на порту 3000 local port_pid port_pid=$(lsof -ti :3000 2>/dev/null || true) if [[ -n "$port_pid" ]]; then kill "$port_pid" 2>/dev/null && ((killed++)) || true fi if [[ $killed -gt 0 ]]; then log_ok "Убито процессов: $killed" sleep 1 else log_ok "Старых процессов нет" fi # 6. Очистка .next кеша log_step "6/7 — Очистка кеша" [[ -d ".next" ]] && rm -rf .next log_ok "Кеш очищен" # 7. MinIO bucket log_step "7/7 — MinIO bucket" local bucket bucket=$(env_get S3_BUCKET) bucket="${bucket:-liveserver}" local minio_user minio_user=$(env_get MINIO_ROOT_USER) minio_user="${minio_user:-minioadmin}" local minio_pass minio_pass=$(env_get MINIO_ROOT_PASSWORD) minio_pass="${minio_pass:-minioadmin}" if docker compose exec -T minio mc alias set local http://localhost:9000 "$minio_user" "$minio_pass" &>/dev/null; then if docker compose exec -T minio mc ls "local/${bucket}" &>/dev/null; then log_ok "Bucket '${bucket}' существует" else docker compose exec -T minio mc mb "local/${bucket}" &>/dev/null && \ log_ok "Bucket '${bucket}' создан" || \ log_warn "Не удалось создать bucket — создай вручную: http://localhost:9001" fi else log_warn "MinIO ещё не готов — bucket создастся при следующем запуске" fi # Автоопределение LAN IP для auth local lan_ip lan_ip=$(hostname -I 2>/dev/null | awk '{print $1}' || true) if [[ -n "$lan_ip" && -z "$(env_get LAN_HOST)" ]]; then log_info "LAN IP: ${lan_ip}" if ! grep -q "^LAN_HOST=" .env 2>/dev/null; then echo "LAN_HOST=${lan_ip}" >> .env log_ok "LAN_HOST=${lan_ip} добавлен в .env" fi fi # Запуск echo "" log_ok "${GREEN}${BOLD}Готово! Запускаю dev-сервер...${NC}" echo "" exec npm run dev } # ============================================================ # MAIN — Роутер команд # ============================================================ print_banner CMD="${1:-}" shift 2>/dev/null || true case "$CMD" in install) cmd_install ;; update) cmd_update ;; dev) cmd_dev ;; doctor|fix) cmd_doctor ;; admin) cmd_admin ;; status|st) cmd_status ;; logs|log) cmd_logs "$@" ;; restart) cmd_restart ;; reset) cmd_reset ;; help|-h|--help) print_usage ;; "") # Интерактивное меню if [[ ! -f .env ]]; then echo -e " ${YELLOW}.env не найден — похоже, первый запуск${NC}" echo "" if ask_yn "Запустить установку?" "y"; then cmd_install else print_usage fi else echo -e " ${BOLD}Что делаем?${NC}" echo "" echo -e " ${BOLD}1)${NC} ${CYAN}dev${NC} — ${GREEN}обновить + запустить dev-сервер${NC}" echo -e " ${BOLD}2)${NC} ${CYAN}doctor${NC} — диагностика и исправление" echo -e " ${BOLD}3)${NC} ${CYAN}update${NC} — обновить проект" echo -e " ${BOLD}4)${NC} ${CYAN}status${NC} — статус сервисов" echo -e " ${BOLD}5)${NC} ${CYAN}admin${NC} — создать администратора" echo -e " ${BOLD}6)${NC} ${CYAN}restart${NC} — перезапуск контейнеров" echo -e " ${BOLD}7)${NC} ${CYAN}install${NC} — полная переустановка" echo -e " ${BOLD}8)${NC} ${CYAN}reset${NC} — сброс всего" echo "" ask "Выбери действие" "1" CHOICE case "$CHOICE" in 1|dev) cmd_dev ;; 2|doctor) cmd_doctor ;; 3|update) cmd_update ;; 4|status) cmd_status ;; 5|admin) cmd_admin ;; 6|restart) cmd_restart ;; 7|install) cmd_install ;; 8|reset) cmd_reset ;; *) log_err "Неизвестный выбор: ${CHOICE}"; print_usage ;; esac fi ;; *) log_err "Неизвестная команда: ${CMD}" print_usage exit 1 ;; esac