@@ -208,10 +208,58 @@ export function preprocessArgs(
208208 return result ;
209209}
210210
211+ /**
212+ * Walk a JSON schema fragment and collect the set of allowed primitive types,
213+ * descending into `anyOf` / `oneOf` (used for unions). For variants whose type
214+ * is `array`, item types are collected separately into `itemTypes`.
215+ */
216+ function collectAllowedTypes ( prop : Record < string , any > | undefined , types : Set < string > , itemTypes : Set < string > ) : void {
217+ if ( ! prop ) return ;
218+
219+ if ( prop . type !== undefined ) {
220+ const list = Array . isArray ( prop . type ) ? prop . type : [ prop . type ] ;
221+ for ( const t of list ) {
222+ if ( typeof t === 'string' ) types . add ( t ) ;
223+ }
224+ if ( list . includes ( 'array' ) && prop . items ) {
225+ collectAllowedTypes ( prop . items , itemTypes , new Set ( ) ) ;
226+ }
227+ }
228+
229+ const variants = prop . anyOf ?? prop . oneOf ;
230+ if ( Array . isArray ( variants ) ) {
231+ for ( const variant of variants ) collectAllowedTypes ( variant , types , itemTypes ) ;
232+ }
233+ }
234+
235+ /** Coerce a single CLI string to a primitive based on the set of allowed types. */
236+ function coerceScalar ( value : unknown , allowedTypes : Set < string > ) : unknown {
237+ if ( typeof value !== 'string' ) return value ;
238+
239+ if ( allowedTypes . has ( 'boolean' ) ) {
240+ const lower = value . toLowerCase ( ) ;
241+ if ( lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on' ) return true ;
242+ if ( lower === 'false' || lower === '0' || lower === 'no' || lower === 'off' ) return false ;
243+ }
244+
245+ if ( allowedTypes . has ( 'number' ) || allowedTypes . has ( 'integer' ) ) {
246+ const trimmed = value . trim ( ) ;
247+ if ( trimmed !== '' ) {
248+ const num = Number ( trimmed ) ;
249+ if ( ! Number . isNaN ( num ) ) return num ;
250+ }
251+ }
252+
253+ return value ;
254+ }
255+
211256/**
212257 * Auto-coerce CLI string values to match the expected schema types.
213258 * Handles: string → number, string → boolean for primitive schema fields.
214259 * Arrays of primitives are also coerced element-wise.
260+ * Union types (`anyOf` / `oneOf`) are coerced to the most specific matching
261+ * primitive — e.g. `--test true` for `z.union([z.boolean(), z.string()])`
262+ * becomes the boolean `true` rather than the string "true".
215263 */
216264export function coerceArgs ( data : Record < string , unknown > , schema : StandardJSONSchemaV1 ) : Record < string , unknown > {
217265 let properties : Record < string , any > ;
@@ -229,43 +277,21 @@ export function coerceArgs(data: Record<string, unknown>, schema: StandardJSONSc
229277 const prop = properties [ key ] ;
230278 if ( ! prop ) continue ;
231279
232- const targetType = prop . type as string | undefined ;
233-
234- if ( targetType === 'number' || targetType === 'integer' ) {
235- if ( typeof value === 'string' ) {
236- const num = Number ( value ) ;
237- if ( ! Number . isNaN ( num ) ) result [ key ] = num ;
238- }
239- } else if ( targetType === 'boolean' ) {
240- if ( typeof value === 'string' ) {
241- const lower = value . toLowerCase ( ) ;
242- if ( lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on' ) result [ key ] = true ;
243- else if ( lower === 'false' || lower === '0' || lower === 'no' || lower === 'off' ) result [ key ] = false ;
244- }
245- } else if ( targetType === 'array' ) {
246- // Coerce single items to array
247- const arr = Array . isArray ( value ) ? value : [ value ] ;
248- const itemType = prop . items ?. type as string | undefined ;
249- if ( itemType === 'number' || itemType === 'integer' ) {
250- result [ key ] = arr . map ( ( v ) => {
251- if ( typeof v === 'string' ) {
252- const num = Number ( v ) ;
253- return Number . isNaN ( num ) ? v : num ;
254- }
255- return v ;
256- } ) ;
257- } else if ( itemType === 'boolean' ) {
258- result [ key ] = arr . map ( ( v ) => {
259- if ( typeof v === 'string' ) {
260- const lower = v . toLowerCase ( ) ;
261- if ( lower === 'true' || lower === '1' || lower === 'yes' || lower === 'on' ) return true ;
262- if ( lower === 'false' || lower === '0' || lower === 'no' || lower === 'off' ) return false ;
263- }
264- return v ;
265- } ) ;
266- } else if ( ! Array . isArray ( value ) ) {
267- result [ key ] = arr ;
268- }
280+ const types = new Set < string > ( ) ;
281+ const itemTypes = new Set < string > ( ) ;
282+ collectAllowedTypes ( prop , types , itemTypes ) ;
283+
284+ const isArrayValue = Array . isArray ( value ) ;
285+ const allowsArray = types . has ( 'array' ) ;
286+ const allowsScalar = types . has ( 'string' ) || types . has ( 'boolean' ) || types . has ( 'number' ) || types . has ( 'integer' ) ;
287+
288+ if ( isArrayValue && allowsArray ) {
289+ result [ key ] = value . map ( ( v ) => coerceScalar ( v , itemTypes ) ) ;
290+ } else if ( ! isArrayValue && allowsArray && ! allowsScalar ) {
291+ // Wrap single value into an array when only array shapes are allowed
292+ result [ key ] = [ coerceScalar ( value , itemTypes ) ] ;
293+ } else if ( ! isArrayValue ) {
294+ result [ key ] = coerceScalar ( value , types ) ;
269295 }
270296 }
271297
0 commit comments