Plugins
Extend Rempts's functionality with a powerful plugin system
Plugins
Rempts's plugin system provides a powerful way to extend your CLI's functionality with reusable components. The plugin system is designed with type safety at its core, ensuring that plugin data flows seamlessly through your application with full TypeScript support.
Overview
Plugins in Rempts can:
- Modify CLI configuration during setup
- Register new commands dynamically
- Hook into command lifecycle events
- Share type-safe data through a store system
- Extend core interfaces via module augmentation
Creating a Plugin
Basic Plugin Structure
A plugin is an object that implements the RemptsPlugin interface:
import { createPlugin } from '@reliverse/rempts-core/plugin'
const myPlugin = createPlugin({
name: 'my-plugin',
version: '1.0.0',
// Define type-safe store
store: {
count: 0,
message: '' as string
},
// Lifecycle hooks
setup(context) {
console.log('Plugin setup')
},
beforeCommand(context) {
context.store.count++ // TypeScript knows the type!
}
})Plugin with Options
For configurable plugins, use a factory function:
import { createPlugin } from '@reliverse/rempts-core/plugin'
interface MyPluginOptions {
prefix: string
verbose?: boolean
}
const myPlugin = createPlugin((options: MyPluginOptions) => ({
name: 'my-plugin',
store: {
messages: [] as string[]
},
beforeCommand({ store }) {
const message = `${options.prefix}: Command starting`
store.messages.push(message)
if (options.verbose) {
console.log(message)
}
}
}))
// Usage
myPlugin({ prefix: 'APP', verbose: true })Plugin Lifecycle
Plugins go through several lifecycle stages:
1. Setup Phase
The setup hook runs during CLI initialization. Use it to:
- Modify configuration
- Register commands
- Initialize resources
setup(context) {
// Update configuration
context.updateConfig({
description: 'Enhanced by my plugin'
})
// Register a command
context.registerCommand(defineCommand({
name: 'plugin-command',
handler: async () => {
console.log('Command from plugin!')
}
}))
}2. Config Resolved
The configResolved hook runs after all configuration is finalized:
configResolved(config) {
console.log(`CLI initialized: ${config.name} v${config.version}`)
}3. Before Command
The beforeCommand hook runs before each command execution:
beforeCommand(context) {
console.log(`Running command: ${context.command}`)
console.log(`Arguments: ${context.args.join(', ')}`)
// Access and modify store
context.store.lastCommand = context.command
}4. After Command
The afterCommand hook runs after command execution:
afterCommand(context) {
if (context.error) {
console.error(`Command failed: ${context.error.message}`)
} else {
console.log('Command completed successfully')
}
}Type-Safe Store System
The store system provides compile-time type safety for sharing data between plugins and commands.
Defining Store Types
interface TimingStore {
startTime: number | null
duration: number | null
}
const timingPlugin = createPlugin<TimingStore>({
name: 'timing',
store: {
startTime: null,
duration: null
},
beforeCommand({ store }) {
store.startTime = Date.now() // Type-safe!
},
afterCommand({ store }) {
if (store.startTime) {
store.duration = Date.now() - store.startTime
}
}
})Accessing Store in Commands
const cli = await createCLI({
name: 'my-cli',
plugins: [timingPlugin()] as const // Use 'as const' for better inference
})
// TypeScript knows about startTime and duration!
if (context?.store.startTime) {
console.log(`Started at: ${new Date(context.store.startTime)}`)
}
}
}))Multiple Plugins
When using multiple plugins, their stores are automatically merged:
const cli = await createCLI({
name: 'my-cli',
plugins: [
pluginA(), // store: { foo: string }
pluginB(), // store: { bar: number }
pluginC() // store: { baz: boolean }
] as const
})
// In commands, the merged store type is available:
// TypeScript knows about all store properties!
context.store.foo // string
context.store.bar // number
context.store.baz // boolean
}
}))Module Augmentation
Plugins can extend core interfaces using TypeScript's module augmentation:
declare module '@reliverse/rempts-core/plugin' {
interface EnvironmentInfo {
isDocker: boolean
containerName?: string
}
}
const dockerPlugin = createPlugin({
name: 'docker-detect',
beforeCommand({ env }) {
env.isDocker = !!process.env.DOCKER_CONTAINER
env.containerName = process.env.HOSTNAME
}
})Built-in Plugins
Rempts provides several built-in plugins:
@reliverse/rempts-plugin-config
Loads and merges configuration from multiple sources:
import { configMergerPlugin } from '@reliverse/rempts-plugin-config'
const cli = await createCLI({
plugins: [
configMergerPlugin({
sources: [
'~/.config/{{name}}/config.json',
'.{{name}}rc.json'
],
mergeStrategy: 'deep'
})
]
})@reliverse/rempts-plugin-ai-detect
Detects AI coding assistants from environment variables:
import { aiAgentPlugin } from '@reliverse/rempts-plugin-ai-detect'
const cli = await createCLI({
plugins: [
aiAgentPlugin({ verbose: true })
]
})
// In commands:
if (context?.env.isAIAgent) {
console.log(`AI agents: ${context.store.aiAgents.join(', ')}`)
}
}
}))Global Hooks System
In addition to plugin hooks, Rempts provides a global hooks system that allows you to run code before and after command execution across all commands in your CLI.
Before Hooks
Before hooks run before any command is executed and can set up shared context:
import { createCLI } from '@reliverse/rempts-core'
const cli = await createCLI({
name: 'my-cli',
version: '1.0.0'
})
// Global setup hook
cli.before(async (context) => {
// Initialize shared resources
const logger = setupLogger({
level: context.flags.verbose ? 'debug' : 'info',
silent: context.flags.quiet
})
const config = await loadConfig()
// Store data for command handlers
context.set('logger', logger)
context.set('config', config)
})
// Commands can access hook data
const logger = hooks?.logger
const config = hooks?.config
logger?.log('Starting build...')
// Use config for build settings
}
}))After Hooks
After hooks run after command completion and can perform cleanup or logging:
cli.after(async (context) => {
const logger = context.get('logger')
if (context.error) {
logger?.error(`Command failed: ${context.error.message}`)
} else {
logger?.info('Command completed successfully')
}
// Cleanup resources
await cleanupTempFiles()
})Hook Context
Both before and after hooks receive a context object with:
flags: Parsed command-line flagsstore: Plugin store (if plugins are used)env: Environment variablescwd: Current working directoryset(key, value): Store data for command handlersget(key): Retrieve stored dataexitCode: Exit code (after hooks only)error: Error object if command failed (after hooks only)
Hook Data in Commands
Data stored in before hooks is available to command handlers:
handler: async ({ flags, positional, shell, env, cwd, prompt, spinner, colors, hooks }) => {
// Access data set by before hooks
const logger = hooks?.logger
const config = hooks?.config
const userSession = hooks?.session
logger?.debug('Command starting...')
// Use shared configuration
if (config?.debug) {
console.log('Debug mode enabled')
}
}Hook Execution Order
When both global hooks and plugin hooks are present, they execute in this order:
- Plugin Setup Hooks (during CLI initialization)
- Plugin Config Resolved Hooks (after config is finalized)
- Before Command Plugin Hooks (before each command)
- Global Before Hooks (before each command)
- Command Handler (the actual command logic)
- Global After Hooks (after each command)
- After Command Plugin Hooks (after each command)
Error Handling in Hooks
Hooks should handle their own errors to prevent disrupting command execution:
cli.before(async (context) => {
try {
const config = await loadConfig()
context.set('config', config)
} catch (error) {
// Log error but don't throw - allow command to continue with defaults
console.warn('Failed to load config:', error.message)
context.set('config', { debug: false }) // Provide fallback
}
})Best Practices
1. Use Type-Safe Stores
Always define explicit types for your plugin stores:
interface MyStore {
items: string[]
count: number
}
// For direct plugins:
const plugin = createPlugin<MyStore>({
name: 'my-plugin',
store: { items: [], count: 0 }
})
// For plugin factories:
const plugin = createPlugin<Options, MyStore>((options) => ({
name: 'my-plugin',
store: { items: [], count: 0 }
}))
// ❌ Avoid - Less type safety
const plugin = createPlugin({
store: { items: [], count: 0 } // Types are inferred but less explicit
})2. Handle Errors Gracefully
Wrap plugin operations in try-catch blocks:
beforeCommand(context) {
try {
// Plugin logic
} catch (error) {
context.logger.warn(`Plugin error: ${error.message}`)
// Don't throw - let the command continue
}
}3. Use Plugin Context
Leverage the plugin context for shared functionality:
setup(context) {
// Use the logger
context.logger.info('Plugin loaded')
// Access paths
console.log(`Config dir: ${context.paths.config}`)
// Share data between plugins
context.store.set('shared-key', 'value')
}4. Document Plugin Options
Provide clear TypeScript interfaces for plugin options:
export interface MyPluginOptions {
/**
* Enable verbose logging
* @default false
*/
verbose?: boolean
/**
* Custom timeout in milliseconds
* @default 5000
*/
timeout?: number
}Plugin Loading
Plugins can be loaded in various ways:
const cli = await createCLI({
plugins: [
// Plugin object
myPlugin,
// Plugin factory
myPlugin({ verbose: true }),
// Path to plugin file
'./plugins/custom.js',
// Plugin with options as tuple
[myPlugin, { verbose: true }]
]
})Examples
Analytics Plugin
Track command usage:
interface AnalyticsStore {
commandCount: number
commandHistory: string[]
}
const analyticsPlugin = createPlugin<AnalyticsStore>({
name: 'analytics',
store: {
commandCount: 0,
commandHistory: []
},
beforeCommand({ store, command }) {
store.commandCount++
store.commandHistory.push(command)
},
afterCommand({ store }) {
if (store.commandCount % 10 === 0) {
console.log(`🎉 You've run ${store.commandCount} commands!`)
}
}
})Environment Plugin
Add environment-specific behavior:
interface EnvStore {
isDevelopment: boolean
isProduction: boolean
}
const envPlugin = createPlugin<EnvStore>({
name: 'env-plugin',
store: {
isDevelopment: process.env.NODE_ENV === 'development',
isProduction: process.env.NODE_ENV === 'production'
},
setup(context) {
if (context.store.isDevelopment) {
context.updateConfig({
// Enable debug features in development
debug: true
})
}
}
})Generated Types and Plugins
// Generated types include plugin context
interface CommandContext<TStore> {
store: TStore
// Plugin-specific context is included
}
// Access plugin store in commands
const infoApi = getCommandApi('info')
console.log(infoApi.plugins) // Available plugin data
// Type-safe plugin store access
function usePluginData(context: CommandContext<any>) {
// Access plugin store with full type safety
const timingData = context.store.timing
const configData = context.store.config
const aiData = context.store.aiAgent
}This enables:
- Plugin context types with full type safety
- Store access for all registered plugins
- Type-safe plugin data in command handlers
- IntelliSense for plugin-specific functionality
Generated types work seamlessly with plugins, providing type safety for plugin store access and context usage.
Next Steps
- Explore the Plugin API Reference for detailed API documentation
- Check out Built-in Plugins for ready-to-use plugins
- Learn about Testing Plugins in your CLI