prefactor(commands): Command Service Prefactor for Extensible Commands (#4511)

This commit is contained in:
Abhi
2025-07-20 16:57:34 -04:00
committed by GitHub
parent 7a9821607b
commit 2a95c8287e
36 changed files with 919 additions and 720 deletions

View File

@@ -4,81 +4,80 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { Config } from '@google/gemini-cli-core';
import { SlashCommand } from '../ui/commands/types.js';
import { memoryCommand } from '../ui/commands/memoryCommand.js';
import { helpCommand } from '../ui/commands/helpCommand.js';
import { clearCommand } from '../ui/commands/clearCommand.js';
import { copyCommand } from '../ui/commands/copyCommand.js';
import { corgiCommand } from '../ui/commands/corgiCommand.js';
import { docsCommand } from '../ui/commands/docsCommand.js';
import { mcpCommand } from '../ui/commands/mcpCommand.js';
import { authCommand } from '../ui/commands/authCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js';
import { editorCommand } from '../ui/commands/editorCommand.js';
import { chatCommand } from '../ui/commands/chatCommand.js';
import { statsCommand } from '../ui/commands/statsCommand.js';
import { privacyCommand } from '../ui/commands/privacyCommand.js';
import { aboutCommand } from '../ui/commands/aboutCommand.js';
import { extensionsCommand } from '../ui/commands/extensionsCommand.js';
import { toolsCommand } from '../ui/commands/toolsCommand.js';
import { compressCommand } from '../ui/commands/compressCommand.js';
import { ideCommand } from '../ui/commands/ideCommand.js';
import { bugCommand } from '../ui/commands/bugCommand.js';
import { quitCommand } from '../ui/commands/quitCommand.js';
import { restoreCommand } from '../ui/commands/restoreCommand.js';
const loadBuiltInCommands = async (
config: Config | null,
): Promise<SlashCommand[]> => {
const allCommands = [
aboutCommand,
authCommand,
bugCommand,
chatCommand,
clearCommand,
copyCommand,
compressCommand,
corgiCommand,
docsCommand,
editorCommand,
extensionsCommand,
helpCommand,
ideCommand(config),
mcpCommand,
memoryCommand,
privacyCommand,
quitCommand,
restoreCommand(config),
statsCommand,
themeCommand,
toolsCommand,
];
return allCommands.filter(
(command): command is SlashCommand => command !== null,
);
};
import { ICommandLoader } from './types.js';
/**
* 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 {
private commands: SlashCommand[] = [];
/**
* 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[]) {}
constructor(
private config: Config | null,
private commandLoader: (
config: Config | null,
) => Promise<SlashCommand[]> = loadBuiltInCommands,
) {
// The constructor can be used for dependency injection in the future.
/**
* 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
* name conflicts by letting the last-loaded command win, and then returns a
* fully constructed `CommandService` instance.
*
* @param loaders An array of objects that conform to the `ICommandLoader`
* interface. The order of loaders is significant: if multiple loaders
* provide a command with the same name, the command from the loader that
* appears later in the array will take precedence.
* @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)),
);
const allCommands: SlashCommand[] = [];
for (const result of results) {
if (result.status === 'fulfilled') {
allCommands.push(...result.value);
} else {
console.debug('A command loader failed:', result.reason);
}
}
// De-duplicate commands using a Map. The last one found with a given name wins.
// This creates a natural override system based on the order of the loaders
// passed to the constructor.
const commandMap = new Map<string, SlashCommand>();
for (const cmd of allCommands) {
commandMap.set(cmd.name, cmd);
}
const finalCommands = Object.freeze(Array.from(commandMap.values()));
return new CommandService(finalCommands);
}
async loadCommands(): Promise<void> {
// For now, we only load the built-in commands.
// File-based and remote commands will be added later.
this.commands = await this.commandLoader(this.config);
}
getCommands(): SlashCommand[] {
/**
* 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[] {
return this.commands;
}
}