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

clifer

v1.11.0

Published

A lightweight, type-safe TypeScript library for building beautiful command-line interfaces with zero dependencies

Downloads

1,436

Readme

Clifer

npm version npm downloads license

A type-safe TypeScript framework for building beautiful command-line interfaces — with a fluent API, interactive prompts, and rich terminal UI powered by Ink and React.

Features

  • Type-Safe — Full TypeScript support with compile-time type checking
  • Fluent API — Chainable, intuitive interface for building CLIs
  • Interactive Prompts — Built-in support for user input, confirmations, and multi-select
  • Rich Terminal UI — Ink-powered React components for beautiful output (cards, tables, spinners, and more)
  • Multi-Format Output — Render as rich (default), plain text, or JSON with a single flag
  • Auto-Generated Help — Beautiful help screens rendered with Ink, no extra configuration
  • Auto-Generated Docs — Markdown documentation generated from your command definitions with --doc
  • Nested Commands — Organize complex CLIs with deeply nested command structures
  • Async Config Loading — Load configuration before argument parsing with .load()
  • Scaffolding CLI — Bootstrap new CLI projects and add commands with npx clifer init

Installation

npm install clifer
# or
yarn add clifer
# or
pnpm add clifer
# or
bun add clifer

Quick Start

import { cli, input, runCli } from 'clifer'

interface Props {
  name: string
  greeting?: string
}

const program = cli<Props>('greet')
  .version('1.0.0')
  .description('A friendly greeting CLI')
  .argument(input('name').description('Your name').string().required())
  .option(input('greeting').description('Custom greeting').string())
  .handle(async ({ name, greeting }) => {
    console.log(`${greeting ?? 'Hello'}, ${name}!`)
  })

runCli(program)
$ greet World
Hello, World!

$ greet World --greeting Hey
Hey, World!

$ greet --help

greet   <name> [--greeting=<string>] [--help] [--doc] [--version]

ARGUMENTS
  name                   Your name

OPTIONS
  --greeting=<string>    Custom greeting

COMMON
  --help                 Show help
  --doc                  Generate documentation
  --version              Show version

Input Types

Clifer supports various input types with full TypeScript inference:

// String
.option(input('name').description('Your name').string())

// Number with default
.option(input('port').description('Port number').number().default(3000))

// Boolean flag
.option(input('force').description('Force operation'))

// Single choice
.option(input('env').string().choices(['dev', 'staging', 'prod']))

// Multi choice (comma-separated via CLI, checkbox prompt interactively)
.option(input('languages').string().choices(['en', 'ml', 'fr']).many())
// CLI: --languages=en,ml  →  ['en', 'ml']

// Required argument
.argument(input('file').string().required())

// Custom validation
.option(input('email').string().validate(value => {
  if (!value.includes('@')) throw new Error('Invalid email')
  return value
}))

Nested Commands

Build complex CLIs with nested command structures:

import { cli, command, input, runCli } from 'clifer'

const addUser = command<{ name: string; email: string }>('add')
  .description('Add a new user')
  .argument(input('name').string().required())
  .argument(input('email').string().required())
  .handle(async ({ name, email }) => {
    console.log(`Adding user: ${name} (${email})`)
  })

const listUsers = command('list')
  .description('List all users')
  .option(input('format').string().choices(['json', 'table']).default('table'))
  .handle(async ({ format }) => {
    console.log(`Listing users in ${format} format`)
  })

const userCommand = command('user')
  .description('User management')
  .command(addUser)
  .command(listUsers)

const program = cli('myapp')
  .version('1.0.0')
  .command(userCommand)

runCli(program)
$ myapp user add "John Doe" [email protected]
$ myapp user list --format json

Help is automatically generated for every level of the command tree:

$ myapp --help

myapp   <user> [--help] [--doc] [--version]

COMMANDS
  user       User management

COMMON
  --help     Show help
  --doc      Generate documentation
  --version  Show version

$ myapp user --help

myapp user   <add|list> [--help] [--doc]

COMMANDS
  add    Add a new user
  list   List all users

