Command Architecture

The CLI uses TypeScript, TypeDI for dependency injection, Commander.js for argument 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 injectable services:

src/services/
├── ticket-service-factory.ts
├── file-service.ts
├── input/input-service.ts
└── git-service.ts

Integrations

Third-party integrations include:

src/integrations/
├── github/
├── google/
└── yaml/

Dependency Injection

Use the @Service() decorator and constructor injection:

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

The CLI uses separate containers for local commands and tests.

Configuration

Personal configuration lives in ~/.wseng. Project configuration lives in .wseng in the repository root. Access configuration through 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

Registered commands use these categories:

  • common for general commands such as commit and start-ticket
  • ws for WS.Eng commands such as save-plan and load-context
  • qc for QC commands
  • tk for ticket commands
  • cicd for CI commands

The category determines the invocation form. For example, common commands use wseng commit, while ws commands use wseng ws save-plan.

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 can be set with --debug and --quiet.

Error Handling

Use custom errors to provide useful messages:

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

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

Testing

Test commands with a local build and command invocation, or call them programmatically through the container.

Project Structure

src/
├── commands/
│   ├── common/
│   ├── ws/
│   ├── qc/
│   ├── tk/
│   └── cicd/
├── services/
├── integrations/
├── models/
├── utils/
└── containers/

Command Organization

Simple commands can use one file. Complex commands can use a folder with command-specific services/, models/, and constants/ directories.