Skip to main content
The @ooneex/command component is a lightweight CLI command framework. Each command implements the ICommand interface — getName, getDescription, and run — and registers itself with @decorator.command(). The run() entry point parses Bun.argv, resolves the matching command from the container by name, and executes it with fully typed options. Commands are resolved through @ooneex/container, so they support dependency injection and the full range of scopes.

Why this component

  • Decorator-based registration. @decorator.command() registers a class with the DI container and the global command registry in one step.
  • Type-safe options. The generic ICommand<Options> interface carries your option type through run().
  • Automatic argument parsing. run() parses named options, booleans, and positionals from Bun.argv and forwards them to the command.
  • Container-managed. Commands resolve through @ooneex/container, supporting Singleton, Request, and Transient scopes.
  • Structured errors. CommandException carries a machine-readable key, a message, and a data object, logged automatically on failure.
  • Code generation. commandCreate scaffolds a command class, its test stub, and a barrel export from built-in templates.

How it works

You implement ICommand, register it with @decorator.command(), then import the file so the decorator runs before you call run(). The entry point reads the command name from the third positional argument, looks it up by getName(), and invokes run() with the parsed options.
MemberPurpose
getName()Returns the command name used to match CLI input (e.g. db:seed).
getDescription()Returns a human-readable description of the command.
run(options)Executes the command with the parsed options.
The runtime pieces fit together as follows:
ExportRole
decorator.command(scope?)Registers a command class with the container and COMMANDS_CONTAINER.
run()Parses Bun.argv, resolves the command by name, and runs it.
getCommand(name)Resolves a single registered command by name, or null.
commandCreate(config)Scaffolds a command file and test file from templates.
COMMANDS_CONTAINERGlobal array of every registered command constructor.
When no command matches the requested name, or the command throws, run() logs the error via TerminalLogger and exits with code 1.

Decorator and usage

@decorator.command()

Registers a command class with the container and pushes it onto the global COMMANDS_CONTAINER. It accepts an optional EContainerScope (defaults to Singleton). Implement ICommand and register the class:
import { decorator } from "@ooneex/command";
import type { ICommand } from "@ooneex/command";

interface GreetOptions {
  name?: string;
}

@decorator.command()
class GreetCommand implements ICommand<GreetOptions> {
  public getName(): string {
    return "greet";
  }

  public getDescription(): string {
    return "Greet a user by name";
  }

  public async run(options: GreetOptions): Promise<void> {
    const who = options.name ?? "World";
    console.log(`Hello, ${who}!`);
  }
}
Pass a scope when you need a fresh instance per resolution:
import { decorator } from "@ooneex/command";
import { EContainerScope } from "@ooneex/container";

@decorator.command(EContainerScope.Transient)
class BuildCommand implements ICommand {
  // ...
}

Running the entry point

Call run() in your CLI entry file. Import the files that register commands first so their decorators execute:
import { run } from "@ooneex/command";
import "./commands"; // imports files that register commands via @decorator.command()

await run();
Invoke from the terminal — the command name is the third positional argument:
bun run cli.ts greet --name Alice
# Hello, Alice!

Resolving a command manually

getCommand() returns a registered command by name, or null when none matches:
import { getCommand } from "@ooneex/command";

const command = getCommand("greet");

if (command) {
  await command.run({ name: "Bob" });
}

Injecting dependencies

Because commands resolve through @ooneex/container, you can inject services in the constructor:
import { decorator } from "@ooneex/command";
import { inject } from "@ooneex/container";

@decorator.command()
class MigrateCommand implements ICommand {
  constructor(@inject(MigrationService) private readonly migrations: MigrationService) {}

  public getName(): string {
    return "db:migrate";
  }

  public getDescription(): string {
    return "Run database migrations";
  }

  public async run(options: { drop?: boolean }): Promise<void> {
    await this.migrations.run({ drop: options.drop });
  }
}

Exceptions

