@@ -11,7 +11,13 @@ import * as Fetch from './Fetch.js'
1111import * as Filter from './Filter.js'
1212import * as Formatter from './Formatter.js'
1313import * as Help from './Help.js'
14- import { builtinCommands , type CommandMeta , type Shell , shells } from './internal/command.js'
14+ import {
15+ builtinCommands ,
16+ type CommandMeta ,
17+ findBuiltin ,
18+ type Shell ,
19+ shells ,
20+ } from './internal/command.js'
1521import * as Command from './internal/command.js'
1622import { isRecord , suggest } from './internal/helpers.js'
1723import { detectRunner } from './internal/pm.js'
@@ -239,6 +245,8 @@ export function create(
239245 return cli
240246 }
241247 commands . set ( nameOrCli , def )
248+ if ( def . aliases )
249+ for ( const a of def . aliases ) commands . set ( a , { _alias : true , target : nameOrCli } )
242250 return cli
243251 }
244252 const mountedRootDef = toRootDefinition . get ( nameOrCli )
@@ -530,8 +538,8 @@ async function serveImpl(
530538 } )
531539 }
532540 } else if ( nonFlags . length === 2 ) {
533- const parent = nonFlags [ nonFlags . length - 1 ]
534- const builtin = builtinCommands . find ( ( b ) => b . name === parent && b . subcommands )
541+ const parent = nonFlags [ nonFlags . length - 1 ] !
542+ const builtin = findBuiltin ( parent )
535543 if ( builtin ?. subcommands )
536544 for ( const sub of builtin . subcommands )
537545 if ( sub . name . startsWith ( current ) )
@@ -546,9 +554,8 @@ async function serveImpl(
546554 // Skills staleness check (skip for built-in commands)
547555 let skillsCta : FormattedCtaBlock | undefined
548556 if ( ! llms && ! llmsFull && ! schema && ! help && ! version ) {
549- const isSkillsAdd =
550- filtered [ 0 ] === 'skills' || ( filtered [ 0 ] === name && filtered [ 1 ] === 'skills' )
551- const isMcpAdd = filtered [ 0 ] === 'mcp' || ( filtered [ 0 ] === name && filtered [ 1 ] === 'mcp' )
557+ const isSkillsAdd = builtinIdx ( filtered , name , 'skills' ) !== - 1
558+ const isMcpAdd = builtinIdx ( filtered , name , 'mcp' ) !== - 1
552559 if ( ! isSkillsAdd && ! isMcpAdd ) {
553560 const stored = SyncSkills . readHash ( name )
554561 if ( stored ) {
@@ -574,8 +581,9 @@ async function serveImpl(
574581 const prefix : string [ ] = [ ]
575582 let scopedDescription : string | undefined = options . description
576583 for ( const token of filtered ) {
577- const entry = scopedCommands . get ( token )
578- if ( ! entry ) break
584+ const rawEntry = scopedCommands . get ( token )
585+ if ( ! rawEntry ) break
586+ const entry = resolveAlias ( scopedCommands , rawEntry )
579587 if ( isGroup ( entry ) ) {
580588 scopedCommands = entry . commands
581589 scopedDescription = entry . description
@@ -613,19 +621,11 @@ async function serveImpl(
613621 }
614622
615623 // completions <shell>: print shell hook script to stdout
616- const completionsIdx = ( ( ) => {
617- // e.g. `completions bash`
618- if ( filtered [ 0 ] === 'completions' ) return 0
619- // e.g. `my-cli completions bash`
620- if ( filtered [ 0 ] === name && filtered [ 1 ] === 'completions' ) return 1
621- // not a completions invocation
622- return - 1
623- } ) ( )
624- // TODO: refactor built-in command handlers (completions, skills, mcp) into a generic dispatch loop on `builtinCommands`
625- if ( completionsIdx !== - 1 && filtered [ completionsIdx ] === 'completions' ) {
624+ const completionsIdx = builtinIdx ( filtered , name , 'completions' )
625+ if ( completionsIdx !== - 1 ) {
626626 const shell = filtered [ completionsIdx + 1 ]
627627 if ( help || ! shell ) {
628- const b = builtinCommands . find ( ( c ) => c . name === 'completions' ) !
628+ const b = findBuiltin ( 'completions' ) !
629629 writeln (
630630 Help . formatCommand ( `${ name } completions` , {
631631 args : b . args ,
@@ -652,9 +652,8 @@ async function serveImpl(
652652 }
653653
654654 // skills add: generate skill files and install via `<pm>x skills add` (only when sync is configured)
655- const skillsIdx =
656- filtered [ 0 ] === 'skills' ? 0 : filtered [ 0 ] === name && filtered [ 1 ] === 'skills' ? 1 : - 1
657- if ( skillsIdx !== - 1 && filtered [ skillsIdx ] === 'skills' ) {
655+ const skillsIdx = builtinIdx ( filtered , name , 'skills' )
656+ if ( skillsIdx !== - 1 ) {
658657 const skillsSub = filtered [ skillsIdx + 1 ]
659658 if ( skillsSub && skillsSub !== 'add' && skillsSub !== 'list' ) {
660659 const suggestion = suggest ( skillsSub , [ 'add' , 'list' ] )
@@ -681,13 +680,13 @@ async function serveImpl(
681680 return
682681 }
683682 if ( ! skillsSub ) {
684- const b = builtinCommands . find ( ( c ) => c . name === 'skills' ) !
683+ const b = findBuiltin ( 'skills' ) !
685684 writeln ( formatBuiltinHelp ( name , b ) )
686685 return
687686 }
688687 if ( skillsSub === 'list' ) {
689688 if ( help ) {
690- const b = builtinCommands . find ( ( c ) => c . name === 'skills' ) !
689+ const b = findBuiltin ( 'skills' ) !
691690 writeln ( formatBuiltinSubcommandHelp ( name , b , 'list' ) )
692691 return
693692 }
@@ -733,7 +732,7 @@ async function serveImpl(
733732 return
734733 }
735734 if ( help ) {
736- const b = builtinCommands . find ( ( c ) => c . name === 'skills' ) !
735+ const b = findBuiltin ( 'skills' ) !
737736 writeln ( formatBuiltinSubcommandHelp ( name , b , 'add' ) )
738737 return
739738 }
@@ -797,8 +796,8 @@ async function serveImpl(
797796 }
798797
799798 // mcp add: register CLI as MCP server via `npx add-mcp`
800- const mcpIdx = filtered [ 0 ] === 'mcp' ? 0 : filtered [ 0 ] === name && filtered [ 1 ] === 'mcp' ? 1 : - 1
801- if ( mcpIdx !== - 1 && filtered [ mcpIdx ] === 'mcp' ) {
799+ const mcpIdx = builtinIdx ( filtered , name , 'mcp' )
800+ if ( mcpIdx !== - 1 ) {
802801 const mcpSub = filtered [ mcpIdx + 1 ]
803802 if ( mcpSub && mcpSub !== 'add' ) {
804803 const suggestion = suggest ( mcpSub , [ 'add' ] )
@@ -822,12 +821,12 @@ async function serveImpl(
822821 return
823822 }
824823 if ( ! mcpSub ) {
825- const b = builtinCommands . find ( ( c ) => c . name === 'mcp' ) !
824+ const b = findBuiltin ( 'mcp' ) !
826825 writeln ( formatBuiltinHelp ( name , b ) )
827826 return
828827 }
829828 if ( help ) {
830- const b = builtinCommands . find ( ( c ) => c . name === 'mcp' ) !
829+ const b = findBuiltin ( 'mcp' ) !
831830 writeln ( formatBuiltinSubcommandHelp ( name , b , 'add' ) )
832831 return
833832 }
@@ -1008,7 +1007,7 @@ async function serveImpl(
10081007 writeln (
10091008 Help . formatCommand ( commandName , {
10101009 alias : cmd . alias as Record < string , string > | undefined ,
1011- aliases : isRootCmd ? options . aliases : undefined ,
1010+ aliases : isRootCmd ? options . aliases : cmd . aliases ,
10121011 configFlag,
10131012 description : cmd . description ,
10141013 version : isRootCmd ? options . version : undefined ,
@@ -1897,7 +1896,7 @@ function resolveCommand(
18971896
18981897 if ( ! first || ! commands . has ( first ) ) return { error : first ?? '(none)' , path : '' , commands, rest }
18991898
1900- let entry = commands . get ( first ) !
1899+ let entry = resolveAlias ( commands , commands . get ( first ) ! )
19011900 const path = [ first ]
19021901 let remaining = rest
19031902 let inheritedOutputPolicy : OutputPolicy | undefined
@@ -1927,15 +1926,16 @@ function resolveCommand(
19271926 commands : entry . commands ,
19281927 }
19291928
1930- const child = entry . commands . get ( next )
1931- if ( ! child ) {
1929+ const rawChild = entry . commands . get ( next )
1930+ if ( ! rawChild ) {
19321931 return {
19331932 error : next ,
19341933 path : path . join ( ' ' ) ,
19351934 commands : entry . commands ,
19361935 rest : remaining . slice ( 1 ) ,
19371936 }
19381937 }
1938+ let child = resolveAlias ( entry . commands , rawChild )
19391939
19401940 path . push ( next )
19411941 remaining = remaining . slice ( 1 )
@@ -2260,14 +2260,26 @@ function collectHelpCommands(
22602260) : { name : string ; description ?: string | undefined } [ ] {
22612261 const result : { name : string ; description ?: string | undefined } [ ] = [ ]
22622262 for ( const [ name , entry ] of commands ) {
2263+ if ( isAlias ( entry ) ) continue
22632264 result . push ( { name, description : entry . description } )
22642265 }
22652266 return result . sort ( ( a , b ) => a . name . localeCompare ( b . name ) )
22662267}
22672268
2269+ /** @internal Finds the index of a builtin command token in the filtered argv. Returns -1 if not found. */
2270+ function builtinIdx ( filtered : string [ ] , cliName : string , builtin : string ) : number {
2271+ // e.g. `skills add` or `skill add`
2272+ if ( findBuiltin ( filtered [ 0 ] ! ) ?. name === builtin ) return 0
2273+ // e.g. `my-cli skills add`
2274+ if ( filtered [ 0 ] === cliName && findBuiltin ( filtered [ 1 ] ! ) ?. name === builtin ) return 1
2275+ // not a match
2276+ return - 1
2277+ }
2278+
22682279/** @internal Formats group-level help for a built-in command (e.g. `cli skills`). */
22692280function formatBuiltinHelp ( cli : string , builtin : ( typeof builtinCommands ) [ number ] ) : string {
22702281 return Help . formatRoot ( `${ cli } ${ builtin . name } ` , {
2282+ aliases : builtin . aliases ,
22712283 description : builtin . description ,
22722284 commands : builtin . subcommands ?. map ( ( s ) => ( { name : s . name , description : s . description } ) ) ,
22732285 } )
@@ -2314,7 +2326,11 @@ export type CommandsMap = Record<
23142326>
23152327
23162328/** @internal Entry stored in a command map — either a leaf definition, a group, or a fetch gateway. */
2317- type CommandEntry = CommandDefinition < any , any , any > | InternalGroup | InternalFetchGateway
2329+ type CommandEntry =
2330+ | CommandDefinition < any , any , any >
2331+ | InternalGroup
2332+ | InternalFetchGateway
2333+ | InternalAlias
23182334
23192335/** Controls when output data is displayed. `'all'` displays to both humans and agents. `'agent-only'` suppresses data output in human/TTY mode. */
23202336export type OutputPolicy = 'agent-only' | 'all'
@@ -2350,6 +2366,27 @@ function isFetchGateway(entry: CommandEntry): entry is InternalFetchGateway {
23502366 return '_fetch' in entry
23512367}
23522368
2369+ /** @internal An alias entry that points to another command by name. */
2370+ type InternalAlias = {
2371+ _alias : true
2372+ /** The canonical command name this alias resolves to. */
2373+ target : string
2374+ }
2375+
2376+ /** @internal Type guard for alias entries. */
2377+ function isAlias ( entry : CommandEntry ) : entry is InternalAlias {
2378+ return '_alias' in entry
2379+ }
2380+
2381+ /** @internal Follows an alias entry to its canonical target. Returns the entry unchanged if not an alias. */
2382+ function resolveAlias (
2383+ commands : Map < string , CommandEntry > ,
2384+ entry : CommandEntry ,
2385+ ) : Exclude < CommandEntry , InternalAlias > {
2386+ if ( isAlias ( entry ) ) return commands . get ( entry . target ) ! as Exclude < CommandEntry , InternalAlias >
2387+ return entry
2388+ }
2389+
23532390/** @internal Maps CLI instances to their command maps. */
23542391export const toCommands = new WeakMap < Cli , Map < string , CommandEntry > > ( )
23552392
@@ -2672,6 +2709,7 @@ function collectIndexCommands(
26722709) : { name : string ; description ?: string | undefined } [ ] {
26732710 const result : { name : string ; description ?: string | undefined } [ ] = [ ]
26742711 for ( const [ name , entry ] of commands ) {
2712+ if ( isAlias ( entry ) ) continue
26752713 const path = [ ...prefix , name ]
26762714 if ( isGroup ( entry ) ) {
26772715 result . push ( ...collectIndexCommands ( entry . commands , path ) )
@@ -2706,6 +2744,7 @@ function collectCommands(
27062744} [ ] {
27072745 const result : ReturnType < typeof collectCommands > = [ ]
27082746 for ( const [ name , entry ] of commands ) {
2747+ if ( isAlias ( entry ) ) continue
27092748 const path = [ ...prefix , name ]
27102749 if ( isFetchGateway ( entry ) ) {
27112750 const cmd : ( typeof result ) [ number ] = { name : path . join ( ' ' ) }
@@ -2762,6 +2801,7 @@ function collectSkillCommands(
27622801 result . push ( cmd )
27632802 }
27642803 for ( const [ name , entry ] of commands ) {
2804+ if ( isAlias ( entry ) ) continue
27652805 const path = [ ...prefix , name ]
27662806 if ( isFetchGateway ( entry ) ) {
27672807 const cmd : Skill . CommandInfo = { name : path . join ( ' ' ) }
@@ -2931,6 +2971,8 @@ type CommandDefinition<
29312971 vars extends z . ZodObject < any > | undefined = undefined ,
29322972 cliEnv extends z . ZodObject < any > | undefined = undefined ,
29332973> = CommandMeta < options > & {
2974+ /** Alternative names for this command (e.g. `['extensions', 'ext']` for an `extension` command). */
2975+ aliases ?: string [ ] | undefined
29342976 /** Zod schema for positional arguments. */
29352977 args ?: args | undefined
29362978 /** Zod schema for environment variables. Keys are the variable names (e.g. `NPM_TOKEN`). */
0 commit comments