diff --git a/_blocks/registries b/_blocks/registries index c559065..b904993 160000 --- a/_blocks/registries +++ b/_blocks/registries @@ -1 +1 @@ -Subproject commit c5590658ee636398c512ce2b25d8aedb3212a9b6 +Subproject commit b90499311ed09cbf88ced93aaa9d890dcf861fac diff --git a/install/i18n/en.sh b/install/i18n/en.sh index 750683f..3c975fa 100644 --- a/install/i18n/en.sh +++ b/install/i18n/en.sh @@ -19,7 +19,7 @@ STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+key)" STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)" STR_TR_LOCAL="Local (Ollama/MLX/LMStudio)" STR_TR_PROXY="Proxy (LiteLLM/OpenRouter)" -STR_TR_SUBSCRIPTION="OAuth subscription (ChatGPT)" +STR_TR_SUBSCRIPTION="Subscription login (Claude Code / ChatGPT — no API key)" # Auth collection STR_AUTH_INTRO="Auth for" @@ -40,3 +40,9 @@ STR_MENU_CONFIRM="Confirm selection?" # Preflight warnings STR_PREFLIGHT_FAILED="Preflight failed — provider may not work." STR_PREFLIGHT_CONTINUE="Continue anyway? [y/N]" + +# Wizard explanations + input validation +STR_PICK_INVALID="please type one of the numbers shown" +STR_EXPLAIN_TRANSPORT="How the agents reach the AI. subscription = log in with your plan, no API key (Claude Code is option 1); direct-api = your own API key. Press Enter for the default." +STR_EXPLAIN_PROVIDER="Which AI service. Option 1 is the recommended default — press Enter." +STR_EXPLAIN_MODEL="Default model the agents use. Option 1 is the recommended default — press Enter." diff --git a/install/i18n/ru.sh b/install/i18n/ru.sh index 1764178..15d56f2 100644 --- a/install/i18n/ru.sh +++ b/install/i18n/ru.sh @@ -19,7 +19,7 @@ STR_TR_AZURE_OPENAI="Azure OpenAI (deployment+ключ)" STR_TR_GOOGLE_VERTEX="Google Vertex AI (GCP)" STR_TR_LOCAL="Локально (Ollama/MLX/LMStudio)" STR_TR_PROXY="Прокси (LiteLLM/OpenRouter)" -STR_TR_SUBSCRIPTION="OAuth-подписка (ChatGPT)" +STR_TR_SUBSCRIPTION="Вход по подписке (Claude Code / ChatGPT — без API-ключа)" # Сбор ключей STR_AUTH_INTRO="Аутентификация для" @@ -40,3 +40,9 @@ STR_MENU_CONFIRM="Подтвердить выбор?" # Preflight-предупреждения STR_PREFLIGHT_FAILED="Preflight упал — провайдер может не работать." STR_PREFLIGHT_CONTINUE="Продолжить всё равно? [y/N]" + +# Пояснения мастера + валидация ввода +STR_PICK_INVALID="введите один из показанных номеров" +STR_EXPLAIN_TRANSPORT="Как агенты обращаются к ИИ. subscription = вход по подписке, без API-ключа (Claude Code — вариант 1); direct-api = свой API-ключ. Нажми Enter для варианта по умолчанию." +STR_EXPLAIN_PROVIDER="Какой ИИ-сервис. Вариант 1 — рекомендуемый по умолчанию, нажми Enter." +STR_EXPLAIN_MODEL="Модель, которую используют агенты. Вариант 1 — рекомендуемый по умолчанию, нажми Enter." diff --git a/install/lib-onboarding-registry.sh b/install/lib-onboarding-registry.sh index 6f0995a..70f9c76 100644 --- a/install/lib-onboarding-registry.sh +++ b/install/lib-onboarding-registry.sh @@ -45,12 +45,20 @@ onboarding_fallback_providers() { 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 "claude-code\tsubscription\tClaude Code (subscription — your claude CLI, no API key)\t_\n" printf "codex\tsubscription\tOpenAI Codex (ChatGPT OAuth)\t_\n" } # Уникальные транспорты — для первого экрана выбора. +# Claude-Code-native kit → выводим subscription + direct-api ПЕРВЫМИ, чтобы +# рекомендованный путь (Claude Code, опция 1) был дефолтом. Остальные следом. onboarding_list_transports() { - onboarding_list_providers | awk -F'\t' '{print $2}' | sort -u + local all; all="$(onboarding_list_providers | awk -F'\t' '{print $2}' | sort -u)" + local t + for t in subscription direct-api; do + printf '%s\n' "$all" | grep -qx "$t" && echo "$t" + done + printf '%s\n' "$all" | grep -vxE 'subscription|direct-api' || true } # Провайдеры внутри транспорта. diff --git a/install/lib-onboarding-ui.sh b/install/lib-onboarding-ui.sh index c07fad1..5fe87aa 100644 --- a/install/lib-onboarding-ui.sh +++ b/install/lib-onboarding-ui.sh @@ -12,6 +12,23 @@ # - lib-i18n.sh: STR_* словарь + i18n_available_languages + i18n_load_lang # - lib-onboarding-registry.sh: списки провайдеров/моделей +# Read a validated 1-based menu choice. Non-numeric or out-of-range input is +# rejected with a re-prompt instead of crashing: bash arithmetic $((ans-1)) +# treats a non-numeric "ans" (e.g. the user typing "claude") as a variable name +# → "unbound variable" under `set -u`. $1=option count, $2=prompt. +# Echoes a number in [1,$1] on stdout; prompts/warnings go to stderr. +_onb_read_choice() { + local max="$1" prompt="$2" ans + while true; do + read -r -p "$prompt" ans + ans="${ans:-1}" + if [[ "$ans" =~ ^[0-9]+$ ]] && [ "$ans" -ge 1 ] && [ "$ans" -le "$max" ]; then + printf '%s' "$ans"; return 0 + fi + printf ' ⚠ %s\n' "${STR_PICK_INVALID:-please enter a number from 1 to $max}" >&2 + done +} + onboarding_pick_language() { local langs langs="$(i18n_available_languages 2>/dev/null)" @@ -43,8 +60,7 @@ onboarding_pick_language() { 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}" + ans="$(_onb_read_choice "${#codes[@]}" "[1-${#codes[@]}, default 1=en]: ")" ONBOARDING_LANG="${codes[$((ans-1))]:-en}" fi command -v i18n_load_lang >/dev/null 2>&1 && i18n_load_lang "$ONBOARDING_LANG" @@ -76,6 +92,7 @@ onboarding_pick_transport() { else echo "" >&2 echo "$prompt" >&2 + echo " ${STR_EXPLAIN_TRANSPORT:-How the agents reach the AI. subscription = log in with your plan (no API key); direct-api = your own API key. Default is fine for most.}" >&2 local i=1 declare -a opts=() while IFS= read -r tr; do @@ -83,8 +100,7 @@ onboarding_pick_transport() { echo " $i) $tr" >&2 i=$((i+1)) done <<< "$transports" - read -r -p "[1-${#opts[@]}, default 1]: " ans - ans="${ans:-1}" + ans="$(_onb_read_choice "${#opts[@]}" "[1-${#opts[@]}, default 1]: ")" ONBOARDING_TRANSPORT="${opts[$((ans-1))]:-direct-api}" fi } @@ -111,6 +127,7 @@ onboarding_pick_provider() { else echo "" >&2 echo "${STR_PICK_PROVIDER:-Provider within} $ONBOARDING_TRANSPORT:" >&2 + echo " ${STR_EXPLAIN_PROVIDER:-Which AI service. Option 1 is the recommended default.}" >&2 declare -a ids=() local i=1 while IFS=$'\t' read -r id dn ae; do @@ -118,8 +135,7 @@ onboarding_pick_provider() { echo " $i) $id — $dn" >&2 i=$((i+1)) done <<< "$rows" - read -r -p "[1-${#ids[@]}, default 1]: " ans - ans="${ans:-1}" + ans="$(_onb_read_choice "${#ids[@]}" "[1-${#ids[@]}, default 1]: ")" ONBOARDING_PROVIDER="${ids[$((ans-1))]:-${ids[0]}}" fi } @@ -135,6 +151,12 @@ onboarding_pick_model() { local rows; rows=$(onboarding_models_for_provider "$lookup") [ -z "$rows" ] && rows=$(printf "claude-sonnet-4-6\tClaude Sonnet 4.6 (fallback)\n") + # Single model → auto-select, no dead-end prompt (mirrors provider count==1). + if [ "$(printf '%s\n' "$rows" | grep -c .)" = "1" ]; then + ONBOARDING_MODEL=$(printf '%s\n' "$rows" | head -1 | 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; do @@ -146,6 +168,7 @@ onboarding_pick_model() { else echo "" >&2 echo "${STR_PICK_MODEL:-Models for} $lookup:" >&2 + echo " ${STR_EXPLAIN_MODEL:-Default model the agents use. Option 1 is the recommended default.}" >&2 declare -a ids=() local i=1 while IFS=$'\t' read -r id dn; do @@ -153,8 +176,7 @@ onboarding_pick_model() { echo " $i) $id — $dn" >&2 i=$((i+1)) done <<< "$rows" - read -r -p "[1-${#ids[@]}, default 1]: " ans - ans="${ans:-1}" + ans="$(_onb_read_choice "${#ids[@]}" "[1-${#ids[@]}, default 1]: ")" ONBOARDING_MODEL="${ids[$((ans-1))]:-${ids[0]}}" fi } diff --git a/install/lib-onboarding.sh b/install/lib-onboarding.sh index dabaa88..444bc5a 100644 --- a/install/lib-onboarding.sh +++ b/install/lib-onboarding.sh @@ -95,4 +95,9 @@ onboarding_run() { say " ${STR_DONE_CONFIG:-config:} $ONBOARDING_CONFIG" [ "${#ONBOARDING_AUTH_ENV_KEYS[@]}" -gt 0 ] && say " ${STR_DONE_SECRETS:-secrets:} $SECRETS_ENV (chmod 600)" fi + # MUST end on success: the last statement above is a short-circuit `&&` that + # evaluates false when the provider has no auth keys (claude-code / codex / + # local) → function would return 1 → `set -e` aborts the whole install at the + # onboarding_run call. Subscription/local providers are exactly the no-key case. + return 0 }