98 lines
2.7 KiB
TypeScript
98 lines
2.7 KiB
TypeScript
import type { CommandContext, CommandDefinition, CommandResult } from './types.js';
|
|
|
|
const MAX_INPUT_LENGTH = 2000;
|
|
|
|
export class CommandRegistry {
|
|
private commands = new Map<string, CommandDefinition>();
|
|
private aliasToCommand = new Map<string, string>();
|
|
|
|
register(def: CommandDefinition): void {
|
|
const canonicalName = this.normalizeName(def.name);
|
|
if (!canonicalName) {
|
|
throw new Error('Command name is required');
|
|
}
|
|
if (this.commands.has(canonicalName)) {
|
|
throw new Error(`Command already registered: ${canonicalName}`);
|
|
}
|
|
|
|
this.commands.set(canonicalName, { ...def, name: canonicalName });
|
|
|
|
for (const alias of def.aliases ?? []) {
|
|
const canonicalAlias = this.normalizeName(alias);
|
|
if (!canonicalAlias) {
|
|
continue;
|
|
}
|
|
if (canonicalAlias === canonicalName || this.aliasToCommand.has(canonicalAlias) || this.commands.has(canonicalAlias)) {
|
|
throw new Error(`Command alias already registered: ${canonicalAlias}`);
|
|
}
|
|
this.aliasToCommand.set(canonicalAlias, canonicalName);
|
|
}
|
|
}
|
|
|
|
get(nameOrAlias: string): CommandDefinition | undefined {
|
|
const normalized = this.normalizeName(nameOrAlias);
|
|
if (!normalized) {
|
|
return undefined;
|
|
}
|
|
const canonicalName = this.aliasToCommand.get(normalized) ?? normalized;
|
|
return this.commands.get(canonicalName);
|
|
}
|
|
|
|
list(): CommandDefinition[] {
|
|
return Array.from(this.commands.values());
|
|
}
|
|
|
|
isCommand(input: string): boolean {
|
|
return this.parse(input) !== null;
|
|
}
|
|
|
|
parse(input: string): { name: string; args: string[] } | null {
|
|
const trimmed = input.trim();
|
|
if (!trimmed.startsWith('/') || trimmed.length > MAX_INPUT_LENGTH) {
|
|
return null;
|
|
}
|
|
|
|
const withoutSlash = trimmed.slice(1).trim();
|
|
if (!withoutSlash) {
|
|
return null;
|
|
}
|
|
|
|
const [rawName, ...rest] = withoutSlash.split(/\s+/);
|
|
const name = this.normalizeName(rawName);
|
|
if (!name) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
name,
|
|
args: rest,
|
|
};
|
|
}
|
|
|
|
async execute(input: string, ctx: CommandContext): Promise<CommandResult> {
|
|
const parsed = this.parse(input);
|
|
if (!parsed) {
|
|
return { handled: false, text: '' };
|
|
}
|
|
|
|
const command = this.get(parsed.name);
|
|
if (!command) {
|
|
return { handled: false, text: '' };
|
|
}
|
|
|
|
try {
|
|
return await command.execute(parsed.args, ctx);
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : 'Unknown command error';
|
|
return {
|
|
handled: true,
|
|
text: `Command failed: ${message}`,
|
|
};
|
|
}
|
|
}
|
|
|
|
private normalizeName(value: string): string {
|
|
return value.trim().replace(/^\//, '').toLowerCase();
|
|
}
|
|
}
|