1+ /**
2+ * sry api
3+ *
4+ * Make raw authenticated API requests to Sentry.
5+ * Similar to 'gh api' for GitHub.
6+ */
7+
18import { buildCommand } from "@stricli/core" ;
29import type { SryContext } from "../context.js" ;
310import { rawApiRequest } from "../lib/api-client.js" ;
411
12+ type HttpMethod = "GET" | "POST" | "PUT" | "DELETE" | "PATCH" ;
13+
514type ApiFlags = {
6- readonly method : "GET" | "POST" | "PUT" | "DELETE" | "PATCH" ;
15+ readonly method : HttpMethod ;
716 readonly field : string [ ] ;
817 readonly header : string [ ] ;
918 readonly include : boolean ;
1019 readonly silent : boolean ;
1120} ;
1221
22+ // ─────────────────────────────────────────────────────────────────────────────
23+ // Request Parsing
24+ // ─────────────────────────────────────────────────────────────────────────────
25+
26+ const VALID_METHODS : HttpMethod [ ] = [ "GET" , "POST" , "PUT" , "DELETE" , "PATCH" ] ;
27+
28+ /**
29+ * Parse HTTP method from string
30+ */
31+ function parseMethod ( value : string ) : HttpMethod {
32+ const upper = value . toUpperCase ( ) ;
33+ if ( ! VALID_METHODS . includes ( upper as HttpMethod ) ) {
34+ throw new Error (
35+ `Invalid method: ${ value } . Must be one of: ${ VALID_METHODS . join ( ", " ) } `
36+ ) ;
37+ }
38+ return upper as HttpMethod ;
39+ }
40+
41+ /**
42+ * Parse a single key=value field into nested object structure
43+ */
44+ function parseFieldValue ( value : string ) : unknown {
45+ try {
46+ return JSON . parse ( value ) ;
47+ } catch {
48+ return value ;
49+ }
50+ }
51+
52+ /**
53+ * Set a nested value in an object using dot notation key
54+ */
55+ function setNestedValue (
56+ obj : Record < string , unknown > ,
57+ key : string ,
58+ value : unknown
59+ ) : void {
60+ const keys = key . split ( "." ) ;
61+ let current = obj ;
62+
63+ for ( let i = 0 ; i < keys . length - 1 ; i ++ ) {
64+ const k = keys [ i ] ;
65+ if ( ! ( k in current ) ) {
66+ current [ k ] = { } ;
67+ }
68+ current = current [ k ] as Record < string , unknown > ;
69+ }
70+
71+ const lastKey = keys . at ( - 1 ) ;
72+ if ( lastKey ) {
73+ current [ lastKey ] = value ;
74+ }
75+ }
76+
77+ /**
78+ * Parse field arguments into request body object
79+ */
1380function parseFields ( fields : string [ ] ) : Record < string , unknown > {
1481 const result : Record < string , unknown > = { } ;
1582
@@ -20,31 +87,18 @@ function parseFields(fields: string[]): Record<string, unknown> {
2087 }
2188
2289 const key = field . substring ( 0 , eqIndex ) ;
23- let value : unknown = field . substring ( eqIndex + 1 ) ;
90+ const rawValue = field . substring ( eqIndex + 1 ) ;
91+ const value = parseFieldValue ( rawValue ) ;
2492
25- // Try to parse as JSON for complex values
26- try {
27- value = JSON . parse ( value as string ) ;
28- } catch {
29- // Keep as string if not valid JSON
30- }
31-
32- // Handle nested keys like "data.name"
33- const keys = key . split ( "." ) ;
34- let current = result ;
35- for ( let i = 0 ; i < keys . length - 1 ; i ++ ) {
36- const k = keys [ i ] ;
37- if ( ! ( k in current ) ) {
38- current [ k ] = { } ;
39- }
40- current = current [ k ] as Record < string , unknown > ;
41- }
42- current [ keys . at ( - 1 ) ] = value ;
93+ setNestedValue ( result , key , value ) ;
4394 }
4495
4596 return result ;
4697}
4798
99+ /**
100+ * Parse header arguments into headers object
101+ */
48102function parseHeaders ( headers : string [ ] ) : Record < string , string > {
49103 const result : Record < string , string > = { } ;
50104
@@ -62,6 +116,44 @@ function parseHeaders(headers: string[]): Record<string, string> {
62116 return result ;
63117}
64118
119+ // ─────────────────────────────────────────────────────────────────────────────
120+ // Response Output
121+ // ─────────────────────────────────────────────────────────────────────────────
122+
123+ /**
124+ * Write response headers to stdout
125+ */
126+ function writeResponseHeaders (
127+ stdout : NodeJS . WriteStream ,
128+ status : number ,
129+ headers : Headers
130+ ) : void {
131+ stdout . write ( `HTTP ${ status } \n` ) ;
132+ headers . forEach ( ( value , key ) => {
133+ stdout . write ( `${ key } : ${ value } \n` ) ;
134+ } ) ;
135+ stdout . write ( "\n" ) ;
136+ }
137+
138+ /**
139+ * Write response body to stdout
140+ */
141+ function writeResponseBody ( stdout : NodeJS . WriteStream , body : unknown ) : void {
142+ if ( body === null || body === undefined ) {
143+ return ;
144+ }
145+
146+ if ( typeof body === "object" ) {
147+ stdout . write ( `${ JSON . stringify ( body , null , 2 ) } \n` ) ;
148+ } else {
149+ stdout . write ( `${ String ( body ) } \n` ) ;
150+ }
151+ }
152+
153+ // ─────────────────────────────────────────────────────────────────────────────
154+ // Command Definition
155+ // ─────────────────────────────────────────────────────────────────────────────
156+
65157export const apiCommand = buildCommand ( {
66158 docs : {
67159 brief : "Make an authenticated API request" ,
@@ -87,16 +179,7 @@ export const apiCommand = buildCommand({
87179 flags : {
88180 method : {
89181 kind : "parsed" ,
90- parse : ( value : string ) => {
91- const valid = [ "GET" , "POST" , "PUT" , "DELETE" , "PATCH" ] ;
92- const upper = value . toUpperCase ( ) ;
93- if ( ! valid . includes ( upper ) ) {
94- throw new Error (
95- `Invalid method: ${ value } . Must be one of: ${ valid . join ( ", " ) } `
96- ) ;
97- }
98- return upper as "GET" | "POST" | "PUT" | "DELETE" | "PATCH" ;
99- } ,
182+ parse : parseMethod ,
100183 brief : "HTTP method (GET, POST, PUT, DELETE, PATCH)" ,
101184 default : "GET" as const ,
102185 variableName : "X" ,
@@ -133,60 +216,43 @@ export const apiCommand = buildCommand({
133216 endpoint : string
134217 ) : Promise < void > {
135218 const { process } = this ;
219+ const { stdout, stderr } = process ;
136220
137221 try {
138- // Parse request body from fields
139222 const body =
140- flags . field && flags . field . length > 0
141- ? parseFields ( flags . field )
142- : undefined ;
143-
144- // Parse additional headers
223+ flags . field ?. length > 0 ? parseFields ( flags . field ) : undefined ;
145224 const headers =
146- flags . header && flags . header . length > 0
147- ? parseHeaders ( flags . header )
148- : undefined ;
225+ flags . header ?. length > 0 ? parseHeaders ( flags . header ) : undefined ;
149226
150- // Make the request
151227 const response = await rawApiRequest ( endpoint , {
152228 method : flags . method ,
153229 body,
154230 headers,
155231 } ) ;
156232
157- // Silent mode - just set exit code
233+ // Silent mode - only set exit code
158234 if ( flags . silent ) {
159235 if ( response . status >= 400 ) {
160236 process . exitCode = 1 ;
161237 }
162238 return ;
163239 }
164240
165- // Include headers in output
241+ // Output headers if requested
166242 if ( flags . include ) {
167- process . stdout . write ( `HTTP ${ response . status } \n` ) ;
168- response . headers . forEach ( ( value , key ) => {
169- process . stdout . write ( `${ key } : ${ value } \n` ) ;
170- } ) ;
171- process . stdout . write ( "\n" ) ;
243+ writeResponseHeaders ( stdout , response . status , response . headers ) ;
172244 }
173245
174246 // Output body
175- if ( response . body !== null && response . body !== undefined ) {
176- if ( typeof response . body === "object" ) {
177- process . stdout . write ( `${ JSON . stringify ( response . body , null , 2 ) } \n` ) ;
178- } else {
179- process . stdout . write ( `${ String ( response . body ) } \n` ) ;
180- }
181- }
247+ writeResponseBody ( stdout , response . body ) ;
182248
183249 // Set exit code for error responses
184250 if ( response . status >= 400 ) {
185251 process . exitCode = 1 ;
186252 }
187253 } catch ( error ) {
188254 const message = error instanceof Error ? error . message : String ( error ) ;
189- process . stderr . write ( `Error: ${ message } \n` ) ;
255+ stderr . write ( `Error: ${ message } \n` ) ;
190256 process . exitCode = 1 ;
191257 }
192258 } ,
0 commit comments