diff --git a/README.md b/README.md index 6bf3b64..f2ec596 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/in bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install-server.sh) ``` +### Uninstall +```bash +bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/uninstall.sh) +``` + ## Usage ### First Time Setup diff --git a/README_CN.md b/README_CN.md index 6538ef7..dbaeae4 100644 --- a/README_CN.md +++ b/README_CN.md @@ -64,6 +64,11 @@ bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/in bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/install-server.sh) ``` +### 卸载 +```bash +bash <(curl -sL https://raw.githubusercontent.com/Gouryella/drip/main/scripts/uninstall.sh) +``` + ## 使用 ### 首次配置 diff --git a/scripts/install-server.sh b/scripts/install-server.sh index 877a772..1625871 100755 --- a/scripts/install-server.sh +++ b/scripts/install-server.sh @@ -10,6 +10,7 @@ SERVICE_USER="${SERVICE_USER:-drip}" CONFIG_DIR="/etc/drip" WORK_DIR="/var/lib/drip" VERSION="${VERSION:-}" +COMMAND_MADE_AVAILABLE=false # Default values DEFAULT_PORT=8443 @@ -231,6 +232,73 @@ print_warning() { echo -e "${YELLOW}[!]${NC} $1"; } print_error() { echo -e "${RED}[✗]${NC} $1"; } print_step() { echo -e "${CYAN}[→]${NC} $1"; } +repeat_char() { + local char="$1" + local count="$2" + local out="" + for _ in $(seq 1 "$count"); do + out+="$char" + done + echo "$out" +} + +print_panel() { + local title="$1" + shift + local width=58 + local bar + bar=$(repeat_char "=" "$width") + echo "" + echo -e "${CYAN}${bar}${NC}" + echo -e "${CYAN}${title}${NC}" + echo -e "${CYAN}${bar}${NC}" + for line in "$@"; do + echo -e " $line" + done + echo -e "${CYAN}${bar}${NC}" + echo "" +} + +print_subheader() { + local title="$1" + local width=58 + local bar + bar=$(repeat_char "-" "$width") + echo "" + echo -e "${CYAN}${title}${NC}" + echo -e "${CYAN}${bar}${NC}" +} + +ensure_command_access() { + hash -r 2>/dev/null || true + command -v rehash >/dev/null 2>&1 && rehash || true + + if command -v drip >/dev/null 2>&1; then + COMMAND_MADE_AVAILABLE=true + return + fi + + local target_path="${INSTALL_DIR}/drip" + local preferred="/usr/local/bin" + + if [[ ":$PATH:" == *":$preferred:"* ]]; then + if [[ -w "$preferred" ]]; then + if ln -sf "$target_path" "$preferred/drip" 2>/dev/null; then + COMMAND_MADE_AVAILABLE=true + print_success "Made drip available at $preferred/drip" + fi + else + if sudo ln -sf "$target_path" "$preferred/drip" 2>/dev/null; then + COMMAND_MADE_AVAILABLE=true + print_success "Made drip available at $preferred/drip" + fi + fi + fi + + hash -r 2>/dev/null || true + command -v rehash >/dev/null 2>&1 && rehash || true +} + # Print banner print_banner() { echo -e "${GREEN}" @@ -272,14 +340,9 @@ get_version_from_binary() { # Language selection # ============================================================================ select_language() { - echo "" - echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ $(msg select_lang) ║${NC}" - echo -e "${CYAN}╠════════════════════════════════════════╣${NC}" - echo -e "${CYAN}║${NC} ${GREEN}1)${NC} English ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${GREEN}2)${NC} 中文 ${CYAN}║${NC}" - echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg select_lang)" \ + "${GREEN}1)${NC} English" \ + "${GREEN}2)${NC} 中文" read -p "Select [1]: " lang_choice < /dev/tty case "$lang_choice" in @@ -492,11 +555,7 @@ generate_token() { } configure_server() { - echo "" - echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ $(msg config_title) ${CYAN}║${NC}" - echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" - echo "" + print_subheader "$(msg config_title)" # Domain while true; do @@ -533,15 +592,10 @@ configure_server() { fi # TLS certificate selection - echo "" - echo -e "${CYAN}╔════════════════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ $(msg cert_option_title) ${CYAN}║${NC}" - echo -e "${CYAN}╠════════════════════════════════════════════════════╣${NC}" - echo -e "${CYAN}║${NC} ${GREEN}1)${NC} $(msg cert_option_certbot) ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${GREEN}2)${NC} $(msg cert_option_self) ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${GREEN}3)${NC} $(msg cert_option_provide) ${CYAN}║${NC}" - echo -e "${CYAN}╚════════════════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg cert_option_title)" \ + "${GREEN}1)${NC} $(msg cert_option_certbot)" \ + "${GREEN}2)${NC} $(msg cert_option_self)" \ + "${GREEN}3)${NC} $(msg cert_option_provide)" read -p "Select [1]: " cert_choice < /dev/tty @@ -938,11 +992,7 @@ start_service() { # Final output # ============================================================================ show_completion() { - echo "" - echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ $(msg install_complete) ${GREEN}║${NC}" - echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg install_complete)" echo -e "${CYAN}$(msg client_info):${NC}" echo -e " ${BOLD}$(msg server_addr):${NC} ${DOMAIN}:${PORT}" @@ -976,11 +1026,12 @@ main() { check_existing_install echo "" - download_binary - install_binary +download_binary +install_binary +ensure_command_access - # If updating, check if systemd service exists - if [[ "$IS_UPDATE" == true ]]; then +# If updating, check if systemd service exists +if [[ "$IS_UPDATE" == true ]]; then # Check if systemd service file exists if [[ -f /etc/systemd/system/drip-server.service ]]; then echo "" @@ -997,10 +1048,7 @@ main() { echo "" local new_version=$(get_version_from_binary "$INSTALL_DIR/drip") - echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ $(msg update_ok) ${GREEN}║${NC}" - echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg update_ok)" print_info "$(msg current_version): $new_version" echo "" exit 0 diff --git a/scripts/install.sh b/scripts/install.sh index 4da6e05..6236c70 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -8,6 +8,8 @@ GITHUB_REPO="Gouryella/drip" INSTALL_DIR="${INSTALL_DIR:-}" VERSION="${VERSION:-}" BINARY_NAME="drip" +UNINSTALL_MODE=false +COMMAND_MADE_AVAILABLE=false # Colors RED='\033[0;31m' @@ -83,6 +85,15 @@ msg_en() { verify_ok) echo "Verification passed" ;; verify_failed) echo "Verification failed" ;; insecure_note) echo "Only use --insecure for development/testing" ;; + uninstall_title) echo "Drip Client - Uninstall" ;; + uninstall_prompt) echo "Uninstall Drip client now?" ;; + uninstalling) echo "Uninstalling Drip client..." ;; + uninstall_done) echo "Uninstall completed" ;; + uninstall_not_found) echo "Drip is not installed" ;; + remove_config_prompt) echo "Remove Drip config directory as well?" ;; + config_removed) echo "Config removed" ;; + path_cleanup) echo "Cleaning PATH entries..." ;; + path_cleanup_done) echo "PATH entries cleaned" ;; *) echo "$1" ;; esac } @@ -146,6 +157,15 @@ msg_zh() { verify_ok) echo "验证通过" ;; verify_failed) echo "验证失败" ;; insecure_note) echo "--insecure 仅用于开发/测试环境" ;; + uninstall_title) echo "Drip 客户端 - 卸载" ;; + uninstall_prompt) echo "现在卸载 Drip 客户端?" ;; + uninstalling) echo "正在卸载 Drip 客户端..." ;; + uninstall_done) echo "卸载完成" ;; + uninstall_not_found) echo "未检测到已安装的 Drip" ;; + remove_config_prompt) echo "是否同时删除 Drip 配置目录?" ;; + config_removed) echo "配置已删除" ;; + path_cleanup) echo "清理 PATH 相关配置..." ;; + path_cleanup_done) echo "PATH 配置已清理" ;; *) echo "$1" ;; esac } @@ -177,6 +197,43 @@ print_warning() { echo -e "${YELLOW}[!]${NC} $1"; } print_error() { echo -e "${RED}[✗]${NC} $1"; } print_step() { echo -e "${CYAN}[→]${NC} $1"; } +repeat_char() { + local char="$1" + local count="$2" + local out="" + for _ in $(seq 1 "$count"); do + out+="$char" + done + echo "$out" +} + +print_panel() { + local title="$1" + shift + local width=58 + local bar + bar=$(repeat_char "=" "$width") + echo "" + echo -e "${CYAN}${bar}${NC}" + echo -e "${CYAN}${title}${NC}" + echo -e "${CYAN}${bar}${NC}" + for line in "$@"; do + echo -e " $line" + done + echo -e "${CYAN}${bar}${NC}" + echo "" +} + +print_subheader() { + local title="$1" + local width=58 + local bar + bar=$(repeat_char "-" "$width") + echo "" + echo -e "${CYAN}${title}${NC}" + echo -e "${CYAN}${bar}${NC}" +} + # Print banner print_banner() { echo -e "${GREEN}" @@ -218,14 +275,9 @@ get_version_from_binary() { # Language selection # ============================================================================ select_language() { - echo "" - echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ $(msg select_lang) ║${NC}" - echo -e "${CYAN}╠════════════════════════════════════════╣${NC}" - echo -e "${CYAN}║${NC} ${GREEN}1)${NC} English ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${GREEN}2)${NC} 中文 ${CYAN}║${NC}" - echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg select_lang)" \ + "${GREEN}1)${NC} English" \ + "${GREEN}2)${NC} 中文" prompt_input "Select [1]: " lang_choice case "$lang_choice" in @@ -406,16 +458,11 @@ select_install_dir() { return fi - echo "" - echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ $(msg select_install_dir) ${CYAN}║${NC}" - echo -e "${CYAN}╠════════════════════════════════════════╣${NC}" - echo -e "${CYAN}║${NC} ${GREEN}1)${NC} ~/.local/bin $(msg option_user) ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${GREEN}2)${NC} /usr/local/bin $(msg option_system) ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${GREEN}3)${NC} ./ $(msg option_current) ${CYAN}║${NC}" - echo -e "${CYAN}║${NC} ${GREEN}4)${NC} $(msg option_custom) ${CYAN}║${NC}" - echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg select_install_dir)" \ + "${GREEN}1)${NC} ~/.local/bin $(msg option_user)" \ + "${GREEN}2)${NC} /usr/local/bin $(msg option_system)" \ + "${GREEN}3)${NC} ./ $(msg option_current)" \ + "${GREEN}4)${NC} $(msg option_custom)" prompt_input "Select [1]: " dir_choice @@ -470,6 +517,9 @@ update_path() { if command -v drip &> /dev/null; then return fi + if [[ "$COMMAND_MADE_AVAILABLE" == true ]]; then + return + fi # Skip for system directories (usually already in PATH) if [[ "$INSTALL_DIR" == "/usr/local/bin" ]] || [[ "$INSTALL_DIR" == "/usr/bin" ]]; then @@ -508,6 +558,42 @@ update_path() { print_warning "$(msg path_note)" } +ensure_command_access() { + # Try to make the command available immediately without requiring shell restart + if [[ "$OS" == "windows" ]]; then + return + fi + + hash -r 2>/dev/null || true + command -v rehash >/dev/null 2>&1 && rehash || true + + if command -v "$BINARY_NAME" >/dev/null 2>&1; then + COMMAND_MADE_AVAILABLE=true + return + fi + + local target_path="$INSTALL_DIR/$BINARY_NAME" + local preferred="/usr/local/bin" + + if [[ ":$PATH:" == *":$preferred:"* ]]; then + if [[ -w "$preferred" ]]; then + if ln -sf "$target_path" "$preferred/$BINARY_NAME" 2>/dev/null; then + COMMAND_MADE_AVAILABLE=true + print_success "Made ${BINARY_NAME} available at $preferred/$BINARY_NAME" + fi + else + if sudo ln -sf "$target_path" "$preferred/$BINARY_NAME" 2>/dev/null; then + COMMAND_MADE_AVAILABLE=true + print_success "Made ${BINARY_NAME} available at $preferred/$BINARY_NAME" + fi + fi + fi + + # Refresh hash table + hash -r 2>/dev/null || true + command -v rehash >/dev/null 2>&1 && rehash || true +} + verify_installation() { print_step "$(msg verify_install)" @@ -536,11 +622,7 @@ configure_client() { return fi - echo "" - echo -e "${CYAN}╔════════════════════════════════════════╗${NC}" - echo -e "${CYAN}║ $(msg config_title) ${CYAN}║${NC}" - echo -e "${CYAN}╚════════════════════════════════════════╝${NC}" - echo "" + print_subheader "$(msg config_title)" local binary_path="$INSTALL_DIR/$BINARY_NAME" @@ -605,11 +687,7 @@ test_connection() { show_completion() { local binary_path="$INSTALL_DIR/$BINARY_NAME" - echo "" - echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ $(msg install_complete) ${GREEN}║${NC}" - echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg install_complete)" echo -e "${CYAN}$(msg usage_title):${NC}" echo "" @@ -629,16 +707,122 @@ show_completion() { echo "" } +# ============================================================================ +# Uninstall +# ============================================================================ +cleanup_path_entries() { + local install_dir="$1" + + local candidates=() + + local os_name="$OS" + if [[ -z "$os_name" ]]; then + case "$(uname -s)" in + Darwin*) os_name="darwin" ;; + *) os_name="linux" ;; + esac + fi + + # Determine shell config files + if [[ -n "$ZSH_VERSION" ]] || [[ "$SHELL" == *"zsh"* ]]; then + candidates+=("$HOME/.zshrc") + fi + if [[ -n "$BASH_VERSION" ]] || [[ "$SHELL" == *"bash"* ]]; then + if [[ "$os_name" == "darwin" ]]; then + candidates+=("$HOME/.bash_profile") + else + candidates+=("$HOME/.bashrc") + fi + fi + if [[ "$SHELL" == *"fish"* ]]; then + candidates+=("$HOME/.config/fish/config.fish") + fi + + for file in "${candidates[@]}"; do + if [[ -f "$file" ]] && grep -q "$install_dir" "$file" 2>/dev/null; then + local tmp + tmp=$(mktemp) + # Remove the comment marker and PATH export line we added + grep -v "Drip client" "$file" | grep -v "$install_dir" > "$tmp" || true + mv "$tmp" "$file" + fi + done +} + +remove_config_dirs() { + local removed=false + local dirs=("$HOME/.drip" "$HOME/.config/drip") + + echo "" + prompt_input "$(msg remove_config_prompt) [y/N]: " remove_choice + if [[ ! "$remove_choice" =~ ^[Yy]$ ]]; then + return + fi + + for dir in "${dirs[@]}"; do + if [[ -d "$dir" ]]; then + rm -rf "$dir" + removed=true + fi + done + + if [[ "$removed" == true ]]; then + print_success "$(msg config_removed)" + fi +} + +uninstall_client() { + if ! command -v drip &> /dev/null; then + print_warning "$(msg uninstall_not_found)" + return + fi + + local current_path + current_path=$(command -v drip) + + prompt_input "$(msg uninstall_prompt) [y/N]: " confirm_uninstall + if [[ ! "$confirm_uninstall" =~ ^[Yy]$ ]]; then + return + fi + + print_step "$(msg uninstalling)" + + if [[ -w "$current_path" ]]; then + rm -f "$current_path" || true + else + sudo rm -f "$current_path" || true + fi + + print_success "$(msg uninstall_done)" + + local install_dir + install_dir=$(dirname "$current_path") + print_step "$(msg path_cleanup)" + cleanup_path_entries "$install_dir" + print_success "$(msg path_cleanup_done)" + + remove_config_dirs +} + # ============================================================================ # Main # ============================================================================ main() { + if [[ "$1" == "--uninstall" || "$1" == "uninstall" ]]; then + UNINSTALL_MODE=true + fi + clear print_banner select_language echo -e "${BOLD}────────────────────────────────────────────${NC}" + if [[ "$UNINSTALL_MODE" == true ]]; then + uninstall_client + exit 0 + fi + check_os check_arch check_dependencies @@ -648,6 +832,7 @@ main() { download_binary select_install_dir install_binary + ensure_command_access update_path verify_installation @@ -658,10 +843,7 @@ main() { else echo "" local new_version=$(get_version_from_binary "$INSTALL_DIR/$BINARY_NAME") - echo -e "${GREEN}╔════════════════════════════════════════════════════════════╗${NC}" - echo -e "${GREEN}║ $(msg update_ok) ${GREEN}║${NC}" - echo -e "${GREEN}╚════════════════════════════════════════════════════════════╝${NC}" - echo "" + print_panel "$(msg update_ok)" print_info "Version: $new_version" echo "" return diff --git a/scripts/uninstall.sh b/scripts/uninstall.sh new file mode 100644 index 0000000..da23346 --- /dev/null +++ b/scripts/uninstall.sh @@ -0,0 +1,448 @@ +#!/bin/bash +set -euo pipefail + +# Drip unified uninstaller (client + server) + +# ----------------------------------------------------------------------------- +# Colors +# ----------------------------------------------------------------------------- +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' + +# ----------------------------------------------------------------------------- +# Runtime options +# ----------------------------------------------------------------------------- +LANG_CODE="${LANG_CODE:-zh}" # zh | en (UI language) +AUTO_YES=false # true => auto-confirm prompts +TARGET="prompt" # client | server | both | prompt + +# Prefer prompting/reading from real TTY to avoid "stuck" when piped (curl | bash) +TTY="/dev/tty" + +has_tty() { [[ -e "$TTY" ]] && [[ -r "$TTY" ]] && [[ -w "$TTY" ]]; } + +say() { echo -e "$*"; } +say_info() { say "${BLUE}[INFO]${NC} $*"; } +say_ok() { say "${GREEN}[✓]${NC} $*"; } +say_warn() { say "${YELLOW}[!]${NC} $*"; } +say_err() { say "${RED}[✗]${NC} $*"; } + +# ----------------------------------------------------------------------------- +# TTY helpers +# ----------------------------------------------------------------------------- +# Write raw text to TTY if available (so prompts are always visible and never +# interpreted as printf format strings). +tty_put() { + if has_tty; then + printf "%b" "$1" > "$TTY" + else + printf "%b" "$1" + fi +} + +# Read user input from TTY first, fallback to stdin +tty_read() { + local __var="$1" + local __tmp="" + if has_tty; then + IFS= read -r __tmp < "$TTY" || true + else + IFS= read -r __tmp || true + fi + printf -v "$__var" "%s" "$__tmp" +} + +# ----------------------------------------------------------------------------- +# i18n messages (UI strings only) +# ----------------------------------------------------------------------------- +msg() { + local key="$1" + if [[ "$LANG_CODE" == "zh" ]]; then + case "$key" in + title) echo "Drip 卸载脚本" ;; + select_target) echo "选择卸载对象" ;; + opt_client) echo "客户端" ;; + opt_server) echo "服务器端" ;; + opt_both) echo "客户端 + 服务器端" ;; + confirm) echo "确认卸载?" ;; + removing_client) echo "卸载客户端..." ;; + removing_server) echo "卸载服务器端..." ;; + done) echo "卸载完成" ;; + remove_config) echo "删除配置目录?" ;; + remove_data) echo "删除数据/日志目录?" ;; + no_binary) echo "未发现可执行文件,跳过二进制删除" ;; + stop_service) echo "停止并禁用 systemd 服务..." ;; + remove_service) echo "删除 systemd 服务文件..." ;; + clean_path) echo "清理 PATH 配置..." ;; + skip) echo "跳过" ;; + select_lang) echo "选择语言" ;; + lang_en) echo "English" ;; + lang_zh) echo "中文" ;; + target_label) echo "目标" ;; + need_root) echo "卸载服务器需要 root 权限(请用 sudo 运行)" ;; + still_delete_shared) echo "检测到与服务器共用同一二进制,仍然删除?" ;; + *) echo "$key" ;; + esac + else + case "$key" in + title) echo "Drip Uninstaller" ;; + select_target) echo "Select what to uninstall" ;; + opt_client) echo "Client" ;; + opt_server) echo "Server" ;; + opt_both) echo "Client + Server" ;; + confirm) echo "Proceed with uninstall?" ;; + removing_client) echo "Removing client..." ;; + removing_server) echo "Removing server..." ;; + done) echo "Uninstall completed" ;; + remove_config) echo "Remove config directory as well?" ;; + remove_data) echo "Remove data/log directory as well?" ;; + no_binary) echo "No binary found, skipping binary removal" ;; + stop_service) echo "Stopping and disabling systemd service..." ;; + remove_service) echo "Removing systemd service file..." ;; + clean_path) echo "Cleaning PATH entries..." ;; + skip) echo "Skip" ;; + select_lang) echo "Select language" ;; + lang_en) echo "English" ;; + lang_zh) echo "中文" ;; + target_label) echo "Target" ;; + need_root) echo "Server uninstall requires root (run with sudo)" ;; + still_delete_shared) echo "Server uses the same binary. Delete anyway?" ;; + *) echo "$key" ;; + esac + fi +} + +repeat_char() { printf "%*s" "$2" "" | tr ' ' "$1"; } + +print_panel() { + local title="$1"; shift || true + local width=58 + local bar; bar="$(repeat_char "=" "$width")" + say "" + say "${CYAN}${bar}${NC}" + say "${CYAN}${title}${NC}" + say "${CYAN}${bar}${NC}" + for line in "$@"; do say " $line"; done + say "${CYAN}${bar}${NC}" + say "" +} + +print_subheader() { + local title="$1" + local width=58 + local bar; bar="$(repeat_char "-" "$width")" + say "" + say "${CYAN}${title}${NC}" + say "${CYAN}${bar}${NC}" +} + +print_menu() { + local title="$1"; shift + say "" + say "${CYAN}------------------------------${NC}" + say "${CYAN}${title}${NC}" + say "${CYAN}------------------------------${NC}" + for line in "$@"; do say " $line"; done + say "" +} + +# Ask an interactive y/n question (always visible even when piped) +prompt_yes() { + local prompt="$1" + local default_no="${2:-false}" # true => default No + + if [[ "$AUTO_YES" == true ]]; then + echo "y"; return + fi + + local suffix="[Y/n]" + local default_ans="y" + if [[ "$default_no" == "true" ]]; then + suffix="[y/N]" + default_ans="n" + fi + + while true; do + tty_put "${prompt} ${suffix} " + local ans; tty_read ans + ans="$(echo "${ans}" | tr '[:upper:]' '[:lower:]' | tr -d ' \t')" + + if [[ -z "$ans" ]]; then + echo "$default_ans"; return + fi + case "$ans" in + y|yes) echo "y"; return ;; + n|no) echo "n"; return ;; + *) say_warn "Please enter y or n." ;; + esac + done +} + +# ----------------------------------------------------------------------------- +# Privilege helpers +# ----------------------------------------------------------------------------- +need_root() { [[ "${EUID:-0}" -eq 0 ]]; } + +run_root() { + if need_root; then + "$@" + else + command -v sudo >/dev/null 2>&1 || return 1 + sudo "$@" + fi +} + +# Remove a file/dir, using sudo when needed +remove_path() { + local path="$1" + [[ -z "$path" ]] && return 0 + [[ ! -e "$path" ]] && return 0 + + if [[ -w "$path" ]]; then + rm -rf "$path" || true + else + run_root rm -rf "$path" || true + fi +} + +# Clean PATH entries by removing a dedicated marked block (safer than grep -v dir) +cleanup_shell_rc() { + local candidates=("$HOME/.zshrc" "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.config/fish/config.fish") + for file in "${candidates[@]}"; do + [[ -f "$file" ]] || continue + if grep -q ">>> Drip client" "$file" 2>/dev/null; then + local tmp; tmp="$(mktemp)" + awk ' + />>> Drip client/ {inblock=1; next} + /<<< Drip client/ {inblock=0; next} + !inblock {print} + ' "$file" > "$tmp" || true + mv "$tmp" "$file" || true + fi + done +} + +# Locate client binary "drip" (best-effort) +find_client_binary() { + local candidate="" + candidate="$(command -v drip 2>/dev/null || true)" + if [[ -n "$candidate" ]] && [[ -x "$candidate" ]]; then + echo "$candidate"; return + fi + for candidate in "/usr/local/bin/drip" "/usr/bin/drip" "/opt/drip/drip" "$HOME/.local/bin/drip"; do + [[ -x "$candidate" ]] && { echo "$candidate"; return; } + done +} + +# Extract actual binary path from systemd ExecStart (server install truth source) +get_systemd_exec_binary() { + local unit="drip-server.service" + command -v systemctl >/dev/null 2>&1 || return 0 + local val; val="$(systemctl show -p ExecStart "$unit" --value 2>/dev/null || true)" + [[ -z "$val" ]] && return 0 + + # Typical format includes "path=/path/to/bin" + local bin + bin="$(echo "$val" | sed -n 's/.*path=\([^ ;]*\).*/\1/p' | head -n1)" + [[ -n "$bin" ]] && echo "$bin" +} + +# Get real unit file path, if present +get_systemd_unit_path() { + local unit="drip-server.service" + command -v systemctl >/dev/null 2>&1 || return 0 + systemctl show -p FragmentPath "$unit" --value 2>/dev/null || true +} + +select_language() { + # Avoid blocking when no TTY (e.g., non-interactive CI) + if ! has_tty; then return; fi + + print_panel "$(msg select_lang)" \ + "${GREEN}1)${NC} $(msg lang_en)" \ + "${GREEN}2)${NC} $(msg lang_zh)" + tty_put "Select [1]: " + local choice; tty_read choice + case "$choice" in + 2) LANG_CODE="zh" ;; + *) LANG_CODE="en" ;; + esac +} + +select_target() { + # If no TTY, default to client to avoid hang + if ! has_tty; then + TARGET="client" + return + fi + + print_menu "$(msg select_target)" \ + "${GREEN}1)${NC} $(msg opt_client)" \ + "${GREEN}2)${NC} $(msg opt_server)" \ + "${GREEN}3)${NC} $(msg opt_both)" + tty_put "Select [1]: " + local choice; tty_read choice + case "$choice" in + 2) TARGET="server" ;; + 3) TARGET="both" ;; + *) TARGET="client" ;; + esac +} + +remove_client() { + print_subheader "$(msg removing_client)" + + local binary_path="" + binary_path="$(find_client_binary || true)" + + # If server uses the same binary, avoid accidental removal unless user confirms + local server_bin="" + server_bin="$(get_systemd_exec_binary || true)" + + if [[ -n "$binary_path" ]]; then + if [[ -n "$server_bin" ]] && [[ "$server_bin" == "$binary_path" ]]; then + say_warn "Detected shared binary with server: $binary_path" + if [[ "$(prompt_yes "$(msg still_delete_shared)" true)" == "y" ]]; then + remove_path "$binary_path" + say_ok "Removed binary: $binary_path" + else + say_info "$(msg skip)" + fi + else + remove_path "$binary_path" + say_ok "Removed binary: $binary_path" + fi + + cleanup_shell_rc + say_ok "$(msg clean_path)" + else + say_warn "$(msg no_binary)" + fi + + if [[ "$(prompt_yes "$(msg remove_config)" true)" == "y" ]]; then + remove_path "$HOME/.drip" + remove_path "$HOME/.config/drip" + say_ok "Client config removed" + else + say_info "$(msg skip)" + fi +} + +remove_server() { + print_subheader "$(msg removing_server)" + + if ! need_root; then + say_warn "$(msg need_root)" + fi + + local unit="drip-server.service" + + if command -v systemctl >/dev/null 2>&1; then + say_info "$(msg stop_service)" + run_root systemctl stop drip-server 2>/dev/null || true + run_root systemctl disable drip-server 2>/dev/null || true + fi + + say_info "$(msg remove_service)" + local unit_path="" + unit_path="$(get_systemd_unit_path || true)" + + # Remove unit file from common locations + if [[ -n "$unit_path" ]] && [[ -e "$unit_path" ]]; then + remove_path "$unit_path" + fi + remove_path "/etc/systemd/system/${unit}" + remove_path "/lib/systemd/system/${unit}" + remove_path "/usr/lib/systemd/system/${unit}" + + if command -v systemctl >/dev/null 2>&1; then + run_root systemctl daemon-reload 2>/dev/null || true + run_root systemctl reset-failed 2>/dev/null || true + fi + + # Remove server binary using the real ExecStart path first + local server_bin="" + server_bin="$(get_systemd_exec_binary || true)" + if [[ -n "$server_bin" ]]; then + remove_path "$server_bin" + say_ok "Removed server binary: $server_bin" + else + # Fallback guesses + remove_path "/usr/local/bin/drip-server" + remove_path "/usr/bin/drip-server" + remove_path "/usr/local/bin/drip" + fi + + if [[ "$(prompt_yes "$(msg remove_config)" true)" == "y" ]]; then + remove_path "/etc/drip" + say_ok "Server config removed" + else + say_info "$(msg skip)" + fi + + if [[ "$(prompt_yes "$(msg remove_data)" true)" == "y" ]]; then + remove_path "/var/lib/drip" + remove_path "/var/log/drip" + say_ok "Server data/log removed" + else + say_info "$(msg skip)" + fi +} + +parse_args() { + while [[ $# -gt 0 ]]; do + case "$1" in + --yes|-y) AUTO_YES=true ;; + --client) TARGET="client" ;; + --server) TARGET="server" ;; + --all|--both) TARGET="both" ;; + --lang=*) LANG_CODE="${1#*=}" ;; + --lang) shift; LANG_CODE="${1:-$LANG_CODE}" ;; + *) ;; + esac + shift + done +} + +main() { + parse_args "$@" + + # Note: If you run via `curl ... | bash`, TTY prompts still work. + select_language + print_panel "$(msg title)" + + if [[ "$TARGET" == "prompt" ]]; then + select_target + fi + + local target_desc="" + case "$TARGET" in + client) target_desc="$(msg opt_client)" ;; + server) target_desc="$(msg opt_server)" ;; + both) target_desc="$(msg opt_both)" ;; + esac + print_subheader "$(msg target_label): ${target_desc}" + + # This is where older versions "looked stuck": it was waiting for y/n input. + if [[ "$(prompt_yes "$(msg confirm)" false)" != "y" ]]; then + say_info "$(msg skip)" + exit 0 + fi + + case "$TARGET" in + client) remove_client ;; + server) remove_server ;; + both) + remove_client + remove_server + ;; + esac + + say_ok "$(msg done)" +} + +main "$@"