You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
fix(cli): an error thrown by an async command prints a Node stack trace #1270
When an async command action throws, the CLI prints a raw Node stack trace instead of one error line. src/index.ts ends with program.parse(). Commander does not await async actions under parse(), so a rejected action becomes an unhandled rejection. Node then prints the error with its stack and exits 1.
Before a fix in the #1084 work, every teamai session command on a user-scope install showed this. They threw No teamai project config found… with a stack trace. That case is fixed, but any other error an async action throws still reaches users the same way.
Expected
An error from any command prints one actionable line on stderr, and the command exits 1. A stack trace appears only with a debug flag or environment variable.
Suggested fix
Replace program.parse() with await program.parseAsync() inside one catch that prints error.message through the logger and sets process.exitCode = 1. Keep the stack behind the existing debug switch.
Commands that already call process.exit keep working. A test that spies on process.exit for a specific command (for example the session pull failure test in session-cmd.test.ts) is unaffected.
One correction to the repro path in the description: the boundary on main is already await program.parseAsync(), not a bare parse(), so the sync-parse framing no longer applies. The leak is on the async side. The boundary only special-cases NotInitializedError and rethrows everything else, so any command action that throws a plain Error escapes as an unhandled rejection and Node prints it under a cli.js:NNNN header followed by the whole stack. Current repro on main is teamai session list in a directory that is not initialised.
The fix moves that whole job to the boundary. reportCommandError logs the message as one line through log.error, sets process.exitCode = 1, and sends the stack to log.debug, which always appends to ~/.teamai/debug.log and reaches the console only under --verbose. reportNotInitialized is unchanged; the boundary is what now covers the commands that never wrapped themselves.
Verified with unit tests for each branch (message plus exit code, stack routed to debug, empty-message fallback, non-Error values) and a real-CLI e2e that spawns the built binary: one line on stderr, no cli.js: prefix, no at frames, empty stdout, and under -v the stack on stdout. Reverting only the src/index.ts change makes the new e2e fail 2/2, so the test is pinned to the fix rather than to the local environment.
Activity
Smilewithoutfalling commentedon Oct 22, 2026
Thanks — picked this up in #1271.
One correction to the repro path in the description: the boundary on
mainis alreadyawait program.parseAsync(), not a bareparse(), so the sync-parse framing no longer applies. The leak is on the async side. The boundary only special-casesNotInitializedErrorand rethrows everything else, so any command action that throws a plainErrorescapes as an unhandled rejection and Node prints it under acli.js:NNNNheader followed by the whole stack. Current repro onmainisteamai session listin a directory that is not initialised.The fix moves that whole job to the boundary.
reportCommandErrorlogs the message as one line throughlog.error, setsprocess.exitCode = 1, and sends the stack tolog.debug, which always appends to~/.teamai/debug.logand reaches the console only under--verbose.reportNotInitializedis unchanged; the boundary is what now covers the commands that never wrapped themselves.Verified with unit tests for each branch (message plus exit code, stack routed to debug, empty-message fallback, non-
Errorvalues) and a real-CLI e2e that spawns the built binary: one line on stderr, nocli.js:prefix, noatframes, empty stdout, and under-vthe stack on stdout. Reverting only thesrc/index.tschange makes the new e2e fail 2/2, so the test is pinned to the fix rather than to the local environment.fix(cli): report an async command error as one line, not a stack trace (