Custom CLI Command
Vercube CLI is built on top of citty but exposes a class-based decorator layer that lets you define commands as TypeScript classes. Custom commands are loaded at runtime via jiti - no build step required.
How Custom Commands Work
- Define a class extending
BaseCommandand decorate it with@Command. - Annotate properties with
@Arg(positional) or@Flag(named option). - Register the class in
vercube.config.tsundercli.commands. - Run
vercube <name>- the CLI loads your config, resolves the class, injects dependencies, injects parsed args, and callsrun().
// src/Commands/Deploy.ts
import { BaseCommand, Command, Flag } from '@vercube/cli/toolkit';
@Command({ name: 'deploy', description: 'Deploy to production' })
export class DeployCommand extends BaseCommand {
@Flag({ name: 'env', description: 'Target environment', default: 'staging' })
public env: string;
public override async run(): Promise<void> {
console.log(`Deploying to ${this.env}...`);
}
}
// vercube.config.ts
import { defineConfig } from '@vercube/core';
import { DeployCommand } from './src/Commands/Deploy';
export default defineConfig({
cli: {
commands: [DeployCommand],
},
});
vercube deploy --env production
BaseCommand
Every command must extend BaseCommand and implement run().
import { BaseCommand } from '@vercube/cli/toolkit';
export abstract class BaseCommand {
protected container: Container; // CLI DI container - prefer @Inject instead
public abstract run(): Promise<void>;
}
Dependency Injection
Commands are resolved through the CLI DI container, so any property decorated with @Inject is automatically wired before run() is called.
import { Inject } from '@vercube/di';
import { Logger } from '@vercube/logger';
import { BaseCommand, Command } from '@vercube/cli/toolkit';
@Command({ name: 'status', description: 'Show application status' })
export class StatusCommand extends BaseCommand {
@Inject(Logger)
private readonly gLogger: Logger;
public override async run(): Promise<void> {
this.gLogger.info('Application is running.');
}
}
@Command
Class decorator that registers command metadata.
@Command(meta: CommandMeta)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Command name used on the CLI (vercube <name>) |
description | string | ✅ | Short description shown in --help |
subCommands | (new () => BaseCommand)[] | - | Child command classes (see Subcommands) |
@Arg
Property decorator for positional arguments (order-sensitive, no -- prefix).
@Arg(options: ArgOptions)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Argument name shown in usage (vercube cmd <name>) |
description | string | - | Short description shown in --help |
required | boolean | - | Whether the argument is required (default: false) |
@Command({ name: 'greet', description: 'Print a greeting' })
export class GreetCommand extends BaseCommand {
@Arg({ name: 'name', description: 'Name to greet', required: true })
public name: string;
public override async run(): Promise<void> {
console.log(`Hello, ${this.name}!`);
}
}
@Flag
Property decorator for named options (--flag-name value).
@Flag(options: FlagOptions)
| Field | Type | Required | Description |
|---|---|---|---|
name | string | ✅ | Flag name used on the CLI (--name) |
description | string | - | Short description shown in --help |
default | unknown | - | Default value - also used to infer type |
required | boolean | - | Whether the flag is required (default: false) |
type | 'string' | 'boolean' | 'number' | - | Explicit citty value type (inferred from default when omitted) |
The value type is automatically inferred from default:
@Flag({ name: 'limit', default: 10 }) // → type: 'number'
@Flag({ name: 'json', default: false }) // → type: 'boolean'
@Flag({ name: 'env', default: 'dev' }) // → type: 'string'
Full example with multiple flags:
@Command({ name: 'export', description: 'Export data' })
export class ExportCommand extends BaseCommand {
@Flag({ name: 'format', description: 'Output format', default: 'json' })
public format: string;
@Flag({ name: 'limit', description: 'Max records', default: 100 })
public limit: number;
@Flag({ name: 'pretty', description: 'Pretty-print output', default: false })
public pretty: boolean;
public override async run(): Promise<void> {
console.log(`Exporting ${this.limit} records as ${this.format} (pretty: ${this.pretty})`);
}
}
Subcommands
Commands can be nested by passing subCommands to @Command. Each subcommand is an independent BaseCommand class.
// src/Commands/DbMigrate.ts
@Command({ name: 'migrate', description: 'Run pending migrations' })
export class DbMigrateCommand extends BaseCommand {
@Flag({ name: 'dry-run', description: 'Preview without applying', default: false })
public dryRun: boolean;
public override async run(): Promise<void> {
console.log(this.dryRun ? 'Dry run - no changes applied.' : 'Migrations applied.');
}
}
// src/Commands/Db.ts
@Command({
name: 'db',
description: 'Database utilities',
subCommands: [DbMigrateCommand, DbSeedCommand],
})
export class DbCommand extends BaseCommand {
public override async run(): Promise<void> {}
}
Register only the top-level parent in cli.commands - subcommands are discovered automatically:
// vercube.config.ts
export default defineConfig({
cli: {
commands: [DbCommand], // DbMigrateCommand and DbSeedCommand are picked up automatically
},
});
Usage:
vercube db --help
vercube db migrate
vercube db migrate --dry-run
vercube db seed --env test
Registration via plugins
Plugins can call ctx.register(MyCommand) from setupCLI (on a BasePlugin subclass) or from the cli hook on an object / defineVercubePlugin (syntax sugar for the same shape). The CLI uses the same loadVercubeConfig pipeline as the runtime, so commands registered from plugins appear next to those listed under cli.commands. See Plugins.
Registration
Register commands in vercube.config.ts under cli.commands, or from a plugin’s setupCLI / cli hook. The CLI loads this file using jiti at startup - TypeScript is transpiled on-the-fly, so no separate build step is needed for your command files.
// vercube.config.ts
import { defineConfig } from '@vercube/core';
import { DeployCommand } from './src/Commands/Deploy';
import { DbCommand } from './src/Commands/Db';
export default defineConfig({
cli: {
commands: [
DeployCommand,
DbCommand, // registers DbCommand + all its subCommands
],
},
});
subCommands: [...] are discovered and registered automatically by the CLI.Full Example
A working example is available in the repository under examples/cli-commands. It demonstrates:
- A simple
greetcommand with@Arg,@Flag, and@Inject - A
dbparent command withmigrateandseedsubcommands - Registering everything in
vercube.config.ts