# djs-commands — full documentation corpus > Full markdown export of every docs page on https://djscommands.deoxy.dev. Cite back to individual pages via the `Source:` line under each page heading. --- # DJS Commands Source: https://djscommands.deoxy.dev/ > Modern Discord.js command handler — TypeScript-first, Components V2 native, with pluggable persistence.
Built for Discord.js v14.26+ TypeScript 5.9+ Node 22+ / Bun 1.2+ Components V2

— Framework

Everything you need.
Nothing you don't.

} accent="#5865F2" title="TypeScript-first" body="Plain objects, full inference. defineCommand returns its argument unchanged — autocomplete is real, decorators are extinct." href="/concepts/commands" /> } accent="#7C3AED" title="Components V2 native" body="JSX runtime + plain-function fallback. Containers, sections, modals, action rows — both APIs return identical builders." href="/components-v2" /> } accent="#67E8F9" title="Validators that compose" body="Built-in role / permission / channel gates plus your own async predicates. First failure wins, with a typed reason." href="/concepts/validators" /> } accent="#5865F2" title="Distributed cooldowns" body="Per-user, per-guild, per-user-per-guild, global. Pluggable CacheAdapter — Redis for sharded bots, in-memory for dev." href="/concepts/cooldowns" /> } accent="#7C3AED" title="Bring your own DB" body="Drizzle, Prisma, Mongoose — first-party adapters for the Storage contract. Or write six methods and ship your own." href="/concepts/storage" /> } accent="#67E8F9" title="Plugins, not magic" body="setup / teardown lifecycle, command bundles, no global state. Plugins add what you need — nothing you don't." href="/concepts/plugins" />

— defineCommand

Plain objects.
Strongly typed.

Define a slash command as a plain object. Options are inferred. The handler context unifies slash and legacy invocations behind a single reply().

Read the deep dive

— Adapters

Pluggable persistence.

Six CRUD methods. The framework reads / writes guild prefixes, disabled commands, and channel locks through the Storage contract — pick a first-party adapter or write your own.

Or write your own — six methods

— v1 → v2

Coming from @d3oxy/djs-commands?

Every v1 API has a v2 equivalent. The migration guide walks through every option that moved or was dropped — with side-by-side examples.

