fix: eliminate duplicate name prompts, use cloud-native terminology (#1755)

* fix: eliminate duplicate name prompts, use cloud-native terminology

Users were prompted for a name up to 4 times per spawn. Now each cloud
has a single prompt using its native resource terminology (e.g. "Hetzner
server name", "Fly machine name") and getServerName() returns the
already-collected name silently instead of re-prompting.

Closes #1753

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

* fix: never use bare "spawn" as default name, always append random suffix

Extract defaultSpawnName() helper to shared/ui.ts that generates
"spawn-xxxx" with a random suffix. All cloud modules now use it
instead of bare "spawn" for every fallback path.

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

---------

Co-authored-by: lab <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
A 2026-02-22 14:20:47 -08:00 committed by GitHub
parent d1b6a20535
commit 7c37a793de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 72 additions and 216 deletions

View file

@ -13,6 +13,7 @@ import {
validateServerName,
validateRegionName,
toKebabCase,
defaultSpawnName,
} from "../shared/ui";
import type { CloudInitTier } from "../shared/agents";
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
@ -805,47 +806,25 @@ export async function getServerName(): Promise<string> {
const kebab = process.env.SPAWN_NAME_KEBAB
|| (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
const defaultName = kebab || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return defaultName;
}
const answer = await prompt(`Enter instance name [${defaultName}]: `);
const name = answer || defaultName;
if (!validateServerName(name)) {
logError(`Invalid instance name: '${name}'`);
throw new Error("Invalid server name");
}
return name;
return kebab || defaultSpawnName();
}
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) return;
let displayName: string;
let kebab: string;
if (process.env.SPAWN_NAME) {
displayName = process.env.SPAWN_NAME;
logInfo(`Spawn name: ${displayName}`);
kebab = toKebabCase(process.env.SPAWN_NAME) || defaultSpawnName();
} else if (process.env.SPAWN_NON_INTERACTIVE === "1") {
displayName = "spawn";
kebab = defaultSpawnName();
} else {
const fallback = defaultSpawnName();
process.stderr.write("\n");
displayName = await prompt('Spawn name (e.g. "My Dev Box"): ');
if (!displayName) displayName = "spawn";
const answer = await prompt(`AWS instance name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
let kebab = toKebabCase(displayName) || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE !== "1") {
const confirmed = await prompt(`Resource name [${kebab}]: `);
if (confirmed) {
kebab = toKebabCase(confirmed) || "spawn";
}
}
process.env.SPAWN_NAME_DISPLAY = displayName;
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}

View file

@ -10,6 +10,7 @@ import {
jsonEscape,
validateServerName,
toKebabCase,
defaultSpawnName,
} from "../shared/ui";
import type { CloudInitTier } from "../shared/agents";
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
@ -519,47 +520,25 @@ export async function getServerName(): Promise<string> {
const kebab = process.env.SPAWN_NAME_KEBAB
|| (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
const defaultName = kebab || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return defaultName;
}
const answer = await prompt(`Enter sandbox name [${defaultName}]: `);
const name = answer || defaultName;
if (!validateServerName(name)) {
logError(`Invalid sandbox name: '${name}'`);
throw new Error("Invalid server name");
}
return name;
return kebab || defaultSpawnName();
}
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) return;
let displayName: string;
let kebab: string;
if (process.env.SPAWN_NAME) {
displayName = process.env.SPAWN_NAME;
logInfo(`Spawn name: ${displayName}`);
kebab = toKebabCase(process.env.SPAWN_NAME) || defaultSpawnName();
} else if (process.env.SPAWN_NON_INTERACTIVE === "1") {
displayName = "spawn";
kebab = defaultSpawnName();
} else {
const fallback = defaultSpawnName();
process.stderr.write("\n");
displayName = await prompt('Spawn name (e.g. "My Dev Box"): ');
if (!displayName) displayName = "spawn";
const answer = await prompt(`Daytona workspace name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
let kebab = toKebabCase(displayName) || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE !== "1") {
const confirmed = await prompt(`Resource name [${kebab}]: `);
if (confirmed) {
kebab = toKebabCase(confirmed) || "spawn";
}
}
process.env.SPAWN_NAME_DISPLAY = displayName;
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}

View file

@ -11,6 +11,7 @@ import {
validateServerName,
validateRegionName,
toKebabCase,
defaultSpawnName,
} from "../shared/ui";
import type { CloudInitTier } from "../shared/agents";
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
@ -945,47 +946,25 @@ export async function getServerName(): Promise<string> {
const kebab = process.env.SPAWN_NAME_KEBAB
|| (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
const defaultName = kebab || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return defaultName;
}
const answer = await prompt(`Enter droplet name [${defaultName}]: `);
const name = answer || defaultName;
if (!validateServerName(name)) {
logError(`Invalid droplet name: '${name}'`);
throw new Error("Invalid server name");
}
return name;
return kebab || defaultSpawnName();
}
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) return;
let displayName: string;
let kebab: string;
if (process.env.SPAWN_NAME) {
displayName = process.env.SPAWN_NAME;
logInfo(`Spawn name: ${displayName}`);
kebab = toKebabCase(process.env.SPAWN_NAME) || defaultSpawnName();
} else if (process.env.SPAWN_NON_INTERACTIVE === "1") {
displayName = "spawn";
kebab = defaultSpawnName();
} else {
const fallback = defaultSpawnName();
process.stderr.write("\n");
displayName = await prompt('Spawn name (e.g. "My Dev Box"): ');
if (!displayName) displayName = "spawn";
const answer = await prompt(`DigitalOcean droplet name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
let kebab = toKebabCase(displayName) || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE !== "1") {
const confirmed = await prompt(`Resource name [${kebab}]: `);
if (confirmed) {
kebab = toKebabCase(confirmed) || "spawn";
}
}
process.env.SPAWN_NAME_DISPLAY = displayName;
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}

View file

@ -12,6 +12,7 @@ import {
validateServerName,
validateRegionName,
toKebabCase,
defaultSpawnName,
} from "../shared/ui";
import type { CloudInitTier } from "../shared/agents";
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
@ -908,51 +909,27 @@ export async function getServerName(): Promise<string> {
return name;
}
// Derive from spawn name
const kebab = process.env.SPAWN_NAME_KEBAB
|| (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
const defaultName = kebab || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return defaultName;
}
const answer = await prompt(`Enter app name [${defaultName}]: `);
const name = answer || defaultName;
if (!validateServerName(name)) {
logError(`Invalid app name: '${name}'`);
throw new Error("Invalid server name");
}
return name;
return kebab || defaultSpawnName();
}
/** Prompt for a spawn display name, derive kebab-case resource name. */
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) return;
let displayName: string;
let kebab: string;
if (process.env.SPAWN_NAME) {
displayName = process.env.SPAWN_NAME;
logInfo(`Spawn name: ${displayName}`);
kebab = toKebabCase(process.env.SPAWN_NAME) || defaultSpawnName();
} else if (process.env.SPAWN_NON_INTERACTIVE === "1") {
displayName = "spawn";
kebab = defaultSpawnName();
} else {
const fallback = defaultSpawnName();
process.stderr.write("\n");
displayName = await prompt('Spawn name (e.g. "My Dev Box"): ');
if (!displayName) displayName = "spawn";
const answer = await prompt(`Fly machine name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
let kebab = toKebabCase(displayName) || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE !== "1") {
const confirmed = await prompt(`Resource name [${kebab}]: `);
if (confirmed) {
kebab = toKebabCase(confirmed) || "spawn";
}
}
process.env.SPAWN_NAME_DISPLAY = displayName;
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}