COMMON
  --help   Show help
  --doc    Generate documentation

Interactive Prompts

Create interactive CLI experiences with the prompt() function:

import { cli, input, prompt, runCli } from 'clifer'

const program = cli('setup')
  .description('Interactive setup wizard')
  .handle(async () => {
    const config = await prompt(
      input('projectName').prompt('Project name?').string().required(),
      input('description').prompt('Description?').string(),
      input('typescript').prompt('Use TypeScript?').boolean(),
      input('framework')
        .prompt('Choose framework:')
        .string()
        .choices(['express', 'fastify', 'koa']),
    )
    console.log('Configuration:', config)
  })

runCli(program)

You can also attach prompts directly to arguments and options — they'll prompt interactively when the value isn't provided via the command line:

const program = cli('deploy')
  .argument(
    input('environment')
      .string()
      .required()
      .prompt('Which environment?')
      .choices(['dev', 'staging', 'prod']),
  )
  .option(
    input('force')
      .prompt('Skip confirmation?'),
  )
  .handle(async ({ environment }) => {
    console.log(`Deploying to ${environment}...`)
  })

Prompt types are inferred automatically from the input configuration:

| Input Config | Prompt Type | | --------------------------- | -------------- | | .boolean() | Confirm | | .number() | Numeral | | .string().choices([...]) | Autocomplete | | .choices([...]).many() | Multi-select | | .string() | Text input |

Loading Configuration

Use .load() to fetch configuration asynchronously before argument parsing:

import { cli, input, runCli } from 'clifer'
import { readFile } from 'fs/promises'

const program = cli<{ config?: string }>('myapp')
  .option(input('config').description('Config file path').string())
  .load(async (props) => {
    if (props.config) {
      const content = await readFile(props.config, 'utf-8')
      return JSON.parse(content)
    }
    return {}
  })
  .handle(async (props) => {
    console.log('Configuration loaded:', props)
  })

runCli(program)

Custom Help Formatting

Override the default help output with a custom renderer:

const program = cli('myapp')
  .version('1.0.0')
  .description('My application')
  .help(() => {
    return `
Custom Help Message
===================

Usage: myapp [options]

This is a custom help message with your own formatting.

Options:
  --help     Show this help message
  --version  Show version number
    `
  })

Rich Terminal UI

Clifer includes a set of Ink-powered React components for rendering beautiful terminal output.

Components

import {
  Card,
  Message,
  Spinner,
  Heading,
  ErrorBox,
  StatusBadge,
  LabelValue,
  KeyValueTable,
  RichTable,
  renderOnce,
  theme,
} from 'clifer'

Message — Display success, error, info, or warning messages:

renderOnce(<Message type="success">Deployment complete!</Message>)
renderOnce(<Message type="error">Build failed.</Message>)
renderOnce(<Message type="info">Checking for updates...</Message>)
renderOnce(<Message type="warning">Deprecated API detected.</Message>)

Card — Bordered card with an optional title:

renderOnce(
  <Card title="Server Status">
    <LabelValue label="Status" value="Running" />
    <LabelValue label="Port" value="3000" />
    <LabelValue label="Uptime" value="2h 15m" />
  </Card>,
)

Spinner — Animated braille-pattern loading indicator:

renderOnce(<Spinner label="Installing dependencies..." />)

Heading — Bold, primary-colored heading:

renderOnce(<Heading>Deployment Summary</Heading>)

ErrorBox — Error container with cross symbol:

renderOnce(<ErrorBox>Failed to connect to database.</ErrorBox>)

StatusBadge — Inline status indicator with predefined styles:

renderOnce(<StatusBadge label="Build" value="active" />)
// Supported values: active, inactive, archived, completed, error, draft, published

LabelValue — Single label-value pair:

renderOnce(<LabelValue label="Version" value="1.8.0" />)

KeyValueTable — Pretty-print an object as a key-value table:

renderOnce(<KeyValueTable data={{ name: 'myapp', version: '1.0.0', port: 3000 }} />)

RichTable — Advanced table with column priority and pagination:

