Skip to content

roushou/boune

Repository files navigation

boune

A modern CLI framework for Bun.

Features

  • TypeScript-first - Full type inference for commands, arguments, and options
  • Declarative API - Schema-based pattern for defining CLIs
  • Subcommands - Nested command hierarchies with aliases
  • Auto-generated help - --help and --version out of the box
  • Argument parsing - Required, optional, and variadic positional arguments
  • Option parsing - Short/long flags, typed values, defaults, environment variables
  • Interactive prompts - Text, password (with masking), select, confirm, and more
  • Middleware - Before/after hooks and error handling
  • Output utilities - Colors, tables, spinners, formatted messages
  • Devtools - Web dashboard for CLI inspection and debugging
  • Auto documentation - Generate interactive docs from your CLI schema
  • Zero dependencies - Built specifically for Bun's APIs

Installation

bun add boune

Quick Start

import { defineCli, defineCommand } from "boune";

const greet = defineCommand({
  name: "greet",
  description: "Greet someone",
  arguments: {
    name: { type: "string", required: true, description: "Name to greet" },
  },
  options: {
    loud: { type: "boolean", short: "l", description: "Shout the greeting" },
  },
  action({ args, options }) {
    const msg = `Hello, ${args.name}!`;
    console.log(options.loud ? msg.toUpperCase() : msg);
  },
});

defineCli({
  name: "my-app",
  version: "1.0.0",
  commands: { greet },
}).run();
$ my-app greet World --loud
HELLO, WORLD!

$ my-app --help
Usage:
  my-app <command> [options]

Commands:
  greet  Greet someone

Options:
  -h, --help     Show help
  -V, --version  Show version

Arguments

Arguments are positional values passed to commands. Define them as plain objects with type inference.

// Required argument
defineCommand({
  name: "greet",
  arguments: {
    name: { type: "string", required: true, description: "Name to greet" },
  },
  action({ args }) {
    console.log(`Hello, ${args.name}!`);
  },
});

// Optional argument with default
defineCommand({
  name: "greet",
  arguments: {
    name: { type: "string", default: "World", description: "Name to greet" },
  },
  action({ args }) {
    console.log(`Hello, ${args.name}!`);
  },
});

// Variadic argument (collects remaining args)
defineCommand({
  name: "cat",
  arguments: {
    files: { type: "string", required: true, variadic: true, description: "Files to concatenate" },
  },
  action({ args }) {
    // args.files is string[]
  },
});

// Typed argument
defineCommand({
  name: "repeat",
  arguments: {
    count: { type: "number", required: true, description: "Times to repeat" },
  },
  action({ args }) {
    // args.count is number
  },
});

Options

Use the option builder to define options. Boolean options act as flags (no value).

// Boolean option (flag - no value)
defineCommand({
  name: "build",
  options: {
    verbose: option.boolean().short("v").describe("Verbose output"),
  },
  action({ options }) {
    // options.verbose is boolean (defaults to false)
  },
});

// Option with string value
defineCommand({
  name: "build",
  options: {
    output: option.string().short("o").describe("Output directory"),
  },
  action({ options }) {
    // options.output is string | undefined
  },
});

// Option with default (type is inferred as always present)
defineCommand({
  name: "serve",
  options: {
    port: option.number().short("p").default(3000).describe("Port to listen on"),
  },
  action({ options }) {
    // options.port is number
  },
});

// Environment variable fallback
defineCommand({
  name: "deploy",
  options: {
    token: option.string().required().env("API_TOKEN").describe("API token"),
  },
  action({ options }) {
    // options.token is string
  },
});

Boolean vs Value Options

Kind Usage Example
boolean No value (toggle) --verbose, -v
string Takes a string --output dist
number Takes a number --port 8080

Subcommands

const watch = defineCommand({
  name: "watch",
  description: "Watch mode",
  action() {
    console.log("Watching...");
  },
});

const build = defineCommand({
  name: "build",
  description: "Build project",
  subcommands: { watch },
  action() {
    console.log("Building...");
  },
});

