diff --git a/packages/cli/patches/@stricli%2Fcore@1.2.8.patch b/packages/cli/patches/@stricli%2Fcore@1.2.8.patch index c8256f2a8..b99802b98 100644 --- a/packages/cli/patches/@stricli%2Fcore@1.2.8.patch +++ b/packages/cli/patches/@stricli%2Fcore@1.2.8.patch @@ -1,8 +1,60 @@ diff --git a/dist/index.cjs b/dist/index.cjs -index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5ea209e29 100644 +index a9afbc1ab78cad7ba97d53882a80236af62854df..bb41ae32d8d26c83934865440f393a20ae7c8305 100644 --- a/dist/index.cjs +++ b/dist/index.cjs -@@ -1277,6 +1277,35 @@ var RouteMapSymbol = Symbol("RouteMap"); +@@ -1182,13 +1182,32 @@ function shouldUseAnsiColor(process, stream, config) { + } + + // src/routing/command/run.ts ++// PATCH(getsentry/cli): pluggable exit code for argument-parsing failures. ++// Stock Stricli always exits with `ExitCode.InvalidArgument` when scanning ++// fails, even when a parameter's `parse` function threw an application error ++// that carries its own exit code. When the host application supplies ++// `determineArgumentErrorExitCode`, the first error it maps to a number wins; ++// returning `undefined` keeps `ExitCode.InvalidArgument`. Inert when unset. If ++// `exitCodeForArgumentErrors` is missing, the CLI's `check:patches` fails. ++function exitCodeForArgumentErrors(errors, determineArgumentErrorExitCode) { ++ if (determineArgumentErrorExitCode) { ++ for (const error of errors) { ++ const exitCode = determineArgumentErrorExitCode(error); ++ if (typeof exitCode === "number") { ++ return exitCode; ++ } ++ } ++ } ++ return ExitCode.InvalidArgument; ++} + async function runCommand({ loader, parameters }, { + context, + inputs, + scannerConfig, + errorFormatting, + documentationConfig, +- determineExitCode ++ determineExitCode, ++ determineArgumentErrorExitCode + }) { + let parsedArguments; + try { +@@ -1209,7 +1228,7 @@ async function runCommand({ loader, parameters }, { + ` + ); + } +- return ExitCode.InvalidArgument; ++ return exitCodeForArgumentErrors(result.errors, determineArgumentErrorExitCode); + } + } catch (exc) { + const ansiColor = shouldUseAnsiColor(context.process, context.process.stderr, documentationConfig); +@@ -1219,7 +1238,7 @@ async function runCommand({ loader, parameters }, { + ` : `${errorMessage} + ` + ); +- return ExitCode.InvalidArgument; ++ return exitCodeForArgumentErrors([exc], determineArgumentErrorExitCode); + } + let commandFunction; + try { +@@ -1277,6 +1296,35 @@ var RouteMapSymbol = Symbol("RouteMap"); var CommandSymbol = Symbol("Command"); // src/routing/scanner.ts @@ -38,7 +90,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 function buildRouteScanner(root, config, startingPrefix) { const prefix = [...startingPrefix]; const unprocessedInputs = []; -@@ -1285,7 +1314,14 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1285,7 +1333,14 @@ function buildRouteScanner(root, config, startingPrefix) { let target; let rootLevel = true; let helpRequested = false; @@ -53,7 +105,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 return { next: (input) => { if (!treatInputsAsArguments && config.allowArgumentEscapeSequence && input === "--") { -@@ -1293,6 +1329,14 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1293,6 +1348,14 @@ function buildRouteScanner(root, config, startingPrefix) { unprocessedInputs.push(input); return; } @@ -68,7 +120,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 if (!treatInputsAsArguments) { if (input === "--help" || input === "-h") { helpRequested = true; -@@ -1300,12 +1341,28 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1300,12 +1363,28 @@ function buildRouteScanner(root, config, startingPrefix) { target = current; } return; @@ -98,7 +150,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 } } if (target) { -@@ -1357,6 +1414,7 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1357,6 +1436,7 @@ function buildRouteScanner(root, config, startingPrefix) { target, unprocessedInputs, helpRequested, @@ -106,7 +158,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 prefix, rootLevel, aliases -@@ -1410,16 +1468,6 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) +@@ -1410,16 +1490,6 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) } } const inputs = rawInputs.slice(); @@ -123,7 +175,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 const scanner = buildRouteScanner(root, config.scanner, [config.name]); let error; while (inputs.length > 0 && !error) { -@@ -1445,7 +1493,37 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) +@@ -1445,7 +1515,37 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) return ExitCode.UnknownCommand; } const result = scanner.finish(); @@ -161,7 +213,17 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 const ansiColor = shouldUseAnsiColor(context.process, context.process.stdout, config.documentation); context.process.stdout.write( result.target.formatHelp({ -@@ -1514,6 +1592,9 @@ function withDefaults(config) { +@@ -1481,7 +1581,8 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) + scannerConfig: config.scanner, + documentationConfig: config.documentation, + errorFormatting: text, +- determineExitCode: config.determineExitCode ++ determineExitCode: config.determineExitCode, ++ determineArgumentErrorExitCode: config.determineArgumentErrorExitCode + }); + } + +@@ -1514,6 +1615,9 @@ function withDefaults(config) { const scannerConfig = { caseStyle: scannerCaseStyle, allowArgumentEscapeSequence: config.scanner?.allowArgumentEscapeSequence ?? false, @@ -171,7 +233,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 distanceOptions: config.scanner?.distanceOptions ?? { threshold: 7, weights: { -@@ -1529,7 +1610,10 @@ function withDefaults(config) { +@@ -1529,7 +1633,10 @@ function withDefaults(config) { useAliasInUsageLine: config.documentation?.useAliasInUsageLine ?? false, onlyRequiredInUsageLine: config.documentation?.onlyRequiredInUsageLine ?? false, caseStyle: displayCaseStyle, @@ -183,7 +245,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 }; const completionConfig = { includeAliases: config.completion?.includeAliases ?? documentationConfig.useAliasInUsageLine, -@@ -1881,7 +1965,7 @@ function formatDocumentationForFlagParameters(flags, aliases, args) { +@@ -1881,7 +1988,7 @@ function formatDocumentationForFlagParameters(flags, aliases, args) { if (args.includeHelpAllFlag) { const helpAllFlagName = formatForDisplay("helpAll", args.config.caseStyle); rows.push({ @@ -192,7 +254,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 flagName: atLeastOneOptional ? ` --${helpAllFlagName}` : `--${helpAllFlagName}`, brief: briefs.helpAll, hidden: !args.config.alwaysShowHelpAllFlag -@@ -1920,7 +2004,7 @@ function* generateBuiltInFlagUsageLines(args) { +@@ -1920,7 +2027,7 @@ function* generateBuiltInFlagUsageLines(args) { yield args.config.useAliasInUsageLine ? "-h" : "--help"; if (args.includeHelpAllFlag) { const helpAllFlagName = formatForDisplay("helpAll", args.config.caseStyle); @@ -201,7 +263,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 } if (args.includeVersionFlag) { yield args.config.useAliasInUsageLine ? "-v" : "--version"; -@@ -2057,7 +2141,7 @@ function checkForInvalidVariadicSeparators(flags) { +@@ -2057,7 +2164,7 @@ function checkForInvalidVariadicSeparators(flags) { function buildCommand(builderArgs) { const { flags = {}, aliases = {} } = builderArgs.parameters; checkForReservedFlags(flags, ["help", "helpAll", "help-all"]); @@ -211,7 +273,7 @@ index a9afbc1ab78cad7ba97d53882a80236af62854df..141532779be923b8f3b421b0afc031b5 checkForInvalidVariadicSeparators(flags); let loader; diff --git a/dist/index.d.cts b/dist/index.d.cts -index 111d1fd9e926f23b71359a692cd2e9e05ebf379e..1714047c8cf7d46e38062bab670451851709a7e2 100644 +index 111d1fd9e926f23b71359a692cd2e9e05ebf379e..9c4cc5dd09306f92f6fc0b023598eeeaa042b277 100644 --- a/dist/index.d.cts +++ b/dist/index.d.cts @@ -402,6 +402,25 @@ interface ScannerConfiguration { @@ -269,8 +331,30 @@ index 111d1fd9e926f23b71359a692cd2e9e05ebf379e..1714047c8cf7d46e38062bab67045185 } /** * Configuration for controlling the behavior of completion proposals. +@@ -561,11 +602,20 @@ interface ApplicationConfiguration { + * If unspecified, the exit code will default to 1 when a command function throws some value. + */ + readonly determineExitCode?: (exc: unknown) => number; ++ /** ++ * Optional exit code for a failure while parsing command line arguments, ++ * such as an error thrown from a parameter's `parse` function. Called for ++ * each scanner error in order; the first number returned is used. Returning ++ * `undefined` keeps the default `ExitCode.InvalidArgument`. ++ * ++ * Default value is `undefined` (always `ExitCode.InvalidArgument`). ++ */ ++ readonly determineArgumentErrorExitCode?: (exc: unknown) => number | undefined; + } + /** + * Partial configuration for application, see individual description for behavior when each value is unspecified. + */ +-type PartialApplicationConfiguration = Pick & { ++type PartialApplicationConfiguration = Pick & { + [K in "scanner" | "documentation" | "completion" | "localization"]?: Partial; + }; + diff --git a/dist/index.d.ts b/dist/index.d.ts -index 111d1fd9e926f23b71359a692cd2e9e05ebf379e..1714047c8cf7d46e38062bab670451851709a7e2 100644 +index 111d1fd9e926f23b71359a692cd2e9e05ebf379e..9c4cc5dd09306f92f6fc0b023598eeeaa042b277 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -402,6 +402,25 @@ interface ScannerConfiguration { @@ -328,11 +412,85 @@ index 111d1fd9e926f23b71359a692cd2e9e05ebf379e..1714047c8cf7d46e38062bab67045185 } /** * Configuration for controlling the behavior of completion proposals. +@@ -561,11 +602,20 @@ interface ApplicationConfiguration { + * If unspecified, the exit code will default to 1 when a command function throws some value. + */ + readonly determineExitCode?: (exc: unknown) => number; ++ /** ++ * Optional exit code for a failure while parsing command line arguments, ++ * such as an error thrown from a parameter's `parse` function. Called for ++ * each scanner error in order; the first number returned is used. Returning ++ * `undefined` keeps the default `ExitCode.InvalidArgument`. ++ * ++ * Default value is `undefined` (always `ExitCode.InvalidArgument`). ++ */ ++ readonly determineArgumentErrorExitCode?: (exc: unknown) => number | undefined; + } + /** + * Partial configuration for application, see individual description for behavior when each value is unspecified. + */ +-type PartialApplicationConfiguration = Pick & { ++type PartialApplicationConfiguration = Pick & { + [K in "scanner" | "documentation" | "completion" | "localization"]?: Partial; + }; + diff --git a/dist/index.js b/dist/index.js -index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d87c52de17 100644 +index f76639637e812453d107bda3e3ec165fc195f4d8..a2216e46e4e6b5aba1f87316682c78388ec91e7e 100644 --- a/dist/index.js +++ b/dist/index.js -@@ -1229,6 +1229,35 @@ var RouteMapSymbol = Symbol("RouteMap"); +@@ -1134,13 +1134,32 @@ function shouldUseAnsiColor(process, stream, config) { + } + + // src/routing/command/run.ts ++// PATCH(getsentry/cli): pluggable exit code for argument-parsing failures. ++// Stock Stricli always exits with `ExitCode.InvalidArgument` when scanning ++// fails, even when a parameter's `parse` function threw an application error ++// that carries its own exit code. When the host application supplies ++// `determineArgumentErrorExitCode`, the first error it maps to a number wins; ++// returning `undefined` keeps `ExitCode.InvalidArgument`. Inert when unset. If ++// `exitCodeForArgumentErrors` is missing, the CLI's `check:patches` fails. ++function exitCodeForArgumentErrors(errors, determineArgumentErrorExitCode) { ++ if (determineArgumentErrorExitCode) { ++ for (const error of errors) { ++ const exitCode = determineArgumentErrorExitCode(error); ++ if (typeof exitCode === "number") { ++ return exitCode; ++ } ++ } ++ } ++ return ExitCode.InvalidArgument; ++} + async function runCommand({ loader, parameters }, { + context, + inputs, + scannerConfig, + errorFormatting, + documentationConfig, +- determineExitCode ++ determineExitCode, ++ determineArgumentErrorExitCode + }) { + let parsedArguments; + try { +@@ -1161,7 +1180,7 @@ async function runCommand({ loader, parameters }, { + ` + ); + } +- return ExitCode.InvalidArgument; ++ return exitCodeForArgumentErrors(result.errors, determineArgumentErrorExitCode); + } + } catch (exc) { + const ansiColor = shouldUseAnsiColor(context.process, context.process.stderr, documentationConfig); +@@ -1171,7 +1190,7 @@ async function runCommand({ loader, parameters }, { + ` : `${errorMessage} + ` + ); +- return ExitCode.InvalidArgument; ++ return exitCodeForArgumentErrors([exc], determineArgumentErrorExitCode); + } + let commandFunction; + try { +@@ -1229,6 +1248,35 @@ var RouteMapSymbol = Symbol("RouteMap"); var CommandSymbol = Symbol("Command"); // src/routing/scanner.ts @@ -368,7 +526,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 function buildRouteScanner(root, config, startingPrefix) { const prefix = [...startingPrefix]; const unprocessedInputs = []; -@@ -1237,7 +1266,14 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1237,7 +1285,14 @@ function buildRouteScanner(root, config, startingPrefix) { let target; let rootLevel = true; let helpRequested = false; @@ -383,7 +541,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 return { next: (input) => { if (!treatInputsAsArguments && config.allowArgumentEscapeSequence && input === "--") { -@@ -1245,6 +1281,14 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1245,6 +1300,14 @@ function buildRouteScanner(root, config, startingPrefix) { unprocessedInputs.push(input); return; } @@ -398,7 +556,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 if (!treatInputsAsArguments) { if (input === "--help" || input === "-h") { helpRequested = true; -@@ -1252,12 +1293,28 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1252,12 +1315,28 @@ function buildRouteScanner(root, config, startingPrefix) { target = current; } return; @@ -428,7 +586,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 } } if (target) { -@@ -1309,6 +1366,7 @@ function buildRouteScanner(root, config, startingPrefix) { +@@ -1309,6 +1388,7 @@ function buildRouteScanner(root, config, startingPrefix) { target, unprocessedInputs, helpRequested, @@ -436,7 +594,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 prefix, rootLevel, aliases -@@ -1362,16 +1420,6 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) +@@ -1362,16 +1442,6 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) } } const inputs = rawInputs.slice(); @@ -453,7 +611,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 const scanner = buildRouteScanner(root, config.scanner, [config.name]); let error; while (inputs.length > 0 && !error) { -@@ -1397,7 +1445,37 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) +@@ -1397,7 +1467,37 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) return ExitCode.UnknownCommand; } const result = scanner.finish(); @@ -491,7 +649,17 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 const ansiColor = shouldUseAnsiColor(context.process, context.process.stdout, config.documentation); context.process.stdout.write( result.target.formatHelp({ -@@ -1466,6 +1544,9 @@ function withDefaults(config) { +@@ -1433,7 +1533,8 @@ async function runApplication({ root, defaultText, config }, rawInputs, context) + scannerConfig: config.scanner, + documentationConfig: config.documentation, + errorFormatting: text, +- determineExitCode: config.determineExitCode ++ determineExitCode: config.determineExitCode, ++ determineArgumentErrorExitCode: config.determineArgumentErrorExitCode + }); + } + +@@ -1466,6 +1567,9 @@ function withDefaults(config) { const scannerConfig = { caseStyle: scannerCaseStyle, allowArgumentEscapeSequence: config.scanner?.allowArgumentEscapeSequence ?? false, @@ -501,7 +669,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 distanceOptions: config.scanner?.distanceOptions ?? { threshold: 7, weights: { -@@ -1481,7 +1562,10 @@ function withDefaults(config) { +@@ -1481,7 +1585,10 @@ function withDefaults(config) { useAliasInUsageLine: config.documentation?.useAliasInUsageLine ?? false, onlyRequiredInUsageLine: config.documentation?.onlyRequiredInUsageLine ?? false, caseStyle: displayCaseStyle, @@ -513,7 +681,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 }; const completionConfig = { includeAliases: config.completion?.includeAliases ?? documentationConfig.useAliasInUsageLine, -@@ -1833,7 +1917,7 @@ function formatDocumentationForFlagParameters(flags, aliases, args) { +@@ -1833,7 +1940,7 @@ function formatDocumentationForFlagParameters(flags, aliases, args) { if (args.includeHelpAllFlag) { const helpAllFlagName = formatForDisplay("helpAll", args.config.caseStyle); rows.push({ @@ -522,7 +690,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 flagName: atLeastOneOptional ? ` --${helpAllFlagName}` : `--${helpAllFlagName}`, brief: briefs.helpAll, hidden: !args.config.alwaysShowHelpAllFlag -@@ -1872,7 +1956,7 @@ function* generateBuiltInFlagUsageLines(args) { +@@ -1872,7 +1979,7 @@ function* generateBuiltInFlagUsageLines(args) { yield args.config.useAliasInUsageLine ? "-h" : "--help"; if (args.includeHelpAllFlag) { const helpAllFlagName = formatForDisplay("helpAll", args.config.caseStyle); @@ -531,7 +699,7 @@ index f76639637e812453d107bda3e3ec165fc195f4d8..eff534dc79cdbe5bf8ee14839157b6d8 } if (args.includeVersionFlag) { yield args.config.useAliasInUsageLine ? "-v" : "--version"; -@@ -2009,7 +2093,7 @@ function checkForInvalidVariadicSeparators(flags) { +@@ -2009,7 +2116,7 @@ function checkForInvalidVariadicSeparators(flags) { function buildCommand(builderArgs) { const { flags = {}, aliases = {} } = builderArgs.parameters; checkForReservedFlags(flags, ["help", "helpAll", "help-all"]); diff --git a/packages/cli/script/check-patches.ts b/packages/cli/script/check-patches.ts index 627148c57..892050a02 100644 --- a/packages/cli/script/check-patches.ts +++ b/packages/cli/script/check-patches.ts @@ -226,6 +226,12 @@ for (const [key, patchPath] of Object.entries(patches)) { * `--verbose` alias at every position; `--version` remains the version flag. * The stale marker is the original `inputs[0] === "-v"` version check. * + * @stricli/core (argument-error exit codes): the patch adds an optional + * `determineArgumentErrorExitCode` application hook so a CliError thrown from a + * flag or positional `parse` function keeps its own exit code (e.g. 21 for a + * ValidationError) instead of Stricli's InvalidArgument. This is a pure + * insertion, guarded by the `exitCodeForArgumentErrors` required marker. + * * @sentry/core and @sentry/node-core: these are tree-shaking patches that strip * unused re-exports (AI/integration modules) from the build barrels so esbuild * excludes them from the bundle. They edit bundler-generated barrels and are @@ -308,6 +314,18 @@ const CONTENT_ASSERTIONS: ReadonlyArray<{ description: "@stricli/core CJS: pluggable `renderHelp` hook missing (`sentry --help --json` will fall back to text help instead of structured JSON)", }, + { + file: "@stricli/core/dist/index.js", + requiredMarker: "exitCodeForArgumentErrors", + description: + "@stricli/core ESM: `determineArgumentErrorExitCode` hook missing (a CliError thrown by a flag or positional parser exits with Stricli's InvalidArgument code instead of its own)", + }, + { + file: "@stricli/core/dist/index.cjs", + requiredMarker: "exitCodeForArgumentErrors", + description: + "@stricli/core CJS: `determineArgumentErrorExitCode` hook missing (a CliError thrown by a flag or positional parser exits with Stricli's InvalidArgument code instead of its own)", + }, { file: "@sentry/core/build/cjs/index.js", staleMarker: "exports.instrumentOpenAiClient", diff --git a/packages/cli/src/app.ts b/packages/cli/src/app.ts index 49002ebcc..f2d9bb2f1 100644 --- a/packages/cli/src/app.ts +++ b/packages/cli/src/app.ts @@ -1,5 +1,6 @@ import { type ApplicationText, + ArgumentParseError, buildApplication, text_en, UnexpectedPositionalError, @@ -312,6 +313,22 @@ function escapesToOuterMiddleware(exc: unknown): boolean { return exc instanceof ApiError && (exc.status === 401 || exc.status === 403); } +/** + * Exit code for an argument-parsing failure. + * + * Stricli wraps exceptions from flag and positional `parse` functions in + * {@link ArgumentParseError}. When the wrapped value is a {@link CliError}, + * return its own exit code (e.g. 21 for a `ValidationError`). Every other + * scanner error returns `undefined`, which keeps Stricli's + * `ExitCode.InvalidArgument`. + */ +function getArgumentErrorExitCode(exc: unknown): number | undefined { + if (exc instanceof ArgumentParseError && exc.exception instanceof CliError) { + return exc.exception.exitCode; + } + return; +} + /** * Custom error formatting for CLI errors. * @@ -325,6 +342,17 @@ const customText: ApplicationText = { exc: unknown, ansiColor: boolean, ): string => { + // A parser threw a CliError: keep Stricli's input and parameter context, + // but render the error the same way as one thrown from the command. + if ( + exc instanceof ArgumentParseError && + exc.exception instanceof CliError + ) { + return redactCredentialText( + `Failed to parse "${exc.input}" for ${exc.externalFlagNameOrPlaceholder}: ${formatError(exc.exception)}`, + ); + } + // Case A: bare route group with no subcommand (e.g., `sentry issue`) if (exc instanceof UnsatisfiedPositionalError) { const bareHint = detectBareRouteGroup(ansiColor); @@ -442,6 +470,7 @@ export const app = buildApplication(routes, { topLevelFlags: buildTopLevelFlags(), }, determineExitCode: getExitCode, + determineArgumentErrorExitCode: getArgumentErrorExitCode, documentation: { // Pluggable help renderer (via our @stricli/core patch): render `--help // --json` as structured JSON through the same introspection path as diff --git a/packages/cli/test/lib/scanner-flags.integration.test.ts b/packages/cli/test/lib/scanner-flags.integration.test.ts index 8571d7fb0..f9e7a9750 100644 --- a/packages/cli/test/lib/scanner-flags.integration.test.ts +++ b/packages/cli/test/lib/scanner-flags.integration.test.ts @@ -20,11 +20,12 @@ import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { run } from "@stricli/core"; +import { ExitCode, run } from "@stricli/core"; import { describe, expect, test } from "vitest"; import { app } from "../../src/app.js"; import type { SentryContext } from "../../src/context.js"; import { CLI_VERSION } from "../../src/lib/constants.js"; +import { EXIT } from "../../src/lib/errors.js"; import { useTestConfigDir } from "../helpers.js"; useTestConfigDir("scanner-flags-integration-"); @@ -317,3 +318,33 @@ describe("--help --json renders structured help via renderHelp hook", () => { expect(parsed.error).toContain("nope"); }); }); + +describe("CliErrors thrown by argument parsers", () => { + // Stricli wraps a parser exception in ArgumentParseError and exits with + // ExitCode.InvalidArgument. The patch's `determineArgumentErrorExitCode` + // hook lets the app keep a CliError's own exit code instead. + + test("a ValidationError from a flag parser exits with the validation code", async () => { + const { stderr, exitCode } = await runApp([ + "issue", + "list", + "--period", + "bogus", + ]); + expect(exitCode).toBe(EXIT.VALIDATION); + expect(stderr).toContain('Failed to parse "bogus" for period'); + expect(stderr).toContain("Invalid period 'bogus'"); + }); + + test("a non-CliError parser failure keeps Stricli's exit code", async () => { + // `--limit` uses Stricli's numberParser, which throws a plain SyntaxError. + const { stderr, exitCode } = await runApp([ + "issue", + "list", + "--limit", + "abc", + ]); + expect(exitCode).toBe(ExitCode.InvalidArgument); + expect(stderr).toContain('Failed to parse "abc" for limit'); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8966fa47c..a63fac469 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -199,7 +199,7 @@ patchedDependencies: '@cloudflare/workers-oauth-provider@0.10.3': 34910b67929c1888b1492384c514d6baeedc4a439f66e21375dee5af404834f8 '@sentry/core@10.63.0': e663994979ff877a26ab6d4dea5968fbaee4ccdfdeb85623983535a572678940 '@sentry/node-core@10.63.0': 0a5f08471bd72cb833a2b36220c96001d966e5c390d71aaf598fdef75b552640 - '@stricli/core@1.2.8': cbf047581d38be04108d3921321c14a778272c24004da8110d4498d4f2a6b2e1 + '@stricli/core@1.2.8': 4f42fc9b875fb61132fb69eff6cb253c19afbc5a7ec0eb725d85c1a5bda8b677 importers: @@ -478,7 +478,7 @@ importers: version: 1.3.0 '@stricli/core': specifier: 1.2.8 - version: 1.2.8(patch_hash=cbf047581d38be04108d3921321c14a778272c24004da8110d4498d4f2a6b2e1) + version: 1.2.8(patch_hash=4f42fc9b875fb61132fb69eff6cb253c19afbc5a7ec0eb725d85c1a5bda8b677) '@types/http-cache-semantics': specifier: ^4.2.0 version: 4.2.0 @@ -13284,7 +13284,7 @@ snapshots: dependencies: '@stricli/core': 1.3.0 - '@stricli/core@1.2.8(patch_hash=cbf047581d38be04108d3921321c14a778272c24004da8110d4498d4f2a6b2e1)': {} + '@stricli/core@1.2.8(patch_hash=4f42fc9b875fb61132fb69eff6cb253c19afbc5a7ec0eb725d85c1a5bda8b677)': {} '@stricli/core@1.3.0': {} @@ -15149,7 +15149,7 @@ snapshots: fossilize@0.10.1: dependencies: '@stricli/auto-complete': 1.3.0 - '@stricli/core': 1.2.8(patch_hash=cbf047581d38be04108d3921321c14a778272c24004da8110d4498d4f2a6b2e1) + '@stricli/core': 1.2.8(patch_hash=4f42fc9b875fb61132fb69eff6cb253c19afbc5a7ec0eb725d85c1a5bda8b677) binpunch: 1.0.0 esbuild: 0.25.12 macho-unsign: 2.0.6 @@ -18472,7 +18472,7 @@ snapshots: tsx@4.23.13: dependencies: - esbuild: 0.28.1 + esbuild: 0.28.2 optionalDependencies: fsevents: 2.3.3