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 seedCommand 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:
- File Extensions: Scans for
.ts,.tsx,.js,.jsx,.mjs,.mtsxfiles - Content Analysis: Checks for
defineCommandusage and Rempts imports - Exclusion Filters: Skips test files, node_modules, and generated files
- 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
- Use descriptive names - Command names should be clear and memorable
- Add descriptions - Help users understand what each command does
- Provide examples - Show common usage patterns in descriptions
- Group related commands - Use nested commands for logical organization
- Handle errors gracefully - Provide helpful error messages
- Keep handlers focused - Each command should do one thing well
Next Steps
- Learn about Type Inference for better autocomplete
- Add Validation to ensure correct input
- Explore Configuration options
- See Examples of real commands