custom
Custom CLI Commands
Author your own CLI commands using Balda's command framework.
1) Create a command class
import { Command } from 'balda-js';
export default class HelloCommand extends Command {
static commandName = 'hello';
static description = 'Say hello';
static help = [
'Example: npx balda hello --name Alice',
];
static async handle(): Promise<void> {
const name = this.commandName || 'world';
console.log(`Hello, ${name}!`);
}
}
2) Add flags/args with decorators
import { flag, Command } from 'balda-js';
export default class HelloCommand extends Command {
@flag.string({ name: 'name', aliases: 'n', description: 'Your name' })
static name: string;
// ...
}
3) Make the CLI discover your commands
import { commandRegistry } from 'balda-js';
// All the commands must be in the src/commands directory
commandRegistry.setCommandsPattern('./src/commands/**/*.ts');
Run npx balda list to verify your command is loaded.
Important: Using Decorators with Static Properties
When using @flag and @arg decorators on static properties, do not use the declare keyword as it prevents the property from being emitted at runtime. Instead, use the definite assignment assertion (!):
export default class MyCommand extends Command {
@flag.string({ name: 'name' })
static name!: string; // ✅ Correct - property exists at runtime
}
❌ Incorrect:
export default class MyCommand extends Command {
@flag.string({ name: 'name' })
static declare name: string; // ❌ Wrong - property won't exist at runtime
}
The declare keyword tells TypeScript to not emit the property in the compiled JavaScript, which breaks the decorator's ability to set the value.
Flag Types
Balda supports multiple flag types with full type safety:
String Flags
import { Command, flag } from 'balda-js';
export default class MyCommand extends Command {
static commandName = 'mycommand';
static description = 'Example command';
@flag.string({
name: 'name',
aliases: ['n'],
description: 'User name',
defaultValue: 'guest',
})
static name: string;
static async handle(): Promise<void> {
console.log(`Hello, ${this.name}!`);
}
}
Usage: npx balda mycommand --name=Alice or npx balda mycommand -n Alice
Boolean Flags
@flag.boolean({
name: 'verbose',
aliases: ['v'],
description: 'Enable verbose output',
defaultValue: false,
})
static verbose: boolean;
Usage: npx balda mycommand --verbose or npx balda mycommand -v
Number Flags
@flag.number({
name: 'port',
aliases: ['p'],
description: 'Port number',
defaultValue: 3000,
})
static port: number;
Usage: npx balda mycommand --port=8080 or npx balda mycommand -p 8080
List Flags (Multiple Values)
List flags can be specified multiple times to collect an array of values:
@flag.list({
name: 'tag',
aliases: ['t'],
description: 'Add tags (can be specified multiple times)',
defaultValue: [],
})
static tags: string[];
Usage: npx balda mycommand --tag=frontend --tag=api --tag=v2
Or with aliases: npx balda mycommand -t frontend -t api -t v2
Complete Example:
import { Command, flag } from 'balda-js';
export default class DeployCommand extends Command {
static commandName = 'deploy';
static description = 'Deploy application with tags';
@flag.list({
name: 'tag',
aliases: ['t'],
description: 'Deployment tags',
defaultValue: [],
})
static tags: string[];
@flag.list({
name: 'env',
aliases: ['e'],
description: 'Environment variables (KEY=VALUE)',
parse: (value: string) => {
const [key, val] = value.split('=');
return `${key}=${val}`;
},
})
static envVars?: string[];
@flag.string({
name: 'region',
description: 'Deployment region',
required: true,
})
static region!: string;
static async handle(): Promise<void> {
console.log(`Deploying to ${this.region}`);
console.log(`Tags: ${this.tags.join(', ')}`);
if (this.envVars) {
console.log(`Environment: ${this.envVars.join(', ')}`);
}
}
}
Usage:
npx balda deploy \
--region=us-east-1 \
--tag=v1.0.0 \
--tag=production \
--tag=hotfix \
-e NODE_ENV=production \
-e DEBUG=false
Flag Options
All flag types support the following options:
| Option | Type | Description |
|---|---|---|
name | string | Flag name (defaults to property name) |
aliases | string | string[] | Short aliases for the flag |
description | string | Help text description |
required | boolean | Whether the flag is required |
defaultValue | varies by type | Default value if not provided |
parse | (value: any) => T | Custom parser function |
Note: For list flags, the parse function is called on each individual item, not the entire array.
Command Options
Commands can be configured with additional options using the options static property:
import { Command, CommandOptions } from 'balda-js';
export default class MyCommand extends Command {
static commandName = 'mycommand';
static description = 'Example command';
static options: CommandOptions = {
// Whether the command should keep the process alive after completion or not
keepAlive: true,
category: 'utility',
};
static async handle(): Promise<void> {
// Command implementation
}
}
Available Options
| Option | Type | Description |
|---|---|---|
keepAlive | boolean | Keep process alive after command completes (default: false) |
category | string | Group commands in help output ('generator', 'utility', etc.) |
deprecated | object | Mark command as deprecated with optional migration info |
validate | function | Custom validation function that runs before handle() |
Command Categories
Organize your commands in the list output by assigning them to categories:
export default class GenerateApiCommand extends Command {
static commandName = 'generate-api';
static description = 'Generate API boilerplate';
static options: CommandOptions = {
category: 'generator',
};
static async handle(): Promise<void> {
// Generate API code
}
}
Common categories: 'generator', 'setup', 'production', 'utility', 'database'
When you run npx balda list, commands will be grouped by category:
User Commands:
GENERATOR:
generate-api Generate API boilerplate
generate-model Generate database model
UTILITY:
clear-cache Clear application cache
Deprecated Commands
Mark commands as deprecated to guide users toward newer alternatives:
export default class OldCommand extends Command {
static commandName = 'old-deploy';
static description = 'Legacy deployment command';
static options: CommandOptions = {
deprecated: {
message: 'This command is deprecated',
replacement: 'deploy',
},
};
static async handle(): Promise<void> {
// Still functional but discouraged
}
}
When users run a deprecated command, they'll see a warning:
⚠️ Warning: This command is deprecated
Use 'deploy' instead.
Deprecated commands also appear with a [deprecated] tag in the list output.
Custom Validation
Add custom validation logic that runs before the command executes:
export default class DeployCommand extends Command {
static commandName = 'deploy';
static description = 'Deploy to production';
@flag.string({ name: 'env', required: true })
static environment!: string;
static options: CommandOptions = {
validate: (command) => {
const validEnvs = ['staging', 'production'];
if (!validEnvs.includes(command.environment)) {
console.error(`Invalid environment. Must be one of: ${validEnvs.join(', ')}`);
return false;
}
return true;
},
};
static async handle(): Promise<void> {
console.log(`Deploying to ${this.environment}...`);
}
}
The validate function:
- Receives the command class as a parameter
- Must return
booleanorPromise<boolean> - Runs after flags are parsed but before
handle() - If it returns
false, the command will not execute
Complete Example
import { Command, CommandOptions, flag } from 'balda-js';
export default class BackupCommand extends Command {
static commandName = 'backup';
static description = 'Create database backup';
static options: CommandOptions = {
category: 'database',
validate: async (command) => {
// Check if database is accessible
const isConnected = await checkDatabaseConnection();
if (!isConnected) {
console.error('Cannot connect to database');
return false;
}
return true;
},
};
@flag.string({
name: 'output',
aliases: ['o'],
description: 'Output directory',
defaultValue: './backups',
})
static outputDir!: string;
@flag.boolean({
name: 'compress',
description: 'Compress backup file',
defaultValue: true,
})
static compress!: boolean;
static async handle(): Promise<void> {
console.log(`Creating backup in ${this.outputDir}`);
console.log(`Compression: ${this.compress ? 'enabled' : 'disabled'}`);
// Backup logic here
}
}