@@ -12,6 +12,7 @@ import {
1212 getDescription ,
1313 getSchemaTypes ,
1414 getEnumChoices ,
15+ getAllowedSchemas ,
1516} from './json-schema'
1617import { lineByLineConsoleLogger } from './logging'
1718import { parseProcedureInputs } from './parse-procedure'
@@ -258,109 +259,53 @@ export function createCli<R extends AnyRouter>({router, ...params}: TrpcCliParam
258259 delete unusedOptionAliases [ propertyKey ]
259260 }
260261
261- const defaultValue =
262- 'default' in propertyValue
263- ? ( { exists : true , value : propertyValue . default } as const )
264- : ( { exists : false } as const )
262+ const allowedSchemas = getAllowedSchemas ( propertyValue )
263+ const firstSchemaWithDefault = allowedSchemas . find ( subSchema => 'default' in subSchema )
264+ const defaultValue = firstSchemaWithDefault
265+ ? ( { exists : true , value : firstSchemaWithDefault . default } as const )
266+ : ( { exists : false } as const )
265267
266268 const rootTypes = getSchemaTypes ( propertyValue ) . sort ( )
267269
268- const parseJson = ( value : string , ErrorClass : new ( message : string ) => Error = InvalidArgumentError ) => {
269- try {
270- return JSON . parse ( value ) as { }
271- } catch {
272- throw new ErrorClass ( `Malformed JSON.` )
273- }
274- }
275- /** try to get a parser that can confidently parse a string into the correct type. Returns null if it can't confidently parse */
276- const getValueParser = ( types : ReturnType < typeof getSchemaTypes > ) => {
277- types = types . map ( t => ( t === 'integer' ? 'number' : t ) )
278- if ( types . length === 2 && types [ 0 ] === 'boolean' && types [ 1 ] === 'number' ) {
279- return {
280- type : 'boolean|number' ,
281- parser : ( value : string ) => booleanParser ( value , { fallback : null } ) ?? numberParser ( value ) ,
282- } as const
283- }
284- if ( types . length === 1 && types [ 0 ] === 'boolean' ) {
285- return { type : 'boolean' , parser : ( value : string ) => booleanParser ( value ) } as const
286- }
287- if ( types . length === 1 && types [ 0 ] === 'number' ) {
288- return { type : 'number' , parser : ( value : string ) => numberParser ( value ) } as const
289- }
290- if ( types . length === 1 && types [ 0 ] === 'string' ) {
291- return { type : 'string' , parser : null } as const
292- }
293- return {
294- type : 'object' ,
295- parser : ( value : string ) => {
296- const parsed = parseJson ( value )
297- if ( ! types . length ) return parsed // if types is empty, it means any type is allowed - e.g. for json input
298- const jsonSchemaType = Array . isArray ( parsed ) ? 'array' : parsed === null ? 'null' : typeof parsed
299- if ( ! types . includes ( jsonSchemaType ) ) {
300- throw new InvalidArgumentError ( `Got ${ jsonSchemaType } but expected ${ types . join ( ' or ' ) } ` )
301- }
302- return parsed
303- } ,
304- } as const
305- }
306-
307270 const propertyType = rootTypes [ 0 ]
308271 const isValueRequired =
309272 'required' in parsedProcedure . optionsJsonSchema &&
310273 parsedProcedure . optionsJsonSchema . required ?. includes ( propertyKey )
311274 const isCliOptionRequired = isValueRequired && propertyType !== 'boolean' && ! defaultValue . exists
312275
313276 function negate ( ) {
314- const negation = new Option ( longOption . replace ( '--' , '--no-' ) , `Negate \`${ longOption } \` option.` . trim ( ) )
315- command . addOption ( negation )
277+ const shouldNegate = 'negatable' in propertyValue ? propertyValue . negatable : meta . negateBooleans
278+ if ( shouldNegate ) {
279+ const negation = new Option ( longOption . replace ( '--' , '--no-' ) , `Negate \`${ longOption } \` option.` . trim ( ) )
280+ command . addOption ( negation )
281+ }
316282 }
317283
318284 const bracketise = ( name : string ) => ( isCliOptionRequired ? `<${ name } >` : `[${ name } ]` )
319285
320- if ( rootTypes . length === 2 && rootTypes [ 0 ] === 'boolean' && rootTypes [ 1 ] === 'string' ) {
321- const option = new Option ( `${ flags } [value]` , description )
322- option . default ( defaultValue . exists ? defaultValue . value : false )
323- command . addOption ( option )
324- negate ( )
325- return
326- }
327- if ( rootTypes . length === 2 && rootTypes [ 0 ] === 'boolean' && rootTypes [ 1 ] === 'number' ) {
286+ if ( allowedSchemas . length > 1 ) {
328287 const option = new Option ( `${ flags } [value]` , description )
329- option . argParser ( getValueParser ( rootTypes ) . parser ! )
330- option . default ( defaultValue . exists ? defaultValue . value : false )
331- command . addOption ( option )
332- negate ( )
333- return
334- }
335- if ( rootTypes . length === 2 && rootTypes [ 0 ] === 'number' && rootTypes [ 1 ] === 'string' ) {
336- const option = new Option ( `${ flags } ${ bracketise ( 'value' ) } ` , description )
337- option . argParser ( value => {
338- const number = numberParser ( value , { fallback : null } )
339- return number ?? value
340- } )
341288 if ( defaultValue . exists ) option . default ( defaultValue . value )
289+ else if ( rootTypes . includes ( 'boolean' ) ) option . default ( false )
290+ option . argParser ( getOptionValueParser ( propertyValue ) )
342291 command . addOption ( option )
292+ if ( rootTypes . includes ( 'boolean' ) ) negate ( )
343293 return
344294 }
345295
346296 if ( rootTypes . length !== 1 ) {
347297 const option = new Option ( `${ flags } ${ bracketise ( 'json' ) } ` , description )
348- option . argParser ( getValueParser ( rootTypes ) . parser ! )
298+ option . argParser ( getOptionValueParser ( propertyValue ) )
349299 command . addOption ( option )
350300 return
351301 }
352302
353- if ( propertyType === 'boolean' && isValueRequired ) {
354- const option = new Option ( flags , description )
355- option . default ( defaultValue . exists ? defaultValue . value : false )
356- command . addOption ( option )
357- negate ( )
358- return
359- } else if ( propertyType === 'boolean' ) {
303+ if ( propertyType === 'boolean' ) {
360304 const option = new Option ( `${ flags } [boolean]` , description )
361305 option . argParser ( value => booleanParser ( value ) )
362306 // don't set a default value of `false`, because `undefined` is accepted by the procedure
363- if ( defaultValue . exists ) option . default ( defaultValue . value )
307+ if ( isValueRequired ) option . default ( false )
308+ else if ( defaultValue . exists ) option . default ( defaultValue . value )
364309 command . addOption ( option )
365310 negate ( )
366311 return
@@ -369,10 +314,7 @@ export function createCli<R extends AnyRouter>({router, ...params}: TrpcCliParam
369314 let option : Option | null = null
370315
371316 // eslint-disable-next-line unicorn/prefer-switch
372- if ( propertyType === 'string' && 'format' in propertyValue && ( propertyValue . format as string ) === 'json' ) {
373- // option = new Option(`${flags} ${bracketise('json')}`, description)
374- // option.argParser(value => parseJson(value, InvalidOptionArgumentError))
375- } else if ( propertyType === 'string' ) {
317+ if ( propertyType === 'string' ) {
376318 option = new Option ( `${ flags } ${ bracketise ( 'string' ) } ` , description )
377319 } else if ( propertyType === 'boolean' ) {
378320 option = new Option ( flags , description )
@@ -383,20 +325,18 @@ export function createCli<R extends AnyRouter>({router, ...params}: TrpcCliParam
383325 option = new Option ( `${ flags } [values...]` , description )
384326 if ( defaultValue . exists ) option . default ( defaultValue . value )
385327 else if ( isValueRequired ) option . default ( [ ] )
386- const itemsProp = 'items' in propertyValue ? ( propertyValue . items as JsonSchema7Type ) : null
387- const itemTypes = itemsProp ? getSchemaTypes ( itemsProp ) : [ ]
328+ const itemsSchema = 'items' in propertyValue ? ( propertyValue . items as JsonSchema7Type ) : { }
388329
389- const itemEnumTypes = itemsProp && getEnumChoices ( itemsProp )
330+ const itemEnumTypes = getEnumChoices ( itemsSchema )
390331 if ( itemEnumTypes ?. type === 'string_enum' ) {
391332 option . choices ( itemEnumTypes . choices )
392333 }
393334
394- const itemParser = getValueParser ( itemTypes )
395- if ( itemParser . parser ) {
396- option . argParser ( ( value , previous ) => {
397- const parsed = itemParser . parser ( value )
398- if ( Array . isArray ( previous ) ) return [ ...previous , parsed ] as unknown [ ]
399- return [ parsed ] as unknown [ ]
335+ const itemParser = getOptionValueParser ( itemsSchema )
336+ if ( itemParser ) {
337+ option . argParser ( ( value , previous ) : unknown [ ] => {
338+ const parsed = itemParser ( value )
339+ return Array . isArray ( previous ) ? [ ...previous , parsed ] : [ parsed ]
400340 } )
401341 }
402342 }
@@ -441,7 +381,6 @@ export function createCli<R extends AnyRouter>({router, ...params}: TrpcCliParam
441381 program . __ran ||= [ ]
442382 program . __ran . push ( command )
443383 const options = command . opts ( )
444- // console.dir({options, args}, {depth: null})
445384
446385 if ( args . at ( - 2 ) !== options ) {
447386 // This is a code bug and not recoverable. Will hopefully never happen but if commander totally changes their API this will break
@@ -672,3 +611,57 @@ const booleanParser = (val: string, {fallback = val as unknown} = {}) => {
672611 if ( val === 'false' ) return false
673612 return fallback
674613}
614+
615+ const getOptionValueParser = ( schema : JsonSchema7Type ) => {
616+ const allowedSchemas = getAllowedSchemas ( schema )
617+ . slice ( )
618+ . sort ( ( a , b ) => String ( getSchemaTypes ( a ) [ 0 ] ) . localeCompare ( String ( getSchemaTypes ( b ) [ 0 ] ) ) )
619+
620+ const typesArray = allowedSchemas . flatMap ( getSchemaTypes )
621+ const types = new Set ( typesArray )
622+
623+ return ( value : string ) => {
624+ const definitelyPrimitive = typesArray . every (
625+ t => t === 'boolean' || t === 'number' || t === 'integer' || t === 'string' ,
626+ )
627+ if ( types . size === 0 || ! definitelyPrimitive ) {
628+ // parse this as JSON - too risky to fall back to a string because that will probably do the wrong thing if someone passes malformed JSON like `{"foo": 1,}` (trailing comma)
629+ const hint = `Malformed JSON. If passing a string, pass it as a valid JSON string with quotes (${ JSON . stringify ( value ) } )`
630+ const parsed = parseJson ( value , InvalidOptionArgumentError , hint )
631+ if ( ! types . size ) return parsed // if types is empty, it means any type is allowed - e.g. for json input
632+ const jsonSchemaType = Array . isArray ( parsed ) ? 'array' : parsed === null ? 'null' : typeof parsed
633+ if ( ! types . has ( jsonSchemaType ) ) {
634+ throw new InvalidOptionArgumentError ( `Got ${ jsonSchemaType } but expected ${ [ ...types ] . join ( ' or ' ) } ` )
635+ }
636+ return parsed
637+ }
638+ if ( types . has ( 'boolean' ) ) {
639+ const parsed = booleanParser ( value , { fallback : null } )
640+ if ( typeof parsed === 'boolean' ) return parsed
641+ }
642+ if ( types . has ( 'number' ) ) {
643+ const parsed = numberParser ( value , { fallback : null } )
644+ if ( typeof parsed === 'number' ) return parsed
645+ }
646+ if ( types . has ( 'integer' ) ) {
647+ const parsed = numberParser ( value , { fallback : null } )
648+ if ( typeof parsed === 'number' && Number . isInteger ( parsed ) ) return parsed
649+ }
650+ if ( types . has ( 'string' ) ) {
651+ return value
652+ }
653+ throw new InvalidOptionArgumentError ( `Got ${ JSON . stringify ( value ) } but expected ${ [ ...types ] . join ( ' or ' ) } ` )
654+ }
655+ }
656+
657+ const parseJson = (
658+ value : string ,
659+ ErrorClass : new ( message : string ) => Error = InvalidArgumentError ,
660+ hint = `Malformed JSON.` ,
661+ ) => {
662+ try {
663+ return JSON . parse ( value ) as { }
664+ } catch {
665+ throw new ErrorClass ( hint )
666+ }
667+ }
0 commit comments