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

@bunli/core

v0.9.1

Published

Minimal, type-safe CLI framework for Bun

Readme

@bunli/core

The minimal, type-safe CLI framework for Bun.

Installation

bun add @bunli/core

Quick 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:

  • --verbose sets the value to true
  • --verbose=true and --verbose=false are both valid
  • --verbose build does not consume build; 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 IDE

DeepPartial - 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

Documentation

License

MIT © Arya Labs, Inc.