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

@light-merlin-dark/merlin-cli

v1.0.8

Published

Blazingly fast CLI - Minimalware, Type-safe DI, Plugins!

Readme

███╗   ███╗███████╗██████╗ ██╗     ██╗███╗   ██╗
████╗ ████║██╔════╝██╔══██╗██║     ██║████╗  ██║
██╔████╔██║█████╗  ██████╔╝██║     ██║██╔██╗ ██║
██║╚██╔╝██║██╔══╝  ██╔══██╗██║     ██║██║╚██╗██║
██║ ╚═╝ ██║███████╗██║  ██║███████╗██║██║ ╚████║
╚═╝     ╚═╝╚══════╝╚═╝  ╚═╝╚══════╝╚═╝╚═╝  ╚═══╝

 ██████╗██╗     ██╗
██╔════╝██║     ██║
██║     ██║     ██║
██║     ██║     ██║
╚██████╗███████╗██║
 ╚═════╝╚══════╝╚═╝

Modern CLI framework for TypeScript & Bun
Type-safe • Minimal boilerplate • Plugin system • Built-in services

Why?

  • 90% less boilerplate than traditional CLI libraries (Commander.js, yargs, etc.)
  • Type-safe service registry with dependency injection out of the box
  • Lazy command loading for instant startup times
  • Built-in validation with Valibot integration
  • Plugin system for extensible functionality
  • Testing utilities included - no setup required
  • Bun-first with Node.js compatibility
  • Production-ready with comprehensive error handling

Installation

# Using npm
npm install @light-merlin-dark/merlin-cli

# Using bun (recommended)
bun add @light-merlin-dark/merlin-cli

Quick Start

Create a production-ready CLI in under 10 lines:

import { createCLI, createCommand } from '@light-merlin-dark/merlin-cli';

const cli = createCLI({
  name: 'my-tool',
  version: '1.0.0',
  commands: {
    greet: createCommand({
      name: 'greet',
      description: 'Greet someone',
      examples: ['greet World', 'greet "John Doe" --excited'],
      execute: ({ args, options }) => {
        const name = args[0] || 'World';
        const greeting = options.excited ? `Hello ${name}!!!` : `Hello ${name}`;
        console.log(greeting);
      }
    })
  }
});

cli.run();

Your CLI now includes:

  • ✅ Built-in help command (my-tool help)
  • ✅ Version command (my-tool version)
  • ✅ Error handling with stack traces
  • ✅ Service registry for dependency injection
  • ✅ TypeScript support with full type safety
  • ✅ Middleware support
  • ✅ Plugin system (optional)

Key Features

Type-Safe Service Registry

Built-in dependency injection with compile-time type safety:

import { createToken, LoggerToken } from '@light-merlin-dark/merlin-cli';

// Create custom service tokens
const DatabaseToken = createToken<Database>('database');
const CacheToken = createToken<Cache>('cache');

// Register services during bootstrap
cli.bootstrap = async (registry) => {
  const db = new Database(process.env.DATABASE_URL);
  await db.connect();
  registry.register(DatabaseToken, db);

  registry.register(CacheToken, new RedisCache());
};

// Use in commands with full type safety
createCommand({
  name: 'query',
  execute: async ({ args, registry }) => {
    const db = registry.get(DatabaseToken);      // Type: Database
    const cache = registry.get(CacheToken);      // Type: Cache
    const logger = registry.get(LoggerToken);    // Type: Logger

    const result = await db.query(args[0]);
    logger.success(`Query returned ${result.length} rows`);
  }
})

Lightning-Fast Lazy Loading

Load commands only when needed for instant CLI startup:

const cli = createCLI({
  name: 'my-cli',
  version: '1.0.0',
  commands: {
    // Lazy-loaded commands
    deploy: () => import('./commands/deploy.ts').then(m => m.default),
    test: () => import('./commands/test.ts').then(m => m.default),
    build: () => import('./commands/build.ts').then(m => m.default),

    // Or eagerly loaded
    version: createVersionCommand({ version: '1.0.0' })
  }
});

Result: CLI starts in milliseconds, even with 100+ commands

Built-in Services

Logger Service

const logger = registry.get(LoggerToken);

logger.info('Starting deployment...');
logger.success('✓ Deployment complete!');
logger.error('✗ Deployment failed');
logger.warn('⚠ Resource usage high');
logger.debug('Detailed debug info');  // Only with --verbose

Prompter Service

const prompter = registry.get(PrompterToken);

// Confirmation
const confirmed = await prompter.confirm('Delete database?', false);

// Text input
const name = await prompter.text('Enter project name:', {
  validate: (value) => value.length > 0
});

// Selection
const env = await prompter.select('Choose environment:', [
  { title: 'Production', value: 'prod' },
  { title: 'Staging', value: 'staging' },
  { title: 'Development', value: 'dev' }
]);

// Multi-select
const features = await prompter.multiselect('Select features:', [
  { title: 'Authentication', value: 'auth', selected: true },
  { title: 'Database', value: 'db' },
  { title: 'Cache', value: 'cache' }
]);

Valibot Integration

Built-in validation with Valibot for type-safe schemas:

import { createCommand, validate, string, minLength, email } from '@light-merlin-dark/merlin-cli';

createCommand({
  name: 'create-user',
  execute: async ({ args, options }) => {
    // Validate with Valibot schemas
    const username = validate(
      pipe(string(), minLength(3)),
      args[0],
      'username'
    );

    const userEmail = validate(
      email(),
      options.email,
      'email'
    );

    // Create user...
  }
})

Subcommands Made Easy

Create nested command hierarchies:

createCommand({
  name: 'docker',
  description: 'Docker management commands',
  subcommands: {
    ps: createCommand({
      name: 'ps',
      description: 'List containers',
      execute: () => {
        // List containers
      }
    }),
    logs: createCommand({
      name: 'logs',
      description: 'View container logs',
      execute: ({ args }) => {
        const container = args[0];
        // Show logs
      }
    })
  }
});

// Usage: my-cli docker ps
// Usage: my-cli docker logs my-container

Middleware System

Add validation, logging, authentication, or any cross-cutting concerns:

import type { Middleware } from '@light-merlin-dark/merlin-cli';

// Authentication middleware
const authMiddleware: Middleware = async (context, spec, next) => {
  const token = process.env.AUTH_TOKEN;
  if (!token) {
    throw new Error('Authentication required: Set AUTH_TOKEN environment variable');
  }

  // Verify token...
  await next();
};

// Logging middleware
const loggingMiddleware: Middleware = async (context, spec, next) => {
  const start = performance.now();
  console.log(`[${spec.name}] Starting...`);

  await next();

  const duration = performance.now() - start;
  console.log(`[${spec.name}] Completed in ${duration.toFixed(2)}ms`);
};

// Apply middleware
createCommand({
  name: 'deploy',
  middleware: [authMiddleware, loggingMiddleware],
  execute: async () => {
    // Deploy logic
  }
})

Custom Routing

Implement custom command routing logic:

const cli = createCLI({
  name: 'git-tool',
  version: '1.0.0',
  customRouter: (args) => {
    // Custom routing logic
    if (args[0] === 'commit' && args.includes('-m')) {
      return {
        command: 'commit',
        args: args.slice(1),
        skipNormalRouting: true
      };
    }
    return null; // Fall back to normal routing
  }
});

Plugin System

Extend your CLI with plugins:

// Enable plugins
const cli = createCLI({
  name: 'my-cli',
  version: '1.0.0',
  plugins: {
    enabled: true,
    autoLoad: true,           // Auto-load from node_modules
    allowLocal: true,         // Load from local directories
    searchPaths: ['./plugins']
  }
});

// Create a plugin: plugins/database-plugin.ts
import type { Plugin } from '@light-merlin-dark/merlin-cli';

export default {
  name: 'database',
  version: '1.0.0',
  description: 'Database management plugin',

  // Register services
  services: [{
    token: DatabaseToken,
    factory: () => new Database(),
    lifecycle: 'singleton'
  }],

  // Add commands
  commands: {
    'db:migrate': createCommand({
      name: 'db:migrate',
      description: 'Run database migrations',
      async execute({ registry }) {
        const db = registry.get(DatabaseToken);
        await db.migrate();
      }
    })
  },

  // Lifecycle hooks
  hooks: {
    beforeInit: async () => console.log('Database plugin initializing...'),
    afterInit: async () => console.log('Database plugin ready'),
    beforeCommand: async (cmd) => console.log(`Running ${cmd}`),
    afterCommand: async (cmd) => console.log(`Completed ${cmd}`)
  }
} satisfies Plugin;

Testing

Built-in testing utilities for easy CLI testing:

import { describe, it, expect } from 'bun:test';
import { createTestHarness, captureOutput } from '@light-merlin-dark/merlin-cli';

describe('my-cli', () => {
  it('should greet user', async () => {
    const harness = createTestHarness(cli);

    const output = await captureOutput(async () => {
      await harness.run(['greet', 'Alice']);
    });

    expect(output).toContain('Hello Alice');
  });

  it('should handle errors gracefully', async () => {
    const harness = createTestHarness(cli);

    await expect(
      harness.run(['unknown-command'])
    ).rejects.toThrow('Unknown command');
  });
});

Advanced Usage

Error Handling

const cli = createCLI({
  name: 'my-cli',
  version: '1.0.0',
  onError: async (error, context) => {
    const logger = context.registry?.get(LoggerToken);

    if (error.code === 'EACCES') {
      logger?.error('Permission denied. Try running with sudo.');
    } else if (error.code === 'ENOENT') {
      logger?.error(`File not found: ${error.path}`);
    } else {
      logger?.error(`Error: ${error.message}`);
    }
  },
  onBeforeRoute: async (context) => {
    // Log all commands
    console.log(`Executing: ${context.commandName}`);
  },
  onAfterRoute: async (context) => {
    // Cleanup or analytics
  }
});

Smart Release Automation

Built-in release automation for NPM packages:

import { SmartRelease } from '@light-merlin-dark/merlin-cli';

const release = new SmartRelease({
  packageName: '@my-org/my-cli',
  registryUrl: 'https://registry.npmjs.org/',
  autoCommit: true,
  autoPush: true,
  tagRelease: true
});

await release.run();

Progress Indicators

Built-in progress indicators for long-running operations:

import { createSpinner, createProgressBar } from '@light-merlin-dark/merlin-cli';

// Spinner
const spinner = createSpinner('Loading data...');
spinner.start();
await fetchData();
spinner.succeed('Data loaded!');

// Progress bar
const bar = createProgressBar({
  total: 100,
  format: 'Progress: {bar} {percentage}% | {value}/{total}'
});

for (let i = 0; i <= 100; i++) {
  bar.update(i);
  await sleep(10);
}
bar.complete();

Examples

See the examples directory for complete working examples:

API Reference

Core Functions

createCLI(config: CLIConfig): CLI

Creates a new CLI instance with the specified configuration.

createCommand(spec: CommandSpec): CommandDefinition

Creates a type-safe command with validation and middleware support.

createToken<T>(name: string): Token<T>

Creates a type-safe token for the service registry.

Built-in Tokens

  • LoggerToken - Logger service for console output
  • ConfigToken - CLI configuration object
  • PrompterToken - Interactive prompts service

Validation

All Valibot functions are re-exported for convenience:

  • validate() - Validate with custom error handling
  • string(), number(), boolean(), array(), object() - Type validators
  • minLength(), maxLength(), minValue(), maxValue() - Value constraints
  • email(), url(), regex() - Format validators
  • pipe(), union(), optional(), nullable() - Composition helpers

See Valibot documentation for complete API.

Best Practices

  1. Use lazy loading - Load commands on-demand for faster startup
  2. Leverage the service registry - Avoid global state and singletons
  3. Provide rich examples - At least 2-3 diverse examples per command
  4. Use Valibot for validation - Type-safe schemas with excellent error messages
  5. Test thoroughly - Use the built-in testing utilities
  6. Handle errors gracefully - Provide actionable error messages
  7. Use middleware wisely - Keep middleware focused and composable

Migration Guide

From Commander.js

Before (Commander.js):

import { Command } from 'commander';

const program = new Command();

program
  .name('my-tool')
  .version('1.0.0')
  .description('My CLI tool');

program
  .command('greet <name>')
  .option('-e, --excited', 'Add excitement')
  .action((name, options) => {
    const greeting = options.excited ? `Hello ${name}!!!` : `Hello ${name}`;
    console.log(greeting);
  });

program.parse();

After (Merlin CLI):

import { createCLI, createCommand } from '@light-merlin-dark/merlin-cli';

const cli = createCLI({
  name: 'my-tool',
  version: '1.0.0',
  commands: {
    greet: createCommand({
      name: 'greet',
      execute: ({ args, options }) => {
        const name = args[0];
        const greeting = options.excited ? `Hello ${name}!!!` : `Hello ${name}`;
        console.log(greeting);
      }
    })
  }
});

cli.run();

Benefits: Type safety, service registry, lazy loading, middleware support, plugin system

Performance

Benchmarks against popular CLI frameworks:

| Framework | Startup Time | Memory Usage | LOC for Basic CLI | |-----------|--------------|--------------|-------------------| | Merlin CLI | 8ms | 12MB | 10 lines | | Commander.js | 15ms | 18MB | 25 lines | | yargs | 45ms | 32MB | 35 lines | | oclif | 120ms | 45MB | 50+ lines |

Benchmarks run on Node.js 20 with Bun 1.2+

Requirements

  • Node.js 20.0.0 or higher
  • Bun 1.2.0 or higher (recommended)
  • TypeScript 5.0.0 or higher (for development)

License

MIT License - see LICENSE file for details.


Built by Robert E. Beckner III (Merlin)

Powered by: TypeScript • Bun • Valibot • Picocolors