Bunli
API Reference

Plugin API

Complete API reference for Bunli's plugin system

This page provides a complete API reference for Bunli's plugin system.

Types

BunliPlugin<TStore>

The core plugin interface.

interface BunliPlugin<TStore = {}> {
  /** Unique plugin name */
  name: string;

  /** Optional plugin version */
  version?: string;

  /** Plugin store schema/initial state */
  store?: TStore;

  /** Setup hook - Called during CLI initialization */
  setup?(context: PluginContext): void | Promise<void>;

  /** Config resolved hook - Called after configuration is finalized */
  configResolved?(config: ResolvedConfig): void | Promise<void>;

  /** Before command hook - Called before command execution */
  beforeCommand?(context: CommandContext<TStore>): void | Promise<void>;

  /**
   * Pre-run hook - Called immediately before the command handler executes.
   * Receives an ExecutionState for sharing per-run data between plugins.
   * Lifecycle: setup → configResolved → beforeCommand → preRun → [handler] → postRun → afterCommand
   */
  preRun?(context: CommandContext<TStore>, state: ExecutionState): void | Promise<void>;

  /**
   * Post-run hook - Called immediately after the command handler completes.
   * Receives the command result and the same ExecutionState from preRun.
   * Lifecycle: setup → configResolved → beforeCommand → preRun → [handler] → postRun → afterCommand
   */
  postRun?(
    context: CommandContext<TStore> & CommandResult,
    state: ExecutionState,
  ): void | Promise<void>;

  /** After command hook - Called after command execution */
  afterCommand?(context: CommandContext<TStore> & CommandResult): void | Promise<void>;
}

PluginContext

Context available during the setup phase.

interface PluginContext {
  /** Current configuration (readonly snapshot; use updateConfig to accumulate changes) */
  readonly config: BunliConfigInput;

  /** Accumulate partial configuration changes (config is immutable — changes are merged separately) */
  updateConfig(partial: Partial<BunliConfigInput>): void;

  /** Register a new command */
  registerCommand(command: CommandDefinition): void;

  /** Add global middleware */
  use(middleware: Middleware): void;

  /** Shared storage between plugins */
  readonly store: Map<string, any>;

  /** Plugin logger */
  readonly logger: Logger;

  /** System paths */
  readonly paths: PathInfo;
}

CommandContext<TStore>

Context available during command execution.

interface CommandContext<TStore = {}> {
  /** Command name being executed */
  readonly command: string;

  /** The Command object being executed */
  readonly commandDef: Command<any, TStore>;

  /** Positional arguments */
  readonly args: string[];

  /** Parsed flags/options */
  readonly flags: Record<string, unknown>;

  /** Environment information */
  readonly env: EnvironmentInfo;

  /** Type-safe context store */
  readonly store: TStore;

  /** Type-safe store value access */
  getStoreValue<K extends keyof TStore>(key: K): TStore[K];
  getStoreValue(key: string | number | symbol): unknown;

  /** Type-safe store value update */
  setStoreValue<K extends keyof TStore>(key: K, value: TStore[K]): void;
  setStoreValue(key: string | number | symbol, value: unknown): void;

  /** Check if a store property exists */
  hasStoreValue<K extends keyof TStore>(key: K): boolean;
  hasStoreValue(key: string | number | symbol): boolean;
}

CommandResult

Result of command execution.

interface CommandResult {
  /** Command return value */
  result?: unknown;

  /** Error if command failed */
  error?: unknown;

  /** Exit code */
  exitCode?: number;
}

PathInfo

System path information.

interface PathInfo {
  /** Current working directory */
  cwd: string;

  /** User home directory */
  home: string;

  /** Config directory path */
  config: string;

  /** Data directory path */
  data: string;

  /** State directory path */
  state: string;

  /** Cache directory path */
  cache: string;
}

EnvironmentInfo

Environment information available to plugins. The isCI field is built-in; plugins like @bunli/plugin-ai-detect extend this with additional fields via module augmentation.

interface EnvironmentInfo {
  /** Running in CI environment */
  isCI: boolean;
}

Functions

createPlugin

Type-safe plugin factory. Acts as a thin type helper — it returns the plugin as-is (an identity function), enabling TypeScript to infer store and option types:

  • Direct form: createPlugin(plugin) — infers TStore from the plugin's store property
  • Factory form: createPlugin(factory) — infers TOptions and TStore from the factory function's parameter and return type
// Direct plugin overload
function createPlugin<TStore = {}>(plugin: BunliPlugin<TStore>): BunliPlugin<TStore>;

// Factory overload
function createPlugin<TOptions, TStore = {}>(
  factory: (options: TOptions) => BunliPlugin<TStore>,
): (options: TOptions) => BunliPlugin<TStore>;

