Bunli
Examples

Dev Server CLI

Development server with advanced plugin system and configuration management

Overview

This example consolidates plugin system and configuration patterns:

  • Plugin system with lifecycle hooks and type-safe stores
  • Configuration management from multiple sources
  • Long-running processes with graceful shutdown
  • Real-time operations like log following
  • Type-safe plugin context for command access
  • Advanced patterns for production CLIs

Commands

start - Start Development Server

bun run cli.ts start --port 3000 --host localhost --watch

Starts a development server with hot reload:

  • Configurable port and host
  • File watching and hot reload
  • Browser auto-opening
  • Graceful shutdown handling
  • Plugin context access

build - Build for Production

bun run cli.ts build --output dist --target node --minify --sourcemap

Builds the project for production:

  • Multi-step progress indicators
  • Plugin metrics recording
  • Configuration access
  • Build optimization options

env - Environment Management

bun run cli.ts env --set API_KEY=abc123
bun run cli.ts env --get API_KEY
bun run cli.ts env --list

Manages environment variables:

  • Set, get, and list environment variables
  • File-based configuration
  • Plugin event recording
  • Conditional command flows

logs - View Server Logs

bun run cli.ts logs --follow --lines 100 --level info --service server

Views and follows server logs:

  • Real-time log streaming
  • Log filtering and formatting
  • Process signal handling
  • Service-specific filtering

Plugin System

Built-in Plugins

import { configMergerPlugin } from "@bunli/plugin-config";
import { aiAgentPlugin } from "@bunli/plugin-ai-detect";

const cli = await createCLI({
  plugins: [
    configMergerPlugin({
      sources: [".devserverrc.json", "devserver.config.json"],
    }),
    aiAgentPlugin({ verbose: true }),
  ],
});

Custom Metrics Plugin

// plugins/metrics.ts
import { createPlugin } from "@bunli/core/plugin";

interface MetricsStore {
  metrics: {
    events: Array<{
      name: string;
      timestamp: Date;
      data: Record<string, any>;
    }>;
    recordEvent: (name: string, data?: Record<string, any>) => void;
    getEvents: (
      name?: string,
    ) => Array<{ name: string; timestamp: Date; data: Record<string, any> }>;
    clearEvents: () => void;
  };
}

export const metricsPlugin = createPlugin<MetricsStore>({
  name: "metrics" as const,
  store: {
    metrics: {
      events: [],
      recordEvent(name: string, data: Record<string, any> = {}) {
        this.events.push({
          name,
          timestamp: new Date(),
          data,
        });

        // Keep only last 100 events to prevent memory leaks
        if (this.events.length > 100) {
          this.events = this.events.slice(-100);
        }
      },
      getEvents(name?: string) {
        if (name) {
          return this.events.filter((event) => event.name === name);
        }
        return [...this.events];
      },
      clearEvents() {
        this.events = [];
      },
    },
  },

  beforeCommand({ store, command }) {
    // Record command start
    store.metrics.recordEvent("command_started", {
      command,
      timestamp: new Date().toISOString(),
    });
  },

  afterCommand({ store, command }) {
    // Record command completion
    store.metrics.recordEvent("command_completed", {
      command,
      timestamp: new Date().toISOString(),
    });
  },
});

Key Features Demonstrated

1. Plugin Context Access

handler: async ({ flags, context, spinner, colors }) => {
  const { port, host, watch, open } = flags;

  spinner.start("Starting development server...");

  // Access plugin context
  if (context?.store.metrics) {
    context.store.metrics.recordEvent("server_started", { port, host });
  }

  if (context?.store.config) {
    console.log(colors.dim(`Config loaded: ${JSON.stringify(context.store.config, null, 2)}`));
  }

  // Continue with server startup
};

2. Long-running Processes

handler: async ({ flags, colors, context }) => {
  // Start server
  console.log(colors.green(`Server started on http://${host}:${port}`));

  // Keep the process alive
  process.on("SIGINT", () => {
    console.log(colors.yellow("\nShutting down server..."));

    // Record shutdown event
    if (context?.store.metrics) {
      context.store.metrics.recordEvent("server_shutdown", {
        timestamp: new Date().toISOString(),
      });
    }

    process.exit(0);
  });

  // Simulate server running
  await new Promise(() => {}); // Never resolves
};

3. Real-time Operations

