Skip to content

Make commands

Spinning up a new slash command, event handler, or middleware class from scratch is the kind of thing you’d forget the exact shape of after a couple of weeks away from the code. You’d open another file, copy it, paste it, rename the class, tweak the decorator, and hope you didn’t miss an import. Warden takes that chore away from you with a family of make:* generators that hand you a working file with the correct decorator, imports, and boilerplate — one class per file, one purpose per class, exactly the way the rest of the framework is organised.

Every generator is available through the warden binary, which ships with @warden/console. If you scaffolded your project with create-@warden it’s already installed; if you set things up by hand, a quick pnpm add -D @warden/console gets you going.

Terminal window
pnpm warden make:command Warn

That’s all it takes to stub a brand new slash command. Your new file appears at src/commands/WarnCommand.ts, wired up and ready for you to start filling in the actual behaviour.

You’ll find a generator for every handler type the framework supports:

Console commandWhat it generates
make:commandSlash command class
make:eventEvent handler class
make:middlewareMiddleware class
make:buttonButton interaction handler
make:modalModal submission handler
make:menuSelect menu handler
make:contextContext menu command (user or message)
make:errorError handler class
make:jobCron job class
make:decomposeCustom event decomposer

We’ll walk through each in turn, but before we do there are a handful of flags every generator shares — worth knowing up front.

No matter which generator you reach for, the following flags are always available:

FlagEffect
-f, --forceOverwrite an existing file without prompting
--dry-runPrint what would be created without touching disk
-v, --verboseShow the rendered file contents in dry-run output
--no-interactionFail instead of prompting when required input is missing

If you ever forget what a particular generator accepts, --help shows you the full signature:

Terminal window
pnpm warden make:command --help

Slash commands are the bread and butter of most Discord bots, so make:command is the generator you’ll reach for most often. Give it a class name and it takes care of the rest:

Terminal window
pnpm warden make:command Warn

Open the newly created src/commands/WarnCommand.ts and you’ll see:

import {command, reply} from '@warden/core';
import type {Command, Params} from '@warden/core';
import type {ChatInputCommandInteraction} from 'discord.js';
@command({name: 'warn', description: 'A command'})
export default class WarnCommand implements Command {
public async execute(interaction: ChatInputCommandInteraction, params: Params): Promise<void> {
await reply(interaction, 'Hello from warn!');
}
}

There are a couple of things worth calling out. First, the class name you pass in is normalised to PascalCase and the Command suffix is appended automatically if you didn’t already include it — so warn, Warn, and WarnCommand all produce the same file. Second, the slash command’s name: in the decorator is derived by stripping that suffix and kebab-casing the rest, which gives you warn instead of warn-command. ModStatsCommand becomes mod-stats, and so on. It’s the kind of polish you wouldn’t think to add yourself but will appreciate every day.

If you’d rather set the description up front instead of editing the decorator afterwards, pass --description:

Terminal window
pnpm warden make:command Warn --description "Warn a user"

Most bots react to something happening in Discord — a member joining, a role being added, a message being flagged. Each of those lives in its own event handler. Running make:event gives you a class wired up to listen:

Terminal window
pnpm warden make:event MemberJoinLog --event guildMemberAdd

Which produces src/events/MemberJoinLogEvent.ts:

import {event, Logger} from '@warden/core';
import type {Event} from '@warden/core';
@event({name: 'MemberJoinLogEvent', event: 'guildMemberAdd'})
export default class MemberJoinLogEvent implements Event<'guildMemberAdd'> {
constructor(private logger: Logger) {}
public async execute(): Promise<void> {
this.logger.info('MemberJoinLogEvent fired');
}
}

Notice that the Logger is already injected into the constructor — because every generated handler is @injectable, you can add new dependencies just by listing them as constructor parameters and Warden will wire everything up. No extra imports, no registration step.

The --event flag defaults to ready if you leave it off — handy when you’re stubbing something quickly and plan to change the event afterwards, or when you genuinely are listening to ready.

Middleware is where you put the cross-cutting checks and setup that run before your slash commands: “is this user a moderator?”, “is this guild in the database?”, “has this member cooled down yet?”. Running make:middleware gives you a typed middleware class ready to slot into your command’s middleware option:

