Reliverse Docs
LibsIntroductionCore Concepts

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 flags
  • store: Plugin store (if plugins are used)
  • env: Environment variables
  • cwd: Current working directory
  • set(key, value): Store data for command handlers
  • get(key): Retrieve stored data
  • exitCode: 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:

  1. Plugin Setup Hooks (during CLI initialization)
  2. Plugin Config Resolved Hooks (after config is finalized)
  3. Before Command Plugin Hooks (before each command)
  4. Global Before Hooks (before each command)
  5. Command Handler (the actual command logic)
  6. Global After Hooks (after each command)
  7. 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

On this page