CLI Patterns
Command Structure
Use Commander.js for all CLI commands. Commands live in packages/cli/src/commands/.
Basic Command
typescript1import { Command } from 'commander' 2 3export const deployCommand = new Command('deploy') 4 .description('Deploy an application to Battery') 5 .argument('<path>', 'Path to the application directory') 6 .option('-e, --env <environment>', 'Target environment', 'production') 7 .option('--dry-run', 'Show what would be deployed without deploying') 8 .action(async (path, options) => { 9 // Implementation 10 })
Command with Subcommands
typescript1export const configCommand = new Command('config') 2 .description('Manage Battery configuration') 3 4configCommand 5 .command('set <key> <value>') 6 .description('Set a configuration value') 7 .action(async (key, value) => {}) 8 9configCommand 10 .command('get <key>') 11 .description('Get a configuration value') 12 .action(async (key) => {})
Output Formatting
Chalk for Colors
typescript1import chalk from 'chalk' 2 3// Status messages 4console.log(chalk.green('✓'), 'Deployment successful') 5console.log(chalk.yellow('!'), 'Warning: No auth detected') 6console.log(chalk.red('✗'), 'Error: Invalid credentials') 7 8// Emphasis 9console.log(chalk.bold('Scanning...')) 10console.log(chalk.dim('Press Ctrl+C to cancel')) 11 12// URLs and paths 13console.log(chalk.cyan('https://app.battery.dev')) 14console.log(chalk.gray('./src/config.ts'))
Ora for Spinners
typescript1import ora from 'ora' 2 3const spinner = ora('Scanning for credentials...').start() 4 5try { 6 const results = await scan(path) 7 spinner.succeed('Scan complete') 8} catch (error) { 9 spinner.fail('Scan failed') 10 throw error 11}
Tree Output
typescript1function printTree(items: string[], indent = 0): void { 2 const prefix = ' '.repeat(indent) 3 items.forEach((item, i) => { 4 const isLast = i === items.length - 1 5 const branch = isLast ? '└──' : '├──' 6 console.log(`${prefix}${branch} ${item}`) 7 }) 8} 9 10// Output: 11// ├── Detected: Next.js 12// ├── Found credentials: 13// │ ├── SNOWFLAKE_PASSWORD in .env 14// │ └── SALESFORCE_TOKEN in lib/api.ts 15// └── Deploying...
Error Handling
Exit Codes
typescript1export const ExitCode = { 2 Success: 0, 3 GeneralError: 1, 4 InvalidArgument: 2, 5 ConfigError: 3, 6 NetworkError: 4, 7 AuthError: 5, 8} as const 9 10process.exit(ExitCode.InvalidArgument)
Error Display
typescript1import chalk from 'chalk' 2 3function handleError(error: Error): never { 4 console.error() 5 console.error(chalk.red('Error:'), error.message) 6 7 if (error.cause) { 8 console.error(chalk.dim('Cause:'), String(error.cause)) 9 } 10 11 if (process.env.DEBUG) { 12 console.error(chalk.dim(error.stack)) 13 } 14 15 process.exit(ExitCode.GeneralError) 16}
Graceful Shutdown
typescript1process.on('SIGINT', () => { 2 console.log() 3 console.log(chalk.dim('Cancelled')) 4 process.exit(130) 5})
Interactive Prompts
Use @inquirer/prompts for user input.
Confirmation
typescript1import { confirm } from '@inquirer/prompts' 2 3const proceed = await confirm({ 4 message: 'Deploy to production?', 5 default: false, 6})
Selection
typescript1import { select } from '@inquirer/prompts' 2 3const environment = await select({ 4 message: 'Select environment', 5 choices: [ 6 { name: 'Production', value: 'production' }, 7 { name: 'Staging', value: 'staging' }, 8 { name: 'Development', value: 'development' }, 9 ], 10})
Text Input
typescript1import { input } from '@inquirer/prompts' 2 3const projectName = await input({ 4 message: 'Project name', 5 default: path.basename(process.cwd()), 6 validate: (value) => { 7 if (!/^[a-z0-9-]+$/.test(value)) { 8 return 'Must be lowercase alphanumeric with hyphens' 9 } 10 return true 11 }, 12})
Password Input
typescript1import { password } from '@inquirer/prompts' 2 3const token = await password({ 4 message: 'Enter your API token', 5 mask: '*', 6})
Configuration Files
Config Location
typescript1import { homedir } from 'os' 2import { join } from 'path' 3 4const CONFIG_DIR = join(homedir(), '.battery') 5const CONFIG_FILE = join(CONFIG_DIR, 'config.json') 6const CREDENTIALS_FILE = join(CONFIG_DIR, 'credentials.json')
Config Schema
typescript1interface BatteryConfig { 2 defaultOrg?: string 3 defaultEnvironment?: 'production' | 'staging' 4 telemetry?: boolean 5}
Patterns to Follow
- Always show progress - Use spinners for operations > 500ms
- Confirm destructive actions - Prompt before deleting or overwriting
- Support --json flag - For programmatic output
- Respect NO_COLOR - Disable colors when env var is set
- Use stderr for errors - Keep stdout clean for piping