Creating Commands

Guide to creating your own CLI commands.

Overview

Commands are TypeScript classes that implement the TypedCommand interface. The CLI uses TypeDI for dependency injection and Zod for input validation.

Prerequisites

  • TypeScript knowledge
  • Familiarity with the CLI structure
  • Development environment set up

Basic Command Structure

Create a file in src/commands/{category}/{command-name}.ts:

import { getLogger } from "log4js";
import { Service } from "typedi";
import { z } from "zod";
import { CommandInputs, TypedCommand, TypedInputs } from "../base";

const INPUTS = [
    {
        name: "message",
        schema: z.string(),
        description: "Message to display",
        argument: true, // Makes it a positional argument
    },
    {
        name: "uppercase",
        schema: z.boolean(),
        description: "Convert message to uppercase",
        default: false,
    },
] as const satisfies CommandInputs;

@Service()
export class HelloCommand implements TypedCommand<typeof INPUTS> {
    readonly name = "hello";
    readonly description = "A simple hello command";
    readonly category = "common";
    readonly aliases = ["hi"];
    readonly inputs = INPUTS;

    private readonly logger = getLogger(HelloCommand.name);

    public async execute({ message, uppercase }: TypedInputs<typeof INPUTS>) {
        const output = uppercase ? message.toUpperCase() : message;
        this.logger.info(`Hello: ${output}`);
    }
}

Command Metadata

Required fields:

  • name - Command name (e.g., "hello")
  • description - Brief description
  • category - Category (common, ws, competencies, etc.)
  • inputs - Input definitions (can be empty array)
  • execute() - Command logic

Optional fields:

  • aliases - Alternative names (e.g., ["hi"])
  • demo - Loom video URL
  • example - Usage example markdown

Input Types

String:

{
    name: "ticketId",
    schema: z.string(),
    description: "The ticket ID",
}

Boolean:

{
    name: "force",
    schema: z.boolean(),
    description: "Force the operation",
    default: false,
}

Enum:

{
    name: "type",
    schema: z.enum(["feature", "bugfix"]),
    description: "Type of change",
}

Optional:

{
    name: "output",
    schema: z.string().optional(),
    description: "Output file path",
}

Positional argument:

{
    name: "ticketId",
    schema: z.string(),
    description: "The ticket ID",
    argument: true, // Makes it positional: wseng start LAMBDA-12345
}

Dependency Injection

Use constructor injection for services:

@Service()
export class SaveRcaCommand implements TypedCommand<typeof INPUTS> {
    // ... metadata ...

    constructor(
        private readonly rca: YamlRcaService,
        private readonly storage: YamlStorageService,
        private readonly ticketService: TicketServiceFactory,
    ) {}

    public async execute({ keepPage }: TypedInputs<typeof INPUTS>) {
        const parsed = await this.rca.validate();
        await this.storage.save("rca", parsed.ticket, keepPage);
    }
}

Services are automatically injected by TypeDI.

Registering Commands

Add your command to src/containers/all-commands.ts:

import { HelloCommand } from "../commands/common/hello-command";

export const ALL_COMMANDS: ServiceIdentifier<TypedCommand>[] = [
    // ... existing commands ...
    HelloCommand,
];

Testing Your Command

# Build
pnpm run build

# Run
wseng hello "World"
wseng hello "World" --uppercase
wseng hi "World"  # Using alias

Common Patterns

Get ticket from context:

import { TicketContext } from "../../services/ticket-context";

constructor(private readonly context: TicketContext) {}

public async execute(inputs: TypedInputs<typeof INPUTS>) {
    const ticket = await this.context.getTicketKey();
    // ...
}

Prompt user for input:

import { InputService } from "../../services/input/input-service";

constructor(private readonly input: InputService) {}

public async execute() {
    const answer = await this.input.getChoiceInput(
        "Choose action",
        [
            { name: "Continue", value: "continue" },
            { name: "Abort", value: "abort" },
        ]
    );
}

Understanding the CLI's internal architecture and patterns.

Browse existing commands for examples and patterns.