fix(cli): interrupt running commands cleanly on Ctrl+C#1370
fix(cli): interrupt running commands cleanly on Ctrl+C#1370Jeppe Fredsgaard Blaabjerg (jfblaa) wants to merge 2 commits into
Conversation
The launcher (bin/cli.js) spawns the real CLI as a child with inherited stdio. Previously it was torn down by SIGINT before that child finished, so the child's final status printed after the shell prompt had already returned (and the child could be left running in the background). Wait briefly for the child to handle the interrupt itself and mirror its exit, so output stays ordered. The wait is bounded: if the child outlives a short grace period, or on a second Ctrl+C, SIGKILL it and exit with the conventional 128+signum code. The grace timer is unref'd, so a child that exits on its own is mirrored with no added latency.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 010d858. Configure here.
| // Mirror a signal death. Drop our own handlers first so the re-raise actually | ||
| // terminates us instead of being swallowed by onSigint/onSigterm above. | ||
| process.removeListener('SIGINT', onSigint) | ||
| process.removeListener('SIGTERM', onSigterm) |
There was a problem hiding this comment.
Wrong parent exit after signal kill
Medium Severity
When the child dies with a signal, the launcher re-raises it via process.kill instead of calling process.exit with 128 + signum. Because the new handlers keep the parent alive through the first interrupt, await spawnPromise can finish while process.exitCode is still 1, so the shell may report exit code 1 rather than 130/143 after Ctrl+C.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 010d858. Configure here.
There was a problem hiding this comment.
Good catch — fixed in 65534b2.
The on('exit') signal branch no longer re-raises via process.kill(process.pid, signalName). As you noted, with our SIGINT/SIGTERM handlers installed the re-raise raced await spawnPromise resolving and could let the process fall through to its default process.exitCode = 1, so Ctrl+C could report 1 instead of 130/143.
It now exits explicitly and deterministically with the conventional 128 + signum code (resolved via os.constants.signals[signalName]), matching the grace/second-signal path. Verified: a child that dies from SIGINT now yields parent exit 130 (previously 1).
Addresses review feedback: re-raising the child's signal on the launcher
raced against `await spawnPromise` resolving and could leave the default
`process.exitCode` of 1, so Ctrl+C could report exit 1 instead of 130.
Exit explicitly with the conventional 128+signum code (resolved from
os.constants.signals) in the on('exit') signal branch instead.


What
The launcher (
bin/cli.js) spawns the real CLI as a child process with inherited stdio. Previously the launcher was torn down bySIGINTbefore that child finished, so onCtrl+Cthe child's final status could print after the shell prompt had already returned — and the child could be left running in the background.This makes the launcher wait for the child to handle the interrupt itself and then mirror its exit, so output stays ordered.
How
SIGINT/SIGTERM, stay alive and let the child (which shares our process group and receives the signal directly) run its own teardown; exit by mirroring the child's real exit.SHUTDOWN_GRACE_MS, 3s), or on a secondCtrl+C,SIGKILLit and exit with the conventional128 + signumcode.unref'd, so a child that exits on its own is mirrored with no added latency — the cap only applies to a slow or wedged child.Notes
Note
Low Risk
Launcher-only process/signal plumbing in bin/cli.js; no auth, data, or business-logic changes, with bounded fallback via SIGKILL.
Overview
The CLI launcher (
bin/cli.js) now coordinates shutdown when you hit Ctrl+C or sendSIGTERM, instead of exiting before the spawned CLI child finishes.On the first signal it stays alive so the child (same process group, inherited stdio) can tear down and print final output in order; the parent then mirrors the child’s exit. If the child doesn’t exit within 3s, or you signal again, it SIGKILLs the child and exits with 130 (
SIGINT) or 143 (SIGTERM). The grace timer is unref’d so a normal fast exit isn’t delayed.When the child exits on a signal, the launcher removes its own handlers before re-raising so the parent actually terminates instead of swallowing the signal.
Reviewed by Cursor Bugbot for commit 010d858. Configure here.