Agent skill
bun-cli
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/bun-cli
SKILL.md
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)
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?