@@ -34,7 +34,7 @@ const defaultHttpClient: HttpClient = {
3434} ;
3535
3636interface AddProfileArgs {
37- "api-base-url" : string ;
37+ "api-base-url" ? : string ;
3838 "openapi-spec" : string ;
3939 "api-basic-auth" ? : string ;
4040 "api-bearer-token" ? : string ;
@@ -229,26 +229,25 @@ function buildRequestUrl(profile: Profile, command: CliCommand, flags: Record<st
229229 const value = flags [ opt . name ] ;
230230 if ( value !== undefined ) {
231231 const token = `{${ opt . name } }` ;
232- pathValue = pathValue . replace ( token , encodeURIComponent ( value ) ) ;
232+ pathValue = pathValue . replace ( token , serializePathParameter ( opt , value ) ) ;
233233 }
234234 } ) ;
235235
236- const baseUrl = profile . apiBaseUrl . replace ( / \/ + $ / , "" ) ;
237- let url = `${ baseUrl } ${ pathValue } ` ;
236+ const baseUrl = ( command . serverUrl ?? profile . apiBaseUrl ) . replace ( / \/ + $ / , "" ) ;
237+ let url = baseUrl ? `${ baseUrl } ${ pathValue } ` : pathValue ;
238238
239- const queryParams = new URLSearchParams ( ) ;
239+ const queryParts : string [ ] = [ ] ;
240240 command . options
241241 . filter ( ( opt ) => opt . location === "query" )
242242 . forEach ( ( opt ) => {
243243 const value = flags [ opt . name ] ;
244244 if ( value !== undefined ) {
245- queryParams . set ( opt . name , value ) ;
245+ queryParts . push ( ... serializeQueryParameter ( opt , value ) ) ;
246246 }
247247 } ) ;
248248
249- const queryString = queryParams . toString ( ) ;
250- if ( queryString ) {
251- url += url . includes ( "?" ) ? `&${ queryString } ` : `?${ queryString } ` ;
249+ if ( queryParts . length > 0 ) {
250+ url += url . includes ( "?" ) ? `&${ queryParts . join ( "&" ) } ` : `?${ queryParts . join ( "&" ) } ` ;
252251 }
253252
254253 return url ;
@@ -351,6 +350,164 @@ function buildRequestPayload(
351350 return { } ;
352351}
353352
353+ function serializePathParameter ( option : CliCommandOption , rawValue : string ) : string {
354+ const value = parseStructuredParameterValue ( option , rawValue ) ;
355+
356+ if ( Array . isArray ( value ) ) {
357+ const encoded = value . map ( ( item ) => encodeURIComponent ( String ( item ) ) ) ;
358+ const style = option . style ?? "simple" ;
359+ const explode = option . explode ?? false ;
360+
361+ if ( style === "label" ) {
362+ return explode ? `.${ encoded . join ( "." ) } ` : `.${ encoded . join ( "," ) } ` ;
363+ }
364+
365+ if ( style === "matrix" ) {
366+ return explode
367+ ? encoded . map ( ( item ) => `;${ encodeURIComponent ( option . name ) } =${ item } ` ) . join ( "" )
368+ : `;${ encodeURIComponent ( option . name ) } =${ encoded . join ( "," ) } ` ;
369+ }
370+
371+ return encoded . join ( "," ) ;
372+ }
373+
374+ if ( value && typeof value === "object" ) {
375+ const entries = Object . entries ( value as Record < string , unknown > ) . map (
376+ ( [ key , item ] ) => [ encodeURIComponent ( key ) , encodeURIComponent ( String ( item ) ) ] as const
377+ ) ;
378+ const style = option . style ?? "simple" ;
379+ const explode = option . explode ?? false ;
380+
381+ if ( style === "label" ) {
382+ return explode
383+ ? `.${ entries . map ( ( [ key , item ] ) => `${ key } =${ item } ` ) . join ( "." ) } `
384+ : `.${ entries . flat ( ) . join ( "," ) } ` ;
385+ }
386+
387+ if ( style === "matrix" ) {
388+ return explode
389+ ? entries . map ( ( [ key , item ] ) => `;${ key } =${ item } ` ) . join ( "" )
390+ : `;${ encodeURIComponent ( option . name ) } =${ entries . flat ( ) . join ( "," ) } ` ;
391+ }
392+
393+ return explode
394+ ? entries . map ( ( [ key , item ] ) => `${ key } =${ item } ` ) . join ( "," )
395+ : entries . flat ( ) . join ( "," ) ;
396+ }
397+
398+ return encodeURIComponent ( String ( value ) ) ;
399+ }
400+
401+ function serializeQueryParameter ( option : CliCommandOption , rawValue : string ) : string [ ] {
402+ const value = parseStructuredParameterValue ( option , rawValue ) ;
403+ const encodedName = encodeURIComponent ( option . name ) ;
404+
405+ if ( Array . isArray ( value ) ) {
406+ const encodedValues = value . map ( ( item ) => encodeURIComponent ( String ( item ) ) ) ;
407+
408+ if ( option . collectionFormat === "multi" ) {
409+ return encodedValues . map ( ( item ) => `${ encodedName } =${ item } ` ) ;
410+ }
411+
412+ const joiner = option . collectionFormat === "ssv"
413+ ? " "
414+ : option . collectionFormat === "tsv"
415+ ? "\t"
416+ : option . collectionFormat === "pipes"
417+ ? "|"
418+ : option . style === "spaceDelimited"
419+ ? " "
420+ : option . style === "pipeDelimited"
421+ ? "|"
422+ : "," ;
423+
424+ const explode = option . collectionFormat
425+ ? option . collectionFormat === "multi"
426+ : option . explode ?? true ;
427+
428+ if ( explode && joiner === "," ) {
429+ return encodedValues . map ( ( item ) => `${ encodedName } =${ item } ` ) ;
430+ }
431+
432+ return [ `${ encodedName } =${ encodedValues . join ( encodeURIComponent ( joiner ) ) } ` ] ;
433+ }
434+
435+ if ( value && typeof value === "object" ) {
436+ const entries = Object . entries ( value as Record < string , unknown > ) . map (
437+ ( [ key , item ] ) => [ encodeURIComponent ( key ) , encodeURIComponent ( String ( item ) ) ] as const
438+ ) ;
439+ const style = option . style ?? "form" ;
440+ const explode = option . explode ?? true ;
441+
442+ if ( style === "deepObject" ) {
443+ return entries . map ( ( [ key , item ] ) => `${ encodedName } %5B${ key } %5D=${ item } ` ) ;
444+ }
445+
446+ if ( explode ) {
447+ return entries . map ( ( [ key , item ] ) => `${ key } =${ item } ` ) ;
448+ }
449+
450+ return [ `${ encodedName } =${ entries . flat ( ) . join ( "," ) } ` ] ;
451+ }
452+
453+ return [ `${ encodedName } =${ encodeURIComponent ( String ( value ) ) } ` ] ;
454+ }
455+
456+ function parseStructuredParameterValue ( option : CliCommandOption , rawValue : string ) : unknown {
457+ if ( option . schemaType === "array" ) {
458+ const trimmed = rawValue . trim ( ) ;
459+ if ( trimmed . startsWith ( "[" ) ) {
460+ return parseBodyFlagValue ( rawValue ) ;
461+ }
462+ return rawValue . split ( "," ) . map ( ( item ) => item . trim ( ) ) . filter ( ( item ) => item . length > 0 ) ;
463+ }
464+
465+ if ( option . schemaType === "object" ) {
466+ const trimmed = rawValue . trim ( ) ;
467+ if ( ! trimmed . startsWith ( "{" ) ) {
468+ throw new Error ( `Object parameter --${ option . name } expects JSON object value` ) ;
469+ }
470+ return parseBodyFlagValue ( rawValue ) ;
471+ }
472+
473+ return rawValue ;
474+ }
475+
476+ function deriveApiBaseUrlFromSpec ( spec : unknown ) : string {
477+ if ( ! spec || typeof spec !== "object" ) {
478+ return "";
479+ }
480+
481+ const record = spec as Record < string , unknown > ;
482+ const servers = record . servers ;
483+ if ( Array . isArray ( servers ) && servers . length > 0 ) {
484+ const server = servers [ 0 ] as Record < string , unknown > ;
485+ if ( typeof server ?. url === "string" && server . url . length > 0 ) {
486+ let url = server . url ;
487+ const variables = server . variables as Record < string , { default ?: string } > | undefined ;
488+ if ( variables ) {
489+ Object . entries ( variables ) . forEach ( ( [ name , variable ] ) => {
490+ url = url . replace ( new RegExp ( `\\{${ name } \\}` , "g" ) , variable ?. default ?? "" ) ;
491+ } ) ;
492+ }
493+ if ( / ^ h t t p s ? : \/ \/ / i. test ( url ) ) {
494+ return url . replace ( / \/ + $ / , "" ) ;
495+ }
496+ }
497+ }
498+
499+ const host = typeof record . host === "string" ? record . host : "" ;
500+ if ( ! host ) {
501+ return "";
502+ }
503+
504+ const schemes = Array . isArray ( record . schemes ) && record . schemes . length > 0
505+ ? record . schemes as string [ ]
506+ : [ "https" ] ;
507+ const basePath = typeof record . basePath === "string" ? record . basePath : "" ;
508+ return `${ schemes [ 0 ] } ://${ host } ${ basePath } ` . replace ( / \/ + $ / , "" ) ;
509+ }
510+
354511export async function run ( argv : string [ ] , options ?: RunOptions ) : Promise < void > {
355512 const cwd = options ?. cwd ?? process . cwd ( ) ;
356513 const configLocator = options ?. configLocator ?? new ConfigLocator ( ) ;
@@ -394,7 +551,7 @@ export async function run(argv: string[], options?: RunOptions): Promise<void> {
394551
395552 const profile : Profile = {
396553 name : profileName ,
397- apiBaseUrl : args [ "api-base-url" ] ,
554+ apiBaseUrl : args [ "api-base-url" ] ?? "" ,
398555 apiBasicAuth : args [ "api-basic-auth" ] ?? "" ,
399556 apiBearerToken : args [ "api-bearer-token" ] ?? "" ,
400557 openapiSpecSource : args [ "openapi-spec" ] ,
@@ -405,13 +562,21 @@ export async function run(argv: string[], options?: RunOptions): Promise<void> {
405562 customHeaders,
406563 } ;
407564
408- await openapiLoader . loadSpec ( profile , { refresh : true } ) ;
565+ const spec = await openapiLoader . loadSpec ( profile , { refresh : true } ) ;
566+ profile . apiBaseUrl = profile . apiBaseUrl || deriveApiBaseUrlFromSpec ( spec ) ;
567+ if ( ! profile . apiBaseUrl ) {
568+ throw new Error ( "Unable to determine API base URL. Provide --api-base-url explicitly." ) ;
569+ }
409570 profileStore . saveProfile ( cwd , profile , { makeCurrent : true } ) ;
410571 } ;
411572
412573 const addProfileOptions = ( y : ReturnType < typeof yargs > ) =>
413574 y
414- . option ( "api-base-url" , { type : "string" , demandOption : true } )
575+ . option ( "api-base-url" , {
576+ type : "string" ,
577+ demandOption : false ,
578+ description : "Base URL for API requests. If omitted, derived from spec servers/host/basePath when possible." ,
579+ } )
415580 . option ( "openapi-spec" , { type : "string" , demandOption : true } )
416581 . option ( "api-basic-auth" , { type : "string" , default : "" } )
417582 . option ( "api-bearer-token" , { type : "string" , default : "" } )
0 commit comments