Building Your First CLI
Complete walkthrough of building a CLI with Bunli
This guide walks you through building your first CLI application with Bunli.
Prerequisites
- Bun installed (v1.0 or later)
- Basic TypeScript knowledge
- A terminal/command line
Creating a New Project
Start by creating a new Bunli project:
bunx create-bunli todo-cli
cd todo-cliThis creates a new project with:
- TypeScript configuration
- Bunli dependencies
- Example command structure
- Development scripts
Project Structure
Your new project has this structure:
todo-cli/
├── cli.ts # CLI entry point
├── commands/ # Command definitions
│ └── hello.ts # Example command
├── .bunli/ # Generated files (auto-created)
│ └── commands.gen.ts # Generated TypeScript types
├── package.json
├── tsconfig.json
├── bunli.config.ts # Bunli configuration
└── README.mdYour First Command
Let's create a simple todo list CLI. Replace the hello command with a new add command:
// src/commands/add.ts
import { defineCommand, option } from "@bunli/core";
import { z } from "zod";
export default defineCommand({
name: "add" as const,
description: "Add a new todo item",
options: {
task: option(z.string().min(1), { description: "Task description" }),
priority: option(z.enum(["low", "medium", "high"]).default("medium"), {
short: "p",
description: "Task priority",
}),
due: option(z.string().optional(), { short: "d", description: "Due date" }),
},
handler: async ({ flags, colors }) => {
console.log(colors.green("✓"), "Added task:", flags.task);
console.log(colors.dim(`Priority: ${flags.priority}`));
if (flags.due) {
console.log(colors.dim(`Due: ${flags.due}`));
}
},
});Setting Up the CLI
Update your main CLI file:
// src/index.ts
import { createCLI } from "@bunli/core";
import add from "./commands/add.js";
const cli = await createCLI({
name: "todo",
version: "1.0.0",
description: "Simple todo list manager",
});
cli.command(add);
await cli.run();Running in Development
Start the development server:
bunli devNow test your command:
./src/index.ts add --task "Write documentation" --priority highAdding More Commands
Let's add a list command:
// src/commands/list.ts
import { defineCommand, option } from "@bunli/core";
import { z } from "zod";
export default defineCommand({
name: "list" as const,
description: "List all todos",
alias: "ls",
options: {
filter: option(z.enum(["all", "pending", "completed"]).default("all"), {
short: "f",
description: "Filter tasks",
}),
sort: option(z.enum(["priority", "due", "created"]).default("created"), {
short: "s",
description: "Sort order",
}),
},
handler: async ({ flags, colors, spinner }) => {
const spin = spinner("Loading tasks...");
spin.start();
// Simulate loading
await new Promise((resolve) => setTimeout(resolve, 500));
const tasks = [
{ id: 1, task: "Write documentation", priority: "high", completed: false },
{ id: 2, task: "Add tests", priority: "medium", completed: false },
{ id: 3, task: "Review PR", priority: "low", completed: true },
];
spin.succeed("Tasks loaded");
const filtered = tasks.filter((task) => {
if (flags.filter === "pending") return !task.completed;
if (flags.filter === "completed") return task.completed;
return true;
});
console.log("\nYour tasks:\n");
filtered.forEach((task) => {
const status = task.completed ? colors.green("✓") : colors.yellow("○");
const priority = colors.dim(`[${task.priority}]`);
console.log(`${status} ${task.task} ${priority}`);
});
},
});Interactive Commands
Add an interactive complete command:
// src/commands/complete.ts
import { defineCommand } from "@bunli/core";
export default defineCommand({
name: "complete" as const,
description: "Mark a task as completed",
handler: async ({ prompt, colors }) => {
const tasks = [
{ id: 1, task: "Write documentation", completed: false },
{ id: 2, task: "Add tests", completed: false },
{ id: 3, task: "Review PR", completed: false },
];
const pendingTasks = tasks.filter((t) => !t.completed);
if (pendingTasks.length === 0) {
console.log(colors.yellow("No pending tasks!"));
return;
}
const selected = await prompt.select("Which task did you complete?", {
options: pendingTasks.map((task) => ({
value: task.id,
label: task.task,
})),
});
console.log(colors.green("✓"), "Marked as complete!");
},
});Nested Commands
Create a group of database commands:
// src/commands/db.ts
import { defineCommand } from "@bunli/core";
export default defineCommand({
name: "db" as const,
description: "Database operations",
commands: [
defineCommand({
name: "init" as const,
description: "Initialize database",
handler: async ({ colors }) => {
console.log(colors.green("✓"), "Database initialized");
},
}),
defineCommand({
name: "backup" as const,
description: "Backup database",
handler: async ({ spinner }) => {
const spin = spinner("Creating backup...");
spin.start();
await new Promise((resolve) => setTimeout(resolve, 2000));
spin.succeed("Backup created: backup-2024-01-15.db");
},
}),
],
});Building for Production
Build your CLI for distribution:
# Build for current platform
bunli build
# Build for all platforms
bunli build --targets all
# Build a standalone executable for your platform
bunli build --targets nativeAdding Tests
Create a test for your command:
// src/commands/add.test.ts
import { test, expect } from "bun:test";
import { testCommand, createTestCLI } from "@bunli/test";
import add from "./add";
test("add command creates a task", async () => {
const cli = await createTestCLI({ commands: [add] });
const result = await cli.run(["add", "--task", "Test task"]);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain("Added task: Test task");
});Run tests:
bunli testAdding Plugins
Enhance your CLI with plugins. Let's add configuration loading and AI detection:
// src/index.ts
import { createCLI } from "@bunli/core";
import { configMergerPlugin } from "@bunli/plugin-config";
import { aiAgentPlugin } from "@bunli/plugin-ai-detect";
const cli = await createCLI({
name: "todo",
version: "1.0.0",
description: "Simple todo list manager",
plugins: [
// Load config from .todorc.json or ~/.config/todo/config.json
configMergerPlugin(),
// Detect if running in AI assistant
aiAgentPlugin({ verbose: true }),
],
});
await cli.run();Now your commands can use plugin features:
// src/commands/list.ts
handler: async ({ flags, colors, agent }) => {
// Provide structured output for AI agents
if (agent) {
console.log(JSON.stringify({ tasks }, null, 2));
} else {
// Human-friendly output
tasks.forEach((task) => {
console.log(`${colors.green("✓")} ${task.name}`);
});
}
};Type Generation
Enable type generation for enhanced developer experience:
// bunli.config.ts
import { defineConfig } from "@bunli/core";
export default defineConfig({
name: "todo",
commands: {
directory: "./src/commands",
},
build: {
entry: "./src/index.ts",
outdir: "./dist",
targets: ["darwin-arm64", "linux-x64", "windows-x64"],
},
});Now you can use generated types for advanced patterns:
// Use generated types
import { getCommandApi, listCommands } from "./.bunli/commands.gen";
// Get command metadata
const commands = listCommands();
console.log(commands); // [{ name: 'add', description: 'Add a new todo item' }]
// Type-safe command access
const addApi = getCommandApi("add");
console.log(addApi.options); // { task: {...}, priority: {...}, due: {...} }Type generation provides autocomplete, type safety, and enables advanced patterns like CLI wrappers and documentation generation.
Configuration
Customize your CLI behavior with bunli.config.ts:
import { defineConfig } from "@bunli/core";
export default defineConfig({
name: "todo",
version: "1.0.0",
commands: {
directory: "./src/commands",
},
build: {
entry: "./src/index.ts",
outdir: "./dist",
targets: ["darwin-arm64", "linux-x64", "windows-x64"],
},
});Distribution
When you're ready to share your CLI:
-
Build for all platforms:
bunli build --targets all -
Create a release:
bunli release -
Publish to npm:
npm publish
Next Steps
You've built a functional CLI! Here's what to explore next:
- Type Generation Guide - Learn about code generation
- Schema Validation - Advanced validation
- Interactive Prompts - Rich user interactions
- Testing - Comprehensive testing strategies
- Distribution - Publishing and deployment
Complete Example
Find the complete todo CLI example in the Bunli repository.