spearkit 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,74 @@
1
+ # Auto-defer
2
+
3
+ The single most common discord.js error is
4
+ `DiscordAPIError[10062]: Unknown interaction`. An interaction token is valid for
5
+ only **3 seconds** before your first response; any handler that awaits a database
6
+ query or an HTTP call risks blowing past that window, after which the interaction
7
+ is dead and every reply throws.
8
+
9
+ Auto-defer removes the footgun: spearkit arms a timer when your handler starts
10
+ and, if you haven't responded in time, calls `deferReply()` for you. The timer is
11
+ cancelled the instant your handler replies or defers itself.
12
+
13
+ ## Per command
14
+
15
+ ```ts
16
+ import { command, option } from "spearkit";
17
+
18
+ export const weather = command({
19
+ name: "weather",
20
+ description: "Look up the weather",
21
+ autoDefer: true, // defers automatically if the handler takes too long
22
+ options: { city: option.string({ description: "City", required: true }) },
23
+ run: async (ctx) => {
24
+ const report = await fetchWeather(ctx.options.city); // slow
25
+ await ctx.send(`Weather in ${ctx.options.city}: ${report}`);
26
+ },
27
+ });
28
+ ```
29
+
30
+ > With auto-defer on, respond via `ctx.send(...)` or `ctx.editReply(...)`, not
31
+ > `ctx.reply(...)` — the initial reply slot may already be taken by the
32
+ > auto-defer. `ctx.send` is state-aware and always does the right thing.
33
+
34
+ ## Options
35
+
36
+ `autoDefer` accepts `true` (defaults) or an object:
37
+
38
+ ```ts
39
+ command({
40
+ name: "report",
41
+ description: "Generate a report",
42
+ autoDefer: { ephemeral: true, delayMs: 1500 },
43
+ run: async (ctx) => ctx.send("…"),
44
+ });
45
+ ```
46
+
47
+ | Field | Default | Meaning |
48
+ | --- | --- | --- |
49
+ | `ephemeral` | `false` | Defer as a hidden ("thinking…") response. |
50
+ | `delayMs` | `2000` | How long to wait before the safety defer fires. Kept under the 3s cutoff. |
51
+
52
+ ## Client-wide default
53
+
54
+ Apply auto-defer to **every** slash command and context menu; each handler can
55
+ still override with its own `autoDefer`.
56
+
57
+ ```ts
58
+ import { SpearClient } from "spearkit";
59
+
60
+ const client = new SpearClient({ autoDefer: true });
61
+ ```
62
+
63
+ ## Scope
64
+
65
+ Auto-defer covers slash commands and context menus (answered with `deferReply`).
66
+ Component handlers (buttons/selects) usually respond instantly with `update`, so
67
+ they're not auto-deferred — call `ctx.deferUpdate()` yourself if a component
68
+ handler does slow work.
69
+
70
+ ## Lower-level helpers
71
+
72
+ `normalizeAutoDefer(input)` resolves `true`/object/`undefined` into an
73
+ `AutoDeferConfig`; `armAutoDefer(interaction, config)` arms the timer and returns
74
+ a cancel function. Both are exported for custom dispatch.
package/docs/client.md ADDED
@@ -0,0 +1,245 @@
1
+ # Client
2
+
3
+ `SpearClient` is a discord.js `Client` with command, event and component
4
+ registries — plus interaction routing — wired up for you. You construct it the
5
+ same way you construct a discord.js client, register your handlers, log in, and
6
+ (optionally) push your slash commands to Discord.
7
+
8
+ ```ts
9
+ import { SpearClient, Intents } from "spearkit";
10
+
11
+ const client = new SpearClient({ intents: Intents.default });
12
+ ```
13
+
14
+ ## Constructing a client
15
+
16
+ `new SpearClient(options?)` takes the same options as discord.js'
17
+ `ClientOptions`, except `intents` may be omitted: it defaults to
18
+ `Intents.default` (just the `Guilds` intent, enough for slash commands and
19
+ interactions).
20
+
21
+ ```ts
22
+ import { SpearClient, Intents } from "spearkit";
23
+
24
+ // Explicit preset.
25
+ const a = new SpearClient({ intents: Intents.messages });
26
+
27
+ // Omitted — falls back to Intents.default.
28
+ const b = new SpearClient();
29
+ ```
30
+
31
+ The options type is exported as `SpearClientOptions` — `Partial<ClientOptions> &
32
+ SpearOptions`. Every discord.js option (`partials`, `presence`, `sweepers`, …) is
33
+ available, plus spearkit's own: `logger`, `dotenv`, `cooldown`, `prefix`, `usage`,
34
+ `embeds`, `guards` and `autoDefer` (each covered in its own guide).
35
+
36
+ ### Intents presets
37
+
38
+ `Intents` is a set of ready-made arrays of `GatewayIntentBits`. Pass one as
39
+ `intents`, or compose your own array of `GatewayIntentBits` if you need
40
+ something in between.
41
+
42
+ | Preset | Contents |
43
+ | ------ | -------- |
44
+ | `Intents.none` | `[]` |
45
+ | `Intents.default` | `[Guilds]` |
46
+ | `Intents.guilds` | `[Guilds, GuildMembers]` |
47
+ | `Intents.messages` | `[Guilds, GuildMessages, MessageContent]` |
48
+ | `Intents.all` | Every intent, including privileged ones. |
49
+
50
+ `Intents.messages` includes `MessageContent`, and `Intents.guilds` includes
51
+ `GuildMembers` — both are **privileged intents**. You must enable them in the
52
+ Discord developer portal for your application, otherwise the gateway will reject
53
+ the connection. `Intents.all` includes every privileged intent for the same
54
+ reason.
55
+
56
+ ```ts
57
+ import { SpearClient, GatewayIntentBits } from "spearkit";
58
+
59
+ // A custom intent set, mixing a preset idea with explicit bits.
60
+ const client = new SpearClient({
61
+ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates],
62
+ });
63
+ ```
64
+
65
+ ## Registries and subsystems
66
+
67
+ Every client owns a set of registries and subsystems, each populated by
68
+ `register` (or `load`) or configured by an option:
69
+
70
+ | Member | Type | Holds / does |
71
+ | ------ | ---- | ------------ |
72
+ | `client.commands` | `CommandRegistry` | Slash commands; dispatches chat-input and autocomplete interactions. |
73
+ | `client.events` | `EventRegistry` | Event listeners; attached to the client automatically. |
74
+ | `client.components` | `ComponentRegistry` | Buttons, selects and modals; routed by custom-id namespace. |
75
+ | `client.contextMenus` | `ContextMenuRegistry` | User / message context-menu ("Apps") commands. |
76
+ | `client.prefix` | `PrefixRegistry` | Prefix (text) commands, dispatched from `messageCreate`. |
77
+ | `client.scheduler` | `TaskScheduler` | Cron / interval tasks; started on ready, stopped on `destroy`. |
78
+ | `client.cooldowns` | `CooldownManager` | Shared rate-limit state across commands and prefix commands. |
79
+ | `client.usage` | `UsageTracker` | Records who used what to a store and/or channel. |
80
+ | `client.logger` | `Logger` | Structured, scoped logger used across spearkit. |
81
+ | `client.embeds` | `Embeds` | Preset embed factory behind `ctx.success/error/...`. |
82
+
83
+ You rarely touch the registries directly — `register` routes items into the right
84
+ one — but they are public for inspection and advanced control (e.g.
85
+ `client.commands.size`, `client.commands.toJSON()`). Each subsystem has its own
86
+ guide: [Cooldowns](./cooldown.md), [Scheduled tasks](./scheduler.md),
87
+ [Prefix commands](./prefix.md), [Context menus](./context-menus.md),
88
+ [Logging](./logging.md), [Usage tracking](./usage.md), [Guards](./guards.md).
89
+
90
+ ## Registering handlers
91
+
92
+ `client.register(...items)` accepts commands, events, components, context-menu
93
+ commands, prefix commands and scheduled tasks in a single call and routes each to
94
+ its registry by kind. The accepted union is exported as `Registerable`
95
+ (`SlashCommand | EventDef | ComponentDef | ScheduledTask | PrefixCommand |
96
+ ContextMenuCommand`). It returns the client for chaining.
97
+
98
+ ```ts
99
+ import { SpearClient, command, event, button, option } from "spearkit";
100
+
101
+ const client = new SpearClient();
102
+
103
+ const greet = command({
104
+ name: "greet",
105
+ description: "Greet someone",
106
+ options: { who: option.user({ description: "Who", required: true }) },
107
+ run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
108
+ });
109
+
110
+ const ready = event("clientReady", (c) => {
111
+ console.log(`Logged in as ${c.user.tag}`); // c: Client<true>
112
+ });
113
+
114
+ const ping = button({
115
+ id: "ping:{n}",
116
+ label: "Ping",
117
+ run: (ctx) => ctx.reply(`pong #${ctx.params.n}`), // n: string
118
+ });
119
+
120
+ // Commands, events and components in one call.
121
+ client.register(greet, ready, ping);
122
+ ```
123
+
124
+ ## Plugins
125
+
126
+ `client.use(...plugins)` installs one or more plugins, awaiting each plugin's
127
+ `setup`. It is async and returns the client.
128
+
129
+ ```ts
130
+ import { SpearClient } from "spearkit";
131
+ import { statsPlugin } from "./plugins/stats.js";
132
+
133
+ const client = new SpearClient();
134
+ await client.use(statsPlugin);
135
+ ```
136
+
137
+ See [Plugins](./plugins.md) for authoring `SpearPlugin`s.
138
+
139
+ ## File-based loading
140
+
141
+ `client.load(dir, options?)` recursively imports a directory and registers every
142
+ spearkit-registrable export it finds — commands, events, components, scheduled
143
+ tasks and prefix commands. It returns the number of items registered.
144
+
145
+ ```ts
146
+ import { SpearClient } from "spearkit";
147
+
148
+ const client = new SpearClient();
149
+ const count = await client.load("./src/commands");
150
+ console.log(`Loaded ${count} handlers`);
151
+ ```
152
+
153
+ See [File-based loading](./loading.md) for the layout and `LoadOptions`.
154
+
155
+ ## Starting and deploying
156
+
157
+ `client.start(token?)` logs in. If you omit the token it falls back to the
158
+ `DISCORD_TOKEN` environment variable, and throws if neither is present.
159
+
160
+ ```ts
161
+ import { SpearClient } from "spearkit";
162
+
163
+ const client = new SpearClient();
164
+
165
+ // Pass a token explicitly…
166
+ await client.start("your-token");
167
+
168
+ // …or set DISCORD_TOKEN and call start() with no argument.
169
+ await client.start();
170
+ ```
171
+
172
+ `client.deployCommands({ guildId })` pushes the registered slash commands to
173
+ Discord using the client's own authenticated REST connection — there is no
174
+ separate token or application id to supply. Because it reads the application id
175
+ from the logged-in client, it **must run after the client is ready**. Pass a
176
+ `guildId` to deploy instantly to a single guild (ideal for development); omit it
177
+ to deploy globally.
178
+
179
+ ```ts
180
+ import { SpearClient, Intents } from "spearkit";
181
+
182
+ const client = new SpearClient({ intents: Intents.default });
183
+ // …register commands…
184
+
185
+ await client.start(); // uses DISCORD_TOKEN
186
+
187
+ // Deploy once the client is ready.
188
+ client.once("clientReady", async () => {
189
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
190
+ });
191
+ ```
192
+
193
+ ## Reliability: auto-defer and graceful shutdown
194
+
195
+ A slow handler that doesn't respond within Discord's 3-second window dies with
196
+ `Unknown interaction` (10062). Set `autoDefer` to have spearkit `deferReply()`
197
+ automatically just before that window closes — per handler (`command({ autoDefer:
198
+ true })`, `userCommand`/`messageCommand`) or for every slash + context-menu
199
+ handler at once:
200
+
201
+ ```ts
202
+ const client = new SpearClient({ autoDefer: true });
203
+ // or { ephemeral: true, delayMs: 1500 } for a hidden defer / earlier fire.
204
+ ```
205
+
206
+ With auto-defer on, respond via `ctx.send(...)` or `ctx.editReply(...)` — the
207
+ initial reply slot may already be taken by the safety defer.
208
+
209
+ `client.enableGracefulShutdown(options?)` closes the bot cleanly on `SIGINT` /
210
+ `SIGTERM`: it runs an optional `onShutdown` hook, calls `destroy()` (stopping the
211
+ scheduler and gateway), and exits, with a hard timeout so a wedged shutdown can't
212
+ hang. It returns a disposer that removes the signal handlers.
213
+
214
+ ```ts
215
+ client.enableGracefulShutdown({ onShutdown: () => db.close() });
216
+ ```
217
+
218
+ ## Everything discord.js still works
219
+
220
+ `SpearClient` extends discord.js `Client`, so the full client surface is
221
+ available unchanged. spearkit adds registries on top — it never hides what is
222
+ underneath:
223
+
224
+ ```ts
225
+ import { SpearClient } from "spearkit";
226
+
227
+ const client = new SpearClient();
228
+
229
+ client.on("guildCreate", (guild) => console.log(`Joined ${guild.name}`));
230
+ client.ws.on("VOICE_SERVER_UPDATE", () => {});
231
+
232
+ await client.start();
233
+
234
+ console.log(client.application?.id); // application
235
+ console.log(client.user?.tag); // user
236
+ console.log(client.rest); // REST manager (used by deployCommands)
237
+
238
+ await client.destroy(); // graceful shutdown
239
+ ```
240
+
241
+ ## See also
242
+
243
+ - [Commands](./commands.md) — defining slash commands you register here.
244
+ - [Plugins](./plugins.md) — bundling features for `client.use`.
245
+ - [File-based loading](./loading.md) — populating the client from a directory.
@@ -0,0 +1,65 @@
1
+ # Collectors
2
+
3
+ discord.js collectors are powerful but fiddly: you wire an event emitter, set a
4
+ `time`, write a `filter`, remember that dismissed modals need their own timeout,
5
+ and translate the "timed out" rejection into something you can branch on.
6
+ spearkit collapses the common cases to a single `await` that resolves to the
7
+ result — or `null` on timeout.
8
+
9
+ Beyond these, see the [pagination and confirm helpers](./api-reference.md#pagination--confirmation)
10
+ for ready-made paged lists and yes/no gates.
11
+
12
+ ## Wait for a message ("type your answer")
13
+
14
+ `ctx.awaitMessageFrom(userId?, options?)` waits for the next message in the
15
+ current channel from a user (defaults to the invoking user):
16
+
17
+ ```ts
18
+ await ctx.reply("What's your favourite colour?");
19
+ const answer = await ctx.awaitMessageFrom(ctx.user.id, { time: 30_000 });
20
+ if (answer === null) return ctx.followUp("Timed out.");
21
+ await ctx.followUp(`Nice — ${answer.content}!`);
22
+ ```
23
+
24
+ The standalone `awaitMessage(channel, options)` does the same for any text
25
+ channel; `options` takes `{ filter, time }` (default `time` 60s).
26
+
27
+ ## Wait for a modal submission
28
+
29
+ `ctx.awaitModal(modal, options?)` (on command and component contexts) shows a
30
+ modal and waits for the submission — scoped to the same user and that modal's
31
+ custom-id, always bounded — sidestepping the "Unknown interaction after a
32
+ cancelled modal" trap:
33
+
34
+ ```ts
35
+ import { modal, textInput } from "spearkit";
36
+
37
+ const form = modal({
38
+ id: "feedback",
39
+ title: "Feedback",
40
+ fields: { text: textInput({ label: "Your feedback", required: true }) },
41
+ run: (ctx) => ctx.replyEphemeral("thanks"), // routed fallback
42
+ });
43
+
44
+ const submitted = await ctx.awaitModal(form.build(), { time: 120_000 });
45
+ if (submitted === null) return; // dismissed or timed out
46
+ await submitted.reply({ content: submitted.fields.getTextInputValue("text"), ephemeral: true });
47
+ ```
48
+
49
+ This is the inline alternative to registering a separate modal handler and
50
+ threading state through its custom-id.
51
+
52
+ ## Wait for a component click
53
+
54
+ `awaitComponent(message, options)` waits for the next button/select interaction
55
+ on a message. `options` takes `{ filter, time, componentType }`. You must still
56
+ acknowledge the returned interaction (`update`/`deferUpdate`/`reply`):
57
+
58
+ ```ts
59
+ import { awaitComponent } from "spearkit";
60
+
61
+ const sent = await ctx.channel!.send({ content: "Pick one", components: [row] });
62
+ const click = await awaitComponent(sent, { time: 15_000 });
63
+ if (click === null) return;
64
+ await click.update("Got it!");
65
+ ```
@@ -0,0 +1,203 @@
1
+ # Commands
2
+
3
+ Slash commands in spearkit are defined as a single object: the metadata, the typed
4
+ options, and the handler all live together. spearkit serialises them for discord
5
+ and routes incoming interactions to the right handler for you.
6
+
7
+ ## A first command
8
+
9
+ ```ts
10
+ import { command } from "spearkit";
11
+
12
+ export const ping = command({
13
+ name: "ping",
14
+ description: "Check latency",
15
+ run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
16
+ });
17
+ ```
18
+
19
+ Register it on a client (`client.register(ping)`) and deploy it (see
20
+ [Deployment](#deployment)). That's the whole loop.
21
+
22
+ ## The command context
23
+
24
+ The handler receives a `CommandContext`. It wraps the discord.js
25
+ `ChatInputCommandInteraction` and adds ergonomic accessors and reply helpers.
26
+
27
+ | Member | Description |
28
+ | ------ | ----------- |
29
+ | `ctx.options` | Resolved, fully-typed option values (see [Options](./options.md)). |
30
+ | `ctx.commandName` | The invoked command name. |
31
+ | `ctx.subcommand` | The invoked subcommand name, or `null`. |
32
+ | `ctx.showModal(modal)` | Present a modal in response. |
33
+ | `ctx.user` / `ctx.member` / `ctx.guild` / `ctx.guildId` / `ctx.channel` / `ctx.channelId` / `ctx.locale` | Actor and location accessors. |
34
+ | `ctx.reply` / `ctx.replyEphemeral` / `ctx.defer` / `ctx.editReply` / `ctx.followUp` / `ctx.send` / `ctx.error` | Reply helpers (see [Contexts](./context.md)). |
35
+ | `ctx.interaction` | The raw discord.js interaction, for anything not wrapped. |
36
+
37
+ ```ts
38
+ import { command, option } from "spearkit";
39
+
40
+ export const echo = command({
41
+ name: "echo",
42
+ description: "Repeat a message",
43
+ options: {
44
+ text: option.string({ description: "What to say", required: true }),
45
+ times: option.integer({ description: "Repeat count", minValue: 1, maxValue: 5 }),
46
+ },
47
+ run: (ctx) => {
48
+ ctx.options.text; // string
49
+ ctx.options.times; // number | undefined
50
+ return ctx.reply({
51
+ content: ctx.options.text.repeat(ctx.options.times ?? 1),
52
+ ephemeral: true,
53
+ });
54
+ },
55
+ });
56
+ ```
57
+
58
+ Options are covered in depth in [Options](./options.md).
59
+
60
+ ## Command metadata
61
+
62
+ ```ts
63
+ import { command, PermissionFlagsBits } from "spearkit";
64
+
65
+ export const purge = command({
66
+ name: "purge",
67
+ description: "Delete recent messages",
68
+ guildOnly: true, // only usable in guilds
69
+ nsfw: false, // age-restricted command
70
+ defaultMemberPermissions: PermissionFlagsBits.ManageMessages, // who sees it by default
71
+ nameLocalizations: { tr: "temizle" }, // localized name
72
+ descriptionLocalizations: { tr: "Mesajları sil" },
73
+ run: (ctx) => ctx.reply("…"),
74
+ });
75
+ ```
76
+
77
+ | Field | Type | Effect |
78
+ | ----- | ---- | ------ |
79
+ | `guildOnly` | `boolean` | Restricts the command to guild contexts. |
80
+ | `nsfw` | `boolean` | Marks the command age-restricted. |
81
+ | `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate (members without it don't see the command). |
82
+ | `nameLocalizations` / `descriptionLocalizations` | `LocalizationMap` | Per-locale name/description. |
83
+ | `cooldown` | `number \| CooldownConfig` | Rate-limit the command (a number is milliseconds). See [Cooldowns](./cooldown.md). |
84
+ | `guards` | `readonly Guard[]` | Preconditions run before the handler. See [Guards](./guards.md). |
85
+ | `autoDefer` | `boolean \| { ephemeral?, delayMs? }` | Auto-`deferReply()` if the handler is slow (>~2s), preventing `Unknown interaction`. Respond via `ctx.send`/`ctx.editReply`. |
86
+
87
+ ## Subcommands and groups
88
+
89
+ For commands with subcommands, use `commandGroup` together with `subcommand`
90
+ and (optionally) `subcommandGroup`. Each subcommand has its own typed options
91
+ and handler; spearkit routes to the right one automatically.
92
+
93
+ ```ts
94
+ import { commandGroup, subcommand, subcommandGroup, option } from "spearkit";
95
+
96
+ export const admin = commandGroup({
97
+ name: "admin",
98
+ description: "Administration",
99
+ guildOnly: true,
100
+ // Direct subcommands: /admin say
101
+ subcommands: {
102
+ say: subcommand({
103
+ description: "Make the bot say something",
104
+ options: { message: option.string({ description: "Message", required: true }) },
105
+ run: (ctx) => ctx.reply(ctx.options.message),
106
+ }),
107
+ },
108
+ // Grouped subcommands: /admin users ban
109
+ groups: {
110
+ users: subcommandGroup({
111
+ description: "Manage users",
112
+ subcommands: {
113
+ ban: subcommand({
114
+ description: "Ban a member",
115
+ options: {
116
+ target: option.user({ description: "Member", required: true }),
117
+ reason: option.string({ description: "Reason" }),
118
+ },
119
+ run: (ctx) =>
120
+ ctx.reply(`Banned ${ctx.options.target.tag}: ${ctx.options.reason ?? "no reason"}`),
121
+ }),
122
+ },
123
+ }),
124
+ },
125
+ });
126
+ ```
127
+
128
+ Inside a subcommand handler, `ctx.options` is typed from *that subcommand's*
129
+ options. There is no `switch (subcommand)` to write — spearkit dispatches by the
130
+ invoked subcommand group/name.
131
+
132
+ ## The command registry
133
+
134
+ `client.commands` is a `CommandRegistry`. You usually feed it through
135
+ `client.register(...)`, but you can use it directly:
136
+
137
+ ```ts
138
+ import { CommandRegistry } from "spearkit";
139
+
140
+ const registry = new CommandRegistry();
141
+ registry.add(ping, echo, admin);
142
+
143
+ registry.get("ping"); // SlashCommand | undefined
144
+ registry.names; // string[]
145
+ registry.size; // number
146
+ registry.remove("ping"); // boolean
147
+ registry.toJSON(); // REST payloads for all commands
148
+ ```
149
+
150
+ `SpearClient` calls `registry.handle(interaction)` and
151
+ `registry.handleAutocomplete(interaction)` for you on every interaction.
152
+
153
+ ### Error handling
154
+
155
+ If a handler throws, spearkit catches it. By default it emits the client's `error`
156
+ event and replies with an ephemeral "something went wrong" message. Override
157
+ that:
158
+
159
+ ```ts
160
+ client.commands.onError((error, interaction) => {
161
+ console.error(`/${interaction.commandName} failed`, error);
162
+ if (!interaction.replied && !interaction.deferred) {
163
+ return interaction.reply({ content: "Command failed.", ephemeral: true });
164
+ }
165
+ });
166
+ ```
167
+
168
+ ## Deployment
169
+
170
+ Commands must be registered with discord before they appear. spearkit gives you two
171
+ ways.
172
+
173
+ **From the client** (uses the client's authenticated REST; call after ready):
174
+
175
+ ```ts
176
+ await client.start(process.env.DISCORD_TOKEN);
177
+ await client.deployCommands({ guildId: process.env.GUILD_ID }); // omit guildId for global
178
+ ```
179
+
180
+ **Standalone** (a separate deploy script, no running client needed):
181
+
182
+ ```ts
183
+ import { CommandRegistry } from "spearkit";
184
+
185
+ const registry = new CommandRegistry().add(ping, echo, admin);
186
+ await registry.deploy({
187
+ token: process.env.DISCORD_TOKEN,
188
+ applicationId: process.env.DISCORD_APP_ID,
189
+ guildId: process.env.GUILD_ID, // optional
190
+ });
191
+ ```
192
+
193
+ Guild deploys apply **instantly** and are ideal during development. Global
194
+ deploys (no `guildId`) can take up to an hour to propagate.
195
+
196
+ ## See also
197
+
198
+ - [Options](./options.md) — typed option builders, choices, autocomplete.
199
+ - [Components](./components.md) — buttons, selects, modals.
200
+ - [Client](./client.md) — registering and deploying from the client.
201
+ - [Contexts](./context.md) — the reply helpers every handler shares.
202
+ - [Cooldowns](./cooldown.md) — rate-limit a command with `cooldown`.
203
+ - [Guards](./guards.md) — gate a command with `guards`.