Examples:

// Direct plugin with explicit store type
interface MyStore {
  count: number;
  message: string;
}

const myPlugin = createPlugin<MyStore>({
  name: "my-plugin",
  store: { count: 0, message: "" },
});

// Plugin factory with options and store type
interface Options {
  prefix: string;
}

interface MyStore {
  count: number;
}

const myPlugin = createPlugin<Options, MyStore>((options) => ({
  name: "my-plugin",
  store: { count: 0 },
  beforeCommand(context) {
    console.log(`${options.prefix}: ${context.store.count}`);
  },
}));

Type Utilities

StoreOf<P>

Extract the store type from a plugin.

type StoreOf<P> = P extends BunliPlugin<infer S> ? S : {};

// Example
type MyStore = StoreOf<typeof myPlugin>;

MergeStores<Plugins>

Merge multiple plugin stores into one type.

type MergeStores<Plugins extends readonly BunliPlugin[]>

// Example
type CombinedStore = MergeStores<[
  typeof pluginA,
  typeof pluginB
]>

Plugin Configuration

PluginConfig

Type for plugin configuration in createCLI.

type PluginConfig =
  | string // Path to plugin
  | BunliPlugin // Plugin object
  | PluginFactory // Plugin factory function
  | [PluginFactory, unknown]; // Plugin with options

Examples:

const cli = await createCLI({
  plugins: [
    // Plugin object
    myPlugin,

    // Plugin factory with options
    myPlugin({ verbose: true }),

    // Path to plugin file
    "./plugins/custom.js",

    // Plugin with options as tuple
    [myPlugin, { verbose: true }],
  ],
});

Logger API

The logger available in plugin context.

interface Logger {
  /** Log debug message */
  debug(message: string): void;

  /** Log info message */
  info(message: string): void;

  /** Log warning message */
  warn(message: string): void;

  /** Log error message */
  error(message: string): void;
}

Middleware

Middleware function type for global command interception.

type Middleware = (context: CommandContext, next: () => Promise<unknown>) => Promise<unknown>;

// Example
const loggingMiddleware: Middleware = async (context, next) => {
  console.log(`Before: ${context.command}`);
  const result = await next();
  console.log(`After: ${context.command}`);
  return result;
};

Module Augmentation

Extend core interfaces in your plugins.

declare module "@bunli/core" {
  interface PluginStore {
    // Extend shared store
    myPluginData: string;
  }

  interface CommandContext {
    // Extend command context
    customField: number;
  }
}

Plugin Lifecycle

Execution Order

  1. Load Phase: Plugins are loaded and validated
  2. Setup Phase: All setup hooks run in order
  3. Config Resolution: Configuration is finalized
  4. Config Resolved Phase: All configResolved hooks run
  5. Command Execution:
    • All beforeCommand hooks run in order
    • Command handler executes
    • All afterCommand hooks run in reverse order

Error Handling

  • Errors in setup or configResolved will prevent CLI initialization
  • Errors in beforeCommand will prevent command execution
  • Errors in afterCommand are logged but don't affect the command result

Complete Example

import { createPlugin, type BunliPlugin } from "@bunli/core/plugin";
import { defineCommand } from "@bunli/core";

interface MetricsStore {
  commandCount: number;
  totalDuration: number;
  averageDuration: number;
}

export const metricsPlugin = createPlugin<{ detailed?: boolean }, MetricsStore>((options = {}) => ({
  name: "@company/metrics-plugin",
  version: "1.0.0",

  store: {
    commandCount: 0,
    totalDuration: 0,
    averageDuration: 0,
  },

  setup(context) {
    context.logger.info("Metrics plugin initialized");

    // Register metrics command
    context.registerCommand(
      defineCommand({
        name: "metrics",
        description: "Show command metrics",
        handler: async ({ context }) => {
          const store = context?.store;
          console.log(`Commands run: ${store.commandCount}`);
          console.log(`Average duration: ${store.averageDuration}ms`);
        },
      }),
    );
  },

  configResolved(config) {
    console.log(`Metrics enabled for ${config.name}`);
  },

  beforeCommand(context) {
    // Store start time in command context
    (context as any)._startTime = Date.now();
  },

  afterCommand(context) {
    const duration = Date.now() - (context as any)._startTime;

    // Update metrics
    context.store.commandCount++;
    context.store.totalDuration += duration;
    context.store.averageDuration = Math.round(
      context.store.totalDuration / context.store.commandCount,
    );

    if (options.detailed) {
      console.log(`Command duration: ${duration}ms`);
    }
  },
}));

On this page