@@ -38,6 +38,7 @@ import {
3838} from "@stricli/core" ;
3939import type { Writer } from "../types/index.js" ;
4040import { getAuthConfig } from "./db/auth.js" ;
41+ import { getEnv } from "./env.js" ;
4142import { AuthError , CliError , OutputError } from "./errors.js" ;
4243import { warning } from "./formatters/colors.js" ;
4344import { parseFieldsList } from "./formatters/json.js" ;
@@ -58,6 +59,7 @@ import { GLOBAL_FLAGS } from "./global-flags.js";
5859import {
5960 LOG_LEVEL_NAMES ,
6061 type LogLevelName ,
62+ logger ,
6163 parseLogLevel ,
6264 setLogLevel ,
6365} from "./logger.js" ;
@@ -238,6 +240,40 @@ export const FIELDS_FLAG = {
238240 optional : true as const ,
239241} as const ;
240242
243+ // ---------------------------------------------------------------------------
244+ // Hidden org/project compat flags (LLM error recovery)
245+ // ---------------------------------------------------------------------------
246+
247+ /**
248+ * Hidden `--org` flag injected into every command by {@link buildCommand}.
249+ *
250+ * Backward-compatibility shim for the older `sentry-cli` that accepted
251+ * `--org` on every command. LLMs trained on the old CLI generate this flag.
252+ * The value is written to `SENTRY_ORG` so the existing resolution chain
253+ * in `resolve-target.ts` picks it up at priority #2 (env vars).
254+ */
255+ const ORG_FLAG = {
256+ kind : "parsed" as const ,
257+ parse : String ,
258+ brief : "Organization slug" ,
259+ optional : true as const ,
260+ hidden : true as const ,
261+ } as const ;
262+
263+ /**
264+ * Hidden `--project` flag injected into every command by {@link buildCommand}.
265+ *
266+ * Same backward-compatibility shim as `--org`. Written to `SENTRY_PROJECT`
267+ * before the command's `func` runs.
268+ */
269+ const PROJECT_FLAG = {
270+ kind : "parsed" as const ,
271+ parse : String ,
272+ brief : "Project slug" ,
273+ optional : true as const ,
274+ hidden : true as const ,
275+ } as const ;
276+
241277/** The flag key for the injected --log-level flag (always stripped) */
242278const LOG_LEVEL_KEY = "log-level" ;
243279
@@ -261,6 +297,44 @@ export function applyLoggingFlags(
261297 }
262298}
263299
300+ const orgProjectLog = logger . withTag ( "compat-flags" ) ;
301+
302+ /**
303+ * Write `--org` / `--project` flag values to environment variables.
304+ *
305+ * Called by the {@link buildCommand} wrapper before the command's `func()`
306+ * runs. The values are written to `SENTRY_ORG` and `SENTRY_PROJECT` so
307+ * the existing resolution chain in `resolve-target.ts` picks them up at
308+ * priority #2 (env vars). Overwrites existing env vars because explicit
309+ * CLI flags are highest-priority user intent.
310+ *
311+ * Empty and whitespace-only values are treated as "not provided" — they
312+ * fall through to the rest of the resolution chain rather than overwriting
313+ * a real pre-existing env var with garbage. This matters for LLM-generated
314+ * commands that occasionally emit `--org ""` or stray whitespace.
315+ *
316+ * Skipped when the command defines its own `--org` / `--project` flag
317+ * (e.g., `release create --project`) — those are passed through to `func()`.
318+ */
319+ export function applyOrgProjectFlags (
320+ org : string | undefined ,
321+ project : string | undefined ,
322+ commandOwnsOrg : boolean ,
323+ commandOwnsProject : boolean
324+ ) : void {
325+ const env = getEnv ( ) ;
326+ const orgTrimmed = org ?. trim ( ) ;
327+ const projectTrimmed = project ?. trim ( ) ;
328+ if ( orgTrimmed && ! commandOwnsOrg ) {
329+ orgProjectLog . debug ( `--org flag → SENTRY_ORG=${ orgTrimmed } ` ) ;
330+ env . SENTRY_ORG = orgTrimmed ;
331+ }
332+ if ( projectTrimmed && ! commandOwnsProject ) {
333+ orgProjectLog . debug ( `--project flag → SENTRY_PROJECT=${ projectTrimmed } ` ) ;
334+ env . SENTRY_PROJECT = projectTrimmed ;
335+ }
336+ }
337+
264338// ---------------------------------------------------------------------------
265339// buildCommand — the single entry point for all Sentry CLI commands
266340// ---------------------------------------------------------------------------
@@ -342,6 +416,70 @@ function enrichDocsWithSchema(
342416 } ;
343417}
344418
419+ /**
420+ * Global flag defaults keyed by flag name.
421+ * Used by {@link mergeGlobalFlags} to inject flags when the command
422+ * doesn't define its own.
423+ */
424+ const GLOBAL_FLAG_DEFAULTS : Record < string , unknown > = {
425+ [ LOG_LEVEL_KEY ] : LOG_LEVEL_FLAG ,
426+ verbose : VERBOSE_FLAG ,
427+ org : ORG_FLAG ,
428+ project : PROJECT_FLAG ,
429+ } ;
430+
431+ /** Flags that are always stripped (command never sees them). */
432+ const ALWAYS_STRIP = new Set ( [ LOG_LEVEL_KEY ] ) ;
433+
434+ /**
435+ * Merge global flags into a command's existing flags.
436+ *
437+ * For each global flag, if the command doesn't already define it, the
438+ * default shape is injected and its key is added to the returned
439+ * `stripKeys` set (so the wrapper strips it before calling `func`).
440+ *
441+ * @returns The merged flags, ownership booleans, and keys to strip.
442+ */
443+ function mergeGlobalFlags (
444+ existingFlags : Record < string , unknown > ,
445+ // biome-ignore lint/suspicious/noExplicitAny: OutputConfig type is erased at the builder level
446+ outputConfig ?: OutputConfig < any >
447+ ) : {
448+ mergedFlags : Record < string , unknown > ;
449+ commandOwnsOrg : boolean ;
450+ commandOwnsProject : boolean ;
451+ stripKeys : Set < string > ;
452+ } {
453+ const commandOwnsJson = "json" in existingFlags ;
454+ const commandOwnsOrg = "org" in existingFlags ;
455+ const commandOwnsProject = "project" in existingFlags ;
456+
457+ const mergedFlags : Record < string , unknown > = { ...existingFlags } ;
458+ const stripKeys = new Set < string > ( ALWAYS_STRIP ) ;
459+
460+ for ( const [ name , shape ] of Object . entries ( GLOBAL_FLAG_DEFAULTS ) ) {
461+ if ( ! ( name in existingFlags ) ) {
462+ mergedFlags [ name ] = shape ;
463+ stripKeys . add ( name ) ;
464+ }
465+ }
466+
467+ // Inject --json and --fields when output config is set
468+ if ( outputConfig ) {
469+ if ( ! commandOwnsJson ) {
470+ mergedFlags . json = JSON_FLAG ;
471+ }
472+ mergedFlags . fields = buildFieldsFlag ( outputConfig ) ;
473+ }
474+
475+ return {
476+ mergedFlags,
477+ commandOwnsOrg,
478+ commandOwnsProject,
479+ stripKeys,
480+ } ;
481+ }
482+
345483export function buildCommand <
346484 const FLAGS extends BaseFlags = NonNullable < unknown > ,
347485 const ARGS extends BaseArgs = [ ] ,
@@ -354,34 +492,15 @@ export function buildCommand<
354492 const requiresAuth = builderArgs . auth !== false ;
355493 const skipRcUrlCheck = builderArgs . skipRcUrlCheck === true ;
356494
357- // Merge logging flags into the command's flag definitions.
358- // Quoted keys produce kebab-case CLI flags: "log-level" → --log-level
495+ // Merge global flags into the command's flag definitions.
359496 const existingParams = ( builderArgs . parameters ?? { } ) as Record <
360497 string ,
361498 unknown
362499 > ;
363500 const existingFlags = ( existingParams . flags ?? { } ) as Record < string , unknown > ;
364501
365- // If the command already defines --verbose (e.g. api command), don't override it.
366- const commandOwnsVerbose = "verbose" in existingFlags ;
367- // If the command already defines --json (e.g. custom brief), don't override it.
368- const commandOwnsJson = "json" in existingFlags ;
369-
370- const mergedFlags : Record < string , unknown > = {
371- ...existingFlags ,
372- [ LOG_LEVEL_KEY ] : LOG_LEVEL_FLAG ,
373- } ;
374- if ( ! commandOwnsVerbose ) {
375- mergedFlags . verbose = VERBOSE_FLAG ;
376- }
377-
378- // Inject --json and --fields when output config is set
379- if ( outputConfig ) {
380- if ( ! commandOwnsJson ) {
381- mergedFlags . json = JSON_FLAG ;
382- }
383- mergedFlags . fields = buildFieldsFlag ( outputConfig ) ;
384- }
502+ const { mergedFlags, commandOwnsOrg, commandOwnsProject, stripKeys } =
503+ mergeGlobalFlags ( existingFlags , outputConfig ) ;
385504
386505 // Enrich fullDescription with JSON fields when schema is registered.
387506 // This makes field info visible in Stricli's --help output.
@@ -443,19 +562,16 @@ export function buildCommand<
443562
444563 /**
445564 * Strip injected flags from the raw Stricli-parsed flags object.
446- * --log-level is always stripped. --verbose is stripped only when we
447- * injected it (not when the command defines its own) . --fields is
448- * pre-parsed from comma-string to string[] when output: { human: ... }.
565+ * Global flags we injected (tracked in `stripKeys` from
566+ * { @link mergeGlobalFlags}) are removed . --fields is pre-parsed from
567+ * comma-string to string[] when output: { human: ... }.
449568 */
450569 function cleanRawFlags (
451570 raw : Record < string , unknown >
452571 ) : Record < string , unknown > {
453572 const clean : Record < string , unknown > = { } ;
454573 for ( const [ key , value ] of Object . entries ( raw ) ) {
455- if ( key === LOG_LEVEL_KEY ) {
456- continue ;
457- }
458- if ( key === "verbose" && ! commandOwnsVerbose ) {
574+ if ( stripKeys . has ( key ) ) {
459575 continue ;
460576 }
461577 clean [ key ] = value ;
@@ -558,6 +674,15 @@ export function buildCommand<
558674 flags . verbose as boolean
559675 ) ;
560676
677+ // Map --org / --project compat flags to env vars before anything
678+ // else reads them (auth guard, org/project resolution, etc.)
679+ applyOrgProjectFlags (
680+ flags . org as string | undefined ,
681+ flags . project as string | undefined ,
682+ commandOwnsOrg ,
683+ commandOwnsProject
684+ ) ;
685+
561686 const cleanFlags = cleanRawFlags ( flags as Record < string , unknown > ) ;
562687 setFlagContext ( cleanFlags ) ;
563688 if ( args . length > 0 ) {
0 commit comments