feat: spawn name prompt + kebab resource naming across all clouds (#1507)

* feat: add spawn name prompt and project confirmation to GCP flow

Ask for spawn name upfront (before auth), derive kebab-case default for
VM naming, and confirm the current GCP project before using it.

New interaction order:
  1. Spawn name: "My Dev Box" → kebab "my-dev-box" exported as
     GCP_INSTANCE_NAME_KEBAB
  2. gcloud auth + project confirm: "Current project: X  Keep? [Y/n]"
     If no → project picker shown
  3. SSH key
  4. Machine type picker (existing)
  5. Zone picker (existing)
  6. Instance name prompt: "Instance name [my-dev-box]: "
     User can press Enter to accept or type a custom name

New functions:
  _to_kebab_case()         — lowercases, replaces non-alnum with hyphens
  _gcp_prompt_spawn_name() — prompts for display name, exports kebab default;
                             honours SPAWN_NAME env var set by CLI (--name flag)

Modified:
  _gcp_resolve_project()  — adds Y/n confirmation when project already set
  get_server_name()       — shows kebab default in prompt, accepts Enter
  cloud_authenticate()    — calls _gcp_prompt_spawn_name first

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

* feat: add spawn name prompt to all clouds via shared/common.sh

Move _to_kebab_case() and prompt_spawn_name() to shared/common.sh so all
clouds get upfront spawn name prompting and kebab-based resource naming.

shared/common.sh:
  + _to_kebab_case()    — "My Dev Box" → "my-dev-box"
  + prompt_spawn_name() — asks for display name, exports SPAWN_NAME_DISPLAY
                          and SPAWN_NAME_KEBAB; skips if already set;
                          honours SPAWN_NAME env var from CLI --name flag
  ~ get_resource_name() — replaces silent SPAWN_NAME fallback with a visible
                          prefilled default: "Enter server name [my-dev-box]: "

Per-cloud changes (cloud_authenticate gains prompt_spawn_name first):
  hetzner, fly, aws, daytona, digitalocean, sprite — one-line change each

gcp/lib/common.sh:
  - Remove _to_kebab_case()        (now in shared)
  - Remove _gcp_prompt_spawn_name() (now in shared as prompt_spawn_name)
  ~ cloud_authenticate: _gcp_prompt_spawn_name → prompt_spawn_name
  ~ get_server_name: simplified back to get_validated_server_name
    (shared get_resource_name now shows the kebab default in the prompt)

Result — every cloud shows this flow upfront:
  Spawn name (e.g. "My Dev Box"): My Claude Box
  ℹ Resource name: my-claude-box
  ...
  Enter server name [my-claude-box]: ⏎

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

* fix: use "Use project '...'?" instead of "Keep this project?" in GCP prompt

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 01:22:59 -05:00 committed by GitHub
parent ff261f3544
commit d5690a8b11
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 69 additions and 17 deletions

View file

@ -266,7 +266,7 @@ list_servers() {
# Cloud adapter interface
# ============================================================
cloud_authenticate() { ensure_aws_cli; ensure_ssh_key; }
cloud_authenticate() { prompt_spawn_name; ensure_aws_cli; ensure_ssh_key; }
cloud_provision() { create_server "$1"; }
cloud_wait_ready() { verify_server_connectivity "${LIGHTSAIL_SERVER_IP}"; wait_for_cloud_init "${LIGHTSAIL_SERVER_IP}" 60; }
cloud_run() { run_server "${LIGHTSAIL_SERVER_IP}" "$1"; }

View file

@ -325,7 +325,7 @@ list_servers() {
# Cloud adapter interface
# ============================================================
cloud_authenticate() { ensure_daytona_cli; ensure_daytona_token; }
cloud_authenticate() { prompt_spawn_name; ensure_daytona_cli; ensure_daytona_token; }
cloud_provision() { create_server "$1"; }
cloud_wait_ready() { wait_for_cloud_init; }
cloud_run() { run_server "$1"; }

View file

@ -259,7 +259,7 @@ for d in droplets:
# Cloud adapter interface
# ============================================================
cloud_authenticate() { register_cleanup_trap; ensure_do_token; ensure_ssh_key; }
cloud_authenticate() { prompt_spawn_name; register_cleanup_trap; ensure_do_token; ensure_ssh_key; }
cloud_provision() { create_server "$1"; }
cloud_wait_ready() { verify_server_connectivity "${DO_SERVER_IP}"; wait_for_cloud_init "${DO_SERVER_IP}" 60; }
cloud_run() { run_server "${DO_SERVER_IP}" "$1"; }

View file

@ -486,7 +486,7 @@ for a in apps:
# Cloud adapter interface
# ============================================================
cloud_authenticate() { ensure_fly_cli; ensure_fly_token; }
cloud_authenticate() { prompt_spawn_name; ensure_fly_cli; ensure_fly_token; }
cloud_provision() { create_server "$1"; }
cloud_wait_ready() { wait_for_cloud_init; }
cloud_run() { run_server "$1"; }

View file

@ -145,15 +145,25 @@ _gcp_pick_project() {
interactive_pick "GCP_PROJECT" "" "GCP projects" _gcp_project_options
}
# Resolve and export GCP_PROJECT — prompt interactively if not already set
# Resolve and export GCP_PROJECT — confirm existing or pick interactively
_gcp_resolve_project() {
# Check env var and gcloud config
local project="${GCP_PROJECT:-$(gcloud config get-value project 2>/dev/null)}"
if [[ "${project}" == "(unset)" ]]; then project=""; fi
# If not set, offer an interactive project picker
# When a project is already set, confirm before using it
if [[ -n "${project}" && "${SPAWN_NON_INTERACTIVE:-}" != "1" ]]; then
local confirm
confirm=$(safe_read "Use project '${project}'? [Y/n]: ") || confirm=""
confirm="${confirm:-y}"
if [[ "${confirm}" =~ ^[nN] ]]; then
project=""
fi
fi
# If not set (or user chose to change), offer an interactive project picker
if [[ -z "${project}" ]]; then
log_info "No GCP project configured — fetching your projects..."
log_info "Fetching your GCP projects..."
project=$(_gcp_pick_project)
fi
@ -373,7 +383,7 @@ list_servers() {
# Cloud adapter interface
# ============================================================
cloud_authenticate() { ensure_gcloud; ensure_ssh_key; }
cloud_authenticate() { prompt_spawn_name; ensure_gcloud; ensure_ssh_key; }
cloud_provision() { create_server "$1"; }
cloud_wait_ready() { verify_server_connectivity "${GCP_SERVER_IP}"; wait_for_cloud_init "${GCP_SERVER_IP}" 60; }
cloud_run() { run_server "${GCP_SERVER_IP}" "$1"; }

View file

@ -650,7 +650,7 @@ list_servers() {
# Cloud adapter interface
# ============================================================
cloud_authenticate() { ensure_hcloud_token; ensure_ssh_key; }
cloud_authenticate() { prompt_spawn_name; ensure_hcloud_token; ensure_ssh_key; }
cloud_provision() {
local exit_code=0
create_server "$1" || exit_code=$?

View file

@ -511,6 +511,46 @@ validated_read() {
done
}
# Convert a display name to a valid kebab-case resource identifier.
# "My Dev Box" → "my-dev-box" "Claude 2024!" → "claude-2024"
_to_kebab_case() {
printf '%s' "${1}" \
| tr '[:upper:]' '[:lower:]' \
| sed 's/[^a-z0-9-]/-/g' \
| sed 's/-\{2,\}/-/g' \
| sed 's/^-//;s/-$//'
}
# Ask for a human-readable spawn name upfront, then derive a kebab-case
# default used for resource naming on every cloud.
# Idempotent — safe to call multiple times; skips prompt if already done.
# Respects SPAWN_NAME when set by the CLI (e.g. spawn gcp claude --name "My Box").
# Exports: SPAWN_NAME_DISPLAY, SPAWN_NAME_KEBAB
prompt_spawn_name() {
# Already prompted this session — nothing to do
if [[ -n "${SPAWN_NAME_KEBAB:-}" ]]; then
return 0
fi
local display_name
if [[ -n "${SPAWN_NAME:-}" ]]; then
display_name="${SPAWN_NAME}"
log_info "Spawn name: ${display_name}"
else
echo "" >&2
display_name=$(safe_read 'Spawn name (e.g. "My Dev Box"): ') || display_name=""
[[ -z "${display_name}" ]] && display_name="spawn"
fi
local kebab
kebab=$(_to_kebab_case "${display_name}")
[[ -z "${kebab}" ]] && kebab="spawn"
export SPAWN_NAME_DISPLAY="${display_name}"
export SPAWN_NAME_KEBAB="${kebab}"
log_info "Resource name: ${kebab}"
}
# Generic function to get resource name from environment or prompt
# Usage: get_resource_name ENV_VAR_NAME PROMPT_TEXT
# Returns: Resource name via stdout
@ -520,22 +560,24 @@ get_resource_name() {
local prompt_text="${2}"
local resource_value="${!env_var_name}"
# First check platform-specific env var
# Platform-specific env var takes absolute precedence
if [[ -n "${resource_value}" ]]; then
log_info "Using ${prompt_text%:*} from environment: ${resource_value}"
echo "${resource_value}"
return 0
fi
# Then check for SPAWN_NAME (set by CLI)
if [[ -n "${SPAWN_NAME:-}" ]]; then
log_info "Using spawn name: ${SPAWN_NAME}"
echo "${SPAWN_NAME}"
return 0
# Show spawn name kebab as a pre-filled default (press Enter to accept)
local default_name="${SPAWN_NAME_KEBAB:-}"
local effective_prompt="${prompt_text}"
if [[ -n "${default_name}" ]]; then
effective_prompt="${prompt_text%:*} [${default_name}]: "
fi
local name
name=$(safe_read "${prompt_text}")
name=$(safe_read "${effective_prompt}") || name=""
[[ -z "${name}" && -n "${default_name}" ]] && name="${default_name}"
if [[ -z "${name}" ]]; then
log_error "${prompt_text%:*} is required but not provided"
log_error ""

View file

@ -358,7 +358,7 @@ destroy_server() {
# Wrapper for spawn_agent compatibility (sprite uses get_sprite_name)
get_server_name() { get_sprite_name; }
cloud_authenticate() { ensure_sprite_installed; ensure_sprite_authenticated; }
cloud_authenticate() { prompt_spawn_name; ensure_sprite_installed; ensure_sprite_authenticated; }
cloud_provision() {
SPRITE_NAME="$1"
ensure_sprite_exists "${SPRITE_NAME}"