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

cli-atelier

v1.0.2

Published

A complete terminal UI framework for bun and typescript with Zero dependencies (Pure ANSI)

Readme

cli-atelier

A complete terminal UI framework for Bun + TypeScript. Zero dependencies. Pure ANSI.

# Bun
bun add cli-atelier
# or Node / bundlers
npm install cli-atelier
# or
pnpm add cli-atelier
# or
yarn add cli-atelier

Why

Most CLI libraries are heavy, Node-centric, or force you into their patterns. cli-atelier is a zero-dependency, pure-ANSI terminal framework built specifically for Bun and TypeScript. Every module is a named export so you import only what you need.


Requirements

| Runtime | Minimum version | | ------- | --------------- | | Bun | >= 1.1.0 | | Node | >= 18.0.0 |

The "bun" export condition ships raw .ts source so Bun users get zero-overhead imports. Node/bundler consumers get the compiled dist/.


Installation

bun add cli-atelier    # Bun
npm install cli-atelier  # Node / bundlers

Full API Reference

CLI: a Command router

import { CLI } from "cli-atelier"

const app = new CLI("my-app")

app.command("greet", "Say hello", (args, flags) => {
  const name = (args[0] as string) ?? (flags["name"] as string) ?? "world"
  console.log("Hello, " + name)
})

app.run()
bun index.ts greet Nolly
bun index.ts greet --name Nolly
bun index.ts help

CLI methods

| Method | Description | | ------------------------------------- | ---------------------------------------- | | new CLI(name) | Create a new CLI app with a display name | | .command(name, description, action) | Register a command | | .run() | Parse process.argv and dispatch |

The action callback receives (args: string[], flags: Record<string, string | boolean>).


Display: ANSI engine

The foundational primitive everything else is built on.

import { Display } from "cli-atelier"

// Paint text with one or more codes
console.log(Display.paint("Hello", Display.color.cyan, Display.color.bold))

// Compose styles freely
console.log(Display.paint("Error", Display.color.bgRed, Display.color.white, Display.color.bold))

// xterm-256 gradient (char by char)
console.log(Display.gradient("Atelier", 51, 21))

// Truecolor RGB gradient
console.log(Display.gradientRGB("Atelier", [0, 255, 255], [255, 0, 255]))

// ANSI-safe string utilities
const raw = Display.strip("\x1b[32mHello\x1b[0m")  // -> "Hello"
const len = Display.len("\x1b[32mHello\x1b[0m")    // -> 5
const pad = Display.pad("Hi", 10, "center")        // -> "    Hi    "

Display methods

| Method | Description | | --------------------------------------------- | ------------------------------------------------------------------- | | Display.paint(text, ...codes) | Wrap text in ANSI codes, always reset at end | | Display.gradient(text, from, to) | xterm-256 horizontal gradient (code indices 0–255) | | Display.gradientRGB(text, [r,g,b], [r,g,b]) | Truecolor 24-bit horizontal gradient | | Display.strip(text) | Remove all ANSI escape codes | | Display.len(text) | Visible character length (ANSI-safe) | | Display.pad(text, width, align?) | Pad to visible width -> 'left' (default) / 'right' / 'center' | | Display.clear() | Clear the terminal | | Display.cursorTo(x, y) | Move cursor to absolute position | | Display.hideCursor() | Hide terminal cursor | | Display.showCursor() | Show terminal cursor | | Display.moveUp(n?) | Move cursor up n lines | | Display.moveDown(n?) | Move cursor down n lines | | Display.clearLine() | Clear the current line | | Display.clearDown() | Clear from cursor to bottom | | Display.width() | Terminal column count (fallback: 80) |

Display.color tokens

// Text styles
Display.color.reset | bold | dim | italic | underline | strike

// Foreground
Display.color.black | red | green | yellow | blue | magenta | cyan | white | gray

// Bright foreground
Display.color.brightRed | brightGreen | brightYellow | brightBlue
Display.color.brightMagenta | brightCyan | brightWhite

// Background
Display.color.bgBlack | bgRed | bgGreen | bgYellow | bgBlue
Display.color.bgMagenta | bgCyan | bgWhite | bgGray

Log: Structured logger

import { Log } from "cli-atelier"

Log.info("Server started on :3000")
Log.success("Deployment complete")
Log.warn("Memory usage above 80%")
Log.error("Connection refused")
Log.debug("Cache hit rate: 94.2%")
Log.blank()

