@darksnow-ui/commander
Version:
Command pattern implementation with React hooks for building command palettes and keyboard-driven UIs
670 lines (658 loc) • 24.7 kB
text/typescript
import * as react_jsx_runtime from 'react/jsx-runtime';
import { ReactNode } from 'react';
type CommandKey = string & {
__brand: "CommandKey";
};
type CommandCategory = "system" | "file" | "edit" | "view" | "debug" | "tools" | "custom";
/**
* Validation error detail for a specific field
*/
interface ValidationErrorDetail {
/** Field path (e.g., "email", "user.name", "items[0].id") */
path: string;
/** Error message */
message: string;
/** Error code (e.g., "required", "type", "format") */
code?: string;
/** Expected value/type */
expected?: unknown;
/** Received value */
received?: unknown;
}
/**
* Result of input validation
* - true: valid
* - ValidationErrorDetail[]: invalid with errors
*/
type ValidationResult = true | ValidationErrorDetail[];
/**
* Input validator function
* Supports sync and async validation (for Zod, Yup, AJV, custom, etc.)
*/
type InputValidator<TInput = any> = (input: TInput | undefined) => ValidationResult | Promise<ValidationResult>;
interface Command<TInput = any, TOutput = any> {
key: CommandKey;
label: string;
description?: string;
category?: CommandCategory;
owner?: string;
tags?: string[];
icon?: string;
shortcut?: string;
when?: () => boolean | Promise<boolean>;
handle: (this: Commander, input?: TInput) => Promise<TOutput>;
timeout?: number;
searchKeywords?: string[];
priority?: number;
/**
* Input validator function
* Can be used with Zod, Yup, AJV, or any custom validation
*
* @example
* // With Zod
* inputValidator: (input) => {
* const result = schema.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 custom validation
* inputValidator: (input) => {
* const errors = [];
* if (!input?.email) errors.push({ path: 'email', message: 'Email required', code: 'required' });
* if (!input?.name) errors.push({ path: 'name', message: 'Name required', code: 'required' });
* return errors.length ? errors : true;
* }
*/
inputValidator?: InputValidator<TInput>;
}
interface EventListener<T = any> {
id: string;
event: string;
callback: (...args: T[]) => void | Promise<void>;
once?: boolean;
}
interface CommandExecutionContext {
command: Command;
input: any;
startTime: Date;
source: "palette" | "shortcut" | "api";
}
interface SearchResult {
command: Command;
score: number;
matchedTerms: string[];
}
interface SearchOptions {
category?: CommandCategory;
owner?: string;
tags?: string[];
limit?: number;
includeUnavailable?: boolean;
}
interface ExecutionResult<T = any> {
success: boolean;
result?: T;
error?: any;
command: CommandKey;
}
interface CommanderStats {
totalCommands: number;
categories: number;
executionHistory: number;
recentCommands: number;
listeners: number;
}
declare class CommandHandler {
private key;
private commander;
constructor(key: CommandKey, commander: Commander);
exists(): boolean;
isAvailable(): Promise<boolean>;
invoke<T = any>(input?: any, source?: "palette" | "shortcut" | "api"): Promise<T>;
attempt<T = any>(input?: any, source?: "palette" | "shortcut" | "api"): Promise<ExecutionResult<T>>;
getCommand(): Command | undefined;
}
declare class Commander {
protected _commands: Map<CommandKey, Command>;
protected listeners: Map<string, EventListener[]>;
protected executionHistory: CommandExecutionContext[];
protected recentCommands: CommandKey[];
maxHistorySize: number;
maxRecentSize: number;
constructor(commands?: Command[]);
listen<T = any>(event: string, callback: (...args: T[]) => void | Promise<void>, options?: {
once?: boolean;
}): EventListener<T>;
removeListener(listener: EventListener): void;
private emit;
commands(): Command[];
getCommand(key: CommandKey): Command | undefined;
add(command: Command): Command;
remove(commandOrKey: string | CommandKey | Command): boolean;
removeByOwner(owner: string): number;
removeByCategory(category: CommandCategory): number;
has(key: CommandKey): boolean;
search(query: string, options?: SearchOptions): Promise<SearchResult[]>;
private getAllAvailable;
isCommandAvailable(command: Command): Promise<boolean>;
getAvailableCommands(options?: {
category?: CommandCategory;
owner?: string;
}): Promise<Command[]>;
invoke<T = any>(key: CommandKey, input?: any, source?: "palette" | "shortcut" | "api"): Promise<T>;
/**
* Validate input against command's inputValidator without executing
* Useful for pre-validation before invoking
*
* @returns true if valid, or ValidationErrorDetail[] if invalid
*/
validateInput<TInput = any>(key: CommandKey, input?: TInput): Promise<true | ValidationErrorDetail[]>;
private executeWithTimeout;
attempt<T = any>(key: CommandKey, input?: any, source?: "palette" | "shortcut" | "api"): Promise<ExecutionResult<T>>;
private trackExecution;
private addToRecent;
getExecutionHistory(limit?: number): CommandExecutionContext[];
getRecentCommands(): Command[];
clearHistory(): void;
getCommandsByCategory(): Map<CommandCategory, Command[]>;
getCategories(): CommandCategory[];
createInvoker(key: CommandKey): CommandHandler;
invoker(key: CommandKey): CommandHandler;
getStats(): CommanderStats;
destroy(): void;
}
declare class CommandBuilder<TInput = any, TOutput = any> {
private command;
/**
* Create a new CommandBuilder instance
*/
static create<T = any, R = any>(key: string): CommandBuilder<T, R>;
/**
* Set the command key (required)
*/
key(key: CommandKey): this;
/**
* Set the command label (required)
*/
label(label: string): this;
/**
* Set the command description
*/
description(description: string): this;
/**
* Set the command category
*/
category(category: CommandCategory): this;
/**
* Set the command owner
*/
owner(owner: string): this;
/**
* Add tags to the command
*/
tags(...tags: string[]): this;
/**
* Set the command icon
*/
icon(icon: string): this;
/**
* Set the keyboard shortcut
*/
shortcut(shortcut: string): this;
/**
* Set the availability condition
*/
when(condition: () => boolean | Promise<boolean>): this;
/**
* Add search keywords
*/
searchKeywords(...keywords: string[]): this;
/**
* Set the command priority
*/
priority(priority: number): this;
/**
* Set the command timeout
*/
timeout(ms: number): 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: InputValidator<TInput>): this;
/**
* Set the command handler (required)
*/
handle(handler: (input?: TInput) => Promise<TOutput>): this;
/**
* Build and return the command
*/
build(): Command<TInput, TOutput>;
/**
* Clone this builder to create a new one with the same configuration
*/
clone(): CommandBuilder<TInput, TOutput>;
/**
* Reset the builder to start over
*/
reset(): this;
/**
* Get the current command state (for debugging)
*/
getState(): Partial<Command<TInput, TOutput>>;
}
declare class CommandTemplate {
/**
* Create a system command template
*/
static system(): CommandBuilder;
/**
* Create a file command template
*/
static file(): CommandBuilder;
/**
* Create a debug command template
*/
static debug(): CommandBuilder;
/**
* Create a view command template
*/
static view(): CommandBuilder;
/**
* Create a tools command template
*/
static tools(): CommandBuilder;
/**
* Create a custom command template
*/
static custom(): CommandBuilder;
}
/**
* Start building a command with a fluent API
*/
declare function command<TInput = any, TOutput = any>(key: string): CommandBuilder<TInput, TOutput>;
/**
* Create a simple command quickly
*/
declare function simpleCommand<TInput = any, TOutput = any>(key: string, label: string, handler: (input?: TInput) => Promise<TOutput>): Command<TInput, TOutput>;
interface CommanderContextValue {
commander: Commander;
commands: () => Command[];
add: (command: Command) => Command;
remove: (commandOrKey: string | CommandKey | Command) => boolean;
removeByOwner: (owner: string) => number;
removeByCategory: (category: CommandCategory) => number;
has: (key: CommandKey) => boolean;
getCommand: (key: CommandKey) => Command | undefined;
invoke: <T = any>(key: CommandKey, input?: any, source?: "palette" | "shortcut" | "api") => Promise<T>;
attempt: <T = any>(key: CommandKey, input?: any, source?: "palette" | "shortcut" | "api") => Promise<ExecutionResult<T>>;
search: (query: string, options?: SearchOptions) => Promise<SearchResult[]>;
isCommandAvailable: (command: Command) => Promise<boolean>;
getAvailableCommands: (options?: {
category?: CommandCategory;
owner?: string;
}) => Promise<Command[]>;
getCommandsByCategory: () => Map<CommandCategory, Command[]>;
getCategories: () => CommandCategory[];
getExecutionHistory: (limit?: number) => any[];
getRecentCommands: () => Command[];
clearHistory: () => void;
listen: (event: string, callback: (...args: any[]) => void | Promise<void>, options?: {
once?: boolean;
}) => any;
removeListener: (listener: any) => void;
createInvoker: (key: CommandKey) => any;
getStats: () => any;
isReady: boolean;
}
interface CommanderProviderProps {
children: ReactNode;
commander: Commander;
onReady?: (commander: Commander) => void;
enableDevTools?: boolean;
}
declare function CommanderProvider({ children, commander, onReady, enableDevTools, }: CommanderProviderProps): react_jsx_runtime.JSX.Element;
/**
* Hook to access the Commander context
* Returns the full Commander API
*/
declare function useCommander(): CommanderContextValue;
/**
* Hook to get just the commander instance
*/
declare function useCommanderInstance(): Commander;
/**
* Hook for direct command execution
*/
declare function useInvoke(): <T = any>(key: CommandKey, input?: any, source?: "palette" | "shortcut" | "api") => Promise<T>;
/**
* Hook for safe command execution
*/
declare function useAttempt(): <T = any>(key: CommandKey, input?: any, source?: "palette" | "shortcut" | "api") => Promise<ExecutionResult<T>>;
/**
* Hook for command search
*/
declare function useSearch(): (query: string, options?: SearchOptions) => Promise<SearchResult[]>;
/**
* Hook to get all commands
*/
declare function useCommands(): Command<any, any>[];
/**
* Hook to get commands by category
*/
declare function useCommandsByCategory(): Map<CommandCategory, Command<any, any>[]>;
/**
* Hook to get recent commands
*/
declare function useRecentCommands(): Command<any, any>[];
/**
* Hook to get command stats
*/
declare function useCommanderStats(): any;
interface UseCustomCommandProps<TInput = any, TOutput = any> {
key: string;
label?: string;
description?: string;
category?: CommandCategory;
tags?: string[];
icon?: string;
shortcut?: string;
when?: () => boolean | Promise<boolean>;
handle: (input?: TInput) => Promise<TOutput>;
timeout?: number;
priority?: number;
owner?: string;
searchKeywords?: string[];
}
interface CustomCommandInvoker<TInput, TOutput> {
key: CommandKey;
exists: () => boolean;
isAvailable: () => Promise<boolean>;
invoke: (input?: TInput, source?: "palette" | "shortcut" | "api") => Promise<TOutput>;
attempt: (input?: TInput, source?: "palette" | "shortcut" | "api") => Promise<{
success: boolean;
result?: TOutput;
error?: any;
command: CommandKey;
}>;
getCommand: () => Command<TInput, TOutput> | undefined;
}
/**
* Hook to register a temporary command that exists only while the component is mounted
*
* @param props Command configuration
* @returns Invoker object for executing the command
*
* @example
* ```tsx
* function FileEditor({ file }) {
* const saveInvoker = useCustomCommand({
* key: `file:save:${file.id}`,
* label: `Save ${file.name}`,
* icon: '💾',
* shortcut: 'ctrl+s',
* handle: async () => saveFile(file)
* });
*
* // Command is automatically registered and appears in Command Palette
* // Execute programmatically if needed:
* const handleSave = () => saveInvoker.invoke();
* }
* ```
*/
declare function useCustomCommand<TInput = any, TOutput = any>(props: UseCustomCommandProps<TInput, TOutput>): CustomCommandInvoker<TInput, TOutput>;
/**
* Hook for creating a simple action command (no input/output)
*/
declare function useAction$1(key: string, label: string, action: () => Promise<void> | void, options?: Partial<Omit<UseCustomCommandProps<void, void>, "key" | "label" | "handle">>): CustomCommandInvoker<void, void>;
/**
* Hook for creating a toggle command
*/
declare function useToggleCommand(key: string, label: string, isActive: boolean, onToggle: (newState: boolean) => Promise<void> | void, options?: Partial<Omit<UseCustomCommandProps<void, boolean>, "key" | "label" | "handle">>): CustomCommandInvoker<void, boolean>;
/**
* Hook for creating a modal command that returns a promise
*/
declare function useModalCommand<TInput, TOutput>(key: string, label: string, openModal: (input?: TInput) => Promise<TOutput>, options?: Partial<Omit<UseCustomCommandProps<TInput, TOutput>, "key" | "label" | "handle">>): CustomCommandInvoker<TInput, TOutput>;
/**
* Hook for creating a navigation command
*/
declare function useNavigationCommand(key: string, label: string, path: string, navigate: (path: string, state?: any) => void, options?: Partial<Omit<UseCustomCommandProps<void, void>, "key" | "label" | "handle">>): CustomCommandInvoker<void, void>;
/**
* Hook for creating contextual commands based on data
*/
declare function useContextualCommands<TData>(data: TData[], commandFactory: (item: TData, index: number) => UseCustomCommandProps): {
count: number;
refresh: () => void;
};
/**
* Hook to check if a custom command exists and is available
*/
declare function useCommandStatus(key: CommandKey): {
exists: boolean;
checkAvailability: () => Promise<boolean>;
isExecuting: boolean;
lastResult: any;
lastError: any;
};
/**
* Hook to listen for custom command events
*/
declare function useCommandEvents(key: CommandKey, handlers: {
onExecuting?: (context: any) => void;
onExecuted?: (context: {
result: any;
}) => void;
onError?: (error: any) => void;
}): void;
type CommandSource = "palette" | "shortcut" | "api";
interface UseCommandOptions<TInput = any, TOutput = any> {
source?: CommandSource;
throwOnError?: boolean;
timeout?: number;
retry?: number;
retryDelay?: number | ((attempt: number) => number);
debounce?: number;
throttle?: number;
defaultInput?: Partial<TInput>;
validateInput?: (input?: TInput) => boolean | string;
transformInput?: (input?: TInput) => TInput;
transformOutput?: (output: TOutput) => TOutput;
onSuccess?: (result: TOutput, input?: TInput) => void;
onError?: (error: Error, input?: TInput) => void;
onFinally?: (input?: TInput) => void;
resetOnKeyChange?: boolean;
}
interface CommandInvoker<TInput = any, TOutput = any> {
exists: boolean;
isAvailable: boolean;
isLoading: boolean;
lastResult: TOutput | null;
lastError: Error | null;
lastInput: TInput | null;
lastExecution: Date | null;
executionCount: number;
command: Command<TInput, TOutput> | null;
invoke: (input?: TInput, options?: Partial<UseCommandOptions<TInput, TOutput>>) => Promise<TOutput>;
attempt: (input?: TInput, options?: Partial<UseCommandOptions<TInput, TOutput>>) => Promise<ExecutionResult<TOutput>>;
execute: (input?: TInput, callbacks?: ExecuteCallbacks<TInput, TOutput>) => Promise<TOutput | null>;
canInvoke: (input?: TInput) => Promise<boolean>;
validateInput: (input?: TInput) => boolean | string;
reset: () => void;
clearError: () => void;
refresh: () => void;
}
interface ExecuteCallbacks<TInput = any, TOutput = any> {
onSuccess?: (result: TOutput, input?: TInput) => void;
onError?: (error: Error, input?: TInput) => void;
onFinally?: (input?: TInput) => void;
}
/**
* Hook para executar comandos existentes no Commander
*
* @param key - Chave do comando a ser executado
* @param options - Opções de configuração
* @returns CommandInvoker com estado e métodos de execução
*/
declare function useCommand<TInput = any, TOutput = any>(key: CommandKey, options?: UseCommandOptions<TInput, TOutput>): CommandInvoker<TInput, TOutput>;
interface UseInvokerOptions<TInput = any, TOutput = any> {
source?: CommandSource;
throwOnError?: boolean;
defaultInput?: Partial<TInput>;
onSuccess?: (result: TOutput) => void;
onError?: (error: Error) => void;
}
/**
* Hook simplificado para execução de comandos
*
* Retorna diretamente a função de execução, sem necessidade de chamar .invoke()
*
* @param key - Chave do comando a ser executado
* @param options - Opções simplificadas de configuração
* @returns Função de execução direta
*/
declare function useInvoker<TInput = any, TOutput = any>(key: CommandKey, options?: UseInvokerOptions<TInput, TOutput>): (input?: TInput) => Promise<TOutput>;
/**
* Hook para comandos que não precisam de input
*/
declare function useAction(key: CommandKey, options?: Omit<UseInvokerOptions<void, any>, "defaultInput">): () => Promise<any>;
/**
* Hook que retorna sempre o objeto CommandInvoker (alias para useCommand)
*/
declare function useCommandState<TInput = any, TOutput = any>(key: CommandKey, options?: UseInvokerOptions<TInput, TOutput>): CommandInvoker<TInput, TOutput>;
/**
* Hook para execução segura (nunca lança erro)
*/
declare function useSafeInvoker<TInput = any, TOutput = any>(key: CommandKey, options?: Omit<UseInvokerOptions<TInput, TOutput>, "throwOnError">): (input?: TInput) => Promise<ExecutionResult<TOutput>>;
/**
* Hook para comandos com input pré-configurado
*/
declare function useBoundInvoker<TInput = any, TOutput = any>(key: CommandKey, boundInput: Partial<TInput>, options?: Omit<UseInvokerOptions<TInput, TOutput>, "defaultInput">): (input?: Partial<TInput>) => Promise<TOutput>;
/**
* Hook para comandos toggle (boolean)
*/
declare function useToggleInvoker(key: CommandKey, options?: Omit<UseInvokerOptions<boolean, boolean>, "defaultInput">): (state?: boolean) => Promise<boolean>;
/**
* Hook para execução sequencial de múltiplos comandos
*/
declare function useBatchInvoker(keys: CommandKey[], options?: {
source?: CommandSource;
stopOnError?: boolean;
onProgress?: (completed: number, total: number) => void;
}): (inputs?: any[], batchOptions?: {
stopOnError?: boolean;
}) => Promise<ExecutionResult[]>;
/**
* Hook para execução paralela de múltiplos comandos
*/
declare function useParallelInvoker(keys: CommandKey[], options?: {
source?: CommandSource;
}): (inputs?: any[]) => Promise<PromiseSettledResult<any>[]>;
declare class CommandError extends Error {
command?: string | undefined;
name: string;
constructor(message: string, command?: string | undefined);
}
/**
* Error thrown when command input validation fails
* Contains detailed information about which fields failed and why
*/
declare class InputValidationError extends CommandError {
errors: ValidationErrorDetail[];
input?: unknown | undefined;
name: string;
constructor(command: string, errors: ValidationErrorDetail[], input?: unknown | undefined);
/**
* Get errors for a specific field path
*/
getFieldErrors(path: string): ValidationErrorDetail[];
/**
* Get all required field errors
*/
getRequiredErrors(): ValidationErrorDetail[];
/**
* Get missing required field paths
*/
getMissingFields(): string[];
/**
* Check if a specific field has errors
*/
hasFieldError(path: string): boolean;
/**
* Convert to a simple object for serialization
*/
toJSON(): {
name: string;
message: string;
command: string | undefined;
errors: ValidationErrorDetail[];
};
}
declare class CommandNotFoundError extends CommandError {
name: string;
constructor(command: string);
}
declare class CommandUnavailableError extends CommandError {
name: string;
constructor(command: string);
}
declare class CommandTimeoutError extends CommandError {
name: string;
constructor(command: string, timeout: number);
}
declare class CommandExecutionError extends CommandError {
name: string;
cause: Error;
constructor(command: string, originalError: Error);
}
declare function createCommandError(type: string, command: string, details?: any): CommandError;
declare function isInputValidationError(error: unknown): error is InputValidationError;
declare function isCommandError(error: unknown): error is CommandError;
declare function generateId(): string;
declare function normalizeSearchTerm(term: string): string;
declare function calculateSearchScore(command: any, queryTerms: string[]): number;
declare function getMatchedTerms(command: any, queryTerms: string[]): string[];
declare function isValidCommandKey(key: any): key is string;
declare function isValidCommand(command: any): boolean;
declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, errorMessage?: string): Promise<T>;
declare function delay(ms: number): Promise<void>;
declare function debounce<T extends (...args: any[]) => any>(func: T, wait: number, immediate?: boolean): (...args: Parameters<T>) => void;
declare function unique<T>(array: T[]): T[];
declare function removeItem<T>(array: T[], item: T): T[];
declare function deepClone<T>(obj: T): T;
declare class PriorityQueue<T> {
private items;
enqueue(item: T, priority?: number): void;
dequeue(): T | undefined;
peek(): T | undefined;
isEmpty(): boolean;
size(): number;
clear(): void;
remove(predicate: (item: T) => boolean): boolean;
}
declare const VERSION = "1.0.0";
export { type Command, CommandBuilder, type CommandCategory, CommandError, type CommandExecutionContext, CommandExecutionError, type CommandInvoker, type CommandKey, CommandNotFoundError, type CommandSource, CommandTemplate, CommandTimeoutError, CommandUnavailableError, Commander, CommanderProvider, type CommanderStats, type EventListener, type ExecuteCallbacks, type ExecutionResult, InputValidationError, type InputValidator, PriorityQueue, type SearchOptions, type SearchResult, type UseCommandOptions, type UseInvokerOptions, VERSION, type ValidationErrorDetail, type ValidationResult, calculateSearchScore, command, createCommandError, debounce, deepClone, delay, generateId, getMatchedTerms, isCommandError, isInputValidationError, isValidCommand, isValidCommandKey, normalizeSearchTerm, removeItem, simpleCommand, unique, useAction$1 as useAction, useAction 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 };