Skip to content

fix(interactive): keep the shell alive on Ctrl+C at a y/n prompt - #475

Merged
AndyTWF merged 1 commit into
mainfrom
claude/clever-lehmann-30b2b9
Oct 21, 2026
Merged

AndyTWF merged 1 commit into
mainfrom
claude/clever-lehmann-30b2b9

Conversation

@AndyTWF

@AndyTWF AndyTWF commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Why

Inside ably interactive, pressing Ctrl+C at a [y/n] confirmation (for example push devices remove dev1) exited the whole shell with code 0. It should cancel that one command and return to ably>, the way Ctrl+C at a running command already does.

Root cause

The original suspicion was the shell's readline close handler. It isn't that:

  • In wrapper mode that handler exits 42, but the shell exited 0.
  • --trace-exit shows no process.exit call.

What actually happens:

  1. promptForConfirmation creates its own readline on stdin with no SIGINT listener.
  2. With no listener, readline handles Ctrl+C by silently closing the interface, so the rl.question promise never settles.
  3. The shell's readline is paused while a command runs, and handleCommand is fire-and-forget. Nothing keeps the event loop alive, so Node drains it and exits with code 0.

Fix

On Ctrl+C, promptForConfirmation now closes its interface and rejects with an InterruptError ("Interrupted by user"). Callers already send rejections through fail(), and the interactive shell's catch block prints the message and redraws the prompt. interactive.ts needed no change.

Rejected alternative: resolving false (treating Ctrl+C as "no"). Several callers carry on when the answer is no: login's "create your first app?", "running the control plane locally?" and "use a different alias?", base-command's "remove invalid key?", and init's global install. For those, Ctrl+C would have meant "no, keep going".

Outside interactive mode, Ctrl+C at a confirm now exits 1 with "Interrupted by user". Before this change it exited 0 silently, for the same drain reason.

Interaction with #474 (DX-1967)

#474 adds forwardSigintToProcess(rl) to this prompt: on Ctrl+C it closes the prompt and re-raises the signal as a process SIGINT. That alone doesn't settle the promise, which is why this bug reproduced with it applied. Whichever PR merges second needs to combine the two:

If two separate classes survive the merge, the prompt's rejection reaches withSigintCancellation's race before the re-raised signal does. Login's instanceof InterruptError check would then miss it, and login would exit 1 instead of 130.

Tests

  • New TTY test test/tty/commands/interactive-confirm-sigint.test.ts:
    • runs under node-pty with test mode off;
    • presses Ctrl+C at the push devices remove confirm;
    • asserts "Interrupted by user" followed by ably>;
    • checks the shell then still runs version and exits 42 on exit.
    • It fails on main (the shell exits 0).
  • New unit test: the prompt rejects with InterruptError and closes its interface on SIGINT.

Not fixed here

Answering n + Enter at the same prompt in interactive mode echoes nn, and the shell then runs n as a command ("Command n not found"). The paused shell readline still has its line listener attached. That is a separate bug, to be fixed in its own PR.

🤖 Generated with Claude Code

Pressing Ctrl+C at a promptForConfirmation prompt inside `ably interactive`
exited the whole shell with code 0 instead of cancelling the command.

With no SIGINT listener, readline handles Ctrl+C by silently closing the
confirm's interface, so its promise never settles. The shell's readline is
paused while a command runs, so nothing keeps the event loop alive and Node
drains it and exits. It was not the shell's readline 'close' handler: that
would exit 42 in wrapper mode, and --trace-exit shows no process.exit call.

The prompt now closes and rejects with an InterruptError on Ctrl+C. Callers
already route rejections through fail(), and the shell's catch returns to
the prompt. It rejects rather than answering "no" because several callers
(login, invalid-key removal, init) carry on when the answer is no.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vercel

vercel Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cli-web-cli Ready Ready Preview Oct 21, 2026 9:38am UTC

Request Review

@claude-code-ably-assistant

Copy link
Copy Markdown

Walkthrough

This PR fixes a bug in ably interactive where pressing Ctrl+C at a [y/n] confirmation prompt (e.g. push devices remove) exits the entire shell with code 0 instead of cancelling the one command and returning to the ably> prompt. The fix adds a SIGINT listener to promptForConfirmation's internal readline interface that closes it and rejects with a new InterruptError, propagating cleanly through the existing error-handling path in the interactive shell. No changes to interactive.ts were required.

Changes

Area Files Summary
Utils src/utils/prompt-confirmation.ts Added InterruptError class and SIGINT listener that rejects the prompt promise instead of leaving it unsettled
Tests test/tty/commands/interactive-confirm-sigint.test.ts New TTY test (node-pty) verifying shell survives Ctrl+C at confirmation and returns to ably>
Tests test/unit/utils/prompt-confirmation.test.ts New unit test verifying InterruptError is thrown and readline is closed on SIGINT
Docs docs/Exit-Codes.md Documents that Ctrl+C at a y/n prompt cancels the command and returns to prompt
Docs docs/Project-Structure.md Updated directory comment for test/tty/commands/ to reflect the new test

Review Notes

@AndyTWF
AndyTWF requested a review from splindsay-92 October 21, 2026 11:36
@AndyTWF
AndyTWF merged commit 3cbf595 into main Oct 21, 2026
13 checks passed
@AndyTWF
AndyTWF deleted the claude/clever-lehmann-30b2b9 branch October 21, 2026 12:59

This branch was successfully deployed

1 active deployment
Preview — 36bb0f1b Deployed Oct 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants