Bunli
Packages

@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

  1. 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)
    })
  2. 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
  3. Provide defaults for better UX:

    const host = await prompt('Host:', { default: 'localhost' })
    const port = await prompt('Port:', { default: '3000' })
  4. 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))
    }
  5. 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.