# Commands

Create your first slash command from two files, one that declares it to Discord and one that answers it when someone runs it.

Slash commands are the way Discord gives people to reach your bot, and they run without a privileged intent. Discord needs your command's shape before anyone can run one, and when someone does, the interaction it sends back carries that command's name. Uploading a shape and routing a name are different jobs.

Each of those gets a file. 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://seedcord.org/docs/packages/core/latest/classes/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`](https://seedcord.org/guide/commands/deployed/) finds the duplicates and deletes them.

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

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

### One list of servers for every command

You paste the same ids into every command file. Eighty commands means eighty copies of one array, and a new server would mean editing all of them.

`commands.guilds` holds the list once to save you that effort.

```ts title="src/bot.ts"
import { resolve } from 'node:path';

import { Seedcord } from '@seedcord/gateway';
import { GatewayIntentBits } from 'discord.js';

export const seedcord = new Seedcord({
    bot: {
        clientOptions: { intents: [GatewayIntentBits.Guilds] },
        interactions: {
            path: resolve(import.meta.dirname, './handlers')
        },
        commands: {
            path: resolve(import.meta.dirname, './commands'),
            guilds: ['613425648685547541']
        },
        events: { path: null }
    },
    subscribers: { path: null }
});
```

A command decorated with [`@RegisterCommand()`](https://seedcord.org/docs/packages/core/latest/functions/register-command) deploys to every server in that list.

```ts
@RegisterCommand()
```

With `guilds` left out, or with no ids in it, those same commands deploy globally. Delete that one line in `bot.ts` to take your bot public. Every command file stays as it is, and the copies already sitting in those servers stay too, so run [`seedcord commands`](https://seedcord.org/guide/commands/deployed/) to clear them if need be.

When you pass a scope yourself, seedcord skips the list. `'global'` keeps the command global. `'guild'` with ids sends it to those servers. Add `'config'` to that array to send the command to the configured servers as well.

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

`'config'` there stands for every id under `commands.guilds`, so this command goes to those servers and to `613425648685547541`. If `commands.guilds` is empty, the same line throws `CoreCommandGuildsEmpty` at startup, and the message points to your command class.

## 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://seedcord.org/docs/packages/core/latest/interfaces/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')
```
