Subcommands
llms.txtBasic Usage
Section titled “Basic Usage”Default recommendation: use arg()/flag() for typed reads inside handlers. Keep args.shift() for command-token routing.
import { subcommands } from 'bunmagic/extras'
const commands = subcommands({ add: async () => { const file = arg(0).string().required('Missing file to add') console.log(`Adding ${file}...`) }, remove: async () => { const file = arg(0).string().required('Missing file to remove') console.log(`Removing ${file}...`) }, list: async () => { console.log('Listing all items...') },})
commands.maybeHelp()
// args.shift() mutates global args (consumes command token globally)const commandName = args.shift() || 'list'const command = commands.get(commandName)await command()API Reference
Section titled “API Reference”subcommands(config)
Section titled “subcommands(config)”Creates a subcommands handler around a command map.
commands.get(name?, fallback?)
Section titled “commands.get(name?, fallback?)”Retrieves a command callback.
Behavior:
- Valid
namereturns that command. fallbackis used only whennameisundefined.- Invalid names still throw, even with
fallback. - Empty-string names are invalid.
commands.name(commandName)
Section titled “commands.name(commandName)”Validates and returns a typed command name.
commands.commands
Section titled “commands.commands”Array of available command names.
commands.maybeHelp()
Section titled “commands.maybeHelp()”Prints command list and exits when --help is set.
Note: maybeHelp() checks flags.help; it does not trigger on -h (flags.h).
Typed Subcommands
Section titled “Typed Subcommands”import { subcommandFactory } from 'bunmagic/extras'
const typed = subcommandFactory<string, void>()
const commands = typed({ echo: async (...parts: string[]) => { console.log(parts.join(' ')) },})Complete Example: Todo CLI
Section titled “Complete Example: Todo CLI”#!/usr/bin/env bunmagicimport { subcommands } from 'bunmagic/extras'
interface Todo { id: number text: string done: boolean}
const todosPath = '~/.todos.json'let todos: Todo[] = []
if (await files.pathExists(todosPath)) { todos = JSON.parse(await files.readFile(todosPath)) as Todo[]}
async function saveTodos() { await files.outputFile(todosPath, `${JSON.stringify(todos, null, 2)}\n`)}
const commands = subcommands({ add: async () => { const text = args.join(' ') if (!text) throw new Exit('Please provide todo text')
todos.push({ id: Date.now(), text, done: false }) await saveTodos() console.log(ansis.green('Added:'), text) },
list: async () => { if (todos.length === 0) { console.log(ansis.dim('No todos yet')) return }
for (const todo of todos) { const status = todo.done ? ansis.green('✓') : ansis.red('○') console.log(`${status} [${todo.id}] ${todo.text}`) } },
done: async () => { const id = arg(0).int().required('Please provide todo id') const todo = todos.find(t => t.id === id) if (!todo) throw new Exit(`Todo ${id} not found`)
todo.done = true await saveTodos() console.log(ansis.green('Completed:'), todo.text) },})
commands.maybeHelp()
const commandName = args.shift() || 'list'try { await commands.get(commandName)()} catch (error) { const message = error instanceof Error ? error.message : String(error) console.error(ansis.red(message)) throw new Exit(1)}Best Practices
Section titled “Best Practices”- Call
maybeHelp()early. - Consume command token once with
args.shift(). - Prefer explicit fallback (
args.shift() || 'list'). - Default to
arg()/flag()for typed argument and flag reads. - Keep global
flagsfor quick booleans (flags.help,flags.verbose,flags.debug). - Use
throw new Exit(...)for user-facing failures.
SAF Note
Section titled “SAF Note”Avoid SAF in new subcommand examples. It is deprecated in 1.4.x and planned for removal in 2.0.0.