CLI
@voltage/cli provides the voltage binary and a CLI framework built on top of nest-commander. Commands run inside a full NestJS application context — the entire module graph, DI container, and all registered services are available exactly as they are in the HTTP application.
Installation
Section titled “Installation”yarn add @voltage/cli @voltage/loggerBasic usage
Section titled “Basic usage”Packages that ship commands — such as @voltage/database — register CommandRunnerModule internally. All you need to do is register your own command classes as providers in the module they belong to:
@Module({ providers: [GreetCommand],})export class AppModule {}Defining a command
Section titled “Defining a command”Extend CommandRunner and decorate the class with @Command. Implement run() — it receives positional arguments as a string[] and parsed options as an object:
import { Command, CommandRunner } from '@voltage/cli';
@Command({ name: 'greet', description: 'Print a greeting', arguments: '<name>',})@Injectable()export class GreetCommand extends CommandRunner { async run([name]: string[]): Promise<void> { console.log(`Hello, ${name}!`); }}yarn voltage greet Alice# Hello, Alice!Options
Section titled “Options”Decorate a parser method with @Option. The method receives the raw string value, transforms or validates it, and returns the result. The options object passed to run() is assembled from all parser return values:
import { Command, CommandRunner, Option } from '@voltage/cli';
@Command({ name: 'export', description: 'Export data to a file',})@Injectable()export class ExportCommand extends CommandRunner { async run(_: string[], options: { directory: string; limit: number }): Promise<void> { console.log(`Exporting up to ${options.limit} records to ${options.directory}`); }
@Option({ flags: '-d, --directory <path>', description: 'Output directory', required: true, }) parseDirectory(value: string): string { return value; }
@Option({ flags: '-l, --limit <number>', description: 'Maximum number of records', defaultValue: 1000, }) parseLimit(value: string): number { return parseInt(value, 10); }}yarn voltage export --directory ./output --limit 500Subcommands
Section titled “Subcommands”Declare subcommands in a parent @Command via the subCommands array. Each subcommand is its own @SubCommand-decorated class:
import { Command, SubCommand, CommandRunner } from '@voltage/cli';
@SubCommand({ name: 'run', description: 'Run pending migrations' })@Injectable()export class MigrationRunCommand extends CommandRunner { constructor(private readonly db: DataSource) { super(); }
async run(): Promise<void> { await this.db.runMigrations(); }}
@SubCommand({ name: 'revert', description: 'Revert the last migration' })@Injectable()export class MigrationRevertCommand extends CommandRunner { constructor(private readonly db: DataSource) { super(); }
async run(): Promise<void> { await this.db.undoLastMigration(); }}
@Command({ name: 'migration', description: 'Database migration commands', subCommands: [MigrationRunCommand, MigrationRevertCommand],})@Injectable()export class MigrationCommand extends CommandRunner { async run(): Promise<void> {}}yarn voltage migration runyarn voltage migration revertSubcommands nest to any depth by including further subCommands arrays.
The CLI token
Section titled “The CLI token”The CLI injection token is provided as true when the application boots via the voltage binary. It is absent in the normal HTTP context. Use it with @Optional() to let services behave differently depending on how they are started:
import { CLI } from '@voltage/cli';
@Injectable()export class DatabaseService { constructor( @Optional() @Inject(CLI) private readonly isCLI: boolean = false, ) {}
onModuleInit() { if (this.isCLI) { // Skip cache warming, health checks, etc. } }}@voltage/database uses this pattern to decide whether to load migration files — only necessary when running migration commands, not when serving HTTP requests.
How the CLI boots
Section titled “How the CLI boots”When you run yarn voltage, the binary resolves your application module using the following priority:
- Compiled JS — searches
dist/for*.module.jsfiles. Prefers{appName}.module.js, thenapp.module.js, then the shortest path found. - TypeScript source — if no compiled output is found, searches
src/for*.module.tsand loads it via ts-node (transpile-only mode for speed, falling back to full ts-node).tsconfig-pathsis registered automatically if present, so path aliases resolve correctly.
In a monorepo (a project with an apps/ directory), the --app <name> flag is required. It narrows the search to dist/apps/{name}/ and apps/{name}/src/ respectively:
# Single-app projectyarn voltage <command>
# Monorepoyarn voltage --app website <command>yarn voltage --app admin database migration run