Bunli
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 greetCommand

This example demonstrates:

  • render: property for TUI rendering with React components
  • handler: property for non-interactive CLI mode
  • @bunli/runtime/app for runtime integration
  • @bunli/tui components (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 Alice

Project 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 documentation

Key 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

  1. TUI Commands: Use render: property for interactive terminal UI
  2. Dual Mode: Commands can work in both TUI and non-interactive modes
  3. Runtime Integration: Access runtime features via useRuntime() hook
  4. TUI Components: Build UI with @bunli/tui components
  5. Type Safety: Full TypeScript support with Zod validation
  6. Production Ready: Can be compiled to standalone binary

Next Steps

On this page