View file

@ -11,6 +11,7 @@ import {
jsonEscape,
validateServerName,
toKebabCase,
defaultSpawnName,
} from "../shared/ui";
import type { CloudInitTier } from "../shared/agents";
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
@ -379,48 +380,25 @@ export async function getServerName(): Promise<string> {
const kebab = process.env.SPAWN_NAME_KEBAB
|| (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
const defaultName = kebab || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return defaultName;
}
const answer = await prompt(`Enter instance name [${defaultName}]: `);
const name = answer || defaultName;
if (!validateServerName(name)) {
logError(`Invalid instance name: '${name}'`);
throw new Error("Invalid server name");
}
return name;
return kebab || defaultSpawnName();
}
/** Prompt for a spawn display name, derive kebab-case resource name. */
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) return;
let displayName: string;
let kebab: string;
if (process.env.SPAWN_NAME) {
displayName = process.env.SPAWN_NAME;
logInfo(`Spawn name: ${displayName}`);
kebab = toKebabCase(process.env.SPAWN_NAME) || defaultSpawnName();
} else if (process.env.SPAWN_NON_INTERACTIVE === "1") {
displayName = "spawn";
kebab = defaultSpawnName();
} else {
const fallback = defaultSpawnName();
process.stderr.write("\n");
displayName = await prompt('Spawn name (e.g. "My Dev Box"): ');
if (!displayName) displayName = "spawn";
const answer = await prompt(`GCP instance name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
let kebab = toKebabCase(displayName) || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE !== "1") {
const confirmed = await prompt(`Resource name [${kebab}]: `);
if (confirmed) {
kebab = toKebabCase(confirmed) || "spawn";
}
}
process.env.SPAWN_NAME_DISPLAY = displayName;
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}

View file

@ -11,6 +11,7 @@ import {
validateServerName,
validateRegionName,
toKebabCase,
defaultSpawnName,
} from "../shared/ui";
import type { CloudInitTier } from "../shared/agents";
import { getPackagesForTier, needsNode, needsBun, NODE_INSTALL_CMD } from "../shared/cloud-init";
@ -576,47 +577,25 @@ export async function getServerName(): Promise<string> {
const kebab = process.env.SPAWN_NAME_KEBAB
|| (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
const defaultName = kebab || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return defaultName;
}
const answer = await prompt(`Enter server name [${defaultName}]: `);
const name = answer || defaultName;
if (!validateServerName(name)) {
logError(`Invalid server name: '${name}'`);
throw new Error("Invalid server name");
}
return name;
return kebab || defaultSpawnName();
}
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) return;
let displayName: string;
let kebab: string;
if (process.env.SPAWN_NAME) {
displayName = process.env.SPAWN_NAME;
logInfo(`Spawn name: ${displayName}`);
kebab = toKebabCase(process.env.SPAWN_NAME) || defaultSpawnName();
} else if (process.env.SPAWN_NON_INTERACTIVE === "1") {
displayName = "spawn";
kebab = defaultSpawnName();
} else {
const fallback = defaultSpawnName();
process.stderr.write("\n");
displayName = await prompt('Spawn name (e.g. "My Dev Box"): ');
if (!displayName) displayName = "spawn";
const answer = await prompt(`Hetzner server name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
let kebab = toKebabCase(displayName) || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE !== "1") {
const confirmed = await prompt(`Resource name [${kebab}]: `);
if (confirmed) {
kebab = toKebabCase(confirmed) || "spawn";
}
}
process.env.SPAWN_NAME_DISPLAY = displayName;
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}