The component throws CommandException for command-related errors. It extends Exception from @ooneex/exception, carries a machine-readable key, a human-readable message, and a data object, and reports an InternalServerError HTTP status. The key is supplied by you at throw time — choose a stable, descriptive value per failure.
new CommandException(message: string, key: string, data?: Record<string, unknown>)
Throw it from run() with a stable key and contextual data:
import { CommandException } from "@ooneex/command";

@decorator.command()
class ValidateCommand implements ICommand {
  public getName(): string {
    return "validate";
  }

  public getDescription(): string {
    return "Validate project configuration";
  }

  public async run(options: { name?: string }): Promise<void> {
    if (!options.name) {
      throw new CommandException("The --name option is required", "MISSING_NAME", {
        received: options,
      });
    }
  }
}
Catch it to inspect the structured fields:
import { CommandException } from "@ooneex/command";

try {
  await command.run(options);
} catch (error) {
  if (error instanceof CommandException) {
    logger.error(`Command error [${error.key}]: ${error.message}`, error.data);
  } else {
    throw error;
  }
}
When run() catches an exception it logs it via TerminalLogger and exits with code 1, so uncaught CommandExceptions surface automatically.

Best practices

  • Name commands namespace:action. Use a colon-separated scheme like db:migrate or user:import so related commands group naturally.
  • Type your options. Pass an options type to ICommand<Options> and define every required and optional flag, instead of relying on Record<string, unknown>.
  • Import command files before run(). Decorators only register a command when its module is loaded; keep a barrel file and import it first.
  • Inject dependencies, don’t construct them. Resolve services through the container constructor so commands stay testable.
  • Throw CommandException with a stable key. Keep the key constant per failure and put variable detail in data.
  • Keep run() focused. Validate options up front, fail fast with a clear exception, then perform the work.
  • Pick a scope deliberately. Default to Singleton; use Transient only when a command must not share state across resolutions.

CLI command

Scaffold a command class and its test file with the generator. It writes the class under modules/<module>/src/commands/<Name>Command.ts, a test under modules/<module>/tests/commands/<Name>Command.spec.ts, updates the commands.ts barrel export, and creates bin/command/run.ts for the module if it is missing.
# Interactive: prompts for the name
ooneex command:create

# Provide the name
ooneex command:create --name=ImportUser

# Target a module and overwrite
ooneex command:create --name=ImportUser --module=auth --override
OptionDescriptionDefault
--nameCommand class name. Pass any casing; it is normalized to PascalCase and the Command suffix is appended automatically.Prompted if omitted
--moduleTarget module the class is generated into.shared
--overrideOverwrite an existing class without prompting.false
The generated class is a registered ICommand stub, ready for you to define its options and implement run():
import { type ICommand, decorator } from "@ooneex/command";

type CommandOptionsType = {
  name?: string;
};

@decorator.command()
export class ImportUserCommand<T extends CommandOptionsType = CommandOptionsType> implements ICommand<T> {
  public getName(): string {
    return "import:user";
  }

  public getDescription(): string {
    return "Execute import:user command";
  }

  public async run(options: T): Promise<void> {
    // TODO: Implement command logic
  }
}
Run a generated command from its module by its getName() value — extra arguments are forwarded to the command:
ooneex command:run import:user --name=fixtures
command:run scans every module under modules/ for a bin/command/run.ts, locates the command whose getName() matches, and spawns it. It exits with code 1 when the command is not found in any module or when it fails. See command:create and command:run for the full command references.

Use with Claude and Codex

The generator ships a matching command:create skill. It runs the scaffold and then guides your AI agent through completing the command — defining the options type and implementing run(). Initialize the skills once for your agent:
ooneex claude:init
Then ask Claude in natural language — it maps the request to the generator, runs it, and fills in the implementation:
Prompt
Create a command that imports users from a CSV file.
For example, the prompt above maps to command:create --name=ImportUser, then implements the run() method to read the CSV and persist the users.