@@ -4,6 +4,7 @@ import path from "node:path";
44import { type Readable as ReadableType } from "node:stream" ;
55import { fileURLToPath , pathToFileURL } from "node:url" ;
66import util from "node:util" ;
7+ import { type RootCommand } from "@bomb.sh/tab" ;
78import { type stringifyChunked as stringifyChunkedType } from "@discoveryjs/json-ext" ;
89import {
910 type Command as CommanderCommand ,
@@ -149,6 +150,7 @@ interface KnownWebpackCLICommands {
149150 help : CommandOptions < void , CommanderArgs , Context > ;
150151 info : CommandOptions < void , CommanderArgs , Context > ;
151152 configtest : CommandOptions < string | undefined , CommanderArgs , WebpackContext & Context > ;
153+ complete : CommandOptions < void , CommanderArgs , Context > ;
152154}
153155
154156// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -577,6 +579,12 @@ class WebpackCLI {
577579 return str . length > 0 ? str . charAt ( 0 ) . toUpperCase ( ) + str . slice ( 1 ) : str ;
578580 }
579581
582+ #commandTerm( command : CommanderCommand ) : string {
583+ const aliases = command . aliases ( ) . filter ( Boolean ) ;
584+
585+ return aliases . length > 0 ? `${ command . name ( ) } |${ aliases . join ( "|" ) } ` : command . name ( ) ;
586+ }
587+
580588 toKebabCase ( str : string ) : string {
581589 return str . replaceAll ( / ( [ a - z 0 - 9 ] ) ( [ A - Z ] ) / g, "$1-$2" ) . toLowerCase ( ) ;
582590 }
@@ -1424,14 +1432,12 @@ class WebpackCLI {
14241432 return `${ parentCmdNames } ${ command . usage ( ) } | ${ parentCmdNames } [command] [options]` ;
14251433 }
14261434
1427- return `${ parentCmdNames } ${ command . name ( ) } |${ command
1428- . aliases ( )
1429- . join ( "|" ) } ${ command . usage ( ) } `;
1435+ return `${ parentCmdNames } ${ this . #commandTerm( command ) } ${ command . usage ( ) } ` ;
14301436 } ,
14311437 // Support multiple aliases
14321438 subcommandTerm : ( command ) => {
14331439 const usage = command . usage ( ) ;
1434- return `${ command . name ( ) } | ${ command . aliases ( ) . join ( "|" ) } ${ usage . length > 0 ? ` ${ usage } ` : "" } ` ;
1440+ return `${ this . #commandTerm ( command ) } ${ usage . length > 0 ? ` ${ usage } ` : "" } ` ;
14351441 } ,
14361442 visibleOptions : function visibleOptions ( command ) {
14371443 return command . options . filter ( ( option ) => {
@@ -2439,6 +2445,16 @@ class WebpackCLI {
24392445 ) ;
24402446 } ,
24412447 } ,
2448+ complete : {
2449+ rawName : "complete" ,
2450+ name : "complete [shell]" ,
2451+ alias : [ ] ,
2452+ description : "Generate shell completion scripts." ,
2453+ action : ( ) => {
2454+ // Display-only stub for `--help`. Completion requests are handled in `run()`
2455+ // before parse so the completion adapter can inspect the loaded command tree.
2456+ } ,
2457+ } ,
24422458 } ;
24432459
24442460 #isCommand< A = void , O extends CommanderArgs = CommanderArgs , C extends Context = Context > (
@@ -2483,6 +2499,8 @@ class WebpackCLI {
24832499 return await this . makeCommand ( this . #commands. info ) ;
24842500 } else if ( this . #isCommand( commandName , this . #commands. configtest ) ) {
24852501 return await this . makeCommand ( this . #commands. configtest ) ;
2502+ } else if ( this . #isCommand( commandName , this . #commands. complete ) ) {
2503+ return await this . makeCommand ( this . #commands. complete ) ;
24862504 }
24872505
24882506 const pkg : string = commandName ;
@@ -2521,7 +2539,125 @@ class WebpackCLI {
25212539 return externalCommand ;
25222540 }
25232541
2524- async run ( args : readonly string [ ] , parseOptions : ParseOptions ) {
2542+ #isCompletionRequest( args : readonly string [ ] , parseOptions ?: ParseOptions ) : boolean {
2543+ const userArgs = parseOptions ?. from === "user" ? args : args . slice ( 2 ) ;
2544+
2545+ return userArgs . find ( ( arg ) => arg !== "--" && ! arg . startsWith ( "-" ) ) === "complete" ;
2546+ }
2547+
2548+ async #prepareCompletions( ) : Promise < void > {
2549+ // Register every option (same as `--help`) so tab can inspect the full tree.
2550+ this . program . forHelp = true ;
2551+
2552+ await Promise . all (
2553+ Object . values ( this . #commands)
2554+ . filter ( ( command ) => command . rawName !== "complete" )
2555+ . map ( ( command ) => this . #loadCommandByName( command . rawName ) ) ,
2556+ ) ;
2557+
2558+ const { default : tab } = await import ( "@bomb.sh/tab/commander" ) ;
2559+
2560+ this . #attachCompletionHandlers( tab ( this . program ) ) ;
2561+ }
2562+
2563+ #attachCompletionHandlers( completion : RootCommand ) : void {
2564+ const helpOption = completion . options . get ( "help" ) ;
2565+
2566+ if ( helpOption ) {
2567+ helpOption . handler = ( complete ) => {
2568+ complete ( "verbose" , "Show all available commands and options" ) ;
2569+ } ;
2570+ }
2571+
2572+ const outputHandler = ( complete : ( value : string , description : string ) => void ) => {
2573+ complete ( "json" , "JSON output" ) ;
2574+ complete ( "markdown" , "Markdown output" ) ;
2575+ } ;
2576+
2577+ for ( const name of [ "info" , "version" ] ) {
2578+ const outputOption = completion . commands . get ( name ) ?. options . get ( "output" ) ;
2579+
2580+ if ( outputOption ) {
2581+ outputOption . handler = outputHandler ;
2582+ }
2583+ }
2584+
2585+ const progressHandler = ( complete : ( value : string , description : string ) => void ) => {
2586+ complete ( "profile" , "Capture profiling data" ) ;
2587+ } ;
2588+
2589+ for ( const name of [ "build" , "watch" , "serve" ] ) {
2590+ const progressOption = completion . commands . get ( name ) ?. options . get ( "progress" ) ;
2591+
2592+ if ( progressOption ) {
2593+ progressOption . handler = progressHandler ;
2594+ }
2595+ }
2596+
2597+ const completeCommand = completion . commands . get ( "complete" ) ;
2598+ const shellArg = completeCommand ?. arguments . get ( "shell" ) ;
2599+
2600+ if ( shellArg ) {
2601+ shellArg . handler = ( complete ) => {
2602+ complete ( "zsh" , "Zsh" ) ;
2603+ complete ( "bash" , "Bash" ) ;
2604+ complete ( "fish" , "Fish" ) ;
2605+ complete ( "powershell" , "PowerShell" ) ;
2606+ } ;
2607+ }
2608+
2609+ for ( const command of this . program . commands ) {
2610+ const tabCommand = completion . commands . get ( command . name ( ) ) ;
2611+
2612+ if ( ! tabCommand ) {
2613+ continue ;
2614+ }
2615+
2616+ for ( const option of command . options ) {
2617+ const longName = option . long ?. slice ( 2 ) ;
2618+
2619+ if ( ! longName ) {
2620+ continue ;
2621+ }
2622+
2623+ const { configs } = option as Option & { configs ?: ArgumentConfig [ ] } ;
2624+
2625+ if ( ! configs ) {
2626+ continue ;
2627+ }
2628+
2629+ const values : string [ ] = [ ] ;
2630+
2631+ for ( const config of configs ) {
2632+ if ( config . type !== "enum" || ! config . values ) {
2633+ continue ;
2634+ }
2635+
2636+ for ( const value of config . values ) {
2637+ if ( typeof value === "string" ) {
2638+ values . push ( value ) ;
2639+ }
2640+ }
2641+ }
2642+
2643+ if ( values . length === 0 ) {
2644+ continue ;
2645+ }
2646+
2647+ const tabOption = tabCommand . options . get ( longName ) ;
2648+
2649+ if ( tabOption ) {
2650+ tabOption . handler = ( complete ) => {
2651+ for ( const value of values ) {
2652+ complete ( value , "" ) ;
2653+ }
2654+ } ;
2655+ }
2656+ }
2657+ }
2658+ }
2659+
2660+ async run ( args : readonly string [ ] , parseOptions ?: ParseOptions ) {
25252661 // Default `--color` and `--no-color` options
25262662
25272663 const self : WebpackCLI = this ;
@@ -2729,6 +2865,10 @@ class WebpackCLI {
27292865 await command . parseAsync ( [ ...commandOperands , ...unknown ] , { from : "user" } ) ;
27302866 } ) ;
27312867
2868+ if ( this . #isCompletionRequest( args , parseOptions ) ) {
2869+ await this . #prepareCompletions( ) ;
2870+ }
2871+
27322872 await this . program . parseAsync ( args , parseOptions ) ;
27332873 }
27342874
0 commit comments