View file

@ -141,3 +141,9 @@ export function toKebabCase(name: string): string {
.replace(/-{2,}/g, "-")
.replace(/^-|-$/g, "");
}
/** Generate a default spawn name with random suffix (e.g. "spawn-a1b2"). */
export function defaultSpawnName(): string {
const suffix = Math.random().toString(36).slice(2, 6);
return `spawn-${suffix}`;
}

View file

@ -5,7 +5,7 @@ import {
ensureSpriteCli,
ensureSpriteAuthenticated,
promptSpawnName,
getSpriteName,
getServerName,
createSprite,
verifySpriteConnectivity,
setupShellEnvironment,
@ -45,7 +45,7 @@ async function main() {
await setupShellEnvironment();
saveVmConnection();
},
getServerName: getSpriteName,
getServerName,
async waitForReady() {},
interactiveSession,
saveLaunchCmd,

View file

@ -9,6 +9,7 @@ import {
prompt,
validateServerName,
toKebabCase,
defaultSpawnName,
} from "../shared/ui";
// ─── Configurable Constants ──────────────────────────────────────────────────
@ -193,33 +194,24 @@ function orgFlags(): string[] {
export async function promptSpawnName(): Promise<void> {
if (process.env.SPAWN_NAME_KEBAB) return;
let displayName: string;
let kebab: string;
if (process.env.SPAWN_NAME) {
displayName = process.env.SPAWN_NAME;
logInfo(`Spawn name: ${displayName}`);
kebab = toKebabCase(process.env.SPAWN_NAME) || defaultSpawnName();
} else if (process.env.SPAWN_NON_INTERACTIVE === "1") {
displayName = "spawn";
kebab = defaultSpawnName();
} else {
const fallback = defaultSpawnName();
process.stderr.write("\n");
displayName = await prompt('Spawn name (e.g. "My Dev Box"): ');
if (!displayName) displayName = "spawn";
const answer = await prompt(`Sprite name [${fallback}]: `);
kebab = toKebabCase(answer || fallback) || defaultSpawnName();
}
let kebab = toKebabCase(displayName) || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE !== "1") {
const confirmed = await prompt(`Resource name [${kebab}]: `);
if (confirmed) {
kebab = toKebabCase(confirmed) || "spawn";
}
}
process.env.SPAWN_NAME_DISPLAY = displayName;
process.env.SPAWN_NAME_DISPLAY = kebab;
process.env.SPAWN_NAME_KEBAB = kebab;
logInfo(`Using resource name: ${kebab}`);
}
export async function getSpriteName(): Promise<string> {
export async function getServerName(): Promise<string> {
if (process.env.SPRITE_NAME) {
const name = process.env.SPRITE_NAME;
if (!validateServerName(name)) {
@ -232,20 +224,7 @@ export async function getSpriteName(): Promise<string> {
const kebab = process.env.SPAWN_NAME_KEBAB
|| (process.env.SPAWN_NAME ? toKebabCase(process.env.SPAWN_NAME) : "");
const defaultName = kebab || "spawn";
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
return defaultName;
}
const answer = await prompt(`Enter sprite name [${defaultName}]: `);
const name = answer || defaultName;
if (!validateServerName(name)) {
logError(`Invalid sprite name: '${name}'`);
throw new Error("Invalid server name");
}
return name;
return kebab || defaultSpawnName();
}
// ─── Provisioning ────────────────────────────────────────────────────────────