renderOnce(
  <RichTable
    data={users}
    columns={['name', 'email', 'role']}
  />,
)

Theme

All components use a consistent theme with colors and symbols:

import { theme } from 'clifer'

// Colors
theme.colors.primary    // Blue
theme.colors.secondary  // Cyan
theme.colors.success    // Green
theme.colors.warning    // Yellow
theme.colors.error      // Red
theme.colors.muted      // Gray
theme.colors.label      // Cyan (labels)
theme.colors.value      // White (values)
theme.colors.border     // Gray (borders)
theme.colors.dim        // Dim gray

// Symbols
theme.symbols.bullet    // ●
theme.symbols.dash      // ─
theme.symbols.dot       // ·
theme.symbols.arrow     // →
theme.symbols.check     // ✓
theme.symbols.cross     // ✗
theme.symbols.ellipsis  // …

Multi-Format Output

Add .format() to any command to enable a --format=<default|text|json> option:

| Flag | Format | Use Case | | ----------------- | ------- | --------------------------------- | | (none) | Default | Human-readable with colors & Ink | | --format=text | Plain | Pipe-friendly, no colors | | --format=json | JSON | Machine-readable, structured data | | --doc | Docs | Auto-generated markdown documentation |

Use the renderUI() function to support all three modes with a single call:

import { renderUI } from 'clifer'
import type { FormatProps } from 'clifer'

interface Props extends FormatProps {}

const program = cli<Props>('status')
  .format()  // adds --format=<default|text|json>
  .handle(async (props) => {
    const data = { status: 'running', port: 3000 }
    renderUI(data, props.format, (data) => (
      <Card title="Server Status">
        <LabelValue label="Status" value={data.status} />
        <LabelValue label="Port" value={String(data.port)} />
      </Card>
    ))
  })
$ status                    # Default rich Ink output
$ status --format=text      # Plain text key-value pairs
$ status --format=json      # {"status":"running","port":3000}
$ status --doc              # Markdown documentation

Output Utilities

For more control over output formatting:

import {
  printJson,
  printText,
  printTextList,
  printMarkdown,
  formatAsTable,
  formatAsList,
  stripAnsi,
  getTerminalWidth,
  wrapText,
  renderInline,
} from 'clifer'

// Print structured data as JSON
printJson({ name: 'myapp', version: '1.0.0' })

// Print an object as formatted key-value pairs
printText({ name: 'myapp', version: '1.0.0', port: 3000 })

// Print an array as a formatted table
printTextList(users, ['name', 'email', 'role'])

// Render markdown with syntax-highlighted code blocks
printMarkdown('# Title\n\nSome **bold** text')

// Format data as a markdown table (returns string)
const table = formatAsTable([{ name: 'Alice', role: 'Admin' }])

// Format items as a markdown list table (returns string)
const list = formatAsList(items, ['name', 'value'])

// Strip ANSI escape codes from a string
const plain = stripAnsi(coloredString)

// Get current terminal width
const width = getTerminalWidth()

// Wrap text to a specific width
const wrapped = wrapText(longText, 80)

// Convert **bold** and *italic* markdown to ANSI codes
const styled = renderInline('This is **bold** and *italic*')

Error Handling

Clifer provides two error classes for different scenarios:

import { CliExpectedError, CliError } from 'clifer'

// CliExpectedError — for user-facing errors with clean output
// Displays the error message without a stack trace
throw new CliExpectedError('Invalid input. Expected a valid email address.')

// CliError — for runtime parsing errors (includes command context)
// Used internally by clifer during argument validation

Handle errors gracefully:

const program = cli('deploy')
  .argument(input('environment').string().required())
  .handle(async ({ environment }) => {
    if (!['dev', 'staging', 'prod'].includes(environment)) {
      throw new CliExpectedError(
        `Invalid environment "${environment}". Use: dev, staging, or prod`,
      )
    }
    // Deploy logic...
  })

runCli(program).catch((error) => {
  if (error instanceof CliExpectedError) {
    console.error(`Error: ${error.message}`)
    process.exit(1)
  }
  throw error
})

