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 descriptioncategory- 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 URLexample- 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" },
]
);
}
Related Docs
Command Architecture
Understanding the CLI's internal architecture and patterns.
Command Reference
Browse existing commands for examples and patterns.