Interactive Prompts
Build interactive CLI experiences with prompts and user input
Interactive Prompts
Create engaging CLI experiences with interactive prompts, confirmations, and selections.
Available Prompt Types
Rempts provides several built-in prompt utilities through the handler context:
export default defineCommand({
handler: async ({ prompt }) => {
// Text input
const name = await prompt('What is your name?')
// Confirmation
const confirmed = await prompt.confirm('Continue?')
// Selection
const choice = await prompt.select('Choose an option', {
choices: [
{ value: 'a', label: 'Option A' },
{ value: 'b', label: 'Option B' }
]
})
// Password input
const password = await prompt.password('Enter password:')
}
})Text Input Prompts
Basic Text Input
export default defineCommand({
name: 'init',
handler: async ({ prompt, colors }) => {
const projectName = await prompt('Project name:', {
default: 'my-project',
validate: (value) => {
if (!value) return 'Project name is required'
if (!/^[a-z0-9-]+$/.test(value)) {
return 'Project name can only contain lowercase letters, numbers, and dashes'
}
return true
}
})
console.log(colors.green('✓'), `Creating project: ${projectName}`)
}
})Multi-line Input
export default defineCommand({
name: 'note',
handler: async ({ prompt }) => {
const content = await prompt('Enter your note (Ctrl+D to finish):', {
multiline: true
})
console.log('Note saved with', content.split('\n').length, 'lines')
}
})Input with Validation
export default defineCommand({
name: 'register',
handler: async ({ prompt }) => {
const email = await prompt('Email address:', {
validate: (value) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!emailRegex.test(value)) {
return 'Please enter a valid email address'
}
return true
}
})
const age = await prompt('Age:', {
validate: (value) => {
const num = parseInt(value, 10)
if (isNaN(num)) return 'Please enter a number'
if (num < 18) return 'You must be 18 or older'
if (num > 150) return 'Please enter a valid age'
return true
},
transform: (value) => parseInt(value, 10)
})
console.log(`Registered: ${email}, age ${age}`)
}
})Confirmation Prompts
Basic Confirmation
export default defineCommand({
name: 'delete',
handler: async ({ prompt, colors, positional }) => {
const [file] = positional
const confirmed = await prompt.confirm(
`Are you sure you want to delete ${file}?`,
{ default: false }
)
if (confirmed) {
console.log(colors.red('✗'), `Deleted ${file}`)
} else {
console.log(colors.yellow('⚠'), 'Operation cancelled')
}
}
})Dangerous Operations
export default defineCommand({
name: 'deploy',
options: {
env: option(z.enum(['dev', 'staging', 'production']))
},
handler: async ({ flags, prompt, colors }) => {
if (flags.env === 'production') {
console.log(colors.yellow('⚠ WARNING: Deploying to production!'))
const confirmed = await prompt.confirm(
'This will affect live users. Continue?',
{ default: false }
)
if (!confirmed) {
console.log('Deployment cancelled')
process.exit(0)
}
// Double confirmation for critical operations
const reallyConfirmed = await prompt.confirm(
'Are you REALLY sure?',
{ default: false }
)
if (!reallyConfirmed) {
console.log('Deployment cancelled')
process.exit(0)
}
}
console.log(`Deploying to ${flags.env}...`)
}
})Selection Prompts
Single Selection
export default defineCommand({
name: 'create',
handler: async ({ prompt, colors }) => {
const projectType = await prompt.select('What type of project?', {
choices: [
{ value: 'web', label: 'Web Application' },
{ value: 'api', label: 'REST API' },
{ value: 'cli', label: 'CLI Tool' },
{ value: 'lib', label: 'Library' }
]
})
const framework = await prompt.select('Choose a framework:', {
choices: projectType === 'web' ? [
{ value: 'next', label: 'Next.js' },
{ value: 'remix', label: 'Remix' },
{ value: 'astro', label: 'Astro' }
] : [
{ value: 'hono', label: 'Hono' },
{ value: 'elysia', label: 'Elysia' },
{ value: 'express', label: 'Express' }
]
})
console.log(colors.green('✓'), `Creating ${projectType} with ${framework}`)
}
})Multi-Selection
export default defineCommand({
name: 'install',
handler: async ({ prompt, spinner }) => {
const features = await prompt.multiselect('Select features to install:', {
choices: [
{ value: 'auth', label: 'Authentication', selected: true },
{ value: 'db', label: 'Database', selected: true },
{ value: 'email', label: 'Email Service' },
{ value: 'storage', label: 'File Storage' },
{ value: 'cache', label: 'Caching Layer' },
{ value: 'queue', label: 'Job Queue' }
],
min: 1,
max: 4
})
const spin = spinner('Installing features...')
spin.start()
for (const feature of features) {
spin.update(`Installing ${feature}...`)
await new Promise(resolve => setTimeout(resolve, 1000))
}
spin.succeed(`Installed ${features.length} features`)
}
})Password Prompts
export default defineCommand({
name: 'login',
handler: async ({ prompt, colors }) => {
const username = await prompt('Username:')
const password = await prompt.password('Password:', {
mask: '*',
validate: (value) => {
if (value.length < 8) {
return 'Password must be at least 8 characters'
}
return true
}
})
// For sensitive operations, confirm password
const confirmPassword = await prompt.password('Confirm password:', {
mask: '*'
})
if (password !== confirmPassword) {
console.log(colors.red('✗'), 'Passwords do not match')
process.exit(1)
}
console.log(colors.green('✓'), 'Login successful')
}
})Complex Interactive Flows
Setup Wizard
export default defineCommand({
name: 'setup',
handler: async ({ prompt, colors, spinner }) => {
console.log(colors.blue('Welcome to the setup wizard!\n'))
// Step 1: Basic Info
const projectName = await prompt('Project name:', {
default: 'my-app'
})
const description = await prompt('Project description:', {
default: 'A Rempts CLI application'
})
// Step 2: Configuration
const useTypeScript = await prompt.confirm('Use TypeScript?', {
default: true
})
const features = await prompt.multiselect('Select features:', {
choices: [
{ value: 'tests', label: 'Testing framework', selected: true },
{ value: 'lint', label: 'Linting', selected: true },
{ value: 'format', label: 'Code formatting', selected: true },
{ value: 'git', label: 'Git repository', selected: true },
{ value: 'ci', label: 'CI/CD pipeline' }
]
})
// Step 3: Advanced Options
let database = null
if (await prompt.confirm('Configure database?')) {
database = await prompt.select('Database type:', {
choices: [
{ value: 'sqlite', label: 'SQLite' },
{ value: 'postgres', label: 'PostgreSQL' },
{ value: 'mysql', label: 'MySQL' },
{ value: 'mongodb', label: 'MongoDB' }
]
})
}
// Step 4: Confirmation
console.log('\n' + colors.blue('Configuration Summary:'))
console.log(` Name: ${projectName}`)
console.log(` Description: ${description}`)
console.log(` TypeScript: ${useTypeScript ? 'Yes' : 'No'}`)
console.log(` Features: ${features.join(', ')}`)
if (database) {
console.log(` Database: ${database}`)
}
const proceed = await prompt.confirm('\nProceed with setup?', {
default: true
})
if (!proceed) {
console.log(colors.yellow('Setup cancelled'))
process.exit(0)
}
// Step 5: Execute Setup
const spin = spinner('Setting up project...')
spin.start()
// Simulate setup steps
const steps = [
'Creating project structure',
'Installing dependencies',
'Configuring tools',
'Initializing git repository'
]
for (const step of steps) {
spin.update(step + '...')
await new Promise(resolve => setTimeout(resolve, 1000))
}
spin.succeed('Project setup complete!')
console.log(colors.green('\n✓'), `cd ${projectName} && @reliverse/rempts dev`)
}
})Interactive Configuration Editor
export default defineCommand({
name: 'config',
handler: async ({ prompt, colors }) => {
const action = await prompt.select('What would you like to do?', {
choices: [
{ value: 'view', label: 'View current configuration' },
{ value: 'edit', label: 'Edit configuration' },
{ value: 'reset', label: 'Reset to defaults' }
]
})
if (action === 'edit') {
const section = await prompt.select('Which section to edit?', {
choices: [
{ value: 'general', label: 'General Settings' },
{ value: 'api', label: 'API Configuration' },
{ value: 'advanced', label: 'Advanced Options' }
]
})
switch (section) {
case 'general':
const theme = await prompt.select('Color theme:', {
choices: [
{ value: 'auto', label: 'Auto (system)' },
{ value: 'light', label: 'Light' },
{ value: 'dark', label: 'Dark' }
]
})
const verbose = await prompt.confirm('Enable verbose logging?')
console.log(colors.green('✓'), 'Settings updated')
break
case 'api':
const endpoint = await prompt('API endpoint:', {
default: 'https://api.example.com',
validate: (value) => {
try {
new URL(value)
return true
} catch {
return 'Please enter a valid URL'
}
}
})
const timeout = await prompt('Request timeout (seconds):', {
default: '30',
validate: (value) => {
const num = parseInt(value, 10)
if (isNaN(num) || num <= 0) {
return 'Please enter a positive number'
}
return true
}
})
console.log(colors.green('✓'), 'API configuration updated')
break
}
}
}
})Prompt Options
Common Options
interface PromptOptions {
// Default value
default?: string
// Validation function
validate?: (value: string) => boolean | string
// Transform the value after validation
transform?: (value: string) => any
// Custom error message
error?: string
// Allow empty input
required?: boolean
}Select Options
interface SelectOptions<T> {
// Available choices
choices: Array<{
value: T
label: string
hint?: string
}>
// Default selected value
default?: T
// Maximum items to display
maxVisible?: number
}Best Practices
- Provide Defaults: Always offer sensible defaults
- Validate Input: Catch errors early with validation
- Show Progress: Use spinners for long operations
- Confirm Dangerous Actions: Double-check destructive operations
- Group Related Prompts: Create logical flows
- Handle Cancellation: Allow users to exit gracefully
Error Handling
export default defineCommand({
handler: async ({ prompt, colors }) => {
try {
const input = await prompt('Enter value:')
// Process input
} catch (error) {
if (error.message === 'Prompt cancelled') {
console.log(colors.yellow('\nOperation cancelled'))
process.exit(0)
}
throw error
}
}
})Type-Safe Interactive Prompts
// Get command metadata for dynamic prompts
const commands = listCommands()
const setupApi = getCommandApi('setup')
// Use command options for dynamic prompt generation
const availablePresets = Object.keys(setupApi.options.preset?.choices || {})
const projectTypes = Object.keys(setupApi.options.type?.choices || {})
// Type-safe interactive command execution
async function runInteractiveCommand(commandName: string, options: any) {
const command = getCommandApi(commandName as any)
// Validate options against command schema
for (const [key, value] of Object.entries(options)) {
if (command.options[key]) {
// Type-safe option validation
console.log(`Setting ${key}: ${value}`)
}
}
}This enables:
- Dynamic prompt generation from command metadata
- Type-safe option validation in interactive flows
- Command discovery for building wizards
- IntelliSense for command-specific options
Testing Interactive Commands
import { test, expect } from '@reliverse/rempts-test'
import { createTestCLI } from '@reliverse/rempts-test'
test('interactive setup', async () => {
const cli = createTestCLI()
// Mock prompt responses
cli.mockPrompts([
'my-project', // Project name
true, // Use TypeScript
['tests', 'git'], // Features
true // Confirm
])
const result = await cli.run(['setup'])
expect(result.exitCode).toBe(0)
expect(result.output).toContain('Project setup complete')
})Next Steps
- Testing - Test interactive commands
- Building Your First CLI - Complete example
- @reliverse/rempts-utils - Utility functions reference