Built-in Flags

Every command automatically includes these flags:

| Flag | Short | Description | | ----------- | ----- | ------------------------------------------ | | --help | -h | Show auto-generated help screen | | --version | | Show version (when .version() is set) | | --doc | | Generate markdown documentation |

When .format() is used, the following option is also available:

| Flag | Description | | ------------------------------- | ------------------------------------------ | | --format=<default\|text\|json> | Output format (default: rich Ink rendering) |

Help and Documentation

Help screens are automatically generated from your command definitions and rendered with Ink components. They include argument/option types, defaults, required indicators, and descriptions.

$ myapp --help        # Ink-rendered help screen
$ myapp user --help   # Help for a specific subcommand
$ myapp --doc         # Full markdown documentation

The --doc flag generates complete markdown documentation for your entire CLI, including all subcommands:

# myapp

​```sh
myapp   <user> [--help] [--doc] [--version]

COMMANDS

  user       User management

COMMON

  --help     Show help
  --doc      Generate documentation
  --version  Show version
​```

## myapp user add

Add a new user

​```sh
myapp user add   <name> <email> [--help] [--doc]
​```

## myapp user list

List all users

​```sh
myapp user list   [--format=<json|table>] [--help] [--doc]
​```

Help Format Reference

The help output uses these conventions:

| Notation | Meaning | | ----------------- | --------------------------------- | | <name> | Required argument | | [name] | Optional argument | | --flag | Boolean flag | | --opt=<string> | String option | | --opt=<number> | Number option | | --opt=<a\|b\|c> | Choice option | | --opt=<a\|b>,... | Multi-value choice (comma-separated) | | * | Required indicator (shown after option name) |

Programmatic API

You can also use the help and documentation functions programmatically:

import { showCliHelp, showDocumentation, showCliError, toHelp, toDocumentation } from 'clifer'

// Render Ink help screen for a command
showCliHelp(command, parentCommands)

// Generate and print markdown documentation
showDocumentation(command, parentCommands)

// Display a formatted error box
showCliError('Something went wrong', 'myapp deploy')

// Get help as a plain text string
const helpText = toHelp(command, prefix, includeCommonInputs)

// Get documentation as a markdown string array
const docs = toDocumentation(command)

Scaffolding CLI

Clifer includes a scaffolding tool to bootstrap new projects:

# Create a new CLI project
npx clifer init my-cli-app

# Add a new command to an existing project
npx clifer command add my-command

# Add a nested subcommand
npx clifer command add parent/subcommand

# Remove a command
npx clifer command remove my-command

Async Command Handlers

All handlers are async, enabling complex operations:

const program = cli('fetch')
  .argument(input('url').string().required())
  .option(input('timeout').number().default(5000))
  .handle(async ({ url, timeout }) => {
    const controller = new AbortController()
    const timeoutId = setTimeout(() => controller.abort(), timeout)

    try {
      const response = await fetch(url, { signal: controller.signal })
      const data = await response.json()
      console.log(data)
    } catch (error) {
      if (error.name === 'AbortError') {
        throw new CliExpectedError(`Request timed out after ${timeout}ms`)
      }
      throw error
    } finally {
      clearTimeout(timeoutId)
    }
  })

Complete Example

A full TODO CLI demonstrating commands, arguments, options, error handling, and nested structure:

import { cli, command, input, runCli, CliExpectedError } from 'clifer'
import { readFileSync, writeFileSync, existsSync } from 'fs'

interface Todo {
  id: number
  text: string
  done: boolean
}

const TODO_FILE = './todos.json'

const loadTodos = (): Todo[] => {
  if (!existsSync(TODO_FILE)) return []
  return JSON.parse(readFileSync(TODO_FILE, 'utf-8'))
}

const saveTodos = (todos: Todo[]) => {
  writeFileSync(TODO_FILE, JSON.stringify(todos, null, 2))
}

const addCommand = command<{ text: string }>('add')
  .description('Add a new todo')
  .argument(input('text').string().required())
  .handle(({ text }) => {
    const todos = loadTodos()
    todos.push({ id: Date.now(), text, done: false })
    saveTodos(todos)
    console.log(`Added: "${text}"`)
  })

