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 <noreply@anthropic.com>
This commit is contained in:
A 2026-03-03 11:55:45 -08:00 committed by GitHub
parent 931fbed8b3
commit 0aea348b8f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<void> {
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<boolean> {
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}...`);