npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@boba-cli/dsl

v1.0.0-alpha.4

Published

Declarative DSL for building CLI applications with minimal ceremony

Readme

@boba-cli/dsl

The main API for building Boba CLI applications. A declarative DSL for building terminal UIs with minimal ceremony using a fluent builder API and view primitives inspired by SwiftUI.

Install

pnpm add @boba-cli/dsl

Quick Start

import { createApp, spinner, vstack, hstack, text, Style } from '@boba-cli/dsl'

const app = createApp()
  .state({ message: 'Loading something amazing...' })
  .component('loading', spinner({ style: new Style().foreground('#50fa7b') }))
  .onKey(['q', 'ctrl+c'], ({ quit }) => quit())
  .view(({ state, components }) =>
    vstack(
      text('🧋 My App').bold().foreground('#ff79c6'),
      spacer(),
      hstack(components.loading, text('  ' + state.message)),
      spacer(),
      text('Press [q] to quit').dim()
    )
  )
  .build()

await app.run()

Why the DSL?

The DSL is the recommended way to build Boba CLI applications. For advanced use cases requiring fine-grained control, you can use the low-level Elm Architecture API directly.

| Aspect | DSL (Main API) | Low-Level TEA | |--------|----------------|---------------| | Lines of code | ~35 lines | ~147 lines | | Boilerplate | Declarative builder, automatic state handling | Manual class, state management, instanceof checks | | Type safety | Phantom types provide compile-time safety | Manual type guards, verbose generics | | View composition | Composable view primitives | String concatenation | | Component integration | Automatic component lifecycle management | Manual model wrapping and message routing |

See examples/spinner (DSL) vs examples/spinner-low-level (low-level) for a real comparison.

API Reference

App Builder

createApp()

Creates a new application builder. Start here to build your CLI app.

const app = createApp()
  .state({ count: 0 })
  .view(({ state }) => text(`Count: ${state.count}`))
  .build()

AppBuilder.state<S>(initial: S)

Sets the initial application state. The state type is inferred from the provided object.

.state({ count: 0, name: 'World' })

AppBuilder.component<K, M>(key: K, builder: ComponentBuilder<M>)

Registers a component with a unique key. The component's rendered view is available in the view function via components[key].

.component('spinner', spinner())
.component('input', textInput())

AppBuilder.onKey(keys: string | string[], handler: KeyHandler)

Registers a key handler. Supports single keys, key arrays, and modifiers.

.onKey('q', ({ quit }) => quit())
.onKey(['up', 'k'], ({ state, update }) => update({ index: state.index - 1 }))
.onKey('ctrl+c', ({ quit }) => quit())

Key handler context:

  • state - Current application state
  • components - Current component views
  • update(patch) - Merge partial state (shallow merge)
  • setState(newState) - Replace entire state
  • quit() - Gracefully quit the application

AppBuilder.view(fn: ViewFunction)

Sets the view function. Called on every render cycle. Returns a view node tree describing the UI.

.view(({ state, components }) =>
  vstack(
    text('Hello ' + state.name),
    components.spinner
  )
)

AppBuilder.build()

Finalizes the builder chain and creates an App ready to run.

const app = builder.build()
await app.run()

View Primitives

text(content: string): TextNode

Creates a text node with chainable style methods.

text('Hello').bold().foreground('#ff79c6')
text('Warning').dim().italic()
text('Success').background('#282a36')

Style methods:

  • bold() - Apply bold styling
  • dim() - Apply dim styling
  • italic() - Apply italic styling
  • foreground(color) - Set foreground color (hex or named)
  • background(color) - Set background color (hex or named)

vstack(...children: ViewNode[]): LayoutNode

Arranges child views vertically with newlines between them.

vstack(
  text('Line 1'),
  text('Line 2'),
  text('Line 3')
)

hstack(...children: ViewNode[]): LayoutNode

Arranges child views horizontally on the same line.

hstack(
  text('Left'),
  text(' | '),
  text('Right')
)

spacer(height?: number): string

Creates empty vertical space. Default height is 1 line.

vstack(
  text('Header'),
  spacer(2),
  text('Content')
)

divider(char?: string, width?: number): string

Creates a horizontal divider line. Default is 40 '─' characters.

vstack(
  text('Section 1'),
  divider(),
  text('Section 2'),
  divider('=', 50)
)

Conditional Helpers

when(condition: boolean, node: ViewNode): ViewNode

Conditionally renders a node. Returns empty string if condition is false.

vstack(
  text('Always visible'),
  when(state.showHelp, text('Help text'))
)

choose(condition: boolean, ifTrue: ViewNode, ifFalse: ViewNode): ViewNode

Chooses between two nodes based on a condition.

choose(
  state.isLoading,
  text('Loading...').dim(),
  text('Ready!').bold()
)

