UNPKG

@darksnow-ui/commander

Version:

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

1,759 lines (1,751 loc) 49.1 kB
// src/errors.ts var CommandError = class extends Error { constructor(message, command2) { super(message); this.command = command2; this.name = "CommandError"; } }; var CommandNotFoundError = class extends CommandError { constructor(command2) { super(`Command not found: ${command2}`, command2); this.name = "CommandNotFoundError"; } }; var CommandUnavailableError = class extends CommandError { constructor(command2) { super(`Command not available: ${command2}`, command2); this.name = "CommandUnavailableError"; } }; var CommandTimeoutError = class extends CommandError { constructor(command2, timeout) { super(`Command timeout: ${command2} (${timeout}ms)`, command2); this.name = "CommandTimeoutError"; } }; var CommandExecutionError = class extends CommandError { constructor(command2, originalError) { super( `Command execution failed: ${command2} - ${originalError.message}`, command2 ); this.name = "CommandExecutionError"; this.cause = originalError; } }; function createCommandError(type, command2, details) { switch (type) { case "not-found": return new CommandNotFoundError(command2); case "unavailable": return new CommandUnavailableError(command2); case "timeout": return new CommandTimeoutError(command2, (details == null ? void 0 : details.timeout) || 0); case "execution": return new CommandExecutionError( command2, (details == null ? void 0 : details.error) || new Error("Unknown error") ); default: return new CommandError(`Unknown error type: ${type}`, command2); } } // src/utils.ts var counter = 0; function generateId() { return `cmd_${Date.now()}_${++counter}`; } function normalizeSearchTerm(term) { return term.toLowerCase().trim(); } function calculateSearchScore(command2, queryTerms) { let score = 0; const searchableText = [ command2.label, command2.description || "", ...command2.tags || [], ...command2.searchKeywords || [] ].join(" ").toLowerCase(); queryTerms.forEach((term) => { var _a, _b, _c; if (command2.label.toLowerCase().includes(term)) { score += command2.label.toLowerCase() === term ? 100 : 50; } if ((_a = command2.description) == null ? void 0 : _a.toLowerCase().includes(term)) { score += 25; } if ((_b = command2.tags) == null ? void 0 : _b.some((tag) => tag.toLowerCase().includes(term))) { score += 15; } if ((_c = command2.searchKeywords) == null ? void 0 : _c.some( (keyword) => keyword.toLowerCase().includes(term) )) { score += 10; } if (searchableText.includes(term)) { score += 5; } }); return score; } function getMatchedTerms(command2, queryTerms) { const matched = []; const searchableText = [ command2.label, command2.description || "", ...command2.tags || [], ...command2.searchKeywords || [] ].join(" ").toLowerCase(); queryTerms.forEach((term) => { if (searchableText.includes(term)) { matched.push(term); } }); return matched; } function isValidCommandKey(key) { return typeof key === "string" && key.length > 0; } function isValidCommand(command2) { return command2 && typeof command2 === "object" && isValidCommandKey(command2.key) && typeof command2.label === "string" && command2.label.length > 0 && typeof command2.handle === "function"; } function withTimeout(promise, timeoutMs, errorMessage = "Operation timed out") { return Promise.race([ promise, new Promise((_, reject) => { setTimeout(() => { reject(new Error(errorMessage)); }, timeoutMs); }) ]); } async function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } function debounce(func, wait, immediate = false) { let timeout; return function executedFunction(...args) { const later = () => { timeout = void 0; if (!immediate) func(...args); }; const callNow = immediate && !timeout; clearTimeout(timeout); timeout = setTimeout(later, wait); if (callNow) func(...args); }; } function unique(array) { return [...new Set(array)]; } function removeItem(array, item) { const index = array.indexOf(item); if (index > -1) { array.splice(index, 1); } return array; } function deepClone(obj) { if (obj === null || typeof obj !== "object") { return obj; } if (obj instanceof Date) { return new Date(obj.getTime()); } if (obj instanceof Array) { return obj.map((item) => deepClone(item)); } if (typeof obj === "object") { const cloned = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { cloned[key] = deepClone(obj[key]); } } return cloned; } return obj; } var PriorityQueue = class { constructor() { this.items = []; } enqueue(item, priority = 0) { const queueItem = { item, priority }; let added = false; for (let i = 0; i < this.items.length; i++) { if (queueItem.priority > this.items[i].priority) { this.items.splice(i, 0, queueItem); added = true; break; } } if (!added) { this.items.push(queueItem); } } dequeue() { var _a; return (_a = this.items.shift()) == null ? void 0 : _a.item; } peek() { var _a; return (_a = this.items[0]) == null ? void 0 : _a.item; } isEmpty() { return this.items.length === 0; } size() { return this.items.length; } clear() { this.items = []; } remove(predicate) { const index = this.items.findIndex((queueItem) => predicate(queueItem.item)); if (index >= 0) { this.items.splice(index, 1); return true; } return false; } }; // src/commander.ts var CommandHandler = class { constructor(key, commander) { this.key = key; this.commander = commander; } exists() { return this.commander.has(this.key); } async isAvailable() { const command2 = this.commander.getCommand(this.key); return command2 ? this.commander.isCommandAvailable(command2) : false; } async invoke(input, source = "api") { return this.commander.invoke(this.key, input, source); } async attempt(input, source = "api") { return this.commander.attempt(this.key, input, source); } getCommand() { return this.commander.getCommand(this.key); } }; var Commander = class { constructor(commands = []) { this._commands = /* @__PURE__ */ new Map(); this.listeners = /* @__PURE__ */ new Map(); this.executionHistory = []; this.recentCommands = []; this.maxHistorySize = 100; this.maxRecentSize = 10; commands.forEach((cmd) => this.add(cmd)); } // ============================================================================ // EVENT SYSTEM // ============================================================================ listen(event, callback, options = {}) { const listener = { id: generateId(), event, callback, once: options.once }; if (!this.listeners.has(event)) { this.listeners.set(event, []); } this.listeners.get(event).push(listener); return listener; } removeListener(listener) { for (const [event, listeners] of this.listeners) { const index = listeners.findIndex((l) => l.id === listener.id); if (index >= 0) { listeners.splice(index, 1); if (listeners.length === 0) { this.listeners.delete(event); } break; } } } async emit(event, ...args) { const listeners = this.listeners.get(event) || []; const toRemove = []; await Promise.allSettled( listeners.map(async (listener) => { try { await listener.callback(...args); if (listener.once) { toRemove.push(listener); } } catch (error) { console.error(`Event listener error for ${event}:`, error); } }) ); toRemove.forEach((listener) => this.removeListener(listener)); } // ============================================================================ // COMMAND MANAGEMENT // ============================================================================ commands() { return Array.from(this._commands.values()); } getCommand(key) { return this._commands.get(key); } add(command2) { if (!isValidCommand(command2)) { throw new Error(`Invalid command: ${JSON.stringify(command2)}`); } this._commands.set(command2.key, command2); this.emit("command:added", command2); return command2; } remove(commandOrKey) { const key = typeof commandOrKey === "string" ? commandOrKey : typeof commandOrKey === "object" ? commandOrKey.key : commandOrKey; const command2 = this._commands.get(key); if (command2) { this._commands.delete(key); this.emit("command:removed", command2); return true; } return false; } removeByOwner(owner) { let removed = 0; for (const [key, command2] of this._commands) { if (command2.owner === owner) { this._commands.delete(key); this.emit("command:removed", command2); removed++; } } return removed; } removeByCategory(category) { let removed = 0; for (const [key, command2] of this._commands) { if (command2.category === category) { this._commands.delete(key); this.emit("command:removed", command2); removed++; } } return removed; } has(key) { return this._commands.has(key); } // ============================================================================ // SEARCH & FILTERING // ============================================================================ async search(query, options = {}) { var _a; const normalizedQuery = normalizeSearchTerm(query); if (!normalizedQuery) { return this.getAllAvailable(options); } const results = []; const queryTerms = normalizedQuery.split(/\s+/); for (const command2 of this._commands.values()) { if (options.category && command2.category !== options.category) continue; if (options.owner && command2.owner !== options.owner) continue; if (((_a = options.tags) == null ? void 0 : _a.length) && !options.tags.some((tag) => { var _a2; return (_a2 = command2.tags) == null ? void 0 : _a2.includes(tag); })) continue; if (!options.includeUnavailable && !await this.isCommandAvailable(command2)) { continue; } const score = calculateSearchScore(command2, queryTerms); if (score > 0) { results.push({ command: command2, score, matchedTerms: getMatchedTerms(command2, queryTerms) }); } } results.sort((a, b) => { if (a.score !== b.score) return b.score - a.score; return (b.command.priority || 0) - (a.command.priority || 0); }); return options.limit ? results.slice(0, options.limit) : results; } async getAllAvailable(options) { var _a; const results = []; for (const command2 of this._commands.values()) { if (options.category && command2.category !== options.category) continue; if (options.owner && command2.owner !== options.owner) continue; if (((_a = options.tags) == null ? void 0 : _a.length) && !options.tags.some((tag) => { var _a2; return (_a2 = command2.tags) == null ? void 0 : _a2.includes(tag); })) continue; if (!options.includeUnavailable && !await this.isCommandAvailable(command2)) { continue; } results.push({ command: command2, score: command2.priority || 0, matchedTerms: [] }); } results.sort((a, b) => { const aRecentIndex = this.recentCommands.indexOf(a.command.key); const bRecentIndex = this.recentCommands.indexOf(b.command.key); if (aRecentIndex >= 0 && bRecentIndex >= 0) { return aRecentIndex - bRecentIndex; } if (aRecentIndex >= 0) return -1; if (bRecentIndex >= 0) return 1; return (b.command.priority || 0) - (a.command.priority || 0); }); return options.limit ? results.slice(0, options.limit) : results; } // ============================================================================ // COMMAND AVAILABILITY // ============================================================================ async isCommandAvailable(command2) { if (!command2.when) return true; try { return await command2.when(); } catch (error) { console.warn( `Command availability check failed for ${command2.key}:`, error ); return false; } } async getAvailableCommands(options = {}) { const available = []; for (const command2 of this._commands.values()) { if (options.category && command2.category !== options.category) continue; if (options.owner && command2.owner !== options.owner) continue; if (await this.isCommandAvailable(command2)) { available.push(command2); } } return available; } // ============================================================================ // EXECUTION // ============================================================================ async invoke(key, input, source = "api") { const command2 = this._commands.get(key); if (!command2) { const error = new CommandNotFoundError(key); this.emit("command:error", key, error); throw error; } if (!await this.isCommandAvailable(command2)) { const error = new CommandUnavailableError(key); this.emit("command:error", key, error); throw error; } const context = { command: command2, input, startTime: /* @__PURE__ */ new Date(), source }; this.emit("command:executing", context); try { const result = await this.executeWithTimeout(command2, input); this.trackExecution(context, result); this.addToRecent(key); this.emit("command:completed", context, result); return result; } catch (error) { const executionError = error instanceof Error ? new CommandExecutionError(key, error) : new CommandExecutionError(key, new Error(String(error))); this.emit("command:failed", context, executionError); throw executionError; } } async executeWithTimeout(command2, input) { const timeoutMs = command2.timeout || 3e4; try { return await withTimeout( command2.handle.call(this, input), timeoutMs, `Command timeout: ${command2.key} (${timeoutMs}ms)` ); } catch (error) { if (error instanceof Error && error.message.includes("timeout")) { throw new CommandTimeoutError(command2.key, timeoutMs); } throw error; } } async attempt(key, input, source = "api") { try { const result = await this.invoke(key, input, source); return { success: true, result, command: key }; } catch (error) { return { success: false, error, command: key }; } } // ============================================================================ // EXECUTION TRACKING // ============================================================================ trackExecution(context, result) { this.executionHistory.push({ ...context, // Don't store large results in history input: typeof context.input === "object" ? "<object>" : context.input }); if (this.executionHistory.length > this.maxHistorySize) { this.executionHistory.splice( 0, this.executionHistory.length - this.maxHistorySize ); } } addToRecent(key) { const existingIndex = this.recentCommands.indexOf(key); if (existingIndex >= 0) { this.recentCommands.splice(existingIndex, 1); } this.recentCommands.unshift(key); if (this.recentCommands.length > this.maxRecentSize) { this.recentCommands.splice(this.maxRecentSize); } } getExecutionHistory(limit) { return limit ? this.executionHistory.slice(-limit) : [...this.executionHistory]; } getRecentCommands() { return this.recentCommands.map((key) => this._commands.get(key)).filter((cmd) => cmd !== void 0); } clearHistory() { this.executionHistory = []; this.recentCommands = []; this.emit("history:cleared"); } // ============================================================================ // CATEGORIES & ORGANIZATION // ============================================================================ getCommandsByCategory() { const categories = /* @__PURE__ */ new Map(); for (const command2 of this._commands.values()) { const category = command2.category || "custom"; if (!categories.has(category)) { categories.set(category, []); } categories.get(category).push(command2); } return categories; } getCategories() { const categories = /* @__PURE__ */ new Set(); for (const command2 of this._commands.values()) { categories.add(command2.category || "custom"); } return Array.from(categories); } // ============================================================================ // UTILITY METHODS // ============================================================================ createInvoker(key) { return new CommandHandler(key, this); } // Alias for backward compatibility invoker(key) { return this.createInvoker(key); } getStats() { return { totalCommands: this._commands.size, categories: this.getCategories().length, executionHistory: this.executionHistory.length, recentCommands: this.recentCommands.length, listeners: Array.from(this.listeners.values()).flat().length }; } // ============================================================================ // CLEANUP // ============================================================================ destroy() { this._commands.clear(); this.listeners.clear(); this.executionHistory = []; this.recentCommands = []; } }; // src/builder.ts var CommandBuilder = class _CommandBuilder { constructor() { this.command = {}; } /** * Create a new CommandBuilder instance */ static create(key) { return new _CommandBuilder().key(key); } /** * Set the command key (required) */ key(key) { if (!isValidCommandKey(key)) { throw new Error("Command key must be a non-empty string"); } this.command.key = key; return this; } /** * Set the command label (required) */ label(label) { if (!label || typeof label !== "string") { throw new Error("Command label must be a non-empty string"); } this.command.label = label; return this; } /** * Set the command description */ description(description) { this.command.description = description; return this; } /** * Set the command category */ category(category) { this.command.category = category; return this; } /** * Set the command owner */ owner(owner) { this.command.owner = owner; return this; } /** * Add tags to the command */ tags(...tags) { this.command.tags = [...this.command.tags || [], ...tags]; return this; } /** * Set the command icon */ icon(icon) { this.command.icon = icon; return this; } /** * Set the keyboard shortcut */ shortcut(shortcut) { this.command.shortcut = shortcut; return this; } /** * Set the availability condition */ when(condition) { this.command.when = condition; return this; } /** * Add search keywords */ searchKeywords(...keywords) { this.command.searchKeywords = [ ...this.command.searchKeywords || [], ...keywords ]; return this; } /** * Set the command priority */ priority(priority) { this.command.priority = priority; return this; } /** * Set the command timeout */ timeout(ms) { if (ms <= 0) { throw new Error("Timeout must be a positive number"); } this.command.timeout = ms; return this; } /** * Set the command handler (required) */ handle(handler) { if (typeof handler !== "function") { throw new Error("Command handler must be a function"); } this.command.handle = handler; return this; } /** * Build and return the command */ build() { if (!this.command.key) { throw new Error("Command key is required"); } if (!this.command.label) { throw new Error("Command label is required"); } if (!this.command.handle) { throw new Error("Command handle is required"); } return this.command; } /** * Clone this builder to create a new one with the same configuration */ clone() { const cloned = new _CommandBuilder(); cloned.command = { ...this.command }; return cloned; } /** * Reset the builder to start over */ reset() { this.command = {}; return this; } /** * Get the current command state (for debugging) */ getState() { return { ...this.command }; } }; var CommandTemplate = class { /** * Create a system command template */ static system() { return CommandBuilder.create("system:placeholder").category("system").owner("system").priority(10); } /** * Create a file command template */ static file() { return CommandBuilder.create("file:placeholder").category("file").tags("file").priority(5); } /** * Create a debug command template */ static debug() { return CommandBuilder.create("debug:placeholder").category("debug").owner("debug").tags("debug", "development").when(() => process.env.NODE_ENV === "development").priority(15); } /** * Create a view command template */ static view() { return CommandBuilder.create("view:placeholder").category("view").tags("view", "ui").priority(3); } /** * Create a tools command template */ static tools() { return CommandBuilder.create("tools:placeholder").category("tools").tags("tools", "utility").priority(8); } /** * Create a custom command template */ static custom() { return CommandBuilder.create("custom:placeholder").category("custom").owner("temporary").priority(1); } }; function command(key) { return CommandBuilder.create(key); } function simpleCommand(key, label, handler) { return CommandBuilder.create(key).label(label).handle(handler).build(); } // src/hooks/context.tsx import { createContext, useContext, useEffect, useMemo } from "react"; import { jsx } from "react/jsx-runtime"; var CommanderContext = createContext(null); function CommanderProvider({ children, commander, onReady, enableDevTools = process.env.NODE_ENV === "development" }) { useEffect(() => { if (enableDevTools && typeof window !== "undefined") { ; window.__commander = commander; commander.add({ key: "dev:inspect-commands", label: "Inspect All Commands", description: "Log all registered commands to console", category: "debug", icon: "\u{1F50D}", owner: "dev-tools", tags: ["debug", "inspect"], priority: 100, handle: async () => { console.group("\u{1F4CB} Registered Commands"); commander.commands().forEach((cmd) => { console.log(`${cmd.icon || "\u{1F4DD}"} ${cmd.label}`, { key: cmd.key, category: cmd.category, owner: cmd.owner, shortcut: cmd.shortcut, tags: cmd.tags }); }); console.groupEnd(); return { commands: commander.commands(), total: commander.commands().length }; } }); commander.add({ key: "dev:clear-history", label: "Clear Command History", description: "Clear execution history and recent commands", category: "debug", icon: "\u{1F9F9}", owner: "dev-tools", tags: ["debug", "clear"], priority: 90, handle: async () => { commander.clearHistory(); console.log("Command history cleared"); return { cleared: true }; } }); commander.add({ key: "dev:stats", label: "Show Commander Stats", description: "Display commander statistics", category: "debug", icon: "\u{1F4CA}", owner: "dev-tools", tags: ["debug", "stats"], priority: 80, handle: async () => { const stats = commander.getStats(); console.log("Commander Stats:", stats); return stats; } }); commander.add({ key: "dev:search-test", label: "Test Search System", description: "Test search functionality with sample queries", category: "debug", icon: "\u{1F50E}", owner: "dev-tools", tags: ["debug", "search"], priority: 70, handle: async () => { const queries = ["save", "debug", "file", "system"]; const results = {}; for (const query of queries) { const searchResults = await commander.search(query, { limit: 3 }); results[query] = searchResults.map((r) => ({ label: r.command.label, score: r.score, matched: r.matchedTerms })); } console.log("Search Test Results:", results); return results; } }); } onReady == null ? void 0 : onReady(commander); return () => { if (enableDevTools && typeof window !== "undefined") { delete window.__commander; commander.removeByOwner("dev-tools"); } }; }, [commander, enableDevTools, onReady]); const value = useMemo( () => ({ // Core instance commander, // Command management commands: () => commander.commands(), add: (command2) => commander.add(command2), remove: (commandOrKey) => commander.remove(commandOrKey), removeByOwner: (owner) => commander.removeByOwner(owner), removeByCategory: (category) => commander.removeByCategory(category), has: (key) => commander.has(key), getCommand: (key) => commander.getCommand(key), // Execution invoke: (key, input, source = "api") => commander.invoke(key, input, source), attempt: (key, input, source = "api") => commander.attempt(key, input, source), // Search & filtering search: (query, options) => commander.search(query, options), isCommandAvailable: (command2) => commander.isCommandAvailable(command2), getAvailableCommands: (options) => commander.getAvailableCommands(options), // Organization getCommandsByCategory: () => commander.getCommandsByCategory(), getCategories: () => commander.getCategories(), // History & tracking getExecutionHistory: (limit) => commander.getExecutionHistory(limit), getRecentCommands: () => commander.getRecentCommands(), clearHistory: () => commander.clearHistory(), // Events listen: (event, callback, options) => commander.listen(event, callback, options), removeListener: (listener) => commander.removeListener(listener), // Utilities createInvoker: (key) => commander.createInvoker(key), getStats: () => commander.getStats(), // State isReady: true }), [commander] ); return /* @__PURE__ */ jsx(CommanderContext.Provider, { value, children }); } function useCommander() { const context = useContext(CommanderContext); if (!context) { throw new Error( "useCommander must be used within a CommanderProvider. Wrap your app with <CommanderProvider> to use commander hooks." ); } if (!context.isReady) { throw new Error("Commander is not ready yet."); } return context; } function useCommanderInstance() { const { commander } = useCommander(); return commander; } function useInvoke() { const { invoke } = useCommander(); return invoke; } function useAttempt() { const { attempt } = useCommander(); return attempt; } function useSearch() { const { search } = useCommander(); return search; } function useCommands() { const { commands } = useCommander(); return commands(); } function useCommandsByCategory() { const { getCommandsByCategory } = useCommander(); return getCommandsByCategory(); } function useRecentCommands() { const { getRecentCommands } = useCommander(); return getRecentCommands(); } function useCommanderStats() { const { getStats } = useCommander(); return getStats(); } // src/hooks/useCustomCommand.ts import { useEffect as useEffect2, useMemo as useMemo2, useCallback as useCallback2, useState as useState2 } from "react"; function useCustomCommand(props) { const commander = useCommander(); const { key, label, description, category = "custom", tags = [], icon, shortcut, when, handle, timeout, priority, owner = "temporary", searchKeywords = [] } = props; const command2 = useMemo2( () => ({ key, label: label || key, description, category, tags, icon, shortcut, when, handle, timeout, priority, owner, searchKeywords }), [ key, label, description, category, tags.join(","), icon, shortcut, when, handle, timeout, priority, owner, searchKeywords.join(",") ] ); useEffect2(() => { try { const registeredCommand = commander.add(command2); return () => { commander.remove(registeredCommand); }; } catch (error) { console.error(`Failed to register custom command "${key}":`, error); } }, [commander, command2, key]); const invoker = useMemo2(() => { const commandKey = key; return { key: commandKey, exists: () => commander.has(commandKey), isAvailable: async () => { const cmd = commander.getCommand(commandKey); return cmd ? commander.isCommandAvailable(cmd) : false; }, invoke: async (input, source = "api") => { return commander.invoke(commandKey, input, source); }, attempt: async (input, source = "api") => { return commander.attempt(commandKey, input, source); }, getCommand: () => commander.getCommand(commandKey) }; }, [commander, key]); return invoker; } function useAction(key, label, action, options = {}) { return useCustomCommand({ key, label, handle: async () => { await action(); }, ...options }); } function useToggleCommand(key, label, isActive, onToggle, options = {}) { return useCustomCommand({ key, label: `${isActive ? "Disable" : "Enable"} ${label}`, icon: isActive ? "\u{1F534}" : "\u{1F7E2}", handle: async () => { const newState = !isActive; await onToggle(newState); return newState; }, ...options }); } function useModalCommand(key, label, openModal, options = {}) { return useCustomCommand({ key, label, handle: openModal, category: "tools", ...options }); } function useNavigationCommand(key, label, path, navigate, options = {}) { return useCustomCommand({ key, label, category: "view", icon: "\u{1F517}", handle: async () => { navigate(path); }, ...options }); } function useContextualCommands(data, commandFactory) { const commander = useCommander(); const invokers = []; useEffect2(() => { const registeredCommands = []; data.forEach((item, index) => { try { const commandProps = commandFactory(item, index); const command2 = { key: commandProps.key, label: commandProps.label || commandProps.key, description: commandProps.description, category: commandProps.category || "custom", tags: commandProps.tags || [], icon: commandProps.icon, shortcut: commandProps.shortcut, when: commandProps.when, handle: commandProps.handle, timeout: commandProps.timeout, priority: commandProps.priority, owner: commandProps.owner || "contextual", searchKeywords: commandProps.searchKeywords }; const registeredCommand = commander.add(command2); registeredCommands.push(registeredCommand); } catch (error) { console.error( `Failed to register contextual command for item ${index}:`, error ); } }); return () => { registeredCommands.forEach((command2) => { commander.remove(command2); }); }; }, [data, commandFactory, commander]); return { count: data.length, refresh: useCallback2(() => { }, []) }; } function useCommandStatus(key) { const commander = useCommander(); const [isExecuting, setIsExecuting] = useState2(false); const [lastResult, setLastResult] = useState2(void 0); const [lastError, setLastError] = useState2(void 0); const exists = commander.has(key); const checkAvailability = useCallback2(async () => { const command2 = commander.getCommand(key); return command2 ? commander.isCommandAvailable(command2) : false; }, [commander, key]); useEffect2(() => { const executingListener = commander.listen( "command:executing", (context) => { if (context.command.key === key) { setIsExecuting(true); setLastError(void 0); } } ); const completedListener = commander.listen( "command:completed", (context, result) => { if (context.command.key === key) { setIsExecuting(false); setLastResult(result); } } ); const failedListener = commander.listen( "command:failed", (context, error) => { if (context.command.key === key) { setIsExecuting(false); setLastError(error); } } ); return () => { commander.removeListener(executingListener); commander.removeListener(completedListener); commander.removeListener(failedListener); }; }, [commander, key]); return { exists, checkAvailability, isExecuting, lastResult, lastError }; } function useCommandEvents(key, handlers) { const commander = useCommander(); useEffect2(() => { const listeners = []; if (handlers.onExecuting) { const listener = commander.listen("command:executing", (context) => { if (context.command.key === key) { handlers.onExecuting(context); } }); listeners.push(listener); } if (handlers.onExecuted) { const listener = commander.listen( "command:completed", (context, result) => { if (context.command.key === key) { handlers.onExecuted({ result }); } } ); listeners.push(listener); } if (handlers.onError) { const listener = commander.listen("command:failed", (context, error) => { if (context.command.key === key) { handlers.onError(error); } }); listeners.push(listener); } return () => { listeners.forEach((listener) => { commander.removeListener(listener); }); }; }, [ commander, key, handlers.onExecuting, handlers.onExecuted, handlers.onError ]); } // src/hooks/useCommand.ts import { useCallback as useCallback3, useMemo as useMemo3, useState as useState3, useEffect as useEffect3, useRef } from "react"; function createRetryDelay(delay2) { return typeof delay2 === "function" ? delay2 : () => delay2; } function debouncePromise(fn, delay2) { let timeoutId = null; let resolvePromise = null; let rejectPromise = null; return async (...args) => { return new Promise((resolve, reject) => { if (timeoutId) { clearTimeout(timeoutId); if (rejectPromise) { rejectPromise(new Error("Debounced")); } } resolvePromise = resolve; rejectPromise = reject; timeoutId = setTimeout(async () => { try { const result = await fn(...args); if (resolvePromise) resolvePromise(result); } catch (error) { if (rejectPromise) rejectPromise(error); } }, delay2); }); }; } function throttlePromise(fn, delay2) { let lastExecution = 0; let pending = null; return async (...args) => { const now = Date.now(); if (pending) { return pending; } if (now - lastExecution < delay2) { return new Promise((resolve, reject) => { setTimeout( async () => { try { const result2 = await fn(...args); resolve(result2); } catch (error) { reject(error); } pending = null; }, delay2 - (now - lastExecution) ); }); } lastExecution = now; pending = fn(...args); const result = await pending; pending = null; return result; }; } function useCommand(key, options = {}) { const commander = useCommander(); const [isLoading, setIsLoading] = useState3(false); const [lastResult, setLastResult] = useState3(null); const [lastError, setLastError] = useState3(null); const [lastInput, setLastInput] = useState3(null); const [lastExecution, setLastExecution] = useState3(null); const [executionCount, setExecutionCount] = useState3(0); const [isAvailable, setIsAvailable] = useState3(false); const optionsRef = useRef(options); optionsRef.current = options; const { source = "api", throwOnError = true, timeout, retry = 0, retryDelay = 1e3, debounce: debounce2, throttle, defaultInput, validateInput, transformInput, transformOutput, onSuccess, onError, onFinally, resetOnKeyChange = true } = options; const command2 = commander.getCommand(key); const exists = !!command2; useEffect3(() => { if (resetOnKeyChange) { setLastResult(null); setLastError(null); setLastInput(null); setLastExecution(null); setExecutionCount(0); } }, [key, resetOnKeyChange]); useEffect3(() => { let mounted = true; const checkAvailability = async () => { if (command2) { const available = await commander.isCommandAvailable(command2); if (mounted) { setIsAvailable(available); } } else { if (mounted) { setIsAvailable(false); } } }; checkAvailability(); const listener = commander.listen("command:added", checkAvailability); const listener2 = commander.listen("command:removed", checkAvailability); return () => { mounted = false; commander.removeListener(listener); commander.removeListener(listener2); }; }, [commander, command2, key]); const inputValidator = useCallback3( (input) => { if (validateInput) { return validateInput(input); } return true; }, [validateInput] ); const canInvoke = useCallback3( async (input) => { if (!exists || !isAvailable) return false; const validation = inputValidator(input); if (validation !== true) return false; return true; }, [exists, isAvailable, inputValidator] ); const executeWithRetry = useCallback3( async (input, overrideOptions = {}) => { const finalOptions = { ...optionsRef.current, ...overrideOptions }; const retryDelayFn = createRetryDelay( finalOptions.retryDelay || retryDelay ); let lastError2; let currentInput = input; if (finalOptions.transformInput || transformInput) { const transformer = finalOptions.transformInput || transformInput; currentInput = transformer(currentInput); } if (defaultInput && currentInput) { currentInput = { ...defaultInput, ...currentInput }; } else if (defaultInput && !currentInput) { currentInput = defaultInput; } const validation = inputValidator(currentInput); if (validation !== true) { throw new Error( typeof validation === "string" ? validation : "Invalid input" ); } for (let attempt2 = 0; attempt2 <= (finalOptions.retry || retry); attempt2++) { try { const result = await commander.invoke( key, currentInput, finalOptions.source || source ); let finalResult = result; if (finalOptions.transformOutput || transformOutput) { const transformer = finalOptions.transformOutput || transformOutput; finalResult = transformer(result); } return finalResult; } catch (error) { lastError2 = error instanceof Error ? error : new Error(String(error)); if (attempt2 < (finalOptions.retry || retry)) { await new Promise( (resolve) => setTimeout(resolve, retryDelayFn(attempt2 + 1)) ); } } } throw lastError2; }, [ commander, key, source, retry, retryDelay, defaultInput, transformInput, transformOutput, inputValidator ] ); const processedExecute = useMemo3(() => { let fn = executeWithRetry; if (debounce2 && debounce2 > 0) { fn = debouncePromise(fn, debounce2); } else if (throttle && throttle > 0) { fn = throttlePromise(fn, throttle); } return fn; }, [executeWithRetry, debounce2, throttle]); const invoke = useCallback3( async (input, overrideOptions = {}) => { const finalOptions = { ...optionsRef.current, ...overrideOptions }; if (!await canInvoke(input)) { const error = new Error( `Command "${key}" is not available or input is invalid` ); if (finalOptions.throwOnError !== false) { throw error; } return null; } setIsLoading(true); setLastError(null); setLastInput(input || null); try { const result = await processedExecute(input, overrideOptions); setLastResult(result); setLastExecution(/* @__PURE__ */ new Date()); setExecutionCount((prev) => prev + 1); const successCallback = finalOptions.onSuccess || onSuccess; if (successCallback) { successCallback(result, input); } return result; } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); setLastError(err); const errorCallback = finalOptions.onError || onError; if (errorCallback) { errorCallback(err, input); } if (finalOptions.throwOnError !== false) { throw err; } return null; } finally { setIsLoading(false); const finallyCallback = finalOptions.onFinally || onFinally; if (finallyCallback) { finallyCallback(input); } } }, [key, canInvoke, processedExecute, onSuccess, onError, onFinally] ); const attempt = useCallback3( async (input, overrideOptions = {}) => { try { const result = await invoke(input, { ...overrideOptions, throwOnError: false }); return { success: true, result, command: key }; } catch (error) { return { success: false, error: error instanceof Error ? error : new Error(String(error)), command: key }; } }, [invoke, key] ); const execute = useCallback3( async (input, callbacks = {}) => { try { const result = await invoke(input, { onSuccess: callbacks.onSuccess, onError: callbacks.onError, onFinally: callbacks.onFinally, throwOnError: false }); return result; } catch (error) { return null; } }, [invoke] ); const reset = useCallback3(() => { setLastResult(null); setLastError(null); setLastInput(null); setLastExecution(null); setExecutionCount(0); }, []); const clearError = useCallback3(() => { setLastError(null); }, []); const refresh = useCallback3(() => { if (command2) { commander.isCommandAvailable(command2).then(setIsAvailable); } }, [commander, command2]); const invoker = useMemo3( () => ({ // Estado exists, isAvailable, isLoading, lastResult, lastError, lastInput, lastExecution, executionCount, command: command2, // Métodos invoke, attempt, execute, canInvoke, validateInput: inputValidator, reset, clearError, refresh }), [ exists, isAvailable, isLoading, lastResult, lastError, lastInput, lastExecution, executionCount, command2, invoke, attempt, execute, canInvoke, inputValidator, reset, clearError, refresh ] ); return invoker; } // src/hooks/useInvoker.ts function useInvoker(key, options = {}) { const { source = "api", throwOnError = true, defaultInput, onSuccess, onError } = options; const commandOptions = { source, throwOnError, defaultInput, onSuccess, onError }; const commandInvoker = useCommand(key, commandOptions); return commandInvoker.invoke; } function useAction2(key, options = {}) { const invoke = useInvoker(key, options); return () => invoke(); } function useCommandState(key, options = {}) { const commandOptions = { source: options.source, throwOnError: options.throwOnError, defaultInput: options.defaultInput, onSuccess: options.onSuccess, onError: options.onError }; return useCommand(key, commandOptions); } function useSafeInvoker(key, options = {}) { const invoker = useCommand(key, { ...options, throwOnError: false }); return (input) => invoker.attempt(input); } function useBoundInvoker(key, boundInput, options = {}) { const invoke = useInvoker(key, { ...options, defaultInput: boundInput }); return (input) => { const mergedInput = { ...boundInput, ...input }; return invoke(mergedInput); }; } function useToggleInvoker(key, options = {}) { const invoke = useInvoker(key, options); return (state) => invoke(state); } function useBatchInvoker(keys, options = {}) { const invokers = keys.map( (key) => useCommand(key, { source: options.source, throwOnError: false }) ); return async (inputs = [], batchOptions = {}) => { var _a, _b; const results = []; const stopOnError = (_b = (_a = batchOptions.stopOnError) != null ? _a : options.stopOnError) != null ? _b : false; for (let i = 0; i < keys.length; i++) { const input = inputs[i]; const result = await invokers[i].attempt(input); results.push(result); if (options.onProgress) { options.onProgress(i + 1, keys.length); } if (stopOnError && !result.success) { break; } } return results; }; } function useParallelInvoker(keys, options = {}) { const invokers = keys.map( (key) => useCommand(key, { source: options.source }) ); return async (inputs = []) => { const promises = keys.map((_, i) => { const input = inputs[i]; return invokers[i].invoke(input); }); return Promise.allSettled(promises); }; } // src/index.ts var VERSION = "1.0.0"; export { CommandBuilder, CommandError, CommandExecutionError, CommandNotFoundError, CommandTemplate, CommandTimeoutError, CommandUnavailableError, Commander, CommanderProvider, PriorityQueue, VERSION, calculateSearchScore, command, createCommandError, debounce, deepClone, delay, generateId, getMatchedTerms, isValidCommand, isValidCommandKey, normalizeSearchTerm, removeItem, simpleCommand, unique, useAction, useAction2 as useActionInvoker, useAttempt, useBatchInvoker, useBoundInvoker, useCommand, useCommandEvents, useCommandState, useCommandStatus, useCommander, useCommanderInstance, useCommanderStats, useCommands, useCommandsByCategory, useContextualCommands, useCustomCommand, useInvoke, useInvoker, useModalCommand, useNavigationCommand, useParallelInvoker, useRecentCommands, useSafeInvoker, useSearch, useToggleCommand, useToggleInvoker, withTimeout }; //# sourceMappingURL=index.js.map