diff --git a/README.md b/README.md index 7597fb7..8fe5ddd 100644 --- a/README.md +++ b/README.md @@ -157,6 +157,22 @@ Detected state: В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`. +## Логи установщика + +Установщик печатает пошаговый лог с emoji, цветом в интерактивном терминале и коротким пояснением, зачем нужен каждый шаг. Например, перед сборкой Swift-приложения он отдельно пишет, что `swift build` может занять время и что строки компилятора вида `[2/5] Write swift-version...` являются нормальным выводом. + +Отключить цвет: + +```sh +NO_COLOR=1 sh install.sh +``` + +Отключить emoji: + +```sh +LEMANA_VPN_NO_EMOJI=1 sh install.sh +``` + ## Модули ### Core diff --git a/install.sh b/install.sh index 8a114e0..cd6eddd 100755 --- a/install.sh +++ b/install.sh @@ -31,6 +31,42 @@ CONFIGURE_KEYCHAIN_FORCED=0 APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}" LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist" +if [ -t 1 ] && [ -z "${NO_COLOR:-}" ] && [ "${TERM:-}" != "dumb" ]; then + C_RESET="$(printf '\033[0m')" + C_BOLD="$(printf '\033[1m')" + C_DIM="$(printf '\033[2m')" + C_RED="$(printf '\033[31m')" + C_GREEN="$(printf '\033[32m')" + C_YELLOW="$(printf '\033[33m')" + C_BLUE="$(printf '\033[34m')" + C_CYAN="$(printf '\033[36m')" +else + C_RESET="" + C_BOLD="" + C_DIM="" + C_RED="" + C_GREEN="" + C_YELLOW="" + C_BLUE="" + C_CYAN="" +fi + +if [ "${LEMANA_VPN_NO_EMOJI:-0}" = "1" ]; then + E_STEP=">" + E_INFO="i" + E_OK="+" + E_WARN="!" + E_SKIP="-" + E_ERROR="x" +else + E_STEP="➡️" + E_INFO="ℹ️" + E_OK="✅" + E_WARN="⚠️" + E_SKIP="⏭️" + E_ERROR="❌" +fi + usage() { cat <<'USAGE' Usage: @@ -155,8 +191,38 @@ log() { printf '%s\n' "$*" } +color_line() { + color="$1" + shift + printf '%s%s%s\n' "$color" "$*" "$C_RESET" +} + +log_step() { + color_line "$C_BOLD$C_CYAN" "$E_STEP $*" +} + +log_info() { + color_line "$C_BLUE" "$E_INFO $*" +} + +log_detail() { + color_line "$C_DIM" " $*" +} + +log_ok() { + color_line "$C_GREEN" "$E_OK $*" +} + +log_warn() { + color_line "$C_YELLOW" "$E_WARN $*" +} + +log_skip() { + color_line "$C_DIM" "$E_SKIP $*" +} + die() { - printf 'ERROR: %s\n' "$*" >&2 + printf '%s%s ERROR: %s%s\n' "$C_RED" "$E_ERROR" "$*" "$C_RESET" >&2 exit 1 } @@ -206,7 +272,7 @@ yes_no() { fi while :; do - printf '%s %s ' "$prompt" "$suffix" > /dev/tty + printf '%s%s %s%s ' "$C_BOLD" "$prompt" "$suffix" "$C_RESET" > /dev/tty IFS= read -r answer < /dev/tty || answer="" case "$answer" in "") [ "$default_answer" = "y" ]; return $? ;; @@ -234,6 +300,8 @@ zsh_aliases_installed() { } print_detected_state() { + log_step "Проверяю установленное окружение" + log_detail "Так установщик понимает, какие модули уже есть, а по каким нужно спросить." log "Detected state:" log " openconnect: $(bool_word command -v openconnect)" log " pipx: $(bool_word command -v pipx)" @@ -254,11 +322,12 @@ choose_modules() { print_detected_state if ! interactive_enabled; then - log "Interactive prompts: off" + log_skip "Interactive prompts: off" return 0 fi - log "Interactive prompts: on" + log_info "Interactive prompts: on" + log_detail "Будут вопросы только по отсутствующим опциональным модулям; флаги командной строки имеют приоритет." if [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then @@ -365,53 +434,71 @@ write_file() { install_homebrew_packages() { need_cmd brew + log_step "Проверяю Homebrew-зависимости" + log_detail "openconnect нужен как VPN-клиент, pipx — для изолированной установки openconnect-lite." + for pkg in openconnect pipx; do if brew list "$pkg" >/dev/null 2>&1; then - log "Homebrew package already installed: $pkg" + log_ok "Homebrew package already installed: $pkg" else - log "Installing Homebrew package: $pkg" + log_info "Installing Homebrew package: $pkg" run brew install "$pkg" fi done if [ "$USE_BITWARDEN" -eq 1 ]; then if brew list bitwarden-cli >/dev/null 2>&1; then - log "Homebrew package already installed: bitwarden-cli" + log_ok "Homebrew package already installed: bitwarden-cli" else - log "Installing Homebrew package: bitwarden-cli" + log_info "Installing Homebrew package: bitwarden-cli" + log_detail "Bitwarden CLI нужен, чтобы брать LDAP-пароль и TOTP seed из item '$BW_ITEM'." run brew install bitwarden-cli fi + else + log_skip "Bitwarden module disabled; пропускаю bitwarden-cli." fi } install_openconnect_lite() { need_cmd pipx + log_step "Проверяю openconnect-lite" + log_detail "openconnect-lite открывает SAML/Keycloak SSO и передаёт полученную сессию в openconnect." + if [ -x "$HOME/.local/bin/openconnect-lite" ] && [ "$FORCE" -eq 0 ]; then - log "openconnect-lite already installed" + log_ok "openconnect-lite already installed" else - log "Installing openconnect-lite via pipx" + log_info "Installing openconnect-lite via pipx" run pipx install openconnect-lite fi if pipx --help 2>/dev/null | grep -q ' pin '; then + log_detail "Закрепляю pipx package, чтобы случайное обновление не сломало SSO-патчи." run pipx pin openconnect-lite >/dev/null 2>&1 || true fi } install_cli() { tmp="$1" + log_step "Обновляю CLI и uninstall helper" + log_detail "CLI управляет подключением, статусом модулей, Bitwarden/Keychain sync и runtime-патчами." + run mkdir -p "$INSTALL_BIN_DIR" download_file "bin/vpn-lemanapro.sh" "$tmp/vpn-lemanapro.sh" run install -m 755 "$tmp/vpn-lemanapro.sh" "$INSTALL_BIN_DIR/vpn-lemanapro.sh" + log_ok "CLI installed: $INSTALL_BIN_DIR/vpn-lemanapro.sh" download_file "uninstall.sh" "$tmp/uninstall.sh" run install -m 755 "$tmp/uninstall.sh" "$INSTALL_BIN_DIR/uninstall-lemana-vpn.sh" + log_ok "Uninstall helper installed: $INSTALL_BIN_DIR/uninstall-lemana-vpn.sh" } install_config() { tmp="$1" + log_step "Записываю конфигурацию" + log_detail "Здесь сохраняются выбранные модули, LDAP username и профиль openconnect-lite для Keycloak SSO." + run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR" download_file "templates/openconnect-lite-config.toml" "$tmp/openconnect-lite-config.toml" @@ -429,20 +516,32 @@ LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\" LEMANA_VPN_DNS_CLEANUP=\"$DNS_CLEANUP\"" write_file "$tmp/env" "$env_content" run install -m 600 "$tmp/env" "$CONFIG_DIR/env" + log_ok "Config installed: $CONFIG_DIR/env" } install_dns_cleanup() { tmp="$1" dns_cleanup_dir="$(dirname "$DNS_CLEANUP")" + log_step "Устанавливаю DNS cleanup wrapper" + log_detail "Wrapper сбрасывает только похожие на корпоративные 10.x DNS после аварийного завершения VPN." + log_detail "macOS может запросить sudo-пароль для записи в $dns_cleanup_dir." + download_file "libexec/lemana-vpn-dns-cleanup" "$tmp/lemana-vpn-dns-cleanup" run sudo install -d -m 755 -o root -g wheel "$dns_cleanup_dir" - log "Installing DNS cleanup wrapper: $DNS_CLEANUP" + log_info "Installing DNS cleanup wrapper: $DNS_CLEANUP" run sudo install -m 755 -o root -g wheel "$tmp/lemana-vpn-dns-cleanup" "$DNS_CLEANUP" + log_ok "DNS cleanup wrapper installed" } install_sudoers() { - [ "$INSTALL_SUDOERS" -eq 1 ] || return 0 + if [ "$INSTALL_SUDOERS" -ne 1 ]; then + log_skip "Sudoers module disabled; openconnect/DNS cleanup могут спрашивать пароль sudo." + return 0 + fi + + log_step "Настраиваю sudoers" + log_detail "Это убирает повторный sudo-пароль при подключении, но разрешает только openconnect и DNS cleanup wrapper." openconnect_bin="$(brew --prefix)/bin/openconnect" [ -x "$openconnect_bin" ] || openconnect_bin="$(command -v openconnect || true)" @@ -456,17 +555,25 @@ install_sudoers() { write_file "$tmp/sudoers-openconnect" "$current_user ALL=(ALL) NOPASSWD: $openconnect_bin" run sudo install -m 440 -o root -g wheel "$tmp/sudoers-openconnect" /etc/sudoers.d/lemana-vpn-openconnect run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-openconnect + log_ok "sudoers openconnect rule is valid" write_file "$tmp/sudoers-dns" "$current_user ALL=(ALL) NOPASSWD: $DNS_CLEANUP" run sudo install -m 440 -o root -g wheel "$tmp/sudoers-dns" /etc/sudoers.d/lemana-vpn-dns run sudo visudo -c -f /etc/sudoers.d/lemana-vpn-dns + log_ok "sudoers DNS cleanup rule is valid" } install_touchid_helper() { - [ "$USE_TOUCHID" -eq 1 ] || return 0 + if [ "$USE_TOUCHID" -ne 1 ]; then + log_skip "Touch ID module disabled; мастер-пароль Bitwarden будет вводиться вручную при необходимости." + return 0 + fi + + log_step "Проверяю Touch ID helper" + log_detail "Helper добавляет локальный Touch ID prompt перед чтением мастер-пароля Bitwarden из Keychain." if [ -x "$INSTALL_BIN_DIR/keychain-fingerprint" ] && [ "$FORCE" -eq 0 ]; then - log "Touch ID helper already installed: $INSTALL_BIN_DIR/keychain-fingerprint" + log_ok "Touch ID helper already installed: $INSTALL_BIN_DIR/keychain-fingerprint" return 0 fi @@ -474,26 +581,34 @@ install_touchid_helper() { need_cmd swiftc tmp="$1" - log "Building keychain-fingerprint helper" + log_info "Building keychain-fingerprint helper" run git clone --depth 1 https://github.com/dss99911/keychain-fingerprint.git "$tmp/keychain-fingerprint" run swiftc -o "$tmp/keychain-fingerprint-bin" "$tmp/keychain-fingerprint/main.swift" -framework LocalAuthentication -framework Security run install -m 700 "$tmp/keychain-fingerprint-bin" "$INSTALL_BIN_DIR/keychain-fingerprint" + log_ok "Touch ID helper installed" } install_menu_bar_app() { - [ "$INSTALL_APP" -eq 1 ] || return 0 + if [ "$INSTALL_APP" -ne 1 ]; then + log_skip "Menu Bar app disabled; CLI и алиасы останутся основным интерфейсом." + return 0 + fi need_cmd swift tmp="$1" app_src="$tmp/app" + log_step "Собираю Swift Menu Bar app" + log_detail "Приложение живёт в status bar и вызывает $INSTALL_BIN_DIR/vpn-lemanapro.sh для подключения и статуса модулей." + log_detail "Swift build может занять минуту; строки вида '[2/5] Write swift-version...' — нормальный вывод компилятора." + run mkdir -p "$app_src/Sources/LemanaVPN" download_file "app/Package.swift" "$app_src/Package.swift" download_file "app/Sources/LemanaVPN/LemanaVPNApp.swift" "$app_src/Sources/LemanaVPN/LemanaVPNApp.swift" download_file "app/Sources/LemanaVPN/VPNManager.swift" "$app_src/Sources/LemanaVPN/VPNManager.swift" - log "Building LemanaVPN.app" + log_info "Building LemanaVPN.app" run swift build -c release --package-path "$app_src" app_bin="$app_src/.build/release/LemanaVPN" @@ -531,14 +646,23 @@ install_menu_bar_app() { ' run install -m 644 "$info_plist" "$APP_DIR/Contents/Info.plist" + log_ok "Menu Bar app installed: $APP_DIR" } install_launch_agent() { - [ "$INSTALL_AUTOSTART" -eq 1 ] || return 0 - [ "$INSTALL_APP" -eq 1 ] || return 0 + if [ "$INSTALL_AUTOSTART" -ne 1 ]; then + log_skip "Autostart disabled; LemanaVPN.app можно запускать вручную." + return 0 + fi + if [ "$INSTALL_APP" -ne 1 ]; then + log_skip "Autostart skipped because Menu Bar app is disabled." + return 0 + fi tmp="$1" plist="$tmp/ru.dokops.LemanaVPN.plist" + log_step "Настраиваю автозапуск Menu Bar app" + log_detail "LaunchAgent запускает LemanaVPN.app при логине, чтобы VPN был доступен из status bar." run mkdir -p "$HOME/Library/LaunchAgents" write_file "$plist" " @@ -564,6 +688,7 @@ install_launch_agent() { else printf '+ launchctl load %s\n' "$LAUNCH_AGENT" fi + log_ok "LaunchAgent installed: $LAUNCH_AGENT" } restart_running_menu_bar_app() { @@ -575,18 +700,28 @@ restart_running_menu_bar_app() { fi if pgrep -x LemanaVPN >/dev/null 2>&1; then - log "Restarting running LemanaVPN.app" + log_step "Перезапускаю уже запущенное LemanaVPN.app" + log_detail "Это нужно, чтобы status bar приложение сразу увидело свежий CLI и новый код." killall LemanaVPN >/dev/null 2>&1 || true sleep 1 open "$APP_DIR" >/dev/null 2>&1 || true + log_ok "LemanaVPN.app restarted" + else + log_skip "LemanaVPN.app сейчас не запущен; перезапуск не нужен." fi } install_shell_aliases() { - [ "$INSTALL_ALIASES" -eq 1 ] || return 0 + if [ "$INSTALL_ALIASES" -ne 1 ]; then + log_skip "Shell aliases disabled; команды можно вызывать напрямую из $INSTALL_BIN_DIR." + return 0 + fi zshrc="$HOME/.zshrc" tmp="$1" + log_step "Обновляю shell aliases" + log_detail "Алиасы vpn, vpn-debug и vpn-fix-dns добавляются идемпотентным блоком в ~/.zshrc." + [ -f "$zshrc" ] || run touch "$zshrc" block="$tmp/zshrc-block" @@ -616,19 +751,29 @@ EOF } > "$tmp/zshrc.new" mv "$tmp/zshrc.new" "$zshrc" + log_ok "Shell aliases updated: $zshrc" } maybe_login_bitwarden() { - [ "$USE_BITWARDEN" -eq 1 ] || return 0 - command -v bw >/dev/null 2>&1 || return 0 + if [ "$USE_BITWARDEN" -ne 1 ]; then + log_skip "Bitwarden module disabled; credentials будут браться из macOS Keychain." + return 0 + fi + if ! command -v bw >/dev/null 2>&1; then + log_warn "Bitwarden CLI enabled but bw not found." + return 0 + fi + + log_step "Проверяю Bitwarden CLI session" + log_detail "На первом запуске vpn CLI сможет понять, нужно ли делать bw login/unlock." status="$(bw status 2>/dev/null || true)" if printf '%s\n' "$status" | grep -q '"status":"unauthenticated"'; then - log "Bitwarden CLI is not logged in. Run later: bw login" + log_warn "Bitwarden CLI is not logged in. Run later: bw login" elif printf '%s\n' "$status" | grep -q '"status":"locked"'; then - log "Bitwarden CLI is logged in but locked. First vpn run will ask for master password." + log_info "Bitwarden CLI is logged in but locked. First vpn run will ask for master password." else - log "Bitwarden CLI is available." + log_ok "Bitwarden CLI is available." fi } @@ -640,8 +785,9 @@ main() { choose_modules - log "Installing Lemana VPN" - log "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART" + log_step "Начинаю установку Lemana VPN" + log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются." + log_info "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART" install_homebrew_packages install_openconnect_lite @@ -657,16 +803,18 @@ main() { maybe_login_bitwarden if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then + log_step "Записываю credentials в macOS Keychain" + log_detail "Будут запрошены LDAP-пароль и постоянный TOTP seed, не текущий 30-секундный код." run "$INSTALL_BIN_DIR/vpn-lemanapro.sh" --configure-keychain fi log "" - log "Done." - log "Open a new shell or run: exec zsh" - log "Connect: vpn" - log "Status: vpn --status" + log_ok "Done." + log_info "Open a new shell or run: exec zsh" + log_info "Connect: vpn" + log_info "Status: vpn --status" if [ "$INSTALL_APP" -eq 1 ]; then - log "App: open '$APP_DIR'" + log_info "App: open '$APP_DIR'" fi } diff --git a/tests/smoke.sh b/tests/smoke.sh index 5c5fbb8..32febec 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -16,11 +16,19 @@ output="$(cd "$ROOT" && sh install.sh --dry-run --non-interactive --minimal)" printf '%s\n' "$output" | grep -q 'Detected state:' printf '%s\n' "$output" | grep -q 'Interactive prompts: off' printf '%s\n' "$output" | grep -q 'Modules: bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1' +printf '%s\n' "$output" | grep -q 'Проверяю Homebrew-зависимости' +printf '%s\n' "$output" | grep -q 'Swift build может занять минуту' printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin' printf '%s\n' "$output" | grep -q 'swift build -c release --package-path' printf '%s\n' "$output" | grep -q 'launchctl load' printf '%s\n' "$output" | grep -q 'restart LemanaVPN.app if running' +esc="$(printf '\033')" +if printf '%s\n' "$output" | grep -q "$esc"; then + echo "non-tty dry-run output contains ANSI color codes" >&2 + exit 1 +fi + status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)" printf '%s\n' "$status_json" | grep -q '"modules":' printf '%s\n' "$status_json" | grep -q '"app":'