11// Reparse support for lazy commands after their placeholder has been replaced.
2- import type { Command } from "commander" ;
2+ import type { Command , Option } from "commander" ;
33import { buildParseArgv } from "../argv.js" ;
44import { resolveActionArgs , resolveCommandOptionArgs } from "./helpers.js" ;
55
6- function getCommandPathFromRoot ( command : Command | undefined ) : string [ ] {
7- const path : string [ ] = [ ] ;
6+ function getCommandPathFromRoot ( command : Command | undefined ) : Command [ ] {
7+ const path : Command [ ] = [ ] ;
88 let current = command ;
99 while ( current ?. parent ) {
10- const name = current . name ( ) ;
11- if ( name ) {
12- path . unshift ( name ) ;
10+ if ( current . name ( ) ) {
11+ path . unshift ( current ) ;
1312 }
1413 current = current . parent ;
1514 }
@@ -20,7 +19,7 @@ function buildFallbackArgv(program: Command, actionCommand: Command | undefined)
2019 const actionArgsList = resolveActionArgs ( actionCommand ) ;
2120 const parentOptionArgs =
2221 actionCommand ?. parent === program ? resolveCommandOptionArgs ( program ) : [ ] ;
23- const commandPath = getCommandPathFromRoot ( actionCommand ) ;
22+ const commandPath = getCommandPathFromRoot ( actionCommand ) . map ( ( command ) => command . name ( ) ) ;
2423 if ( commandPath . length === 0 ) {
2524 return [ ...parentOptionArgs , ...actionArgsList ] ;
2625 }
@@ -40,6 +39,151 @@ function findRootCommand(cmd: Command): Command {
4039 return current ;
4140}
4241
42+ function findOption ( command : Command , token : string ) : Option | undefined {
43+ const equalsIndex = token . indexOf ( "=" ) ;
44+ const flag = equalsIndex === - 1 ? token : token . slice ( 0 , equalsIndex ) ;
45+ return command . options . find (
46+ ( candidate ) =>
47+ ( candidate . short === flag || candidate . long === flag ) &&
48+ ( equalsIndex === - 1 || candidate . required || candidate . optional ) ,
49+ ) ;
50+ }
51+
52+ function findNearestOption ( commands : readonly Command [ ] , token : string ) : Option | undefined {
53+ for ( let index = commands . length - 1 ; index >= 0 ; index -= 1 ) {
54+ const command = commands [ index ] ;
55+ const option = command ? findOption ( command , token ) : undefined ;
56+ if ( option ) {
57+ return option ;
58+ }
59+ }
60+ return undefined ;
61+ }
62+
63+ function matchesCommandName ( command : Command , token : string ) : boolean {
64+ return command . name ( ) === token || command . aliases ( ) . includes ( token ) ;
65+ }
66+
67+ // Returns 0 for a missing required value, otherwise the number of consumed tokens.
68+ function optionTokenCount ( option : Option , argv : readonly string [ ] , index : number ) : number {
69+ const token = argv [ index ] ?? "" ;
70+ if ( token . includes ( "=" ) || ( ! option . required && ! option . optional ) ) {
71+ return 1 ;
72+ }
73+ const next = argv [ index + 1 ] ;
74+ if ( option . required ) {
75+ return next === undefined ? 0 : 2 ;
76+ }
77+ const optionalValue = next && ( ! next . startsWith ( "-" ) || / ^ - \d / . test ( next ) ) ;
78+ return optionalValue ? 2 : 1 ;
79+ }
80+
81+ function findCommandPathEnd ( argv : readonly string [ ] , command : Command ) : number {
82+ const path = getCommandPathFromRoot ( command ) ;
83+ const root = path [ 0 ] ?. parent ;
84+ if ( ! root ) {
85+ return - 1 ;
86+ }
87+ const selectedCommands = [ root ] ;
88+ let pathIndex = 0 ;
89+ for ( let index = 2 ; index < argv . length ; index += 1 ) {
90+ const token = argv [ index ] ?? "" ;
91+ const option = findNearestOption ( selectedCommands , token ) ;
92+ if ( option ) {
93+ const count = optionTokenCount ( option , argv , index ) ;
94+ if ( count === 0 ) {
95+ return - 1 ;
96+ }
97+ index += count - 1 ;
98+ continue ;
99+ }
100+ const nextCommand = path [ pathIndex ] ;
101+ if ( ! nextCommand || ! matchesCommandName ( nextCommand , token ) ) {
102+ return - 1 ;
103+ }
104+ selectedCommands . push ( nextCommand ) ;
105+ pathIndex += 1 ;
106+ if ( pathIndex === path . length ) {
107+ return index + 1 ;
108+ }
109+ }
110+ return - 1 ;
111+ }
112+
113+ /** Restore parent-option placement without stealing options owned by the loaded child command. */
114+ function hoistLazyParentOptions (
115+ argv : string [ ] ,
116+ parentCommand : Command ,
117+ lazyCommandName : string ,
118+ ) : string [ ] {
119+ let lazyCommandIndex = findCommandPathEnd ( argv , parentCommand ) ;
120+ if ( lazyCommandIndex === - 1 ) {
121+ return argv ;
122+ }
123+ while ( lazyCommandIndex < argv . length ) {
124+ const option = findOption ( parentCommand , argv [ lazyCommandIndex ] ?? "" ) ;
125+ if ( ! option ) {
126+ break ;
127+ }
128+ const count = optionTokenCount ( option , argv , lazyCommandIndex ) ;
129+ if ( count === 0 ) {
130+ return argv ;
131+ }
132+ lazyCommandIndex += count ;
133+ }
134+ if ( argv [ lazyCommandIndex ] !== lazyCommandName ) {
135+ return argv ;
136+ }
137+
138+ const lazyCommand = parentCommand . commands . find ( ( command ) =>
139+ matchesCommandName ( command , lazyCommandName ) ,
140+ ) ;
141+ if ( ! lazyCommand ) {
142+ return argv ;
143+ }
144+ let selectedCommand = lazyCommand ;
145+ const selectedCommands = [ selectedCommand ] ;
146+
147+ const hoisted : string [ ] = [ ] ;
148+ const remaining : string [ ] = [ ] ;
149+ let acceptsSubcommands = true ;
150+ for ( let index = lazyCommandIndex + 1 ; index < argv . length ; index += 1 ) {
151+ const token = argv [ index ] ?? "" ;
152+ if ( token === "--" ) {
153+ remaining . push ( ...argv . slice ( index ) ) ;
154+ break ;
155+ }
156+ const childOption = findNearestOption ( selectedCommands , token ) ;
157+ const parentOption = findOption ( parentCommand , token ) ;
158+ const option = childOption ?? parentOption ;
159+ if ( option ) {
160+ const count = optionTokenCount ( option , argv , index ) ;
161+ if ( count === 0 ) {
162+ return argv ;
163+ }
164+ const tokens = argv . slice ( index , index + count ) ;
165+ ( childOption ? remaining : hoisted ) . push ( ...tokens ) ;
166+ index += count - 1 ;
167+ continue ;
168+ }
169+ if ( acceptsSubcommands && ! token . startsWith ( "-" ) ) {
170+ const nextCommand : Command | undefined = selectedCommand . commands . find ( ( command ) =>
171+ matchesCommandName ( command , token ) ,
172+ ) ;
173+ if ( nextCommand ) {
174+ selectedCommand = nextCommand ;
175+ selectedCommands . push ( nextCommand ) ;
176+ } else {
177+ acceptsSubcommands = false ;
178+ }
179+ }
180+ remaining . push ( token ) ;
181+ }
182+ return hoisted . length === 0
183+ ? argv
184+ : [ ...argv . slice ( 0 , lazyCommandIndex ) , ...hoisted , lazyCommandName , ...remaining ] ;
185+ }
186+
43187/** Rebuild argv from Commander action args and re-run parsing after lazy registration. */
44188export async function reparseProgramFromActionArgs (
45189 program : Command ,
@@ -57,5 +201,8 @@ export async function reparseProgramFromActionArgs(
57201 rawArgs,
58202 fallbackArgv,
59203 } ) ;
60- await rootProgram . parseAsync ( parseArgv ) ;
204+ const normalizedArgv = actionCommand
205+ ? hoistLazyParentOptions ( parseArgv , program , actionCommand . name ( ) )
206+ : parseArgv ;
207+ await rootProgram . parseAsync ( normalizedArgv ) ;
61208}
0 commit comments