@@ -148,12 +148,26 @@ const hasJsonFlag = process.argv.includes("--json");
148148// exit handler is synchronous-only).
149149let _flush : ( ( ) => Promise < void > ) | undefined ;
150150let _flushSync : ( ( ) => void ) | undefined ;
151+ let _trackCliError :
152+ | ( ( props : {
153+ error_name : string ;
154+ error_message: string ;
155+ stack_trace ? : string ;
156+ command ? : string ;
157+ kind: "uncaught_exception" | "unhandled_rejection" | "command_error" ;
158+ } ) => void )
159+ | undefined ;
160+ let _trackCommandResult :
161+ | ( ( props : { command : string ; success: boolean ; exitCode: number ; durationMs: number } ) => void )
162+ | undefined ;
151163let _printUpdateNotice : ( ( ) => void ) | undefined ;
152164
153165if ( ! isHelp && command !== "telemetry" && command !== "unknown" ) {
154166 import ( "./telemetry/index.js" ) . then ( ( mod ) => {
155167 _flush = mod . flush ;
156168 _flushSync = mod . flushSync ;
169+ _trackCliError = mod . trackCliError ;
170+ _trackCommandResult = mod . trackCommandResult ;
157171 mod . showTelemetryNotice ( ) ;
158172 mod . trackCommand ( command ) ;
159173 if ( mod . shouldTrack ( ) ) mod . incrementCommandCount ( ) ;
@@ -176,6 +190,7 @@ if (!isHelp && !hasJsonFlag && command !== "upgrade") {
176190 } ) ;
177191}
178192
193+ const commandStart = Date . now ( ) ;
179194let commandFailed = false ;
180195
181196// Async flush for normal exit (beforeExit fires when the event loop drains)
@@ -184,65 +199,45 @@ process.on("beforeExit", () => {
184199 if ( ! hasJsonFlag ) _printUpdateNotice ?. ( ) ;
185200} ) ;
186201
187- // Sync flush for process.exit() calls (exit event only allows synchronous code).
188- // Also emits cli_command_result with the real exit code.
202+ // Sync-only: exit handlers cannot await promises or drain microtasks.
203+ // _trackCommandResult / _trackCliError are captured references resolved
204+ // at init time, so they're callable synchronously here.
189205process . on ( "exit" , ( code ) => {
190- try {
191- import ( "./telemetry/index.js" )
192- . then ( ( mod ) => {
193- mod . trackCommandResult ( {
194- command,
195- success : code === 0 && ! commandFailed ,
196- exitCode : code ,
197- durationMs : Date . now ( ) - commandStart ,
198- } ) ;
199- } )
200- . catch ( ( ) => { } ) ;
201- } catch {
202- // telemetry must never block exit
203- }
206+ _trackCommandResult ?. ( {
207+ command,
208+ success : code === 0 && ! commandFailed ,
209+ exitCode : code ,
210+ durationMs : Date . now ( ) - commandStart ,
211+ } ) ;
204212 _flushSync ?. ( ) ;
205213} ) ;
206214
207215process . on ( "uncaughtException" , ( error ) => {
208216 commandFailed = true ;
209- try {
210- import ( "./telemetry/index.js" )
211- . then ( ( mod ) => {
212- mod . trackCliError ( {
213- error_name : error . name ,
214- error_message : error . message ,
215- stack_trace : error . stack ,
216- command,
217- kind : "uncaught_exception" ,
218- } ) ;
219- } )
220- . catch ( ( ) => { } ) ;
221- } catch {
222- // telemetry must never suppress the crash
223- }
217+ _trackCliError ?. ( {
218+ error_name : error . name ,
219+ error_message : error . message ,
220+ stack_trace : error . stack ,
221+ command,
222+ kind : "uncaught_exception" ,
223+ } ) ;
224224 _flushSync ?. ( ) ;
225225 process . exit ( 1 ) ;
226226} ) ;
227227
228+ // unhandledRejection does not call process.exit() — Node may continue
229+ // running if the rejection is non-fatal (e.g. a fire-and-forget promise).
230+ // The exit handler above will still fire with the real exit code.
228231process . on ( "unhandledRejection" , ( reason ) => {
229232 commandFailed = true ;
230- try {
231- const error = reason instanceof Error ? reason : new Error ( String ( reason ) ) ;
232- import ( "./telemetry/index.js" )
233- . then ( ( mod ) => {
234- mod . trackCliError ( {
235- error_name : error . name ,
236- error_message : error . message ,
237- stack_trace : error . stack ,
238- command,
239- kind : "unhandled_rejection" ,
240- } ) ;
241- } )
242- . catch ( ( ) => { } ) ;
243- } catch {
244- // telemetry must never suppress the crash
245- }
233+ const error = reason instanceof Error ? reason : new Error ( String ( reason ) ) ;
234+ _trackCliError ?. ( {
235+ error_name : error . name ,
236+ error_message : error . message ,
237+ stack_trace : error . stack ,
238+ command,
239+ kind : "unhandled_rejection" ,
240+ } ) ;
246241} ) ;
247242
248243// Lazy-load help renderer — avoids allocating help data on non-help invocations
@@ -254,5 +249,4 @@ async function showUsage<T extends ArgsDef>(
254249 return impl ( cmd as CommandDef , parent as CommandDef | undefined ) ;
255250}
256251
257- const commandStart = Date . now ( ) ;
258252runMain ( main , { showUsage } ) ;
0 commit comments