@bunli/utils
Shared utilities for CLI development
@bunli/utils
Shared utilities for building beautiful, interactive CLI applications. Includes prompts, spinners, colors, and validation helpers.
@bunli/utils has zero dependencies and provides a custom ANSI-based implementation for all features. It respects NO_COLOR and CI environment variables automatically.
Installation
bun add @bunli/utils
npm install @bunli/utils
pnpm add @bunli/utils
When using @bunli/core, these utilities are automatically injected into your command handlers. You typically don't need to import them directly.
Prompts
Interactive prompts for user input with built-in validation.
Text Input
import { prompt } from '@bunli/utils'
// Simple text input
const name = await prompt('What is your name?')
// With default value
const host = await prompt('Host:', {
default: 'localhost'
})
// With validation
const email = await prompt('Email:', {
validate: (value) => {
if (!value.includes('@')) {
return 'Please enter a valid email'
}
}
})
// With schema validation
import { z } from 'zod'
const url = await prompt('URL:', {
schema: z.string().url()
})
Confirmation
// Simple yes/no
const proceed = await prompt.confirm('Continue?')
// With default
const install = await prompt.confirm('Install dependencies?', {
default: true
})
Selection
// Simple select
const color = await prompt.select('Choose a color:', {
options: ['red', 'green', 'blue']
})
// With labels and hints
const env = await prompt.select('Select environment:', {
options: [
{ value: 'dev', label: 'Development', hint: 'Local development' },
{ value: 'staging', label: 'Staging', hint: 'Pre-production' },
{ value: 'prod', label: 'Production', hint: 'Live environment' }
],
default: 'dev'
})
Multi-select is not yet implemented in the current version of @bunli/utils. Use multiple confirm prompts or select prompts as a workaround.
Password Input
// Basic password (input is masked)
const password = await prompt.password('Enter password:')
// With validation
const securePassword = await prompt.password('Password:', {
validate: (value) => {
if (value.length < 8) {
return 'Password must be at least 8 characters'
}
}
})
// With schema validation
const apiKey = await prompt.password('API Key:', {
schema: z.string().min(32).regex(/^[A-Za-z0-9-_]+$/)
})
Spinners
Beautiful progress indicators for long-running operations.
Basic Usage
import { spinner } from '@bunli/utils'
const spin = spinner('Loading...')
spin.start()
// Update message
spin.update('Processing files...')
// Stop with success
spin.succeed('Done!')
// Stop with failure
spin.fail('Failed to process')
// Stop with warning
spin.warn('Completed with warnings')
// Just stop
spin.stop()
Spinner Patterns
// With async operation
const spin = spinner('Downloading...')
spin.start()
try {
await downloadFile()
spin.succeed('Download complete')
} catch (error) {
spin.fail(`Download failed: ${error.message}`)
}
// Multiple steps
async function deploy() {
const spin = spinner('Deploying application...')
spin.start()
spin.update('Building...')
await build()
spin.update('Uploading...')
await upload()
spin.update('Restarting services...')
await restart()
spin.succeed('Deployment complete!')
}
// Info and warning states
spin.info('Configuration loaded')
spin.warn('Using default settings')
Colors
Terminal colors with automatic detection and fallback.
Basic Colors
import { colors } from '@bunli/utils'
// Basic colors
console.log(colors.red('Error!'))
console.log(colors.green('Success!'))
console.log(colors.yellow('Warning!'))
console.log(colors.blue('Info'))
console.log(colors.magenta('Debug'))
console.log(colors.cyan('Note'))
console.log(colors.gray('Disabled'))
// Bright colors
console.log(colors.brightRed('Critical error!'))
console.log(colors.brightGreen('Great success!'))
console.log(colors.brightYellow('Important warning!'))
Text Styles
// Modifiers
console.log(colors.bold('Bold text'))
console.log(colors.dim('Dimmed text'))
console.log(colors.italic('Italic text'))
console.log(colors.underline('Underlined'))
console.log(colors.strikethrough('Strikethrough'))
// Strip ANSI codes
const colored = colors.red('Error!')
const plain = colors.strip(colored) // 'Error!'
Background Colors
console.log(colors.bgRed('Error background'))
console.log(colors.bgGreen('Success background'))
console.log(colors.bgYellow.black('Warning'))
Color Detection
// Colors automatically disabled when:
// - Not in TTY environment (process.stdout.isTTY === false)
// - NO_COLOR environment variable is set
// - CI environment detected
// The colors object always works, but returns plain text when disabled
const message = colors.red('Error!') // Returns plain 'Error!' if colors disabled
Validation
Schema validation helpers that work with any Standard Schema v1 library.
validate
import { validate } from '@bunli/utils'
import { z } from 'zod'
const schema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().positive()
})
// Throws SchemaError on validation failure
try {
const data = await validate(schema, input)
// data is fully typed
} catch (error) {
if (error instanceof SchemaError) {
// Handle validation errors
for (const issue of error.issues) {
console.error(`${issue.path}: ${issue.message}`)
}
}
}
validateFields
import { validateFields } from '@bunli/utils'
import { z } from 'zod'
// Validate multiple fields at once
const schemas = {
name: z.string().min(1),
email: z.string().email(),
port: z.number().int().positive()
}
const result = await validateFields(schemas, {
name: 'John',
email: 'invalid-email',
port: -1
})
if ('errors' in result) {
// result.errors is Record<string, string[]>
console.error('Validation failed:')
for (const [field, errors] of Object.entries(result.errors)) {
console.error(` ${field}: ${errors.join(', ')}`)
}
} else {
// result is fully typed with validated values
console.log(result.name) // string
console.log(result.port) // number
}
Validation in Prompts
// Schema validation in prompts
const port = await prompt('Port:', {
schema: z.coerce.number().min(1).max(65535),
default: '3000'
})
// The value is automatically parsed and validated
// port is typed as number
// Password with schema validation
const apiKey = await prompt.password('API Key:', {
schema: z.string()
.min(32, 'API key must be at least 32 characters')
.regex(/^[A-Za-z0-9-_]+$/, 'Invalid characters in API key')
})
The formatting utilities (tables, lists, boxes) and utility functions (clear, sleep, exit) shown below are planned features and not yet implemented in the current version.
Best Practices
-
Handle Ctrl+C gracefully:
// Prompts automatically handle Ctrl+C and exit the process // The spinner cleans up on process exit process.on('SIGINT', () => { console.log('\nOperation cancelled') process.exit(0) })
-
Use appropriate spinner states:
// ✅ Good spin.succeed('Build complete') spin.fail('Build failed') spin.warn('Build completed with warnings') // ❌ Avoid spin.stop() // User doesn't know if it succeeded
-
Provide defaults for better UX:
const host = await prompt('Host:', { default: 'localhost' }) const port = await prompt('Port:', { default: '3000' })
-
Use colors consistently:
const log = { error: (msg: string) => console.error(colors.red(msg)), success: (msg: string) => console.log(colors.green(msg)), warning: (msg: string) => console.warn(colors.yellow(msg)), info: (msg: string) => console.log(colors.blue(msg)) }
-
Schema validation in prompts:
// Prefer schema validation over custom validate functions // ✅ Good - automatic retry on validation failure const email = await prompt('Email:', { schema: z.string().email() }) // ❌ Avoid - requires manual retry logic const email = await prompt('Email:', { validate: (val) => val.includes('@') || 'Invalid email' })
All utilities respect NO_COLOR and CI environment variables, automatically adjusting their behavior for different environments.