Subcommands
llms.txtBunmagic provides a powerful subcommands system that allows you to create CLI tools with multiple commands, similar to how git has git add, git commit, etc. The system includes automatic help generation, command validation, and TypeScript support.
Basic Usage
Section titled “Basic Usage”import { subcommands } from 'bunmagic/extras';
// Define your subcommandsconst commands = subcommands({ add: async () => { const file = args[0]; console.log(`Adding ${file}...`); },
remove: async () => { const file = args[0]; console.log(`Removing ${file}...`); },
list: async () => { console.log('Listing all items...'); }});
// Automatically handle --help flagcommands.maybeHelp(); // Shows available commands if --help is passed
// Get the command from argumentsconst commandName = args.shift();const command = commands.get(commandName, 'list'); // 'list' is the fallback
// Execute the commandawait command();API Reference
Section titled “API Reference”subcommands(config)
Section titled “subcommands(config)”Creates a new subcommands handler with the provided command configuration.
const commands = subcommands({ commandName: async () => { /* implementation */ }, // ... more commands});.get(name?, fallback?)
Section titled “.get(name?, fallback?)”Retrieves a command by name with optional fallback.
// Get specific command (throws if not found)const addCommand = commands.get('add');
// Get with fallbackconst command = commands.get(args[0], 'help');
// Get fallback when no name providedconst defaultCommand = commands.get(undefined, 'help');.name(commandName)
Section titled “.name(commandName)”Validates and returns a command name. Throws an error with available commands if invalid.
try { const validName = commands.name(userInput); console.log(`Running ${validName} command`);} catch (error) { console.error(error.message); // Shows valid commands}.commands
Section titled “.commands”Returns an array of all available command names.
console.log('Available commands:', commands.commands.join(', '));.maybeHelp()
Section titled “.maybeHelp()”Automatically handles the --help flag by displaying available commands and exiting.
// Call this early in your scriptcommands.maybeHelp();
// If --help was passed, it will print:// Available commands:// • add// • remove// • list// And exit with code 0Advanced: Typed Subcommands
Section titled “Advanced: Typed Subcommands”For commands that accept specific arguments and return values, use the typed factory:
import { subcommandFactory } from 'bunmagic/extras';
// Create a typed subcommands factory// First type parameter: arguments array// Second type parameter: return valueconst typedCommands = subcommandFactory<[string, number], void>();
const commands = typedCommands({ process: async (name: string, count: number) => { console.log(`Processing ${name} ${count} times`); },
analyze: async (data: string, threshold: number) => { console.log(`Analyzing ${data} with threshold ${threshold}`); }});
// TypeScript ensures correct argument typesconst processCmd = commands.get('process');await processCmd('data.csv', 5); // ✓ Correct typesComplete Example: Todo CLI
Section titled “Complete Example: Todo CLI”Here’s a complete example of a todo list manager with subcommands:
#!/usr/bin/env bunmagicimport { subcommands } from 'bunmagic/extras';
interface Todo { id: number; text: string; done: boolean;}
const todosFile = SAF.from('~/.todos.json');let todos: Todo[] = [];
// Load todosif (await todosFile.exists()) { todos = await todosFile.readJSON();}
// Save todosasync function saveTodos() { await todosFile.writeJSON(todos);}
// Define commandsconst commands = subcommands({ add: async () => { const text = args.join(' '); if (!text) die('Please provide todo text');
const todo: Todo = { id: Date.now(), text, done: false };
todos.push(todo); await saveTodos(); console.log(ansis.green('✓ Added:'), text); },
list: async () => { if (todos.length === 0) { console.log(ansis.dim('No todos yet!')); return; }
todos.forEach(todo => { const status = todo.done ? ansis.green('✓') : ansis.red('○'); console.log(`${status} [${todo.id}] ${todo.text}`); }); },
done: async () => { const id = parseInt(args[0]); const todo = todos.find(t => t.id === id);
if (!todo) die(`Todo ${id} not found`);
todo.done = true; await saveTodos(); console.log(ansis.green('✓ Completed:'), todo.text); },
remove: async () => { const id = parseInt(args[0]); const index = todos.findIndex(t => t.id === id);
if (index === -1) die(`Todo ${id} not found`);
const [removed] = todos.splice(index, 1); await saveTodos(); console.log(ansis.red('✗ Removed:'), removed.text); }});
// Handle helpcommands.maybeHelp();
// Get and execute commandconst commandName = args.shift() || 'list';try { const command = commands.get(commandName); await command();} catch (error) { console.error(ansis.red(error.message)); console.log('\nUse --help to see available commands'); exit(1);}JSDoc Integration
Section titled “JSDoc Integration”You can document subcommands in your script’s JSDoc header using the @subcommand tag:
/** * Todo list manager * @autohelp * @usage todo <command> [args] * @subcommand add <text> - Add a new todo * @subcommand list - List all todos * @subcommand done <id> - Mark todo as complete * @subcommand remove <id> - Remove a todo */This integrates with Bunmagic’s automatic help generation system.
Best Practices
Section titled “Best Practices”- Always call
.maybeHelp()early in your script to handle the help flag - Provide a fallback command when using
.get()to handle missing arguments - Use the typed factory when your commands have specific parameter requirements
- Validate arguments within each command handler
- Show helpful error messages that include available commands
Error Handling
Section titled “Error Handling”The subcommands system provides helpful error messages automatically:
try { const command = commands.get('invalid');} catch (error) { console.error(error.message); // Output: "Invalid command. Valid commands are: add, remove, list"}Comparison with Simple Scripts
Section titled “Comparison with Simple Scripts”| Feature | Simple Script | With Subcommands |
|---|---|---|
| Multiple actions | Use flags (--add, --remove) | Use commands (add, remove) |
| Help generation | Manual | Automatic with .maybeHelp() |
| Command validation | Manual | Automatic |
| TypeScript support | Basic | Full typing with factory |
| User experience | script --add item | script add item |
The subcommands pattern is ideal when your script has distinct operations that would feel more natural as separate commands rather than flags.