map<T>(items: T[], render: (item: T, index: number) => ViewNode): ViewNode[]

Maps an array of items to view nodes. Spread the result into a layout.

vstack(
  ...map(state.items, (item, index) =>
    text(`${index + 1}. ${item.name}`)
  )
)

Component Builders

The DSL provides 17 component builders for common CLI patterns. All components integrate seamlessly with the App builder pattern and provide declarative configuration.

code(options: CodeBuilderOptions): ComponentBuilder<CodeModel>

Displays syntax-highlighted source code from files with scrolling support.

import { NodeFileSystemAdapter, NodePathAdapter } from '@boba-cli/machine/node'

.component('viewer', code({
  filesystem: new NodeFileSystemAdapter(),
  path: new NodePathAdapter(),
  active: true,
  theme: 'dracula',
  width: 80,
  height: 24
}))

Options:

  • filesystem - Filesystem adapter (required, use NodeFileSystemAdapter for Node.js)
  • path - Path adapter (required, use NodePathAdapter for Node.js)
  • active - Whether component receives keyboard input (default: false)
  • theme - Syntax theme like "dracula", "monokai", "github-light" (default: "dracula")
  • width - Viewer width in characters (default: 0)
  • height - Viewer height in lines (default: 0)

filepicker(options: FilepickerBuilderOptions): ComponentBuilder<FilepickerModel>

Interactive file system browser with selection support.

import { NodeFileSystemAdapter, NodePathAdapter } from '@boba-cli/machine/node'

.component('picker', filepicker({
  filesystem: new NodeFileSystemAdapter(),
  path: new NodePathAdapter(),
  currentPath: process.cwd(),
  height: 15,
  width: 60
}))

Options:

  • filesystem - Filesystem adapter (required)
  • path - Path adapter (required)
  • currentPath - Initial directory path
  • height - Picker height in lines
  • width - Picker width in characters
  • Additional styling and behavior options available

filetree(options: FiletreeBuilderOptions): ComponentBuilder<FiletreeModel>

Directory tree viewer with expandable folders.

.component('tree', filetree({
  root: { name: 'src', isDirectory: true, children: [...] }
}))

Options:

  • root - Root directory item (required, must be a DirectoryItem)
  • showFiles - Whether to show files or only directories (default: true)
  • Custom styling options available

help(options?: HelpBuilderOptions): ComponentBuilder<HelpModel>

Full-screen key binding help display.

.component('help', help({
  keyBindings: [
    { key: 'q', description: 'Quit application' },
    { key: '↑/↓', description: 'Navigate list' }
  ],
  showHelp: true
}))

Options:

  • keyBindings - Array of key binding descriptions
  • showHelp - Whether help is visible (default: false)
  • Custom styling options available

helpBubble(options?: HelpBubbleBuilderOptions): ComponentBuilder<HelpBubbleModel>

Compact inline help bubble with keyboard shortcuts.

.component('shortcuts', helpBubble({
  entries: [
    { key: 'enter', description: 'Select' },
    { key: 'q', description: 'Quit' }
  ]
}))

Options:

  • entries - Array of shortcut entries with key and description
  • Custom styling options available

list<T>(options: ListBuilderOptions<T>): ComponentBuilder<ListModel<T>>

Filterable, paginated list with keyboard navigation. Items must implement the Item interface.

import { list, type Item } from '@boba-cli/dsl'

interface TodoItem extends Item {
  filterValue: () => string
  title: () => string
  description: () => string
}

const items: TodoItem[] = [
  { filterValue: () => 'Buy milk', title: () => 'Buy milk', description: () => 'From the store' }
]

.component('todos', list({ items, title: 'Tasks', height: 20 }))

Options:

  • items - Array of items implementing Item interface (required)
  • title - List title
  • height - List height in lines
  • width - List width in characters
  • showTitle, showFilter, showPagination, showHelp, showStatusBar - Toggle UI elements
  • filteringEnabled - Enable/disable filtering (default: true)
  • styles - Custom styles for list components
  • keyMap - Custom key bindings
  • delegate - Custom item rendering

markdown(options?: MarkdownBuilderOptions): ComponentBuilder<MarkdownModel>

Renders markdown with syntax highlighting.

.component('docs', markdown({
  content: '# Hello\n\nThis is **markdown**.',
  width: 80
}))

Options:

  • content - Markdown content to render
  • width - Rendering width in characters
  • Custom styling options available

paginator(options?: PaginatorBuilderOptions): ComponentBuilder<PaginatorModel>

Dot-style page indicator (e.g., ● ○ ○).

.component('pages', paginator({
  totalPages: 5,
  currentPage: 0
}))

Options:

  • totalPages - Total number of pages (default: 3)
  • currentPage - Current page index (default: 0)
  • Custom styling options available

progress(options?: ProgressBuilderOptions): ComponentBuilder<ProgressModel>

