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 0
Advanced: 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 types
Complete 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.