Skip to main content

Command Basics

Fire commands extend the Command base class from lib/util/command.ts and support both traditional text commands and slash commands.

Command Structure

Basic Command Template

Here’s a minimal command structure:
src/commands/Main/ping.ts
import { ApplicationCommandMessage } from "@fire/lib/extensions/appcommandmessage";
import { Command } from "@fire/lib/util/command";
import { Language } from "@fire/lib/util/language";
import { MessageEmbed } from "discord.js";

export default class Ping extends Command {
  constructor() {
    super("ping", {
      description: (language: Language) =>
        language.get("PING_COMMAND_DESCRIPTION"),
      enableSlashCommand: true,
      restrictTo: "all",
      slashOnly: true,
    });
  }

  async run(command: ApplicationCommandMessage) {
    const embed = new MessageEmbed()
      .setTitle(
        `:ping_pong: ${this.client.restPing}ms\n:heartpulse: ${
          this.client.ws.shards.get(command.shard)?.ping ?? -1
        }ms`
      )
      .setColor(command.member?.displayColor || "#FFFFFF")
      .setFooter({
        text: command.language.get("PING_FOOTER", {
          shard: command.shard,
          cluster: this.client.manager.id,
        }),
      })
      .setTimestamp();

    return await command.channel.send({ embeds: [embed] });
  }
}

Command with Arguments

Here’s an example with arguments:
src/commands/Utilities/avatar.ts
import { ApplicationCommandMessage } from "@fire/lib/extensions/appcommandmessage";
import { FireMember } from "@fire/lib/extensions/guildmember";
import { FireUser } from "@fire/lib/extensions/user";
import { Command } from "@fire/lib/util/command";
import { Language } from "@fire/lib/util/language";
import { MessageActionRow, MessageButton, MessageEmbed } from "discord.js";

export default class Avatar extends Command {
  constructor() {
    super("avatar", {
      description: (language: Language) =>
        language.get("AVATAR_COMMAND_DESCRIPTION"),
      args: [
        {
          id: "user",
          type: "user|member",
          match: "rest",
          default: undefined,
          required: false,
        },
      ],
      enableSlashCommand: true,
      restrictTo: "all",
      slashOnly: true,
    });
  }

  async run(
    command: ApplicationCommandMessage,
    args: { user: FireMember | FireUser | null }
  ) {
    let user = args.user;
    if (typeof user == "undefined") user = command.member ?? command.author;
    else if (!user) return;

    const color =
      user instanceof FireMember
        ? user?.displayColor
        : command.member?.displayColor;

    const embed = new MessageEmbed()
      .setColor(color)
      .setTimestamp()
      .setTitle(command.language.get("AVATAR_TITLE", { user: user.toString() }))
      .setImage(
        user?.displayAvatarURL({
          size: 2048,
          format: "png",
          dynamic: true,
        })
      );

    return await command.channel.send({ embeds: [embed] });
  }
}

Command Options

The Command constructor accepts various options:

Basic Options

interface CommandOptions {
  // Command description (supports i18n)
  description?: ((language: Language) => string) | string;
  
  // Command aliases
  aliases?: string[];
  
  // Where command can be used
  restrictTo?: "guild" | "dm" | "all";
  
  // Enable as slash command
  enableSlashCommand?: boolean;
  
  // Only available as slash command
  slashOnly?: boolean;
  
  // Command arguments
  args?: ArgumentOptions[];
  
  // Cooldown in milliseconds (default: 2500)
  cooldown?: number;
  
  // User permissions required
  userPermissions?: bigint[];
  
  // Bot permissions required
  clientPermissions?: bigint[];
}

Advanced Options

interface CommandOptions {
  // Only for bot moderators
  moderatorOnly?: boolean;
  
  // Only for superusers
  superuserOnly?: boolean;
  
  // Requires premium
  premium?: boolean;
  
  // Hide from help command
  hidden?: boolean;
  
  // Response is ephemeral (only slash)
  ephemeral?: boolean;
  
  // Defer response
  deferAnyways?: boolean;
  
  // Guild-specific command
  guilds?: Snowflake[];
  
  // Parent command (for subcommands)
  parent?: string;
  
  // Is a command group
  group?: boolean;
  
  // Context menu command
  context?: string[];
  
  // Requires experiment
  requiresExperiment?: { id: number; bucket: number };
}

Argument Types

Fire provides many built-in argument types:

Basic Types

