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,281 @@
1
+ # Components
2
+
3
+ Buttons, select menus and modals in spearkit follow one pattern: define the
4
+ appearance, the **custom-id pattern**, and the handler in one place; register
5
+ it; then `build()` the discord.js component to put in a message. spearkit decodes
6
+ incoming interactions and routes them to your handler — no `interactionCreate`
7
+ switch statements, no manual custom-id parsing.
8
+
9
+ ```ts
10
+ import { button, row } from "spearkit";
11
+
12
+ const vote = button({
13
+ id: "vote:{choice}",
14
+ label: "Yes",
15
+ style: "Success",
16
+ run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // ctx.params.choice: string
17
+ });
18
+
19
+ client.register(vote); // or client.components.add(vote)
20
+
21
+ await channel.send({
22
+ content: "Cast your vote:",
23
+ components: [row(vote.build({ choice: "yes" }))], // build() requires { choice }
24
+ });
25
+ ```
26
+
27
+ ## Custom-id patterns
28
+
29
+ The `id` is a pattern with the grammar `name` or `name:{param}` or
30
+ `name:{a}:{b}`. The leading `name` is the routing **namespace**; each `{param}`
31
+ becomes a positional value carried in the custom-id.
32
+
33
+ - In the handler, params are available as a typed object: `ctx.params.choice`.
34
+ - `build(params)` requires **exactly** those params and encodes them into the
35
+ custom-id.
36
+
37
+ ```ts
38
+ const page = button({
39
+ id: "page:{id}:{dir}",
40
+ label: "Next",
41
+ run: (ctx) => ctx.update(`item ${ctx.params.id}, going ${ctx.params.dir}`),
42
+ });
43
+
44
+ page.build({ id: "42", dir: "next" }); // custom-id "page:42:next"
45
+ ```
46
+
47
+ spearkit percent-escapes param values, so they may safely contain `:`. Custom-ids
48
+ are limited to 100 characters (`MAX_CUSTOM_ID_LENGTH`); `build()` throws if you
49
+ exceed it.
50
+
51
+ For advanced use, the codec is exported directly: `compilePattern`,
52
+ `buildCustomId`, `parseCustomId`, and `paramsFromValues`.
53
+
54
+ ## Buttons
55
+
56
+ ```ts
57
+ import { button, linkButton, ButtonStyle } from "spearkit";
58
+
59
+ const confirm = button({
60
+ id: "confirm:{action}",
61
+ label: "Confirm",
62
+ style: ButtonStyle.Danger, // or the string "Danger"
63
+ emoji: "⚠️",
64
+ disabled: false,
65
+ run: (ctx) => ctx.update(`Confirmed: ${ctx.params.action}`),
66
+ });
67
+
68
+ // Link buttons have no handler and no custom-id:
69
+ const docs = linkButton({ url: "https://example.com", label: "Docs" });
70
+ ```
71
+
72
+ `style` accepts the string names `"Primary"`, `"Secondary"`, `"Success"`,
73
+ `"Danger"`, or the `ButtonStyle` enum. It defaults to `"Secondary"`.
74
+
75
+ All component builders (`button`, the five selects, and `modal`) also accept
76
+ `guards?: readonly Guard[]` — preconditions evaluated before the handler runs.
77
+ See [Guards](./guards.md).
78
+
79
+ The `ButtonContext` adds, on top of the shared [reply helpers](./context.md):
80
+
81
+ | Member | Description |
82
+ | ------ | ----------- |
83
+ | `ctx.params` | Decoded custom-id params. |
84
+ | `ctx.update(input)` | Edit the message the button is on. |
85
+ | `ctx.deferUpdate()` | Acknowledge without editing yet. |
86
+ | `ctx.showModal(modal)` | Open a modal in response. |
87
+ | `ctx.message` | The message the button belongs to. |
88
+ | `ctx.customId` | The raw custom-id. |
89
+
90
+ ## Select menus
91
+
92
+ There are five select builders. All share `placeholder`, `minValues`,
93
+ `maxValues`, and `disabled`; the string select additionally takes `options`,
94
+ and the channel select takes `channelTypes`.
95
+
96
+ ```ts
97
+ import { stringSelect, channelSelect, ChannelType } from "spearkit";
98
+
99
+ const colour = stringSelect({
100
+ id: "colour",
101
+ placeholder: "Pick a colour",
102
+ minValues: 1,
103
+ maxValues: 1,
104
+ options: [
105
+ { label: "Red", value: "red" },
106
+ { label: "Green", value: "green", description: "the calm one" },
107
+ { label: "Blue", value: "blue", default: true },
108
+ ],
109
+ run: (ctx) => ctx.reply({ content: `You picked ${ctx.values.join(", ")}`, ephemeral: true }),
110
+ });
111
+
112
+ const pickChannel = channelSelect({
113
+ id: "pick-channel",
114
+ channelTypes: [ChannelType.GuildText],
115
+ run: (ctx) => ctx.reply({ content: `${ctx.values.length} channel(s)`, ephemeral: true }),
116
+ });
117
+ ```
118
+
119
+ Each select context exposes the relevant resolved data:
120
+
121
+ | Builder | Context | Extra accessors |
122
+ | ------- | ------- | --------------- |
123
+ | `stringSelect` | `StringSelectContext` | `values: string[]`, `value: string \| undefined` |
124
+ | `userSelect` | `UserSelectContext` | `values`, `users`, `members` |
125
+ | `roleSelect` | `RoleSelectContext` | `values`, `roles` |
126
+ | `channelSelect` | `ChannelSelectContext` | `values`, `channels` |
127
+ | `mentionableSelect` | `MentionableSelectContext` | `values`, `users`, `roles`, `members` |
128
+
129
+ Select contexts also have `ctx.params`, `ctx.update`, `ctx.deferUpdate`,
130
+ `ctx.showModal`, and the shared reply helpers.
131
+
132
+ ## Modals
133
+
134
+ A modal declares its `fields` as a map of name → `textInput`. The submit handler
135
+ receives the submitted values in `ctx.fields`, keyed (and typed) by those names,
136
+ plus any custom-id params in `ctx.params`.
137
+
138
+ ```ts
139
+ import { modal, textInput } from "spearkit";
140
+
141
+ const feedback = modal({
142
+ id: "feedback:{ticket}",
143
+ title: "Feedback",
144
+ fields: {
145
+ summary: textInput({ label: "Summary", required: true }),
146
+ detail: textInput({ label: "Details", style: "Paragraph", maxLength: 2000 }),
147
+ },
148
+ run: (ctx) =>
149
+ ctx.reply({
150
+ // ctx.params.ticket: string, ctx.fields.summary / ctx.fields.detail: string
151
+ content: `#${ctx.params.ticket}: ${ctx.fields.summary}`,
152
+ ephemeral: true,
153
+ }),
154
+ });
155
+ ```
156
+
157
+ `textInput` config: `label` (required), `style` (`"Short"` default, or
158
+ `"Paragraph"`, or a `TextInputStyle`), `placeholder`, `required`, `minLength`,
159
+ `maxLength`, `value`.
160
+
161
+ Open a modal from a command or a component handler with `showModal` — modals
162
+ cannot be the *response* to another modal, but they can follow a command or a
163
+ button/select:
164
+
165
+ ```ts
166
+ import { command } from "spearkit";
167
+
168
+ const ask = command({
169
+ name: "ask",
170
+ description: "Open the feedback form",
171
+ run: (ctx) => ctx.showModal(feedback.build({ ticket: "1234" })),
172
+ });
173
+ ```
174
+
175
+ ## Action rows
176
+
177
+ `row(...components)` wraps builders in an `ActionRowBuilder`. A row holds up to
178
+ five buttons, or exactly one select menu.
179
+
180
+ ```ts
181
+ import { row } from "spearkit";
182
+
183
+ const components = [
184
+ row(confirm.build({ action: "delete" }), docs),
185
+ row(colour.build()),
186
+ ];
187
+ await channel.send({ content: "Choose:", components });
188
+ ```
189
+
190
+ ## Registering and routing
191
+
192
+ Register components like anything else:
193
+
194
+ ```ts
195
+ client.register(vote, colour, feedback);
196
+ // equivalently:
197
+ client.components.add(vote, colour, feedback);
198
+ ```
199
+
200
+ `SpearClient` routes every button, select and modal interaction to the matching
201
+ namespace automatically. The `ComponentRegistry` API:
202
+
203
+ | Member | Description |
204
+ | ------ | ----------- |
205
+ | `add(...defs)` | Register components (override by namespace). |
206
+ | `size` | Number registered. |
207
+ | `onError(handler)` | Set the error handler. |
208
+ | `handle(interaction)` | Route an interaction; returns `true` if matched. |
209
+ | `setDefaultGuards(guards)` | Guards run before each component's own guards. |
210
+
211
+ `setLogger` and `setUsageHook` also exist; the client wires all three for you.
212
+
213
+ ### Error handling
214
+
215
+ By default a throwing handler emits the client `error` event and replies with an
216
+ ephemeral message. Customise it:
217
+
218
+ ```ts
219
+ client.components.onError((error, interaction) => {
220
+ console.error("component failed", error);
221
+ });
222
+ ```
223
+
224
+ ## End-to-end example
225
+
226
+ ```ts
227
+ import {
228
+ SpearClient,
229
+ Intents,
230
+ command,
231
+ button,
232
+ stringSelect,
233
+ modal,
234
+ textInput,
235
+ row,
236
+ } from "spearkit";
237
+
238
+ const client = new SpearClient({ intents: Intents.default });
239
+
240
+ const open = button({
241
+ id: "open-form:{topic}",
242
+ label: "Open form",
243
+ style: "Primary",
244
+ run: (ctx) => ctx.showModal(form.build({ topic: ctx.params.topic })),
245
+ });
246
+
247
+ const rating = stringSelect({
248
+ id: "rating",
249
+ placeholder: "Rate us",
250
+ options: [
251
+ { label: "Good", value: "good" },
252
+ { label: "Bad", value: "bad" },
253
+ ],
254
+ run: (ctx) => ctx.reply({ content: `Thanks: ${ctx.value}`, ephemeral: true }),
255
+ });
256
+
257
+ const form = modal({
258
+ id: "form:{topic}",
259
+ title: "Tell us more",
260
+ fields: { body: textInput({ label: "Message", style: "Paragraph", required: true }) },
261
+ run: (ctx) => ctx.reply({ content: `[${ctx.params.topic}] ${ctx.fields.body}`, ephemeral: true }),
262
+ });
263
+
264
+ const panel = command({
265
+ name: "panel",
266
+ description: "Show the panel",
267
+ run: (ctx) =>
268
+ ctx.reply({
269
+ content: "How was it?",
270
+ components: [row(open.build({ topic: "support" })), row(rating.build())],
271
+ }),
272
+ });
273
+
274
+ client.register(panel, open, rating, form);
275
+ ```
276
+
277
+ ## See also
278
+
279
+ - [Commands](./commands.md) — opening components from commands.
280
+ - [Contexts](./context.md) — the reply/update helpers contexts share.
281
+ - [Client](./client.md) — registration and routing.
@@ -0,0 +1,121 @@
1
+ # Context-menu commands
2
+
3
+ Context-menu commands are the right-click **"Apps"** actions Discord shows on a
4
+ user or a message. spearkit makes them first-class: define one with `userCommand`
5
+ or `messageCommand`, register it like anything else, and deploy it alongside your
6
+ slash commands. The handler gets a typed `targetUser` or `targetMessage`.
7
+
8
+ ```ts
9
+ import { userCommand } from "spearkit";
10
+
11
+ export const whois = userCommand({
12
+ name: "Who is this?",
13
+ run: (ctx) => ctx.replyEphemeral(`That's ${ctx.targetUser.tag}.`),
14
+ });
15
+ ```
16
+
17
+ `name` is the label shown in the Apps menu (no description — Discord does not show
18
+ one for context-menu commands).
19
+
20
+ ## User vs message commands
21
+
22
+ | Builder | Appears on | Target context |
23
+ | ------- | ---------- | -------------- |
24
+ | `userCommand` | a user (right-click → Apps) | `ctx.targetUser`, `ctx.targetMember` |
25
+ | `messageCommand` | a message (right-click → Apps) | `ctx.targetMessage` |
26
+
27
+ ```ts
28
+ import { messageCommand } from "spearkit";
29
+
30
+ export const report = messageCommand({
31
+ name: "Report message",
32
+ run: (ctx) =>
33
+ ctx.replyEphemeral(`Reported message ${ctx.targetMessage.id}.`),
34
+ });
35
+ ```
36
+
37
+ Both handler contexts extend the shared [`BaseContext`](./context.md), so
38
+ `ctx.reply`, `ctx.replyEphemeral`, `ctx.defer`, `ctx.success/error/...` and the
39
+ usual accessors are all available.
40
+
41
+ ## Metadata, cooldowns and guards
42
+
43
+ Both builders accept the same metadata, plus a `cooldown` and `guards`:
44
+
45
+ | Field | Type | Effect |
46
+ | ----- | ---- | ------ |
47
+ | `name` | `string` | The Apps-menu label. |
48
+ | `defaultMemberPermissions` | `PermissionResolvable \| null` | Default permission gate. |
49
+ | `nsfw` | `boolean` | Marks the command age-restricted. |
50
+ | `guildOnly` | `boolean` | Restricts it to guild contexts. |
51
+ | `nameLocalizations` | `LocalizationMap` | Per-locale label. |
52
+ | `cooldown` | `number \| CooldownConfig` | Rate limit (shares `client.cooldowns`). |
53
+ | `guards` | `readonly Guard[]` | Preconditions run before the handler. |
54
+ | `autoDefer` | `boolean \| { ephemeral?, delayMs? }` | Auto-`deferReply()` if the handler is slow, preventing `Unknown interaction`. |
55
+
56
+ ```ts
57
+ import { userCommand, guildOnly, requireUserPermissions, PermissionFlagsBits } from "spearkit";
58
+
59
+ export const warn = userCommand({
60
+ name: "Warn user",
61
+ guildOnly: true,
62
+ cooldown: 5_000,
63
+ guards: [requireUserPermissions(PermissionFlagsBits.ModerateMembers)],
64
+ run: (ctx) => ctx.replyEphemeral(`Warned ${ctx.targetUser.tag}.`),
65
+ });
66
+ ```
67
+
68
+ See [Cooldowns](./cooldown.md) and [Guards](./guards.md) for the shared options.
69
+
70
+ ## Registering and deploying
71
+
72
+ Register context-menu commands like everything else with `client.register(...)`.
73
+ They route automatically — spearkit dispatches user- and message-context-menu
74
+ interactions to the matching command.
75
+
76
+ ```ts
77
+ client.register(whois, report, warn);
78
+ ```
79
+
80
+ Because context menus and slash commands deploy to the same Discord endpoint,
81
+ push them together with `deployAllCommands` once you mix the two — it sends both
82
+ in a single request. (`deployCommands` is slash-only.)
83
+
84
+ ```ts
85
+ await client.start(process.env.DISCORD_TOKEN);
86
+ await client.deployAllCommands({ guildId: process.env.GUILD_ID }); // slash + menus
87
+ ```
88
+
89
+ `deployAllCommands` also supports a `dryRun` flag and a `strategy: "diff"` that
90
+ skips the PUT when the remote set already matches — handy in CI:
91
+
92
+ ```ts
93
+ // Preview without touching Discord:
94
+ const result = await client.deployAllCommands({ guildId, dryRun: true });
95
+ // → { skipped: true, reason: "dry-run", body: [...] }
96
+
97
+ // Only deploy when something changed:
98
+ await client.deployAllCommands({ guildId, strategy: "diff" });
99
+ ```
100
+
101
+ ## The registry
102
+
103
+ `client.contextMenus` is a `ContextMenuRegistry`. The client wires it to the
104
+ logger and cooldown manager and routes interactions for you, so you rarely touch
105
+ it directly:
106
+
107
+ ```ts
108
+ client.contextMenus.size; // number registered
109
+ client.contextMenus.all(); // ContextMenuCommand[]
110
+ client.contextMenus.toJSON(); // REST payloads (also included by deployAllCommands)
111
+ ```
112
+
113
+ Note: context-menu commands are **not** picked up by `client.load(...)`
114
+ directory loading — register them explicitly with `client.register(...)`.
115
+
116
+ ## See also
117
+
118
+ - [Commands](./commands.md) — slash commands.
119
+ - [Client](./client.md) — `deployAllCommands` and registration.
120
+ - [Guards](./guards.md) / [Cooldowns](./cooldown.md) — the shared preconditions.
121
+ - [Contexts](./context.md) — the reply helpers every handler shares.
@@ -0,0 +1,293 @@
1
+ # Contexts
2
+
3
+ Every spearkit handler — command, button, select, modal — receives a context
4
+ object. They all share `BaseContext`, which smooths over discord.js'
5
+ reply/defer/edit/follow-up state machine and exposes the common
6
+ actor/location accessors. Learn it once and it applies everywhere.
7
+
8
+ ```ts
9
+ import { command, option } from "spearkit";
10
+
11
+ export default command({
12
+ name: "hello",
13
+ description: "Say hello",
14
+ options: { name: option.string({ description: "Name", required: true }) },
15
+ run: (ctx) => ctx.reply(`Hi, ${ctx.options.name}!`),
16
+ });
17
+ ```
18
+
19
+ `CommandContext`, `ButtonContext`, `StringSelectContext`, modal contexts and the
20
+ rest extend `BaseContext`, adding their own specifics (e.g. `ctx.options`,
21
+ `ctx.params`, `ctx.fields`) on top of everything below.
22
+
23
+ ## Reply helpers
24
+
25
+ | Method | Returns | Behaviour |
26
+ | ------ | ------- | --------- |
27
+ | `reply(input)` | `Promise<InteractionResponse>` | Send the initial response. |
28
+ | `replyEphemeral(input)` | `Promise<InteractionResponse>` | Reply, hidden to everyone but the invoking user. |
29
+ | `defer({ ephemeral })` | `Promise<InteractionResponse>` | Acknowledge now, respond later via `editReply`. |
30
+ | `editReply(input)` | `Promise<Message>` | Edit the original (or deferred) response. |
31
+ | `followUp(input)` | `Promise<Message>` | Add a message after the initial response. |
32
+ | `send(input)` | `Promise<void>` | State-aware: replies, edits, or follows up automatically. |
33
+ | `error(input, options?)` | `Promise<void>` | State-aware preset error embed; ephemeral by default. |
34
+ | `success` / `info` / `warn` `(input, options?)` | `Promise<void>` | State-aware preset embeds. |
35
+ | `replyError` / `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `Promise<InteractionResponse>` | Initial-reply preset embeds. |
36
+
37
+ ```ts
38
+ import { command } from "spearkit";
39
+
40
+ export default command({
41
+ name: "demo",
42
+ description: "Reply helpers",
43
+ run: async (ctx) => {
44
+ await ctx.reply("Working on it…");
45
+ await ctx.followUp("…almost done.");
46
+ },
47
+ });
48
+ ```
49
+
50
+ ### `send` is the one most handlers need
51
+
52
+ `send` inspects the interaction state and does the right thing:
53
+
54
+ - not yet answered → `reply`
55
+ - already deferred → `editReply`
56
+ - already replied → `followUp`
57
+
58
+ This means you can call `send` without tracking whether you deferred, which is
59
+ ideal for shared helpers that may run before or after a `defer`.
60
+
61
+ ```ts
62
+ import { command } from "spearkit";
63
+
64
+ export default command({
65
+ name: "report",
66
+ description: "Generate a report",
67
+ run: async (ctx) => {
68
+ await ctx.defer(); // acknowledge while we do slow work
69
+ const data = await buildReport();
70
+ await ctx.send(data); // sees the deferred state → edits the reply
71
+ },
72
+ });
73
+ ```
74
+
75
+ ### `error` for ephemeral failures
76
+
77
+ `error(input, options?)` sends a state-aware preset **error embed** — ephemeral
78
+ by default (pass `{ ephemeral: false }` to make it public) — perfect for
79
+ validation failures that only the invoking user should see.
80
+
81
+ ```ts
82
+ import { command, option } from "spearkit";
83
+
84
+ export default command({
85
+ name: "kick",
86
+ description: "Kick a member",
87
+ options: { who: option.user({ description: "Member", required: true }) },
88
+ run: async (ctx) => {
89
+ if (!ctx.guild) return ctx.error("This command only works in a server.");
90
+ await ctx.reply(`Kicked ${ctx.options.who}.`);
91
+ },
92
+ });
93
+ ```
94
+
95
+ ## Preset embeds
96
+
97
+ `BaseContext` builds consistent, colored embeds from `client.embeds` (or a shared
98
+ default). Each takes an `EmbedPresetInput` — a plain string, or a structured body
99
+ (`{ title?, description?, fields?, footer?, ... }`) — and an optional
100
+ `{ ephemeral? }`.
101
+
102
+ | Method | Sends via | Default visibility |
103
+ | ------ | --------- | ------------------ |
104
+ | `success(input, options?)` | `send` (state-aware) | public |
105
+ | `info(input, options?)` | `send` (state-aware) | public |
106
+ | `warn(input, options?)` | `send` (state-aware) | public |
107
+ | `error(input, options?)` | `send` (state-aware) | **ephemeral** |
108
+ | `replySuccess` / `replyInfo` / `replyWarn` `(input, options?)` | `reply` (initial only) | public |
109
+ | `replyError(input, options?)` | `reply` (initial only) | **ephemeral** |
110
+
111
+ ```ts
112
+ import { command } from "spearkit";
113
+
114
+ export default command({
115
+ name: "save",
116
+ description: "Save settings",
117
+ run: async (ctx) => {
118
+ await ctx.success("Settings saved."); // green embed, public
119
+ await ctx.warn({ title: "Heads up", description: "Quota is almost full." });
120
+ // error defaults to ephemeral; make it public with { ephemeral: false }:
121
+ // await ctx.error("Failed to save.", { ephemeral: false });
122
+ },
123
+ });
124
+ ```
125
+
126
+ Configure the colors/icons with the client `embeds` option; see the
127
+ [API reference](./api-reference.md#embeds--preset-replies).
128
+
129
+ ## The `{ ephemeral: true }` shortcut
130
+
131
+ discord.js represents an ephemeral reply with `flags: MessageFlags.Ephemeral`.
132
+ spearkit lets you write the more obvious `{ ephemeral: true }` on any reply payload
133
+ and maps it to that flag for you. The input type is `ReplyInput`
134
+ (`string | ReplyData`), where `ReplyData` is discord.js'
135
+ `InteractionReplyOptions` plus the optional `ephemeral` boolean.
136
+
137
+ ```ts
138
+ import { command, EmbedBuilder } from "spearkit";
139
+
140
+ export default command({
141
+ name: "secret",
142
+ description: "Only you can see this",
143
+ run: (ctx) =>
144
+ ctx.reply({
145
+ embeds: [new EmbedBuilder().setTitle("Just for you")],
146
+ ephemeral: true, // mapped to MessageFlags.Ephemeral
147
+ }),
148
+ });
149
+ ```
150
+
151
+ `replyEphemeral(input)` is sugar for the same thing, accepting either a string
152
+ or a payload:
153
+
154
+ ```ts
155
+ await ctx.replyEphemeral("Saved.");
156
+ await ctx.replyEphemeral({ embeds: [embed] });
157
+ ```
158
+
159
+ If you set `flags` yourself, spearkit preserves them and adds the ephemeral flag
160
+ rather than overwriting it.
161
+
162
+ ### Exported helpers
163
+
164
+ spearkit exports the two functions it uses internally, so you can normalise reply
165
+ input yourself (e.g. in a plugin or shared utility):
166
+
167
+ - `normalizeReply(input: ReplyInput): InteractionReplyOptions` — converts a
168
+ string or `ReplyData` into a discord.js reply payload, applying the ephemeral
169
+ flag mapping.
170
+ - `asEphemeral(input: ReplyInput): ReplyData` — marks any input ephemeral,
171
+ regardless of how it was passed.
172
+
173
+ ```ts
174
+ import { normalizeReply, asEphemeral } from "spearkit";
175
+
176
+ normalizeReply("hi");
177
+ // → { content: "hi" }
178
+
179
+ normalizeReply({ content: "hi", ephemeral: true });
180
+ // → { content: "hi", flags: MessageFlags.Ephemeral }
181
+
182
+ asEphemeral("hidden");
183
+ // → { content: "hidden", ephemeral: true }
184
+ ```
185
+
186
+ ## Accessors
187
+
188
+ `BaseContext` forwards the common interaction fields so you do not reach through
189
+ `ctx.interaction` for everyday data:
190
+
191
+ | Accessor | Description |
192
+ | -------- | ----------- |
193
+ | `interaction` | The raw discord.js interaction. |
194
+ | `client` | The `SpearClient` (typed as the interaction's client). |
195
+ | `user` | The invoking `User`. |
196
+ | `member` | The invoking guild member (or `null` outside a guild). |
197
+ | `guild` | The `Guild`, or `null` in DMs. |
198
+ | `guildId` | The guild id, or `null`. |
199
+ | `channel` | The channel the interaction came from. |
200
+ | `channelId` | The channel id. |
201
+ | `locale` | The user's locale. |
202
+ | `deferred` | Whether the interaction is already deferred. |
203
+ | `replied` | Whether the interaction already received an initial response. |
204
+ | `botPermissions` | The bot's resolved permissions in the channel (`PermissionsBitField`, zero-fetch). |
205
+
206
+ ```ts
207
+ import { command } from "spearkit";
208
+
209
+ export default command({
210
+ name: "whereami",
211
+ description: "Report context",
212
+ run: (ctx) =>
213
+ ctx.reply(
214
+ ctx.guild
215
+ ? `In ${ctx.guild.name} (#${ctx.channelId}), locale ${ctx.locale}.`
216
+ : "We're in a DM.",
217
+ ),
218
+ });
219
+ ```
220
+
221
+ `deferred` and `replied` let you branch when you are not using `send`:
222
+
223
+ ```ts
224
+ import { button } from "spearkit";
225
+
226
+ export default button({
227
+ id: "refresh",
228
+ label: "Refresh",
229
+ run: async (ctx) => {
230
+ if (ctx.replied || ctx.deferred) await ctx.followUp("Refreshed.");
231
+ else await ctx.reply("Refreshed.");
232
+ },
233
+ });
234
+ ```
235
+
236
+ ## Permission preflights
237
+
238
+ `BaseContext` reads the permissions Discord already attached to the interaction —
239
+ no extra fetches — so you can check before attempting a privileged action:
240
+
241
+ ```ts
242
+ import { command, PermissionFlagsBits } from "spearkit";
243
+
244
+ export default command({
245
+ name: "slowmode",
246
+ description: "Set slowmode",
247
+ run: async (ctx) => {
248
+ const missing = ctx.botMissing(PermissionFlagsBits.ManageChannels);
249
+ if (missing.length > 0) return ctx.error(`I'm missing: ${missing.join(", ")}`);
250
+ // …apply slowmode…
251
+ },
252
+ });
253
+ ```
254
+
255
+ - `ctx.botPermissions` — the bot's `PermissionsBitField` in the current channel.
256
+ - `ctx.botMissing(required)` — permission names the bot lacks here (`[]` if none).
257
+ - `ctx.userMissing(required)` — permission names the invoking user lacks here.
258
+
259
+ For role-hierarchy and moderation preflights (acting on self/owner, comparing top
260
+ roles) see `moderationCheck` and the permission helpers in the
261
+ [API reference](./api-reference.md#permissions--moderation).
262
+
263
+ ## Awaiting input
264
+
265
+ When a flow needs a follow-up message or a modal, the context wraps discord.js
266
+ collectors so you skip the boilerplate. Both resolve to `null` on timeout.
267
+
268
+ ```ts
269
+ import { command, modal, textInput } from "spearkit";
270
+
271
+ const nameModal = modal({ id: "name", title: "Your name", fields: { name: textInput({ label: "Name" }) }, run: () => {} });
272
+
273
+ export default command({
274
+ name: "setup",
275
+ description: "Interactive setup",
276
+ run: async (ctx) => {
277
+ // Wait for the user to type an answer in this channel:
278
+ const reply = await ctx.awaitMessageFrom(ctx.user.id, { time: 30_000 });
279
+ if (reply === null) return ctx.error("Timed out.");
280
+ // Or show a modal and await its submission:
281
+ const submission = await ctx.awaitModal(nameModal);
282
+ if (submission !== null) await submission.reply(`Hi, ${submission.fields.getTextInputValue("name")}!`);
283
+ },
284
+ });
285
+ ```
286
+
287
+ The standalone `awaitMessage`, `awaitComponent` and `showAndAwaitModal` helpers
288
+ are also exported; see the [API reference](./api-reference.md#collectors).
289
+
290
+ ## See also
291
+
292
+ - [Commands](./commands.md) — `CommandContext`, options and `showModal`.
293
+ - [Components](./components.md) — button, select and modal contexts.