diff --git a/fly/lib/common.sh b/fly/lib/common.sh index ac2727d0..76ed4909 100644 --- a/fly/lib/common.sh +++ b/fly/lib/common.sh @@ -67,16 +67,7 @@ ensure_fly_cli() { log_info "flyctl CLI installed" } -# Ensure FLY_API_TOKEN is available (env var -> config file -> prompt+save) -# Save Fly.io token to config file -_save_fly_token() { - local token="$1" - local config_dir="$HOME/.config/spawn" - local config_file="$config_dir/fly.json" - mkdir -p "$config_dir" - printf '{\n "token": %s\n}\n' "$(json_escape "$token")" > "$config_file" - chmod 600 "$config_file" -} +# Ensure FLY_API_TOKEN is available (env var -> config file -> flyctl CLI -> prompt+save) # Try to get token from flyctl CLI if available _try_flyctl_auth() { @@ -117,58 +108,23 @@ _validate_fly_token() { } ensure_fly_token() { - check_python_available || return 1 - - # 1. Check environment variable - if [[ -n "${FLY_API_TOKEN:-}" ]]; then - log_info "Using Fly.io API token from environment" - return 0 - fi - - local config_file="$HOME/.config/spawn/fly.json" - - # 2. Check config file - if [[ -f "$config_file" ]]; then - local saved_token - saved_token=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1])).get('token',''))" "$config_file" 2>/dev/null) - if [[ -n "$saved_token" ]]; then - export FLY_API_TOKEN="$saved_token" - log_info "Using Fly.io API token from $config_file" + # Try flyctl CLI auth first (unique to Fly.io), then fall through to generic flow + if [[ -z "${FLY_API_TOKEN:-}" ]]; then + local token + token=$(_try_flyctl_auth 2>/dev/null) && { + export FLY_API_TOKEN="$token" + log_info "Using Fly.io API token from flyctl auth" + _save_token_to_config "$HOME/.config/spawn/fly.json" "$token" return 0 - fi + } fi - # 3. Try flyctl CLI auth - local token - token=$(_try_flyctl_auth) && { - export FLY_API_TOKEN="$token" - log_info "Using Fly.io API token from flyctl auth" - _save_fly_token "$token" - return 0 - } - - # 4. Prompt and validate - echo "" - log_warn "Fly.io API Token Required" - printf '%b\n' "${YELLOW}Get your token by running: fly tokens deploy${NC}" - printf '%b\n' "${YELLOW}Or create one at: https://fly.io/dashboard → Tokens${NC}" - echo "" - - token=$(safe_read "Enter your Fly.io API token: ") || return 1 - if [[ -z "$token" ]]; then - log_error "API token cannot be empty" - log_warn "For non-interactive usage, set: FLY_API_TOKEN=your-token" - return 1 - fi - - export FLY_API_TOKEN="$token" - if ! _validate_fly_token; then - unset FLY_API_TOKEN - return 1 - fi - - _save_fly_token "$token" - log_info "API token saved to $config_file" + ensure_api_token_with_provider \ + "Fly.io" \ + "FLY_API_TOKEN" \ + "$HOME/.config/spawn/fly.json" \ + "https://fly.io/dashboard → Tokens" \ + "_validate_fly_token" } # Get the Fly.io org slug (default: personal) diff --git a/hetzner/lib/common.sh b/hetzner/lib/common.sh index d6501c82..1b91d6dd 100755 --- a/hetzner/lib/common.sh +++ b/hetzner/lib/common.sh @@ -158,86 +158,16 @@ for loc in sorted(data.get('locations', []), key=lambda l: l['name']): # Interactive location picker (skipped if HETZNER_LOCATION is set) _pick_location() { - if [[ -n "${HETZNER_LOCATION:-}" ]]; then - echo "$HETZNER_LOCATION" - return - fi - - log_info "Fetching available locations..." - local locations - locations=$(_list_locations) - - if [[ -z "$locations" ]]; then - log_warn "Could not fetch locations, using default: fsn1" - echo "fsn1" - return - fi - - log_info "Available locations:" - local i=1 - local names=() - while IFS='|' read -r name city country; do - printf " %2d) %-6s %s, %s\n" "$i" "$name" "$city" "$country" >&2 - names+=("$name") - i=$((i + 1)) - done <<< "$locations" - - local choice - printf "\n" >&2 - choice=$(safe_read "Select location [1]: ") || choice="" - choice="${choice:-1}" - - if [[ "$choice" -ge 1 && "$choice" -le "${#names[@]}" ]] 2>/dev/null; then - echo "${names[$((choice - 1))]}" - else - log_warn "Invalid choice, using default: fsn1" - echo "fsn1" - fi + interactive_pick "HETZNER_LOCATION" "fsn1" "locations" _list_locations } # Interactive server type picker (skipped if HETZNER_SERVER_TYPE is set) _pick_server_type() { local location="$1" - - if [[ -n "${HETZNER_SERVER_TYPE:-}" ]]; then - echo "$HETZNER_SERVER_TYPE" - return - fi - - log_info "Fetching server types available in ${location}..." - local types - types=$(_list_server_types_for_location "$location") - - if [[ -z "$types" ]]; then - log_warn "Could not fetch server types, using default: cpx11" - echo "cpx11" - return - fi - - log_info "Available server types in ${location}:" - local i=1 - local names=() - local default_idx=1 - while IFS='|' read -r name cores ram disk cpu price; do - printf " %2d) %-10s %-8s %-10s %-12s %-8s %s\n" "$i" "$name" "$cores" "$ram" "$disk" "$cpu" "$price" >&2 - names+=("$name") - if [[ "$name" == "cpx11" ]]; then - default_idx=$i - fi - i=$((i + 1)) - done <<< "$types" - - local choice - printf "\n" >&2 - choice=$(safe_read "Select server type [${default_idx}]: ") || choice="" - choice="${choice:-$default_idx}" - - if [[ "$choice" -ge 1 && "$choice" -le "${#names[@]}" ]] 2>/dev/null; then - echo "${names[$((choice - 1))]}" - else - log_warn "Invalid choice, using default: cpx11" - echo "cpx11" - fi + # Wrap the location-specific list function for interactive_pick + _list_server_types_for_current_location() { _list_server_types_for_location "$location"; } + interactive_pick "HETZNER_SERVER_TYPE" "cpx11" "server types" _list_server_types_for_current_location "cpx11" + unset -f _list_server_types_for_current_location } # Create a Hetzner server with cloud-init diff --git a/hostinger/lib/common.sh b/hostinger/lib/common.sh index c2c1e267..a78061db 100644 --- a/hostinger/lib/common.sh +++ b/hostinger/lib/common.sh @@ -160,84 +160,12 @@ for loc in sorted(data.get('locations', []), key=lambda l: l.get('name', '')): # Interactive location picker (skipped if HOSTINGER_LOCATION is set) _pick_location() { - if [[ -n "${HOSTINGER_LOCATION:-}" ]]; then - echo "$HOSTINGER_LOCATION" - return - fi - - log_info "Fetching available locations..." - local locations - locations=$(_list_locations) - - if [[ -z "$locations" ]]; then - log_warn "Could not fetch locations, using default: eu-central" - echo "eu-central" - return - fi - - log_info "Available locations:" - local i=1 - local ids=() - while IFS='|' read -r id name country; do - printf " %2d) %-15s %s, %s\n" "$i" "$id" "$name" "$country" >&2 - ids+=("$id") - i=$((i + 1)) - done <<< "$locations" - - local choice - printf "\n" >&2 - choice=$(safe_read "Select location [1]: ") || choice="" - choice="${choice:-1}" - - if [[ "$choice" -ge 1 && "$choice" -le "${#ids[@]}" ]] 2>/dev/null; then - echo "${ids[$((choice - 1))]}" - else - log_warn "Invalid choice, using default: eu-central" - echo "eu-central" - fi + interactive_pick "HOSTINGER_LOCATION" "eu-central" "locations" _list_locations } # Interactive VPS plan picker (skipped if HOSTINGER_PLAN is set) _pick_plan() { - if [[ -n "${HOSTINGER_PLAN:-}" ]]; then - echo "$HOSTINGER_PLAN" - return - fi - - log_info "Fetching available VPS plans..." - local plans - plans=$(_list_vps_plans) - - if [[ -z "$plans" ]]; then - log_warn "Could not fetch plans, using default: kvm1" - echo "kvm1" - return - fi - - log_info "Available VPS plans:" - local i=1 - local ids=() - local default_idx=1 - while IFS='|' read -r id name vcpus ram disk price; do - printf " %2d) %-10s %-20s %-8s %-12s %-12s %s\n" "$i" "$id" "$name" "$vcpus" "$ram" "$disk" "$price" >&2 - ids+=("$id") - if [[ "$id" == "kvm1" ]]; then - default_idx=$i - fi - i=$((i + 1)) - done <<< "$plans" - - local choice - printf "\n" >&2 - choice=$(safe_read "Select plan [${default_idx}]: ") || choice="" - choice="${choice:-$default_idx}" - - if [[ "$choice" -ge 1 && "$choice" -le "${#ids[@]}" ]] 2>/dev/null; then - echo "${ids[$((choice - 1))]}" - else - log_warn "Invalid choice, using default: kvm1" - echo "kvm1" - fi + interactive_pick "HOSTINGER_PLAN" "kvm1" "VPS plans" _list_vps_plans "kvm1" } # Create a Hostinger VPS with cloud-init diff --git a/render/lib/common.sh b/render/lib/common.sh index 2d90eeaf..27952435 100644 --- a/render/lib/common.sh +++ b/render/lib/common.sh @@ -67,55 +67,14 @@ ensure_render_cli() { log_info "Render CLI installed" } -# Save Render API key to config file -_save_render_api_key() { - local api_key="$1" - local config_dir="$HOME/.config/spawn" - local config_file="$config_dir/render.json" - mkdir -p "$config_dir" - printf '{\n "api_key": "%s"\n}\n' "$(json_escape "$api_key")" > "$config_file" - chmod 600 "$config_file" -} - # Ensure RENDER_API_KEY is available (env var -> config file -> prompt+save) ensure_render_api_key() { - check_python_available || return 1 - - # 1. Check environment variable - if [[ -n "${RENDER_API_KEY:-}" ]]; then - log_info "Using Render API key from environment" - return 0 - fi - - local config_file="$HOME/.config/spawn/render.json" - - # 2. Check config file - if [[ -f "$config_file" ]]; then - local saved_key - saved_key=$(python3 -c "import json, sys; print(json.load(open(sys.argv[1])).get('api_key',''))" "$config_file" 2>/dev/null) - if [[ -n "$saved_key" ]]; then - export RENDER_API_KEY="$saved_key" - log_info "Using Render API key from $config_file" - return 0 - fi - fi - - # 3. Prompt user for API key - log_warn "Render API key required" - echo "" - echo "Get your API key at: https://dashboard.render.com/u/settings/api-keys" - echo "" - - local api_key - api_key=$(safe_read "Enter Render API key: ") - if [[ -z "$api_key" ]]; then - log_error "No API key provided" - return 1 - fi - - export RENDER_API_KEY="$api_key" - _save_render_api_key "$api_key" - log_info "Render API key saved" + ensure_api_token_with_provider \ + "Render" \ + "RENDER_API_KEY" \ + "$HOME/.config/spawn/render.json" \ + "https://dashboard.render.com/u/settings/api-keys" \ + "" } # Generate a unique server name for Render (must be lowercase alphanumeric + hyphens) diff --git a/shared/common.sh b/shared/common.sh index 54d1bce7..609e7269 100644 --- a/shared/common.sh +++ b/shared/common.sh @@ -1645,6 +1645,77 @@ setup_continue_config() { upload_config_file "${upload_callback}" "${run_callback}" "${continue_json}" "~/.continue/config.json" } +# ============================================================ +# Interactive selection helpers +# ============================================================ + +# Generic interactive picker for numbered menu selection +# Eliminates duplicate _pick_location/_pick_server_type patterns across providers +# +# Usage: interactive_pick ENV_VAR_NAME DEFAULT_VALUE PROMPT_TEXT LIST_CALLBACK [FORMAT_CALLBACK] +# +# Arguments: +# ENV_VAR_NAME - Environment variable to check first (e.g., "HETZNER_LOCATION") +# DEFAULT_VALUE - Default value if env var unset and list is empty or choice invalid +# PROMPT_TEXT - Label shown above the menu (e.g., "locations", "server types") +# LIST_CALLBACK - Function that outputs pipe-delimited lines (first field = ID) +# DEFAULT_ID - Optional: ID to pre-select as default (e.g., "cpx11") +# +# LIST_CALLBACK must output pipe-delimited lines where the first field is the selectable ID. +# Example output: "fsn1|Falkenstein|DE" or "cpx11|2 vCPU|4 GB RAM|40 GB disk" +# +# Returns: selected ID via stdout +interactive_pick() { + local env_var_name="${1}" + local default_value="${2}" + local prompt_text="${3}" + local list_callback="${4}" + local default_id="${5:-}" + + # Check environment variable first + local env_value="${!env_var_name:-}" + if [[ -n "${env_value}" ]]; then + echo "${env_value}" + return + fi + + log_info "Fetching available ${prompt_text}..." + local items + items=$("${list_callback}") + + if [[ -z "${items}" ]]; then + log_warn "Could not fetch ${prompt_text}, using default: ${default_value}" + echo "${default_value}" + return + fi + + log_info "Available ${prompt_text}:" + local i=1 + local ids=() + local default_idx=1 + while IFS= read -r line; do + local id="${line%%|*}" + printf " %2d) %s\n" "${i}" "$(echo "${line}" | tr '|' '\t')" >&2 + ids+=("${id}") + if [[ -n "${default_id}" && "${id}" == "${default_id}" ]]; then + default_idx=${i} + fi + i=$((i + 1)) + done <<< "${items}" + + local choice + printf "\n" >&2 + choice=$(safe_read "Select ${prompt_text%s} [${default_idx}]: ") || choice="" + choice="${choice:-${default_idx}}" + + if [[ "${choice}" -ge 1 && "${choice}" -le "${#ids[@]}" ]] 2>/dev/null; then + echo "${ids[$((choice - 1))]}" + else + log_warn "Invalid choice, using default: ${default_value}" + echo "${default_value}" + fi +} + # ============================================================ # SSH key registration helpers # ============================================================