@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
1,774 lines (1,767 loc) • 52.2 kB
JavaScript
// src/errors.ts
var CommandError = class extends Error {
constructor(message, command2) {
super(message);
this.command = command2;
this.name = "CommandError";
}
};
var InputValidationError = class extends CommandError {
constructor(command2, errors, input) {
const fieldList = errors.map((e) => e.path).join(", ");
super(`Input validation failed for ${command2}: [${fieldList}]`, command2);
this.errors = errors;
this.input = input;
this.name = "InputValidationError";
}
/**
* Get errors for a specific field path
*/
getFieldErrors(path) {
return this.errors.filter((e) => e.path === path);
}
/**
* Get all required field errors
*/
getRequiredErrors() {
return this.errors.filter((e) => e.code === "required");
}
/**
* Get missing required field paths
*/
getMissingFields() {
return this.getRequiredErrors().map((e) => e.path);
}
/**
* Check if a specific field has errors
*/
hasFieldError(path) {
return this.errors.some((e) => e.path === path);
}
/**
* Convert to a simple object for serialization
*/
toJSON() {
return {
name: this.name,
message: this.message,
command: this.command,
errors: this.errors
};
}
};
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")
);
case "validation":
return new InputValidationError(
command2,
(details == null ? void 0 : details.errors) || [],
details == null ? void 0 : details.input
);
default:
return new CommandError(`Unknown error type: ${type}`, command2);
}
}
function isInputValidationError(error) {
return error instanceof InputValidationError;
}
function isCommandError(error) {
return error instanceof CommandError;
}
// 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;
}
if (command2.inputValidator) {
const validationResult = await command2.inputValidator(input);
if (validationResult !== true) {
const error = new InputValidationError(key, validationResult, input);
this.emit("command:validation-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;
}
}
/**
* Validate input against command's inputValidator without executing
* Useful for pre-validation before invoking
*
* @returns true if valid, or ValidationErrorDetail[] if invalid
*/
async validateInput(key, input) {
const command2 = this._commands.get(key);
if (!command2) {
throw new CommandNotFoundError(key);
}
if (!command2.inputValidator) {
return true;
}
return command2.inputValidator(input);
}
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 input validator function
* Works with Zod, Yup, AJV, or any custom validation
*
* @example
* // With Zod
* .inputValidator((input) => {
* const result = userSchema.safeParse(input);
* if (result.success) return true;
* return result.error.issues.map(i => ({
* path: i.path.join('.'),
* message: i.message,
* code: i.code
* }));
* })
*
* @example
* // Simple validation
* .inputValidator((input) => {
* if (!input?.email) return [{ path: 'email', message: 'Required', code: 'required' }];
* return true;
* })
*/
inputValidator(validator) {
if (typeof validator !== "function") {
throw new Error("Input validator must be a function");
}
this.command.inputValidator = validator;
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 useBatchInvok