Examples
Multi-Command CLI
CLI with multiple commands and subcommands
Multi-Command CLI Example
Build a CLI with multiple commands, subcommands, and shared functionality.
Project Structure
task-cli/
├── src/
│ ├── index.ts
│ ├── commands/
│ │ ├── add.ts
│ │ ├── list.ts
│ │ ├── complete.ts
│ │ └── project/
│ │ ├── index.ts
│ │ ├── create.ts
│ │ └── delete.ts
│ └── lib/
│ └── database.ts
├── package.json
└── tsconfig.json
Main CLI Entry
// src/index.ts
import { createCLI } from '@bunli/core'
const cli = createCLI({
name: 'task',
version: '1.0.0',
description: 'Task management CLI',
commands: {
// Auto-discover commands in this directory
directory: './commands'
}
})
await cli.run()
Individual Commands
Add Command
// src/commands/add.ts
import { defineCommand, option } from '@bunli/core'
import { z } from 'zod'
import { db } from '../lib/database'
export default defineCommand({
name: 'add',
description: 'Add a new task',
alias: 'a',
options: {
task: option(
z.string().min(1),
{
description: 'Task description',
short: 't'
}
),
project: option(
z.string().optional(),
{
description: 'Project name',
short: 'p'
}
),
priority: option(
z.enum(['low', 'medium', 'high']).default('medium'),
{
description: 'Task priority',
short: 'r'
}
),
due: option(
z.string().optional(),
{
description: 'Due date (YYYY-MM-DD)',
short: 'd'
}
)
},
handler: async ({ flags, colors }) => {
const task = await db.tasks.create({
description: flags.task,
project: flags.project,
priority: flags.priority,
due: flags.due ? new Date(flags.due) : null
})
console.log(colors.green('✓'), 'Task added:', colors.bold(`#${task.id}`))
console.log(colors.dim(` ${task.description}`))
}
})
List Command
// src/commands/list.ts
import { defineCommand, option } from '@bunli/core'
import { z } from 'zod'
import { db } from '../lib/database'
export default defineCommand({
name: 'list',
description: 'List all tasks',
alias: ['ls', 'l'],
options: {
project: option(
z.string().optional(),
{
description: 'Filter by project',
short: 'p'
}
),
status: option(
z.enum(['all', 'pending', 'completed']).default('pending'),
{
description: 'Filter by status',
short: 's'
}
),
priority: option(
z.enum(['all', 'low', 'medium', 'high']).default('all'),
{
description: 'Filter by priority',
short: 'r'
}
),
sort: option(
z.enum(['created', 'due', 'priority']).default('created'),
{
description: 'Sort order',
short: 'o'
}
)
},
handler: async ({ flags, colors, spinner }) => {
const spin = spinner('Loading tasks...')
spin.start()
const tasks = await db.tasks.list({
project: flags.project,
status: flags.status,
priority: flags.priority !== 'all' ? flags.priority : undefined,
sort: flags.sort
})
spin.stop()
if (tasks.length === 0) {
console.log(colors.yellow('No tasks found'))
return
}
console.log(`\nTasks (${tasks.length}):\n`)
for (const task of tasks) {
const checkbox = task.completed
? colors.green('✓')
: colors.gray('○')
const priority = {
low: colors.blue('●'),
medium: colors.yellow('●'),
high: colors.red('●')
}[task.priority]
const due = task.due
? colors.dim(` due ${formatDate(task.due)}`)
: ''
console.log(
`${checkbox} ${priority} ${task.description}${due}`
)
if (task.project) {
console.log(colors.dim(` Project: ${task.project}`))
}
}
}
})
function formatDate(date: Date): string {
return date.toLocaleDateString()
}
Complete Command
// src/commands/complete.ts
import { defineCommand, option } from '@bunli/core'
import { z } from 'zod'
import { db } from '../lib/database'
export default defineCommand({
name: 'complete',
description: 'Mark tasks as completed',
alias: ['done', 'c'],
options: {
id: option(
z.coerce.number().optional(),
{
description: 'Task ID',
short: 'i'
}
),
all: option(
z.boolean().default(false),
{
description: 'Complete all tasks',
short: 'a'
}
)
},
handler: async ({ flags, prompt, colors, positional }) => {
let taskIds: number[] = []
// Get task IDs from various sources
if (flags.all) {
const confirm = await prompt.confirm(
'Complete ALL pending tasks?',
{ default: false }
)
if (!confirm) return
const tasks = await db.tasks.list({ status: 'pending' })
taskIds = tasks.map(t => t.id)
} else if (flags.id) {
taskIds = [flags.id]
} else if (positional.length > 0) {
// Accept IDs as positional arguments
taskIds = positional.map(id => parseInt(id, 10))
} else {
// Interactive selection
const tasks = await db.tasks.list({ status: 'pending' })
if (tasks.length === 0) {
console.log(colors.yellow('No pending tasks'))
return
}
const selected = await prompt.multiselect(
'Select tasks to complete:',
{
choices: tasks.map(task => ({
value: task.id,
label: task.description,
hint: task.project
}))
}
)
taskIds = selected
}
// Complete the tasks
for (const id of taskIds) {
await db.tasks.complete(id)
const task = await db.tasks.get(id)
console.log(colors.green('✓'), `Completed: ${task.description}`)
}
console.log(colors.green(`\n${taskIds.length} task(s) completed!`))
}
})
Nested Commands
Project Group
// src/commands/project/index.ts
import { defineCommand } from '@bunli/core'
export default defineCommand({
name: 'project',
description: 'Manage projects',
alias: 'p',
// No handler - just a group for subcommands
})
Project Create
// src/commands/project/create.ts
import { defineCommand, option } from '@bunli/core'
import { z } from 'zod'
import { db } from '../../lib/database'
export default defineCommand({
name: 'create',
description: 'Create a new project',
options: {
name: option(
z.string().min(1),
{
description: 'Project name',
short: 'n'
}
),
description: option(
z.string().optional(),
{
description: 'Project description',
short: 'd'
}
),
color: option(
z.enum(['red', 'blue', 'green', 'yellow', 'purple']).optional(),
{
description: 'Project color',
short: 'c'
}
)
},
handler: async ({ flags, colors }) => {
const project = await db.projects.create({
name: flags.name,
description: flags.description,
color: flags.color
})
console.log(colors.green('✓'), 'Project created:', colors.bold(project.name))
if (project.description) {
console.log(colors.dim(` ${project.description}`))
}
}
})
Project Delete
// src/commands/project/delete.ts
import { defineCommand, option } from '@bunli/core'
import { z } from 'zod'
import { db } from '../../lib/database'
export default defineCommand({
name: 'delete',
description: 'Delete a project',
options: {
name: option(
z.string(),
{
description: 'Project name',
short: 'n'
}
),
force: option(
z.boolean().default(false),
{
description: 'Skip confirmation',
short: 'f'
}
)
},
handler: async ({ flags, prompt, colors }) => {
const project = await db.projects.get(flags.name)
if (!project) {
console.log(colors.red('Project not found:', flags.name))
return
}
const taskCount = await db.tasks.count({ project: project.name })
if (!flags.force) {
const message = taskCount > 0
? `Delete project "${project.name}" and its ${taskCount} tasks?`
: `Delete project "${project.name}"?`
const confirm = await prompt.confirm(message, { default: false })
if (!confirm) return
}
await db.projects.delete(project.name)
console.log(colors.red('✗'), `Deleted project: ${project.name}`)
if (taskCount > 0) {
console.log(colors.dim(` Also deleted ${taskCount} tasks`))
}
}
})
Database Module
// src/lib/database.ts
interface Task {
id: number
description: string
project?: string
priority: 'low' | 'medium' | 'high'
completed: boolean
due?: Date
created: Date
}
interface Project {
name: string
description?: string
color?: string
}
// Simple in-memory database for example
class Database {
private tasks: Task[] = []
private projects: Project[] = []
private nextId = 1
tasks = {
create: async (data: Omit<Task, 'id' | 'completed' | 'created'>) => {
const task: Task = {
id: this.nextId++,
completed: false,
created: new Date(),
...data
}
this.tasks.push(task)
return task
},
list: async (filters: any = {}) => {
let results = [...this.tasks]
if (filters.project) {
results = results.filter(t => t.project === filters.project)
}
if (filters.status === 'pending') {
results = results.filter(t => !t.completed)
} else if (filters.status === 'completed') {
results = results.filter(t => t.completed)
}
if (filters.priority) {
results = results.filter(t => t.priority === filters.priority)
}
// Sort
results.sort((a, b) => {
switch (filters.sort) {
case 'due':
return (a.due?.getTime() ?? Infinity) - (b.due?.getTime() ?? Infinity)
case 'priority':
const order = { high: 0, medium: 1, low: 2 }
return order[a.priority] - order[b.priority]
default:
return b.created.getTime() - a.created.getTime()
}
})
return results
},
get: async (id: number) => {
return this.tasks.find(t => t.id === id)!
},
complete: async (id: number) => {
const task = this.tasks.find(t => t.id === id)
if (task) task.completed = true
},
count: async (filters: any = {}) => {
return (await this.tasks.list(filters)).length
}
}
projects = {
create: async (data: Project) => {
this.projects.push(data)
return data
},
get: async (name: string) => {
return this.projects.find(p => p.name === name)
},
delete: async (name: string) => {
this.projects = this.projects.filter(p => p.name !== name)
this.tasks = this.tasks.filter(t => t.project !== name)
}
}
}
export const db = new Database()
Usage Examples
# Add tasks
task add --task "Write documentation" --priority high
task add -t "Review PR" -p "website" -r medium -d 2024-12-20
# List tasks
task list
task list --project website
task list --status all --sort priority
# Complete tasks
task complete --id 1
task complete 1 2 3 # Multiple IDs
task complete # Interactive selection
# Project management
task project create --name website --color blue
task project create -n api -d "Backend API project"
task project delete --name old-project
# Using aliases
task a -t "Quick task" # add
task ls -p website # list
task c 5 # complete
task p create -n mobile # project create
Command Discovery
Bunli automatically discovers commands in the configured directory:
commands/
├── add.ts → task add
├── list.ts → task list
├── complete.ts → task complete
└── project/
├── index.ts → task project
├── create.ts → task project create
└── delete.ts → task project delete
Help Output
task --help
# task v1.0.0
# Task management CLI
#
# Commands:
# add (a) Add a new task
# list (ls, l) List all tasks
# complete (done, c) Mark tasks as completed
# project (p) Manage projects
#
# Run 'task <command> --help' for command details
task add --help
# Add a new task
#
# Usage: task add [options]
#
# Options:
# -t, --task <string> Task description
# -p, --project <string> Project name
# -r, --priority <priority> Task priority (low|medium|high) [default: medium]
# -d, --due <date> Due date (YYYY-MM-DD)
Key Features Demonstrated
- Command Organization: Logical grouping of related commands
- Aliases: Short versions for frequently used commands
- Options: Rich option handling with validation
- Interactive Mode: Fallback to prompts when options not provided
- Subcommands: Nested command structure for complex CLIs
- Shared Code: Database module used across commands
- Auto-discovery: Commands automatically found and loaded
Next Steps
- Interactive Example - Rich interactive features
- Testing Guide - Test multi-command CLIs
- Command Documentation - Deep dive into commands