handler: async ({ flags, colors, context }) => {
  const { follow, lines, level, service } = flags;

  if (follow) {
    console.log(colors.cyan("Following logs (Press Ctrl+C to stop)..."));

    // Simulate log streaming
    const interval = setInterval(() => {
      const timestamp = new Date().toISOString();
      const logLevel = getRandomLogLevel();
      const message = getRandomMessage();

      console.log(`${colors.dim(timestamp)} ${levelColor(logLevel)} ${message}`);
    }, 1000);

    // Handle Ctrl+C
    process.on("SIGINT", () => {
      clearInterval(interval);
      console.log(colors.yellow("\nStopped following logs"));
      process.exit(0);
    });
  }
};

4. Configuration Management

// Configuration is loaded automatically by the config plugin
// Access it through plugin context
if (context?.store.config) {
  const config = context.store.config;

  // Use configuration values
  const defaultPort = config.port || 3000;
  const defaultHost = config.host || "localhost";

  console.log(colors.dim(`Using config: ${JSON.stringify(config, null, 2)}`));
}

Project Structure

dev-server/
├── cli.ts              # Main CLI file
├── commands/
│   ├── start.ts        # Start development server
│   ├── build.ts        # Build for production
│   ├── env.ts          # Environment management
│   └── logs.ts         # Log viewing and following
├── plugins/
│   └── metrics.ts      # Custom metrics plugin
├── bunli.config.ts     # Build configuration
├── package.json        # Dependencies and scripts
└── README.md          # Example documentation

Running the Example

# Navigate to the example
cd examples/dev-server

# Install dependencies
bun install

# Run in development mode
bun run dev

# Try the commands
bun run cli.ts start --port 3000 --watch
bun run cli.ts build --minify --sourcemap
bun run cli.ts env --set DEBUG=true
bun run cli.ts logs --follow

Plugin System Deep Dive

1. Plugin Lifecycle Hooks

export const myPlugin = createPlugin({
  name: "my-plugin" as const,
  store: { count: 0 },

  // Called before any command runs
  beforeCommand({ store, command }) {
    store.count++;
    console.log(`Command ${command} starting...`);
  },

  // Called after command completes successfully
  afterCommand({ store, command }) {
    console.log(`Command ${command} completed`);
  },

  // Called when command throws an error
  onError({ store, error, command }) {
    console.error(`Command ${command} failed:`, error.message);
  },
});

2. Type-safe Plugin Stores

interface MyStore {
  data: {
    users: string[];
    settings: Record<string, any>;
  };
  addUser: (user: string) => void;
  getSettings: () => Record<string, any>;
}

export const myPlugin = createPlugin<MyStore>({
  name: "my-plugin" as const,
  store: {
    data: {
      users: [],
      settings: {},
    },
    addUser(user: string) {
      this.data.users.push(user);
    },
    getSettings() {
      return this.data.settings;
    },
  },
});

3. Plugin Configuration

Plugin options are passed at creation time via the factory pattern. Wrap createPlugin in a factory function to accept configuration:

// Plugin factory accepting options at creation time
export function configurablePlugin(options: { enabled: boolean }) {
  return createPlugin({
    name: "configurable" as const,
    store: { enabled: options.enabled },
  });
}

// Use with configuration
const cli = await createCLI({
  plugins: [configurablePlugin({ enabled: true })],
});

Configuration Management

1. Multiple Configuration Sources

import { configMergerPlugin } from "@bunli/plugin-config";

const cli = await createCLI({
  plugins: [
    configMergerPlugin({
      sources: [
        ".devserverrc.json", // Project-specific config
        "devserver.config.json", // Alternative config file
        "package.json", // Package.json config section
      ],
    }),
  ],
});

2. Configuration Access

// Access configuration in commands
handler: async ({ context, colors }) => {
  if (context?.store.config) {
    const config = context.store.config;

    // Use configuration values
    const port = config.port || 3000;
    const host = config.host || "localhost";

    console.log(colors.dim(`Config: ${JSON.stringify(config, null, 2)}`));
  }
};

Key Takeaways

  1. Plugin System: Extensible architecture with lifecycle hooks
  2. Type Safety: Full TypeScript support for plugin stores
  3. Configuration: Multi-source configuration management
  4. Long-running Processes: Server management with graceful shutdown
  5. Real-time Operations: Log following and live updates
  6. Production Patterns: Advanced patterns for complex CLIs

Next Steps

On this page