Skip to main content

Introduction

Bunli’s plugin system provides a powerful way to extend CLI functionality with a type-safe, hook-based architecture. Plugins can:
  • Modify configuration during initialization
  • Register new commands dynamically
  • Inject context and behavior before/after command execution
  • Maintain typed shared state across the CLI lifecycle
  • Add middleware to the command pipeline

Architecture

The plugin system is built around four core lifecycle hooks:
interface BunliPlugin<TStore = {}> {
  name: string
  version?: string
  store?: TStore  // Type-safe shared state
  
  // Lifecycle hooks
  setup?(context: PluginContext): void | Promise<void>
  configResolved?(config: ResolvedConfig): void | Promise<void>
  beforeCommand?(context: CommandContext): void | Promise<void>
  afterCommand?(context: CommandContext & CommandResult): void | Promise<void>
}

Lifecycle Order

  1. setup - Called during CLI initialization, can modify config and register commands
  2. configResolved - Called after configuration is finalized (config is now immutable)
  3. beforeCommand - Called before each command execution
  4. afterCommand - Called after each command execution, receives result or error

Official Plugins

Bunli ships with four official plugins:

AI Detection

Detect AI coding assistants from environment variables

Completions

Generate shell completion scripts (bash, zsh, fish)

Config Merger

Load and merge configuration from multiple sources

MCP Integration

Create CLI commands from Model Context Protocol tool schemas

Plugin Capabilities

Configuration Modification

Plugins can modify CLI configuration during the setup hook:
setup(context) {
  context.updateConfig({
    description: 'Enhanced with plugin'
  })
}

Command Registration

Dynamically register commands:
setup(context) {
  context.registerCommand({
    name: 'custom',
    description: 'Custom command from plugin',
    handler: async () => {
      console.log('Plugin command executed!')
    }
  })
}

Type-Safe Store

Share typed state across the CLI lifecycle:
interface MyStore {
  count: number
  user?: string
}

const plugin = createPlugin<MyStore>({
  name: 'my-plugin',
  store: {
    count: 0
  },
  beforeCommand(context) {
    context.store.count++ // TypeScript knows the type!
  }
})

Middleware

Add middleware to the command pipeline:
setup(context) {
  context.use(async (ctx, next) => {
    console.log('Before command')
    await next()
    console.log('After command')
  })
}

Use Cases

Authentication

const authPlugin = createPlugin({
  name: 'auth',
  beforeCommand(context) {
    if (!process.env.API_KEY) {
      throw new Error('API_KEY required')
    }
  }
})

Metrics & Telemetry

const metricsPlugin = createPlugin<{ events: Event[] }>({
  name: 'metrics',
  store: { events: [] },
  beforeCommand(context) {
    context.store.events.push({
      command: context.command,
      timestamp: new Date()
    })
  }
})

Feature Flags

const featureFlagPlugin = createPlugin({
  name: 'features',
  setup(context) {
    // Register commands based on feature flags
    if (process.env.FEATURE_BETA) {
      context.registerCommand(betaCommand)
    }
  }
})

Environment Detection

const envPlugin = createPlugin({
  name: 'env-detect',
  beforeCommand(context) {
    if (context.env.isCI) {
      // Adjust behavior for CI environments
    }
  }
})

Plugin Composition

Compose multiple plugins into one:
import { composePlugins } from '@bunli/core/plugin'

const composedPlugin = composePlugins(
  authPlugin({ provider: 'github' }),
  loggingPlugin({ level: 'debug' }),
  metricsPlugin({ enabled: true })
)
The composed plugin:
  • Merges all plugin stores
  • Runs all hooks in sequence
  • Combines behavior from multiple plugins

Next Steps

Creating Plugins

Learn how to create custom plugins

Plugin Hooks

Deep dive into lifecycle hooks

Plugin Store

Master type-safe shared state

Official Plugins

Explore official plugin implementations

Build docs developers (and LLMs) love