Animated progress bar with gradient support and spring physics.

.component('progress', progress({
  width: 40,
  gradient: {
    start: '#5A56E0',
    end: '#EE6FF8',
    scaleGradientToProgress: false
  },
  showPercentage: true,
  spring: {
    frequency: 18,
    damping: 1
  }
}))

Options:

  • width - Progress bar width in characters (default: 40)
  • full - Character for filled portion (default: '█')
  • empty - Character for empty portion (default: '░')
  • fullColor - Color for filled portion (default: '#7571F9')
  • emptyColor - Color for empty portion (default: '#606060')
  • showPercentage - Display percentage value (default: true)
  • percentFormat - Printf-style format string (default: ' %3.0f%%')
  • gradient - Gradient configuration with start, end, scaleGradientToProgress
  • spring - Spring physics with frequency, damping
  • percentageStyle - Style for percentage text

spinner(options?: SpinnerBuilderOptions): ComponentBuilder<SpinnerModel>

Animated loading spinner.

.component('loading', spinner({
  spinner: dot,
  style: new Style().foreground('#50fa7b')
}))

Options:

  • spinner - Animation to use (default: line). Available: line, dot, miniDot, pulse, points, moon, meter, ellipsis
  • style - Style for rendering (default: unstyled)

Re-exported spinners:

import { line, dot, miniDot, pulse, points, moon, meter, ellipsis } from '@boba-cli/dsl'

statusBar(options?: StatusBarBuilderOptions): ComponentBuilder<StatusBarModel>

Multi-column status bar for displaying key-value pairs.

.component('status', statusBar({
  columns: [
    { key: 'mode', value: 'NORMAL' },
    { key: 'line', value: '42' }
  ]
}))

Options:

  • columns - Array of column definitions with key and value
  • Custom styling options available

stopwatch(options?: StopwatchBuilderOptions): ComponentBuilder<StopwatchModel>

Displays elapsed time since start.

.component('elapsed', stopwatch({
  format: 'mm:ss.SSS',
  running: true
}))

Options:

  • format - Time format string (default: 'mm:ss.SSS')
  • running - Whether stopwatch is running (default: false)
  • Custom styling options available

table(options?: TableBuilderOptions): ComponentBuilder<TableModel>

Scrollable data table with headers and rows.

.component('data', table({
  headers: ['Name', 'Age', 'City'],
  rows: [
    ['Alice', '30', 'NYC'],
    ['Bob', '25', 'SF']
  ],
  height: 10
}))

Options:

  • headers - Column headers
  • rows - Data rows
  • height - Table height in lines
  • width - Table width in characters
  • Custom styling and scrolling options available

textArea(options?: TextAreaBuilderOptions): ComponentBuilder<TextAreaModel>

Multi-line text editor with scrolling and editing.

.component('editor', textArea({
  value: 'Initial text',
  width: 80,
  height: 20,
  active: true
}))

Options:

  • value - Initial text content
  • width - Editor width in characters
  • height - Editor height in lines
  • active - Whether editor receives keyboard input (default: false)
  • Custom styling options available

textInput(options?: TextInputBuilderOptions): ComponentBuilder<TextInputModel>

Single-line text input with validation, placeholders, and echo modes.

.component('input', textInput({
  placeholder: 'Enter your name...',
  value: '',
  active: true,
  validate: (value) => value.length > 0 ? null : 'Required'
}))

Options:

  • value - Initial input value
  • placeholder - Placeholder text
  • active - Whether input receives keyboard input (default: false)
  • validate - Validation function returning error message or null
  • echoMode - Input display mode (normal, password, none)
  • cursorMode - Cursor style
  • Custom styling options available

Re-exported types:

import { EchoMode, CursorMode, type ValidateFunc } from '@boba-cli/dsl'

timer(options?: TimerBuilderOptions): ComponentBuilder<TimerModel>

Countdown timer display.

.component('countdown', timer({
  duration: 60000, // 60 seconds in milliseconds
  format: 'mm:ss',
  running: true
}))

Options:

  • duration - Total duration in milliseconds
  • format - Time format string (default: 'mm:ss')
  • running - Whether timer is running (default: false)
  • Custom styling options available

viewport(options?: ViewportBuilderOptions): ComponentBuilder<ViewportModel>

Scrollable content viewport for displaying large content.

.component('content', viewport({
  content: longTextContent,
  width: 80,
  height: 20,
  active: true
}))

Options:

  • content - Content to display (string or array of strings)
  • width - Viewport width in characters
  • height - Viewport height in lines
  • active - Whether viewport receives keyboard input for scrolling (default: false)
  • Custom styling options available

Re-exported Types

For convenience, the DSL re-exports commonly used types from underlying packages:

// From @boba-cli/chapstick
import { Style } from '@boba-cli/dsl'

