Examples
Hello World CLI
A minimal TUI-based CLI with Bunli
Complete Example
This example demonstrates a TUI-based CLI using React components for rendering.
// cli.ts
#!/usr/bin/env bun
import { createCLI } from '@bunli/core'
import greetCommand from './commands/greet.js'
const cli = await createCLI()
cli.command(greetCommand)
await cli.run()// commands/greet.tsx
import { defineCommand, option } from '@bunli/core'
import { useRuntime } from '@bunli/runtime/app'
import { ProgressBar, useKeyboard } from '@bunli/tui'
import { useEffect, useState } from 'react'
import { z } from 'zod'
function GreetProgress({
name,
loud,
times
}: {
name: string
loud: boolean
times: number
}) {
const [progress, setProgress] = useState(0)
const runtime = useRuntime()
const closeTui = () => {
runtime.exit()
}
useKeyboard((key) => {
if (key.name === 'q' || key.name === 'escape') {
closeTui()
}
})
useEffect(() => {
const interval = setInterval(() => {
setProgress((current) => {
if (current >= 100) return 100
return current + 5
})
}, 80)
return () => clearInterval(interval)
}, [])
useEffect(() => {
if (progress < 100) return
const timeout = setTimeout(() => closeTui(), 400)
return () => clearTimeout(timeout)
}, [progress, runtime])
const greeting = `Hello, ${name}!`
const message = loud ? greeting.toUpperCase() : greeting
return (
<ProgressBar
value={progress}
label={`${message} x${times} (press q to quit, auto-exits at 100%)`}
color={loud ? '#f97316' : '#22c55e'}
/>
)
}
const greetCommand = defineCommand({
name: 'greet' as const,
description: 'A minimal greeting CLI',
options: {
name: option(
z.string().default('world'),
{ short: 'n', description: 'Who to greet' }
),
loud: option(
z.coerce.boolean().default(false),
{ short: 'l', description: 'Shout the greeting' }
),
times: option(
z.coerce.number().int().positive().default(1),
{ short: 't', description: 'Number of times to greet' }
)
},
render: ({ flags }) => (
<GreetProgress
name={String(flags.name)}
loud={Boolean(flags.loud)}
times={Number(flags.times)}
/>
),
handler: async ({ flags, colors }) => {
const greeting = `Hello, ${flags.name}!`
const message = flags.loud ? greeting.toUpperCase() : greeting
for (let i = 0; i < flags.times; i++) {
console.log(colors.cyan(message))
}
}
})
export default greetCommandThis example demonstrates:
render:property for TUI rendering with React componentshandler:property for non-interactive CLI mode@bunli/runtime/appfor runtime integration@bunli/tuicomponents (ProgressBar,useKeyboard)- Type-safe options with short flags
Running the CLI
# Navigate to the example
cd examples/hello-world
# Install dependencies
bun install
# Run in development mode (uses bunli dev)
bun run dev
# Run greet command
bun run dev -- greet --name Alice
# Or run directly
bun cli.ts greet --name AliceProject Structure
hello-world/
├── cli.ts # Main CLI entry point
├── commands/
│ └── greet.tsx # Greet command with TUI render
├── bunli.config.ts # Build configuration
├── package.json # Dependencies and scripts
├── tsconfig.json # TypeScript configuration
└── README.md # Example documentationKey Features Demonstrated
1. TUI Rendering with React
Commands can use the render: property to display interactive TUI views:
render: ({ flags }) => (
<GreetProgress
name={String(flags.name)}
loud={Boolean(flags.loud)}
times={Number(flags.times)}
/>
)2. Dual Handler/Render Mode
A command can provide both render: (for TUI mode) and handler: (for non-interactive mode):
const greetCommand = defineCommand({
name: 'greet' as const,
description: 'A minimal greeting CLI',
render: ({ flags }) => <TuiView flags={flags} />,
handler: async ({ flags, colors }) => {
// Non-interactive fallback
console.log(colors.green(`Hello, ${flags.name}!`))
}
})3. Type-Safe Options
options: {
name: option(
z.string().default('world'),
{ short: 'n', description: 'Who to greet' }
),
loud: option(
z.coerce.boolean().default(false),
{ short: 'l', description: 'Shout the greeting' }
),
times: option(
z.coerce.number().int().positive().default(1),
{ short: 't', description: 'Number of times to greet' }
)
}4. Using TUI Components
import { ProgressBar, useKeyboard } from '@bunli/tui'
import { useRuntime } from '@bunli/runtime/app'
// Progress bar with keyboard handling
function GreetProgress({ name, loud, times }) {
const runtime = useRuntime()
useKeyboard((key) => {
if (key.name === 'q') runtime.exit()
})
return (
<ProgressBar
value={progress}
label={`Hello, ${name}! x${times}`}
color={loud ? '#f97316' : '#22c55e'}
/>
)
}Package.json Setup
{
"name": "@bunli-examples/hello-world",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "bunli dev",
"build": "bunli build",
"cli": "bun cli.ts"
},
"dependencies": {
"@bunli/core": "workspace:^",
"@bunli/runtime": "workspace:^",
"@bunli/tui": "workspace:^",
"react": "catalog:",
"zod": "catalog:"
}
}Key Takeaways
- TUI Commands: Use
render:property for interactive terminal UI - Dual Mode: Commands can work in both TUI and non-interactive modes
- Runtime Integration: Access runtime features via
useRuntime()hook - TUI Components: Build UI with
@bunli/tuicomponents - Type Safety: Full TypeScript support with Zod validation
- Production Ready: Can be compiled to standalone binary
Next Steps
- Task Runner Example - Learn validation and interactivity
- Interactive Prompts Guide - Learn about prompt primitives
- Getting Started Guide - Step-by-step tutorial
- API Reference - Complete API documentation