args: [
  { id: "text", type: "string" },
  { id: "amount", type: "number" },
  { id: "enabled", type: "boolean" },
]

Discord Types

args: [
  // User or member
  { id: "target", type: "user|member" },
  
  // Member only (requires guild)
  { id: "member", type: "member" },
  
  // User only
  { id: "user", type: "user" },
  
  // Role
  { id: "role", type: "role" },
  
  // Channel
  { id: "channel", type: "textChannel" },
  
  // Any guild channel
  { id: "channel", type: "guildChannel" },
  
  // Category channel
  { id: "category", type: "category" },
  
  // Member or role
  { id: "target", type: "member|role" },
  
  // Attachment/image
  { id: "image", type: "image" },
]

Fire-Specific Types

args: [
  // Another command
  { id: "command", type: "command" },
  
  // Module
  { id: "module", type: "module" },
  
  // Language code
  { id: "language", type: "language" },
  
  // Message (by ID or link)
  { id: "message", type: "message" },
  
  // Codeblock
  { id: "code", type: "codeblock" },
  
  // Time/duration
  { id: "duration", type: "time" },
  
  // Emoji
  { id: "emoji", type: "emoji" },
]

Argument Options

interface ArgumentOptions {
  // Argument identifier
  id: string;
  
  // Type (see above)
  type: string | ArgumentTypeCaster;
  
  // Description (for slash commands)
  description?: (language: Language) => string;
  
  // Is required?
  required?: boolean;
  
  // Default value
  default?: any;
  
  // Match type
  match?: "rest" | "phrase" | "flag" | "option" | "text";
  
  // Flag name (for --flag arguments)
  flag?: string;
  
  // For slash commands
  slashCommandType?: string;
  autocomplete?: boolean;
  choices?: { name: string; value: string }[];
}

Execution Methods

Commands can implement two execution methods:

run() - For Slash Commands

async run(
  command: ApplicationCommandMessage | ContextCommandMessage,
  args: Record<string, any>
): Promise<any> {
  // Slash command execution
}

exec() - For Text Commands

async exec(
  message: FireMessage,
  args: Record<string, any>
): Promise<any> {
  // Text command execution
}
Most modern commands use slashOnly: true and only implement run().

Command Categories

Commands are organized by category based on their directory:
src/commands/
├── Admin/           # Bot administration
├── Configuration/   # Guild configuration
├── Fun/            # Fun/entertainment commands
├── Main/           # Core bot commands
├── Moderation/     # Moderation tools
├── Premium/        # Premium-only features
├── Starboard/      # Starboard management
├── Tags/           # Custom tags
├── Tickets/        # Ticket system
└── Utilities/      # Utility commands
The category is automatically determined by the directory (lib/Fire.ts:295).

Subcommands and Groups

Creating Subcommands

Parent Command:
export default class Config extends Command {
  constructor() {
    super("config", {
      description: (language: Language) =>
        language.get("CONFIG_DESCRIPTION"),
      enableSlashCommand: true,
      group: true,  // This is a group
    });
  }
}
Subcommand:
export default class ConfigPrefix extends Command {
  constructor() {
    super("config-prefix", {  // Format: parent-subcommand
      description: (language: Language) =>
        language.get("CONFIG_PREFIX_DESCRIPTION"),
      parent: "config",  // Reference parent
      args: [/* ... */],
    });
  }
  
  async run(command: ApplicationCommandMessage, args: any) {
    // Subcommand logic
  }
}

Subcommand Groups

Group:
export default class ConfigModeration extends Command {
  constructor() {
    super("config-moderation", {
      description: (language: Language) =>
        language.get("CONFIG_MODERATION_DESCRIPTION"),
      parent: "config",
      group: true,  // This is a subcommand group
    });
  }
}
Nested Subcommand:
export default class ConfigModerationAutomod extends Command {
  constructor() {
    super("config-moderation-automod", {
      description: (language: Language) =>
        language.get("CONFIG_MODERATION_AUTOMOD_DESCRIPTION"),
      parent: "config-moderation",
      args: [/* ... */],
    });
  }
}

Permissions

User Permissions

constructor() {
  super("ban", {
    userPermissions: [PermissionFlagsBits.BanMembers],
  });
}

Bot Permissions

constructor() {
  super("nickname", {
    clientPermissions: [
      PermissionFlagsBits.ManageNicknames,
      PermissionFlagsBits.SendMessages,
    ],
  });
}

Custom Permission Checks

