feat(install): preflight модуль — проверка CLI по выбранному провайдеру

Добавлен шаг между выбором модели и сбором ключей: для провайдеров
требующих внешний CLI/daemon — проверка наличия, инструкция по
установке, опциональный авто-install (TTY only).

install/lib-preflight.sh — диспетчер:
  preflight_run <provider-id>
  - ищет install/preflight/<id>.sh, source'ит, вызывает
    preflight_check_<sanitized_id>
  - функция возвращает 0/1, печатает инструкцию в stderr
  - non-TTY: только печать, без вопросов

preflight_offer_install <cli> <install-cmd>:
  - TTY: спрашивает [y/N/skip], выполняет install-cmd
  - non-TTY: печатает и пропускает

install/preflight/ — 6 файлов (только для провайдеров с CLI):
  anthropic-bedrock.sh  — aws CLI + sts get-caller-identity
  google-vertex.sh      — gcloud CLI + project config
  codex.sh              — codex CLI (npm) + login status
  ollama-local.sh       — ollama binary + 127.0.0.1:11434 daemon
  mlx-local.sh          — mlx_lm.server (arm64 only) + 127.0.0.1:8080
  lmstudio-local.sh     — порт 127.0.0.1:1234 (desktop app)

Direct-api провайдеры (anthropic, openai, xai, deepseek, google) +
proxy (litellm, openrouter) + openai-azure — preflight-файла нет,
диспетчер тихо пропускает, ключ собирается обычно.

Тесты: bash -n чисто на всех 8 файлах, unit dispatcher показывает
silent-pass для anthropic, warn+exit-1 для bedrock без aws на PATH.
This commit is contained in:
Parfii-bot 2026-05-17 15:57:54 +08:00
parent 0f7e0f45e3
commit 7ce43cd80f
9 changed files with 220 additions and 0 deletions

View file

@ -45,6 +45,8 @@ source "$LIB_DIR/lib-menu.sh"
source "$LIB_DIR/lib-i18n.sh"
# Загружаем английский словарь по умолчанию — welcome banner идёт до выбора языка.
i18n_load_default
# shellcheck source=install/lib-preflight.sh
source "$LIB_DIR/lib-preflight.sh"
# shellcheck source=install/lib-onboarding.sh
source "$LIB_DIR/lib-onboarding.sh"
# shellcheck source=install/lib-plan.sh

View file

@ -319,6 +319,11 @@ onboarding_run() {
onboarding_pick_transport
onboarding_pick_provider
onboarding_pick_model
# Preflight — проверка CLI/daemon до сбора ключей.
# Для direct-api провайдеров файла preflight нет → silent pass.
if command -v preflight_run >/dev/null 2>&1; then
preflight_run "$ONBOARDING_PROVIDER" || true
fi
onboarding_collect_auth
onboarding_write_secrets
onboarding_write_config

66
install/lib-preflight.sh Normal file
View file

@ -0,0 +1,66 @@
# shellcheck shell=bash
# lib-preflight.sh — диспетчер preflight-проверок CLI.
#
# Контракт:
# preflight_run <provider-id>
# 1. Ищет файл install/preflight/<provider-id>.sh
# 2. Если есть — source'ит и вызывает `preflight_check_<sanitized-id>`
# 3. Функция возвращает 0 (ok) / 1 (missing, инструкция напечатана)
# 4. Если файла нет — провайдеру CLI не нужен, тихо exit 0
#
# Файл per-provider должен экспортировать ОДНУ функцию:
# preflight_check_<id>() — печатает инструкцию в stderr, exit 0/1
#
# Sanitize: dashes в id заменяются на underscores для имени функции
# (bash не любит dashes в идентификаторах).
PREFLIGHT_DIR="${LIB_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)}/preflight"
# Печатает инструкцию по установке, спрашивает действие.
# Аргументы: $1 — имя CLI, $2 — команда установки.
preflight_offer_install() {
local cli="$1"
local install_cmd="$2"
echo "" >&2
echo "$cli не найден." >&2
echo " Установить: $install_cmd" >&2
echo "" >&2
if [ -t 0 ] && [ -t 1 ]; then
read -r -p " Поставить сейчас? [y/N/skip] " ans
case "$ans" in
y|Y|yes)
eval "$install_cmd"
return $?
;;
skip|s|S)
echo " пропускаю — поставите вручную позже." >&2
return 0
;;
*)
echo " пропуск (по умолчанию)." >&2
return 1
;;
esac
else
# non-TTY: только печатаем инструкцию.
return 1
fi
}
# Главный диспетчер. Вызывается из onboarding между pick_model и collect_auth.
preflight_run() {
local provider="$1"
[ -z "$provider" ] && return 0
local script="$PREFLIGHT_DIR/${provider}.sh"
if [ ! -f "$script" ]; then
return 0 # CLI не нужен — direct-api, ключ собирается ниже
fi
# shellcheck disable=SC1090
source "$script"
local fn="preflight_check_${provider//-/_}"
if command -v "$fn" >/dev/null 2>&1; then
"$fn"
return $?
fi
return 0
}

View file

@ -0,0 +1,26 @@
# shellcheck shell=bash
# preflight/anthropic-bedrock.sh — AWS CLI + Bedrock региональный доступ.
preflight_check_anthropic_bedrock() {
if ! command -v aws >/dev/null 2>&1; then
local cmd
case "$(uname -s)" in
Darwin) cmd="brew install awscli" ;;
Linux) cmd="curl 'https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip' -o /tmp/awscliv2.zip && unzip -q /tmp/awscliv2.zip -d /tmp && sudo /tmp/aws/install" ;;
*) cmd="см. https://aws.amazon.com/cli/" ;;
esac
preflight_offer_install "aws CLI" "$cmd" || return 1
fi
# Проверяем что credentials хоть как-то настроены (env, ~/.aws/credentials, IAM role).
if ! aws sts get-caller-identity >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ AWS credentials не настроены." >&2
echo " Запустите: aws configure" >&2
echo " Или экспортируйте AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY + AWS_REGION." >&2
echo "" >&2
return 1
fi
echo " ✓ aws CLI: $(aws --version 2>&1 | head -1)" >&2
echo " ✓ identity: $(aws sts get-caller-identity --query Arn --output text 2>&1)" >&2
return 0
}

