name: bun-cli description: Build production-grade CLI tools with Bun. Reference implementation covering argument parsing patterns (--flag value, --flag=value, --flag), dual markdown/JSON output, error handling, subcommands, and testing. Use when building CLIs, designing argument parsing, implementing command structures, reviewing CLI quality, or learning Bun CLI best practices. triggers:
- bun cli
- command line tool
- argument parsing
- cli development
- cli architecture
Bun CLI Development
Build powerful, production-grade CLI tools with Bun. Master argument parsing, output formatting, error handling, subcommands, and testing patterns proven in production across the SideQuest marketplace.
Quick Navigation
- Quick Start — Get a working CLI in 5 minutes
- Core Patterns — Argument parsing, output, usage, errors, subcommands
- Advanced Features — Dry-run, auto-commit, git integration
- Testing Your CLI — Unit and integration test patterns
- Reference — Comprehensive pattern guide + Para Obsidian example (9/10)
Quick Start
Goal: Build a CLI tool that feels natural to use and is easy to maintain.
Minimal CLI Template
#!/usr/bin/env bun
import { color } from "@sidequest/core/formatters";
function printUsage(): void {
console.log(color("cyan", "My CLI Tool v1.0"));
console.log("Usage: my-cli <command> [options]");
console.log(" config Show configuration");
console.log(" help Show this help");
}
async function main(): Promise<void> {
const [, , command] = process.argv;
if (!command || command === "help") {
printUsage();
return;
}
try {
switch (command) {
case "config":
console.log("Config: {...}");
break;
default:
console.error(`Unknown command: ${command}`);
process.exit(1);
}
} catch (error) {
console.error("Error:", error instanceof Error ? error.message : error);
process.exit(1);
}
}
main();
Core Patterns
1. Argument Parsing
The marketplace standard uses manual parsing (not external libraries). This keeps CLIs simple, dependency-light, and predictable.
Handle three flag formats:
--flag value— Spaced syntax--flag=value— Equals syntax--flag— Boolean flag
function parseArgs(argv: string[]) {
const positional: string[] = [];
const flags: Record<string, string | boolean> = {};
for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (!arg) continue;
if (arg.startsWith("--")) {
const [keyRaw, value] = arg.split("=");
const key = keyRaw?.slice(2);
if (!key) continue;
const next = argv[i + 1];
if (value !== undefined) {
flags[key] = value;
} else if (next && !next.startsWith("--")) {
flags[key] = next;
i++;
} else {
flags[key] = true;
}
} else {
positional.push(arg);
}
}
const [command, subcommand, ...rest] = positional;
return { command: command ?? "", subcommand, positional: rest, flags };
}
For detailed patterns and edge cases, see bun-cli-patterns.md § Argument Parsing.
2. Output Formatting
Always support both markdown (human) and JSON (machine) formats.
import { OutputFormat, parseOutputFormat } from "@sidequest/core/formatters";
type Result = { title: string; items: string[] };
function formatMarkdown(result: Result): string {
return `# ${result.title}\n\n${result.items.map(i => `- ${i}`).join("\n")}`;
}
function formatJson(result: Result): string {
return JSON.stringify(result, null, 2);
}
function formatOutput(result: Result, format: OutputFormat): string {
return format === "json" ? formatJson(result) : formatMarkdown(result);
}
// In main()
const format = parseOutputFormat(flags.format);
console.log(formatOutput(result, format));
Benefits: Humans read markdown (colored, readable), scripts parse JSON (structured, typeable).
For color palettes and advanced formatting, see bun-cli-patterns.md § Output Formatting.
3. Usage Text
Make your CLI self-documenting with clear, scannable usage text.
function printUsage(): void {
const lines = [
color("cyan", "My CLI Tool"),
"",
"Usage:",
" my-cli config [--format md|json]",
" my-cli list [path] [--format md|json]",
" my-cli create --template <type> [options]",
"",
"Options:",
" --format md|json Output format (default: md)",
" --dry-run Show changes without applying",
" --help Show this help",
"",
"Examples:",
" my-cli config --format json",
" my-cli list . --format md",
" my-cli create --template project --dry-run",
];
console.log(lines.map(line => color("cyan", line)).join("\n"));
}
Key points:
- Colored headers (cyan)
- Real, copy-paste examples
- All three flag formats shown
- Structure: Usage → Options → Examples
4. Error Handling
Be explicit and contextual with errors.
try {
const config = loadConfig();
if (!config.vault) {
console.error("Error: VAULT environment variable required");
process.exit(1);
}
// Do work...
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(`Error: ${message}`);
process.exit(1);
}
Conventions:
- Exit code 0 = success
- Exit code 1 = error
- Prefix errors with "Error:"
- Include contextual information (missing env, invalid file, etc.)
- Avoid stack traces in user output
5. Subcommands
For CLIs with many operations, use two-level commands:
case "frontmatter": {
const subcommand = args[0];
switch (subcommand) {
case "get":
// ...
case "validate":
// ...
case "migrate":
// ...
default:
console.error(`Unknown subcommand: frontmatter ${subcommand}`);
process.exit(1);
}
break;
}
Benefits:
- Flat namespace
frontmatter getvs.frontmatter-get - Easy to add subcommands
- Clear semantic grouping
Advanced Features
Dry-Run Support
Every write operation should support --dry-run:
const dryRun = flags["dry-run"] === true;
const result = await deleteFile(vault, file, { dryRun });
if (dryRun) {
console.log("Would delete:", file);
} else {
console.log("Deleted:", file);
}
Auto-Commit Integration
For tools that modify files, consider git integration:
if (flags["auto-commit"]) {
const { isRepo, isClean } = await checkGitStatus(vault);
if (!isRepo) throw new Error("Must be in a git repository");
if (!isClean) throw new Error("Working tree must be clean");
await autoCommitChanges(vault, changedFiles);
}
Testing Your CLI
Unit Testing with Bun
import { describe, expect, test } from "bun:test";
import { parseArgs } from "./args";
describe("CLI argument parsing", () => {
test("parses --key value format", () => {
const result = parseArgs(["command", "--name", "test"]);
expect(result.flags.name).toBe("test");
});
test("parses --key=value format", () => {
const result = parseArgs(["command", "--name=test"]);
expect(result.flags.name).toBe("test");
});
test("handles boolean flags", () => {
const result = parseArgs(["command", "--verbose"]);
expect(result.flags.verbose).toBe(true);
});
});
Integration Testing
# Test real CLI invocation
bun run src/cli.ts config --format json
# Verify output is valid JSON
bun run src/cli.ts config --format json | jq .
# Test error handling
bun run src/cli.ts unknown-command
echo $? # Should be 1
Reference
📚 Comprehensive Pattern Guide
See bun-cli-patterns.md for the complete, detailed reference:
- File structure — Project layout and organization
- Entry point — Shebang, imports, main flow
- Argument utilities — Parsing key=value, lists, type coercion
- Output utilities — Color palettes, formatting helpers
- Exit codes — Success (0), errors (1-3)
- Configuration & environment — Loading, validation
- Testing — Unit and integration test patterns
- Bun-specific patterns — Process I/O, file I/O, shell commands
- Command dispatch — Simple vs. complex CLI architectures
- Examples — Real implementations from marketplace
- Checklist — Implementation, testing, documentation verification
- Anti-patterns — Don't do these!
- Migration guide — Updating existing CLIs to standard
🔍 Example Implementation
See bun-cli-patterns.md § Para Obsidian CLI Review:
- Score: 9/10 — Exemplary reference implementation
- Real implementation analyzed against standard
- All patterns demonstrated in production code
- Subcommands, dry-run, auto-commit, error handling
Use Para Obsidian CLI as a template for:
- Argument parsing pattern
- Usage output structure
- Output formatting (md/json)
- Error handling
- Subcommand dispatch
Common Pitfalls
❌ Don't
- Use external CLI libraries (oclif, yargs, commander) — Keep it simple
- Skip error handling — Users need clear feedback
- Ignore markdown output — Always support both markdown + JSON
- Create confusing flag names — Be explicit and consistent
- Forget the shebang —
#!/usr/bin/env bunat the top
✅ Do
- Start with manual parsing — It's simpler than you think
- Test all three flag formats — Users will use all of them
- Provide real examples — Copy-paste examples in usage text
- Support --help — Make your CLI self-documenting
- Exit with proper codes — 0 for success, 1 for error
Checklist: Building a CLI
- Shebang at top:
#!/usr/bin/env bun - JSDoc explaining CLI purpose
- Argument parsing (--flag value, --flag=value, --flag)
- Usage function with examples
- Subcommand dispatch (if needed)
- Try/catch error handling with contextual messages
- Support both markdown (default) and JSON output
- Exit codes: 0 for success, 1 for error
- Tests for argument parsing
- Tests for each command/subcommand
- README explaining usage
- Package.json bin entry (if applicable)
Pro Tips
Tip 1: Progressive Disclosure in Help
// Basic help (what I do)
my-cli help
// Shows: command list + brief descriptions
// Advanced help (how to use me)
my-cli help create
// Shows: create command + all options + examples
Tip 2: Output to Stderr for Errors
// Use console.error for errors (goes to stderr)
console.error("Error:", message); // ✅ Correct
// Avoid using console.log for errors
console.log("Error:", message); // ❌ Goes to stdout
Tip 3: Use Color Strategically
// Color headers and important info
console.log(color("green", "✅ Success"));
console.log(color("yellow", "⚠️ Warning"));
console.error(color("red", "❌ Error"));
// Don't color everything — readers get fatigued
Tip 4: Validate at Boundaries
// Validate user input (flags, args) immediately
if (!flags.name || typeof flags.name !== "string") {
console.error("Error: --name flag required");
process.exit(1);
}
// Trust internal functions (already validated)
function processName(name: string) {
// name is guaranteed to be a non-empty string
}
FAQ
Q: Should I use oclif or similar frameworks? A: No. Manual parsing is simpler and keeps CLIs lean. The marketplace standard uses manual parsing across all CLIs.
Q: How do I handle secrets in CLIs? A: Use environment variables. Never accept secrets as flags (they'd appear in shell history).
Q: Should subcommands have their own help?
A: Yes. my-cli subcommand --help should show help for that subcommand specifically.
Q: When should I add colors? A: For headers, success messages, and errors. Don't color everything — let contrast do the work.
Q: How do I test CLIs effectively? A: Unit test argument parsing. Integration test actual CLI invocations with real files.
Q: Why manual parsing instead of libraries? A: Zero dependencies, explicit and predictable, easy to extend, familiar across all marketplace CLIs.
Last Updated: 2025-12-05 Status: Reference Implementation Related: bun-cli-patterns.md (comprehensive reference + example)