Command Architecture

Understanding the CLI's internal architecture, dependency injection, and design patterns.

Overview

The CLI is built with TypeScript, using TypeDI for dependency injection, Commander.js for CLI parsing, and Zod for validation.

Core Components

Commands

Commands implement TypedCommand<TInputs>:

interface TypedCommand<TInputs extends CommandInputs = CommandInputs> {
    name: string;
    description: string;
    category: string;
    aliases?: string[];
    inputs: TInputs;
    execute(inputs: TypedInputs<TInputs>): Promise<unknown>;
}

Services

Reusable business logic lives in services:

src/services/
├── ticket-service-factory.ts     # JIRA/GitHub ticket operations
├── file-service.ts                # File system operations
├── input/input-service.ts         # User prompts
├── git-service.ts                 # Git operations
└── ...

Services are injectable with @Service() decorator.

Integrations

Third-party integrations:

src/integrations/
├── jira/                          # JIRA API
├── github/                        # GitHub API
├── google/                        # Google Drive, Sheets
└── yaml/                          # YAML validation and storage

Controllers

For REST API and Data API:

src/controllers/
├── command-controller.ts          # Execute commands via HTTP
├── ticket-controller.ts           # Ticket data endpoints
└── ...

Dependency Injection

The CLI uses TypeDI for dependency injection:

@Service()
export class MyCommand implements TypedCommand {
    constructor(
        private readonly fileService: FileService,
        private readonly gitService: GitService,
    ) {}
}

Container isolation: Different containers for different contexts (CLI, Lambda, tests).

Configuration

Configuration lives in two places:

  1. Personal config: ~/.wseng (user-specific)
  2. Project config: .wseng (repository-specific)

Access via ConfigService:

@Service()
export class MyCommand {
    constructor(private readonly config: ConfigService) {}

    async execute() {
        const personal = await this.config.getPersonalConfiguration();
        const project = await this.config.getProjectConfiguration();
    }
}

Command Categories

Commands are organized by category:

  • common - General commands (commit, start-ticket, etc.)
  • competencies - Code quality commands (review-work, unit-test, etc.)
  • ws - Internal WS.Eng commands (build-codemap, save-plan, etc.)
  • qc - QC workflow commands (qc start, qc pass, etc.)
  • Product-specific (athena, cat, tap, tk, xo)

Category determines command invocation:

  • common: wseng commit
  • ws: wseng ws save-plan
  • athena: wseng athena graphql

Logging

Use log4js for logging:

import { getLogger } from "log4js";

const logger = getLogger("MyCommand");

logger.debug("Debug message");
logger.info("Info message");
logger.warn("Warning message");
logger.error("Error message");

Log levels configurable via --debug and --quiet flags.

Error Handling

Use custom error types for better error messages:

import { ValidationError } from "../../utils/error-utils";

if (!ticket) {
    throw new ValidationError("No ticket found in context");
}

Errors are automatically formatted and displayed to users.

Testing

Commands can be tested by:

  1. Manual testing: pnpm run build && wseng <command>
  2. Integration tests: Call commands programmatically via container

Project Structure

src/
├── commands/           # All commands
│   ├── common/
│   │   ├── commit-command.ts           # Simple command (single file)
│   │   └── record-demo-command/        # Complex command (folder)
│   │       ├── record-demo.command.ts
│   │       ├── services/
│   │       ├── models/
│   │       └── constants/
├── services/           # Reusable business logic
├── integrations/       # Third-party APIs
├── controllers/        # REST API controllers
├── models/             # Data models
├── utils/              # Utilities
├── containers/         # DI containers
└── lambdas/            # Lambda entry points

Command Organization

Simple commands: Single file (e.g., commit-command.ts)

Complex commands: Folder structure (e.g., record-demo-command/)

  • Main command file: {command-name}.command.ts
  • services/ - Command-specific services
  • models/ - Command-specific models
  • constants/ - Command-specific constants

Use folder structure when command has multiple services, models, or significant complexity.


Step-by-step guide to creating your first command.

How Python commands integrate with the TypeScript CLI.