Skip to main content

custom

Custom CLI Commands

Author your own CLI commands using Balda's command framework.

1) Create a command class

src/commands/hello.ts
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

src/commands/command_registry.ts
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:

OptionTypeDescription
namestringFlag name (defaults to property name)
aliasesstring | string[]Short aliases for the flag
descriptionstringHelp text description
requiredbooleanWhether the flag is required
defaultValuevaries by typeDefault value if not provided
parse(value: any) => TCustom 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
OptionTypeDescription
keepAlivebooleanKeep process alive after command completes (default: false)
categorystringGroup commands in help output ('generator', 'utility', etc.)
deprecatedobjectMark command as deprecated with optional migration info
validatefunctionCustom 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 boolean or Promise<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
}
}