fix: resolve DigitalOcean token input validation bug (#1720)

Reuse a single readline interface across prompt() calls instead of
creating and closing a new one each time. In Bun, repeatedly calling
createInterface/close on the same stdin causes the "close" event to
fire immediately on subsequent interfaces, which resolved the prompt
with an empty string before the user could type — triggering "Token
cannot be empty".

Fixes #1707

Agent: code-health

Co-authored-by: B <6723574+louisgv@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
A 2026-02-22 11:08:01 -08:00 committed by GitHub
parent 3204ce8eec
commit be72d573e1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -26,19 +26,29 @@ export function logStep(msg: string): void {
process.stderr.write(`${CYAN}${msg}${NC}\n`);
}
// Shared readline interface — reused across prompt() calls to avoid Bun's
// issue where repeatedly creating/closing interfaces on the same stdin causes
// the "close" event to fire immediately on subsequent interfaces (#1707).
let sharedRl: ReturnType<typeof createInterface> | null = null;
function getReadlineInterface(): ReturnType<typeof createInterface> {
if (!sharedRl) {
sharedRl = createInterface({ input: process.stdin, output: process.stderr });
sharedRl.on("close", () => { sharedRl = null; });
}
return sharedRl;
}
/** Prompt for a line of user input. Throws if non-interactive. */
export async function prompt(question: string): Promise<string> {
if (process.env.SPAWN_NON_INTERACTIVE === "1") {
throw new Error("Cannot prompt: SPAWN_NON_INTERACTIVE is set");
}
const rl = createInterface({ input: process.stdin, output: process.stderr });
return new Promise<string>((resolve, reject) => {
const rl = getReadlineInterface();
return new Promise<string>((resolve) => {
rl.question(question, (answer) => {
rl.close();
resolve(answer.trim());
});
rl.on("error", reject);
rl.on("close", () => resolve(""));
});
}