| Method | Icon | Color | | ------------------ | ---- | ---------- | | Log.info(msg) | ℹ | Cyan | | Log.success(msg) | ✔ | Green | | Log.warn(msg) | ⚠ | Yellow | | Log.error(msg) | ✖ | Red | | Log.debug(msg) | ⬡ | Gray dim | | Log.blank() | - | Empty line |


Header: Section headers

import { Header } from "cli-atelier"

Header.render("Deployment Pipeline", { style: "banner",  color: Display.color.magenta })
Header.render("Services", { style: "section", color: Display.color.cyan })
Header.render("Step 1", { style: "inline",  color: Display.color.yellow })
Header.divider()
Header.divider(40) // custom width

Options

| Option | Type | Default | Description | | ------- | ------------------------------------------------ | -------------------- | ---------------------- | | style | 'banner' \| 'section' \| 'divider' \| 'inline' | 'section' | Visual style | | color | string | Display.color.cyan | Border and title color | | width | number | terminal width | Override render width | | align | 'left' \| 'center' \| 'right' | 'center' | Title alignment |


Badge: Inline status badges

import { Badge } from "cli-atelier"

// Logged on its own line
Badge.log("success", "All systems operational")
Badge.log("error",   "Connection refused", "API") // custom label

// Inline in a string
const status = Badge.render("warn", "RATE") + "  Rate limit at 90%"
console.log(status)

Variants: 'success' 'error' 'warn' 'info' 'neutral' 'debug'


Box: Bordered box

import { Box } from "cli-atelier"

Box.render([
  Display.paint("System Info", Display.color.white, Display.color.bold),
  "",
  "  Hostname: prod-node-01",
  "  Uptime: 22d 7h",
], { style: "round", color: Display.color.cyan, title: "Server" })

Options

| Option | Type | Default | | --------- | ------------------------------------------------------ | -------------------- | | style | 'single' \| 'double' \| 'round' \| 'bold' \| 'ascii' | 'round' | | color | string | Display.color.gray | | padding | number | 1 | | title | string | - | | width | number | auto |


Table: data table

import { Table } from "cli-atelier"

Table.render(
  [
    { name: "nginx", status: "running", port: "80"   },
    { name: "postgres", status: "running", port: "5432" },
    { name: "redis", status: "stopped", port: "6379" },
  ],
  [
    { key: "name", header: "Service", align: "left"   },
    { key: "status", header: "Status",  align: "center" },
    { key: "port", header: "Port",    align: "right"  },
  ],
  { style: "round", zebra: true }
)

ColumnDef

| Property | Type | Description | | -------- | ------------------------------- | -------------------------------------------- | | key | string | Object key to read | | header | string | Column heading | | align | 'left' \| 'right' \| 'center' | Cell alignment | | color | string | ANSI color code for all cells in this column | | width | number | Minimum column width override |

Table options

| Option | Type | Default | | ------------- | ------------------------------------------------------------ | ----------------------- | | style | 'single' \| 'double' \| 'round' \| 'minimal' \| 'markdown' | 'round' | | headerColor | string | Display.color.cyan | | borderColor | string | Display.color.gray | | zebra | boolean | false | | zebraColor | string | Display.color.bgBlack |


Progress: Progress bar

import { Progress } from "cli-atelier"

const bar = new Progress({ label: "Uploading", width: 35, color: Display.color.cyan })

for (let i = 0; i <= 100; i += 5) {
  bar.update(i)
  await Bun.sleep(40)
}

bar.finish("Upload complete")

Constructor options

| Option | Type | Default | | ------------ | --------- | -------------------- | | label | string | '' | | width | number | 30 | | color | string | Display.color.cyan | | emptyColor | string | Display.color.gray | | filled | string | '█' | | empty | string | '░' | | showPct | boolean | true |


Spinner: Async indicator

import { Spinner } from "cli-atelier"

const s = new Spinner("Deploying...")
s.start()
await doWork()
s.stop("Deployed successfully")         // ✔ green
s.stop("Deployment failed", false)      // ✖ red

Prompt: Text input

import { Prompt } from "cli-atelier"

const name  = await Prompt.text("Your name:")
const pass  = await Prompt.text("Password:", { mask: true })
const email = await Prompt.text("Email:", {
  validate: v => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? null : "Invalid email"
})

Options

| Option | Type | Description | | ---------- | ------------------------------- | ------------------------------------------- | | mask | boolean | Replace characters with (password mode) | | validate | (v: string) => string \| null | Return error string or null to pass |


Confirm: Yes/No prompt

import { Confirm } from "cli-atelier"

const ok = await Confirm.ask("Deploy to production?", { defaultYes: false })

