@@ -1283,6 +1283,25 @@ fn maybe_print_runtime_header(command: &str, args: &[String], show_header: bool)
12831283 print_runtime_header ( show_header) ;
12841284}
12851285
1286+ fn sanitize_subcommand_aliases_for_completion ( cmd : & mut clap:: Command ) {
1287+ for subcmd in cmd. get_subcommands_mut ( ) {
1288+ if subcmd. get_visible_aliases ( ) . next ( ) . is_some ( ) {
1289+ let aliases: Vec < String > = subcmd. get_all_aliases ( ) . map ( str:: to_string) . collect ( ) ;
1290+ // Clear aliases and re-add them as hidden.
1291+ * subcmd = std:: mem:: take ( subcmd) . alias ( None :: < & str > ) . aliases ( aliases) ;
1292+ }
1293+ sanitize_subcommand_aliases_for_completion ( subcmd) ;
1294+ }
1295+ }
1296+
1297+ /// Build a completion command with hidden aliases so clap_complete's ID-based
1298+ /// deduplication preserves canonical names.
1299+ pub fn command_for_completion ( ) -> clap:: Command {
1300+ let mut cmd = Args :: command ( ) ;
1301+ sanitize_subcommand_aliases_for_completion ( & mut cmd) ;
1302+ cmd
1303+ }
1304+
12861305/// Build a clap Command with custom help formatting matching the JS CLI output.
12871306pub fn command_with_help ( ) -> clap:: Command {
12881307 command_with_help_with_options ( RenderOptions :: default ( ) )
@@ -1338,6 +1357,37 @@ mod tests {
13381357 args. iter ( ) . map ( |arg| ( * arg) . to_string ( ) ) . collect ( )
13391358 }
13401359
1360+ #[ test]
1361+ fn command_for_completion_preserves_canonical_subcommands ( ) {
1362+ let mut cmd = super :: command_for_completion ( ) ;
1363+ for ( parent, alias, name) in [
1364+ ( None , "i" , "install" ) ,
1365+ ( None , "up" , "update" ) ,
1366+ ( None , "rm" , "remove" ) ,
1367+ ( None , "un" , "remove" ) ,
1368+ ( None , "uninstall" , "remove" ) ,
1369+ ( Some ( "pm" ) , "ls" , "list" ) ,
1370+ ] {
1371+ let cmd = match parent {
1372+ Some ( name) => cmd. find_subcommand_mut ( name) . unwrap ( ) ,
1373+ None => & mut cmd,
1374+ } ;
1375+ let candidates =
1376+ clap_complete:: engine:: complete ( cmd, vec ! [ "vp" . into( ) , "" . into( ) ] , 1 , None )
1377+ . unwrap ( ) ;
1378+
1379+ assert ! (
1380+ candidates. iter( ) . any( |c| c. get_value( ) == name) ,
1381+ "missing {name}: {candidates:?}"
1382+ ) ;
1383+ assert ! (
1384+ !candidates. iter( ) . any( |c| c. get_value( ) == alias) ,
1385+ "alias {alias} should be hidden from candidates: {candidates:?}"
1386+ ) ;
1387+ assert_eq ! ( cmd. find_subcommand( alias) . unwrap( ) . get_name( ) , name) ;
1388+ }
1389+ }
1390+
13411391 #[ test]
13421392 fn raw_subcommand_is_the_token_as_written ( ) {
13431393 assert_eq ! ( raw_subcommand( & argv( & [ "vp" , "fmt" , "src/" ] ) ) , Some ( "fmt" ) ) ;
0 commit comments