Reliverse Docs
LibsRemptsCore concepts

Commands

Learn how to define and organize commands in Rempts

Commands

Commands are the building blocks of your CLI. Rempts provides a powerful, type-safe way to define commands with automatic inference and validation.

Basic Command

The simplest command requires just a name and handler:

import { defineCommand } from '@reliverse/rempts-core'

export default defineCommand({
  name: 'hello',
  description: 'Say hello',
  handler: async () => {
    console.log('Hello, World!')
  }
})

Command Options

Add typed options to your commands using the option() helper with Zod schemas:

import { option } from '@reliverse/rempts-core'
import { z } from 'zod'

export default defineCommand({
  name: 'greet',
  description: 'Greet someone',
  options: {
    name: option(
      z.string().min(1),
      { description: 'Name to greet', short: 'n' }
    ),
    times: option(
      z.coerce.number().int().positive().default(1),
      { description: 'Number of times to greet', short: 't' }
    ),
    loud: option(
      z.coerce.boolean().default(false),
      { description: 'Shout the greeting', short: 'l' }
    )
  },
  handler: async ({ flags }) => {
    // TypeScript knows the exact types from Zod schemas:
    // flags.name: string
    // flags.times: number
    // flags.loud: boolean
    
    for (let i = 0; i < flags.times; i++) {
      const greeting = `Hello, ${flags.name}!`
      console.log(flags.loud ? greeting.toUpperCase() : greeting)
    }
  }
})

All options must have a schema - there are no raw options in Rempts. This ensures type safety and validation. Use z.coerce for CLI inputs to handle string-to-type conversions automatically.

Handler Context

Every command handler receives a rich context object:

handler: async ({ flags, positional, shell, env, cwd, prompt, spinner, colors }) => {
  // flags - Parsed and validated command options
  // positional - Non-flag arguments
  // shell - Bun Shell for running commands (Bun.$)
  // env - Environment variables (process.env)
  // cwd - Current working directory
  // prompt - Interactive prompts (auto-imported from @reliverse/rempts-utils)
  // spinner - Progress indicators (auto-imported from @reliverse/rempts-utils)
  // colors - Terminal colors (auto-imported from @reliverse/rempts-utils)
}

Using Positional Arguments

export default defineCommand({
  name: 'copy',
  description: 'Copy files',
  handler: async ({ positional, shell }) => {
    const [source, dest] = positional
    
    if (!source || !dest) {
      throw new Error('Usage: copy <source> <dest>')
    }
    
    await shell`cp ${source} ${dest}`
  }
})

Shell Integration

Use Bun Shell directly in your commands:

handler: async ({ shell, flags }) => {
  // Run shell commands with automatic escaping
  const files = await shell`ls -la ${flags.dir}`.text()
  
  // Pipe commands
  const count = await shell`cat ${flags.file} | wc -l`.text()
  
  // Check exit codes
  try {
    await shell`test -f ${flags.file}`
    console.log('File exists')
  } catch {
    console.log('File not found')
  }
}

Interactive Prompts

handler: async ({ prompt, flags }) => {
  // Text input
  const name = await prompt('What is your name?')
  
  // Confirmation
  const proceed = await prompt.confirm('Continue?', { default: true })
  
  // Selection
  const color = await prompt.select('Favorite color?', {
    options: ['red', 'green', 'blue']
  })
  
  // Password (with masking)
  const secret = await prompt.password('Enter password:')
  
  // With schema validation
  const apiKey = await prompt('API Key:', {
    schema: z.string().min(32).regex(/^[A-Za-z0-9-_]+$/)
  })
}

Progress Indicators

handler: async ({ spinner, shell }) => {
  const spin = spinner('Applying changes (bun install)...')
  spin.start()
  
  try {
    await shell`bun install`
    spin.succeed('Dependencies installed')
  } catch (error) {
    spin.fail('Installation failed')
    throw error
  }
}

Nested Commands

Rempts supports nested commands through file-based directory structure. Commands are organized hierarchically by placing them in subdirectories:

File-Based Commands

Rempts uses file-based command discovery only. Commands are automatically discovered and loaded from a directory structure - manual command registration is not supported.