Migration guide
export const FeatureCard = ({ icon, title, body, href, accent }) => (
{icon}

{title}

{body}

); export const AdapterCard = ({ tag, name, sub, href, accent }) => ( {tag} {name} {sub} ); --- # Getting Started Source: https://djscommands.deoxy.dev/getting-started > Install DJS Commands, register your first command, and ship a working bot. This section walks you from a blank directory to a Discord bot that responds to a slash command. If you already have a Discord.js project, jump straight to [Your first command](/getting-started/your-first-command). ## Fastest path: `create-djs-commands` ```bash npx create-djs-commands my-bot cd my-bot cp .env.example .env # then add your DISCORD_TOKEN bun run dev ``` The CLI scaffolds the directory layout, `tsconfig.json`, `.env.example`, and a working `/ping` command. Pick one of the templates (minimal, components-v2 showcase, or moderation with Drizzle) and you're online in under a minute. DJS Commands v2 targets Discord.js v14.26+ and Node.js 22+. Bun 1.2+ is supported and is the development runtime used by the maintainers. ## Prerequisites - A Discord application + bot token. If you don't have one, follow Discord's [Building Your First Bot](https://discord.com/developers/docs/quick-start/getting-started) guide. - Node.js 22+ or Bun 1.2+ installed. - A package manager you're comfortable with (`bun`, `pnpm`, `npm`, or `yarn`). --- # Installation Source: https://djscommands.deoxy.dev/getting-started/installation > Add @djs-commands/core to a Discord.js project. ### Install Discord.js Skip this if your project already has it. ```bash bun add discord.js ``` ```bash pnpm add discord.js ``` ```bash npm install discord.js ``` ```bash yarn add discord.js ``` ### Install @djs-commands/core ```bash bun add @djs-commands/core ``` ```bash pnpm add @djs-commands/core ``` ```bash npm install @djs-commands/core ``` ```bash yarn add @djs-commands/core ``` v2 is published to the new `@djs-commands` npm organization. The legacy v1 package (`@d3oxy/djs-commands`) is **not** API-compatible — see the [v1 → v2 migration notes](/getting-started/your-first-command#what-changed-from-v1) once you're ready. ### TypeScript (recommended) djs-commands is designed around TypeScript inference. If you don't already have a `tsconfig.json`, the minimal one below works for Node 22+: ```json title="tsconfig.json" { "compilerOptions": { "target": "ES2022", "module": "ESNext", "moduleResolution": "Bundler", "strict": true, "esModuleInterop": true, "skipLibCheck": true } } ``` JavaScript also works — every example in these docs has a JS-compatible shape. Just drop the type annotations. ## Next Continue to [Your first command](/getting-started/your-first-command) to wire it all together. --- # Your first command Source: https://djscommands.deoxy.dev/getting-started/your-first-command > Define, register, and run a slash command end-to-end. The fastest path to a running bot is `npx create-djs-commands` — it scaffolds the directory layout, `tsconfig.json`, and `.env.example` for you. This page walks through the same setup by hand so you understand each piece. ### Define a command A command is just a plain object you build with `defineCommand`. The function is identity at runtime — it exists for IDE autocomplete and type inference. ```ts title="src/commands/ping.ts" export default defineCommand({ name: "ping", description: "Replies with pong", run: async ({ reply }) => { await reply("pong"); }, }); ``` The handler receives a [`CommandRunContext`](/concepts/commands#commandruncontext) with a unified `reply()` (works for both slash and legacy invocations), the invoking `author`, the `guild`, `member`, and typed `options`. ### Wire up a Client Pass your commands to `createCommandHandler` along with a discord.js `Client`. The handler subscribes to `interactionCreate` and dispatches matching slash commands. On `clientReady`, it registers the command list with Discord. ```ts title="src/index.ts" const token = process.env.DISCORD_TOKEN; if (!token) { console.error("DISCORD_TOKEN environment variable is required"); process.exit(1); } const client = new Client({ intents: [GatewayIntentBits.Guilds] }); const handler = createCommandHandler({ client, commands: [ping], }); await handler.ready; client.once("clientReady", (c) => { console.log(`Logged in as ${c.user.tag}`); }); await client.login(token); ``` Or skip the manual import and let the framework load the directory for you: ```ts createCommandHandler({ client, commandDir: "./src/commands", }); ``` In dev (`NODE_ENV !== "production"`) the directory is watched and edited files hot-reload without a restart. ### Run the bot ```bash DISCORD_TOKEN=... bun run src/index.ts ``` Once the bot is online, run `/ping` in any guild it's invited to. You should see `pong` come back. The first time you run a freshly registered command, Discord may take a minute to propagate it. Subsequent code edits with the same command name update instantly. ## Next steps - **Add typed options** — see [Commands → Options](/concepts/commands#options). - **Gate execution** with role/permission/channel checks — see [Validators](/concepts/validators). - **Rate-limit** with built-in cooldowns — see [Cooldowns](/concepts/cooldowns). - **Persist state** (prefixes, disabled commands, your own data) — see [Storage](/concepts/storage). - **Build rich replies** with Components V2 — see [Components V2](/components-v2). - **Extend the handler** with a plugin — see [Plugins](/concepts/plugins). ## Coming from v1? If you're upgrading from `@d3oxy/djs-commands`, the [Migration from v1](/migration-from-v1) guide walks through every API that moved or was dropped, with side-by-side examples. --- # Concepts Source: https://djscommands.deoxy.dev/concepts > The mental model behind DJS Commands v2. These pages explain the **building blocks** of DJS Commands — what they do, when to reach for them, and how they compose. Read them in order on your first pass; jump in topically afterwards. ## How the pieces fit ``` Discord interaction ↓ createCommandHandler (subscribes to events) ↓ Dispatcher (looks up command by name) ↓ Validators (gate the call) ↓ Plugins (wrap the call — before/after) ↓ command.run({ interaction, ...context }) ↓ Components V2 reply (optional) ``` Storage sits orthogonal to that pipeline — anything that needs to remember state across calls (cooldowns, per-guild prefixes, user preferences) reads and writes through the adapter you provide. --- # Commands Source: https://djscommands.deoxy.dev/concepts/commands > Define, register, and dispatch slash and prefix commands with @djs-commands/core. A **command** in DJS Commands is a plain object. There are no decorators, no class hierarchies, and no on-disk conventions you have to follow. You build the object with `defineCommand`, you put the object somewhere your bot can find it, and you pass it to `createCommandHandler`. ## `defineCommand` `defineCommand(command)` returns its argument unchanged. It exists purely so the TypeScript compiler can give you autocomplete and structural validation as you type. At runtime it's identity. ```ts const ping = defineCommand({ name: "ping", description: "Replies with pong", run: async ({ reply }) => { await reply("pong"); }, }); ``` ### Required fields | Field | Type | Description | | --- | --- | --- | | `name` | `string` | The slash-command name. Must match Discord's naming rules: 1–32 characters, lowercase, `[a-z0-9_-]`. | | `description` | `string` | The 1–100-character description shown in the Discord client. | | `run` | `(ctx) => void \| Promise` | The handler. Receives a [`CommandRunContext`](#commandruncontext). | ### Optional fields | Field | Type | Notes | | --- | --- | --- | | `options` | `CommandOptions` | Typed schema for slash-command options — see [Options](#options). | | `validators` | `Validator[]` | Custom pre-handler checks. See [Validators](/concepts/validators). | | `cooldown` | `CooldownConfig` | Rate limit. See [Cooldowns](/concepts/cooldowns). | | `ownerOnly` | `boolean` | When `true`, only IDs in `botOwners` (passed to the handler) can run it. | | `guildOnly` | `boolean` | When `true`, blocks DM invocations. | | `channels` | `string[]` | Allow-list of channel IDs the command can run in. | | `permissions` | `PermissionsString[]` | Discord member permissions required (e.g. `["BanMembers"]`). | | `roles` | `string[]` | Role IDs the invoking member must have. | | `legacy` | `CommandLegacyConfig` | Opt this command into legacy prefix invocation; supports aliases. | ## `CommandRunContext` `CommandRunContext` is a discriminated union over the invocation source. Both branches share a unified surface so most handlers don't need to care whether they were invoked via slash or prefix: ```ts type CommandRunContext = SlashRunContext | LegacyRunContext; interface BaseRunContext { client: Client; author: User; guild: Guild | null; member: GuildMember | null; channel: TextBasedChannel | null; channelId: string | null; options: ResolveOptions; // typed from your `options` schema reply: (content: string | { content?: string; ephemeral?: boolean }) => Promise; } type SlashRunContext = BaseRunContext & { type: "slash"; interaction: ChatInputCommandInteraction }; type LegacyRunContext = BaseRunContext & { type: "legacy"; message: Message }; ``` When you need source-specific behavior, narrow on `ctx.type`: ```ts run: async (ctx) => { if (ctx.type === "slash") { await ctx.interaction.deferReply({ ephemeral: true }); } await ctx.reply("hi"); } ``` ## Options Define typed slash-command options with the `options` field. The handler context's `options` is fully inferred from your schema — required options resolve to their type, optional ones to `T | undefined`. ```ts defineCommand({ name: "ban", description: "Ban a user", options: { target: { type: "user", description: "Who to ban", required: true }, reason: { type: "string", description: "Why", required: false }, days: { type: "integer", description: "Days of messages to delete", choices: [0, 1, 7] as const, }, }, run: async ({ options, reply }) => { // options.target: User // options.reason: string | undefined // options.days: 0 | 1 | 7 | undefined await reply(`Banning ${options.target.tag}`); }, }); ``` Supported option types: `string`, `integer`, `number`, `boolean`, `user`, `channel`, `role`, `mentionable`, `attachment`. `string` and `integer` accept `choices` for enum-style narrowing. ## Registering commands `createCommandHandler` accepts a `client` and an array of commands. It does two things on your behalf: 1. **Subscribes** to `interactionCreate`. When a chat-input interaction comes in, it looks up the command by `name` and dispatches it through validators → cooldown gates → plugins → `run`. 2. **Registers** the command list with Discord on `clientReady`. This calls `client.application.commands.set(...)` with the names, descriptions, and options of every command you passed in. ```ts const client = new Client({ intents: [GatewayIntentBits.Guilds] }); const handler = createCommandHandler({ client, commands: [ping /*, more commands */], }); await handler.ready; await client.login(process.env.DISCORD_TOKEN); ``` The returned `handler` exposes: ```ts handler.ready; // Promise — resolves once all plugin setup hooks complete await handler.destroy(); // Removes listeners, awaits plugin teardown ``` `destroy()` does **not** delete the registered application commands from Discord — those persist until you explicitly clear them. ## Loading commands from a directory Pass a `commandDir` to the handler and every file's default export is registered automatically. In dev (`NODE_ENV !== "production"`) the directory is watched and edited files hot-reload without a restart. ```ts createCommandHandler({ client, commandDir: "./src/commands", }); ``` If you'd rather wire it up by hand, the loader functions are public: ```ts const commands = await loadCommandsFromDir("./src/commands"); const watcher = watchCommandsDir("./src/commands", { onCommandChange: () => { /* re-register */ }, }); ``` ## Best practices - **One command per file.** Export the `defineCommand` result as the default export so the autoloader can pick it up. - **Keep `run` thin.** Move business logic into ordinary functions you can unit-test without spinning up a Discord client. - **Prefer `ctx.reply`** over `ctx.interaction.reply` / `ctx.message.reply` — it works in both slash and legacy contexts. - **Don't share state via module scope.** When a feature wants memory across calls (cooldowns, per-guild config), reach for the [Storage adapter](/concepts/storage). --- # Components V2 Source: https://djscommands.deoxy.dev/concepts/components-v2 > First-class JSX (or function) builders for Discord's new component model. Discord's [Components V2](https://discord.com/developers/docs/components/reference) replaces the old "embeds + button rows" pattern with composable layout primitives. DJS Commands ships **two ways** to build them — a JSX runtime in `@djs-commands/jsx`, and a plain-function fallback in `@djs-commands/core`. Both produce identical discord.js builder objects, so you can mix them freely. ```tsx export const welcome = defineCommand({ name: "welcome", description: "Show a Components V2 welcome card", run: async ({ interaction }) => { await interaction.reply({ flags: MessageFlags.IsComponentsV2, components: render( # Welcome
}> Components V2 lets you compose rich messages from layout primitives.
), }); }, }); ``` For the full surface — every component, side-by-side JSX and function examples, modal forms, and a runnable example bot — see the dedicated reference: → **[Components V2 reference](/components-v2)** The JSX runtime is opt-in. Set `jsxImportSource: "@djs-commands/jsx"` in your `tsconfig.json` and JSX in any `.tsx` file compiles to `discord.js` builders — no Babel, no SWC, no React. Plain-JS users skip JSX entirely and import `button()`, `container()`, `section()`, etc. from `@djs-commands/core`. --- # Cooldowns Source: https://djscommands.deoxy.dev/concepts/cooldowns > Per-user, per-guild, per-command rate limiting backed by your storage adapter. Cooldowns rate-limit a command for an actor. The dispatcher checks the cooldown gate after validators pass — if the actor is on cooldown, the user sees a "try again in N seconds" reply and the handler is not invoked. ## Configuring a cooldown Attach `cooldown` to the command. `duration` is in **milliseconds**. ```ts defineCommand({ name: "vote", description: "Vote on the current poll", cooldown: { type: "perUser", duration: 30_000 }, run: async ({ reply }) => { await reply("voted"); }, }); ``` ## Cooldown types | Type | Key shape | When to use | | --- | --- | --- | | `perUser` | `cd::user:` | One invocation per user globally — e.g. daily reward. | | `perGuild` | `cd::guild:` | One invocation per guild — e.g. server-wide announcement. | | `perUserPerGuild` | `cd::user::guild:` | One per user per server — the most common choice for moderation. | | `global` | `cd::global` | One invocation across all users and guilds — debug/owner commands. | In DMs, `guildId` falls back to the literal string `"dm"` so `perGuild` and `perUserPerGuild` keys remain stable. ## Storage backends By default, cooldowns are stored **in-process** in a `Map`. That works for development and single-process bots, but loses state on restart and doesn't survive horizontal scale. Pass a `cacheAdapter` to the handler to use a shared store. ```ts const redis = new Redis(process.env.REDIS_URL); createCommandHandler({ client, commands: [...], cacheAdapter: redisCacheAdapter(redis), }); ``` ## Writing a custom CacheAdapter The contract is three methods. `expiresAt` is an absolute UNIX millisecond timestamp; `ttlMs` is the same value expressed as a TTL hint for stores that prefer it (Redis `PX`, Memcached, etc). ```ts interface CacheAdapter { get(key: string): Promise; set(key: string, expiresAt: number, ttlMs: number): Promise; delete(key: string): Promise; } ``` `get` should return `null` when the key is missing or already expired. The engine uses `expiresAt` to compute remaining time, so backends that auto-expire keys (Redis with `PX`) and backends that don't (a SQL `cooldowns` table) both work — the engine cleans up expired entries on read for the latter. The Redis adapter writes with `SET key expiresAt PX ttl` so Redis handles expiry server-side and you never accumulate dead keys. ## When cooldowns start The cooldown is **started after** `command.run` returns successfully. If `run` throws, no cooldown is applied — users won't be punished for a failed invocation. If you need pre-handler cooldowns (e.g. expensive permission lookups should rate-limit even on failure), use a custom validator that records the timestamp itself. ## `CooldownEngine` directly The engine is exposed for advanced use — you can construct one yourself to gate non-command flows (autocomplete handlers, button clicks, modal submits) using the same shared cache. ```ts const engine = new CooldownEngine(redisCacheAdapter(redis)); const remaining = await engine.check(myCommand, { userId, guildId }); if (remaining !== null) { // on cooldown — `remaining` ms left } await engine.start(myCommand, { userId, guildId }); ``` --- # Plugins Source: https://djscommands.deoxy.dev/concepts/plugins > Cross-cutting hooks, command bundles, and lifecycle wrappers. Plugins let third-party packages — and your own internal bundles — ship commands and side-effects that hook into the handler's lifecycle. The plugin system is intentionally small: a manifest with three optional hooks (`commands`, `setup`, `teardown`). ## Plugin manifest A plugin is a plain object you pass to `createCommandHandler`. The convention is to expose it as a factory function that returns a `PluginManifest`. ```ts export function loggingPlugin(opts: { level?: "info" | "debug" } = {}): PluginManifest { return { name: "logging", setup({ client }) { client.on("interactionCreate", (i) => { if (i.isChatInputCommand()) { console.log(`[${opts.level ?? "info"}]`, i.commandName, "by", i.user.tag); } }); }, teardown() { // optional cleanup — runs on handler.destroy() }, }; } ``` Register at boot: ```ts createCommandHandler({ client, commands: [...], plugins: [loggingPlugin({ level: "debug" })], }); ``` ### Manifest fields | Field | Type | Purpose | | --- | --- | --- | | `name` | `string` | Human-readable identifier; surfaces in error messages. | | `commands` | `Command[]` | Commands merged into the dispatcher at boot — same shape as the top-level `commands`. | | `setup` | `(ctx) => void \| Promise` | Awaited at boot in registration order. **Throwing aborts handler boot.** | | `teardown` | `() => void \| Promise` | Awaited on `handler.destroy()`. Errors are logged but don't block other teardowns. | ## Bundling commands The most common pattern: a plugin ships a related set of commands plus the listeners they need. ```ts const start = defineCommand({ name: "music-play", description: "Start playback", run: async ({ reply }) => { await reply("started"); }, }); const stop = defineCommand({ name: "music-stop", description: "Stop playback", run: async ({ reply }) => { await reply("stopped"); }, }); export function musicPlugin(): PluginManifest { return { name: "music", commands: [start, stop], setup({ client }) { // connect to voice gateway, etc. }, }; } ``` Plugin commands and top-level commands share a single namespace — duplicates throw at boot. ## Lifecycle and ordering Plugins boot in array order. Each plugin's `setup` is awaited before the next runs, so plugins later in the array can rely on earlier ones having initialized. `handler.ready` resolves once **all** setup hooks have completed. If any setup throws, `handler.ready` rejects with that error and the handler does not attach event listeners. ```ts const handler = createCommandHandler({ client, plugins: [a, b, c] }); await handler.ready; // resolves after a.setup → b.setup → c.setup ``` On `handler.destroy()`: 1. The dispatcher detaches its listeners. 2. Each plugin's `teardown` is awaited in reverse-registration order. 3. A failing teardown is logged but does not stop other teardowns. Plugins do not currently expose pre/post-dispatch hooks (e.g. wrap-every-command middleware). Today, plugins use the discord.js client directly for that. A typed dispatch-middleware surface is on the roadmap. ## Sharing state The setup context is intentionally minimal — just `{ client }`. Plugins that need to expose state to commands typically: - Attach to the client as a property: `client.myState = ...`. Type via module augmentation. - Close over their own values and inject them into the commands they ship. - Use the `Storage` adapter for persistent state. Avoid module-scoped singletons in plugin code — that breaks tests that want to construct multiple handlers in the same process. --- # Storage Source: https://djscommands.deoxy.dev/concepts/storage > The pluggable persistence layer behind framework features and your own commands. `@djs-commands/core` ships **without a database**. Anything that needs to remember state across restarts — guild prefixes, disabled-command lists, channel locks, your own bot's state — goes through the `Storage` adapter you register. The contract is a generic CRUD interface modeled on Better Auth's pattern: adapters implement six methods once, and the framework consumes them for every persistent feature. Adding a new model never requires changes to the adapter contract. ## The `Storage` interface ```ts interface Storage { create(model: string, data: T): Promise; findOne(model: string, where: StorageWhere): Promise; findMany(model: string, opts?: StorageFindOpts): Promise; update(model: string, where: StorageWhere, data: Partial): Promise; delete(model: string, where: StorageWhere): Promise; count(model: string, where?: StorageWhere): Promise; } ``` Models are identified by **string names** (snake_case). The framework ships three model names you'll see in the type signatures: `guild_prefix`, `disabled_commands`, `channel_locks`. ## Built-in adapters `@djs-commands/core` is database-agnostic. First-party adapters live in their own packages so you only depend on what you use. Walk-throughs for each are in the [Adapter Cookbook](/adapter-cookbook). ## Using storage Pass an adapter at boot: ```ts createCommandHandler({ client, commands: [...], storage: drizzleStorage(db), }); ``` The framework reads/writes its own models automatically. To use storage from your own commands, the adapter is exposed via the handler — most commands won't need it because they can use the **helper functions** below. ## Framework models and helpers For each first-party model, `@djs-commands/core` exports typed read/write helpers. Use these instead of calling `storage.findOne(...)` directly — they keep the model name and column shape in one place. ### Guild prefix Per-guild override for the legacy command prefix. ```ts const prefix = await getGuildPrefix(storage, guildId); // string | null await setGuildPrefix(storage, guildId, "?"); await clearGuildPrefix(storage, guildId); ``` ### Disabled commands Per-guild allow-list for commands. Useful for letting moderators turn individual commands off without redeploying. ```ts if (await isCommandDisabled(storage, guildId, "music-play")) { /* ... */ } await disableCommand(storage, guildId, "music-play"); await enableCommand(storage, guildId, "music-play"); ``` The dispatcher consults `isCommandDisabled` automatically — you don't need to wire it into your validators. ### Channel locks Restrict a command to specific channels in a guild. Empty list = no restriction. ```ts const locks = await getChannelLocks(storage, guildId, "music-play"); // string[] await lockCommandToChannel(storage, guildId, "music-play", channelId); await unlockCommandFromChannel(storage, guildId, "music-play", channelId); ``` ## Using storage for your own state Storage is generic — you can register your own models with no framework changes. Pick a snake_case model name, define the row shape, and call the adapter methods directly. ```ts const USER_SETTINGS = "user_settings"; interface UserSettings { user_id: string; timezone: string; } await storage.create(USER_SETTINGS, { user_id: "123", timezone: "America/New_York", }); const row = await storage.findOne(USER_SETTINGS, { user_id: "123" }); ``` You're responsible for **migrations**: every adapter expects the underlying table/collection to exist. The Drizzle and Prisma adapters publish schema definitions you can extend. ## Writing a custom adapter Implement `Storage`, then verify it against the framework's conformance suite: ```ts runStorageConformance("my-adapter", () => myStorageFactory()); ``` The conformance suite is exported as a plain function so you can call it from any test runner. It exercises every CRUD path the framework relies on, including upserts, partial updates, and find-with-options. Adapter authors should **not** implement framework-specific logic. The shipped helpers (`getGuildPrefix`, etc.) call the generic CRUD methods — keep your adapter focused on the transport, and the helpers will compose on top. --- # Validators Source: https://djscommands.deoxy.dev/concepts/validators > Pre-handler checks that gate command execution. Validators are async-friendly checks that run **before** `command.run`. They short-circuit the dispatch with a reason string when a check fails — the user sees the reason as an ephemeral reply. ## Built-in validators Several common gates live on the command object directly. The dispatcher runs them automatically; you don't have to write them as `Validator` functions. ```ts defineCommand({ name: "ban", description: "Ban a user", ownerOnly: false, guildOnly: true, channels: ["123456789012345678"], permissions: ["BanMembers"], roles: ["234567890123456789"], run: async ({ reply }) => { await reply("banned"); }, }); ``` | Field | Effect | | --- | --- | | `ownerOnly: true` | Only IDs in the handler's `botOwners` array can run it. | | `guildOnly: true` | Blocks DM invocations. | | `channels: string[]` | Allow-list of channel IDs (empty/unset = everywhere). | | `permissions: PermissionsString[]` | Required member permissions (e.g. `"BanMembers"`). | | `roles: string[]` | Role IDs the invoking member must have. | Each built-in returns a localized reason string if the check fails, so you don't have to write your own messaging for the common cases. ## Custom validators For anything beyond the built-ins, attach a `validators` array to the command. A `Validator` is just a function that receives a `ValidatorContext` and returns a result. ```ts const inVoiceChannel: Validator = async ({ member }) => { if (member?.voice.channel) return { ok: true }; return { ok: false, reason: "Join a voice channel first." }; }; defineCommand({ name: "play", description: "Play a track", validators: [inVoiceChannel], run: async ({ reply }) => { await reply("playing"); }, }); ``` ### `ValidatorContext` ```ts interface ValidatorContext { command: AnyCommand; // the command being dispatched botOwners: readonly string[]; // bot owners passed to createCommandHandler user: User; // invoker guild: Guild | null; // null in DMs member: GuildMember | null; channelId: string; source: | { type: "slash"; interaction: ChatInputCommandInteraction } | { type: "legacy"; message: Message }; } ``` The context is **unified** across slash and legacy invocations — most validators don't need to look at `source`. When you do (e.g. ephemeral replies are slash-only), narrow on `source.type`. ### `ValidationResult` ```ts type ValidationResult = { ok: true } | { ok: false; reason: string }; ``` `reason` is shown to the user. Keep it short and actionable. ## Global validators Pass `validators` to `createCommandHandler` to run them on **every** command. Useful for cross-cutting policy like "block banned users": ```ts createCommandHandler({ client, commands: [...], validators: [ async ({ user }) => banSet.has(user.id) ? { ok: false, reason: "You're not allowed to use this bot." } : { ok: true }, ], }); ``` ## `canRunCommand` shorthand For a single global gate, `canRunCommand` is a slimmer signature: return `true`, `false`, or a string reason. ```ts createCommandHandler({ client, commands: [...], canRunCommand: ({ command, user }) => { if (command.name === "admin" && !admins.has(user.id)) return "Admins only."; return true; }, }); ``` ## Order of evaluation For each dispatch, the chain runs in this order — first failure wins: 1. Built-in validators (in this order: `ownerOnly`, `guildOnly`, `channels`, `permissions`, `roles`) 2. Handler-level `validators` 3. Command-level `validators` 4. Handler-level `canRunCommand` If every check returns `{ ok: true }` (or `canRunCommand` returns `true`), the dispatcher proceeds to the cooldown gate and then `command.run`. Validators run **before** cooldowns. A failing validator does not start a cooldown. This is intentional — you don't want a permission denial to also rate-limit the user. --- # Components V2 Source: https://djscommands.deoxy.dev/components-v2 > JSX runtime and a function-call fallback for Discord's new component model. Discord's [Components V2](https://discord.com/developers/docs/components/reference) message format replaces the old "embed + button rows" pattern with composable layout primitives — Containers, Sections, Text Displays, Media Galleries, Separators, Files, and Action Rows of Buttons. DJS Commands ships **two ways** to build them. Pick whichever fits your project: - **`@djs-commands/jsx`** — write components as JSX, like React. Best DX if your toolchain (TS, Bun, ESBuild, SWC) already supports JSX. - **Function fallback from `@djs-commands/core`** — the same builders, exposed as plain functions. No JSX pragma, no extra dependency. Both produce real `discord.js` builder objects, so you can mix the two freely. ## Quick start (JSX) ```bash bun add @djs-commands/jsx ``` Configure TypeScript to use the runtime: ```json title="tsconfig.json" { "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "@djs-commands/jsx" } } ``` That's it — no Babel, no SWC. Bun's transpiler and TypeScript both honor `jsxImportSource` directly. ```tsx title="src/welcome.tsx" export const welcome = defineCommand({ name: "welcome", description: "Show a Components V2 welcome card", run: async ({ interaction }) => { await interaction.reply({ flags: MessageFlags.IsComponentsV2, components: render( # Welcome
}> Components V2 lets you compose rich messages from layout primitives.
), }); }, }); ``` ## Quick start (function fallback) If you can't (or won't) enable JSX, every component has a function-form sibling re-exported from `@djs-commands/core`. They return the same `discord.js` builders. ```ts title="src/welcome.ts" export const welcome = defineCommand({ name: "welcome", description: "Show a Components V2 welcome card", run: async ({ interaction }) => { await interaction.reply({ flags: MessageFlags.IsComponentsV2, components: [ container({ accentColor: 0x5865f2, children: [ textDisplay("# Welcome"), section({ accessory: button({ style: "primary", customId: "welcome:next", label: "Next" }), text: "Components V2 lets you compose rich messages from layout primitives.", }), ], }), ], }); }, }); ``` ## Side-by-side: same output The two APIs are isomorphic. Here's the identical message expressed both ways: ```tsx render( # Bug report
}> File an issue if something looks off.