# Commands

Create a slash command from two files, one that declares it to Discord and one that answers it. Covers the command file, deploying globally or to one server, codegen, the handler file, a command with no handler, and a constructor that throws.

A command file declares what Discord shows people. A handler file runs when someone uses it.

## The command file

This `/ban` command takes a user and an optional reason.

```ts title="src/commands/Ban.ts"
import {
    BuilderComponent,
    RegisterCommand
} from '@seedcord/gateway';

@RegisterCommand('global')
export class Ban extends BuilderComponent<'command'> {
    constructor() {
        super('command');

        this.instance
            .setName('ban')
            .setDescription('Ban a member')
            .addUserOption((option) =>
                option
                    .setName('target')
                    .setDescription('Who to ban')
                    .setRequired(true)
            )
            .addStringOption((option) =>
                option.setName('reason').setDescription('Why')
            );
    }
}
```

`'command'` picks which discord.js builder you get. TypeScript reads it from the generic and the constructor takes it at runtime.

`this.instance` is that builder. Every builder method you already know works on it.

[`BuilderComponent`](https://docs.seedcord.org/packages/core/latest/builder-component) calls `setContexts` with `Guild`, so your command only shows up in servers.

`setContexts` replaces the whole list every time it runs, so pass `Guild` again alongside whatever you are adding. `BotDM` covers DMs with your bot and `PrivateChannel` covers group DMs.

```ts
this.instance.setContexts(
    InteractionContextType.Guild,
    InteractionContextType.BotDM
);
```

The file goes under the `commands` path your `bot.ts` declares. seedcord imports every file under that path at startup and sends the commands to Discord.

## Global or one server

`'global'` puts the command in every server your bot is in. A global command takes a while to show up, and Discord doesn't publish how long that takes.

For one server, pass `'guild'` with the ids. The command appears there right away.

That speed is why a command you're still shaping belongs in a guild. You edit it, restart, and run it. Move it to `'global'` once the shape has settled, since every change after that waits on Discord.

The guild copy stays behind when you move it, and Discord shows both in the picker. [`seedcord commands`](/commands/deployed) finds the duplicates and deletes them.

```ts
@RegisterCommand('guild', ['613425648685547541'])
```

The ids are required. [`@RegisterCommand('guild')`](https://docs.seedcord.org/packages/core/latest/register-command) on its own matches neither overload, so TypeScript rejects it before you run anything. A JavaScript caller reaching the same line throws `DecoratorCommandGuildWithoutGuilds` at startup.

## Codegen

`seedcord codegen` imports every command file, constructs each command it finds, calls `toJSON()` on the builder, and writes `src/seedcord-gen.d.ts`.

```ts title="src/seedcord-gen.d.ts" output
declare module '@seedcord/gateway' {
    interface SlashRegistry {
        ban: {
            options: {
                target: { kind: 'user'; required: true };
                reason: { kind: 'string'; required: false };
            };
            cache: 'cached';
        };
    }
}
```

Commit that file. The next codegen run overwrites whatever you put there by hand, and a checkout without it fails `tc` on every handler.

[`SlashRegistry`](https://docs.seedcord.org/packages/core/latest/slash-registry) ships empty, so `keyof SlashRegistry` is `never` until codegen fills it in. A handler can't declare its route before that.

> **Warning**
>
> A stale generated file doesn't break anything at runtime, since nothing there reads these types. Run your project's `tc` script to catch it.
>
> You can turn on `hmr.typecheck` to run `tsc --watch` next to the bot during `seedcord dev`, at the cost of a second node process.
>
> ```ts title="seedcord.config.ts"
> import { defineConfig } from 'seedcord';
>
> export default defineConfig({
>     root: './src',
>     instance: './bot.ts',
>     entry: './index.ts',
>     hmr: {
>         typecheck: true
>     }
> });
> ```

Run `seedcord codegen` after you add a command or change its options. `seedcord dev` runs it when you answer `y` to the re-register prompt.

## The handler file

Now the code that runs when someone uses `/ban`.

```ts title="src/handlers/Ban.ts"
@SlashRoute('ban')
export class Ban extends SlashHandler<'ban'> {
    public async execute(): Promise<void> {
        const target = this.options.getUser('target');
        const reason = this.options.getString('reason');

        await this.reply(
            `Banned ${target.username}. ${reason ?? 'No reason given.'}`
        );
    }
}
```

`'ban'` appears twice, on the decorator and on the generic. You write the command's own name in both, and a mismatch is a compile error.

The two do different jobs. The decorator registers the route at startup, and the generic types `this.options` from what the command declared. Writing the name in both keeps `extends` a plain class name, so go-to-definition lands on `SlashHandler` itself.

`target` was required on the command, so it arrives without a null check. `reason` was optional, so it can be null.

Handler files go under the `interactions` path.

## A command with no handler

seedcord warns at startup about any route with nothing to answer it. The error says which route.

```txt output
Slash route ban has no registered @SlashRoute handler.
```

The command still reaches Discord. Anyone who runs it gets `Feature not implemented yet.`

## When a command's constructor throws

Codegen constructs every command it finds. A throw stops the run, and the error says which class and which file.

```txt output
Roster threw while codegen constructed it. Fix its constructor in
src/commands/Roster.ts. Cannot read properties of undefined (reading 'guildId')
```
