66 buildRouteMap ,
77 text_en ,
88 UnexpectedPositionalError ,
9+ UnsatisfiedPositionalError ,
910} from "@stricli/core" ;
1011import { apiCommand } from "./commands/api.js" ;
1112import { authRoute } from "./commands/auth/index.js" ;
@@ -36,6 +37,11 @@ import { traceRoute } from "./commands/trace/index.js";
3637import { listCommand as traceListCommand } from "./commands/trace/list.js" ;
3738import { trialRoute } from "./commands/trial/index.js" ;
3839import { listCommand as trialListCommand } from "./commands/trial/list.js" ;
40+ import {
41+ getCommandSuggestion ,
42+ getSynonymSuggestionFromArgv ,
43+ ROUTES_WITH_DEFAULT_VIEW ,
44+ } from "./lib/command-suggestions.js" ;
3945import { CLI_VERSION } from "./lib/constants.js" ;
4046import {
4147 AuthError ,
@@ -121,6 +127,46 @@ export const routes = buildRouteMap({
121127 } ,
122128} ) ;
123129
130+ /**
131+ * Detect when the user typed a bare route group with no subcommand (e.g., `sentry issue`).
132+ *
133+ * With `defaultCommand: "view"` on route groups, Stricli routes to the view
134+ * command which then fails with UnsatisfiedPositionalError because no issue ID
135+ * was provided. Returns a usage hint string, or undefined if this isn't the
136+ * bare-route-group case.
137+ */
138+ function detectBareRouteGroup ( ansiColor : boolean ) : string | undefined {
139+ const args = process . argv . slice ( 2 ) ;
140+ const nonFlags = args . filter ( ( t ) => ! t . startsWith ( "-" ) ) ;
141+ if (
142+ nonFlags . length <= 1 &&
143+ nonFlags [ 0 ] &&
144+ ROUTES_WITH_DEFAULT_VIEW . has ( nonFlags [ 0 ] )
145+ ) {
146+ const route = nonFlags [ 0 ] ;
147+ const msg = `Usage: sentry ${ route } <command> [args]\nRun "sentry ${ route } --help" to see available commands` ;
148+ return ansiColor ? warning ( msg ) : msg ;
149+ }
150+ return ;
151+ }
152+
153+ /**
154+ * Detect when a plural alias received extra positional args and suggest the
155+ * singular form. E.g., `sentry projects view cli` → `sentry project view cli`.
156+ */
157+ function detectPluralAliasMisuse ( ansiColor : boolean ) : string | undefined {
158+ const args = process . argv . slice ( 2 ) ;
159+ const firstArg = args [ 0 ] ;
160+ if ( firstArg && firstArg in PLURAL_TO_SINGULAR ) {
161+ const singular = PLURAL_TO_SINGULAR [ firstArg ] ;
162+ const rest = args . slice ( 1 ) . join ( " " ) ;
163+ return ansiColor
164+ ? warning ( `\nDid you mean: sentry ${ singular } ${ rest } \n` )
165+ : `\nDid you mean: sentry ${ singular } ${ rest } \n` ;
166+ }
167+ return ;
168+ }
169+
124170/**
125171 * Custom error formatting for CLI errors.
126172 *
@@ -134,23 +180,65 @@ const customText: ApplicationText = {
134180 exc : unknown ,
135181 ansiColor : boolean
136182 ) : string => {
137- // When a plural alias receives extra positional args (e.g. `sentry projects view cli`),
138- // Stricli throws UnexpectedPositionalError because the list command only accepts 1 arg.
139- // Detect this and suggest the singular form.
183+ // Case A: bare route group with no subcommand (e.g., `sentry issue`)
184+ if ( exc instanceof UnsatisfiedPositionalError ) {
185+ const bareHint = detectBareRouteGroup ( ansiColor ) ;
186+ if ( bareHint ) {
187+ return bareHint ;
188+ }
189+ }
190+
191+ // Case B + plural alias: extra args that Stricli can't consume
140192 if ( exc instanceof UnexpectedPositionalError ) {
141- const args = process . argv . slice ( 2 ) ;
142- const firstArg = args [ 0 ] ;
143- if ( firstArg && firstArg in PLURAL_TO_SINGULAR ) {
144- const singular = PLURAL_TO_SINGULAR [ firstArg ] ;
145- const rest = args . slice ( 1 ) . join ( " " ) ;
146- const hint = ansiColor
147- ? warning ( `\nDid you mean: sentry ${ singular } ${ rest } \n` )
148- : `\nDid you mean: sentry ${ singular } ${ rest } \n` ;
149- return `${ text_en . exceptionWhileParsingArguments ( exc , ansiColor ) } ${ hint } ` ;
193+ const pluralHint = detectPluralAliasMisuse ( ansiColor ) ;
194+ if ( pluralHint ) {
195+ return `${ text_en . exceptionWhileParsingArguments ( exc , ansiColor ) } ${ pluralHint } ` ;
196+ }
197+
198+ // With defaultCommand: "view", unknown tokens like "events" fill the
199+ // positional slot, then extra args (e.g., CLI-AB) trigger this error.
200+ // Check if the first non-route token is a known synonym.
201+ const synonymHint = getSynonymSuggestionFromArgv ( ) ;
202+ if ( synonymHint ) {
203+ const tip = ansiColor
204+ ? warning ( `\nTip: ${ synonymHint } ` )
205+ : `\nTip: ${ synonymHint } ` ;
206+ return `${ text_en . exceptionWhileParsingArguments ( exc , ansiColor ) } ${ tip } ` ;
150207 }
151208 }
209+
152210 return text_en . exceptionWhileParsingArguments ( exc , ansiColor ) ;
153211 } ,
212+ noCommandRegisteredForInput : ( { input, corrections, ansiColor } ) : string => {
213+ // Default error message from Stricli (e.g., "No command registered for `info`")
214+ const base = text_en . noCommandRegisteredForInput ( {
215+ input,
216+ corrections,
217+ ansiColor,
218+ } ) ;
219+
220+ // Check for known synonym suggestions on routes without defaultCommand
221+ // (e.g., `sentry cli info` → suggest `sentry auth status`).
222+ // Routes WITH defaultCommand won't reach here — their unknown tokens
223+ // are consumed as positional args and handled by Cases A/B/C above.
224+ const args = process . argv . slice ( 2 ) ;
225+ const nonFlags = args . filter ( ( t ) => ! t . startsWith ( "-" ) ) ;
226+ const routeContext = nonFlags [ 0 ] ?? "" ;
227+ const suggestion = getCommandSuggestion ( routeContext , input ) ;
228+ if ( suggestion ) {
229+ const hint = suggestion . explanation
230+ ? `${ suggestion . explanation } : ${ suggestion . command } `
231+ : suggestion . command ;
232+ // Stricli wraps our return value in bold-red ANSI codes.
233+ // Reset before applying warning() color so the tip is yellow, not red.
234+ const formatted = ansiColor
235+ ? `\n\x1B[39m\x1B[22m${ warning ( `Tip: ${ hint } ` ) } `
236+ : `\nTip: ${ hint } ` ;
237+ return `${ base } ${ formatted } ` ;
238+ }
239+
240+ return base ;
241+ } ,
154242 exceptionWhileRunningCommand : ( exc : unknown , ansiColor : boolean ) : string => {
155243 // OutputError: data was already rendered to stdout — just re-throw
156244 // so the exit code propagates without Stricli printing an error message.
@@ -174,6 +262,17 @@ const customText: ApplicationText = {
174262
175263 if ( exc instanceof CliError ) {
176264 const prefix = ansiColor ? errorColor ( "Error:" ) : "Error:" ;
265+ // Case C: With defaultCommand: "view", unknown tokens like "events" are
266+ // silently consumed as the positional arg. The view command fails at the
267+ // domain level (e.g., ResolutionError). Check argv for a known synonym
268+ // and append the suggestion to the error.
269+ const synonymHint = getSynonymSuggestionFromArgv ( ) ;
270+ if ( synonymHint ) {
271+ const tip = ansiColor
272+ ? warning ( `Tip: ${ synonymHint } ` )
273+ : `Tip: ${ synonymHint } ` ;
274+ return `${ prefix } ${ exc . format ( ) } \n${ tip } ` ;
275+ }
177276 return `${ prefix } ${ exc . format ( ) } ` ;
178277 }
179278 if ( exc instanceof Error ) {
0 commit comments