Terminal window
pnpm warden make:middleware IsModerator

The generated file at src/middleware/IsModerator.ts:

import {Middleware, injectable} from '@warden/core';
import type {NextFn} from '@warden/core';
import type {ChatInputCommandInteraction} from 'discord.js';
@injectable()
export default class IsModerator extends Middleware<ChatInputCommandInteraction> {
public async handle(
ctx: ChatInputCommandInteraction,
next: NextFn<ChatInputCommandInteraction>,
): Promise<void> {
await next(ctx);
}
}

The generator defaults to ChatInputCommandInteraction as the type parameter since that’s what you’ll want most of the time. If you’re writing middleware for button interactions or modal submissions instead, just swap the generic and the ctx type, and you’re off.

Every button in a Discord embed needs a handler on the backend. Warden routes them by custom ID, and make:button gives you a new handler class with a sensible custom ID already in place:

Terminal window
pnpm warden make:button ConfirmBan

Produces src/buttons/ConfirmBanButton.ts:

import {button} from '@warden/core';
import type {Button, Params} from '@warden/core';
import type {ButtonInteraction} from 'discord.js';
@button({customId: 'confirm-ban'})
export default class ConfirmBanButton implements Button {
public async execute(interaction: ButtonInteraction, params: Params): Promise<void> {
await interaction.reply({content: 'Button clicked!', ephemeral: true});
}
}

By default the custom ID is derived from the class name, but if you want a parameterised ID so you can carry state (like a user or case ID) through to the handler, pass --customId:

Terminal window
pnpm warden make:button ConfirmBan --customId "confirm-ban:{userId}"

Those {userId} placeholders are captured into params at runtime — see Components for the full story.

Modals collect structured input from users, and — like buttons — they route by custom ID. make:modal gives you a handler class ready to consume modal submissions:

Terminal window
pnpm warden make:modal WarnReason

Generated at src/modals/WarnReasonModal.ts:

import {modal} from '@warden/core';
import type {Modal, Params} from '@warden/core';
import type {ModalSubmitInteraction} from 'discord.js';
@modal({customId: 'warn-reason'})
export default class WarnReasonModal implements Modal {
public async execute(interaction: ModalSubmitInteraction, params: Params): Promise<void> {
await interaction.reply({content: 'Modal submitted!', ephemeral: true});
}
}

As with make:button, you can override the custom ID with --customId — useful when you want to carry context through from the button that opened the modal.

Select menus — string selects, role selects, channel selects — all route through the same @menu() decorator. Running make:menu gives you a handler for whichever kind you’re building:

Terminal window
pnpm warden make:menu ModAction

Produces src/menus/ModActionMenu.ts:

import {menu} from '@warden/core';
import type {SelectMenu, Params} from '@warden/core';
import type {AnySelectMenuInteraction} from 'discord.js';
@menu({customId: 'mod-action'})
export default class ModActionMenu implements SelectMenu {
public async execute(interaction: AnySelectMenuInteraction, params: Params): Promise<void> {
await interaction.reply({content: 'Selection made!', ephemeral: true});
}
}

The stub types interaction as AnySelectMenuInteraction so the same class handles whichever select variant you’re building. Narrow to StringSelectMenuInteraction or RoleSelectMenuInteraction once you know which kind you’re receiving.

Context menu commands are the ones users invoke by right-clicking a user or a message, and they come in exactly those two flavours. Because Warden needs to know which flavour you’re building, the generator takes a --type flag:

Terminal window
pnpm warden make:context WarnUser --type user
pnpm warden make:context FlagMessage --type message

If you leave --type off, it defaults to user. A make:context WarnUser --type user invocation produces src/context/WarnUserContext.ts:

import {context} from '@warden/core';
import type {ContextMenu, Params} from '@warden/core';
import type {UserContextMenuCommandInteraction} from 'discord.js';
@context({name: 'WarnUser', type: 'user'})
export default class WarnUserContext implements ContextMenu {
public async execute(interaction: UserContextMenuCommandInteraction, params: Params): Promise<void> {
const target = interaction.targetUser;
await interaction.reply(`Targeted ${target.username}`);
}
}

