1- import { isatty } from "node:tty" ;
2- import { run } from "@stricli/core" ;
3- import { app } from "./app.js" ;
4- import { buildContext } from "./context.js" ;
5- import { AuthError , formatError , getExitCode } from "./lib/errors.js" ;
6- import { error } from "./lib/formatters/colors.js" ;
7- import { runInteractiveLogin } from "./lib/interactive-login.js" ;
8- import { getEnvLogLevel , setLogLevel } from "./lib/logger.js" ;
9- import { isTrialEligible , promptAndStartTrial } from "./lib/seer-trial.js" ;
10- import { withTelemetry } from "./lib/telemetry.js" ;
11- import { startCleanupOldBinary } from "./lib/upgrade.js" ;
12- import {
13- abortPendingVersionCheck ,
14- getUpdateNotification ,
15- maybeCheckForUpdateInBackground ,
16- shouldSuppressNotification ,
17- } from "./lib/version-check.js" ;
1+ /**
2+ * CLI entry point with fast-path dispatch.
3+ *
4+ * Shell completion (`__complete`) is dispatched before any heavy imports
5+ * to avoid loading `@sentry/node-core` (~280ms). All other commands go through
6+ * the full CLI with telemetry, middleware, and error recovery.
7+ */
188
199// Exit cleanly when downstream pipe consumer closes (e.g., `sentry issue list | head`).
2010// EPIPE (errno -32) is normal Unix behavior — not an error. Node.js/Bun ignore SIGPIPE
@@ -30,6 +20,20 @@ function handleStreamError(err: NodeJS.ErrnoException): void {
3020process . stdout . on ( "error" , handleStreamError ) ;
3121process . stderr . on ( "error" , handleStreamError ) ;
3222
23+ /**
24+ * Fast-path: shell completion.
25+ *
26+ * Dispatched before importing the full CLI to avoid loading @sentry/node-core,
27+ * @stricli /core, and other heavy dependencies. Only loads the lightweight
28+ * completion engine and SQLite cache modules.
29+ */
30+ async function runCompletion ( completionArgs : string [ ] ) : Promise < void > {
31+ // Disable telemetry so db/index.ts skips the @sentry /node-core lazy-require (~280ms)
32+ process . env . SENTRY_CLI_NO_TELEMETRY = "1" ;
33+ const { handleComplete } = await import ( "./lib/complete.js" ) ;
34+ await handleComplete ( completionArgs ) ;
35+ }
36+
3337/**
3438 * Error-recovery middleware for the CLI.
3539 *
@@ -46,125 +50,146 @@ process.stderr.on("error", handleStreamError);
4650 * @returns A function with the same signature, with error recovery added
4751 */
4852type ErrorMiddleware = (
49- next : ( argv : string [ ] ) => Promise < void > ,
50- args : string [ ]
53+ proceed : ( cmdInput : string [ ] ) => Promise < void > ,
54+ retryArgs : string [ ]
5155) => Promise < void > ;
5256
5357/**
54- * Seer trial prompt middleware.
58+ * Full CLI execution with telemetry, middleware, and error recovery .
5559 *
56- * Catches trial-eligible SeerErrors and offers to start a free trial.
57- * On success, retries the original command. On failure/decline, re-throws
58- * so the outer error handler displays the full error with upgrade URL.
60+ * All heavy imports are loaded here (not at module top level) so the
61+ * `__complete` fast-path can skip them entirely.
5962 */
60- const seerTrialMiddleware : ErrorMiddleware = async ( next , args ) => {
61- try {
62- await next ( args ) ;
63- } catch ( err ) {
64- if ( isTrialEligible ( err ) ) {
65- const started = await promptAndStartTrial (
66- // biome-ignore lint/style/noNonNullAssertion: isTrialEligible guarantees orgSlug is defined
67- err . orgSlug ! ,
68- err . reason
69- ) ;
70-
71- if ( started ) {
72- process . stderr . write ( "\nRetrying command...\n\n" ) ;
73- await next ( args ) ;
74- return ;
63+ async function runCli ( cliArgs : string [ ] ) : Promise < void > {
64+ const { isatty } = await import ( "node:tty" ) ;
65+ const { run } = await import ( "@stricli/core" ) ;
66+ const { app } = await import ( "./app.js" ) ;
67+ const { buildContext } = await import ( "./context.js" ) ;
68+ const { AuthError, formatError, getExitCode } = await import (
69+ "./lib/errors.js"
70+ ) ;
71+ const { error } = await import ( "./lib/formatters/colors.js" ) ;
72+ const { runInteractiveLogin } = await import ( "./lib/interactive-login.js" ) ;
73+ const { getEnvLogLevel, setLogLevel } = await import ( "./lib/logger.js" ) ;
74+ const { isTrialEligible, promptAndStartTrial } = await import (
75+ "./lib/seer-trial.js"
76+ ) ;
77+ const { withTelemetry } = await import ( "./lib/telemetry.js" ) ;
78+ const { startCleanupOldBinary } = await import ( "./lib/upgrade.js" ) ;
79+ const {
80+ abortPendingVersionCheck,
81+ getUpdateNotification,
82+ maybeCheckForUpdateInBackground,
83+ shouldSuppressNotification,
84+ } = await import ( "./lib/version-check.js" ) ;
85+
86+ // ---------------------------------------------------------------------------
87+ // Error-recovery middleware
88+ // ---------------------------------------------------------------------------
89+
90+ /**
91+ * Seer trial prompt middleware.
92+ *
93+ * Catches trial-eligible SeerErrors and offers to start a free trial.
94+ * On success, retries the original command. On failure/decline, re-throws
95+ * so the outer error handler displays the full error with upgrade URL.
96+ */
97+ const seerTrialMiddleware : ErrorMiddleware = async ( next , argv ) => {
98+ try {
99+ await next ( argv ) ;
100+ } catch ( err ) {
101+ if ( isTrialEligible ( err ) ) {
102+ const started = await promptAndStartTrial (
103+ // biome-ignore lint/style/noNonNullAssertion: isTrialEligible guarantees orgSlug is defined
104+ err . orgSlug ! ,
105+ err . reason
106+ ) ;
107+
108+ if ( started ) {
109+ process . stderr . write ( "\nRetrying command...\n\n" ) ;
110+ await next ( argv ) ;
111+ return ;
112+ }
75113 }
114+ throw err ;
76115 }
77- throw err ;
78- }
79- } ;
80-
81- /**
82- * Auto-authentication middleware.
83- *
84- * Catches auth errors (not_authenticated, expired) in interactive TTYs
85- * and runs the login flow. On success, retries through the full middleware
86- * chain so inner middlewares (e.g., trial prompt) also apply to the retry.
87- */
88- const autoAuthMiddleware : ErrorMiddleware = async ( next , args ) => {
89- try {
90- await next ( args ) ;
91- } catch ( err ) {
92- // Use isatty(0) for reliable stdin TTY detection (process.stdin.isTTY can be undefined in Bun)
93- // Errors can opt-out via skipAutoAuth (e.g., auth status command)
94- if (
95- err instanceof AuthError &&
96- ( err . reason === "not_authenticated" || err . reason === "expired" ) &&
97- ! err . skipAutoAuth &&
98- isatty ( 0 )
99- ) {
100- process . stderr . write (
101- err . reason === "expired"
102- ? "Authentication expired. Starting login flow...\n\n"
103- : "Authentication required. Starting login flow...\n\n"
104- ) ;
105-
106- const loginSuccess = await runInteractiveLogin ( ) ;
107-
108- if ( loginSuccess ) {
109- process . stderr . write ( "\nRetrying command...\n\n" ) ;
110- await next ( args ) ;
116+ } ;
117+
118+ /**
119+ * Auto-authentication middleware.
120+ *
121+ * Catches auth errors (not_authenticated, expired) in interactive TTYs
122+ * and runs the login flow. On success, retries through the full middleware
123+ * chain so inner middlewares (e.g., trial prompt) also apply to the retry.
124+ */
125+ const autoAuthMiddleware : ErrorMiddleware = async ( next , argv ) => {
126+ try {
127+ await next ( argv ) ;
128+ } catch ( err ) {
129+ // Use isatty(0) for reliable stdin TTY detection (process.stdin.isTTY can be undefined in Bun)
130+ // Errors can opt-out via skipAutoAuth (e.g., auth status command)
131+ if (
132+ err instanceof AuthError &&
133+ ( err . reason === "not_authenticated" || err . reason === "expired" ) &&
134+ ! err . skipAutoAuth &&
135+ isatty ( 0 )
136+ ) {
137+ process . stderr . write (
138+ err . reason === "expired"
139+ ? "Authentication expired. Starting login flow...\n\n"
140+ : "Authentication required. Starting login flow...\n\n"
141+ ) ;
142+
143+ const loginSuccess = await runInteractiveLogin ( ) ;
144+
145+ if ( loginSuccess ) {
146+ process . stderr . write ( "\nRetrying command...\n\n" ) ;
147+ await next ( argv ) ;
148+ return ;
149+ }
150+
151+ // Login failed or was cancelled
152+ process . exitCode = 1 ;
111153 return ;
112154 }
113155
114- // Login failed or was cancelled
115- process . exitCode = 1 ;
116- return ;
156+ throw err ;
117157 }
118-
119- throw err ;
158+ } ;
159+
160+ /**
161+ * Error-recovery middlewares applied around command execution.
162+ *
163+ * Order matters: applied innermost-first, so the last entry wraps the
164+ * outermost layer. Auth middleware is outermost so it catches errors
165+ * from both the command and any inner middleware retries.
166+ */
167+ const errorMiddlewares : ErrorMiddleware [ ] = [
168+ seerTrialMiddleware ,
169+ autoAuthMiddleware ,
170+ ] ;
171+
172+ /** Run CLI command with telemetry wrapper */
173+ async function runCommand ( argv : string [ ] ) : Promise < void > {
174+ await withTelemetry ( async ( span ) =>
175+ run ( app , argv , buildContext ( process , span ) )
176+ ) ;
120177 }
121- } ;
122178
123- /**
124- * Error-recovery middlewares applied around command execution.
125- *
126- * Order matters: applied innermost-first, so the last entry wraps the
127- * outermost layer. Auth middleware is outermost so it catches errors
128- * from both the command and any inner middleware retries.
129- *
130- * To add a new middleware, append it to this array.
131- */
132- const errorMiddlewares : ErrorMiddleware [ ] = [
133- seerTrialMiddleware ,
134- autoAuthMiddleware ,
135- ] ;
136-
137- /** Run CLI command with telemetry wrapper */
138- async function runCommand ( args : string [ ] ) : Promise < void > {
139- await withTelemetry ( async ( span ) =>
140- run ( app , args , buildContext ( process , span ) )
141- ) ;
142- }
143-
144- /**
145- * Build the command executor by composing error-recovery middlewares.
146- *
147- * Wraps `runCommand` with each middleware in order (innermost-first),
148- * producing a single function that handles all error recovery.
149- */
150- function buildExecutor ( ) : ( args : string [ ] ) => Promise < void > {
179+ /** Build the command executor by composing error-recovery middlewares. */
151180 let executor = runCommand ;
152181 for ( const mw of errorMiddlewares ) {
153- const next = executor ;
154- executor = ( args ) => mw ( next , args ) ;
182+ const inner = executor ;
183+ executor = ( argv ) => mw ( inner , argv ) ;
155184 }
156- return executor ;
157- }
158185
159- /** Command executor with all error-recovery middlewares applied */
160- const executeCommand = buildExecutor ( ) ;
186+ // ---------------------------------------------------------------------------
187+ // Main execution
188+ // ---------------------------------------------------------------------------
161189
162- async function main ( ) : Promise < void > {
163190 // Clean up old binary from previous Windows upgrade (no-op if file doesn't exist)
164191 startCleanupOldBinary ( ) ;
165192
166- const args = process . argv . slice ( 2 ) ;
167-
168193 // Apply SENTRY_LOG_LEVEL env var early (lazy read, not at module load time).
169194 // CLI flags (--log-level, --verbose) are handled by Stricli via
170195 // buildCommand and take priority when present.
@@ -173,15 +198,15 @@ async function main(): Promise<void> {
173198 setLogLevel ( envLogLevel ) ;
174199 }
175200
176- const suppressNotification = shouldSuppressNotification ( args ) ;
201+ const suppressNotification = shouldSuppressNotification ( cliArgs ) ;
177202
178203 // Start background update check (non-blocking)
179204 if ( ! suppressNotification ) {
180205 maybeCheckForUpdateInBackground ( ) ;
181206 }
182207
183208 try {
184- await executeCommand ( args ) ;
209+ await executor ( cliArgs ) ;
185210 } catch ( err ) {
186211 process . stderr . write ( `${ error ( "Error:" ) } ${ formatError ( err ) } \n` ) ;
187212 process . exitCode = getExitCode ( err ) ;
@@ -200,4 +225,20 @@ async function main(): Promise<void> {
200225 }
201226}
202227
203- main ( ) ;
228+ // ---------------------------------------------------------------------------
229+ // Dispatch: check argv before any heavy imports
230+ // ---------------------------------------------------------------------------
231+
232+ const args = process . argv . slice ( 2 ) ;
233+
234+ if ( args [ 0 ] === "__complete" ) {
235+ runCompletion ( args . slice ( 1 ) ) . catch ( ( ) => {
236+ // Completions should never crash — silently return no results
237+ process . exitCode = 0 ;
238+ } ) ;
239+ } else {
240+ runCli ( args ) . catch ( ( err ) => {
241+ process . stderr . write ( `Fatal: ${ err } \n` ) ;
242+ process . exitCode = 1 ;
243+ } ) ;
244+ }
0 commit comments