// cli.ts
import { createCLI } from '@reliverse/rempts-core'

const cli = await createCLI({
  name: 'my-cli',
  version: '1.0.0',
  commands: {
    directory: './cmds'
  }
})

await cli.run()

Directory Structure

Commands are organized in a cmds/ directory with automatic hierarchy detection:

cmds/
├── build/
│   └── cmd.ts        # → my-cli build
├── test/
│   └── cmd.ts        # → my-cli test
├── deploy/
│   └── cmd.ts        # → my-cli deploy
└── db/
    ├── migrate/
    │   └── cmd.ts    # → my-cli db migrate
    └── seed/
        └── cmd.ts    # → my-cli db seed

Command Files

Each command file exports a default command definition:

// cmds/build/cmd.ts
import { defineCommand, option } from '@reliverse/rempts-core'
import { z } from 'zod'

export default defineCommand({
  name: 'build',
  description: 'Build the project',
  options: {
    watch: option(z.boolean(), { description: 'Watch for changes' }),
    minify: option(z.boolean(), { description: 'Minify output' })
  },
  handler: async ({ flags, shell }) => {
    console.log('Building...')
    if (flags.watch) {
      await shell`bun run build --watch`
    } else {
      await shell`bun run build`
    }
  }
})
// cmds/db/migrate/cmd.ts
import { defineCommand } from '@reliverse/rempts-core'

export default defineCommand({
  name: 'migrate',
  description: 'Run database migrations',
  handler: async ({ shell }) => {
    await shell`bun run db:migrate`
  }
})

How It Works

File-based command loading provides several benefits:

  • Automatic Discovery: Commands are found by scanning the directory for TypeScript/JavaScript files
  • Nested Hierarchy: Directory structure maps directly to command hierarchy
  • Conflict Detection: Prevents duplicate command names across the same level
  • Lazy Loading: Commands are loaded on-demand when executed
  • Type Safety: Full TypeScript support

File Detection Rules

Rempts automatically identifies command files by:

  1. File Extensions: Scans for .ts, .tsx, .js, .jsx, .mjs, .mtsx files
  2. Content Analysis: Checks for defineCommand usage and Rempts imports
  3. Exclusion Filters: Skips test files, node_modules, and generated files
  4. Naming Convention: Converts file paths to command names (spaces for hierarchy)

Command Aliases

Add shortcuts for frequently used commands:

export default defineCommand({
  name: 'development',
  alias: ['dev', 'd'], // Can be string or array
  description: 'Start development server',
  handler: async () => {
    // Users can run any of:
    // my-cli development
    // my-cli dev
    // my-cli d
  }
})

// Aliases work with nested commands too
export default defineCommand({
  name: 'database',
  alias: 'db',
  commands: [
    defineCommand({
      name: 'migrate',
      alias: 'm',
      handler: async () => {
        // Can be called as:
        // my-cli database migrate
        // my-cli db migrate
        // my-cli db m
      }
    })
  ]
})

Error Handling

Rempts provides automatic error handling with formatted output:

// Schema validation errors are automatically caught and formatted
$ my-cli serve --port abc
Validation errors:
  --port:
    • Expected number, received nan

// Custom errors in handlers
handler: async ({ flags, colors }) => {
  if (flags.port < 1024 && !flags.sudo) {
    throw new Error('Ports below 1024 require sudo')
  }
}

// Unknown commands show help
$ my-cli unknown
Unknown command: unknown

// Automatic help generation
$ my-cli serve --help
Usage: my-cli serve [options]

Start the development server

Options:
  --port, -p    Port to listen on (default: 3000)
  --host, -h    Host to bind to (default: localhost)

Commands are lazy-loaded by default when using a manifest, improving startup time for CLIs with many commands.

Best Practices

  1. Use descriptive names - Command names should be clear and memorable
  2. Add descriptions - Help users understand what each command does
  3. Provide examples - Show common usage patterns in descriptions
  4. Group related commands - Use nested commands for logical organization
  5. Handle errors gracefully - Provide helpful error messages
  6. Keep handlers focused - Each command should do one thing well

Next Steps

On this page