From 0aea348b8f16ada8d8f7e82ec9d2c58f03944921 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Tue, 3 Mar 2026 11:55:45 -0800 Subject: [PATCH] fix(ux): stop spinner before credential prompts during delete (#2144) When credentials expire during server deletion, the spinner was running simultaneously with interactive credential prompts, creating confusing overlapping UI. Extract ensureDeleteCredentials() to run all credential checks (which may prompt the user) before starting the deletion spinner. All 6 cloud providers are covered: AWS, Hetzner, DigitalOcean, GCP, Daytona, and Sprite. Fixes #2141 Co-authored-by: B <6723574+louisgv@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- packages/cli/src/commands/delete.ts | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/packages/cli/src/commands/delete.ts b/packages/cli/src/commands/delete.ts index bce5ad58..45d13921 100644 --- a/packages/cli/src/commands/delete.ts +++ b/packages/cli/src/commands/delete.ts @@ -19,6 +19,55 @@ import { destroyServer as spriteDestroyServer, ensureSpriteCli, ensureSpriteAuth import { getErrorMessage, isInteractiveTTY } from "./shared.js"; import { resolveListFilters, activeServerPicker } from "./list.js"; +/** + * Ensure credentials are available for a record's cloud provider. + * This may prompt the user interactively and must be called BEFORE + * starting any spinner to avoid overlapping UI elements. + */ +export async function ensureDeleteCredentials(record: SpawnRecord): Promise { + const conn = record.connection; + if (!conn?.cloud || conn.cloud === "local") { + return; + } + + switch (conn.cloud) { + case "hetzner": + await ensureHcloudToken(); + break; + case "digitalocean": + await ensureDoToken(); + break; + case "gcp": { + const zone = conn.metadata?.zone || "us-central1-a"; + const project = conn.metadata?.project || ""; + validateMetadataValue(zone, "GCP zone"); + if (project) { + validateMetadataValue(project, "GCP project"); + } + process.env.GCP_ZONE = zone; + if (project) { + process.env.GCP_PROJECT = project; + } + await gcpEnsureGcloudCli(); + await gcpAuthenticate(); + break; + } + case "aws": + await ensureAwsCli(); + await awsAuthenticate(); + break; + case "daytona": + await ensureDaytonaToken(); + break; + case "sprite": + await ensureSpriteCli(); + await ensureSpriteAuthenticated(); + break; + default: + break; + } +} + /** Execute server deletion for a given record using TypeScript cloud modules */ export async function execDeleteServer(record: SpawnRecord): Promise { const conn = record.connection; @@ -148,6 +197,10 @@ export async function confirmAndDelete(record: SpawnRecord, manifest: Manifest | return false; } + // Ensure credentials before starting the spinner so interactive + // prompts (e.g. expired API key entry) don't overlap with it. + await ensureDeleteCredentials(record); + const s = p.spinner(); s.start(`Deleting ${label}...`);