| |
| |
| |
| |
| |
|
|
| import { CommandKind, type SlashCommand } from '../ui/commands/types.js'; |
| import type { CommandConflict } from './types.js'; |
|
|
| |
| |
| |
| class CommandRegistry { |
| readonly commandMap = new Map<string, SlashCommand>(); |
| readonly conflictsMap = new Map<string, CommandConflict>(); |
| readonly firstEncounters = new Map<string, SlashCommand>(); |
|
|
| get finalCommands(): SlashCommand[] { |
| return Array.from(this.commandMap.values()); |
| } |
|
|
| get conflicts(): CommandConflict[] { |
| return Array.from(this.conflictsMap.values()); |
| } |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| export class SlashCommandResolver { |
| |
| |
| |
| |
| static resolve(allCommands: SlashCommand[]): { |
| finalCommands: SlashCommand[]; |
| conflicts: CommandConflict[]; |
| } { |
| const registry = new CommandRegistry(); |
|
|
| for (const cmd of allCommands) { |
| const originalName = cmd.name; |
| let finalName = originalName; |
|
|
| const shouldAlwaysPrefix = |
| cmd.kind === CommandKind.SKILL && !!cmd.extensionName; |
|
|
| if (shouldAlwaysPrefix) { |
| finalName = this.getRenamedName( |
| originalName, |
| this.getPrefix(cmd), |
| registry.commandMap, |
| cmd.kind, |
| ); |
| } else if (registry.firstEncounters.has(originalName)) { |
| |
| finalName = this.handleConflict(cmd, registry); |
| } else { |
| |
| registry.firstEncounters.set(originalName, cmd); |
| } |
|
|
| |
| registry.commandMap.set(finalName, { |
| ...cmd, |
| name: finalName, |
| }); |
| } |
|
|
| return { |
| finalCommands: registry.finalCommands, |
| conflicts: registry.conflicts, |
| }; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| private static handleConflict( |
| incoming: SlashCommand, |
| registry: CommandRegistry, |
| ): string { |
| const collidingName = incoming.name; |
| const originalClaimant = registry.firstEncounters.get(collidingName)!; |
|
|
| |
| if (incoming.kind === CommandKind.BUILT_IN) { |
| this.prefixExistingCommand(collidingName, incoming, registry); |
| return collidingName; |
| } |
|
|
| |
| const renamedName = this.getRenamedName( |
| incoming.name, |
| this.getPrefix(incoming), |
| registry.commandMap, |
| incoming.kind, |
| ); |
| this.trackConflict( |
| registry.conflictsMap, |
| collidingName, |
| originalClaimant, |
| incoming, |
| renamedName, |
| ); |
|
|
| |
| this.prefixExistingCommand(collidingName, incoming, registry); |
|
|
| return renamedName; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| private static prefixExistingCommand( |
| name: string, |
| reason: SlashCommand, |
| registry: CommandRegistry, |
| ): void { |
| const currentOwner = registry.commandMap.get(name); |
|
|
| |
| if (!currentOwner || currentOwner.kind === CommandKind.BUILT_IN) { |
| return; |
| } |
|
|
| |
| const renamedName = this.getRenamedName( |
| currentOwner.name, |
| this.getPrefix(currentOwner), |
| registry.commandMap, |
| currentOwner.kind, |
| ); |
|
|
| |
| registry.commandMap.delete(name); |
| const renamedOwner = { ...currentOwner, name: renamedName }; |
| registry.commandMap.set(renamedName, renamedOwner); |
|
|
| |
| this.trackConflict( |
| registry.conflictsMap, |
| name, |
| reason, |
| currentOwner, |
| renamedName, |
| ); |
| } |
|
|
| |
| |
| |
| private static getRenamedName( |
| name: string, |
| prefix: string | undefined, |
| commandMap: Map<string, SlashCommand>, |
| kind?: CommandKind, |
| ): string { |
| const isExtensionPrefix = |
| kind === CommandKind.SKILL || kind === CommandKind.EXTENSION_FILE; |
| const separator = isExtensionPrefix ? ':' : '.'; |
| const base = prefix ? `${prefix}${separator}${name}` : name; |
| let renamedName = base; |
| let suffix = 1; |
|
|
| while (commandMap.has(renamedName)) { |
| renamedName = `${base}${suffix}`; |
| suffix++; |
| } |
| return renamedName; |
| } |
|
|
| |
| |
| |
| private static getPrefix(cmd: SlashCommand): string | undefined { |
| switch (cmd.kind) { |
| case CommandKind.EXTENSION_FILE: |
| case CommandKind.SKILL: |
| return cmd.extensionName; |
| case CommandKind.MCP_PROMPT: |
| return cmd.mcpServerName; |
| case CommandKind.USER_FILE: |
| return 'user'; |
| case CommandKind.WORKSPACE_FILE: |
| return 'workspace'; |
| default: |
| return undefined; |
| } |
| } |
| |
| |
| |
| private static trackConflict( |
| conflictsMap: Map<string, CommandConflict>, |
| originalName: string, |
| reason: SlashCommand, |
| displacedCommand: SlashCommand, |
| renamedTo: string, |
| ) { |
| if (!conflictsMap.has(originalName)) { |
| conflictsMap.set(originalName, { |
| name: originalName, |
| losers: [], |
| }); |
| } |
|
|
| conflictsMap.get(originalName)!.losers.push({ |
| command: displacedCommand, |
| renamedTo, |
| reason, |
| }); |
| } |
| } |
|
|