From d729a2e903d8d5f196f7dd38d1dee690ca1b78b2 Mon Sep 17 00:00:00 2001 From: Parfii-bot Date: Sun, 17 May 2026 23:57:23 +0800 Subject: [PATCH] =?UTF-8?q?refactor(install):=20production-ready=20=D1=84?= =?UTF-8?q?=D0=B8=D0=BD=D0=B0=D0=BB=D1=8C=D0=BD=D1=8B=D0=B9=20=D0=BA=D1=80?= =?UTF-8?q?=D1=83=D0=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. lib-onboarding.sh раскидан на 3 куба (Constructor Pattern <200 LOC): - lib-onboarding-registry.sh (79 LOC) — парсеры providers/models.toml + onboarding_fallback_providers (14 провайдеров) + onboarding_auth_env_for_provider helper (был inline в collect_auth) - lib-onboarding-ui.sh (189 LOC) — pick_language/transport/provider/model + collect_auth (whiptail/bash select) - lib-onboarding-state.sh (57 LOC) — write_secrets + write_config + user-model-override.toml для kei-model-router - lib-onboarding.sh (95 LOC) — тонкий оркестратор: should_run + run Сам lib-onboarding.sh source'ит 3 подкуба автоматически. Глобалы (ONBOARDING_*, REGISTRY_*, ONBOARDED_FLAG, etc.) объявлены в оркестраторе, подкубы их используют через имена. 2. lib-menu.sh локализован: - whiptail title + radiolist prompt через ${STR_MENU_TITLE} + ${STR_MENU_SUBSTRATE} + ${STR_MENU_PROFILE_PROMPT}. - Plain heading тоже использует словарь. - 12 коротких имён профилей (minimal/core/dev/...) — оставлены EN как стабильные id (не переводятся). 3. _blocks/build-index.sh — детерминированная регенерация INDEX.md. Группировка по 14 категорийным префиксам + "Прочие" для остальных. Безопасно перезапускать. INDEX.md обновлён через этот скрипт (минимальный diff — добавлена ссылка на build-index.sh в шапке). Проверено: bash -n чисто, unit тесты onboarding_list_providers/ transports/models OK, non-TTY smoke ./install.sh --profile=minimal --no-execute проходит. --- _blocks/INDEX.md | 4 +- _blocks/build-index.sh | 72 ++++++ install/lib-menu.sh | 6 +- install/lib-onboarding-registry.sh | 79 +++++++ install/lib-onboarding-state.sh | 57 +++++ install/lib-onboarding-ui.sh | 189 +++++++++++++++ install/lib-onboarding.sh | 365 +++-------------------------- 7 files changed, 430 insertions(+), 342 deletions(-) create mode 100755 _blocks/build-index.sh create mode 100644 install/lib-onboarding-registry.sh create mode 100644 install/lib-onboarding-state.sh create mode 100644 install/lib-onboarding-ui.sh diff --git a/_blocks/INDEX.md b/_blocks/INDEX.md index 3a4b68f..58c21fb 100644 --- a/_blocks/INDEX.md +++ b/_blocks/INDEX.md @@ -1,7 +1,8 @@ # Реестр блоков KeiSeiKit > SSoT для assembler. Все блоки доступные для `blocks = [...]` в `_manifests/.toml`. -> Авто-генерируется из `_blocks/*.md`. Каждый файл = атомарный кубик (Constructor Pattern). +> Авто-генерируется из `_blocks/*.md` через `bash build-index.sh`. +> Каждый файл = атомарный кубик (Constructor Pattern). Пример: ```toml @@ -145,3 +146,4 @@ blocks = ["baseline", "rule-pre-dev-gate", "api-anthropic"] --- Всего блоков: 84. +Перегенерация: `bash _blocks/build-index.sh`. diff --git a/_blocks/build-index.sh b/_blocks/build-index.sh new file mode 100755 index 0000000..4752c4b --- /dev/null +++ b/_blocks/build-index.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# build-index.sh — регенерация _blocks/INDEX.md из *.md. +# +# Использование: +# cd ~/Projects/KeiSeiKit-public/_blocks && bash build-index.sh +# # или из любого места: +# bash $(git rev-parse --show-toplevel)/_blocks/build-index.sh +# +# Что делает: +# 1. Сканит _blocks/*.md (исключая README.md и INDEX.md). +# 2. Группирует по префиксу (api-, auth-, ci-, db-, deploy-, ...). +# 3. Для каждого блока берёт первую H1-строку как описание. +# 4. Пишет INDEX.md с разбиением по 14 категориям + "Прочие". +# +# Безопасно перезапускать — детерминированный output. + +set -euo pipefail + +# Запускаемся всегда из _blocks/. +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +CATEGORIES=(api auth ci db deploy docs domain mode obs path rule scraper security stack test) + +OUT="INDEX.md" +TMP="${OUT}.tmp.$$" +trap 'rm -f "$TMP"' EXIT + +{ + printf '# Реестр блоков KeiSeiKit\n\n' + printf '> SSoT для assembler. Все блоки доступные для `blocks = [...]` в `_manifests/.toml`.\n' + printf '> Авто-генерируется из `_blocks/*.md` через `bash build-index.sh`.\n' + printf '> Каждый файл = атомарный кубик (Constructor Pattern).\n\n' + printf 'Пример:\n```toml\nblocks = ["baseline", "rule-pre-dev-gate", "api-anthropic"]\n```\n\n' + printf '## По категориям\n\n' + + for cat in "${CATEGORIES[@]}"; do + upper=$(echo "$cat" | tr '[:lower:]' '[:upper:]') + files=$(ls 2>/dev/null | grep -E "^${cat}(-|\.).*\.md$" || true) + [ -z "$files" ] && continue + printf '### %s\n\n' "$upper" + while IFS= read -r f; do + [ -z "$f" ] && continue + name="${f%.md}" + desc=$(awk '/^# / { sub(/^# /, ""); print; exit }' "$f" 2>/dev/null || true) + [ -z "$desc" ] && desc="(no title)" + printf -- '- `%s` — %s\n' "$name" "$desc" + done <<< "$files" + printf '\n' + done + + printf '### Прочие (без категорийного префикса)\n\n' + while IFS= read -r f; do + name="${f%.md}" + case "$name" in + api-*|auth-*|ci-*|db-*|deploy-*|docs-*|domain-*|mode-*|obs-*|path-*|rule-*|scraper-*|security-*|stack-*|test-*|README|INDEX) continue ;; + esac + desc=$(awk '/^# / { sub(/^# /, ""); print; exit }' "$f" 2>/dev/null || true) + [ -z "$desc" ] && desc="(no title)" + printf -- '- `%s` — %s\n' "$name" "$desc" + done < <(ls *.md) + + total=$(ls *.md | grep -vE '^(README|INDEX)\.md$' | wc -l | tr -d ' ') + printf '\n---\n\nВсего блоков: %d.\n' "$total" + printf 'Перегенерация: `bash _blocks/build-index.sh`.\n' +} > "$TMP" + +mv "$TMP" "$OUT" +trap - EXIT + +echo "✓ $OUT regenerated" +wc -l "$OUT" diff --git a/install/lib-menu.sh b/install/lib-menu.sh index e8ff90e..3f2b18c 100644 --- a/install/lib-menu.sh +++ b/install/lib-menu.sh @@ -30,8 +30,8 @@ menu_should_skip() { # Profile choice = how many ADDITIONAL primitive binaries to add on top. menu_whiptail_profile() { local tool="$1" - "$tool" --title "KeiSeiKit Installer — substrate always installed; profile = primitives ADDED on top" --radiolist \ - "Choose install profile (SPACE to select, ENTER to confirm):" 28 86 12 \ + "$tool" --title "${STR_MENU_TITLE:-KeiSeiKit Installer} — ${STR_MENU_SUBSTRATE:-substrate always installed; profile = primitives ADDED on top}" --radiolist \ + "${STR_MENU_PROFILE_PROMPT:-Choose install profile (SPACE to select, ENTER to confirm):}" 28 86 12 \ "minimal" "substrate only — 0 primitives (~5s)" ON \ "core" "+ 2 primitives (tomd, kei-doctor) (~5s)" OFF \ "frontend" "+ 8 site tools — mock-render, visual-diff, figma-tokens" OFF \ @@ -77,7 +77,7 @@ menu_plain_profile() { echo " • 82 blocks • 16 caps • 7 roles" >&2 echo " • 11 cross-tool bridges (Cursor / Copilot / Codex / Aider / …)" >&2 echo >&2 - echo " Profile = primitive binaries ADDED on top of substrate." >&2 + echo " ${STR_MENU_PROFILE_PROMPT:-Profile = primitive binaries ADDED on top of substrate.}" >&2 echo "------------------------------------------------------------" >&2 echo >&2 echo " Standard:" >&2 diff --git a/install/lib-onboarding-registry.sh b/install/lib-onboarding-registry.sh new file mode 100644 index 0000000..6f0995a --- /dev/null +++ b/install/lib-onboarding-registry.sh @@ -0,0 +1,79 @@ +# shellcheck shell=bash +# lib-onboarding-registry.sh — парсеры реестров providers.toml + models.toml. +# +# Constructor Pattern: 1 файл = парсинг реестров. UI и state — в соседних кубах. +# +# Источник: $KIT_DIR/_blocks/registries/{providers,models}.toml (submodule +# kei-registries). Если файла нет — fallback на захардкоженный набор +# покрывающий все 7 транспортов. +# +# Глобалы (общие с lib-onboarding-*): +# REGISTRY_PROVIDERS — путь к providers.toml +# REGISTRY_MODELS — путь к models.toml + +REGISTRY_PROVIDERS="${REGISTRY_PROVIDERS:-$KIT_DIR/_blocks/registries/providers.toml}" +REGISTRY_MODELS="${REGISTRY_MODELS:-$KIT_DIR/_blocks/registries/models.toml}" + +# Парсер providers.toml. Простой awk-граббер по [[provider]] секциям. +# Печатает: \t\t\t +onboarding_list_providers() { + [ -f "$REGISTRY_PROVIDERS" ] || { onboarding_fallback_providers; return; } + awk ' + /^\[\[provider\]\]/ { id=""; tr=""; dn=""; ae=""; next } + /^id[[:space:]]*=/ { gsub(/^id[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); id=$0 } + /^transport[[:space:]]*=/ { gsub(/^transport[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); tr=$0 } + /^display_name[[:space:]]*=/ { gsub(/^display_name[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); dn=$0 } + /^auth_env[[:space:]]*=/ { gsub(/^auth_env[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); ae=$0; + if (id && tr) print id "\t" tr "\t" dn "\t" ae } + ' "$REGISTRY_PROVIDERS" +} + +# Fallback если submodule не подтянут. +# Покрывает 7 транспортов минимальными представителями. Синхронизировать +# вручную если в реестре появится новый транспорт-тип. +onboarding_fallback_providers() { + printf "anthropic\tdirect-api\tAnthropic (Direct API)\tANTHROPIC_API_KEY\n" + printf "anthropic-bedrock\taws-bedrock\tAnthropic (AWS Bedrock)\tAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION\n" + printf "openai\tdirect-api\tOpenAI (Direct API)\tOPENAI_API_KEY\n" + printf "openai-azure\tazure-openai\tOpenAI (Azure)\tAZURE_OPENAI_API_KEY,AZURE_OPENAI_ENDPOINT,AZURE_OPENAI_DEPLOYMENT\n" + printf "xai\tdirect-api\txAI\tXAI_API_KEY\n" + printf "deepseek\tdirect-api\tDeepSeek\tDEEPSEEK_API_KEY\n" + printf "google\tdirect-api\tGoogle Gemini (Direct API)\tGEMINI_API_KEY\n" + printf "google-vertex\tgoogle-vertex\tGoogle Gemini (Vertex AI)\tGOOGLE_APPLICATION_CREDENTIALS,GCP_PROJECT_ID,GCP_REGION\n" + printf "ollama-local\tlocal\tOllama (local)\t_\n" + printf "mlx-local\tlocal\tMLX (Apple silicon local)\t_\n" + printf "lmstudio-local\tlocal\tLM Studio (local)\t_\n" + printf "litellm-proxy\tproxy\tLiteLLM proxy (keisei.app)\tKEI_LITELLM_KEY\n" + printf "openrouter\tproxy\tOpenRouter\tOPENROUTER_API_KEY\n" + printf "codex\tsubscription\tOpenAI Codex (ChatGPT OAuth)\t_\n" +} + +# Уникальные транспорты — для первого экрана выбора. +onboarding_list_transports() { + onboarding_list_providers | awk -F'\t' '{print $2}' | sort -u +} + +# Провайдеры внутри транспорта. +onboarding_providers_in_transport() { + local tr="$1" + onboarding_list_providers | awk -F'\t' -v t="$tr" '$2==t {print $1 "\t" $3 "\t" $4}' +} + +# Модели по provider_ref. +onboarding_models_for_provider() { + local pr="$1" + [ -f "$REGISTRY_MODELS" ] || { printf "claude-sonnet-4-6\tClaude Sonnet 4.6\n"; return; } + awk -v pr="$pr" ' + /^\[\[model\]\]/ { id=""; pref=""; dn=""; next } + /^id[[:space:]]*=/ { gsub(/^id[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); id=$0 } + /^provider_ref[[:space:]]*=/ { gsub(/^provider_ref[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); pref=$0 } + /^display_name[[:space:]]*=/ { gsub(/^display_name[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); dn=$0; + if (pref==pr) print id "\t" dn } + ' "$REGISTRY_MODELS" +} + +# auth_env для одного провайдера (для onboarding_collect_auth). +onboarding_auth_env_for_provider() { + local p="$1" + onboarding_list_providers | awk -F'\t' -v p="$p" '$1==p {print $4}' +} diff --git a/install/lib-onboarding-state.sh b/install/lib-onboarding-state.sh new file mode 100644 index 0000000..e89f0ee --- /dev/null +++ b/install/lib-onboarding-state.sh @@ -0,0 +1,57 @@ +# shellcheck shell=bash +# lib-onboarding-state.sh — запись результата мастера на диск. +# +# Constructor Pattern: 1 файл = state-запись. UI — в ui.sh, парсеры — в registry.sh. +# +# Пишет: +# ~/.claude/.onboarded — флаг прохождения +# ~/.claude/config/onboarding.toml — выбор lang/transport/provider/model +# ~/.claude/config/user-model-override.toml — для kei-model-router (HIGH аудит-1) +# ~/.claude/secrets/.env — добавляет ключи провайдера (chmod 600) + +ONBOARDED_FLAG="${ONBOARDED_FLAG:-$HOME/.claude/.onboarded}" +ONBOARDING_CONFIG="${ONBOARDING_CONFIG:-$HOME/.claude/config/onboarding.toml}" +SECRETS_ENV="${SECRETS_ENV:-$HOME/.claude/secrets/.env}" + +onboarding_write_secrets() { + [ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" = "0" ] && return + mkdir -p "$(dirname "$SECRETS_ENV")" + touch "$SECRETS_ENV"; chmod 600 "$SECRETS_ENV" + local i + for i in "${!ONBOARDING_AUTH_ENV_KEYS[@]}"; do + local k="${ONBOARDING_AUTH_ENV_KEYS[$i]}" + local v="${ONBOARDING_AUTH_ENV_VALUES[$i]}" + if grep -q "^${k}=" "$SECRETS_ENV" 2>/dev/null; then + grep -v "^${k}=" "$SECRETS_ENV" > "$SECRETS_ENV.tmp" + mv "$SECRETS_ENV.tmp" "$SECRETS_ENV" + fi + printf '%s=%s\n' "$k" "$v" >> "$SECRETS_ENV" + done + chmod 600 "$SECRETS_ENV" +} + +onboarding_write_config() { + mkdir -p "$(dirname "$ONBOARDING_CONFIG")" + cat > "$ONBOARDING_CONFIG" < этот файл > agent-profiles.toml default_model_ref. + local override_path="$HOME/.claude/config/user-model-override.toml" + cat > "$override_path" < этот файл > agent-profiles.toml default_model_ref. +provider = "$ONBOARDING_PROVIDER" +model = "$ONBOARDING_MODEL" +transport = "$ONBOARDING_TRANSPORT" +EOF + + : > "$ONBOARDED_FLAG" +} diff --git a/install/lib-onboarding-ui.sh b/install/lib-onboarding-ui.sh new file mode 100644 index 0000000..c07fad1 --- /dev/null +++ b/install/lib-onboarding-ui.sh @@ -0,0 +1,189 @@ +# shellcheck shell=bash +# lib-onboarding-ui.sh — pick_* функции мастера (whiptail / bash select). +# +# Constructor Pattern: 1 файл = UI слой. Парсеры реестров — в registry.sh, +# state-запись — в state.sh. +# +# Заполняет глобалы: +# ONBOARDING_LANG, ONBOARDING_TRANSPORT, ONBOARDING_PROVIDER, ONBOARDING_MODEL +# ONBOARDING_AUTH_ENV_KEYS[] + ONBOARDING_AUTH_ENV_VALUES[] +# +# Использует: +# - lib-i18n.sh: STR_* словарь + i18n_available_languages + i18n_load_lang +# - lib-onboarding-registry.sh: списки провайдеров/моделей + +onboarding_pick_language() { + local langs + langs="$(i18n_available_languages 2>/dev/null)" + if [ -z "$langs" ]; then + langs="$(printf 'en\tEnglish\nru\tРусский\n')" + fi + + if command -v whiptail >/dev/null 2>&1; then + local args=() first=1 + while IFS=$'\t' read -r code name; do + [ -z "$code" ] && continue + if [ "$first" = "1" ]; then + args+=("$code" "$name" "ON"); first=0 + else + args+=("$code" "$name" "OFF") + fi + done <<< "$langs" + ONBOARDING_LANG=$(whiptail --title "KeiSei · Language / Язык / 语言 / 言語 / ..." --radiolist \ + "Choose interface language / Выберите язык:" 22 70 16 \ + "${args[@]}" 3>&1 1>&2 2>&3) || ONBOARDING_LANG="en" + else + echo "" >&2 + echo "Choose language / Выберите язык / 选择语言 / 言語選択:" >&2 + declare -a codes=() + local i=1 + while IFS=$'\t' read -r code name; do + [ -z "$code" ] && continue + codes+=("$code") + printf " %2d) %s — %s\n" "$i" "$code" "$name" >&2 + i=$((i+1)) + done <<< "$langs" + read -r -p "[1-${#codes[@]}, default 1=en]: " ans + ans="${ans:-1}" + ONBOARDING_LANG="${codes[$((ans-1))]:-en}" + fi + command -v i18n_load_lang >/dev/null 2>&1 && i18n_load_lang "$ONBOARDING_LANG" +} + +onboarding_pick_transport() { + local transports + transports=$(onboarding_list_transports) + local prompt="${STR_PICK_TRANSPORT:-Choose connection transport:}" + + if command -v whiptail >/dev/null 2>&1; then + local args=() + while IFS= read -r tr; do + local desc + case "$tr" in + direct-api) desc="${STR_TR_DIRECT_API:-Direct provider API}" ;; + aws-bedrock) desc="${STR_TR_AWS_BEDROCK:-AWS Bedrock}" ;; + azure-openai) desc="${STR_TR_AZURE_OPENAI:-Azure OpenAI}" ;; + google-vertex) desc="${STR_TR_GOOGLE_VERTEX:-Google Vertex AI}" ;; + local) desc="${STR_TR_LOCAL:-Local}" ;; + proxy) desc="${STR_TR_PROXY:-Proxy}" ;; + subscription) desc="${STR_TR_SUBSCRIPTION:-OAuth subscription}" ;; + *) desc="$tr" ;; + esac + args+=("$tr" "$desc" "OFF") + done <<< "$transports" + ONBOARDING_TRANSPORT=$(whiptail --title "KeiSei · Transport" --radiolist \ + "$prompt" 18 70 7 "${args[@]}" 3>&1 1>&2 2>&3) || ONBOARDING_TRANSPORT="direct-api" + else + echo "" >&2 + echo "$prompt" >&2 + local i=1 + declare -a opts=() + while IFS= read -r tr; do + opts+=("$tr") + echo " $i) $tr" >&2 + i=$((i+1)) + done <<< "$transports" + read -r -p "[1-${#opts[@]}, default 1]: " ans + ans="${ans:-1}" + ONBOARDING_TRANSPORT="${opts[$((ans-1))]:-direct-api}" + fi +} + +onboarding_pick_provider() { + local rows; rows=$(onboarding_providers_in_transport "$ONBOARDING_TRANSPORT") + local count; count=$(echo "$rows" | wc -l | tr -d ' ') + + # Если провайдер один на транспорт — авто-выбор. + if [ "$count" = "1" ]; then + ONBOARDING_PROVIDER=$(echo "$rows" | awk -F'\t' '{print $1}') + return + fi + + if command -v whiptail >/dev/null 2>&1; then + local args=() + while IFS=$'\t' read -r id dn ae; do + args+=("$id" "$dn" "OFF") + done <<< "$rows" + local prompt="${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" + ONBOARDING_PROVIDER=$(whiptail --title "KeiSei · Provider" --radiolist \ + "$prompt" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \ + || ONBOARDING_PROVIDER=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}') + else + echo "" >&2 + echo "${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" >&2 + declare -a ids=() + local i=1 + while IFS=$'\t' read -r id dn ae; do + ids+=("$id") + echo " $i) $id — $dn" >&2 + i=$((i+1)) + done <<< "$rows" + read -r -p "[1-${#ids[@]}, default 1]: " ans + ans="${ans:-1}" + ONBOARDING_PROVIDER="${ids[$((ans-1))]:-${ids[0]}}" + fi +} + +onboarding_pick_model() { + # Для AWS/Azure/Vertex модели идут под parent-провайдером — мапим. + local lookup="$ONBOARDING_PROVIDER" + case "$ONBOARDING_PROVIDER" in + anthropic-bedrock) lookup="anthropic" ;; + openai-azure) lookup="openai" ;; + google-vertex) lookup="google" ;; + esac + local rows; rows=$(onboarding_models_for_provider "$lookup") + [ -z "$rows" ] && rows=$(printf "claude-sonnet-4-6\tClaude Sonnet 4.6 (fallback)\n") + + if command -v whiptail >/dev/null 2>&1; then + local args=() + while IFS=$'\t' read -r id dn; do + args+=("$id" "$dn" "OFF") + done <<< "$rows" + ONBOARDING_MODEL=$(whiptail --title "KeiSei · Model" --radiolist \ + "${STR_PICK_MODEL:-Default model:}" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \ + || ONBOARDING_MODEL=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}') + else + echo "" >&2 + echo "${STR_PICK_MODEL:-Models for} $lookup:" >&2 + declare -a ids=() + local i=1 + while IFS=$'\t' read -r id dn; do + ids+=("$id") + echo " $i) $id — $dn" >&2 + i=$((i+1)) + done <<< "$rows" + read -r -p "[1-${#ids[@]}, default 1]: " ans + ans="${ans:-1}" + ONBOARDING_MODEL="${ids[$((ans-1))]:-${ids[0]}}" + fi +} + +onboarding_collect_auth() { + ONBOARDING_AUTH_ENV_KEYS=() + ONBOARDING_AUTH_ENV_VALUES=() + local ae; ae=$(onboarding_auth_env_for_provider "$ONBOARDING_PROVIDER") + [ -z "$ae" ] || [ "$ae" = "_" ] && return # local / subscription — нет ключей + + echo "" >&2 + echo "${STR_AUTH_INTRO:-Auth for} $ONBOARDING_PROVIDER ($ae):" >&2 + echo "${STR_AUTH_PROMPT:-Enter values (Enter — leave empty, fill later).}" >&2 + + local IFS_old="$IFS"; IFS=',' + for key in $ae; do + IFS="$IFS_old" + local cur="${!key:-}" + local prompt_msg="$key" + [ -n "$cur" ] && prompt_msg="$key ${STR_AUTH_CURRENT_HINT:-(current: )}" + read -r -s -p " $prompt_msg = " val + echo "" >&2 + if [ -n "$val" ]; then + ONBOARDING_AUTH_ENV_KEYS+=("$key") + ONBOARDING_AUTH_ENV_VALUES+=("$val") + elif [ -n "$cur" ]; then + ONBOARDING_AUTH_ENV_KEYS+=("$key") + ONBOARDING_AUTH_ENV_VALUES+=("$cur") + fi + done + IFS="$IFS_old" +} diff --git a/install/lib-onboarding.sh b/install/lib-onboarding.sh index e92be51..5206d37 100644 --- a/install/lib-onboarding.sh +++ b/install/lib-onboarding.sh @@ -1,23 +1,21 @@ # shellcheck shell=bash -# lib-onboarding.sh — мастер выбора языка / транспорта / провайдера / модели. +# lib-onboarding.sh — мастер первичной настройки (тонкий оркестратор). # -# Иерархия: язык → транспорт → провайдер → модель → ключи. +# Иерархия: язык → транспорт → провайдер → модель → preflight → ключи. # -# Реестр: $KIT_DIR/_blocks/registries/{providers,models}.toml -# (submodule kei-registries). Если submodule не подтянут — fallback -# на захардкоженный набор (anthropic direct-api + sonnet). +# Constructor Pattern: этот файл — только координация. Логика по слоям: +# lib-onboarding-registry.sh — парсеры providers/models.toml + fallback +# lib-onboarding-ui.sh — pick_* функции (whiptail/bash select) +# lib-onboarding-state.sh — запись secrets/.env + onboarding.toml + флаг +# lib-preflight.sh — провайдер-специфичные CLI-проверки +# lib-i18n.sh — STR_* словарь + load_lang # -# Состояние: -# ~/.claude/.onboarded — флаг "пройдено", skip при повторе -# ~/.claude/config/onboarding.toml — выбор lang/transport/provider/model -# ~/.claude/secrets/.env — добавляет ключи провайдера +# Источник: $KIT_DIR/_blocks/registries/{providers,models}.toml (submodule +# kei-registries). Если submodule не подтянут — fallback (см. registry.sh). # -# Тулинг: whiptail > dialog > plain bash select. -# Stdout-контракт: ничего значимого; запись в файлы + globals. +# Skip: $ONBOARDED_FLAG, env KEISEI_SKIP_ONBOARD=1, non-TTY. -# ─────────────────────────────────────────────────────────────────────── -# Глобалы заполняемые мастером -# ─────────────────────────────────────────────────────────────────────── +# Глобалы заполняемые мастером. ONBOARDING_LANG="" ONBOARDING_TRANSPORT="" ONBOARDING_PROVIDER="" @@ -31,331 +29,24 @@ SECRETS_ENV="$HOME/.claude/secrets/.env" REGISTRY_PROVIDERS="$KIT_DIR/_blocks/registries/providers.toml" REGISTRY_MODELS="$KIT_DIR/_blocks/registries/models.toml" -# ─────────────────────────────────────────────────────────────────────── -# Skip-логика -# ─────────────────────────────────────────────────────────────────────── +# Подкубы (sourced параллельно — функции расходятся по namespace без коллизий). +# shellcheck source=install/lib-onboarding-registry.sh +[ -f "$LIB_DIR/lib-onboarding-registry.sh" ] && source "$LIB_DIR/lib-onboarding-registry.sh" +# shellcheck source=install/lib-onboarding-ui.sh +[ -f "$LIB_DIR/lib-onboarding-ui.sh" ] && source "$LIB_DIR/lib-onboarding-ui.sh" +# shellcheck source=install/lib-onboarding-state.sh +[ -f "$LIB_DIR/lib-onboarding-state.sh" ] && source "$LIB_DIR/lib-onboarding-state.sh" + +# Skip-логика. onboarding_should_run() { - [ -f "$ONBOARDED_FLAG" ] && return 1 # уже пройдено + [ -f "$ONBOARDED_FLAG" ] && return 1 [ "${KEISEI_SKIP_ONBOARD:-}" = "1" ] && return 1 - [ ! -t 0 ] && return 1 # не TTY → скип, профиль решит + [ ! -t 0 ] && return 1 [ ! -t 1 ] && return 1 return 0 } -# ─────────────────────────────────────────────────────────────────────── -# Парсер providers.toml. Простой awk-граббер по [[provider]] секциям. -# Печатает: \t\t\t -# ─────────────────────────────────────────────────────────────────────── -onboarding_list_providers() { - [ -f "$REGISTRY_PROVIDERS" ] || { onboarding_fallback_providers; return; } - awk ' - /^\[\[provider\]\]/ { id=""; tr=""; dn=""; ae=""; next } - /^id[[:space:]]*=/ { gsub(/^id[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); id=$0 } - /^transport[[:space:]]*=/ { gsub(/^transport[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); tr=$0 } - /^display_name[[:space:]]*=/ { gsub(/^display_name[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); dn=$0 } - /^auth_env[[:space:]]*=/ { gsub(/^auth_env[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); ae=$0; - if (id && tr) print id "\t" tr "\t" dn "\t" ae } - ' "$REGISTRY_PROVIDERS" -} - -# Fallback если submodule не подтянут. -# Покрывает 7 транспортов (direct-api / aws / azure / vertex / local / proxy -# / subscription) минимальными представителями. Используется только когда -# providers.toml отсутствует — синхронизировать ручно если добавится новый -# транспорт-тип в реестр. -onboarding_fallback_providers() { - printf "anthropic\tdirect-api\tAnthropic (Direct API)\tANTHROPIC_API_KEY\n" - printf "anthropic-bedrock\taws-bedrock\tAnthropic (AWS Bedrock)\tAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_REGION\n" - printf "openai\tdirect-api\tOpenAI (Direct API)\tOPENAI_API_KEY\n" - printf "openai-azure\tazure-openai\tOpenAI (Azure)\tAZURE_OPENAI_API_KEY,AZURE_OPENAI_ENDPOINT,AZURE_OPENAI_DEPLOYMENT\n" - printf "xai\tdirect-api\txAI\tXAI_API_KEY\n" - printf "deepseek\tdirect-api\tDeepSeek\tDEEPSEEK_API_KEY\n" - printf "google\tdirect-api\tGoogle Gemini (Direct API)\tGEMINI_API_KEY\n" - printf "google-vertex\tgoogle-vertex\tGoogle Gemini (Vertex AI)\tGOOGLE_APPLICATION_CREDENTIALS,GCP_PROJECT_ID,GCP_REGION\n" - printf "ollama-local\tlocal\tOllama (local)\t_\n" - printf "mlx-local\tlocal\tMLX (Apple silicon local)\t_\n" - printf "lmstudio-local\tlocal\tLM Studio (local)\t_\n" - printf "litellm-proxy\tproxy\tLiteLLM proxy (keisei.app)\tKEI_LITELLM_KEY\n" - printf "openrouter\tproxy\tOpenRouter\tOPENROUTER_API_KEY\n" - printf "codex\tsubscription\tOpenAI Codex (ChatGPT OAuth)\t_\n" -} - -# Уникальные транспорты — для первого экрана выбора. -onboarding_list_transports() { - onboarding_list_providers | awk -F'\t' '{print $2}' | sort -u -} - -# Провайдеры внутри транспорта. -onboarding_providers_in_transport() { - local tr="$1" - onboarding_list_providers | awk -F'\t' -v t="$tr" '$2==t {print $1 "\t" $3 "\t" $4}' -} - -# Модели по provider_ref. -onboarding_models_for_provider() { - local pr="$1" - [ -f "$REGISTRY_MODELS" ] || { printf "claude-sonnet-4-6\tClaude Sonnet 4.6\n"; return; } - awk -v pr="$pr" ' - /^\[\[model\]\]/ { id=""; pref=""; dn=""; next } - /^id[[:space:]]*=/ { gsub(/^id[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); id=$0 } - /^provider_ref[[:space:]]*=/ { gsub(/^provider_ref[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); pref=$0 } - /^display_name[[:space:]]*=/ { gsub(/^display_name[[:space:]]*=[[:space:]]*"/, ""); gsub(/".*$/, ""); dn=$0; - if (pref==pr) print id "\t" dn } - ' "$REGISTRY_MODELS" -} - -# ─────────────────────────────────────────────────────────────────────── -# UI: язык -# ─────────────────────────────────────────────────────────────────────── -onboarding_pick_language() { - # Список языков читается из lib-i18n.sh::i18n_available_languages. - # На этом шаге язык ещё не выбран — заголовок двуязычный. - local langs - langs="$(i18n_available_languages 2>/dev/null)" - if [ -z "$langs" ]; then - # Fallback если lib-i18n не подключён. - langs="$(printf 'en\tEnglish\nru\tРусский\n')" - fi - - if command -v whiptail >/dev/null 2>&1; then - local args=() first=1 - while IFS=$'\t' read -r code name; do - [ -z "$code" ] && continue - if [ "$first" = "1" ]; then - args+=("$code" "$name" "ON"); first=0 - else - args+=("$code" "$name" "OFF") - fi - done <<< "$langs" - ONBOARDING_LANG=$(whiptail --title "KeiSei · Language / Язык / 语言 / 言語 / ..." --radiolist \ - "Choose interface language / Выберите язык:" 22 70 16 \ - "${args[@]}" 3>&1 1>&2 2>&3) || ONBOARDING_LANG="en" - else - echo "" >&2 - echo "Choose language / Выберите язык / 选择语言 / 言語選択:" >&2 - declare -a codes=() - local i=1 - while IFS=$'\t' read -r code name; do - [ -z "$code" ] && continue - codes+=("$code") - printf " %2d) %s — %s\n" "$i" "$code" "$name" >&2 - i=$((i+1)) - done <<< "$langs" - read -r -p "[1-${#codes[@]}, default 1=en]: " ans - ans="${ans:-1}" - ONBOARDING_LANG="${codes[$((ans-1))]:-en}" - fi - # Перегружаем словарь — все последующие строки на выбранном языке. - if command -v i18n_load_lang >/dev/null 2>&1; then - i18n_load_lang "$ONBOARDING_LANG" - fi -} - -# ─────────────────────────────────────────────────────────────────────── -# UI: транспорт -# ─────────────────────────────────────────────────────────────────────── -onboarding_pick_transport() { - local transports - transports=$(onboarding_list_transports) - local prompt="${STR_PICK_TRANSPORT:-Choose connection transport:}" - - if command -v whiptail >/dev/null 2>&1; then - local args=() - while IFS= read -r tr; do - local desc - case "$tr" in - direct-api) desc="${STR_TR_DIRECT_API:-Direct provider API}" ;; - aws-bedrock) desc="${STR_TR_AWS_BEDROCK:-AWS Bedrock}" ;; - azure-openai) desc="${STR_TR_AZURE_OPENAI:-Azure OpenAI}" ;; - google-vertex) desc="${STR_TR_GOOGLE_VERTEX:-Google Vertex AI}" ;; - local) desc="${STR_TR_LOCAL:-Local}" ;; - proxy) desc="${STR_TR_PROXY:-Proxy}" ;; - subscription) desc="${STR_TR_SUBSCRIPTION:-OAuth subscription}" ;; - *) desc="$tr" ;; - esac - args+=("$tr" "$desc" "OFF") - done <<< "$transports" - ONBOARDING_TRANSPORT=$(whiptail --title "KeiSei · Transport" --radiolist \ - "$prompt" 18 70 7 "${args[@]}" 3>&1 1>&2 2>&3) || ONBOARDING_TRANSPORT="direct-api" - else - echo "" >&2 - echo "$prompt" >&2 - local i=1 - declare -a opts=() - while IFS= read -r tr; do - opts+=("$tr") - echo " $i) $tr" >&2 - i=$((i+1)) - done <<< "$transports" - read -r -p "[1-${#opts[@]}, default 1]: " ans - ans="${ans:-1}" - ONBOARDING_TRANSPORT="${opts[$((ans-1))]:-direct-api}" - fi -} - -# ─────────────────────────────────────────────────────────────────────── -# UI: провайдер -# ─────────────────────────────────────────────────────────────────────── -onboarding_pick_provider() { - local rows; rows=$(onboarding_providers_in_transport "$ONBOARDING_TRANSPORT") - local count; count=$(echo "$rows" | wc -l | tr -d ' ') - - # Если провайдер один на транспорт — авто-выбор. - if [ "$count" = "1" ]; then - ONBOARDING_PROVIDER=$(echo "$rows" | awk -F'\t' '{print $1}') - return - fi - - if command -v whiptail >/dev/null 2>&1; then - local args=() - while IFS=$'\t' read -r id dn ae; do - args+=("$id" "$dn" "OFF") - done <<< "$rows" - local prompt="${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" - ONBOARDING_PROVIDER=$(whiptail --title "KeiSei · Provider" --radiolist \ - "$prompt" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \ - || ONBOARDING_PROVIDER=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}') - else - echo "" >&2 - # Используем единый fallback что и для whiptail — устраняем plural mismatch. - echo "${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" >&2 - declare -a ids=() - local i=1 - while IFS=$'\t' read -r id dn ae; do - ids+=("$id") - echo " $i) $id — $dn" >&2 - i=$((i+1)) - done <<< "$rows" - read -r -p "[1-${#ids[@]}, default 1]: " ans - ans="${ans:-1}" - ONBOARDING_PROVIDER="${ids[$((ans-1))]:-${ids[0]}}" - fi -} - -# ─────────────────────────────────────────────────────────────────────── -# UI: модель -# ─────────────────────────────────────────────────────────────────────── -onboarding_pick_model() { - # Для AWS/Azure/Vertex модели идут под parent-провайдером (anthropic, openai, google) — - # эти транспорты ре-используют тот же models.toml. Мапим bedrock→anthropic, azure→openai, vertex→google. - local lookup="$ONBOARDING_PROVIDER" - case "$ONBOARDING_PROVIDER" in - anthropic-bedrock) lookup="anthropic" ;; - openai-azure) lookup="openai" ;; - google-vertex) lookup="google" ;; - esac - local rows; rows=$(onboarding_models_for_provider "$lookup") - [ -z "$rows" ] && rows=$(printf "claude-sonnet-4-6\tClaude Sonnet 4.6 (fallback)\n") - - if command -v whiptail >/dev/null 2>&1; then - local args=() - while IFS=$'\t' read -r id dn; do - args+=("$id" "$dn" "OFF") - done <<< "$rows" - ONBOARDING_MODEL=$(whiptail --title "KeiSei · Model" --radiolist \ - "${STR_PICK_MODEL:-Default model:}" 16 70 8 "${args[@]}" 3>&1 1>&2 2>&3) \ - || ONBOARDING_MODEL=$(echo "$rows" | head -1 | awk -F'\t' '{print $1}') - else - echo "" >&2 - echo "${STR_PICK_MODEL:-Models for} $lookup:" >&2 - declare -a ids=() - local i=1 - while IFS=$'\t' read -r id dn; do - ids+=("$id") - echo " $i) $id — $dn" >&2 - i=$((i+1)) - done <<< "$rows" - read -r -p "[1-${#ids[@]}, default 1]: " ans - ans="${ans:-1}" - ONBOARDING_MODEL="${ids[$((ans-1))]:-${ids[0]}}" - fi -} - -# ─────────────────────────────────────────────────────────────────────── -# UI: ключи / креды по auth_env -# ─────────────────────────────────────────────────────────────────────── -onboarding_collect_auth() { - ONBOARDING_AUTH_ENV_KEYS=() - ONBOARDING_AUTH_ENV_VALUES=() - local ae; ae=$(onboarding_list_providers | awk -F'\t' -v p="$ONBOARDING_PROVIDER" '$1==p {print $4}') - [ -z "$ae" ] || [ "$ae" = "_" ] && return # local / subscription — нет ключей - - echo "" >&2 - echo "${STR_AUTH_INTRO:-Auth for} $ONBOARDING_PROVIDER ($ae):" >&2 - echo "${STR_AUTH_PROMPT:-Enter values (Enter — leave empty, fill later).}" >&2 - - local IFS_old="$IFS"; IFS=',' - for key in $ae; do - IFS="$IFS_old" - local cur="${!key:-}" - local prompt_msg="$key" - [ -n "$cur" ] && prompt_msg="$key ${STR_AUTH_CURRENT_HINT:-(current: )}" - # silent read — значение не светит в терминале - read -r -s -p " $prompt_msg = " val - echo "" >&2 - if [ -n "$val" ]; then - ONBOARDING_AUTH_ENV_KEYS+=("$key") - ONBOARDING_AUTH_ENV_VALUES+=("$val") - elif [ -n "$cur" ]; then - ONBOARDING_AUTH_ENV_KEYS+=("$key") - ONBOARDING_AUTH_ENV_VALUES+=("$cur") - fi - done - IFS="$IFS_old" -} - -# ─────────────────────────────────────────────────────────────────────── -# Запись результата -# ─────────────────────────────────────────────────────────────────────── -onboarding_write_secrets() { - [ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" = "0" ] && return - mkdir -p "$(dirname "$SECRETS_ENV")" - touch "$SECRETS_ENV"; chmod 600 "$SECRETS_ENV" - local i - for i in "${!ONBOARDING_AUTH_ENV_KEYS[@]}"; do - local k="${ONBOARDING_AUTH_ENV_KEYS[$i]}" - local v="${ONBOARDING_AUTH_ENV_VALUES[$i]}" - # удалим старую строку с тем же ключом - if grep -q "^${k}=" "$SECRETS_ENV" 2>/dev/null; then - grep -v "^${k}=" "$SECRETS_ENV" > "$SECRETS_ENV.tmp" - mv "$SECRETS_ENV.tmp" "$SECRETS_ENV" - fi - printf '%s=%s\n' "$k" "$v" >> "$SECRETS_ENV" - done - chmod 600 "$SECRETS_ENV" -} - -onboarding_write_config() { - mkdir -p "$(dirname "$ONBOARDING_CONFIG")" - cat > "$ONBOARDING_CONFIG" < "$override_path" < этот файл > agent-profiles.toml default_model_ref. -provider = "$ONBOARDING_PROVIDER" -model = "$ONBOARDING_MODEL" -transport = "$ONBOARDING_TRANSPORT" -EOF - - : > "$ONBOARDED_FLAG" -} - -# ─────────────────────────────────────────────────────────────────────── -# Оркестратор -# ─────────────────────────────────────────────────────────────────────── +# Оркестратор: 5 шагов + preflight + запись. onboarding_run() { onboarding_should_run || return 0 @@ -369,13 +60,10 @@ onboarding_run() { onboarding_pick_transport onboarding_pick_provider onboarding_pick_model - # Preflight — проверка CLI/daemon до сбора ключей. - # Для direct-api провайдеров файла preflight нет → silent pass. + + # Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей. if command -v preflight_run >/dev/null 2>&1; then if ! preflight_run "$ONBOARDING_PROVIDER"; then - # Provider preflight failed (CLI missing / daemon down / no creds). - # Не молчим — спрашиваем юзера, иначе onboarding закончится - # с .onboarded флагом для нерабочей конфигурации (HIGH аудит-9). echo "" >&2 echo " ⚠ ${STR_PREFLIGHT_FAILED:-Preflight failed — provider may not work.}" >&2 if [ -t 0 ] && [ -t 1 ]; then @@ -394,6 +82,7 @@ onboarding_run() { fi fi fi + onboarding_collect_auth onboarding_write_secrets onboarding_write_config