Bunli Skill
Build type-safe CLIs with Bun. See references/ for detailed documentation.
Quick Start
# Create new CLI
bunli init my-cli
# Or manually:
mkdir my-cli && cd my-cli
npm init -y
bun add @bunli/core @bunli/utils @bunli/tui
bun add -d bunli typescript
Then create your entry point:
// cli.ts
import { createCLI } from "@bunli/core"
import { defineCommand, option } from "@bunli/core"
import { z } from "zod"
const hello = defineCommand({
name: "hello",
description: "Print a greeting",
options: {
name: option(z.string().default("World"), { short: "n", description: "Name to greet" }),
count: option(z.coerce.number().default(1), { short: "c", description: "Number of times" })
},
handler: ({ flags, signal }) => {
if (signal.aborted) return
for (let i = 0; i < flags.count; i++) {
console.log(`Hello, ${flags.name}!`)
}
}
})
const cli = await createCLI({
name: "my-cli",
version: "0.1.0",
})
cli.command(hello)
await cli.run()
Run with: bun run cli.ts
Package Versions
Find current versions:
npm view @bunli/core # Latest version
npm view @bunli/utils # Latest version
npm view @bunli/tui # Latest version
npm view bunli # CLI version
Quick Decision Trees
"I need to define a command"
Define Command?
├─ Basic command → defineCommand({ name: "mycmd", description: "...", handler: ({ flags }) => {...} })
├─ With options → Add options: { options: { debug: option(z.coerce.boolean(), { short: "d" }) } }
├─ Nested commands → Use defineGroup({ name: "group", description: "...", commands: [...] })
├─ With alias → Add alias: "m" for "mycmd"
└─ With TUI → Add render (optionally keep handler; render commands run without TUI flags)
"I need to add options"
Add Options?
├─ Boolean flag → option(z.coerce.boolean(), { short: "d", description: "Debug output" })
├─ String input → option(z.string(), { description: "Name" })
├─ Number input → option(z.coerce.number(), { description: "Port" })
├─ Enum/choice → option(z.enum(["dev", "prod"]), { description: "Environment" })
├─ With default → option(z.string().default("default"))
└─ Required → option(z.string().min(1))
Important: Use
z.coerce.number()andz.coerce.boolean()for numeric and boolean flags because CLI args are strings. Enums should usez.enum(...).
-vis reserved globally for--version, so avoidshort: "v"for command-local options.
"I need to create a plugin"
Create Plugin?
├─ Direct plugin → createPlugin({ name: "my-plugin", setup(ctx) {...} })
├─ Factory → createPlugin((options) => ({ name: "my-plugin", ... }))
├─ With store → Add store: { counter: 0 }, access via context.store
└─ Lifecycle hooks → setup, configResolved, beforeCommand, afterCommand
"I need to use prompts"
Use Prompt?
├─ Text input → prompt("Enter name")
├─ Yes/No → confirm("Continue?")
├─ Single choice → select("Select framework", { options: [...] })
├─ Multiple choice → multiselect("Select tools", { options: [...] })
└─ Password → password("Enter password")
"I keep hitting validation errors"
Validation Errors?
├─ Numbers not working → Use z.coerce.number() instead of z.number()
├─ Booleans not working → Use z.coerce.boolean(); pass --flag=true/false for explicit values
├─ Enums not working → Use z.enum([...]), not z.coerce.enum(...)
├─ createCLI returns Promise → Use await createCLI() or .then()
└─ Commands not registering → Use cli.command(cmd), not commands: [cmd]
"I need to build for production"
Build?
├─ Development → bunli dev (hot reload)
├─ Production build → bunli build
├─ Cross-compile → bunli build --targets darwin-arm64,linux-x64
├─ With bytecode → bunli build --bytecode
├─ Generate types → bunli generate
└─ Release → bunli release
"I need advanced TUI (beyond basic forms)"
Advanced TUI?
├─ Custom components beyond Bunli's built-in form/layout set → Use opentui skill
├─ Advanced animations (timeline, keyframes) → Use opentui skill
├─ Full Flexbox layout control → Use opentui skill
├─ Custom renderables → Use opentui skill
└─ Just need Bunli components/forms/charts → Use @bunli/tui (covered here)
"I need predictable TUI buffer behavior"
Buffer Mode?
├─ Global default policy → standard buffer mode
├─ Use fullscreen alternate buffer → set tui.renderer.bufferMode = "alternate" in defineConfig
├─ Per-command override → set command.tui.renderer.bufferMode
└─ Keep standard-buffer output behavior → set bufferMode = "standard" explicitly
Product Index
Core
| Topic | Reference |
|---|---|
| Command definition | references/core/commands.md |
| Type safety patterns | references/core/types.md |
Plugin System
| Topic | Reference |
|---|---|
| Plugin architecture | references/plugin/system.md |
| Built-in plugins | references/plugin/built-ins.md |
CLI Commands
| Topic | Reference |
|---|---|
| CLI commands | references/cli/commands.md |
| Project scaffolding | references/cli/scaffold.md |
Utilities
| Topic | Reference |
|---|---|
| Prompts | references/utils/prompts.md |
| Spinners | references/utils/spinner.md |
| Colors | references/utils/colors.md |
TUI
| Topic | Reference |
|---|---|
| Components | references/tui/components.md |
| OpenTUI (advanced) | Use opentui skill |
OpenTUI Integration
| Topic | Reference |
|---|---|
| When to use OpenTUI | See "Relationship with OpenTUI" below |
Core Concepts
CLI Structure
// my-cli/src/commands/hello.ts
import { defineCommand, option } from "@bunli/core"
import { z } from "zod"
export const hello = defineCommand({
name: "hello",
description: "Greet someone",
options: {
name: option(z.string().default("World"), {
short: "n",
description: "Name to greet"
})
},
handler: ({ flags }) => {
console.log(`Hello, ${flags.name}!`)
}
})
Plugin Creation
import { createPlugin } from "@bunli/core/plugin"
const myPlugin = createPlugin({
name: "my-plugin",
store: { count: 0 },
setup(context) {
context.registerCommand(myCommand)
},
beforeCommand(ctx) {
ctx.store.count++
}
})
Using Prompts
handler: async ({ prompt }) => {
const name = await prompt("What is your name?")
const proceed = await prompt.confirm("Continue?")
const framework = await prompt.select("Choose framework", {
options: [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Svelte", value: "svelte" }
]
})
}
Key Packages
| Package | Purpose |
|---|---|
@bunli/core | CLI framework (defineCommand, option, createCLI) |
@bunli/utils | Colors and validation utilities |
@bunli/runtime/prompt | Prompt and spinner APIs |
@bunli/tui | Terminal UI components |
bunli | CLI for building CLIs |
create-bunli | Project scaffolding |
@bunli/generator | Type generation from commands |
Built-in Plugins
@bunli/plugin-ai-detect- Detects AI coding assistants@bunli/plugin-completions- Shell completions (bash, zsh, fish)@bunli/plugin-config- Config loading from multiple sources@bunli/plugin-mcp- MCP tools to CLI commands
Relationship with OpenTUI
Bunli uses OpenTUI as its terminal rendering engine. Understanding when to use each:
Use Bunli (this skill) for:
- Building CLI applications with commands and options
- Plugin architecture (auth, config, completions)
- Type-safe CLI with Zod validation
- Interactive prompts via
@bunli/runtime/prompt - Bunli TUI components (
Form,SchemaForm,DataTable,ProgressBar, and more) - Publishing CLI to npm
Use OpenTUI skill when:
- Building standalone terminal applications (not a CLI with subcommands)
- Need advanced animation with timeline, keyframes, easing functions
- Want full Flexbox/Yoga layout control
- Creating custom renderable components
- Need low-level buffer/terminal control
- Building terminal games or complex interactive UIs
How They Connect
// Bunli TUI uses OpenTUI under the hood
import { Form, SchemaForm } from "@bunli/tui" // Bunli's React components
// prompt is provided via handler args by Bunli // Prompt + spinner runtime
import { useTimeline } from "@bunli/tui" // Re-exported from opentui
// Drop down to OpenTUI for advanced control
import { h, Box, Text, instantiate } from "@opentui/core"
import { createCliRenderer } from "@opentui/core"
Package relationship:
@bunli/tuiwraps@opentui/react- Bunli auto-wires the OpenTUI renderer runtime for
rendercommands @bunli/runtime/promptprovides prompt + spinner APIs used in handlers- Bunli TUI hooks (
useKeyboard,useTimeline) are re-exported from OpenTUI - Renderer options map to OpenTUI renderer settings
When to combine:
- Start with Bunli for CLI structure
- Use
@bunli/tuifor common patterns (forms, wizards) - Drop to OpenTUI when you need custom components, advanced animation, or full control