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.
Build production-grade CLI tools with Bun. Use when creating command-line applications, implementing argument parsing for --flag value/--flag=value/--flag formats, or adding dual markdown/JSON output support.
/plugin marketplace add nathanvale/side-quest-marketplace/plugin install bookmarks@side-quest-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/bun-cli-patterns.mdscripts/review-cli.tsscripts/scaffold-cli.tsBuild 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.
Goal: Build a CLI tool that feels natural to use and is easy to maintain.
#!/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();
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 flagfunction 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.
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.
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:
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:
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:
frontmatter get vs. frontmatter-getEvery 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);
}
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);
}
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);
});
});
# 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
See bun-cli-patterns.md for the complete, detailed reference:
See bun-cli-patterns.md § Para Obsidian CLI Review:
Use Para Obsidian CLI as a template for:
#!/usr/bin/env bun at the top#!/usr/bin/env bunTip 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
}
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)
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.