A modern CLI framework for Bun.
- 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 -
--helpand--versionout 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
bun add bouneimport { 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 versionArguments 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
},
});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
},
});| Kind | Usage | Example |
|---|---|---|
boolean |
No value (toggle) | --verbose, -v |
string |
Takes a string | --output dist |
number |
Takes a number | --port 8080 |
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 actionUse 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() {
// ...
},
});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
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"],
]));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
devtoolscommand - Starts the web dashboard
$ myapp devtools
# Opens dashboard at http://localhost:4000Generate interactive documentation from your CLI schema:
$ myapp docs
# Serves documentation at http://localhost:4000Or use the docs utilities directly:
import { createDocsCommand } from "boune/docs";
const cli = defineCli({
name: "myapp",
commands: {
docs: createDocsCommand(),
},
});Bun can compile your CLI to a standalone executable:
bun build ./cli.ts --compile --outfile my-appSee the examples directory:
demo.ts- Basic CLI featuresgit-like.ts- Git-like subcommand structurefile-tool.ts- File operations with Bun APIshttp-client.ts- HTTP client with fetchtask-manager.ts- SQLite persistence with bun:sqlitehooks-example.ts- Middleware and hooks
Run an example:
bun examples/demo.ts --helpThis 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 |
This project is licensed under the MIT License