const listCommand = command<{ all?: boolean }>('list')
  .description('List todos')
  .option(input('all').description('Show completed todos'))
  .handle(({ all }) => {
    const todos = loadTodos()
    const filtered = all ? todos : todos.filter((t) => !t.done)

    if (filtered.length === 0) {
      console.log('No todos found.')
      return
    }

    filtered.forEach((todo) => {
      const status = todo.done ? '✓' : '○'
      console.log(`${status} [${todo.id}] ${todo.text}`)
    })
  })

const doneCommand = command<{ id: number }>('done')
  .description('Mark todo as done')
  .argument(input('id').number().required())
  .handle(({ id }) => {
    const todos = loadTodos()
    const todo = todos.find((t) => t.id === id)

    if (!todo) {
      throw new CliExpectedError(`Todo with id ${id} not found`)
    }

    todo.done = true
    saveTodos(todos)
    console.log(`Marked as done: "${todo.text}"`)
  })

const program = cli('todo')
  .version('1.0.0')
  .description('Simple TODO manager')
  .command(addCommand)
  .command(listCommand)
  .command(doneCommand)

runCli(program)
$ todo --help

todo   <add|list|done> [--help] [--doc] [--version]

Simple TODO manager

COMMANDS

  add    Add a new todo
  list   List todos
  done   Mark todo as done

COMMON

  --help      Show help
  --doc       Generate documentation
  --version   Show version

$ todo add "Buy groceries"
Added: "Buy groceries"

$ todo list
○ [1711234567890] Buy groceries

$ todo done 1711234567890
Marked as done: "Buy groceries"

Real-World Example: Interactive Config with Async Loading

This example demonstrates .load() for async configuration, .prompt() for interactive inputs, choices, defaults, and mixed argument/option patterns:

