66
77import { debugLogger , coreEvents } from '@google/gemini-cli-core' ;
88import type { SlashCommand } from '../ui/commands/types.js' ;
9- import type { ICommandLoader } from './types.js' ;
10-
11- export interface CommandConflict {
12- name : string ;
13- winner : SlashCommand ;
14- losers : Array < {
15- command : SlashCommand ;
16- renamedTo : string ;
17- } > ;
18- }
9+ import type { ICommandLoader , CommandConflict } from './types.js' ;
10+ import { SlashCommandResolver } from './SlashCommandResolver.js' ;
1911
2012/**
2113 * Orchestrates the discovery and loading of all slash commands for the CLI.
@@ -24,9 +16,9 @@ export interface CommandConflict {
2416 * with an array of `ICommandLoader` instances, each responsible for fetching
2517 * commands from a specific source (e.g., built-in code, local files).
2618 *
27- * The CommandService is responsible for invoking these loaders, aggregating their
28- * results, and resolving any name conflicts. This architecture allows the command
29- * system to be extended with new sources without modifying the service itself .
19+ * It uses a delegating resolver to reconcile name conflicts, ensuring that
20+ * all commands are uniquely addressable via source-specific prefixes while
21+ * allowing built-in commands to retain their primary names .
3022 */
3123export class CommandService {
3224 /**
@@ -42,96 +34,71 @@ export class CommandService {
4234 /**
4335 * Asynchronously creates and initializes a new CommandService instance.
4436 *
45- * This factory method orchestrates the entire command loading process. It
46- * runs all provided loaders in parallel, aggregates their results, handles
47- * name conflicts for extension commands by renaming them, and then returns a
48- * fully constructed `CommandService` instance.
37+ * This factory method orchestrates the loading process and delegates
38+ * conflict resolution to the SlashCommandResolver.
4939 *
50- * Conflict resolution:
51- * - Extension commands that conflict with existing commands are renamed to
52- * `extensionName.commandName`
53- * - Non-extension commands (built-in, user, project) override earlier commands
54- * with the same name based on loader order
55- *
56- * @param loaders An array of objects that conform to the `ICommandLoader`
57- * interface. Built-in commands should come first, followed by FileCommandLoader.
58- * @param signal An AbortSignal to cancel the loading process.
59- * @returns A promise that resolves to a new, fully initialized `CommandService` instance.
40+ * @param loaders An array of loaders to fetch commands from.
41+ * @param signal An AbortSignal to allow cancellation.
42+ * @returns A promise that resolves to a fully initialized CommandService.
6043 */
6144 static async create (
6245 loaders : ICommandLoader [ ] ,
6346 signal : AbortSignal ,
6447 ) : Promise < CommandService > {
48+ const allCommands = await this . loadAllCommands ( loaders , signal ) ;
49+ const { finalCommands, conflicts } =
50+ SlashCommandResolver . resolve ( allCommands ) ;
51+
52+ if ( conflicts . length > 0 ) {
53+ this . emitConflictEvents ( conflicts ) ;
54+ }
55+
56+ return new CommandService (
57+ Object . freeze ( finalCommands ) ,
58+ Object . freeze ( conflicts ) ,
59+ ) ;
60+ }
61+
62+ /**
63+ * Invokes all loaders in parallel and flattens the results.
64+ */
65+ private static async loadAllCommands (
66+ loaders : ICommandLoader [ ] ,
67+ signal : AbortSignal ,
68+ ) : Promise < SlashCommand [ ] > {
6569 const results = await Promise . allSettled (
6670 loaders . map ( ( loader ) => loader . loadCommands ( signal ) ) ,
6771 ) ;
6872
69- const allCommands : SlashCommand [ ] = [ ] ;
73+ const commands : SlashCommand [ ] = [ ] ;
7074 for ( const result of results ) {
7175 if ( result . status === 'fulfilled' ) {
72- allCommands . push ( ...result . value ) ;
76+ commands . push ( ...result . value ) ;
7377 } else {
7478 debugLogger . debug ( 'A command loader failed:' , result . reason ) ;
7579 }
7680 }
81+ return commands ;
82+ }
7783
78- const commandMap = new Map < string , SlashCommand > ( ) ;
79- const conflictsMap = new Map < string , CommandConflict > ( ) ;
80-
81- for ( const cmd of allCommands ) {
82- let finalName = cmd . name ;
83-
84- // Extension commands get renamed if they conflict with existing commands
85- if ( cmd . extensionName && commandMap . has ( cmd . name ) ) {
86- const winner = commandMap . get ( cmd . name ) ! ;
87- let renamedName = `${ cmd . extensionName } .${ cmd . name } ` ;
88- let suffix = 1 ;
89-
90- // Keep trying until we find a name that doesn't conflict
91- while ( commandMap . has ( renamedName ) ) {
92- renamedName = `${ cmd . extensionName } .${ cmd . name } ${ suffix } ` ;
93- suffix ++ ;
94- }
95-
96- finalName = renamedName ;
97-
98- if ( ! conflictsMap . has ( cmd . name ) ) {
99- conflictsMap . set ( cmd . name , {
100- name : cmd . name ,
101- winner,
102- losers : [ ] ,
103- } ) ;
104- }
105-
106- conflictsMap . get ( cmd . name ) ! . losers . push ( {
107- command : cmd ,
108- renamedTo : finalName ,
109- } ) ;
110- }
111-
112- commandMap . set ( finalName , {
113- ...cmd ,
114- name : finalName ,
115- } ) ;
116- }
117-
118- const conflicts = Array . from ( conflictsMap . values ( ) ) ;
119- if ( conflicts . length > 0 ) {
120- coreEvents . emitSlashCommandConflicts (
121- conflicts . flatMap ( ( c ) =>
122- c . losers . map ( ( l ) => ( {
123- name : c . name ,
124- renamedTo : l . renamedTo ,
125- loserExtensionName : l . command . extensionName ,
126- winnerExtensionName : c . winner . extensionName ,
127- } ) ) ,
128- ) ,
129- ) ;
130- }
131-
132- const finalCommands = Object . freeze ( Array . from ( commandMap . values ( ) ) ) ;
133- const finalConflicts = Object . freeze ( conflicts ) ;
134- return new CommandService ( finalCommands , finalConflicts ) ;
84+ /**
85+ * Formats and emits telemetry for command conflicts.
86+ */
87+ private static emitConflictEvents ( conflicts : CommandConflict [ ] ) : void {
88+ coreEvents . emitSlashCommandConflicts (
89+ conflicts . flatMap ( ( c ) =>
90+ c . losers . map ( ( l ) => ( {
91+ name : c . name ,
92+ renamedTo : l . renamedTo ,
93+ loserExtensionName : l . command . extensionName ,
94+ winnerExtensionName : l . reason . extensionName ,
95+ loserMcpServerName : l . command . mcpServerName ,
96+ winnerMcpServerName : l . reason . mcpServerName ,
97+ loserKind : l . command . kind ,
98+ winnerKind : l . reason . kind ,
99+ } ) ) ,
100+ ) ,
101+ ) ;
135102 }
136103
137104 /**
0 commit comments