UNPKG

@darksnow-ui/commander

Version:

Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs

1 lines โ€ข 118 kB
{"version":3,"sources":["../src/index.ts","../src/errors.ts","../src/utils.ts","../src/commander.ts","../src/builder.ts","../src/hooks/context.tsx","../src/hooks/useCustomCommand.ts","../src/hooks/useCommand.ts","../src/hooks/useInvoker.ts"],"sourcesContent":["// ============================================================================\n// CORE EXPORTS\n// ============================================================================\n\nexport { default as Commander } from \"./commander\";\nexport {\n CommandBuilder,\n CommandTemplate,\n command,\n simpleCommand,\n} from \"./builder\";\n\n// ============================================================================\n// HOOKS EXPORTS\n// ============================================================================\n\nexport * from \"./hooks\";\n\n// ============================================================================\n// TYPE EXPORTS\n// ============================================================================\n\nexport type {\n CommandKey,\n CommandCategory,\n Command,\n EventListener,\n CommandExecutionContext,\n SearchResult,\n SearchOptions,\n ExecutionResult,\n CommanderStats,\n // Validation types\n ValidationErrorDetail,\n ValidationResult,\n InputValidator,\n} from \"./types\";\n\n// ============================================================================\n// ERROR EXPORTS\n// ============================================================================\n\nexport {\n CommandError,\n CommandNotFoundError,\n CommandUnavailableError,\n CommandTimeoutError,\n CommandExecutionError,\n InputValidationError,\n createCommandError,\n isInputValidationError,\n isCommandError,\n} from \"./errors\";\n\n// ============================================================================\n// UTILITY EXPORTS\n// ============================================================================\n\nexport {\n generateId,\n normalizeSearchTerm,\n calculateSearchScore,\n getMatchedTerms,\n isValidCommandKey,\n isValidCommand,\n withTimeout,\n delay,\n debounce,\n unique,\n removeItem,\n deepClone,\n PriorityQueue,\n} from \"./utils\";\n\n// ============================================================================\n// VERSION\n// ============================================================================\n\nexport const VERSION = \"1.0.0\";\n","import type { ValidationErrorDetail } from \"./types\";\n\n// ============================================================================\n// CUSTOM ERROR CLASSES\n// ============================================================================\n\nexport class CommandError extends Error {\n name = \"CommandError\";\n constructor(\n message: string,\n public command?: string,\n ) {\n super(message);\n }\n}\n\n/**\n * Error thrown when command input validation fails\n * Contains detailed information about which fields failed and why\n */\nexport class InputValidationError extends CommandError {\n name = \"InputValidationError\";\n\n constructor(\n command: string,\n public errors: ValidationErrorDetail[],\n public input?: unknown,\n ) {\n const fieldList = errors.map((e) => e.path).join(\", \");\n super(`Input validation failed for ${command}: [${fieldList}]`, command);\n }\n\n /**\n * Get errors for a specific field path\n */\n getFieldErrors(path: string): ValidationErrorDetail[] {\n return this.errors.filter((e) => e.path === path);\n }\n\n /**\n * Get all required field errors\n */\n getRequiredErrors(): ValidationErrorDetail[] {\n return this.errors.filter((e) => e.code === \"required\");\n }\n\n /**\n * Get missing required field paths\n */\n getMissingFields(): string[] {\n return this.getRequiredErrors().map((e) => e.path);\n }\n\n /**\n * Check if a specific field has errors\n */\n hasFieldError(path: string): boolean {\n return this.errors.some((e) => e.path === path);\n }\n\n /**\n * Convert to a simple object for serialization\n */\n toJSON() {\n return {\n name: this.name,\n message: this.message,\n command: this.command,\n errors: this.errors,\n };\n }\n}\n\nexport class CommandNotFoundError extends CommandError {\n name = \"CommandNotFoundError\";\n constructor(command: string) {\n super(`Command not found: ${command}`, command);\n }\n}\n\nexport class CommandUnavailableError extends CommandError {\n name = \"CommandUnavailableError\";\n constructor(command: string) {\n super(`Command not available: ${command}`, command);\n }\n}\n\nexport class CommandTimeoutError extends CommandError {\n name = \"CommandTimeoutError\";\n constructor(command: string, timeout: number) {\n super(`Command timeout: ${command} (${timeout}ms)`, command);\n }\n}\n\nexport class CommandExecutionError extends CommandError {\n name = \"CommandExecutionError\";\n cause: Error;\n\n constructor(command: string, originalError: Error) {\n super(\n `Command execution failed: ${command} - ${originalError.message}`,\n command,\n );\n this.cause = originalError;\n }\n}\n\n// ============================================================================\n// ERROR FACTORY\n// ============================================================================\n\nexport function createCommandError(\n type: string,\n command: string,\n details?: any,\n): CommandError {\n switch (type) {\n case \"not-found\":\n return new CommandNotFoundError(command);\n case \"unavailable\":\n return new CommandUnavailableError(command);\n case \"timeout\":\n return new CommandTimeoutError(command, details?.timeout || 0);\n case \"execution\":\n return new CommandExecutionError(\n command,\n details?.error || new Error(\"Unknown error\"),\n );\n case \"validation\":\n return new InputValidationError(\n command,\n details?.errors || [],\n details?.input,\n );\n default:\n return new CommandError(`Unknown error type: ${type}`, command);\n }\n}\n\n// ============================================================================\n// TYPE GUARDS\n// ============================================================================\n\nexport function isInputValidationError(\n error: unknown,\n): error is InputValidationError {\n return error instanceof InputValidationError;\n}\n\nexport function isCommandError(error: unknown): error is CommandError {\n return error instanceof CommandError;\n}\n","// ============================================================================\n// ID GENERATION\n// ============================================================================\n\nlet counter = 0;\n\nexport function generateId(): string {\n return `cmd_${Date.now()}_${++counter}`;\n}\n\n// ============================================================================\n// SEARCH UTILITIES\n// ============================================================================\n\nexport function normalizeSearchTerm(term: string): string {\n return term.toLowerCase().trim();\n}\n\nexport function calculateSearchScore(\n command: any,\n queryTerms: string[],\n): number {\n let score = 0;\n const searchableText = [\n command.label,\n command.description || \"\",\n ...(command.tags || []),\n ...(command.searchKeywords || []),\n ]\n .join(\" \")\n .toLowerCase();\n\n queryTerms.forEach((term) => {\n // Exact match in label = highest score\n if (command.label.toLowerCase().includes(term)) {\n score += command.label.toLowerCase() === term ? 100 : 50;\n }\n\n // Match in description\n if (command.description?.toLowerCase().includes(term)) {\n score += 25;\n }\n\n // Match in tags\n if (command.tags?.some((tag: string) => tag.toLowerCase().includes(term))) {\n score += 15;\n }\n\n // Match in keywords\n if (\n command.searchKeywords?.some((keyword: string) =>\n keyword.toLowerCase().includes(term),\n )\n ) {\n score += 10;\n }\n\n // Fuzzy match\n if (searchableText.includes(term)) {\n score += 5;\n }\n });\n\n return score;\n}\n\nexport function getMatchedTerms(command: any, queryTerms: string[]): string[] {\n const matched: string[] = [];\n const searchableText = [\n command.label,\n command.description || \"\",\n ...(command.tags || []),\n ...(command.searchKeywords || []),\n ]\n .join(\" \")\n .toLowerCase();\n\n queryTerms.forEach((term) => {\n if (searchableText.includes(term)) {\n matched.push(term);\n }\n });\n\n return matched;\n}\n\n// ============================================================================\n// VALIDATION UTILITIES\n// ============================================================================\n\nexport function isValidCommandKey(key: any): key is string {\n return typeof key === \"string\" && key.length > 0;\n}\n\nexport function isValidCommand(command: any): boolean {\n return (\n command &&\n typeof command === \"object\" &&\n isValidCommandKey(command.key) &&\n typeof command.label === \"string\" &&\n command.label.length > 0 &&\n typeof command.handle === \"function\"\n );\n}\n\n// ============================================================================\n// ASYNC UTILITIES\n// ============================================================================\n\nexport function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n errorMessage: string = \"Operation timed out\",\n): Promise<T> {\n return Promise.race([\n promise,\n new Promise<never>((_, reject) => {\n setTimeout(() => {\n reject(new Error(errorMessage));\n }, timeoutMs);\n }),\n ]);\n}\n\nexport async function delay(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n\n// ============================================================================\n// DEBOUNCE UTILITY\n// ============================================================================\n\nexport function debounce<T extends (...args: any[]) => any>(\n func: T,\n wait: number,\n immediate: boolean = false,\n): (...args: Parameters<T>) => void {\n let timeout: NodeJS.Timeout | undefined;\n\n return function executedFunction(...args: Parameters<T>) {\n const later = () => {\n timeout = undefined;\n if (!immediate) func(...args);\n };\n\n const callNow = immediate && !timeout;\n\n clearTimeout(timeout);\n timeout = setTimeout(later, wait);\n\n if (callNow) func(...args);\n };\n}\n\n// ============================================================================\n// ARRAY UTILITIES\n// ============================================================================\n\nexport function unique<T>(array: T[]): T[] {\n return [...new Set(array)];\n}\n\nexport function removeItem<T>(array: T[], item: T): T[] {\n const index = array.indexOf(item);\n if (index > -1) {\n array.splice(index, 1);\n }\n return array;\n}\n\n// ============================================================================\n// OBJECT UTILITIES\n// ============================================================================\n\nexport function deepClone<T>(obj: T): T {\n if (obj === null || typeof obj !== \"object\") {\n return obj;\n }\n\n if (obj instanceof Date) {\n return new Date(obj.getTime()) as T;\n }\n\n if (obj instanceof Array) {\n return obj.map((item) => deepClone(item)) as T;\n }\n\n if (typeof obj === \"object\") {\n const cloned = {} as T;\n for (const key in obj) {\n if (obj.hasOwnProperty(key)) {\n cloned[key] = deepClone(obj[key]);\n }\n }\n return cloned;\n }\n\n return obj;\n}\n\n// ============================================================================\n// PRIORITY QUEUE IMPLEMENTATION\n// ============================================================================\n\nexport class PriorityQueue<T> {\n private items: Array<{ item: T; priority: number }> = [];\n\n enqueue(item: T, priority: number = 0): void {\n const queueItem = { item, priority };\n let added = false;\n\n for (let i = 0; i < this.items.length; i++) {\n if (queueItem.priority > this.items[i].priority) {\n this.items.splice(i, 0, queueItem);\n added = true;\n break;\n }\n }\n\n if (!added) {\n this.items.push(queueItem);\n }\n }\n\n dequeue(): T | undefined {\n return this.items.shift()?.item;\n }\n\n peek(): T | undefined {\n return this.items[0]?.item;\n }\n\n isEmpty(): boolean {\n return this.items.length === 0;\n }\n\n size(): number {\n return this.items.length;\n }\n\n clear(): void {\n this.items = [];\n }\n\n remove(predicate: (item: T) => boolean): boolean {\n const index = this.items.findIndex((queueItem) =>\n predicate(queueItem.item),\n );\n if (index >= 0) {\n this.items.splice(index, 1);\n return true;\n }\n return false;\n }\n}\n","import type {\n Command,\n CommandKey,\n CommandCategory,\n EventListener,\n CommandExecutionContext,\n SearchResult,\n SearchOptions,\n ExecutionResult,\n CommanderStats,\n} from \"./types\";\n\nimport {\n CommandNotFoundError,\n CommandUnavailableError,\n CommandTimeoutError,\n CommandExecutionError,\n InputValidationError,\n} from \"./errors\";\n\nimport {\n generateId,\n normalizeSearchTerm,\n calculateSearchScore,\n getMatchedTerms,\n isValidCommand,\n withTimeout,\n removeItem,\n} from \"./utils\";\n\n// ============================================================================\n// COMMAND HANDLER CLASS\n// ============================================================================\n\nclass CommandHandler {\n constructor(\n private key: CommandKey,\n private commander: Commander,\n ) {}\n\n exists(): boolean {\n return this.commander.has(this.key);\n }\n\n async isAvailable(): Promise<boolean> {\n const command = this.commander.getCommand(this.key);\n return command ? this.commander.isCommandAvailable(command) : false;\n }\n\n async invoke<T = any>(\n input?: any,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ): Promise<T> {\n return this.commander.invoke<T>(this.key, input, source);\n }\n\n async attempt<T = any>(\n input?: any,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ): Promise<ExecutionResult<T>> {\n return this.commander.attempt<T>(this.key, input, source);\n }\n\n getCommand(): Command | undefined {\n return this.commander.getCommand(this.key);\n }\n}\n\n// ============================================================================\n// MAIN COMMANDER CLASS\n// ============================================================================\n\nexport default class Commander {\n protected _commands: Map<CommandKey, Command> = new Map();\n protected listeners: Map<string, EventListener[]> = new Map();\n protected executionHistory: CommandExecutionContext[] = [];\n protected recentCommands: CommandKey[] = [];\n\n public maxHistorySize = 100;\n public maxRecentSize = 10;\n\n constructor(commands: Command[] = []) {\n commands.forEach((cmd) => this.add(cmd));\n }\n\n // ============================================================================\n // EVENT SYSTEM\n // ============================================================================\n\n listen<T = any>(\n event: string,\n callback: (...args: T[]) => void | Promise<void>,\n options: { once?: boolean } = {},\n ): EventListener<T> {\n const listener: EventListener<T> = {\n id: generateId(),\n event,\n callback,\n once: options.once,\n };\n\n if (!this.listeners.has(event)) {\n this.listeners.set(event, []);\n }\n this.listeners.get(event)!.push(listener as EventListener);\n\n return listener;\n }\n\n removeListener(listener: EventListener): void {\n for (const [event, listeners] of this.listeners) {\n const index = listeners.findIndex((l) => l.id === listener.id);\n if (index >= 0) {\n listeners.splice(index, 1);\n if (listeners.length === 0) {\n this.listeners.delete(event);\n }\n break;\n }\n }\n }\n\n private async emit(event: string, ...args: any[]): Promise<void> {\n const listeners = this.listeners.get(event) || [];\n const toRemove: EventListener[] = [];\n\n await Promise.allSettled(\n listeners.map(async (listener) => {\n try {\n await listener.callback(...args);\n if (listener.once) {\n toRemove.push(listener);\n }\n } catch (error) {\n console.error(`Event listener error for ${event}:`, error);\n }\n }),\n );\n\n // Remove one-time listeners\n toRemove.forEach((listener) => this.removeListener(listener));\n }\n\n // ============================================================================\n // COMMAND MANAGEMENT\n // ============================================================================\n\n commands(): Command[] {\n return Array.from(this._commands.values());\n }\n\n getCommand(key: CommandKey): Command | undefined {\n return this._commands.get(key);\n }\n\n add(command: Command): Command {\n if (!isValidCommand(command)) {\n throw new Error(`Invalid command: ${JSON.stringify(command)}`);\n }\n\n this._commands.set(command.key, command);\n this.emit(\"command:added\", command);\n return command;\n }\n\n remove(commandOrKey: string | CommandKey | Command): boolean {\n const key =\n typeof commandOrKey === \"string\"\n ? (commandOrKey as CommandKey)\n : typeof commandOrKey === \"object\"\n ? commandOrKey.key\n : commandOrKey;\n\n const command = this._commands.get(key);\n if (command) {\n this._commands.delete(key);\n this.emit(\"command:removed\", command);\n return true;\n }\n return false;\n }\n\n removeByOwner(owner: string): number {\n let removed = 0;\n for (const [key, command] of this._commands) {\n if (command.owner === owner) {\n this._commands.delete(key);\n this.emit(\"command:removed\", command);\n removed++;\n }\n }\n return removed;\n }\n\n removeByCategory(category: CommandCategory): number {\n let removed = 0;\n for (const [key, command] of this._commands) {\n if (command.category === category) {\n this._commands.delete(key);\n this.emit(\"command:removed\", command);\n removed++;\n }\n }\n return removed;\n }\n\n has(key: CommandKey): boolean {\n return this._commands.has(key);\n }\n\n // ============================================================================\n // SEARCH & FILTERING\n // ============================================================================\n\n async search(\n query: string,\n options: SearchOptions = {},\n ): Promise<SearchResult[]> {\n const normalizedQuery = normalizeSearchTerm(query);\n if (!normalizedQuery) {\n return this.getAllAvailable(options);\n }\n\n const results: SearchResult[] = [];\n const queryTerms = normalizedQuery.split(/\\s+/);\n\n for (const command of this._commands.values()) {\n // Filter by options first\n if (options.category && command.category !== options.category) continue;\n if (options.owner && command.owner !== options.owner) continue;\n if (\n options.tags?.length &&\n !options.tags.some((tag) => command.tags?.includes(tag))\n )\n continue;\n\n // Check availability\n if (\n !options.includeUnavailable &&\n !(await this.isCommandAvailable(command))\n ) {\n continue;\n }\n\n const score = calculateSearchScore(command, queryTerms);\n if (score > 0) {\n results.push({\n command,\n score,\n matchedTerms: getMatchedTerms(command, queryTerms),\n });\n }\n }\n\n // Sort by score (desc) and priority (desc)\n results.sort((a, b) => {\n if (a.score !== b.score) return b.score - a.score;\n return (b.command.priority || 0) - (a.command.priority || 0);\n });\n\n return options.limit ? results.slice(0, options.limit) : results;\n }\n\n private async getAllAvailable(\n options: SearchOptions,\n ): Promise<SearchResult[]> {\n const results: SearchResult[] = [];\n\n for (const command of this._commands.values()) {\n if (options.category && command.category !== options.category) continue;\n if (options.owner && command.owner !== options.owner) continue;\n if (\n options.tags?.length &&\n !options.tags.some((tag) => command.tags?.includes(tag))\n )\n continue;\n\n if (\n !options.includeUnavailable &&\n !(await this.isCommandAvailable(command))\n ) {\n continue;\n }\n\n results.push({\n command,\n score: command.priority || 0,\n matchedTerms: [],\n });\n }\n\n // Sort by priority and recent usage\n results.sort((a, b) => {\n const aRecentIndex = this.recentCommands.indexOf(a.command.key);\n const bRecentIndex = this.recentCommands.indexOf(b.command.key);\n\n // Recent commands first\n if (aRecentIndex >= 0 && bRecentIndex >= 0) {\n return aRecentIndex - bRecentIndex;\n }\n if (aRecentIndex >= 0) return -1;\n if (bRecentIndex >= 0) return 1;\n\n // Then by priority\n return (b.command.priority || 0) - (a.command.priority || 0);\n });\n\n return options.limit ? results.slice(0, options.limit) : results;\n }\n\n // ============================================================================\n // COMMAND AVAILABILITY\n // ============================================================================\n\n async isCommandAvailable(command: Command): Promise<boolean> {\n if (!command.when) return true;\n\n try {\n return await command.when();\n } catch (error) {\n console.warn(\n `Command availability check failed for ${command.key}:`,\n error,\n );\n return false;\n }\n }\n\n async getAvailableCommands(\n options: {\n category?: CommandCategory;\n owner?: string;\n } = {},\n ): Promise<Command[]> {\n const available: Command[] = [];\n\n for (const command of this._commands.values()) {\n if (options.category && command.category !== options.category) continue;\n if (options.owner && command.owner !== options.owner) continue;\n\n if (await this.isCommandAvailable(command)) {\n available.push(command);\n }\n }\n\n return available;\n }\n\n // ============================================================================\n // EXECUTION\n // ============================================================================\n\n async invoke<T = any>(\n key: CommandKey,\n input?: any,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ): Promise<T> {\n const command = this._commands.get(key);\n if (!command) {\n const error = new CommandNotFoundError(key);\n this.emit(\"command:error\", key, error);\n throw error;\n }\n\n // Check availability\n if (!(await this.isCommandAvailable(command))) {\n const error = new CommandUnavailableError(key);\n this.emit(\"command:error\", key, error);\n throw error;\n }\n\n // Validate input if validator is defined\n if (command.inputValidator) {\n const validationResult = await command.inputValidator(input);\n if (validationResult !== true) {\n const error = new InputValidationError(key, validationResult, input);\n this.emit(\"command:validation-error\", key, error);\n throw error;\n }\n }\n\n const context: CommandExecutionContext = {\n command,\n input,\n startTime: new Date(),\n source,\n };\n\n this.emit(\"command:executing\", context);\n\n try {\n // Execute with timeout\n const result = await this.executeWithTimeout(command, input);\n\n // Track execution\n this.trackExecution(context, result);\n this.addToRecent(key);\n\n this.emit(\"command:completed\", context, result);\n return result as T;\n } catch (error) {\n const executionError =\n error instanceof Error\n ? new CommandExecutionError(key, error)\n : new CommandExecutionError(key, new Error(String(error)));\n\n this.emit(\"command:failed\", context, executionError);\n throw executionError;\n }\n }\n\n /**\n * Validate input against command's inputValidator without executing\n * Useful for pre-validation before invoking\n *\n * @returns true if valid, or ValidationErrorDetail[] if invalid\n */\n async validateInput<TInput = any>(\n key: CommandKey,\n input?: TInput,\n ): Promise<true | import(\"./types\").ValidationErrorDetail[]> {\n const command = this._commands.get(key);\n if (!command) {\n throw new CommandNotFoundError(key);\n }\n\n if (!command.inputValidator) {\n return true;\n }\n\n return command.inputValidator(input);\n }\n\n private async executeWithTimeout<T>(\n command: Command,\n input?: any,\n ): Promise<T> {\n const timeoutMs = command.timeout || 30000;\n\n try {\n return await withTimeout(\n command.handle.call(this, input),\n timeoutMs,\n `Command timeout: ${command.key} (${timeoutMs}ms)`,\n );\n } catch (error) {\n if (error instanceof Error && error.message.includes(\"timeout\")) {\n throw new CommandTimeoutError(command.key, timeoutMs);\n }\n throw error;\n }\n }\n\n async attempt<T = any>(\n key: CommandKey,\n input?: any,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ): Promise<ExecutionResult<T>> {\n try {\n const result = await this.invoke<T>(key, input, source);\n return { success: true, result, command: key };\n } catch (error) {\n return { success: false, error, command: key };\n }\n }\n\n // ============================================================================\n // EXECUTION TRACKING\n // ============================================================================\n\n private trackExecution(context: CommandExecutionContext, result: any): void {\n this.executionHistory.push({\n ...context,\n // Don't store large results in history\n input: typeof context.input === \"object\" ? \"<object>\" : context.input,\n });\n\n // Keep history size manageable\n if (this.executionHistory.length > this.maxHistorySize) {\n this.executionHistory.splice(\n 0,\n this.executionHistory.length - this.maxHistorySize,\n );\n }\n }\n\n private addToRecent(key: CommandKey): void {\n // Remove if already exists\n const existingIndex = this.recentCommands.indexOf(key);\n if (existingIndex >= 0) {\n this.recentCommands.splice(existingIndex, 1);\n }\n\n // Add to front\n this.recentCommands.unshift(key);\n\n // Keep recent size manageable\n if (this.recentCommands.length > this.maxRecentSize) {\n this.recentCommands.splice(this.maxRecentSize);\n }\n }\n\n getExecutionHistory(limit?: number): CommandExecutionContext[] {\n return limit\n ? this.executionHistory.slice(-limit)\n : [...this.executionHistory];\n }\n\n getRecentCommands(): Command[] {\n return this.recentCommands\n .map((key) => this._commands.get(key))\n .filter((cmd): cmd is Command => cmd !== undefined);\n }\n\n clearHistory(): void {\n this.executionHistory = [];\n this.recentCommands = [];\n this.emit(\"history:cleared\");\n }\n\n // ============================================================================\n // CATEGORIES & ORGANIZATION\n // ============================================================================\n\n getCommandsByCategory(): Map<CommandCategory, Command[]> {\n const categories = new Map<CommandCategory, Command[]>();\n\n for (const command of this._commands.values()) {\n const category = command.category || \"custom\";\n if (!categories.has(category)) {\n categories.set(category, []);\n }\n categories.get(category)!.push(command);\n }\n\n return categories;\n }\n\n getCategories(): CommandCategory[] {\n const categories = new Set<CommandCategory>();\n for (const command of this._commands.values()) {\n categories.add(command.category || \"custom\");\n }\n return Array.from(categories);\n }\n\n // ============================================================================\n // UTILITY METHODS\n // ============================================================================\n\n createInvoker(key: CommandKey): CommandHandler {\n return new CommandHandler(key, this);\n }\n\n // Alias for backward compatibility\n invoker(key: CommandKey): CommandHandler {\n return this.createInvoker(key);\n }\n\n getStats(): CommanderStats {\n return {\n totalCommands: this._commands.size,\n categories: this.getCategories().length,\n executionHistory: this.executionHistory.length,\n recentCommands: this.recentCommands.length,\n listeners: Array.from(this.listeners.values()).flat().length,\n };\n }\n\n // ============================================================================\n // CLEANUP\n // ============================================================================\n\n destroy(): void {\n this._commands.clear();\n this.listeners.clear();\n this.executionHistory = [];\n this.recentCommands = [];\n }\n}\n","import type {\n Command,\n CommandKey,\n CommandCategory,\n InputValidator,\n} from \"./types\";\nimport { isValidCommandKey } from \"./utils\";\n\n// ============================================================================\n// COMMAND BUILDER PATTERN\n// ============================================================================\n\nexport class CommandBuilder<TInput = any, TOutput = any> {\n private command: Partial<Command<TInput, TOutput>> = {};\n\n /**\n * Create a new CommandBuilder instance\n */\n static create<T = any, R = any>(key: string): CommandBuilder<T, R> {\n return new CommandBuilder<T, R>().key(key as CommandKey);\n }\n\n /**\n * Set the command key (required)\n */\n key(key: CommandKey): this {\n if (!isValidCommandKey(key)) {\n throw new Error(\"Command key must be a non-empty string\");\n }\n this.command.key = key;\n return this;\n }\n\n /**\n * Set the command label (required)\n */\n label(label: string): this {\n if (!label || typeof label !== \"string\") {\n throw new Error(\"Command label must be a non-empty string\");\n }\n this.command.label = label;\n return this;\n }\n\n /**\n * Set the command description\n */\n description(description: string): this {\n this.command.description = description;\n return this;\n }\n\n /**\n * Set the command category\n */\n category(category: CommandCategory): this {\n this.command.category = category;\n return this;\n }\n\n /**\n * Set the command owner\n */\n owner(owner: string): this {\n this.command.owner = owner;\n return this;\n }\n\n /**\n * Add tags to the command\n */\n tags(...tags: string[]): this {\n this.command.tags = [...(this.command.tags || []), ...tags];\n return this;\n }\n\n /**\n * Set the command icon\n */\n icon(icon: string): this {\n this.command.icon = icon;\n return this;\n }\n\n /**\n * Set the keyboard shortcut\n */\n shortcut(shortcut: string): this {\n this.command.shortcut = shortcut;\n return this;\n }\n\n /**\n * Set the availability condition\n */\n when(condition: () => boolean | Promise<boolean>): this {\n this.command.when = condition;\n return this;\n }\n\n /**\n * Add search keywords\n */\n searchKeywords(...keywords: string[]): this {\n this.command.searchKeywords = [\n ...(this.command.searchKeywords || []),\n ...keywords,\n ];\n return this;\n }\n\n /**\n * Set the command priority\n */\n priority(priority: number): this {\n this.command.priority = priority;\n return this;\n }\n\n /**\n * Set the command timeout\n */\n timeout(ms: number): this {\n if (ms <= 0) {\n throw new Error(\"Timeout must be a positive number\");\n }\n this.command.timeout = ms;\n return this;\n }\n\n /**\n * Set input validator function\n * Works with Zod, Yup, AJV, or any custom validation\n *\n * @example\n * // With Zod\n * .inputValidator((input) => {\n * const result = userSchema.safeParse(input);\n * if (result.success) return true;\n * return result.error.issues.map(i => ({\n * path: i.path.join('.'),\n * message: i.message,\n * code: i.code\n * }));\n * })\n *\n * @example\n * // Simple validation\n * .inputValidator((input) => {\n * if (!input?.email) return [{ path: 'email', message: 'Required', code: 'required' }];\n * return true;\n * })\n */\n inputValidator(validator: InputValidator<TInput>): this {\n if (typeof validator !== \"function\") {\n throw new Error(\"Input validator must be a function\");\n }\n this.command.inputValidator = validator;\n return this;\n }\n\n /**\n * Set the command handler (required)\n */\n handle(handler: (input?: TInput) => Promise<TOutput>): this {\n if (typeof handler !== \"function\") {\n throw new Error(\"Command handler must be a function\");\n }\n this.command.handle = handler;\n return this;\n }\n\n /**\n * Build and return the command\n */\n build(): Command<TInput, TOutput> {\n if (!this.command.key) {\n throw new Error(\"Command key is required\");\n }\n if (!this.command.label) {\n throw new Error(\"Command label is required\");\n }\n if (!this.command.handle) {\n throw new Error(\"Command handle is required\");\n }\n\n return this.command as Command<TInput, TOutput>;\n }\n\n /**\n * Clone this builder to create a new one with the same configuration\n */\n clone(): CommandBuilder<TInput, TOutput> {\n const cloned = new CommandBuilder<TInput, TOutput>();\n cloned.command = { ...this.command };\n return cloned;\n }\n\n /**\n * Reset the builder to start over\n */\n reset(): this {\n this.command = {};\n return this;\n }\n\n /**\n * Get the current command state (for debugging)\n */\n getState(): Partial<Command<TInput, TOutput>> {\n return { ...this.command };\n }\n}\n\n// ============================================================================\n// COMMAND TEMPLATES\n// ============================================================================\n\nexport class CommandTemplate {\n /**\n * Create a system command template\n */\n static system(): CommandBuilder {\n return CommandBuilder.create(\"system:placeholder\")\n .category(\"system\")\n .owner(\"system\")\n .priority(10);\n }\n\n /**\n * Create a file command template\n */\n static file(): CommandBuilder {\n return CommandBuilder.create(\"file:placeholder\")\n .category(\"file\")\n .tags(\"file\")\n .priority(5);\n }\n\n /**\n * Create a debug command template\n */\n static debug(): CommandBuilder {\n return CommandBuilder.create(\"debug:placeholder\")\n .category(\"debug\")\n .owner(\"debug\")\n .tags(\"debug\", \"development\")\n .when(() => process.env.NODE_ENV === \"development\")\n .priority(15);\n }\n\n /**\n * Create a view command template\n */\n static view(): CommandBuilder {\n return CommandBuilder.create(\"view:placeholder\")\n .category(\"view\")\n .tags(\"view\", \"ui\")\n .priority(3);\n }\n\n /**\n * Create a tools command template\n */\n static tools(): CommandBuilder {\n return CommandBuilder.create(\"tools:placeholder\")\n .category(\"tools\")\n .tags(\"tools\", \"utility\")\n .priority(8);\n }\n\n /**\n * Create a custom command template\n */\n static custom(): CommandBuilder {\n return CommandBuilder.create(\"custom:placeholder\")\n .category(\"custom\")\n .owner(\"temporary\")\n .priority(1);\n }\n}\n\n// ============================================================================\n// FLUENT HELPERS\n// ============================================================================\n\n/**\n * Start building a command with a fluent API\n */\nexport function command<TInput = any, TOutput = any>(\n key: string,\n): CommandBuilder<TInput, TOutput> {\n return CommandBuilder.create<TInput, TOutput>(key);\n}\n\n/**\n * Create a simple command quickly\n */\nexport function simpleCommand<TInput = any, TOutput = any>(\n key: string,\n label: string,\n handler: (input?: TInput) => Promise<TOutput>,\n): Command<TInput, TOutput> {\n return CommandBuilder.create<TInput, TOutput>(key)\n .label(label)\n .handle(handler)\n .build();\n}\n\n// ============================================================================\n// VALIDATION HELPERS\n// ============================================================================\n\nexport function validateCommandConfiguration(\n command: Partial<Command>,\n): string[] {\n const errors: string[] = [];\n\n if (!command.key) {\n errors.push(\"Command key is required\");\n } else if (!isValidCommandKey(command.key)) {\n errors.push(\"Command key must be a non-empty string\");\n }\n\n if (!command.label) {\n errors.push(\"Command label is required\");\n } else if (\n typeof command.label !== \"string\" ||\n command.label.trim().length === 0\n ) {\n errors.push(\"Command label must be a non-empty string\");\n }\n\n if (!command.handle) {\n errors.push(\"Command handle is required\");\n } else if (typeof command.handle !== \"function\") {\n errors.push(\"Command handle must be a function\");\n }\n\n if (\n command.timeout !== undefined &&\n (typeof command.timeout !== \"number\" || command.timeout <= 0)\n ) {\n errors.push(\"Command timeout must be a positive number\");\n }\n\n if (command.priority !== undefined && typeof command.priority !== \"number\") {\n errors.push(\"Command priority must be a number\");\n }\n\n return errors;\n}\n","import {\n createContext,\n useContext,\n useEffect,\n useMemo,\n useState,\n useCallback,\n ReactNode,\n} from \"react\";\nimport Commander from \"../commander\";\nimport type {\n Command,\n CommandKey,\n CommandCategory,\n SearchResult,\n SearchOptions,\n ExecutionResult,\n} from \"../types\";\n\n// ============================================================================\n// CONTEXT TYPES\n// ============================================================================\n\ninterface CommanderContextValue {\n // Core commander instance\n commander: Commander;\n\n // Command management\n commands: () => Command[];\n add: (command: Command) => Command;\n remove: (commandOrKey: string | CommandKey | Command) => boolean;\n removeByOwner: (owner: string) => number;\n removeByCategory: (category: CommandCategory) => number;\n has: (key: CommandKey) => boolean;\n getCommand: (key: CommandKey) => Command | undefined;\n\n // Execution\n invoke: <T = any>(\n key: CommandKey,\n input?: any,\n source?: \"palette\" | \"shortcut\" | \"api\",\n ) => Promise<T>;\n attempt: <T = any>(\n key: CommandKey,\n input?: any,\n source?: \"palette\" | \"shortcut\" | \"api\",\n ) => Promise<ExecutionResult<T>>;\n\n // Search & filtering\n search: (query: string, options?: SearchOptions) => Promise<SearchResult[]>;\n isCommandAvailable: (command: Command) => Promise<boolean>;\n getAvailableCommands: (options?: {\n category?: CommandCategory;\n owner?: string;\n }) => Promise<Command[]>;\n\n // Organization\n getCommandsByCategory: () => Map<CommandCategory, Command[]>;\n getCategories: () => CommandCategory[];\n\n // History & tracking\n getExecutionHistory: (limit?: number) => any[];\n getRecentCommands: () => Command[];\n clearHistory: () => void;\n\n // Events\n listen: (\n event: string,\n callback: (...args: any[]) => void | Promise<void>,\n options?: { once?: boolean },\n ) => any;\n removeListener: (listener: any) => void;\n\n // Utilities\n createInvoker: (key: CommandKey) => any;\n getStats: () => any;\n\n // State\n isReady: boolean;\n}\n\nconst CommanderContext = createContext<CommanderContextValue | null>(null);\n\n// ============================================================================\n// PROVIDER PROPS\n// ============================================================================\n\ninterface CommanderProviderProps {\n children: ReactNode;\n commander: Commander;\n onReady?: (commander: Commander) => void;\n enableDevTools?: boolean;\n}\n\n// ============================================================================\n// PROVIDER COMPONENT\n// ============================================================================\n\nexport function CommanderProvider({\n children,\n commander,\n onReady,\n enableDevTools = process.env.NODE_ENV === \"development\",\n}: CommanderProviderProps) {\n // Setup dev tools and callbacks\n useEffect(() => {\n // Dev tools integration\n if (enableDevTools && typeof window !== \"undefined\") {\n // Expose commander globally for debugging\n (window as any).__commander = commander;\n\n // Add dev commands\n commander.add({\n key: \"dev:inspect-commands\" as CommandKey,\n label: \"Inspect All Commands\",\n description: \"Log all registered commands to console\",\n category: \"debug\",\n icon: \"๐Ÿ”\",\n owner: \"dev-tools\",\n tags: [\"debug\", \"inspect\"],\n priority: 100,\n handle: async () => {\n console.group(\"๐Ÿ“‹ Registered Commands\");\n commander.commands().forEach((cmd) => {\n console.log(`${cmd.icon || \"๐Ÿ“\"} ${cmd.label}`, {\n key: cmd.key,\n category: cmd.category,\n owner: cmd.owner,\n shortcut: cmd.shortcut,\n tags: cmd.tags,\n });\n });\n console.groupEnd();\n return {\n commands: commander.commands(),\n total: commander.commands().length,\n };\n },\n });\n\n commander.add({\n key: \"dev:clear-history\" as CommandKey,\n label: \"Clear Command History\",\n description: \"Clear execution history and recent commands\",\n category: \"debug\",\n icon: \"๐Ÿงน\",\n owner: \"dev-tools\",\n tags: [\"debug\", \"clear\"],\n priority: 90,\n handle: async () => {\n commander.clearHistory();\n console.log(\"Command history cleared\");\n return { cleared: true };\n },\n });\n\n commander.add({\n key: \"dev:stats\" as CommandKey,\n label: \"Show Commander Stats\",\n description: \"Display commander statistics\",\n category: \"debug\",\n icon: \"๐Ÿ“Š\",\n owner: \"dev-tools\",\n tags: [\"debug\", \"stats\"],\n priority: 80,\n handle: async () => {\n const stats = commander.getStats();\n console.log(\"Commander Stats:\", stats);\n return stats;\n },\n });\n\n commander.add({\n key: \"dev:search-test\" as CommandKey,\n label: \"Test Search System\",\n description: \"Test search functionality with sample queries\",\n category: \"debug\",\n icon: \"๐Ÿ”Ž\",\n owner: \"dev-tools\",\n tags: [\"debug\", \"search\"],\n priority: 70,\n handle: async () => {\n const queries = [\"save\", \"debug\", \"file\", \"system\"];\n const results: any = {};\n\n for (const query of queries) {\n const searchResults = await commander.search(query, { limit: 3 });\n results[query] = searchResults.map((r) => ({\n label: r.command.label,\n score: r.score,\n matched: r.matchedTerms,\n }));\n }\n\n console.log(\"Search Test Results:\", results);\n return results;\n },\n });\n }\n\n // Call onReady callback\n onReady?.(commander);\n\n // Cleanup dev tools on unmount\n return () => {\n if (enableDevTools && typeof window !== \"undefined\") {\n delete (window as any).__commander;\n commander.removeByOwner(\"dev-tools\");\n }\n };\n }, [commander, enableDevTools, onReady]);\n\n // Context value with all commander methods\n const value = useMemo<CommanderContextValue>(\n () => ({\n // Core instance\n commander,\n\n // Command management\n commands: () => commander.commands(),\n add: (command: Command) => commander.add(command),\n remove: (commandOrKey: string | CommandKey | Command) =>\n commander.remove(commandOrKey),\n removeByOwner: (owner: string) => commander.removeByOwner(owner),\n removeByCategory: (category: CommandCategory) =>\n commander.removeByCategory(category),\n has: (key: CommandKey) => commander.has(key),\n getCommand: (key: CommandKey) => commander.getCommand(key),\n\n // Execution\n invoke: <T = any,>(\n key: CommandKey,\n input?: any,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ) => commander.invoke<T>(key, input, source),\n attempt: <T = any,>(\n key: CommandKey,\n input?: any,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ) => commander.attempt<T>(key, input, source),\n\n // Search & filtering\n search: (query: string, options?: SearchOptions) =>\n commander.search(query, options),\n isCommandAvailable: (command: Command) =>\n commander.isCommandAvailable(command),\n getAvailableCommands: (options?: {\n category?: CommandCategory;\n owner?: string;\n }) => commander.getAvailableCommands(options),\n\n // Organization\n getCommandsByCategory: () => commander.getCommandsByCategory(),\n getCategories: () => commander.getCategories(),\n\n // History & tracking\n getExecutionHistory: (limit?: number) =>\n commander.getExecutionHistory(limit),\n getRecentCommands: () => commander.getRecentCommands(),\n clearHistory: () => commander.clearHistory(),\n\n // Events\n listen: (\n event: string,\n callback: (...args: any[]) => void | Promise<void>,\n options?: { once?: boolean },\n ) => commander.listen(event, callback, options),\n removeListener: (listener: any) => commander.removeListener(listener),\n\n // Utilities\n createInvoker: (key: CommandKey) => commander.createInvoker(key),\n getStats: () => commander.getStats(),\n\n // State\n isReady: true,\n }),\n [commander],\n );\n\n return (\n <CommanderContext.Provider value={value}>\n {children}\n </CommanderContext.Provider>\n );\n}\n\n// ============================================================================\n// HOOK TO ACCESS CONTEXT\n// ============================================================================\n\n/**\n * Hook to access the Commander context\n * Returns the full Commander API\n */\nexport function useCommander(): CommanderContextValue {\n const context = useContext(CommanderContext);\n\n if (!context) {\n throw new Error(\n \"useCommander must be used within a CommanderProvider. \" +\n \"Wrap your app with <CommanderProvider> to use commander hooks.\",\n );\n }\n\n if (!context.isReady) {\n throw new Error(\"Commander is not ready yet.\");\n }\n\n return context;\n}\n\n// ============================================================================\n// CONVENIENCE HOOKS\n// ============================================================================\n\n/**\n * Hook to get just the commander instance\n */\nexport function useCommanderInstance(): Commander {\n const { commander } = useCommander();\n return commander;\n}\n\n/**\n * Hook for direct command execution\n */\nexport function useInvoke() {\n const { invoke } = useCommander();\n return invoke;\n}\n\n/**\n * Hook for safe command execution\n */\nexport function useAttempt() {\n const { attempt } = useCommander();\n return attempt;\n}\n\n/**\n * Hook for command search\n */\nexport function useSearch() {\n const { search } = useCommander();\n return search;\n}\n\n/**\n * Hook to get all commands\n */\nexport function useCommands() {\n const { commands } = useCommander();\n return commands();\n}\n\n/**\n * Hook to get commands by category\n */\nexport function useCommandsByCategory() {\n const { getCommandsByCategory } = useCommander();\n return getCommandsByCategory();\n}\n\n/**\n * Hook to get recent commands\n */\nexport function useRecentCommands() {\n const { getRecentCommands } = useCommander();\n return getRecentCommands();\n}\n\n/**\n * Hook to get command stats\n */\nexport function useCommanderStats() {\n const { getStats } = useCommander();\n return getStats();\n}\n","import { useEffect, useMemo, useCallback, useState } from \"react\";\nimport { useCommander } from \"./context\";\nimport type { Command, CommandKey, CommandCategory } from \"../types\";\n\n// ============================================================================\n// TYPES\n// ============================================================================\n\ninterface UseCustomCommandProps<TInput = any, TOutput = any> {\n k