Examples
Interactive CLI
Using prompts, spinners, and rich interactions
Interactive CLI Example
Build engaging CLI experiences with prompts, progress indicators, and rich user interactions.
Complete Interactive CLI
// setup-wizard.ts
import { createCLI, defineCommand } from '@bunli/core'
const cli = createCLI({
name: 'setup-wizard',
version: '1.0.0',
description: 'Interactive project setup wizard'
})
cli.command(
defineCommand({
name: 'init',
description: 'Initialize a new project',
handler: async ({ prompt, spinner, colors }) => {
console.log(colors.blue('🚀 Welcome to the Project Setup Wizard!\n'))
// Step 1: Basic Information
const projectName = await prompt('What is your project name?', {
default: 'my-awesome-project',
validate: (value) => {
if (!/^[a-z0-9-]+$/.test(value)) {
return 'Project name can only contain lowercase letters, numbers, and dashes'
}
return true
}
})
const description = await prompt('Project description:', {
default: 'A fantastic new project'
})
const author = await prompt('Author name:', {
default: process.env.USER || 'Anonymous'
})
// Step 2: Project Type Selection
const projectType = await prompt.select('What type of project is this?', {
choices: [
{ value: 'web', label: '🌐 Web Application' },
{ value: 'api', label: '🔌 REST API' },
{ value: 'cli', label: '⌨️ CLI Tool' },
{ value: 'lib', label: '📚 Library' },
{ value: 'other', label: '🔧 Other' }
]
})
// Step 3: Technology Stack
let framework = null
if (projectType === 'web') {
framework = await prompt.select('Choose a web framework:', {
choices: [
{ value: 'next', label: 'Next.js', hint: 'Full-stack React framework' },
{ value: 'remix', label: 'Remix', hint: 'Modern full-stack framework' },
{ value: 'astro', label: 'Astro', hint: 'Content-focused framework' },
{ value: 'vite', label: 'Vite', hint: 'Fast build tool' }
]
})
} else if (projectType === 'api') {
framework = await prompt.select('Choose an API framework:', {
choices: [
{ value: 'hono', label: 'Hono', hint: 'Ultrafast web framework' },
{ value: 'elysia', label: 'Elysia', hint: 'Ergonomic web framework' },
{ value: 'express', label: 'Express', hint: 'Minimalist web framework' }
]
})
}
// Step 4: Features Selection
const features = await prompt.multiselect('Select features to include:', {
choices: [
{ value: 'typescript', label: '📘 TypeScript', selected: true },
{ value: 'eslint', label: '🔍 ESLint', selected: true },
{ value: 'prettier', label: '✨ Prettier', selected: true },
{ value: 'testing', label: '🧪 Testing (Vitest)', selected: false },
{ value: 'git', label: '📦 Git repository', selected: true },
{ value: 'docker', label: '🐳 Docker', selected: false },
{ value: 'ci', label: '🔄 CI/CD (GitHub Actions)', selected: false }
],
min: 1
})
// Step 5: Database Selection (if applicable)
let database = null
if (['web', 'api'].includes(projectType)) {
const useDatabase = await prompt.confirm('Do you need a database?', {
default: true
})
if (useDatabase) {
database = await prompt.select('Choose a database:', {
choices: [
{ value: 'sqlite', label: 'SQLite', hint: 'Embedded database' },
{ value: 'postgres', label: 'PostgreSQL', hint: 'Advanced SQL database' },
{ value: 'mysql', label: 'MySQL', hint: 'Popular SQL database' },
{ value: 'mongodb', label: 'MongoDB', hint: 'NoSQL database' },
{ value: 'redis', label: 'Redis', hint: 'In-memory data store' }
]
})
}
}
// Step 6: Package Manager
const packageManager = await prompt.select('Preferred package manager:', {
choices: [
{ value: 'bun', label: 'Bun', hint: 'Fast all-in-one toolkit' },
{ value: 'npm', label: 'npm', hint: 'Default Node.js package manager' },
{ value: 'pnpm', label: 'pnpm', hint: 'Fast, disk space efficient' },
{ value: 'yarn', label: 'Yarn', hint: 'Fast, reliable, and secure' }
]
})
// Step 7: Additional Options
const license = await prompt.select('Choose a license:', {
choices: [
{ value: 'MIT', label: 'MIT', hint: 'Permissive license' },
{ value: 'Apache-2.0', label: 'Apache 2.0', hint: 'Permissive with patent protection' },
{ value: 'GPL-3.0', label: 'GPL v3', hint: 'Copyleft license' },
{ value: 'BSD-3-Clause', label: 'BSD 3-Clause', hint: 'Permissive license' },
{ value: 'UNLICENSED', label: 'Proprietary', hint: 'All rights reserved' }
]
})
// Step 8: Confirmation
console.log('\n' + colors.blue('📋 Configuration Summary:'))
console.log(colors.dim('─'.repeat(50)))
console.log(` Project: ${colors.bold(projectName)}`)
console.log(` Description: ${description}`)
console.log(` Author: ${author}`)
console.log(` Type: ${projectType}`)
if (framework) console.log(` Framework: ${framework}`)
console.log(` Features: ${features.join(', ')}`)
if (database) console.log(` Database: ${database}`)
console.log(` Package Manager: ${packageManager}`)
console.log(` License: ${license}`)
console.log(colors.dim('─'.repeat(50)))
const proceed = await prompt.confirm('\nProceed with project setup?', {
default: true
})
if (!proceed) {
console.log(colors.yellow('\n⚠️ Setup cancelled'))
process.exit(0)
}
// Step 9: Project Creation
console.log()
const setupSpinner = spinner('Creating project structure...')
setupSpinner.start()
// Simulate project setup steps
const steps = [
'Creating directory structure',
'Initializing package.json',
'Installing dependencies',
'Setting up TypeScript',
'Configuring linters',
'Creating initial files',
'Setting up git repository'
]
for (const step of steps) {
setupSpinner.update(step + '...')
await new Promise(resolve => setTimeout(resolve, 800))
}
setupSpinner.succeed('Project setup complete!')
// Step 10: Next Steps
console.log('\n' + colors.green('✨ Your project is ready!'))
console.log('\nNext steps:')
console.log(colors.dim(` cd ${projectName}`))
console.log(colors.dim(` ${packageManager} ${packageManager === 'npm' ? 'run' : ''} dev`))
console.log('\nHappy coding! 🎉')
}
})
)
await cli.run()
Interactive Components
Progress Indicators
// progress-example.ts
export default defineCommand({
name: 'download',
description: 'Download files with progress',
handler: async ({ positional, spinner, colors }) => {
const [url] = positional
if (!url) {
console.log(colors.red('Please provide a URL'))
return
}
// Simple spinner
const spin = spinner('Connecting...')
spin.start()
await new Promise(resolve => setTimeout(resolve, 1000))
spin.update('Downloading...')
// Simulate download progress
const totalSize = 1024 * 1024 * 50 // 50MB
let downloaded = 0
const interval = setInterval(() => {
downloaded += 1024 * 1024 * 5 // 5MB per tick
const percent = Math.min(100, Math.round((downloaded / totalSize) * 100))
spin.update(`Downloading... ${percent}%`)
if (percent >= 100) {
clearInterval(interval)
spin.succeed('Download complete!')
}
}, 200)
}
})
Multi-Step Forms
// form-example.ts
export default defineCommand({
name: 'register',
description: 'User registration wizard',
handler: async ({ prompt, colors }) => {
console.log(colors.blue('👤 User Registration\n'))
// Personal Information
const personalInfo = {}
personalInfo.firstName = await prompt('First name:', {
validate: (value) => value.length > 0 || 'First name is required'
})
personalInfo.lastName = await prompt('Last name:', {
validate: (value) => value.length > 0 || 'Last name is required'
})
personalInfo.email = await prompt('Email address:', {
validate: (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(value) || 'Please enter a valid email'
}
})
// Account Security
console.log('\n' + colors.blue('🔐 Account Security'))
let password, confirmPassword
do {
password = await prompt.password('Choose a password:', {
mask: '●',
validate: (value) => {
if (value.length < 8) return 'Password must be at least 8 characters'
if (!/[A-Z]/.test(value)) return 'Password must contain an uppercase letter'
if (!/[0-9]/.test(value)) return 'Password must contain a number'
return true
}
})
confirmPassword = await prompt.password('Confirm password:', {
mask: '●'
})
if (password !== confirmPassword) {
console.log(colors.red('Passwords do not match. Please try again.'))
}
} while (password !== confirmPassword)
// Preferences
console.log('\n' + colors.blue('⚙️ Preferences'))
const preferences = {}
preferences.newsletter = await prompt.confirm(
'Subscribe to newsletter?',
{ default: true }
)
preferences.theme = await prompt.select('Preferred theme:', {
choices: [
{ value: 'light', label: '☀️ Light' },
{ value: 'dark', label: '🌙 Dark' },
{ value: 'auto', label: '🔄 Auto (system)' }
]
})
preferences.language = await prompt.select('Preferred language:', {
choices: [
{ value: 'en', label: '🇬🇧 English' },
{ value: 'es', label: '🇪🇸 Spanish' },
{ value: 'fr', label: '🇫🇷 French' },
{ value: 'de', label: '🇩🇪 German' },
{ value: 'ja', label: '🇯🇵 Japanese' }
]
})
// Terms and Conditions
const acceptTerms = await prompt.confirm(
'Do you accept the terms and conditions?',
{ default: false }
)
if (!acceptTerms) {
console.log(colors.red('\nYou must accept the terms to continue.'))
return
}
// Registration
console.log('\n' + colors.green('✅ Registration successful!'))
console.log(colors.dim(`Welcome, ${personalInfo.firstName}!`))
}
})
Dynamic Menus
// menu-example.ts
export default defineCommand({
name: 'menu',
description: 'Interactive menu system',
handler: async ({ prompt, colors }) => {
let running = true
while (running) {
console.clear()
console.log(colors.blue('📱 Main Menu\n'))
const choice = await prompt.select('What would you like to do?', {
choices: [
{ value: 'profile', label: '👤 View Profile' },
{ value: 'settings', label: '⚙️ Settings' },
{ value: 'help', label: '❓ Help' },
{ value: 'exit', label: '🚪 Exit' }
]
})
switch (choice) {
case 'profile':
await showProfile(prompt, colors)
break
case 'settings':
await showSettings(prompt, colors)
break
case 'help':
await showHelp(colors)
break
case 'exit':
running = false
break
}
if (running && choice !== 'exit') {
await prompt.confirm('\nPress enter to continue...', {
default: true
})
}
}
console.log(colors.green('\nGoodbye! 👋'))
}
})
async function showProfile(prompt: any, colors: any) {
console.log('\n' + colors.blue('👤 User Profile'))
console.log(colors.dim('─'.repeat(30)))
console.log('Name: John Doe')
console.log('Email: john@example.com')
console.log('Member since: 2024')
}
async function showSettings(prompt: any, colors: any) {
console.log('\n' + colors.blue('⚙️ Settings'))
const setting = await prompt.select('Choose a setting:', {
choices: [
{ value: 'notifications', label: '🔔 Notifications' },
{ value: 'privacy', label: '🔒 Privacy' },
{ value: 'appearance', label: '🎨 Appearance' },
{ value: 'back', label: '← Back' }
]
})
if (setting !== 'back') {
console.log(colors.green(`\n✓ ${setting} settings updated`))
}
}
async function showHelp(colors: any) {
console.log('\n' + colors.blue('❓ Help'))
console.log(colors.dim('─'.repeat(30)))
console.log('This is an interactive menu demo.')
console.log('Navigate using arrow keys and enter.')
}
Real-time Updates
// monitor-example.ts
export default defineCommand({
name: 'monitor',
description: 'Real-time system monitor',
handler: async ({ colors }) => {
console.log(colors.blue('📊 System Monitor\n'))
console.log('Press Ctrl+C to exit\n')
// Set up real-time updates
const updateInterval = setInterval(() => {
// Clear previous lines
process.stdout.write('\x1B[3A') // Move up 3 lines
process.stdout.write('\x1B[0J') // Clear from cursor to end
// Display updated stats
const cpu = Math.round(Math.random() * 100)
const memory = Math.round(Math.random() * 100)
const disk = Math.round(Math.random() * 100)
console.log(`CPU: ${getBar(cpu, colors)} ${cpu}%`)
console.log(`Memory: ${getBar(memory, colors)} ${memory}%`)
console.log(`Disk: ${getBar(disk, colors)} ${disk}%`)
}, 1000)
// Handle cleanup
process.on('SIGINT', () => {
clearInterval(updateInterval)
console.log('\n\n' + colors.yellow('Monitor stopped'))
process.exit(0)
})
}
})
function getBar(percent: number, colors: any): string {
const width = 20
const filled = Math.round((percent / 100) * width)
const empty = width - filled
const color = percent > 80 ? colors.red :
percent > 50 ? colors.yellow :
colors.green
return color('█'.repeat(filled)) + colors.dim('░'.repeat(empty))
}
Error Handling in Interactive CLIs
export default defineCommand({
name: 'safe-prompt',
description: 'Graceful error handling',
handler: async ({ prompt, colors }) => {
try {
const input = await prompt('Enter value:')
// Process input...
} catch (error) {
if (error.message === 'Prompt cancelled') {
console.log(colors.yellow('\n✋ Operation cancelled by user'))
process.exit(0)
}
console.error(colors.red('An error occurred:'), error.message)
process.exit(1)
}
}
})
Best Practices
- Clear Visual Hierarchy: Use colors and symbols to guide users
- Provide Defaults: Make it easy to proceed with sensible defaults
- Validate Early: Check input as soon as it's entered
- Show Progress: Keep users informed during long operations
- Allow Cancellation: Let users exit gracefully at any point
- Handle Errors: Provide helpful error messages
React Terminal UI Example
Build rich terminal interfaces using React components:
// dashboard.tsx
import React, { useState, useEffect } from 'react'
import { createCLI } from '@bunli/core'
import { uiPlugin, defineComponentCommand } from '@bunli/plugin-ui'
import { Box, Text, Row, Column } from '@bunli/renderer'
import { Table, ProgressBar, Tabs, Button, Alert } from '@bunli/ui'
// Dashboard component
function Dashboard() {
const [activeTab, setActiveTab] = useState('overview')
const [stats, setStats] = useState({
users: 1234,
revenue: 45678,
growth: 12.5
})
// Simulate real-time updates
useEffect(() => {
const interval = setInterval(() => {
setStats(prev => ({
users: prev.users + Math.floor(Math.random() * 10),
revenue: prev.revenue + Math.floor(Math.random() * 100),
growth: prev.growth + (Math.random() - 0.5)
}))
}, 2000)
return () => clearInterval(interval)
}, [])
return (
<Box padding={2}>
<Text style={{ bold: true, fontSize: 'large', marginBottom: 1 }}>
📊 Business Dashboard
</Text>
<Tabs
tabs={[
{ id: 'overview', label: 'Overview' },
{ id: 'users', label: 'Users' },
{ id: 'revenue', label: 'Revenue' }
]}
activeId={activeTab}
onChange={setActiveTab}
/>
<Box style={{ marginTop: 2 }} minHeight={15}>
{activeTab === 'overview' && (
<Row gap={2}>
<Box flex={1} style={{ border: 'single', padding: 1 }}>
<Text style={{ bold: true, color: 'cyan' }}>Total Users</Text>
<Text style={{ fontSize: 'large' }}>{stats.users.toLocaleString()}</Text>
<Text style={{ color: 'green' }}>↑ {stats.growth.toFixed(1)}%</Text>
</Box>
<Box flex={1} style={{ border: 'single', padding: 1 }}>
<Text style={{ bold: true, color: 'cyan' }}>Revenue</Text>
<Text style={{ fontSize: 'large' }}>${stats.revenue.toLocaleString()}</Text>
<ProgressBar value={stats.revenue / 100000} width={20} />
</Box>
</Row>
)}
{activeTab === 'users' && (
<Table
columns={[
{ key: 'name', label: 'Name', width: 20 },
{ key: 'email', label: 'Email', width: 30 },
{ key: 'status', label: 'Status', width: 15 }
]}
rows={[
{ name: 'John Doe', email: 'john@example.com', status: '🟢 Active' },
{ name: 'Jane Smith', email: 'jane@example.com', status: '🟢 Active' },
{ name: 'Bob Johnson', email: 'bob@example.com', status: '🔴 Inactive' }
]}
/>
)}
{activeTab === 'revenue' && (
<Column gap={1}>
<Text style={{ bold: true }}>Monthly Revenue Trend</Text>
{['Jan', 'Feb', 'Mar', 'Apr', 'May'].map((month, i) => (
<Row key={month} gap={1}>
<Text style={{ width: 5 }}>{month}:</Text>
<ProgressBar
value={(i + 1) * 0.2}
width={30}
color={i === 4 ? 'green' : 'blue'}
/>
</Row>
))}
</Column>
)}
</Box>
<Box style={{ marginTop: 2 }}>
<Button variant="primary" onClick={() => process.exit(0)}>
Exit Dashboard
</Button>
</Box>
</Box>
)
}
// CLI setup
const cli = createCLI({
name: 'my-app',
version: '1.0.0',
plugins: [uiPlugin]
})
cli.command(
defineComponentCommand({
name: 'dashboard',
description: 'View business dashboard',
component: Dashboard
})
)
await cli.run()
Interactive Form with Validation
// user-form.tsx
import React, { useState } from 'react'
import { defineComponentCommand } from '@bunli/plugin-ui'
import { Box, Text, Column } from '@bunli/renderer'
import { Input, Button, SelectList, CheckboxList, Alert, styles } from '@bunli/ui'
function UserForm() {
const [formData, setFormData] = useState({
name: '',
email: '',
role: 'user',
permissions: []
})
const [errors, setErrors] = useState<string[]>([])
const [submitted, setSubmitted] = useState(false)
const validate = () => {
const newErrors = []
if (!formData.name) newErrors.push('Name is required')
if (!formData.email) newErrors.push('Email is required')
if (!formData.email.includes('@')) newErrors.push('Invalid email format')
setErrors(newErrors)
return newErrors.length === 0
}
const handleSubmit = () => {
if (validate()) {
setSubmitted(true)
setTimeout(() => process.exit(0), 2000)
}
}
if (submitted) {
return (
<Box padding={2}>
<Alert type="success" title="User Created!">
Successfully created user: {formData.name}
</Alert>
</Box>
)
}
return (
<Box padding={2} width={60}>
<Text style={styles.title}>Create New User</Text>
{errors.length > 0 && (
<Alert type="error" title="Please fix the following errors:">
{errors.map((error, i) => (
<Text key={i}>• {error}</Text>
))}
</Alert>
)}
<Column gap={2} style={{ marginTop: 2 }}>
<Box>
<Text style={{ marginBottom: 0.5 }}>Name:</Text>
<Input
value={formData.name}
onChange={(name) => setFormData({ ...formData, name })}
placeholder="Enter user name"
autoFocus
/>
</Box>
<Box>
<Text style={{ marginBottom: 0.5 }}>Email:</Text>
<Input
value={formData.email}
onChange={(email) => setFormData({ ...formData, email })}
placeholder="user@example.com"
/>
</Box>
<Box>
<Text style={{ marginBottom: 0.5 }}>Role:</Text>
<SelectList
items={[
{ id: 'user', label: 'User' },
{ id: 'admin', label: 'Administrator' },
{ id: 'moderator', label: 'Moderator' }
]}
selectedId={formData.role}
onSelect={(item) => setFormData({ ...formData, role: item.id })}
/>
</Box>
<Box>
<Text style={{ marginBottom: 0.5 }}>Permissions:</Text>
<CheckboxList
items={[
{ id: 'read', label: 'Read Access' },
{ id: 'write', label: 'Write Access' },
{ id: 'delete', label: 'Delete Access' }
]}
selectedIds={formData.permissions}
onToggle={(id, checked) => {
if (checked) {
setFormData({
...formData,
permissions: [...formData.permissions, id]
})
} else {
setFormData({
...formData,
permissions: formData.permissions.filter(p => p !== id)
})
}
}}
/>
</Box>
<Box style={{ marginTop: 1 }}>
<Button variant="primary" onClick={handleSubmit}>
Create User
</Button>
</Box>
</Column>
</Box>
)
}
export default defineComponentCommand({
name: 'create-user',
description: 'Create a new user interactively',
component: UserForm
})
Best Practices
- Clear Visual Hierarchy: Use colors and symbols to guide users
- Provide Defaults: Make it easy to proceed with sensible defaults
- Validate Early: Check input as soon as it's entered
- Show Progress: Keep users informed during long operations
- Allow Cancellation: Let users exit gracefully at any point
- Handle Errors: Provide helpful error messages
- Use React Patterns: Leverage hooks and component composition
- Test Keyboard Navigation: Ensure Tab/Shift+Tab work properly
Next Steps
- Terminal UI Guide - Deep dive into React UI
- Real-World Example - Complete production example
- Interactive Prompts Guide - Traditional prompts
- @bunli/ui - UI components reference
- @bunli/renderer - Renderer documentation