// From @boba-cli/spinner
import { type Spinner, line, dot, miniDot, pulse, points, moon, meter, ellipsis } from '@boba-cli/dsl'

// From @boba-cli/textinput
import { type TextInputModel, EchoMode, CursorMode, type ValidateFunc } from '@boba-cli/dsl'

// From @boba-cli/list
import { type Item } from '@boba-cli/dsl'

// From @boba-cli/filetree
import { type DirectoryItem } from '@boba-cli/dsl'

// From @boba-cli/help-bubble
import { type Entry } from '@boba-cli/dsl'

Type Safety

The DSL uses phantom types to provide compile-time guarantees about application structure:

State Type Safety

const app = createApp()
  .state({ count: 0 })
  .view(({ state }) => text(`Count: ${state.count}`))
  //                                    ^^^^^ TypeScript knows this is number

Component Type Safety

const app = createApp()
  .component('spinner', spinner())
  .view(({ components }) => components.spinner)
  //                        ^^^^^^^^^^^^^^^^^^ ComponentView

If you try to access a component that doesn't exist, TypeScript will error:

.view(({ components }) => components.doesNotExist)
//                                   ^^^^^^^^^^^^ Error: Property 'doesNotExist' does not exist

Builder Chain Validation

The builder enforces that view() is called before build():

createApp()
  .state({ count: 0 })
  .build()
// Error: AppBuilder: view() must be called before build()

Advanced Usage

Accessing the Low-Level TEA Model

For advanced use cases, you can access the generated TEA model:

const app = createApp()
  .state({ count: 0 })
  .view(({ state }) => text(`Count: ${state.count}`))
  .build()

const model = app.getModel()
// model is a TEA Model<Msg> instance

Custom Component Builders

You can create custom component builders by implementing the ComponentBuilder interface:

import type { ComponentBuilder } from '@boba-cli/dsl'
import type { Cmd, Msg } from '@boba-cli/tea'

interface MyComponentModel {
  value: number
}

const myComponent = (): ComponentBuilder<MyComponentModel> => ({
  init() {
    return [{ value: 0 }, null]
  },
  update(model, msg) {
    // Handle messages
    return [model, null]
  },
  view(model) {
    return `Value: ${model.value}`
  }
})

// Use it
createApp()
  .component('custom', myComponent())
  .view(({ components }) => components.custom)

Examples

Counter with State Updates

import { createApp, vstack, hstack, text } from '@boba-cli/dsl'

const app = createApp()
  .state({ count: 0 })
  .onKey('up', ({ state, update }) => update({ count: state.count + 1 }))
  .onKey('down', ({ state, update }) => update({ count: state.count - 1 }))
  .onKey('q', ({ quit }) => quit())
  .view(({ state }) =>
    vstack(
      text('Counter').bold(),
      spacer(),
      text(`Count: ${state.count}`),
      spacer(),
      text('[↑/↓] adjust • [q] quit').dim()
    )
  )
  .build()

await app.run()

Todo List with Conditional Rendering

const app = createApp()
  .state({
    items: ['Buy milk', 'Write docs', 'Build CLI'],
    selected: 0
  })
  .onKey('up', ({ state, update }) =>
    update({ selected: Math.max(0, state.selected - 1) })
  )
  .onKey('down', ({ state, update }) =>
    update({ selected: Math.min(state.items.length - 1, state.selected + 1) })
  )
  .onKey('q', ({ quit }) => quit())
  .view(({ state }) =>
    vstack(
      text('Todo List').bold(),
      divider(),
      ...map(state.items, (item, index) =>
        choose(
          index === state.selected,
          text(`> ${item}`).foreground('#50fa7b'),
          text(`  ${item}`)
        )
      ),
      divider(),
      text('[↑/↓] navigate • [q] quit').dim()
    )
  )
  .build()

await app.run()

Multiple Components

import { createApp, spinner, vstack, hstack, text, Style, dot, pulse } from '@boba-cli/dsl'

const app = createApp()
  .state({ status: 'Initializing...' })
  .component('spinner1', spinner({ spinner: dot, style: new Style().foreground('#50fa7b') }))
  .component('spinner2', spinner({ spinner: pulse, style: new Style().foreground('#ff79c6') }))
  .onKey('q', ({ quit }) => quit())
  .view(({ state, components }) =>
    vstack(
      text('Multi-Spinner Demo').bold(),
      spacer(),
      hstack(components.spinner1, text('  Loading data...')),
      hstack(components.spinner2, text('  Processing...')),
      spacer(),
      text(`Status: ${state.status}`).dim(),
      spacer(),
      text('[q] quit').dim()
    )
  )
  .build()

await app.run()

Scripts

  • pnpm -C packages/dsl build
  • pnpm -C packages/dsl test
  • pnpm -C packages/dsl lint
  • pnpm -C packages/dsl generate:api-report

License

MIT