View file

@ -0,0 +1,29 @@
# shellcheck shell=bash
# preflight/codex.sh — OpenAI Codex CLI через ChatGPT OAuth.
preflight_check_codex() {
if ! command -v codex >/dev/null 2>&1; then
if ! command -v npm >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ npm требуется для установки codex." >&2
echo " Сначала: brew install node (macOS) или apt install nodejs npm (Linux)" >&2
echo "" >&2
return 1
fi
preflight_offer_install "codex CLI" "npm install -g @openai/codex" || return 1
fi
# Проверяем что OAuth активен.
local status
status="$(codex login status 2>&1 || true)"
if ! echo "$status" | grep -qiE "logged.in|active"; then
echo "" >&2
echo " ⚠ codex не залогинен в ChatGPT." >&2
echo " Запустите: codex login" >&2
echo " (требуется ChatGPT Plus/Pro/Team подписка)" >&2
echo "" >&2
return 1
fi
echo " ✓ codex CLI: $(codex --version 2>&1 | head -1)" >&2
echo " ✓ OAuth: $status" >&2
return 0
}

View file

@ -0,0 +1,28 @@
# shellcheck shell=bash
# preflight/google-vertex.sh — gcloud CLI + service-account JSON.
preflight_check_google_vertex() {
if ! command -v gcloud >/dev/null 2>&1; then
local cmd
case "$(uname -s)" in
Darwin) cmd="brew install --cask google-cloud-sdk" ;;
Linux) cmd="curl https://sdk.cloud.google.com | bash" ;;
*) cmd="см. https://cloud.google.com/sdk/docs/install" ;;
esac
preflight_offer_install "gcloud CLI" "$cmd" || return 1
fi
# Проверяем что выбран project.
local project
project="$(gcloud config get-value project 2>/dev/null)"
if [ -z "$project" ] || [ "$project" = "(unset)" ]; then
echo "" >&2
echo " ⚠ GCP project не выбран." >&2
echo " Запустите: gcloud auth login && gcloud config set project YOUR_PROJECT_ID" >&2
echo " Также установите GOOGLE_APPLICATION_CREDENTIALS на путь к service-account JSON." >&2
echo "" >&2
return 1
fi
echo " ✓ gcloud CLI: $(gcloud --version 2>&1 | head -1)" >&2
echo " ✓ project: $project" >&2
return 0
}

View file

@ -0,0 +1,16 @@
# shellcheck shell=bash
# preflight/lmstudio-local.sh — LM Studio desktop GUI на 127.0.0.1:1234.
preflight_check_lmstudio_local() {
# LM Studio это desktop-приложение, не CLI — проверяем только порт.
if ! curl -fsS --max-time 3 http://127.0.0.1:1234/v1/models >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ LM Studio сервер не запущен на 1234." >&2
echo " Скачайте: https://lmstudio.ai/" >&2
echo " В GUI: Local Server → Start Server (порт 1234 по умолчанию)" >&2
echo "" >&2
return 1
fi
echo " ✓ LM Studio: 127.0.0.1:1234 отвечает" >&2
return 0
}

View file

@ -0,0 +1,23 @@
# shellcheck shell=bash
# preflight/mlx-local.sh — MLX inference server (Apple silicon).
preflight_check_mlx_local() {
if [ "$(uname -s)" != "Darwin" ] || [ "$(uname -m)" != "arm64" ]; then
echo "" >&2
echo " ⚠ MLX доступен только на Apple silicon (arm64 macOS)." >&2
echo " Текущая платформа: $(uname -s) $(uname -m)" >&2
return 1
fi
if ! command -v mlx_lm.server >/dev/null 2>&1; then
preflight_offer_install "mlx_lm" "pip install mlx-lm" || return 1
fi
if ! curl -fsS --max-time 3 http://127.0.0.1:8080/v1/models >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ MLX server не запущен на 8080." >&2
echo " Запустите: mlx_lm.server --model mlx-community/Qwen2.5-Coder-32B-Instruct-4bit" >&2
echo "" >&2
return 1
fi
echo " ✓ mlx_lm.server: 127.0.0.1:8080 отвечает" >&2
return 0
}

View file

@ -0,0 +1,25 @@
# shellcheck shell=bash
# preflight/ollama-local.sh — Ollama daemon на 127.0.0.1:11434.
preflight_check_ollama_local() {
if ! command -v ollama >/dev/null 2>&1; then
local cmd
case "$(uname -s)" in
Darwin) cmd="brew install ollama" ;;
Linux) cmd="curl -fsSL https://ollama.com/install.sh | sh" ;;
*) cmd="см. https://ollama.com/download" ;;
esac
preflight_offer_install "ollama" "$cmd" || return 1
fi
# Проверяем что daemon запущен.
if ! curl -fsS --max-time 3 http://127.0.0.1:11434/api/tags >/dev/null 2>&1; then
echo "" >&2
echo " ⚠ ollama daemon не запущен." >&2
echo " Запустите: ollama serve (или brew services start ollama на macOS)" >&2
echo "" >&2
return 1
fi
echo " ✓ ollama: $(ollama --version 2>&1 | head -1)" >&2
echo " ✓ daemon: 127.0.0.1:11434 отвечает" >&2
return 0
}