88import type { SentryContext } from "../context.js" ;
99import { rawApiRequest } from "../lib/api-client.js" ;
1010import { buildCommand } from "../lib/command.js" ;
11+ import { ValidationError } from "../lib/errors.js" ;
1112import type { Writer } from "../types/index.js" ;
1213
1314type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" ;
@@ -295,12 +296,67 @@ export function setNestedValue(
295296 }
296297}
297298
299+ /**
300+ * Auto-correct fields that use ':' instead of '=' as the separator, and warn
301+ * the user on stderr.
302+ *
303+ * This recovers from a common mistake where users write Sentry search-query
304+ * style syntax (`-F status:resolved`) instead of the required key=value form
305+ * (`-F status=resolved`). The correction is safe to apply unconditionally
306+ * because this function is only called for fields that have already been
307+ * confirmed to contain no '=' — at that point the request would fail anyway.
308+ *
309+ * Splitting on the *first* ':' is intentional so that values that themselves
310+ * contain colons (e.g. ISO timestamps, URLs) are preserved intact:
311+ * `since:2026-02-25T11:20:00` → key=`since`, value=`2026-02-25T11:20:00`
312+ *
313+ * Fields with no ':' (truly uncorrectable) are returned unchanged so that the
314+ * downstream parser can throw its normal error.
315+ *
316+ * @param fields - Raw field strings from --field or --raw-field flags
317+ * @param stderr - Writer to emit warnings on (command's stderr)
318+ * @returns New array with corrected field strings (or the original array if no
319+ * corrections were needed)
320+ * @internal Exported for testing
321+ */
322+ export function normalizeFields (
323+ fields : string [ ] | undefined ,
324+ stderr : Writer
325+ ) : string [ ] | undefined {
326+ if ( ! fields || fields . length === 0 ) {
327+ return fields ;
328+ }
329+
330+ return fields . map ( ( field ) => {
331+ // Already valid: has '=' or is the empty-array syntax "key[]"
332+ if ( field . includes ( "=" ) || field . endsWith ( "[]" ) ) {
333+ return field ;
334+ }
335+
336+ const colonIndex = field . indexOf ( ":" ) ;
337+ // ':' must exist and not be the very first character (that would make an
338+ // empty key, which the parser rejects regardless)
339+ if ( colonIndex > 0 ) {
340+ const key = field . substring ( 0 , colonIndex ) ;
341+ const value = field . substring ( colonIndex + 1 ) ;
342+ const corrected = `${ key } =${ value } ` ;
343+ stderr . write (
344+ `warning: field '${ field } ' looks like it uses ':' instead of '=' — interpreting as '${ corrected } '\n`
345+ ) ;
346+ return corrected ;
347+ }
348+
349+ // No correction possible; let the downstream parser throw.
350+ return field ;
351+ } ) ;
352+ }
353+
298354/**
299355 * Process a single field string and set its value in the result object.
300356 * @param result - Target object to modify
301357 * @param field - Field string in "key=value" or "key[]" format
302358 * @param raw - If true, keep value as string (no JSON parsing)
303- * @throws {Error } When field format is invalid
359+ * @throws {ValidationError } When field format is invalid
304360 */
305361function processField (
306362 result : Record < string , unknown > ,
@@ -315,7 +371,10 @@ function processField(
315371 setNestedValue ( result , field , undefined ) ;
316372 return ;
317373 }
318- throw new Error ( `Invalid field format: ${ field } . Expected key=value` ) ;
374+ throw new ValidationError (
375+ `Invalid field format: ${ field } . Expected key=value` ,
376+ "field"
377+ ) ;
319378 }
320379
321380 const key = field . substring ( 0 , eqIndex ) ;
@@ -377,14 +436,17 @@ export function buildQueryParams(
377436 for ( const field of fields ) {
378437 const eqIndex = field . indexOf ( "=" ) ;
379438 if ( eqIndex === - 1 ) {
380- throw new Error ( `Invalid field format: ${ field } . Expected key=value` ) ;
439+ throw new ValidationError (
440+ `Invalid field format: ${ field } . Expected key=value` ,
441+ "field"
442+ ) ;
381443 }
382444
383445 const key = field . substring ( 0 , eqIndex ) ;
384446
385447 // Validate key format (same validation as parseFieldKey for consistency)
386448 if ( ! FIELD_KEY_REGEX . test ( key ) ) {
387- throw new Error ( `Invalid field key format: ${ key } ` ) ;
449+ throw new ValidationError ( `Invalid field key format: ${ key } ` , "field" ) ;
388450 }
389451
390452 const rawValue = field . substring ( eqIndex + 1 ) ;
@@ -409,7 +471,7 @@ export function buildQueryParams(
409471 *
410472 * @param fields - Array of "key=value" strings
411473 * @returns Record suitable for URLSearchParams
412- * @throws {Error } When field doesn't contain "=" or key is empty
474+ * @throws {ValidationError } When field doesn't contain "=" or key is empty
413475 * @internal Exported for testing
414476 */
415477export function buildRawQueryParams (
@@ -420,12 +482,18 @@ export function buildRawQueryParams(
420482 for ( const field of fields ) {
421483 const eqIndex = field . indexOf ( "=" ) ;
422484 if ( eqIndex === - 1 ) {
423- throw new Error ( `Invalid field format: ${ field } . Expected key=value` ) ;
485+ throw new ValidationError (
486+ `Invalid field format: ${ field } . Expected key=value` ,
487+ "field"
488+ ) ;
424489 }
425490
426491 const key = field . substring ( 0 , eqIndex ) ;
427492 if ( key === "" ) {
428- throw new Error ( "Invalid field key format: key cannot be empty" ) ;
493+ throw new ValidationError (
494+ "Invalid field key format: key cannot be empty" ,
495+ "field"
496+ ) ;
429497 }
430498
431499 const value = field . substring ( eqIndex + 1 ) ;
@@ -796,7 +864,7 @@ export const apiCommand = buildCommand({
796864 flags : ApiFlags ,
797865 endpoint : string
798866 ) : Promise < void > {
799- const { stdout, stdin } = this ;
867+ const { stdout, stderr , stdin } = this ;
800868
801869 // Normalize endpoint to ensure trailing slash (Sentry API requirement)
802870 const normalizedEndpoint = normalizeEndpoint ( endpoint ) ;
@@ -810,12 +878,13 @@ export const apiCommand = buildCommand({
810878 // --input takes precedence for body content
811879 body = await buildBodyFromInput ( flags . input , stdin ) ;
812880 } else {
881+ // Auto-correct ':'-separated fields (e.g. -F status:resolved → -F status=resolved)
882+ // before routing to body or params so the correction applies everywhere.
883+ const field = normalizeFields ( flags . field , stderr ) ;
884+ const rawField = normalizeFields ( flags [ "raw-field" ] , stderr ) ;
885+
813886 // Route fields to body or params based on HTTP method
814- const options = prepareRequestOptions (
815- flags . method ,
816- flags . field ,
817- flags [ "raw-field" ]
818- ) ;
887+ const options = prepareRequestOptions ( flags . method , field , rawField ) ;
819888 body = options . body ;
820889 params = options . params ;
821890 }
0 commit comments