Press Y / N or Enter to accept the default. Returns boolean.


Select: Single-choice menu

import { Select } from "cli-atelier"

const env = await Select.prompt("Target environment:", [
  { label: "Production",  value: "prod",    hint: "Live - be careful" },
  { label: "Staging",     value: "staging", hint: "Pre-release"       },
  { label: "Development", value: "dev",     hint: "Local only"        },
])

Controls: ^ / or j / k to move, Enter to confirm.

SelectOption<T>

| Property | Type | Description | | -------- | -------- | ------------------------------- | | label | string | Display text | | value | T | Returned value | | hint | string | Dim secondary text shown inline | | color | string | Override item color |

Config options

| Option | Type | Default | | --------------- | -------- | -------------------- | | pointer | string | '❯' | | selectedColor | string | Display.color.cyan | | hintColor | string | Display.color.gray | | maxVisible | number | 8 |


MultiSelect: Multi-choice menu

import { MultiSelect } from "cli-atelier"

const services = await MultiSelect.prompt("Services to restart:", [
  { label: "nginx",    value: "nginx"    },
  { label: "postgres", value: "postgres" },
  { label: "redis",    value: "redis"    },
], { min: 1, max: 2 })

Controls: / to navigate, Space to toggle, a to select all / none, Enter to confirm.

Additional config options (extends Select config)

| Option | Type | Default | Description | | --------------- | -------- | --------------------- | --------------------------- | | checkedIcon | string | '◉' | Icon for selected items | | uncheckedIcon | string | '◯' | Icon for unselected items | | checkedColor | string | Display.color.green | Color for checked items | | min | number | 0 | Minimum required selections | | max | number | Infinity | Maximum allowed selections |


Input: Raw input

import { Input } from "cli-atelier"

// Standard readline prompt
const answer = await Input.ask("Primary OS? ")

// Raw keypress listener (returns cleanup function)
const stop = Input.listen((key) => {
  if (key === "q") { stop(); process.exit(0) }
  if (key === "\x1b[A") console.log("up arrow")
})

Tree: Recursive tree renderer

import { Tree } from "cli-atelier"
import type { TreeNode } from "cli-atelier"

Tree.render({
  label: "my-project",
  children: [
    {
      label: "src",
      children: [
        { label: "index.ts",  hint: "entry point" },
        { label: "router.ts"                       },
      ]
    },
    { label: "package.json" },
  ]
})

TreeNode

| Property | Type | Description | | ---------- | ------------ | ------------------------------ | | label | string | Display text | | hint | string | Dim text shown after the label | | color | string | Override label color | | children | TreeNode[] | Nested children |


Columns: Multi-column layout

import { Columns } from "cli-atelier"

// Plain strings - auto-fit columns
Columns.render(["index.ts", "router.ts", "display.ts", "input.ts", "prompt.ts"])

// Colored items with a prefix
Columns.render(
  [
    { label: "nginx",  color: Display.color.green },
    { label: "redis",  color: Display.color.red   },
  ],
  { prefix: "● ", prefixColor: Display.color.gray, cols: 3 }
)

Options

| Option | Type | Default | Description | | ------------- | -------- | --------------------- | ----------------------- | | cols | number | auto | Fixed column count | | minWidth | number | auto | Minimum column width | | gap | number | 2 | Spaces between columns | | color | string | Display.color.white | Default item color | | prefix | string | '' | Prefix before each item | | prefixColor | string | Display.color.gray | Prefix color |


Diff: Unified diff renderer

import { Diff } from "cli-atelier"

Diff.render(oldText, newText, {
  labelOld: "--- before.ts",
  labelNew: "+++ after.ts",
  lineNumbers: true,
  contextLines: 3,
})

// Raw output for custom rendering
const lines = Diff.compute(oldText, newText)

Options

| Option | Type | Default | | -------------- | --------- | ----------- | | lineNumbers | boolean | true | | contextLines | number | 3 | | gutterWidth | number | 4 | | labelOld | string | '--- old' | | labelNew | string | '+++ new' |


Form: Multi-step form runner

import { Form } from "cli-atelier"

const result = await Form.run([
  { key: "name", label: "Your name:", type: "text"    },
  { key: "env", label: "Environment:", type: "select",
    options: [
      { label: "Production", value: "prod" },
      { label: "Staging", value: "staging" },
    ]
  },
  { key: "wipe", label: "Wipe data?", type: "confirm",
    when: (v) => v["env"] === "prod"
  },
], { title: "Deploy Config" })

console.log(result.env, result.wipe)

Field types

