# @darksnow-ui/commander

> 🚀 **Enterprise-grade command system for React applications with Command Palette integration**

[![npm version](https://badge.fury.io/js/%40darksnow-ui%2Fcommander.svg)](https://badge.fury.io/js/%40darksnow-ui%2Fcommander)
[![TypeScript](https://img.shields.io/badge/%3C%2F%3E-TypeScript-%230074c1.svg)](http://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)

Transform your React app into a power-user's dream with a **VS Code-style command system**. Register commands from anywhere, execute them programmatically or via Command Palette, and watch your UX reach new levels.

![Commander](https://github.com/andersondrosa/andersondrosa/blob/main/images/commander-gracious-robot.jpeg?raw=true)

## ✨ Why Commander?

### 🎯 **The Problem**
- Scattered actions across your app with no centralized control
- No unified way to execute operations programmatically + via UI
- Command Palettes that require manual registration and maintenance
- Poor discoverability of available actions for power users

### 💡 **The Solution**
```tsx
// 1. Register commands anywhere in your app
useCustomCommand({
  key: 'file:save',
  label: 'Save File',
  shortcut: 'ctrl+s',
  handle: async () => saveCurrentFile()
});

// 2. Execute programmatically with full type safety
const saveFile = useInvoker<SaveInput, SaveResult>('file:save');
const result = await saveFile({ filename: 'document.txt' });

// 3. Users find it instantly in Command Palette (Ctrl+Shift+P)
// 4. Command auto-removes when component unmounts
```

## 🚀 Features

### 🏗️ **Core Architecture**
- **🎯 Command Pattern** - Centralized command management with O(1) lookups
- **🔍 Intelligent Search** - Hierarchical scoring algorithm with fuzzy matching
- **⌨️ Keyboard First** - Built-in shortcut support with conflict resolution
- **🏷️ Smart Organization** - Categories, tags, owners, and priority system
- **🔄 Event-Driven** - Comprehensive lifecycle events for monitoring and analytics

### ⚛️ **React Integration**
- **🎨 Zero Boilerplate** - Context Provider handles everything automatically
- **⚡ Temporary Commands** - Components register/unregister commands automatically
- **🛡️ Type Safety** - Full TypeScript with generics for inputs/outputs
- **🎪 Specialized Hooks** - 10 purpose-built hooks for every use case
- **🔥 Hot Reload Friendly** - Commands survive React Fast Refresh

### 🎛️ **Advanced Features**
- **📊 Execution Tracking** - History, analytics, and recent commands
- **🔒 Conditional Availability** - Commands appear/disappear based on context
- **⏱️ Timeout Handling** - Automatic timeouts with graceful error handling
- **🛠️ Dev Tools** - Built-in debugging commands and global access
- **📈 Performance** - Optimized for apps with hundreds of commands

## 📦 Installation

```bash
npm install @darksnow-ui/commander
# or
yarn add @darksnow-ui/commander
# or
pnpm add @darksnow-ui/commander
```

## ⚡ Quick Start

### 1. **Setup Provider** (one time in your app root)

```tsx
import { CommanderProvider, Commander } from '@darksnow-ui/commander';

// Create your commander instance (usually in src/core/commander.ts)
const commander = new Commander();

function App() {
  return (
    <CommanderProvider 
      commander={commander} 
      enableDevTools
      onReady={(commander) => {
        console.log('Commander ready with', commander.commands().length, 'commands');
      }}
    >
      <MyApp />
    </CommanderProvider>
  );
}
```

### 2. **Register Commands** (in any component)

```tsx
import { useCustomCommand } from '@darksnow-ui/commander';

function FileEditor({ file }) {
  // Command automatically appears in Command Palette!
  useCustomCommand({
    key: `file:save:${file.id}`,
    label: `Save ${file.name}`,
    icon: '💾',
    shortcut: 'ctrl+s',
    when: () => file.isDirty, // Only when file has changes
    handle: async () => {
      await saveFile(file);
      return { saved: true, filename: file.name };
    }
  });

  return <div>Your file editor UI...</div>;
}
```

### 3. **Execute Commands** (programmatically)

```tsx
import { useInvoker } from '@darksnow-ui/commander';

function ActionButton() {
  const saveFile = useInvoker<SaveInput, SaveResult>('file:save');
  
  const handleSave = async () => {
    try {
      const result = await saveFile({ filename: 'document.txt' });
      toast.success(`Saved: ${result.filename}`);
    } catch (error) {
      toast.error('Save failed');
    }
  };

  return <button onClick={handleSave}>Save File</button>;
}
```

## 🎣 React Hooks

### Core Hooks

#### `useCommander()`
Access the complete Commander API:
```tsx
const { commander, search, invoke, commands, has, getCommand } = useCommander();

// Search for commands
const results = await search('save');

// Execute any command
await invoke('file:save', { filename: 'test.txt' });

// Get all commands
const allCommands = commands();

// Check if command exists
if (has('file:save')) {
  const saveCommand = getCommand('file:save');
}
```

#### `useCustomCommand()`
Register temporary commands that auto-cleanup:
```tsx
useCustomCommand({
  key: 'my-command',
  label: 'My Command',
  description: 'Does something awesome',
  category: 'actions',
  icon: '🚀',
  shortcut: 'ctrl+shift+a',
  tags: ['quick', 'action'],
  priority: 10,
  when: () => isFeatureEnabled(), // Conditional availability
  handle: async (input) => {
    // Your command logic
    return { success: true };
  }
});
```

### Execution Hooks

#### `useCommand()`
Full-featured command execution with state tracking:
```tsx
const command = useCommand<Input, Output>('my-command', {
  throwOnError: true,
  source: 'api',
  onSuccess: (result) => console.log('Success:', result),
  onError: (error) => console.error('Error:', error)
});

// Execute with various methods
await command.invoke({ data: 'test' }); // throws on error
const result = await command.attempt({ data: 'test' }); // returns ExecutionResult
await command.execute({ data: 'test' }, {
  onSuccess: (result) => toast.success('Done!'),
  onError: (error) => toast.error(error.message)
});

// Access state
console.log(command.isLoading);
console.log(command.lastResult);
console.log(command.lastError);
console.log(command.executionCount);

// Check availability
if (command.isAvailable) {
  // Show UI
}
```

#### `useInvoker()`
Direct function execution (simplified):
```tsx
const saveFile = useInvoker<Input, Output>('file:save');

// Direct invocation - returns a function!
const result = await saveFile({ data: 'test' });

// With options
const saveWithOptions = useInvoker('file:save', {
  throwOnError: false,
  onSuccess: (result) => toast.success('Saved!')
});
```

### Specialized Hooks

#### `useAction()`
For commands without parameters:
```tsx
const logout = useAction('auth:logout');
const refresh = useAction('app:refresh');

// Simple execution
await logout();
await refresh();
```

#### `useCommandState()`
Always returns the CommandInvoker object (alias for useCommand):
```tsx
const command = useCommandState('my-command');

// Access state and methods
console.log(command.isLoading);
console.log(command.lastResult);
await command.invoke(data);
```

#### `useSafeInvoker()`
Non-throwing execution with ExecutionResult:
```tsx
const saveFile = useSafeInvoker<Input, Output>('file:save');

const result = await saveFile({ filename: 'test.txt' });

if (result.success) {
  console.log('Saved:', result.result);
} else {
  console.error('Failed:', result.error);
}
```

#### `useBoundInvoker()`
Pre-configured command execution:
```tsx
// Always saves as PDF
const savePdf = useBoundInvoker('file:save', { format: 'pdf' });

await savePdf({ filename: 'document' }); // format is already bound
```

#### `useToggleInvoker()`
For boolean toggle commands:
```tsx
const toggleDarkMode = useToggleInvoker('ui:dark-mode');
const toggleSidebar = useToggleInvoker('ui:sidebar');

// Toggle state
await toggleDarkMode(true);
await toggleSidebar(); // toggles current state
```

#### `useBatchInvoker()`
Sequential execution with progress tracking:
```tsx
const deployPipeline = useBatchInvoker(
  ['build', 'test', 'deploy'],
  { 
    stopOnError: true,
    onProgress: (step, total) => console.log(`${step}/${total}`)
  }
);

const results = await deployPipeline([
  { target: 'production' },
  { suite: 'all' },
  { env: 'prod' }
]);
```

#### `useParallelInvoker()`
Parallel execution with Promise.allSettled:
```tsx
const loadDashboard = useParallelInvoker([
  'stats:revenue',
  'stats:users', 
  'stats:orders'
]);

const results = await loadDashboard([
  { period: '30d' },
  { status: 'active' },
  { status: 'pending' }
]);
```

## 🏗️ Core Concepts

### Commander Instance
The heart of the system - manages all commands:

```tsx
// Create and configure
const commander = new Commander();
commander.maxHistorySize = 200;
commander.maxRecentSize = 15;

// Register system commands
commander.add({
  key: 'app:refresh',
  label: 'Refresh Application',
  handle: async () => location.reload()
});
```

### Command Structure
Commands are the building blocks:

```tsx
interface Command<TInput, TOutput> {
  key: CommandKey;                    // Unique identifier
  label: string;                      // Human-readable name
  handle: (input?: TInput) => Promise<TOutput>; // The actual function
  description?: string;               // Detailed description
  category?: CommandCategory;         // Organization
  icon?: string;                      // Visual indicator
  shortcut?: string;                  // Keyboard shortcut
  when?: () => boolean | Promise<boolean>; // Conditional availability
  tags?: string[];                    // Search tags
  priority?: number;                  // Search ranking
  owner?: string;                     // Who registered it
  source?: CommandSource;             // Where it came from
}
```

### Command Builder
Fluent API for command creation:

```tsx
import { CommandBuilder } from '@darksnow-ui/commander';

const saveCommand = CommandBuilder
  .create<SaveInput, SaveOutput>('file:save')
  .label('Save File')
  .description('Save the current file to disk')
  .category('file')
  .icon('💾')
  .shortcut('ctrl+s')
  .tags(['file', 'save', 'disk'])
  .priority(100)
  .handle(async (input) => {
    const result = await saveFile(input);
    return { success: true, path: result.path };
  })
  .build();

commander.add(saveCommand);
```

### Search System
Intelligent hierarchical scoring:

```tsx
const results = await commander.search('save file', {
  category: 'file',     // Filter by category
  owner: 'editor',      // Filter by owner
  tags: ['important'],  // Filter by tags
  limit: 10            // Limit results
});

// Results are scored by:
// 1. Exact key match (highest)
// 2. Label match
// 3. Description match
// 4. Tag match
// 5. Fuzzy match (lowest)
```

### Event System
Monitor command lifecycle:

```tsx
// Listen to events
commander.on('commandRegistered', (command) => {
  console.log('New command:', command.key);
});

commander.on('beforeExecute', ({ command, input }) => {
  analytics.track('command_execute', { key: command.key });
});

commander.on('afterExecute', ({ command, result, duration }) => {
  console.log(`Command ${command.key} took ${duration}ms`);
});

commander.on('executionError', ({ command, error }) => {
  errorReporter.log(error);
});
```

## 📚 Documentation

### 📖 **Guides**
- **[Getting Started](./docs/getting-started.md)** - Complete setup guide
- **[Hooks Guide](./docs/hooks-guide.md)** - Deep dive into all React hooks
- **[useCustomCommand Examples](./docs/useCustomCommand-examples.md)** - 30+ real-world examples
- **[useCommand Guide](./docs/useCommand-guide.md)** - Advanced state management
- **[useInvoker Guide](./docs/useInvoker-guide.md)** - Command execution patterns
- **[Commander Algorithm](./docs/commander-algorithm.md)** - Core architecture deep dive

### 🎯 **Examples**

<details>
<summary><b>📝 Text Editor with Context Commands</b></summary>

```tsx
function TextEditor({ document }) {
  const [content, setContent] = useState(document.content);
  const [isDirty, setIsDirty] = useState(false);

  // Save command - only available when dirty
  useCustomCommand({
    key: `doc:save:${document.id}`,
    label: `Save ${document.name}`,
    category: 'file',
    icon: '💾',
    shortcut: 'ctrl+s',
    when: () => isDirty,
    handle: async () => {
      await saveDocument(document.id, content);
      setIsDirty(false);
      return { saved: true };
    }
  });

  // Format command
  useCustomCommand({
    key: `doc:format:${document.id}`,
    label: `Format ${document.name}`,
    category: 'edit',
    icon: '✨',
    shortcut: 'shift+alt+f',
    handle: async () => {
      const formatted = await formatText(content);
      setContent(formatted);
      setIsDirty(true);
      return { formatted: true };
    }
  });

  return (
    <textarea 
      value={content} 
      onChange={(e) => {
        setContent(e.target.value);
        setIsDirty(true);
      }}
    />
  );
}
```

</details>

<details>
<summary><b>🛍️ E-commerce Admin Dashboard</b></summary>

```tsx
function ProductList({ products }) {
  // Register command for each product
  products.forEach(product => {
    useCustomCommand({
      key: `product:edit:${product.id}`,
      label: `Edit ${product.name}`,
      description: `SKU: ${product.sku}`,
      category: 'products',
      icon: '✏️',
      tags: ['product', 'edit', product.category],
      searchKeywords: [product.name, product.sku, product.brand],
      handle: async () => {
        await openProductEditor(product.id);
        return { opened: true };
      }
    });
  });

  // Bulk operations
  const bulkDelete = useInvoker<{ ids: string[] }>('products:bulk-delete');
  
  const handleBulkDelete = async (selectedIds: string[]) => {
    const result = await bulkDelete({ ids: selectedIds });
    if (result.success) {
      toast.success(`Deleted ${result.count} products`);
    }
  };

  return <ProductGrid products={products} onBulkDelete={handleBulkDelete} />;
}
```

</details>

<details>
<summary><b>🎮 Game Controls</b></summary>

```tsx
function GameControls() {
  const [isPaused, setIsPaused] = useState(false);
  
  // Game actions
  useCustomCommand({
    key: 'game:pause',
    label: isPaused ? 'Resume Game' : 'Pause Game',
    icon: isPaused ? '▶️' : '⏸️',
    shortcut: 'space',
    handle: async () => {
      setIsPaused(!isPaused);
      return { paused: !isPaused };
    }
  });

  useCustomCommand({
    key: 'game:save',
    label: 'Quick Save',
    icon: '💾',
    shortcut: 'f5',
    when: () => !isPaused,
    handle: async () => {
      const slot = await saveGame();
      return { slot };
    }
  });

  useCustomCommand({
    key: 'game:load',
    label: 'Quick Load',
    icon: '📂',
    shortcut: 'f9',
    handle: async () => {
      await loadLastSave();
      return { loaded: true };
    }
  });

  return <GameUI paused={isPaused} />;
}
```

</details>

## 🏆 Comparison

| Feature | **@darksnow-ui/commander** | cmdk | kbar | Custom Solution |
|---------|----------------------------|------|------|-----------------|
| **TypeScript** | ✅ Full generics | ✅ Basic | ✅ Good | 🤷 Depends |
| **React Hooks** | ✅ 10 specialized hooks | ❌ None | ❌ Limited | 🤷 Depends |
| **Auto Cleanup** | ✅ Automatic | ❌ Manual | ❌ Manual | ❌ Manual |
| **Conditional Commands** | ✅ Built-in `when()` | ❌ Manual | ❌ Manual | 🤷 Depends |
| **State Management** | ✅ Built-in tracking | ❌ None | ❌ None | 🤷 Depends |
| **Event System** | ✅ Full lifecycle | ❌ None | ✅ Limited | 🤷 Depends |
| **Search Algorithm** | ✅ Hierarchical scoring | ✅ Fuzzy | ✅ Fuzzy | 🤷 Depends |
| **Batch Execution** | ✅ Built-in hooks | ❌ None | ❌ None | 🤷 Depends |
| **Performance** | ✅ O(1) operations | ⚠️ O(n) | ⚠️ O(n) | 🤷 Depends |
| **Bundle Size** | 📦 ~50KB | 📦 ~40KB | 📦 ~45KB | 🤷 Depends |

## 🚀 Performance

Built for scale with real-world optimization:

- **⚡ O(1) Operations**: Command lookup, registration, and removal
- **🔍 Efficient Search**: Optimized scoring algorithm
- **🧠 Smart Memoization**: React renders optimized automatically
- **📦 Memory Bounded**: Automatic cleanup of history and listeners
- **🎯 Lazy Evaluation**: Commands only checked when needed
- **📊 Battle Tested**: Used in production with 500+ commands

## 🛡️ TypeScript

First-class TypeScript support with advanced patterns:

```tsx
// Strongly typed commands
interface SaveFileInput {
  filename: string;
  content: string;
  format?: 'utf8' | 'binary';
}

interface SaveFileOutput {
  success: boolean;
  path: string;
  size: number;
}

// Type-safe registration
useCustomCommand<SaveFileInput, SaveFileOutput>({
  key: 'file:save',
  label: 'Save File',
  handle: async (input) => {
    // input is typed as SaveFileInput
    const result = await fs.writeFile(input.filename, input.content);
    // Must return SaveFileOutput
    return {
      success: true,
      path: result.path,
      size: input.content.length
    };
  }
});

// Type-safe execution
const saveFile = useInvoker<SaveFileInput, SaveFileOutput>('file:save');
const result = await saveFile({
  filename: 'test.txt',
  content: 'Hello world'
}); // result is typed as SaveFileOutput
```

## 🧪 Testing

Comprehensive test coverage with 192 tests:

```tsx
import { renderHook } from '@testing-library/react';
import { CommanderProvider, useCustomCommand } from '@darksnow-ui/commander';

test('command registration and execution', async () => {
  const { result } = renderHook(
    () => {
      useCustomCommand({
        key: 'test:command',
        label: 'Test Command',
        handle: async (input: { value: number }) => {
          return { doubled: input.value * 2 };
        }
      });
      
      return useInvoker<{ value: number }, { doubled: number }>('test:command');
    },
    {
      wrapper: ({ children }) => (
        <CommanderProvider commander={new Commander()}>
          {children}
        </CommanderProvider>
      )
    }
  );

  const output = await result.current({ value: 5 });
  expect(output.doubled).toBe(10);
});
```

## 🔧 Configuration

### Commander Options

```tsx
const commander = new Commander({
  maxHistorySize: 200,     // Maximum execution history entries
  maxRecentSize: 15,       // Maximum recent commands to track
  executionTimeout: 30000, // Default timeout for commands (ms)
  enableDevTools: true,    // Enable debugging features
});
```

### Provider Options

```tsx
<CommanderProvider
  commander={commander}
  enableDevTools={true}
  onReady={(commander) => {
    // Called when commander is ready
    console.log('Commander initialized');
  }}
>
  {children}
</CommanderProvider>
```

## 📄 License

MIT © [Anderson Rosa](https://github.com/andersondrosa)

## 🤝 Contributing

We love contributions! See our [Contributing Guide](./CONTRIBUTING.md) for details.

## 💬 Community & Support

- **🐛 Bug Reports**: [GitHub Issues](https://github.com/darksnow-ui/darksnow-ui/issues)
- **💡 Feature Requests**: [GitHub Discussions](https://github.com/darksnow-ui/darksnow-ui/discussions)
- **📖 Full Documentation**: [View all docs](./docs/)

## ⭐ Show Your Support

If Commander helps your project, please consider:
- ⭐ **Starring** this repository
- 🐦 **Sharing** on social media
- 📝 **Writing** about your experience
- 🤝 **Contributing** to the project

---

<div align="center">

**Built with ❤️ by [Anderson Rosa](https://github.com/andersondrosa)**

*Part of the [DarkSnow UI](https://github.com/darksnow-ui/darksnow-ui) ecosystem*

[⭐ Star on GitHub](https://github.com/darksnow-ui/darksnow-ui) • [📖 Read the Docs](./docs/) • [🚀 View Examples](./docs/useCustomCommand-examples.md)

</div>