UNPKG

@darksnow-ui/commander

Version:

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

1 lines โ€ข 112 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, type CommandHandler } 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} from \"./types\"\n\n// ============================================================================\n// ERROR EXPORTS\n// ============================================================================\n\nexport {\n CommandError,\n CommandNotFoundError,\n CommandUnavailableError,\n CommandTimeoutError,\n CommandExecutionError,\n createCommandError,\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","// ============================================================================\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\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 default:\n return new CommandError(`Unknown error type: ${type}`, command)\n }\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) => predicate(queueItem.item))\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} 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\nexport class 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 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 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 { Command, CommandKey, CommandCategory } 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 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 key: string\n label?: string\n description?: string\n category?: CommandCategory\n tags?: string[]\n icon?: string\n shortcut?: string\n when?: () => boolean | Promise<boolean>\n handle: (input?: TInput) => Promise<TOutput>\n timeout?: number\n priority?: number\n owner?: string\n searchKeywords?: string[]\n}\n\ninterface CustomCommandInvoker<TInput, TOutput> {\n key: CommandKey\n exists: () => boolean\n isAvailable: () => Promise<boolean>\n invoke: (\n input?: TInput,\n source?: \"palette\" | \"shortcut\" | \"api\",\n ) => Promise<TOutput>\n attempt: (\n input?: TInput,\n source?: \"palette\" | \"shortcut\" | \"api\",\n ) => Promise<{\n success: boolean\n result?: TOutput\n error?: any\n command: CommandKey\n }>\n getCommand: () => Command<TInput, TOutput> | undefined\n}\n\n// ============================================================================\n// MAIN HOOK\n// ============================================================================\n\n/**\n * Hook to register a temporary command that exists only while the component is mounted\n *\n * @param props Command configuration\n * @returns Invoker object for executing the command\n *\n * @example\n * ```tsx\n * function FileEditor({ file }) {\n * const saveInvoker = useCustomCommand({\n * key: `file:save:${file.id}`,\n * label: `Save ${file.name}`,\n * icon: '๐Ÿ’พ',\n * shortcut: 'ctrl+s',\n * handle: async () => saveFile(file)\n * });\n *\n * // Command is automatically registered and appears in Command Palette\n * // Execute programmatically if needed:\n * const handleSave = () => saveInvoker.invoke();\n * }\n * ```\n */\nexport function useCustomCommand<TInput = any, TOutput = any>(\n props: UseCustomCommandProps<TInput, TOutput>,\n): CustomCommandInvoker<TInput, TOutput> {\n const commander = useCommander()\n\n const {\n key,\n label,\n description,\n category = \"custom\",\n tags = [],\n icon,\n shortcut,\n when,\n handle,\n timeout,\n priority,\n owner = \"temporary\",\n searchKeywords = [],\n } = props\n\n // Memoize command to avoid unnecessary re-registrations\n const command = useMemo<Command<TInput, TOutput>>(\n () => ({\n key: key as CommandKey,\n label: label || key,\n description,\n category,\n tags,\n icon,\n shortcut,\n when,\n handle,\n timeout,\n priority,\n owner,\n searchKeywords,\n }),\n [\n key,\n label,\n description,\n category,\n tags.join(\",\"),\n icon,\n shortcut,\n when,\n handle,\n timeout,\n priority,\n owner,\n searchKeywords.join(\",\"),\n ],\n )\n\n // Register command on mount, remove on unmount\n useEffect(() => {\n try {\n const registeredCommand = commander.add(command)\n\n // Return cleanup function\n return () => {\n commander.remove(registeredCommand)\n }\n } catch (error) {\n console.error(`Failed to register custom command \"${key}\":`, error)\n }\n }, [commander, command, key])\n\n // Create invoker object\n const invoker = useMemo<CustomCommandInvoker<TInput, TOutput>>(() => {\n const commandKey = key as CommandKey\n\n return {\n key: commandKey,\n\n exists: () => commander.has(commandKey),\n\n isAvailable: async () => {\n const cmd = commander.getCommand(commandKey)\n return cmd ? commander.isCommandAvailable(cmd) : false\n },\n\n invoke: async (\n input?: TInput,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ) => {\n return commander.invoke<TOutput>(commandKey, input, source)\n },\n\n attempt: async (\n input?: TInput,\n source: \"palette\" | \"shortcut\" | \"api\" = \"api\",\n ) => {\n return commander.attempt<TOutput>(commandKey, input, source)\n },\n\n getCommand: () =>\n commander.getCommand(commandKey) as\n | Command<TInput, TOutput>\n | undefined,\n }\n }, [commander, key])\n\n return invoker\n}\n\n// ============================================================================\n// SPECIALIZED HOOKS\n// ============================================================================\n\n/**\n * Hook for creating a simple action command (no input/output)\n */\nexport function useAction(\n key: string,\n label: string,\