feat: add spawn pick to shared _display_and_select (Hetzner + all clouds) (#1505)

* feat: add spawn pick to _display_and_select in shared/common.sh

All clouds using interactive_pick (Hetzner, DigitalOcean, AWS, fly, etc.)
now get the arrow-key picker UI when the user runs via `spawn`.

Placement: between fzf (rarely installed) and numbered list (plain fallback).
Priority: fzf > spawn pick > numbered list.

Pipe-delimited items "id|field2|field3..." are converted to tab-delimited
"id\tid\tfield2 · field3 · ..." so spawn pick displays:
  > cx22  2 vCPU · 4.0 GB RAM · 40 GB disk · shared · $ 0.0057/hr
  > fsn1  Falkenstein · DE

The --default flag uses default_id when set, otherwise default_value,
so the correct item is pre-selected when the picker opens.

No 2>/dev/tty redirect (avoids the zsh 'file exists' failure that broke
the GCP picker; spawn pick opens /dev/tty internally via fs.openSync).

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* refactor: replace custom _gcp_interactive_pick with shared interactive_pick

- Remove _gcp_interactive_pick (60 lines of custom picker logic)
- Convert option functions to pipe-delimited format (id|detail)
  to match what interactive_pick / _display_and_select expect
- Replace _gcp_pick_{machine_type,zone,project} with direct
  interactive_pick calls — same pattern as Hetzner
- _gcp_project_options: awk now outputs id|name instead of id\tid\tname

GCP now gets fzf → spawn pick → numbered list for free via the
shared helper, with no cloud-specific picker code.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
L 2026-02-20 00:59:00 -05:00 committed by GitHub
parent d8785708c9
commit ff261f3544
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 47 additions and 74 deletions

View file

@ -94,101 +94,55 @@ _gcp_check_auth() {
# Interactive pickers for GCP project, zone, and machine type
# ============================================================
# Curated list of popular GCP machine types (value\tLabel\tHint)
# Curated list of popular GCP machine types (id|detail)
_gcp_machine_type_options() {
printf '%s\n' \
"e2-micro e2-micro Shared CPU · 2 vCPU · 1 GB RAM (~\$7/mo)" \
"e2-small e2-small Shared CPU · 2 vCPU · 2 GB RAM (~\$14/mo)" \
"e2-medium e2-medium Shared CPU · 2 vCPU · 4 GB RAM (~\$28/mo) ← default" \
"e2-standard-2 e2-standard-2 2 vCPU · 8 GB RAM (~\$49/mo)" \
"e2-standard-4 e2-standard-4 4 vCPU · 16 GB RAM (~\$98/mo)" \
"n2-standard-2 n2-standard-2 2 vCPU · 8 GB RAM, higher perf (~\$72/mo)" \
"n2-standard-4 n2-standard-4 4 vCPU · 16 GB RAM, higher perf (~\$144/mo)" \
"c4-standard-2 c4-standard-2 2 vCPU · 8 GB RAM, latest gen (~\$82/mo)"
"e2-micro|Shared CPU · 2 vCPU · 1 GB RAM (~\$7/mo)" \
"e2-small|Shared CPU · 2 vCPU · 2 GB RAM (~\$14/mo)" \
"e2-medium|Shared CPU · 2 vCPU · 4 GB RAM (~\$28/mo)" \
"e2-standard-2|2 vCPU · 8 GB RAM (~\$49/mo)" \
"e2-standard-4|4 vCPU · 16 GB RAM (~\$98/mo)" \
"n2-standard-2|2 vCPU · 8 GB RAM, higher perf (~\$72/mo)" \
"n2-standard-4|4 vCPU · 16 GB RAM, higher perf (~\$144/mo)" \
"c4-standard-2|2 vCPU · 8 GB RAM, latest gen (~\$82/mo)"
}
# Curated list of popular GCP zones (value\tLabel\tHint)
# Curated list of popular GCP zones (id|location)
_gcp_zone_options() {
printf '%s\n' \
"us-central1-a us-central1-a Iowa, US ← default" \
"us-east1-b us-east1-b South Carolina, US" \
"us-east4-a us-east4-a N. Virginia, US" \
"us-west1-a us-west1-a Oregon, US" \
"us-west2-a us-west2-a Los Angeles, US" \
"northamerica-northeast1-a northamerica-northeast1-a Montreal, Canada" \
"europe-west1-b europe-west1-b Belgium" \
"europe-west4-a europe-west4-a Netherlands" \
"europe-west6-a europe-west6-a Zurich, Switzerland" \
"asia-east1-a asia-east1-a Taiwan" \
"asia-southeast1-a asia-southeast1-a Singapore" \
"australia-southeast1-a australia-southeast1-a Sydney, Australia"
"us-central1-a|Iowa, US" \
"us-east1-b|South Carolina, US" \
"us-east4-a|N. Virginia, US" \
"us-west1-a|Oregon, US" \
"us-west2-a|Los Angeles, US" \
"northamerica-northeast1-a|Montreal, Canada" \
"europe-west1-b|Belgium" \
"europe-west4-a|Netherlands" \
"europe-west6-a|Zurich, Switzerland" \
"asia-east1-a|Taiwan" \
"asia-southeast1-a|Singapore" \
"australia-southeast1-a|Sydney, Australia"
}
# Fetch active GCP projects accessible to the authenticated user (value\tLabel\tHint)
# Fetch active GCP projects accessible to the authenticated user (id|name)
_gcp_project_options() {
gcloud projects list \
--filter="lifecycleState=ACTIVE" \
--format="value(projectId,name)" \
2>/dev/null | \
awk -F'\t' '{ print $1 "\t" $1 "\t" $2 }'
}
# Generic GCP interactive picker.
# Respects the named env var (skip picker if already set).
# Tries `spawn pick` for a nice arrow-key UI, falls back to a numbered list.
#
# Usage: _gcp_interactive_pick DISPLAY_NAME ENV_VAR_NAME DEFAULT OPTIONS_FN
# Outputs the selected value on stdout.
_gcp_interactive_pick() {
local display="${1}" # e.g. "GCP machine type"
local env_var="${2}" # e.g. "GCP_MACHINE_TYPE"
local default_val="${3}" # e.g. "e2-medium"
local options_fn="${4}" # function name that prints "value\tLabel\tHint" lines
# Honour an explicit env var override — no prompt needed
local current_val
current_val="${!env_var:-}"
if [[ -n "${current_val}" ]]; then
echo "${current_val}"
return
fi
# Fetch available options
local options_text
options_text=$("${options_fn}")
if [[ -z "${options_text}" ]]; then
log_warn "Could not list ${display} options — using default: ${default_val}"
echo "${default_val}"
return
fi
# Try `spawn pick` for a nicer arrow-key UI (available when user ran `spawn`)
if command -v spawn >/dev/null 2>&1; then
local picked
picked=$(printf '%s\n' "${options_text}" | \
spawn pick --prompt "Select ${display}" --default "${default_val}") && {
echo "${picked}"
return
}
fi
# Fallback: shared/common.sh numbered-list selector
# Convert "value\tLabel\tHint" → "value|Label" for _display_and_select
local items
items=$(printf '%s\n' "${options_text}" | awk -F'\t' '{ print $1 "|" $2 }')
_display_and_select "${display}" "${default_val}" "${default_val}" <<< "${items}"
awk -F'\t' '{ print $1 "|" $2 }'
}
_gcp_pick_machine_type() {
_gcp_interactive_pick "GCP machine type" "GCP_MACHINE_TYPE" "e2-medium" "_gcp_machine_type_options"
interactive_pick "GCP_MACHINE_TYPE" "e2-medium" "GCP machine types" _gcp_machine_type_options "e2-medium"
}
_gcp_pick_zone() {
_gcp_interactive_pick "GCP zone" "GCP_ZONE" "us-central1-a" "_gcp_zone_options"
interactive_pick "GCP_ZONE" "us-central1-a" "GCP zones" _gcp_zone_options "us-central1-a"
}
_gcp_pick_project() {
_gcp_interactive_pick "GCP project" "GCP_PROJECT" "" "_gcp_project_options"
interactive_pick "GCP_PROJECT" "" "GCP projects" _gcp_project_options
}
# Resolve and export GCP_PROJECT — prompt interactively if not already set

View file

@ -3459,7 +3459,26 @@ _display_and_select() {
return
fi
# Fallback to numbered list when fzf is not available
# Try spawn pick for an arrow-key UI (available when the user ran `spawn`)
if command -v spawn >/dev/null 2>&1; then
# Convert pipe-delimited "id|label|extra..." → "id\tid\tlabel · extra · ..."
# so spawn pick shows the id as label and all detail fields as hint.
local spawn_input
spawn_input=$(printf '%s\n' "${items_array[@]}" | awk -F'|' '{
val=$1; hint="";
for (i=2; i<=NF; i++) { hint = hint (hint ? " \xc2\xb7 " : "") $i }
printf "%s\t%s\t%s\n", val, val, hint
}')
local picked
local spawn_default="${default_id:-${default_value}}"
picked=$(printf '%s\n' "${spawn_input}" | \
spawn pick --prompt "Select ${prompt_text}" --default "${spawn_default}") && {
echo "${picked}"
return
}
fi
# Fallback to numbered list when neither fzf nor spawn pick is available
_numbered_list_select "${prompt_text}" "${default_value}" "${default_id}" "${items_array[@]}"
}