@@ -21,10 +21,13 @@ type BooleanArg = CLIArg;
2121 * Any other argument is provided raw in process.argv as a string.
2222 * It can remain a string, or can be transformed into another type by a custom `parse` function.
2323 * It can be optional (by providing a default) or required (no default value).
24+ * It can also supersede other named arguments when provided.
2425 */
2526type StringArg < T = unknown > = CLIArg & {
2627 default ?: T ;
2728 parse ?: ( val : string ) => T ;
29+ supersedes ?: string [ ] ;
30+ required ?: boolean ;
2831} ;
2932
3033/**
@@ -162,6 +165,8 @@ class CLI<TConfig extends CLIConfig> {
162165
163166 const parsedNamedArgs : Partial < Writable < typeof this . namedArgs > > = { } ;
164167 const parsedPositionalArgs : Partial < Writable < typeof this . positionalArgs > > = { } ;
168+ const providedNamedArgs = new Set < string > ( ) ;
169+
165170 let positionalIndex = 0 ;
166171 for ( let i = 0 ; i < rawArgs . length ; i ++ ) {
167172 const rawArg = rawArgs . at ( i ) ;
@@ -177,6 +182,8 @@ class CLI<TConfig extends CLIConfig> {
177182 this . flags [ rawArgName as keyof typeof this . flags ] = true ;
178183 } else if ( config . namedArgs && rawArgName in config . namedArgs ) {
179184 // Arg is a named arg
185+ providedNamedArgs . add ( rawArgName ) ;
186+
180187 // Grab the value from the split token, otherwise go for the next token
181188 let argValueBeforeParse = '' ;
182189 if ( rawArgValue ) {
@@ -205,16 +212,34 @@ class CLI<TConfig extends CLIConfig> {
205212 }
206213 }
207214
215+ // Handle supercession logic
216+ const supersededArgs = new Set < string > ( ) ;
217+ for ( const [ name , spec ] of Object . entries ( config . namedArgs ?? { } ) ) {
218+ if ( providedNamedArgs . has ( name ) && spec . supersedes ) {
219+ for ( const supersededArg of spec . supersedes ) {
220+ supersededArgs . add ( supersededArg ) ;
221+ }
222+ }
223+ }
224+
208225 // Validate that all required args are present, assign defaults where values are not parsed
209226 for ( const [ name , spec ] of Object . entries ( config . namedArgs ?? { } ) ) {
210227 if ( ! ( name in parsedNamedArgs ) ) {
211- if ( spec . default !== undefined ) {
228+ if ( supersededArgs . has ( name ) ) {
229+ // This arg was superseded, so don't require it and don't assign a default
230+ continue ;
231+ } else if ( spec . default !== undefined ) {
212232 parsedNamedArgs [ name as keyof typeof parsedNamedArgs ] = spec . default as ValueOf < typeof parsedNamedArgs > ;
233+ } else if ( spec . required === false ) {
234+ // Explicitly marked as optional, leave undefined
235+ continue ;
213236 } else {
237+ // Arguments without defaults are required by default (unless explicitly marked as optional)
214238 throw new Error ( `Missing required named argument --${ name } ` ) ;
215239 }
216240 }
217241 }
242+
218243 for ( const spec of config . positionalArgs ?? [ ] ) {
219244 if ( ! ( spec . name in parsedPositionalArgs ) ) {
220245 if ( spec . default !== undefined ) {
@@ -263,7 +288,8 @@ class CLI<TConfig extends CLIConfig> {
263288 console . log ( 'Named Arguments:' ) ;
264289 for ( const [ name , spec ] of Object . entries ( namedArgs ) ) {
265290 const defaultLabel = spec . default !== undefined ? ` (default: ${ String ( spec . default ) } )` : '' ;
266- console . log ( ` --${ name . padEnd ( 20 ) } ${ spec . description } ${ defaultLabel } ` ) ;
291+ const supersededLabel = spec . supersedes && spec . supersedes . length > 0 ? ` (supersedes: ${ spec . supersedes . join ( ', ' ) } )` : '' ;
292+ console . log ( ` --${ name . padEnd ( 20 ) } ${ spec . description } ${ defaultLabel } ${ supersededLabel } ` ) ;
267293 }
268294 console . log ( '' ) ;
269295 }
0 commit comments