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)— infersTStorefrom the plugin'sstoreproperty - Factory form:
createPlugin(factory)— infersTOptionsandTStorefrom 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 optionsExamples:
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
- Load Phase: Plugins are loaded and validated
- Setup Phase: All
setuphooks run in order - Config Resolution: Configuration is finalized
- Config Resolved Phase: All
configResolvedhooks run - Command Execution:
- All
beforeCommandhooks run in order - Command handler executes
- All
afterCommandhooks run in reverse order
- All
Error Handling
- Errors in
setuporconfigResolvedwill prevent CLI initialization - Errors in
beforeCommandwill prevent command execution - Errors in
afterCommandare 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`);
}
},
}));