Context menu entries show up in Discord’s UI with whatever you put in name, so you’ll often want something more human-readable than the class name. Pass --contextName "Warn User" to the generator to set that up front, or edit the decorator afterwards.

When something in a handler throws, Warden routes the error through a pipeline of error handlers. make:error stubs out a new one for you:

Terminal window
pnpm warden make:error Command

Generated at src/errors/CommandHandler.ts:

import {errorHandler, ErrorHandler, Logger} from '@warden/core';
import type {ErrorContext} from '@warden/core';
@errorHandler()
export default class CommandHandler extends ErrorHandler {
constructor(private logger: Logger) {
super();
}
public async handle(error: Error, context: ErrorContext): Promise<void> {
this.logger.error(`Error in ${context.type} "${context.name}": ${error.message}`);
}
}

By default the generator produces a catch-all handler — one that sees every unhandled error from every handler type. That’s usually the right starting point. If you want to scope this handler to just slash commands, or just buttons, or just jobs, add a type parameter to the decorator once the file is generated:

@errorHandler({type: 'command'})

You can have as many error handlers as you like, and Warden dispatches to whichever one matches the handler type most specifically. Head over to Error handling if you want the full picture.

Need something to run every minute, every hour, or at the stroke of midnight? That’s a job. Running make:job gives you a class scheduled on a sensible default cadence:

Terminal window
pnpm warden make:job CleanExpiredWarnings

Which lands at src/jobs/CleanExpiredWarningsJob.ts:

import {job, every, Logger} from '@warden/core';
import type {Job} from '@warden/core';
@job({name: 'CleanExpiredWarningsJob', schedule: every.minute()})
export default class CleanExpiredWarningsJob implements Job {
constructor(private logger: Logger) {}
public async execute(): Promise<void> {
this.logger.info('CleanExpiredWarningsJob executed');
}
}

The schedule defaults to every.minute() because that’s the most useful thing to have running when you’re iterating. When you’re ready to commit to the real cadence, swap in every.hour(), every.day(), or a raw cron expression like '0 0 * * *'. See Jobs for the shortlist of helpers and what they expand to.

Discord’s gateway emits fairly coarse events — messageUpdate fires for any change at all, guildMemberUpdate for anything from a nickname change to a timeout to a role being added. Decomposers let you turn one raw Discord event into several named ones that match what you’re actually trying to handle. make:decompose stubs a new one:

Terminal window
pnpm warden make:decompose BannedLinkEdit --source messageUpdate --emit messageBannedLinkEdit

You get src/events/decompositions/BannedLinkEdit.ts:

import {decompose} from '@warden/core';
import type {GuildMember} from 'discord.js';
@decompose({source: 'messageUpdate', emit: 'messageBannedLinkEdit'})
export default class BannedLinkEdit {
public test(oldMember: GuildMember, newMember: GuildMember): boolean {
return false;
}
public extract(oldMember: GuildMember, newMember: GuildMember): [GuildMember] {
return [newMember];
}
}

--source defaults to guildMemberUpdate because that’s the most commonly decomposed event, and --emit defaults to the kebab-cased class name. The stub types arguments as GuildMember for the same reason — if you’re decomposing a different source event, swap the types in test() and extract() to match what that event gives you. The Event decomposition guide walks through the full pattern.

Sometimes you want to see what a generator will produce before committing it to disk — maybe to sanity-check the class name normalisation, or just to remind yourself of the stub’s shape. Every generator supports --dry-run:

Terminal window
pnpm warden make:command Warn --dry-run
# [info] Would create src/commands/WarnCommand.ts

Pair it with --verbose if you’d like to see the whole rendered file, not just the path:

Terminal window
pnpm warden make:command Warn --dry-run --verbose

Nothing is written to disk either way, so --dry-run is completely safe to run repeatedly while you’re figuring out the right class name.

If the target file already exists, the generator plays it safe: in an interactive terminal it asks you whether you want to overwrite, and in a non-interactive one (like CI) it refuses and exits with a non-zero code. When you know what you’re doing and want to skip the prompt — which you usually will when you’re regenerating a file you’ve been editing — pass --force:

Terminal window
pnpm warden make:command Warn --force