defineCli({
  name: "my-app",
  commands: { build },
}).run();
$ my-app build        # runs build action
$ my-app build watch  # runs watch action

Middleware

Use before and after hooks for middleware, and onError for error handling:

const loggingMiddleware = async (ctx, next) => {
  console.log(`Running: ${ctx.command.name}`);
  await next();
  console.log("Done!");
};

defineCli({
  name: "my-app",
  commands: { build },
  middleware: [loggingMiddleware],
  onError(error, ctx) {
    console.error(`Error in ${ctx.command.name}: ${error.message}`);
  },
});

// Or per-command:
defineCommand({
  name: "build",
  before: [loggingMiddleware],
  after: [cleanupMiddleware],
  onError(error, ctx) {
    // Command-specific error handling
  },
  action() {
    // ...
  },
});

Interactive Prompts

Define prompts in your command schema for full type inference:

const init = defineCommand({
  name: "init",
  description: "Initialize a new project",
  prompts: {
    name: { kind: "text", message: "Project name:", default: "my-project" },
    useTS: { kind: "confirm", message: "Use TypeScript?", default: true },
    framework: {
      kind: "select",
      message: "Select framework:",
      options: [
        { label: "React", value: "react" },
        { label: "Vue", value: "vue" },
        { label: "Svelte", value: "svelte" },
      ] as const, // Use 'as const' for literal type inference
    },
  },
  async action({ prompts }) {
    // Prompts are executed explicitly via .run()
    const name = await prompts.name.run();           // string
    const useTS = await prompts.useTS.run();         // boolean
    const framework = await prompts.framework.run(); // "react" | "vue" | "svelte"

    console.log(`Creating ${name} with ${framework}...`);
  },
});

Prompt types: text, password, number, confirm, select, multiselect, autocomplete, filepath

Output Utilities

import { color, createSpinner, table } from "boune";

// Colors
console.log(color.green("Success!"));
console.log(color.red("Error!"));
console.log(color.bold(color.cyan("Bold cyan")));

// Spinner
const spinner = createSpinner("Loading...").start();
await doWork();
spinner.succeed("Done!");

// Table
console.log(table([
  ["Name", "Status"],
  ["Task 1", "Done"],
  ["Task 2", "Pending"],
]));

Devtools

Enable the devtools dashboard to inspect your CLI, capture events, and view live documentation:

import { defineCli } from "boune";
import { withDevtools } from "boune/devtools";

const cli = defineCli(withDevtools({
  name: "myapp",
  version: "1.0.0",
  commands: { deploy, build },
}));

cli.run();

withDevtools() automatically adds:

  • Event capture middleware - Records command executions
  • devtools command - Starts the web dashboard
$ myapp devtools
# Opens dashboard at http://localhost:4000

Auto Documentation

Generate interactive documentation from your CLI schema:

$ myapp docs
# Serves documentation at http://localhost:4000

Or use the docs utilities directly:

import { createDocsCommand } from "boune/docs";

const cli = defineCli({
  name: "myapp",
  commands: {
    docs: createDocsCommand(),
  },
});

Compile to Binary

Bun can compile your CLI to a standalone executable:

bun build ./cli.ts --compile --outfile my-app

Examples

See the examples directory:

  • demo.ts - Basic CLI features
  • git-like.ts - Git-like subcommand structure
  • file-tool.ts - File operations with Bun APIs
  • http-client.ts - HTTP client with fetch
  • task-manager.ts - SQLite persistence with bun:sqlite
  • hooks-example.ts - Middleware and hooks

Run an example:

bun examples/demo.ts --help

Development

This monorepo includes a CLI for common development tasks:

bun run dev <command>
Command Description
test [packages...] Run tests
lint Run linter
format Format code
typecheck Type check
prompt [type] Test prompt types
info Show monorepo info
ci Run full CI pipeline
clean Clean build artifacts

License

This project is licensed under the MIT License

About

A batteries-included CLI framework for Bun — prompts, colors, spinners, and more

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published