Commands
Slash commands in spearkit are defined as a single object: the metadata, the typed options, and the handler all live together. spearkit serialises them for discord and routes…
A first command
import { command } from "spearkit";
export const ping = command({
name: "ping",
description: "Check latency",
run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
});Register it on a client (client.register(ping)) and deploy it (see
Deployment). That's the whole loop.
The command context
The handler receives a CommandContext. It wraps the discord.js
ChatInputCommandInteraction and adds ergonomic accessors and reply helpers.
| Member | Description |
|---|---|
ctx.options | Resolved, fully-typed option values (see Options). |
ctx.commandName | The invoked command name. |
ctx.subcommand | The invoked subcommand name, or null. |
ctx.showModal(modal) | Present a modal in response. |
ctx.user / ctx.member / ctx.guild / ctx.guildId / ctx.channel / ctx.channelId / ctx.locale | Actor and location accessors. |
ctx.reply / ctx.replyEphemeral / ctx.defer / ctx.editReply / ctx.followUp / ctx.send / ctx.error | Reply helpers (see Contexts). |
ctx.interaction | The raw discord.js interaction, for anything not wrapped. |
import { command, option } from "spearkit";
export const echo = command({
name: "echo",
description: "Repeat a message",
options: {
text: option.string({ description: "What to say", required: true }),
times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
},
run: (ctx) => {
ctx.options.text; // string
ctx.options.times; // number | undefined
return ctx.reply({
content: ctx.options.text.repeat(ctx.options.times ?? 1),
ephemeral: true,
});
},
});Options are covered in depth in Options.
Command metadata
import { command, PermissionFlagsBits } from "spearkit";
export const purge = command({
name: "purge",
description: "Delete recent messages",
guildOnly: true, // only usable in guilds
nsfw: false, // age-restricted command
defaultMemberPermissions: PermissionFlagsBits.ManageMessages, // who sees it by default
nameLocalizations: { tr: "temizle" }, // localized name
descriptionLocalizations: { tr: "Mesajları sil" },
run: (ctx) => ctx.reply("…"),
});| Field | Type | Effect |
|---|---|---|
guildOnly | boolean | Restricts the command to guild contexts. |
nsfw | boolean | Marks the command age-restricted. |
defaultMemberPermissions | PermissionResolvable | null | Default permission gate (members without it don't see the command). |
nameLocalizations / descriptionLocalizations | LocalizationMap | Per-locale name/description. |
Subcommands and groups
For commands with subcommands, use commandGroup together with subcommand
and (optionally) subcommandGroup. Each subcommand has its own typed options
and handler; spearkit routes to the right one automatically.
import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";
export const admin = commandGroup({
name: "admin",
description: "Administration",
guildOnly: true,
// Direct subcommands: /admin say
subcommands: {
say: subcommand({
description: "Make the bot say something",
options: { message: option.string({ description: "Message", required: true }) },
run: (ctx) => ctx.reply(ctx.options.message),
}),
},
// Grouped subcommands: /admin users ban
groups: {
users: subcommandGroup({
description: "Manage users",
subcommands: {
ban: subcommand({
description: "Ban a member",
options: {
target: option.user({ description: "Member", required: true }),
reason: option.string({ description: "Reason" }),
},
run: (ctx) =>
ctx.reply(`Banned ${ctx.options.target.tag}: ${ctx.options.reason ?? "no reason"}`),
}),
},
}),
},
});Inside a subcommand handler, ctx.options is typed from that subcommand's
options. There is no switch (subcommand) to write — spearkit dispatches by the
invoked subcommand group/name.
The command registry
client.commands is a CommandRegistry. You usually feed it through
client.register(...), but you can use it directly:
import { CommandRegistry } from "spearkit";
const registry = new CommandRegistry();
registry.add(ping, echo, admin);
registry.get("ping"); // SlashCommand | undefined
registry.names; // string[]
registry.size; // number
registry.remove("ping"); // boolean
registry.toJSON(); // REST payloads for all commandsSpearClient calls registry.handle(interaction) and
registry.handleAutocomplete(interaction) for you on every interaction.
Error handling
If a handler throws, spearkit catches it. By default it emits the client's error
event and replies with an ephemeral "something went wrong" message. Override
that:
client.commands.onError((error, interaction) => {
console.error(`/${interaction.commandName} failed`, error);
if (!interaction.replied && !interaction.deferred) {
return interaction.reply({ content: "Command failed.", ephemeral: true });
}
});Deployment
Commands must be registered with discord before they appear. spearkit gives you two ways.
From the client (uses the client's authenticated REST; call after ready):
await client.start(process.env.DISCORD_TOKEN);
await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId for globalStandalone (a separate deploy script, no running client needed):
import { CommandRegistry } from "spearkit";
const registry = new CommandRegistry().add(ping, echo, admin);
await registry.deploy({
token: process.env.DISCORD_TOKEN,
applicationId: process.env.DISCORD_APP_ID,
guildId: process.env.GUILD_ID, // optional
});Guild deploys apply instantly and are ideal during development. Global
deploys (no guildId) can take up to an hour to propagate.
See also
- Options — typed option builders, choices, autocomplete.
- Components — buttons, selects, modals.
- Client — registering and deploying from the client.
- Contexts — the reply helpers every handler shares.
Getting started
spearkit is discord.js++: it re-exports the entire discord.js surface and adds a fully type-safe layer for events, slash commands and interactive components. This page takes you…
Options
Slash command options are declared as a map of name → builder. spearkit infers the exact value type each option resolves to, so your handler's ctx.options is fully typed — no…