From be72d573e1f54633c908d4d5c697c08917fcdaf6 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Sun, 22 Feb 2026 11:08:01 -0800 Subject: [PATCH] fix: resolve DigitalOcean token input validation bug (#1720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cli/src/shared/ui.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/cli/src/shared/ui.ts b/cli/src/shared/ui.ts index 967d5aa1..284b631d 100644 --- a/cli/src/shared/ui.ts +++ b/cli/src/shared/ui.ts @@ -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 | null = null; + +function getReadlineInterface(): ReturnType { + 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 { 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((resolve, reject) => { + const rl = getReadlineInterface(); + return new Promise((resolve) => { rl.question(question, (answer) => { - rl.close(); resolve(answer.trim()); }); - rl.on("error", reject); - rl.on("close", () => resolve("")); }); }