Skip to content

Subcommands

llms.txt

Bunmagic 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.

import { subcommands } from 'bunmagic/extras';
// Define your subcommands
const 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 flag
commands.maybeHelp(); // Shows available commands if --help is passed
// Get the command from arguments
const commandName = args.shift();
const command = commands.get(commandName, 'list'); // 'list' is the fallback
// Execute the command
await command();

Creates a new subcommands handler with the provided command configuration.

const commands = subcommands({
commandName: async () => { /* implementation */ },
// ... more commands
});

Retrieves a command by name with optional fallback.

// Get specific command (throws if not found)
const addCommand = commands.get('add');
// Get with fallback
const command = commands.get(args[0], 'help');
// Get fallback when no name provided
const defaultCommand = commands.get(undefined, 'help');

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
}

Returns an array of all available command names.

console.log('Available commands:', commands.commands.join(', '));

Automatically handles the --help flag by displaying available commands and exiting.

// Call this early in your script
commands.maybeHelp();
// If --help was passed, it will print:
// Available commands:
// • add
// • remove
// • list
// And exit with code 0

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 value
const 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 types
const processCmd = commands.get('process');
await processCmd('data.csv', 5); // ✓ Correct types

Here’s a complete example of a todo list manager with subcommands:

#!/usr/bin/env bunmagic
import { subcommands } from 'bunmagic/extras';
interface Todo {
id: number;
text: string;
done: boolean;
}
const todosFile = SAF.from('~/.todos.json');
let todos: Todo[] = [];
// Load todos
if (await todosFile.exists()) {
todos = await todosFile.readJSON();
}
// Save todos
async function saveTodos() {
await todosFile.writeJSON(todos);
}
// Define commands
const 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 help
commands.maybeHelp();
// Get and execute command
const 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);
}

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.

  1. Always call .maybeHelp() early in your script to handle the help flag
  2. Provide a fallback command when using .get() to handle missing arguments
  3. Use the typed factory when your commands have specific parameter requirements
  4. Validate arguments within each command handler
  5. Show helpful error messages that include available commands

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"
}
FeatureSimple ScriptWith Subcommands
Multiple actionsUse flags (--add, --remove)Use commands (add, remove)
Help generationManualAutomatic with .maybeHelp()
Command validationManualAutomatic
TypeScript supportBasicFull typing with factory
User experiencescript --add itemscript add item

The subcommands pattern is ideal when your script has distinct operations that would feel more natural as separate commands rather than flags.