import { readFileSync, writeFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { cli, input, runCli } from 'clifer'

interface Props {
  name?: string
  environment: string
  cloud?: string
  awsAccountId: string
  localPort?: number
}

const ENV_FILE = resolve(__dirname, 'env.json')

const program = cli<Props>('configure')
  .version('1.0.0')
  .description('Configure environment for this project')

  // Positional argument with interactive prompt
  .argument(input('name').description('Project name').string().prompt())

  // Required option with choices and default
  .option(
    input('environment')
      .description('Environment')
      .string()
      .required()
      .choices(['local', 'dev', 'prod'])
      .default('dev')
      .prompt(),
  )

  // Optional option with choices
  .option(
    input('cloud')
      .description('Cloud provider')
      .string()
      .choices(['aws', 'gcloud']),
  )

  // Option with interactive prompt
  .option(input('awsAccountId').description('AWS account id').string().prompt())

  // Number option with choices and prompt
  .option(
    input('localPort')
      .description('Local port')
      .number()
      .choices([4000, 4001, 4002])
      .prompt(),
  )

  // Load existing config before parsing arguments
  .load(async () => {
    try {
      return JSON.parse(readFileSync(ENV_FILE, 'utf-8'))
    } catch {
      return {}
    }
  })

  // Handle the command
  .handle(async (props) => {
    writeFileSync(ENV_FILE, JSON.stringify(props, null, 2), 'utf-8')
    console.log('Configuration saved!')
  })

runCli(program)
$ configure --help

configure   [name] --environment=<local|dev|prod> [--cloud=<aws|gcloud>]
[--aws-account-id=<string>] [--local-port=<4000|4001|4002>] [--help] [--doc]
[--version]

Configure environment for this project

ARGUMENTS

  name                                Project name

OPTIONS

  --environment=<local|dev|prod> *   Environment
  --cloud=<aws|gcloud>               Cloud provider
  --aws-account-id=<string>          AWS account id
  --local-port=<4000|4001|4002>      Local port

COMMON

  --help                              Show help
  --doc                               Generate documentation
  --version                           Show version

# Run with arguments
$ configure myapp --environment=prod --cloud=aws

# Run interactively (prompts for missing values)
$ configure
? Project name? _
? Environment? (dev/local/prod) _
? AWS account id? _
? Local port? (4000/4001/4002) _
Configuration saved!

API Reference

Core Functions

| Function | Description | | ------------------- | -------------------------------------------- | | cli(name) | Create a new CLI program | | command(name) | Create a command or subcommand | | input(name) | Create an input (argument or option) | | runCli(program) | Execute the CLI with process arguments | | prompt(...inputs) | Prompt for multiple inputs interactively |

CLI / Command Builder

| Method | Description | | ------------------- | ------------------------------------------------- | | .description(text) | Set command description | | .version(string) | Set version and enable --version flag | | .argument(input) | Add a positional argument | | .option(input) | Add a named option / flag | | .command(sub) | Add a subcommand | | .load(asyncFn) | Async config loader, runs before argument parsing | | .handle(asyncFn) | Set the command handler | | .help(fn) | Override default help output | | .toCommand() | Convert builder to a Command object |

Input Builder

| Method | Description | | -------------------- | ----------------------------------------------------- | | .string() | Define as string type | | .number() | Define as number type | | .boolean() | Define as boolean type | | .required() | Mark as required | | .default(value) | Set default value | | .choices(array) | Limit to specific choices | | .many() | Allow multiple values (comma-separated or checkboxes) | | .prompt(text?) | Enable interactive prompt when value is missing | | .validate(fn) | Add custom validation | | .description(text) | Set description shown in help | | .toInput() | Convert builder to an Input object |

Output & Rendering

| Function | Description | | ------------------------------- | ------------------------------------------- | | renderUI(data, format, richFn) | Unified renderer (rich/text/json) | | renderOnce(element) | Render an Ink component once and unmount | | printJson(data) | Print data as JSON | | printText(data) | Print object as formatted key-value pairs | | printTextList(items, fields?) | Print array as formatted table | | printMarkdown(content) | Render markdown with syntax highlighting | | formatAsTable(data) | Format array as markdown table (returns string) | | formatAsList(items, fields?) | Format array as markdown list (returns string) | | stripAnsi(str) | Remove ANSI escape codes from a string | | getTerminalWidth() | Get current terminal width | | wrapText(text, width) | Wrap text to a specific width | | renderInline(text) | Convert bold/italic to ANSI codes |

Help & Documentation

| Function | Description | | -------------------------------------------- | ------------------------------------- | | showCliHelp(command, parentCommands?) | Render Ink help screen for a command | | showDocumentation(command, parentCommands?) | Print markdown documentation | | showCliError(message, commandText) | Display a formatted error box | | toHelp(command, prefix?, includeCommon?) | Generate plain text help (returns string) | | toDocumentation(command) | Generate markdown docs (returns string) |

UI Components

| Component | Description | | --------------- | ------------------------------------------------------------------------ | | Card | Bordered card with optional title | | Message | Typed message — success, error, info, warning | | Spinner | Animated braille-pattern loading indicator with optional label | | Heading | Bold, primary-colored heading | | ErrorBox | Error container with cross symbol | | StatusBadge | Status indicator — active, inactive, archived, completed, error, draft, published | | LabelValue | Single label-value pair | | KeyValueTable | Pretty-print an object as key-value table | | RichTable | Advanced table with column priority and pagination |

Utility Functions

| Function | Description | | ----------------- | ---------------------------------------------- | | allInputs(cmd) | Extract all user-defined inputs from a command | | isCommand(obj) | Type guard for Command objects | | isInput(obj) | Type guard for Input objects |

Types & Enums

import type { Command, Input, FormatProps, OutputFormat } from 'clifer'
import { Kind, InputType } from 'clifer'

enum Kind {
  Command,
  Input,
}

enum InputType {
  String,
  Number,
  Boolean,
}

type OutputFormat = 'default' | 'text' | 'json'

Contributing

Contributions are welcome! Please open an issue first to discuss what you would like to change.

License

MIT — see LICENSE for details.

Links