2025-07-07 16:45:44 -04:00
/ * *
* @license
* Copyright 2025 Google LLC
* SPDX - License - Identifier : Apache - 2.0
* /
2026-01-21 18:01:12 -05:00
import { debugLogger , coreEvents } from '@google/gemini-cli-core' ;
2025-08-26 00:04:53 +02:00
import type { SlashCommand } from '../ui/commands/types.js' ;
import type { ICommandLoader } from './types.js' ;
2025-07-07 16:45:44 -04:00
2025-07-20 16:57:34 -04:00
/ * *
* Orchestrates the discovery and loading of all slash commands for the CLI .
*
* This service operates on a provider - based loader pattern . It is initialized
* with an array of ` ICommandLoader ` instances , each responsible for fetching
* commands from a specific source ( e . g . , built - in code , local files ) .
*
* The CommandService is responsible for invoking these loaders , aggregating their
* results , and resolving any name conflicts . This architecture allows the command
* system to be extended with new sources without modifying the service itself .
* /
export class CommandService {
2026-01-21 18:01:12 -05:00
private static emittedFeedbacks = new Set < string > ( ) ;
/ * *
* Clears the set of emitted feedback messages .
* This should ONLY be used in tests to ensure isolation between test cases .
* /
static clearEmittedFeedbacksForTest ( ) : void {
CommandService . emittedFeedbacks . clear ( ) ;
}
2025-07-20 16:57:34 -04:00
/ * *
* Private constructor to enforce the use of the async factory .
* @param commands A readonly array of the fully loaded and de - duplicated commands .
* /
private constructor ( private readonly commands : readonly SlashCommand [ ] ) { }
2025-07-16 18:36:14 -04:00
2025-07-20 16:57:34 -04:00
/ * *
* Asynchronously creates and initializes a new CommandService instance .
*
* This factory method orchestrates the entire command loading process . It
* runs all provided loaders in parallel , aggregates their results , handles
2025-07-28 18:40:47 -07:00
* name conflicts for extension commands by renaming them , and then returns a
2025-07-20 16:57:34 -04:00
* fully constructed ` CommandService ` instance .
*
2025-07-28 18:40:47 -07:00
* Conflict resolution :
* - Extension commands that conflict with existing commands are renamed to
* ` extensionName.commandName `
* - Non - extension commands ( built - in , user , project ) override earlier commands
* with the same name based on loader order
*
2025-07-20 16:57:34 -04:00
* @param loaders An array of objects that conform to the ` ICommandLoader `
2025-07-28 18:40:47 -07:00
* interface . Built - in commands should come first , followed by FileCommandLoader .
2025-07-20 16:57:34 -04:00
* @param signal An AbortSignal to cancel the loading process .
* @returns A promise that resolves to a new , fully initialized ` CommandService ` instance .
* /
static async create (
loaders : ICommandLoader [ ] ,
signal : AbortSignal ,
) : Promise < CommandService > {
const results = await Promise . allSettled (
loaders . map ( ( loader ) = > loader . loadCommands ( signal ) ) ,
) ;
2025-07-07 16:45:44 -04:00
2025-07-20 16:57:34 -04:00
const allCommands : SlashCommand [ ] = [ ] ;
for ( const result of results ) {
if ( result . status === 'fulfilled' ) {
allCommands . push ( . . . result . value ) ;
} else {
2025-10-21 16:35:22 -04:00
debugLogger . debug ( 'A command loader failed:' , result . reason ) ;
2025-07-20 16:57:34 -04:00
}
}
2025-07-07 16:45:44 -04:00
2025-07-20 16:57:34 -04:00
const commandMap = new Map < string , SlashCommand > ( ) ;
for ( const cmd of allCommands ) {
2025-07-28 18:40:47 -07:00
let finalName = cmd . name ;
// Extension commands get renamed if they conflict with existing commands
if ( cmd . extensionName && commandMap . has ( cmd . name ) ) {
let renamedName = ` ${ cmd . extensionName } . ${ cmd . name } ` ;
let suffix = 1 ;
// Keep trying until we find a name that doesn't conflict
while ( commandMap . has ( renamedName ) ) {
renamedName = ` ${ cmd . extensionName } . ${ cmd . name } ${ suffix } ` ;
suffix ++ ;
}
2026-01-21 18:01:12 -05:00
const feedbackMsg = ` Extension command '/ ${ cmd . name } ' from ' ${ cmd . extensionName } ' was renamed to '/ ${ renamedName } ' due to a conflict with an existing command. ` ;
if ( ! CommandService . emittedFeedbacks . has ( feedbackMsg ) ) {
coreEvents . emitFeedback ( 'info' , feedbackMsg ) ;
CommandService . emittedFeedbacks . add ( feedbackMsg ) ;
}
2025-07-28 18:40:47 -07:00
finalName = renamedName ;
}
commandMap . set ( finalName , {
. . . cmd ,
name : finalName ,
} ) ;
2025-07-20 16:57:34 -04:00
}
2025-07-07 16:45:44 -04:00
2025-09-06 14:16:58 -07:00
const finalCommands = Object . freeze ( Array . from ( commandMap . values ( ) ) ) ;
2025-07-20 16:57:34 -04:00
return new CommandService ( finalCommands ) ;
2025-07-07 16:45:44 -04:00
}
2025-07-20 16:57:34 -04:00
/ * *
* Retrieves the currently loaded and de - duplicated list of slash commands .
*
* This method is a safe accessor for the service ' s state . It returns a
* readonly array , preventing consumers from modifying the service ' s internal state .
*
* @returns A readonly , unified array of available ` SlashCommand ` objects .
* /
getCommands ( ) : readonly SlashCommand [ ] {
2025-07-07 16:45:44 -04:00
return this . commands ;
}
}