Files
flynn/src/commands/registry.ts
T
2026-02-12 22:47:22 -08:00

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();
}
}