Skip to content

fix(auth): show the interactive login prompt instead of erasing it#39

Open
narthur wants to merge 1 commit into
stephendolan:mainfrom
narthur:fix/auth-login-stdin-hang
Open

fix(auth): show the interactive login prompt instead of erasing it#39
narthur wants to merge 1 commit into
stephendolan:mainfrom
narthur:fix/auth-login-stdin-hang

Conversation

@narthur
Copy link
Copy Markdown

@narthur narthur commented Jun 5, 2026

Problem

On an interactive TTY, ynab auth login looks like it hangs: no prompt appears, just a blank line with a blinking cursor. Reported on Warp; reproducible on any terminal that applies cursor/erase escape sequences promptly.

It isn't actually a non-TTY/stdin issue — stdin, stdout, and stderr are all TTYs in the affected sessions. The bug is in the interactive prompt itself.

Root cause

const rl = createInterface({ input: process.stdin, output: process.stderr });
process.stderr.write('Enter YNAB Personal Access Token: ');  // written outside readline
rl.question('', (answer) => { ... });                        // empty query

The prompt is written directly to the output stream, then readline.question('') is called with an empty query. readline owns the current line and, when it renders, emits ESC[1G (cursor to column 1) + ESC[0J (erase to end of screen) — wiping the just-written prompt.

Captured from the real binary under a PTY, before any input:

Enter YNAB Personal Access Token: \x1b[1G\x1b[0J \x1b[1G
                                   └─ readline clears the line, erasing the prompt

Result: the prompt flashes and vanishes; the user sees nothing and assumes it's stuck. (Input still works — piping/typing a token authenticates — but nothing signals that a token is expected.)

Fix

Pass the prompt as readline's question() query so readline reprints it after its own clear instead of erasing a string it doesn't know about:

const rl = createInterface({ input, output });
rl.question('Enter YNAB Personal Access Token: ', (answer) => { ... });

Same binary under a PTY after the fix — clear first, prompt second, surviving on screen with the cursor parked after it:

\x1b[1G\x1b[0J Enter YNAB Personal Access Token: \x1b[35G

promptForToken() and readTokenFromStdin() now accept injectable streams (defaulting to the real stdio) so the prompt-rendering and stdin-parsing paths can be unit-tested, and the empty-token error gains an actionable hint.

Tests

Adds src/commands/auth.test.ts: the prompt text reaches the output stream and the typed token is returned/trimmed; stdin piping resolves/trims, empty-EOF resolves empty, and stream errors reject.

✓ src/commands/auth.test.ts (5 tests)
Test Files  4 passed (4)
     Tests  35 passed (35)

bun run typecheck and bun run lint (oxlint) pass.

🤖 Generated with Claude Code

@narthur narthur marked this pull request as draft June 5, 2026 17:04
@narthur narthur force-pushed the fix/auth-login-stdin-hang branch from a3e88df to d61c14f Compare June 5, 2026 17:07
@narthur narthur changed the title fix(auth): don't hang forever on auth login with non-TTY stdin fix(auth): show the interactive login prompt instead of erasing it Jun 5, 2026
@narthur narthur marked this pull request as ready for review June 5, 2026 17:11
`ynab auth login` on a TTY appeared to hang with no prompt. The cause was
in promptForToken(): it wrote "Enter YNAB Personal Access Token: " to the
output stream itself and then called readline's `question('')` with an
empty query. readline owns the current line and, when it renders, emits
`ESC[1G ESC[0J` (cursor-home + clear-to-end-of-screen) — which erases the
pre-written prompt the instant it appears. On terminals that apply those
sequences promptly (Warp is one), the user sees a blank line with a
blinking cursor and no hint that a token is expected, so the command
looks dead. (Input actually still worked — typing a token and pressing
Enter authenticated — but nothing told the user to type.)

Pass the prompt as readline's `question()` query instead. readline then
reprints the prompt after its own clear, so it stays visible and the
cursor parks right after it.

Verified under a real PTY: the byte stream is now
`ESC[1G ESC[0J Enter YNAB Personal Access Token: ESC[35G` — clear first,
prompt second, surviving on screen.

Also makes promptForToken/readTokenFromStdin accept injectable streams so
the prompt-rendering and stdin-parsing behaviour can be unit-tested, and
gives the empty-token error an actionable hint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@narthur narthur force-pushed the fix/auth-login-stdin-hang branch from d61c14f to 5f15641 Compare June 5, 2026 17:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant