@light-merlin-dark/merlin-cli
v1.0.8
Published
Blazingly fast CLI - Minimalware, Type-safe DI, Plugins!
Maintainers
Readme
███╗ ███╗███████╗██████╗ ██╗ ██╗███╗ ██╗
████╗ ████║██╔════╝██╔══██╗██║ ██║████╗ ██║
██╔████╔██║█████╗ ██████╔╝██║ ██║██╔██╗ ██║
██║╚██╔╝██║██╔══╝ ██╔══██╗██║ ██║██║╚██╗██║
██║ ╚═╝ ██║███████╗██║ ██║███████╗██║██║ ╚████║
╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═══╝
██████╗██╗ ██╗
██╔════╝██║ ██║
██║ ██║ ██║
██║ ██║ ██║
╚██████╗███████╗██║
╚═════╝╚══════╝╚═╝
Modern CLI framework for TypeScript & Bun
Type-safe • Minimal boilerplate • Plugin system • Built-in servicesWhy?
- 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-cliQuick 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 --verbosePrompter 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-containerMiddleware 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:
- Basic CLI - Simple todo list manager
- API Client - REST API client with CRUD operations
- Database Migrations - SQL migration tool
- Plugin System - CLI with plugin support
- Arguments & Options - Advanced argument handling
- Subcommands - Nested command hierarchies
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 outputConfigToken- CLI configuration objectPrompterToken- Interactive prompts service
Validation
All Valibot functions are re-exported for convenience:
validate()- Validate with custom error handlingstring(),number(),boolean(),array(),object()- Type validatorsminLength(),maxLength(),minValue(),maxValue()- Value constraintsemail(),url(),regex()- Format validatorspipe(),union(),optional(),nullable()- Composition helpers
See Valibot documentation for complete API.
Best Practices
- Use lazy loading - Load commands on-demand for faster startup
- Leverage the service registry - Avoid global state and singletons
- Provide rich examples - At least 2-3 diverse examples per command
- Use Valibot for validation - Type-safe schemas with excellent error messages
- Test thoroughly - Use the built-in testing utilities
- Handle errors gracefully - Provide actionable error messages
- 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