| type | Extra properties | Description | | --------------- | ----------------------- | ------------------ | | "text" | mask, validate | Free text input | | "select" | options | Single-choice menu | | "multiselect" | options, min, max | Multi-choice menu | | "confirm" | defaultYes | Yes/no prompt |

All fields support when: (values) => boolean for conditional rendering.

Form options

| Option | Type | Default | | ------------ | --------- | -------------------- | | title | string | - | | titleColor | string | Display.color.cyan | | summary | boolean | true |


Notify: Non-blocking notifications

import { Notify } from "cli-atelier"

// Auto-dismiss after 3 s
Notify.show("success", "Deployment complete!")
Notify.show("error",   "Connection refused", { duration: 5000 })

// Countdown bar
Notify.show("info", "Running migrations...", { duration: 4000, countdown: true })

// Manual control
const id = Notify.show("warn", "Still processing...", { duration: 0 })
await doWork()
Notify.clear(id)

// Clear everything
Notify.clearAll()

Options

| Option | Type | Default | Description | | ----------- | --------- | ------- | ---------------------------- | | duration | number | 3000 | Auto-dismiss ms. 0 = never | | countdown | boolean | false | Show animated countdown bar |

Variants: 'info' 'success' 'warn' 'error' 'debug'


Menu: Nested interactive menu

import { Menu } from "cli-atelier"

await Menu.run([
  {
    label: "Deploy",
    children: [
      { label: "Production", action: async () => deploy("prod")    },
      { label: "Staging", action: async () => deploy("staging") },
    ],
  },
  {
    label: "Locked feature",
    disabled: true,
    hint: "requires admin",
  },
  { label: "Quit", action: () => process.exit(0) },
])

Controls: / / j / k navigate, Enter selects or enters sub-menu, / Backspace goes back, q quits.

MenuItem

| Property | Type | Description | | ---------- | ----------------------------- | ------------------------- | | label | string | Display text | | hint | string | Dim secondary text | | color | string | Label color override | | children | MenuItem[] | Sub-menu items | | action | () => void \| Promise<void> | Executed on selection | | disabled | boolean | Greyed out, un-selectable |


Autocomplete: Live-filter input

import { Autocomplete } from "cli-atelier"

const pkg = await Autocomplete.prompt("Install package:", [
  { label: "typescript", value: "typescript", hint: "TS compiler"         },
  { label: "express", value: "express", hint: "web framework"       },
  { label: "zod", value: "zod", hint: "schema validation"   },
  { label: "drizzle-orm", value: "drizzle-orm", hint: "ORM for TypeScript"  },
])

Controls: Type to filter, / or Tab to navigate suggestions, Enter to accept, Escape to dismiss suggestions.

Config options

| Option | Type | Default | | ---------------- | ---------------------------- | --------------------- | | maxSuggestions | number | 6 | | minChars | number | 1 | | matchColor | string | Display.color.cyan | | unmatchedColor | string | Display.color.white | | hintColor | string | Display.color.gray | | filter | (query, option) => boolean | substring match |


Layout: Split-pane panels

import { Layout } from "cli-atelier"

Layout.render([
  {
    title: "Services",
    color: Display.color.cyan,
    lines: [
      Display.paint("● nginx", Display.color.green) + "  :80",
      Display.paint("● postgres", Display.color.green) + "  :5432",
    ],
  },
  {
    title: "Logs",
    color: Display.color.yellow,
    lines: [
      "[INFO]  Server started",
      "[WARN]  Memory at 82%",
    ],
  },
])

// Horizontal rule with optional label
Layout.rule("End of section", Display.color.gray)

Panel

| Property | Type | Default | Description | | ----------- | ----------------------------------------------------- | -------------------- | --------------------------- | | title | string | - | Title in top border | | lines | string[] | - | Content lines (ANSI-safe) | | border | 'single' \| 'double' \| 'round' \| 'bold' \| 'none' | 'round' | Border style | | color | string | Display.color.gray | Border color | | width | number | auto-split | Fixed panel width | | emptyText | string | '' | Shown when lines is empty | | align | 'left' \| 'right' \| 'center' | 'left' | Content alignment |

Layout options

| Option | Type | Default | | ---------- | --------- | ------------- | | gap | number | 1 | | height | number | tallest panel | | equalise | boolean | true |


Task: Multi-task runner

import { Task } from "cli-atelier"

