Skip to content

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.

Terminal window
yarn add @voltage/cli @voltage/logger

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 {}

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}!`);
}
}
Terminal window
yarn voltage greet Alice
# Hello, Alice!

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);
}
}
Terminal window
yarn voltage export --directory ./output --limit 500

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> {}
}
Terminal window
yarn voltage migration run
yarn voltage migration revert

Subcommands nest to any depth by including further subCommands arrays.

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.

When you run yarn voltage, the binary resolves your application module using the following priority:

  1. Compiled JS — searches dist/ for *.module.js files. Prefers {appName}.module.js, then app.module.js, then the shortest path found.
  2. TypeScript source — if no compiled output is found, searches src/ for *.module.ts and loads it via ts-node (transpile-only mode for speed, falling back to full ts-node). tsconfig-paths is 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:

Terminal window
# Single-app project
yarn voltage <command>
# Monorepo
yarn voltage --app website <command>
yarn voltage --app admin database migration run