@bunli/core
v0.9.1
Published
Minimal, type-safe CLI framework for Bun
Maintainers
Readme
@bunli/core
The minimal, type-safe CLI framework for Bun.
Installation
bun add @bunli/coreQuick Start
import { defineCommand, option } from "@bunli/core";
import { z } from "zod";
export default defineCommand({
name: "greet",
description: "A friendly greeting",
options: {
name: option(z.string().min(1), { description: "Name to greet", short: "n" }),
excited: option(z.coerce.boolean().default(false), {
description: "Add excitement",
short: "e",
argumentKind: "flag",
}),
},
handler: async ({ flags }) => {
const greeting = `Hello, ${flags.name}${flags.excited ? "!" : "."}`;
console.log(greeting);
},
});Features
- 🚀 Type-safe - Full TypeScript support with automatic type inference
- ⚡ Fast - Powered by Bun's native speed
- 📦 Zero config - Works out of the box with sensible defaults
- 🎯 Minimal API - Learn once, use everywhere
- 🔌 Extensible - Plugin system for custom functionality
- 🧪 Testable - First-class testing utilities included
Core Concepts
Commands
Define commands with automatic type inference:
import { defineCommand } from "@bunli/core";
export default defineCommand({
name: "build",
description: "Build the project",
handler: async () => {
console.log("Building...");
},
});Options
Use the option helper with Standard Schema validation:
import { defineCommand, option } from "@bunli/core";
import { z } from "zod";
export default defineCommand({
name: "deploy",
description: "Deploy the application",
options: {
env: option(z.enum(["dev", "staging", "prod"]), { description: "Target environment" }),
force: option(z.coerce.boolean().default(false), {
description: "Force deployment",
short: "f",
argumentKind: "flag",
}),
},
handler: async ({ flags }) => {
// TypeScript knows:
// flags.env is 'dev' | 'staging' | 'prod'
// flags.force is boolean
},
});Multi-Command CLIs
Create complex CLIs with multiple commands:
import { createCLI, defineCommand } from "@bunli/core";
const build = defineCommand({
name: "build",
description: "Build the project",
handler: async () => {},
});
const deploy = defineCommand({
name: "deploy",
description: "Deploy the project",
handler: async () => {},
});
const test = defineCommand({
name: "test",
description: "Run tests",
handler: async () => {},
});
const cli = await createCLI({
name: "my-tool",
version: "1.0.0",
description: "My awesome CLI tool",
});
cli.command(build);
cli.command(deploy);
cli.command(test);
await cli.run();TUI Renderer Buffer Mode
When using command.render, configure buffer behavior via tui.renderer.bufferMode:
import { defineConfig } from "@bunli/core";
export default defineConfig({
tui: {
renderer: {
bufferMode: "alternate", // or 'standard'
},
},
});Default policy:
'standard'by default- set
'alternate'explicitly for fullscreen/blocking terminal flows
API Reference
defineCommand(config)
Creates a command definition with full type inference.
option(schema, config)
Creates a typed option with schema validation.
For boolean-style flags, set argumentKind: 'flag'.
verbose: option(z.boolean().default(false), {
short: "v",
description: "Verbose output",
argumentKind: "flag",
});Flag parsing contract:
--verbosesets the value totrue--verbose=trueand--verbose=falseare both valid--verbose builddoes not consumebuild; it remains positional
Use the default value schema if you want the flag to be optional and strongly typed in handlers.
createCLI(config)
Creates a multi-command CLI application.
defineConfig(config)
Defines shared configuration for your CLI.
Plugin System
Bunli provides a powerful plugin system with compile-time type safety:
Basic Plugin
import type { BunliPlugin } from "@bunli/core/plugin";
interface MyPluginStore {
apiKey: string;
isAuthenticated: boolean;
}
const myPlugin: BunliPlugin<MyPluginStore> = {
name: "my-plugin",
version: "1.0.0",
// Define the plugin's store
store: {
apiKey: "",
isAuthenticated: false,
},
// Lifecycle hooks
setup(context) {
// One-time initialization
context.updateConfig({ description: "Enhanced by my plugin" });
},
configResolved(config) {
// Called after all configuration is resolved
},
beforeCommand(context) {
// Called before each command - context.store is type-safe!
const apiKey = process.env.API_KEY || "";
context.setStoreValue("apiKey", apiKey);
context.setStoreValue("isAuthenticated", apiKey.length > 0);
},
afterCommand(context) {
// Called after each command with results
if (context.error) {
console.error("Command failed:", context.error);
}
},
};Plugin Factory
Use createPlugin for better ergonomics:
import { createPlugin } from "@bunli/core/plugin";
type AuthOptions = {
provider: "github" | "gitlab";
};
type User = {
name: string;
};
type AuthStore = {
token: string;
user: User | null;
};
async function loadToken() {
return "token";
}
async function fetchUser(_token: string): Promise<User> {
return { name: "octocat" };
}
export const authPlugin = createPlugin<AuthOptions, AuthStore>((options) => ({
name: `auth-${options.provider}`,
store: {
token: "",
user: null,
},
async beforeCommand(context) {
const token = await loadToken();
context.setStoreValue("token", token);
context.setStoreValue("user", await fetchUser(token));
},
}));Using Plugins with Type Safety
const cli = await createCLI({
name: "my-cli",
version: "1.0.0",
plugins: [authPlugin({ provider: "github" }), myPlugin],
});
// In your commands, the store is fully typed!
cli.command({
name: "deploy",
description: "Deploy the application",
handler: async ({ context }) => {
// TypeScript knows about all plugin stores!
if (!context || !context.getStoreValue("isAuthenticated")) {
throw new Error("Not authenticated");
}
console.log(`Deploying as ${context.getStoreValue("user")?.name}`);
},
});Plugin Development Utilities
Bunli provides utilities for plugin development and testing:
import {
createTestPlugin,
composePlugins,
testPluginHooks,
assertPluginBehavior,
} from "@bunli/core/plugin";
// Create a test plugin
const testPlugin = createTestPlugin(
{ count: 0, message: "" },
{
beforeCommand(context) {
const count = context.getStoreValue("count");
context.setStoreValue("count", count + 1);
console.log(`Count: ${context.getStoreValue("count")}`);
},
},
);
// Compose multiple plugins
const composedPlugin = composePlugins(
testPlugin,
createTestPlugin({ enabled: true }, { name: "metrics" }),
);
// Test plugin behavior
const results = await testPluginHooks(testPlugin, {
config: { name: "test-cli", version: "1.0.0" },
store: { count: 0, message: "test" },
});
assertPluginBehavior(results, {
beforeCommandShouldSucceed: true,
});Module Augmentation
Plugins can extend Bunli's interfaces:
declare module "@bunli/core/plugin" {
interface EnvironmentInfo {
isCI: boolean;
ciProvider?: string;
}
}Generated Helpers
Bunli automatically generates type-safe helpers for your commands. These are auto-loaded when you set generated: true in your CLI config:
const cli = await createCLI({
name: "my-cli",
version: "1.0.0",
generated: true, // Auto-load generated types
});Available Helpers
import {
listCommands,
getCommandApi,
getTypedFlags,
validateCommand,
findCommandByName,
findCommandsByDescription,
getCommandNames,
} from "./.bunli/commands.gen";
// List all available commands
const commands = listCommands();
// Result: ['build', 'dev', 'test', ...]
// Get command metadata
const buildMeta = getCommandApi("build");
console.log(buildMeta.description); // "Build the project"
console.log(buildMeta.options); // { outdir: {...}, watch: {...} }
// Get typed flags for a command
const flags = getTypedFlags("build");
// flags.outdir is typed as string | undefined
// flags.watch is typed as boolean | undefined
// Validate command arguments at runtime
const result = validateCommand("build", { outdir: "dist", watch: true });
if (result.success) {
console.log("Valid arguments:", result.data);
} else {
console.error("Validation errors:", result.errors);
}
// Find commands by name or description
const buildCmd = findCommandByName("build");
const devCommands = findCommandsByDescription("development");
const allNames = getCommandNames();IDE Auto-Import Support
Configure TypeScript path mapping for better IDE support:
// tsconfig.json
{
"compilerOptions": {
"paths": {
"~commands/*": ["./.bunli/commands.gen.ts"]
}
}
}Then use auto-imports:
// These will show up in IDE auto-complete
import { listCommands } from "~commands/helpers";
import { getCommandApi } from "~commands/api";Runtime Validation
Bunli provides runtime validation utilities for dynamic type checking:
import {
validateValue,
validateValues,
isValueOfType,
createValidator,
createBatchValidator,
} from "@bunli/core";
import { z } from "zod";
// Validate a single value
const result = await validateValue("hello", z.string().min(1), {
option: "message",
command: "greet",
});
// Validate multiple values
const validated = await validateValues(
{ name: "John", age: 25 },
{
name: z.string(),
age: z.number(),
},
"user",
);
// Check value types
const value: unknown = "hello";
if (isValueOfType(value, "string")) {
console.log("Value is a string");
}
// Create reusable validators
const nameValidator = createValidator(z.string().min(1));
const userValidator = createBatchValidator({
name: z.string(),
age: z.number(),
});Type Utilities
Bunli exports advanced TypeScript type utilities for complex type manipulation, especially useful when working with generated types:
import type {
UnionToIntersection,
MergeAll,
Expand,
DeepPartial,
Constrain,
IsAny,
} from "@bunli/core";Key Utilities
UnionToIntersection - Convert union types to intersection types:
type Example = UnionToIntersection<{ a: string } | { b: number }>;
// Result: { a: string } & { b: number }MergeAll - Merge multiple object types:
type Example = MergeAll<[{ a: string }, { b: number }, { c: boolean }]>;
// Result: { a: string; b: number; c: boolean }Expand - Expand complex types for better IntelliSense:
type Example = Expand<{ nested: { deep: { value: string } } }>;
// Shows full type structure in IDEDeepPartial - Make all properties optional recursively:
type Example = DeepPartial<{ user: { name: string; age: number } }>;
// Result: { user?: { name?: string; age?: number } }Constrain - Constrain types with fallback:
type Example = Constrain<string, "a" | "b" | "c", "a">;
// Result: 'a' | 'b' | 'c' (or 'a' if string doesn't match)Usage with Generated Types
These utilities work particularly well with generated command types:
import { getCommandApi, listCommands } from "./commands.gen";
import { UnionToIntersection, MergeAll } from "@bunli/core";
// Get all command options as a union
type AllCommandOptions = UnionToIntersection<
ReturnType<typeof getCommandApi>[keyof CommandRegistry]["options"]
>;
// Merge all command metadata
type AllCommands = MergeAll<Array<{ name: string; description: string }>>;Related Packages
- @bunli/generator - Generate TypeScript definitions from commands
- @bunli/utils - Shared utilities for CLI development
- @bunli/test - Testing utilities for CLI applications
Documentation
- Getting Started - Step-by-step tutorial
- Type Generation Guide - Learn about code generation
- API Reference - Complete API documentation
License
MIT © Arya Labs, Inc.
