KeiSeiKit-1.0/install/lib-onboarding.sh
KeiSei84 72bf31ba7a refactor(installer): v0.49 — extract kei-prompt cube as interactivity SSoT
Constructor Pattern fix replacing v0.47-v0.48 patch series. The "is the
user interactive?" logic was previously duplicated across 15+ places:

  bootstrap.sh        x4  ([ -t 0 ] gates on profile/onboard/launch/etc)
  install.sh          x1  (PATH wiring decision)
  install/lib-hooks.sh        (activate-hooks prompt)
  install/lib-plan.sh         (auto-confirm gate)
  install/lib-menu.sh         (skip-menu gate)
  install/lib-wizard.sh       (sleep-wizard gate)
  install/lib-onboarding.sh  x2  (onboarding_should_run + preflight retry)
  install/lib-preflight.sh    (install-tool prompt)

Every duplicated check was a chance to get curl|bash semantics wrong.
v0.47 used `[ -t 1 ]` (broke under tee'd stdout). v0.48 used `[ -t 0 ]`
(broke under curl pipe stdin). Each fix was a patch on top of the same
architectural defect: scattered truth.

ARCHITECTURAL FIX (Rule Zero — 1 cube = 1 responsibility):

  scripts/kei-prompt.sh  (NEW, ~110 LOC, public API):
    kei_is_interactive          → 0 if user is at a terminal, 1 if headless
    kei_prompt    Q [DEFAULT]   → answer or default to stdout
    kei_prompt_yn Q [Y|N]       → exit 0=yes, 1=no, with [Y/n] hint
    kei_prompt_secret Q         → no-echo input (tokens, keys)

  Truth signal: /dev/tty accessibility, with [ -t 0 ] as second-choice
  fallback. KEI_NONINTERACTIVE=1 for CI override. Same contract as the
  inline rules — now in ONE place.

  bootstrap.sh + install.sh: source the cube at the top, with self-
  contained inline fallback (mirrors the kei_is_interactive contract
  only) so they remain self-bootable even if scripts/ is missing.

  All 15+ inline gates replaced with `kei_is_interactive` calls.
  All 3 `read -r -p` prompts in installer cubes replaced with
  `kei_prompt` / `kei_prompt_yn`.

  Existing copy_pet_scripts() in lib-scaffold.sh installs scripts/*.sh
  into ~/.claude/scripts/ automatically — no install logic change needed.

WHAT THIS PREVENTS:

- Next time someone writes a prompt in installer code, the only path
  is `kei_prompt`. They CANNOT accidentally type `[ -t 0 ]` because
  there is no `[ -t 0 ]` to copy-paste anymore (except inside the
  cube itself).

- The v2 tty-interactivity-gate-guard.sh hook (added 2026-05-27)
  becomes a regression net rather than the first line of defence.

- Two real install incidents this month (May 2026, 7 prompts each)
  do not happen a third time.

VERIFIED:
- Syntax check passes on all 9 modified files + new cube.
- Primitive functions smoke-tested across 8 cases: headless,
  KEI_NONINTERACTIVE override, default fallback, yn convenience,
  re-source guard, /dev/tty available, /dev/tty open() fails.
- 2 remaining [ -t 0 ] in tree: BOTH inside kei_is_interactive
  fallback in bootstrap.sh + install.sh (single-source contract,
  not patches).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-27 16:05:44 +08:00

105 lines
4.6 KiB
Bash
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# shellcheck shell=bash
# lib-onboarding.sh — мастер первичной настройки (тонкий оркестратор).
#
# Иерархия: язык → транспорт → провайдер → модель → preflight → ключи.
#
# 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
#
# Источник: $KIT_DIR/_blocks/registries/{providers,models}.toml (submodule
# kei-registries). Если submodule не подтянут — fallback (см. registry.sh).
#
# Skip: $ONBOARDED_FLAG, env KEISEI_SKIP_ONBOARD=1, non-TTY.
# Глобалы заполняемые мастером.
ONBOARDING_LANG=""
ONBOARDING_TRANSPORT=""
ONBOARDING_PROVIDER=""
ONBOARDING_MODEL=""
ONBOARDING_STACK=""
ONBOARDING_PACKS=""
declare -a ONBOARDING_AUTH_ENV_KEYS=()
declare -a ONBOARDING_AUTH_ENV_VALUES=()
ONBOARDED_FLAG="$HOME/.claude/.onboarded"
ONBOARDING_CONFIG="$HOME/.claude/config/onboarding.toml"
SECRETS_ENV="$HOME/.claude/secrets/.env"
REGISTRY_PROVIDERS="$KIT_DIR/_blocks/registries/providers.toml"
REGISTRY_MODELS="$KIT_DIR/_blocks/registries/models.toml"
# Подкубы (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
[ "${KEISEI_SKIP_ONBOARD:-}" = "1" ] && return 1
# v0.49: delegate to the kei-prompt cube — covers both plain bash AND
# curl|bash (where stdin is the pipe from curl, so [ -t 0 ] is false
# even with the user at a real terminal — only /dev/tty is reliable).
kei_is_interactive || return 1
return 0
}
# Оркестратор: 6 шагов + preflight + запись.
onboarding_run() {
onboarding_should_run || return 0
if command -v say >/dev/null 2>&1; then
say "${STR_ONBOARDING_INTRO:-Onboarding wizard (6 steps)}"
else
echo "── KeiSei: ${STR_ONBOARDING_INTRO:-onboarding (6 steps)} ──" >&2
fi
onboarding_pick_language
onboarding_pick_transport
onboarding_pick_provider
onboarding_pick_model
onboarding_pick_stack
# Preflight — провайдер-специфичная проверка CLI/daemon до сбора ключей.
if command -v preflight_run >/dev/null 2>&1; then
if ! preflight_run "$ONBOARDING_PROVIDER"; then
echo "" >&2
echo "${STR_PREFLIGHT_FAILED:-Preflight failed — provider may not work.}" >&2
if kei_is_interactive; then # /dev/tty-aware: covers curl|bash
_ans=$(kei_prompt " ${STR_PREFLIGHT_CONTINUE:-Continue anyway? [y/N]} " "N")
case "$_ans" in
y|Y|yes|да|Да)
echo " → продолжаю; ключи запишутся но runtime может упасть." >&2
;;
*)
echo " → прервано; флаг .onboarded НЕ выставляется, перезапустите." >&2
return 1
;;
esac
else
echo " → non-TTY, продолжаю — настройте CLI вручную потом." >&2
fi
fi
fi
onboarding_collect_auth
onboarding_write_secrets
onboarding_write_config
if command -v say >/dev/null 2>&1; then
say "${STR_DONE_TITLE:-onboarding complete}: $ONBOARDING_TRANSPORT / $ONBOARDING_PROVIDER / $ONBOARDING_MODEL"
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
}