constructor() {
  super("admin-command", {
    moderatorOnly: true,  // Requires guild moderator role
  });
}

constructor() {
  super("eval", {
    superuserOnly: true,  // Requires bot superuser
  });
}

Slash Command Features

Autocomplete

args: [
  {
    id: "item",
    type: "string",
    autocomplete: true,
  },
]

async autocomplete(
  interaction: ApplicationCommandMessage,
  focused: CommandInteractionOption
): Promise<string[]> {
  // Return autocomplete suggestions
  return ["option1", "option2", "option3"];
}

Choices

args: [
  {
    id: "type",
    type: "string",
    choices: [
      { name: "Option 1", value: "opt1" },
      { name: "Option 2", value: "opt2" },
    ],
  },
]

Ephemeral Responses

constructor() {
  super("secret", {
    ephemeral: true,  // Only visible to command user
  });
}

Lifecycle Hooks

init() - Command Initialization

Called when command is loaded (lib/util/command.ts:288-300):
async init(): Promise<any> {
  // Custom initialization logic
  // Called after command is loaded
  // Default implementation refreshes slash commands
}

unload() - Command Cleanup

Called when command is removed (lib/util/command.ts:302-314):
async unload(): Promise<any> {
  // Cleanup logic
  // Called before command is unloaded
}

Internationalization (i18n)

Commands should support multiple languages:
constructor() {
  super("help", {
    description: (language: Language) =>
      language.get("HELP_COMMAND_DESCRIPTION"),
    args: [
      {
        id: "command",
        type: "command",
        description: (language: Language) =>
          language.get("HELP_COMMAND_ARG_DESCRIPTION"),
      },
    ],
  });
}

async run(command: ApplicationCommandMessage, args: any) {
  const message = command.language.get("HELP_MESSAGE", {
    user: command.author.toString(),
  });
  // ...
}

Best Practices

Error Handling

async run(command: ApplicationCommandMessage, args: any) {
  try {
    // Command logic
  } catch (error) {
    this.client.console.error(
      `[Command/${this.id}]`,
      error
    );
    return await command.error("COMMAND_ERROR_GENERIC");
  }
}

Deferred Responses

For long-running commands:
constructor() {
  super("slow-command", {
    deferAnyways: true,  // Auto-defer the response
  });
}

async run(command: ApplicationCommandMessage) {
  // Command is already deferred
  // Do slow operation
  const result = await someSlowOperation();
  
  // Edit the deferred response
  return await command.editReply({ content: result });
}

Guild-Only Commands

constructor() {
  super("guild-command", {
    restrictTo: "guild",  // Only works in guilds
  });
}

async run(command: ApplicationCommandMessage) {
  // command.guild is guaranteed to exist
  const guild = command.guild;
}

Response Helpers

Fire’s message extensions provide helpers:
// Success message
await command.success("OPERATION_SUCCESS");

// Error message
await command.error("OPERATION_FAILED");

// Send message
await command.channel.send({ content: "Message" });

// Reply to command
await command.reply({ content: "Reply" });

Testing Your Command

1

Compile and Run

pnpm dev
2

Test in Discord

Use your test server to execute the command
3

Check Logs

Monitor console output for errors
4

Test Edge Cases

  • Invalid arguments
  • Missing permissions
  • In DMs vs guilds
  • Different user roles

Common Patterns

Confirmation Prompts

const button = new MessageButton()
  .setCustomId(`confirm:${command.author.id}`)
  .setLabel("Confirm")
  .setStyle("DANGER");

const row = new MessageActionRow().addComponents(button);

await command.channel.send({
  content: "Are you sure?",
  components: [row],
});

// Register button handler
this.client.buttonHandlersOnce.set(
  `confirm:${command.author.id}`,
  async (interaction) => {
    // Handle confirmation
  }
);

Pagination

const pages = [...]; // Array of embeds
let currentPage = 0;

const buttons = new MessageActionRow().addComponents(
  new MessageButton()
    .setCustomId(`prev:${command.author.id}`)
    .setLabel("Previous")
    .setStyle("PRIMARY"),
  new MessageButton()
    .setCustomId(`next:${command.author.id}`)
    .setLabel("Next")
    .setStyle("PRIMARY")
);

const msg = await command.channel.send({
  embeds: [pages[currentPage]],
  components: [buttons],
});

// Handle button interactions for pagination

Next Steps

Module Development

Learn how to create modules

Architecture

Understand Fire’s architecture

Build docs developers (and LLMs) love