const results = await Task.run([
  {
    label: "Install dependencies",
    run: async (log) => {
      await installDeps()
      log("22 packages resolved")
    },
  },
  {
    label: "Compile TypeScript",
    run: async () => compile(),
  },
  {
    label: "Deploy (prod only)",
    skip: (results) => {
      const env = results["Check env"]?.output as { env: string } | undefined
      return env?.env !== "prod"
    },
    run: async () => deploy(),
  },
], { title: "CI Pipeline", bail: true, parallel: false })

The run callback receives a log(msg) helper. Messages appear indented below the task row in real time.

Options

| Option | Type | Default | Description | | ---------- | --------- | ------- | -------------------------------------- | | title | string | - | Section header above the task list | | parallel | boolean | false | Run all tasks concurrently | | bail | boolean | false | Abort remaining tasks on first failure | | summary | boolean | true | Print result table when complete |

TaskDef

| Property | Type | Description | | -------- | --------------------------- | ------------------------------- | | label | string | Display text | | run | (log) => Promise<unknown> | Async work | | skip | (results) => boolean | Return true to skip this task |

TaskResult (returned per task in the result map)

| Property | Type | | ---------- | ----------------------------------------------------------- | | label | string | | status | 'pending' \| 'running' \| 'done' \| 'failed' \| 'skipped' | | duration | number (ms) | | output | unknown | | error | string \| undefined |


Pager: Scrollable viewport

import { Pager } from "cli-atelier"

await Pager.show(logLines, {
  title: "application.log",
  lineNumbers: true,
  searchable: true,
})

Controls

| Key | Action | | ----------------- | ----------------------- | | / k | Scroll up one line | | / j | Scroll down one line | | u / Page Up | Scroll up half a page | | d / Page Down | Scroll down half a page | | g / Home | Jump to top | | G / End | Jump to bottom | | / | Enter search mode | | n | Next match | | N | Previous match | | Escape | Clear search | | q / Ctrl-C | Quit pager |

Options

| Option | Type | Default | | ------------- | --------- | ---------------------- | | title | string | '' | | height | number | terminal rows - 3 | | lineNumbers | boolean | true | | wrap | boolean | false | | statusColor | string | Display.color.bgGray | | gutterColor | string | Display.color.gray | | searchable | boolean | true |


Complete example

import {
  CLI, Display, Header, Log, Badge, Box,
  Table, Progress, Spinner, Prompt, Confirm,
  Select, MultiSelect, Form, Task, Pager,
  Tree, Columns, Diff, Layout, Menu,
  Autocomplete, Notify,
} from "cli-atelier"

const app = new CLI("myapp")

app.command("deploy", "Full deployment workflow", async () => {
  Display.clear()
  Header.render("Deployment", { style: "banner", color: Display.color.magenta })
  const result = await Form.run([
    {
      key: "env", label: "Environment:", type: "select",
      options: [
        { label: "Production", value: "prod" },
        { label: "Staging", value: "staging" },
      ]
    },
    {
      key: "wipe", label: "Wipe data?", type: "confirm", defaultYes: false,
      when: (v) => v["env"] === "prod"
    },
  ])
  await Task.run([
    { label: "Build", run: async (log) => { await build(); log("dist/index.js - 142 kB") } },
    { label: "Test", run: async () => test() },
    { label: "Upload", run: async () => upload() },
    { label: "Restart", run: async () => restart() },
  ], { title: "Pipeline", bail: true })
  Log.success("Done - deployed to " + result["env"])
})

app.run()

Module index

| Module | Export | Type | | ------------------ | -------------- | ------ | | Command router | CLI | Class | | ANSI engine | Display | Object | | Structured logger | Log | Object | | Section headers | Header | Object | | Status badges | Badge | Object | | Bordered boxes | Box | Object | | Data table | Table | Object | | Progress bar | Progress | Class | | Async spinner | Spinner | Class | | Text input | Prompt | Object | | Yes/no prompt | Confirm | Object | | Single-choice menu | Select | Object | | Multi-choice menu | MultiSelect | Object | | Raw keypress | Input | Object | | Tree renderer | Tree | Object | | Column layout | Columns | Object | | Unified diff | Diff | Object | | Multi-step form | Form | Object | | Notifications | Notify | Object | | Nested menu | Menu | Object | | Live autocomplete | Autocomplete | Object | | Split-pane layout | Layout | Object | | Task list runner | Task | Object | | Scrollable pager | Pager | Object |


License

        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
                Version 2, December 2004

Copyright (C) 2004 Sam Hocevar <[email protected]>

Everyone is permitted to copy and distribute verbatim or modified
copies of this license document, and changing it is allowed as long
as the name is changed.

        DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION

0. You just DO WHAT THE FUCK YOU WANT TO.