spearkit 0.3.0 → 0.3.1
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.
- package/.claude/skills/spearkit/SKILL.md +236 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +218 -0
- package/AGENTS.md +164 -0
- package/README.md +13 -0
- package/docs/README.md +63 -0
- package/docs/api-reference.md +589 -0
- package/docs/client.md +207 -0
- package/docs/commands.md +198 -0
- package/docs/components.md +274 -0
- package/docs/context.md +201 -0
- package/docs/cooldown.md +124 -0
- package/docs/env.md +130 -0
- package/docs/events.md +152 -0
- package/docs/getting-started.md +147 -0
- package/docs/loading.md +142 -0
- package/docs/logging.md +195 -0
- package/docs/migration.md +160 -0
- package/docs/options.md +163 -0
- package/docs/plugins.md +116 -0
- package/docs/prefix.md +180 -0
- package/docs/scheduler.md +87 -0
- package/docs/usage.md +178 -0
- package/llms-full.txt +3367 -0
- package/llms.txt +39 -0
- package/package.json +9 -3
package/docs/context.md
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
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(message)` | `Promise<void>` | State-aware ephemeral message. |
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { command } from "spearkit";
|
|
37
|
+
|
|
38
|
+
export default command({
|
|
39
|
+
name: "demo",
|
|
40
|
+
description: "Reply helpers",
|
|
41
|
+
run: async (ctx) => {
|
|
42
|
+
await ctx.reply("Working on it…");
|
|
43
|
+
await ctx.followUp("…almost done.");
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### `send` is the one most handlers need
|
|
49
|
+
|
|
50
|
+
`send` inspects the interaction state and does the right thing:
|
|
51
|
+
|
|
52
|
+
- not yet answered → `reply`
|
|
53
|
+
- already deferred → `editReply`
|
|
54
|
+
- already replied → `followUp`
|
|
55
|
+
|
|
56
|
+
This means you can call `send` without tracking whether you deferred, which is
|
|
57
|
+
ideal for shared helpers that may run before or after a `defer`.
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
import { command } from "spearkit";
|
|
61
|
+
|
|
62
|
+
export default command({
|
|
63
|
+
name: "report",
|
|
64
|
+
description: "Generate a report",
|
|
65
|
+
run: async (ctx) => {
|
|
66
|
+
await ctx.defer(); // acknowledge while we do slow work
|
|
67
|
+
const data = await buildReport();
|
|
68
|
+
await ctx.send(data); // sees the deferred state → edits the reply
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### `error` for ephemeral failures
|
|
74
|
+
|
|
75
|
+
`error(message)` sends a state-aware, always-ephemeral message — perfect for
|
|
76
|
+
validation failures that only the invoking user should see.
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { command, option } from "spearkit";
|
|
80
|
+
|
|
81
|
+
export default command({
|
|
82
|
+
name: "kick",
|
|
83
|
+
description: "Kick a member",
|
|
84
|
+
options: { who: option.user({ description: "Member", required: true }) },
|
|
85
|
+
run: async (ctx) => {
|
|
86
|
+
if (!ctx.guild) return ctx.error("This command only works in a server.");
|
|
87
|
+
await ctx.reply(`Kicked ${ctx.options.who}.`);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## The `{ ephemeral: true }` shortcut
|
|
93
|
+
|
|
94
|
+
discord.js represents an ephemeral reply with `flags: MessageFlags.Ephemeral`.
|
|
95
|
+
spearkit lets you write the more obvious `{ ephemeral: true }` on any reply payload
|
|
96
|
+
and maps it to that flag for you. The input type is `ReplyInput`
|
|
97
|
+
(`string | ReplyData`), where `ReplyData` is discord.js'
|
|
98
|
+
`InteractionReplyOptions` plus the optional `ephemeral` boolean.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { command, EmbedBuilder } from "spearkit";
|
|
102
|
+
|
|
103
|
+
export default command({
|
|
104
|
+
name: "secret",
|
|
105
|
+
description: "Only you can see this",
|
|
106
|
+
run: (ctx) =>
|
|
107
|
+
ctx.reply({
|
|
108
|
+
embeds: [new EmbedBuilder().setTitle("Just for you")],
|
|
109
|
+
ephemeral: true, // mapped to MessageFlags.Ephemeral
|
|
110
|
+
}),
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`replyEphemeral(input)` is sugar for the same thing, accepting either a string
|
|
115
|
+
or a payload:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
await ctx.replyEphemeral("Saved.");
|
|
119
|
+
await ctx.replyEphemeral({ embeds: [embed] });
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
If you set `flags` yourself, spearkit preserves them and adds the ephemeral flag
|
|
123
|
+
rather than overwriting it.
|
|
124
|
+
|
|
125
|
+
### Exported helpers
|
|
126
|
+
|
|
127
|
+
spearkit exports the two functions it uses internally, so you can normalise reply
|
|
128
|
+
input yourself (e.g. in a plugin or shared utility):
|
|
129
|
+
|
|
130
|
+
- `normalizeReply(input: ReplyInput): InteractionReplyOptions` — converts a
|
|
131
|
+
string or `ReplyData` into a discord.js reply payload, applying the ephemeral
|
|
132
|
+
flag mapping.
|
|
133
|
+
- `asEphemeral(input: ReplyInput): ReplyData` — marks any input ephemeral,
|
|
134
|
+
regardless of how it was passed.
|
|
135
|
+
|
|
136
|
+
```ts
|
|
137
|
+
import { normalizeReply, asEphemeral } from "spearkit";
|
|
138
|
+
|
|
139
|
+
normalizeReply("hi");
|
|
140
|
+
// → { content: "hi" }
|
|
141
|
+
|
|
142
|
+
normalizeReply({ content: "hi", ephemeral: true });
|
|
143
|
+
// → { content: "hi", flags: MessageFlags.Ephemeral }
|
|
144
|
+
|
|
145
|
+
asEphemeral("hidden");
|
|
146
|
+
// → { content: "hidden", ephemeral: true }
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Accessors
|
|
150
|
+
|
|
151
|
+
`BaseContext` forwards the common interaction fields so you do not reach through
|
|
152
|
+
`ctx.interaction` for everyday data:
|
|
153
|
+
|
|
154
|
+
| Accessor | Description |
|
|
155
|
+
| -------- | ----------- |
|
|
156
|
+
| `interaction` | The raw discord.js interaction. |
|
|
157
|
+
| `client` | The `SpearClient` (typed as the interaction's client). |
|
|
158
|
+
| `user` | The invoking `User`. |
|
|
159
|
+
| `member` | The invoking guild member (or `null` outside a guild). |
|
|
160
|
+
| `guild` | The `Guild`, or `null` in DMs. |
|
|
161
|
+
| `guildId` | The guild id, or `null`. |
|
|
162
|
+
| `channel` | The channel the interaction came from. |
|
|
163
|
+
| `channelId` | The channel id. |
|
|
164
|
+
| `locale` | The user's locale. |
|
|
165
|
+
| `deferred` | Whether the interaction is already deferred. |
|
|
166
|
+
| `replied` | Whether the interaction already received an initial response. |
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { command } from "spearkit";
|
|
170
|
+
|
|
171
|
+
export default command({
|
|
172
|
+
name: "whereami",
|
|
173
|
+
description: "Report context",
|
|
174
|
+
run: (ctx) =>
|
|
175
|
+
ctx.reply(
|
|
176
|
+
ctx.guild
|
|
177
|
+
? `In ${ctx.guild.name} (#${ctx.channelId}), locale ${ctx.locale}.`
|
|
178
|
+
: "We're in a DM.",
|
|
179
|
+
),
|
|
180
|
+
});
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
`deferred` and `replied` let you branch when you are not using `send`:
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
import { button } from "spearkit";
|
|
187
|
+
|
|
188
|
+
export default button({
|
|
189
|
+
id: "refresh",
|
|
190
|
+
label: "Refresh",
|
|
191
|
+
run: async (ctx) => {
|
|
192
|
+
if (ctx.replied || ctx.deferred) await ctx.followUp("Refreshed.");
|
|
193
|
+
else await ctx.reply("Refreshed.");
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## See also
|
|
199
|
+
|
|
200
|
+
- [Commands](./commands.md) — `CommandContext`, options and `showModal`.
|
|
201
|
+
- [Components](./components.md) — button, select and modal contexts.
|
package/docs/cooldown.md
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Cooldowns
|
|
2
|
+
|
|
3
|
+
Rate-limit commands per user, per role, per guild, per channel or globally.
|
|
4
|
+
Cooldowns are enforced automatically by command dispatch: when an actor is
|
|
5
|
+
still on cooldown, spearkit replies (ephemerally) with a message and the
|
|
6
|
+
handler does not run.
|
|
7
|
+
|
|
8
|
+
## Per-command
|
|
9
|
+
|
|
10
|
+
Pass a number (milliseconds) or a full config to any command:
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { command } from "spearkit";
|
|
14
|
+
|
|
15
|
+
export const daily = command({
|
|
16
|
+
name: "daily",
|
|
17
|
+
description: "Claim your daily reward",
|
|
18
|
+
cooldown: 86_400_000, // once per day, per user
|
|
19
|
+
run: (ctx) => ctx.reply("Reward claimed!"),
|
|
20
|
+
});
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Client-wide default
|
|
24
|
+
|
|
25
|
+
A default applies to every command; a command's own `cooldown` overrides it.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { SpearClient } from "spearkit";
|
|
29
|
+
|
|
30
|
+
const client = new SpearClient({ cooldown: { duration: 3000 } });
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Scope
|
|
34
|
+
|
|
35
|
+
`scope` controls what the cooldown is keyed on. Default `"user"`.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
command({
|
|
39
|
+
name: "announce",
|
|
40
|
+
description: "Post an announcement",
|
|
41
|
+
cooldown: { duration: 60_000, scope: "guild" }, // one per guild per minute
|
|
42
|
+
run: (ctx) => ctx.reply("Announced."),
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
| Scope | Keyed on |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `user` | the invoking user (default) |
|
|
49
|
+
| `guild` | the guild |
|
|
50
|
+
| `channel` | the channel |
|
|
51
|
+
| `global` | everyone shares one bucket |
|
|
52
|
+
|
|
53
|
+
## Exemptions — who waits and who doesn't
|
|
54
|
+
|
|
55
|
+
`exempt` lists users and roles that bypass the cooldown entirely.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
command({
|
|
59
|
+
name: "purge",
|
|
60
|
+
description: "Bulk delete",
|
|
61
|
+
cooldown: {
|
|
62
|
+
duration: 10_000,
|
|
63
|
+
exempt: { roles: ["111111111111111111"], users: ["222222222222222222"] },
|
|
64
|
+
},
|
|
65
|
+
run: (ctx) => ctx.reply("Purged."),
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Per-role / per-user overrides
|
|
70
|
+
|
|
71
|
+
`overrides` gives specific roles or users a different duration (milliseconds).
|
|
72
|
+
A user override beats role overrides; among matching roles the most lenient
|
|
73
|
+
(shortest) duration wins. Use `0` to effectively disable the wait for them.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
command({
|
|
77
|
+
name: "search",
|
|
78
|
+
description: "Search the archive",
|
|
79
|
+
cooldown: {
|
|
80
|
+
duration: 10_000, // everyone else
|
|
81
|
+
overrides: {
|
|
82
|
+
roles: { "333333333333333333": 2_000 }, // VIP role: 2s
|
|
83
|
+
users: { "444444444444444444": 0 }, // this user: no wait
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
run: (ctx) => ctx.reply("Searching…"),
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## The message
|
|
91
|
+
|
|
92
|
+
`message` customises what blocked users see — a string, or a function of the
|
|
93
|
+
remaining milliseconds.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
command({
|
|
97
|
+
name: "spin",
|
|
98
|
+
description: "Spin the wheel",
|
|
99
|
+
cooldown: {
|
|
100
|
+
duration: 5_000,
|
|
101
|
+
message: (ms) => `Hold on — ${Math.ceil(ms / 1000)}s to go.`,
|
|
102
|
+
},
|
|
103
|
+
run: (ctx) => ctx.reply("🎡"),
|
|
104
|
+
});
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## The manager
|
|
108
|
+
|
|
109
|
+
`client.cooldowns` is the shared `CooldownManager` (also used by
|
|
110
|
+
[prefix commands](./prefix.md)). Use it directly for custom flows:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
const result = client.cooldowns.consume("vote", 5_000, {
|
|
114
|
+
userId: "1",
|
|
115
|
+
roleIds: [],
|
|
116
|
+
guildId: null,
|
|
117
|
+
channelId: null,
|
|
118
|
+
});
|
|
119
|
+
if (!result.allowed) console.log(`wait ${result.remaining}ms`);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`consume` records the use and returns `{ allowed: true }` or
|
|
123
|
+
`{ allowed: false, remaining }`. `peek` checks without recording; `reset` and
|
|
124
|
+
`clear` drop tracked cooldowns.
|
package/docs/env.md
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Environment & dotenv
|
|
2
|
+
|
|
3
|
+
spearkit includes a tiny, dependency-free `.env` loader and a typed reader over
|
|
4
|
+
`process.env`, so a bot needs no extra dotenv dependency. The client auto-loads
|
|
5
|
+
`.env` on `start()`, and the same helpers are exported for your own use.
|
|
6
|
+
|
|
7
|
+
## Loading a `.env` file
|
|
8
|
+
|
|
9
|
+
`loadEnv(options?)` reads a `.env` file and merges it into `process.env`. By
|
|
10
|
+
default it reads `.env` from the current working directory. Variables already
|
|
11
|
+
present in `process.env` win unless you pass `override: true`. A missing file is
|
|
12
|
+
ignored — it simply returns `{}` — so it is safe to call unconditionally.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { loadEnv } from "spearkit";
|
|
16
|
+
|
|
17
|
+
const parsed = loadEnv(); // reads ./.env
|
|
18
|
+
loadEnv({ path: ".env.local" }); // a different file
|
|
19
|
+
loadEnv({ override: true }); // let the file win over existing vars
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
`loadEnv` returns the parsed key/value pairs it read from the file:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { loadEnv } from "spearkit";
|
|
26
|
+
|
|
27
|
+
const parsed = loadEnv(); // ParsedEnv = Record<string, string>
|
|
28
|
+
console.log(Object.keys(parsed));
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Parsing without touching `process.env`
|
|
32
|
+
|
|
33
|
+
`parseEnv(text)` parses `.env`-formatted text into a flat object and never
|
|
34
|
+
mutates `process.env`. It understands single/double quotes, a leading `export `,
|
|
35
|
+
`#` comments, and `\n`/`\r`/`\t` escapes inside double quotes.
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { parseEnv } from "spearkit";
|
|
39
|
+
|
|
40
|
+
const vars = parseEnv(`
|
|
41
|
+
# a comment
|
|
42
|
+
export TOKEN="abc#notacomment"
|
|
43
|
+
GREETING="line one\nline two"
|
|
44
|
+
RAW='no $escapes here'
|
|
45
|
+
`);
|
|
46
|
+
|
|
47
|
+
vars.TOKEN; // "abc#notacomment"
|
|
48
|
+
vars.GREETING; // "line one\nline two" (real newline)
|
|
49
|
+
vars.RAW; // "no $escapes here"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## The typed `env` reader
|
|
53
|
+
|
|
54
|
+
`env` reads from `process.env` with coercion and optional fallbacks. Empty
|
|
55
|
+
strings count as missing.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { env } from "spearkit";
|
|
59
|
+
|
|
60
|
+
env.string("REGION"); // string | undefined
|
|
61
|
+
env.string("REGION", "eu"); // string (fallback when missing)
|
|
62
|
+
|
|
63
|
+
env.number("PORT"); // number | undefined
|
|
64
|
+
env.number("PORT", 3000); // number (fallback when missing or non-numeric)
|
|
65
|
+
|
|
66
|
+
env.boolean("DEBUG"); // boolean | undefined
|
|
67
|
+
env.boolean("DEBUG", false); // boolean
|
|
68
|
+
|
|
69
|
+
env.require("DISCORD_TOKEN"); // string, throws if missing or empty
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`env.boolean` treats `true`/`1`/`yes`/`on` as `true` and `false`/`0`/`no`/`off`
|
|
73
|
+
as `false` (case-insensitive); anything else yields the fallback. `env.require`
|
|
74
|
+
throws a descriptive error when the variable is missing or empty — use it for
|
|
75
|
+
values your bot cannot run without.
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
import { loadEnv, env } from "spearkit";
|
|
79
|
+
|
|
80
|
+
loadEnv();
|
|
81
|
+
const token = env.require("DISCORD_TOKEN"); // guaranteed string
|
|
82
|
+
const port = env.number("PORT", 8080); // number
|
|
83
|
+
const verbose = env.boolean("VERBOSE", false);
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Auto-loading on the client
|
|
87
|
+
|
|
88
|
+
`SpearClient` calls `loadEnv()` for you inside `client.start()`, so `.env` is
|
|
89
|
+
picked up before login. That means `await client.start()` finds
|
|
90
|
+
`DISCORD_TOKEN` from `.env` without any extra wiring:
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
import { SpearClient } from "spearkit";
|
|
94
|
+
|
|
95
|
+
const client = new SpearClient();
|
|
96
|
+
|
|
97
|
+
async function main(): Promise<void> {
|
|
98
|
+
await client.start(); // loads .env, then reads DISCORD_TOKEN
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
void main();
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### The `dotenv` option
|
|
105
|
+
|
|
106
|
+
Control the auto-load with the `dotenv` construction option:
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import { SpearClient } from "spearkit";
|
|
110
|
+
|
|
111
|
+
// Default: load ./.env on start.
|
|
112
|
+
new SpearClient({ dotenv: true });
|
|
113
|
+
|
|
114
|
+
// Disable auto-loading entirely (e.g. env is provided by the platform).
|
|
115
|
+
new SpearClient({ dotenv: false });
|
|
116
|
+
|
|
117
|
+
// Customize: same shape as loadEnv's options.
|
|
118
|
+
new SpearClient({ dotenv: { path: ".env.production", override: true } });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
| `dotenv` value | Effect |
|
|
122
|
+
| -------------- | ------ |
|
|
123
|
+
| `true` / omitted | Load `.env` from the cwd on `start()`. |
|
|
124
|
+
| `false` | Skip auto-loading; `process.env` is used as-is. |
|
|
125
|
+
| `{ path?, override? }` | Load with those `loadEnv` options. |
|
|
126
|
+
|
|
127
|
+
## See also
|
|
128
|
+
|
|
129
|
+
- [Client](./client.md) — the `dotenv` and other construction options.
|
|
130
|
+
- [Logging](./logging.md) — structured logging that pairs with `env`-driven config.
|
package/docs/events.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# Events
|
|
2
|
+
|
|
3
|
+
`event()` defines a reusable, loadable discord.js event listener with a
|
|
4
|
+
fully-typed handler. The handler's arguments are inferred from discord.js'
|
|
5
|
+
`ClientEvents`, so you never annotate them by hand. Register an event with the
|
|
6
|
+
client and spearkit attaches the listener for you.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { event } from "spearkit";
|
|
10
|
+
|
|
11
|
+
export default event("messageCreate", (message) => {
|
|
12
|
+
if (message.author.bot) return;
|
|
13
|
+
// message is fully typed as Message
|
|
14
|
+
});
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Defining an event
|
|
18
|
+
|
|
19
|
+
`event` has two forms. The positional form takes the event name and handler:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { event } from "spearkit";
|
|
23
|
+
|
|
24
|
+
const onMessage = event("messageCreate", (message) => {
|
|
25
|
+
// message: Message
|
|
26
|
+
console.log(message.content);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const onReady = event("clientReady", (client) => {
|
|
30
|
+
// client: Client<true> — the ready client
|
|
31
|
+
console.log(`Logged in as ${client.user.tag}`);
|
|
32
|
+
});
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The object form (`EventConfig`) additionally accepts `once`, which runs the
|
|
36
|
+
handler at most once and then auto-detaches:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
import { event } from "spearkit";
|
|
40
|
+
|
|
41
|
+
const onceReady = event({
|
|
42
|
+
name: "clientReady",
|
|
43
|
+
once: true,
|
|
44
|
+
run: (client) => {
|
|
45
|
+
// client: Client<true>
|
|
46
|
+
console.log(`Ready as ${client.user.tag}`);
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Both forms return an `EventDef` — a type-erased, ready-to-attach listener
|
|
52
|
+
(`{ name, once, attach, detach }`). Register it like anything else:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { SpearClient } from "spearkit";
|
|
56
|
+
|
|
57
|
+
const client = new SpearClient();
|
|
58
|
+
client.register(onMessage, onReady);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Handlers are fully typed from `ClientEvents`
|
|
62
|
+
|
|
63
|
+
The event name drives the parameter types. There is nothing to import or
|
|
64
|
+
annotate — picking `"messageCreate"` types the argument as `Message`, picking
|
|
65
|
+
`"guildMemberAdd"` types it as `GuildMember`, and so on. The handler type is
|
|
66
|
+
exported as `EventHandler<E>` (`(...args: ClientEvents[E]) => Awaitable<void>`).
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { event } from "spearkit";
|
|
70
|
+
|
|
71
|
+
const onJoin = event("guildMemberAdd", (member) => {
|
|
72
|
+
// member: GuildMember
|
|
73
|
+
void member.roles.add("123456789012345678");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const onReaction = event("messageReactionAdd", (reaction, user) => {
|
|
77
|
+
// reaction: MessageReaction | PartialMessageReaction
|
|
78
|
+
// user: User | PartialUser
|
|
79
|
+
console.log(`${user.id} reacted with ${reaction.emoji.name}`);
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Intents are required
|
|
84
|
+
|
|
85
|
+
An event only fires if the client connected with the matching gateway intents.
|
|
86
|
+
For example, `messageCreate` with message content needs `Intents.messages` (or
|
|
87
|
+
at least the `GuildMessages` / `MessageContent` bits); `guildMemberAdd` needs
|
|
88
|
+
`GuildMembers`. See [Client](./client.md) for the intent presets.
|
|
89
|
+
|
|
90
|
+
## Errors are routed, not fatal
|
|
91
|
+
|
|
92
|
+
If a handler throws synchronously or rejects a returned promise, spearkit catches
|
|
93
|
+
it and emits it on the client's `error` event instead of crashing the process.
|
|
94
|
+
Listen for `error` to log or report failures centrally:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { SpearClient } from "spearkit";
|
|
98
|
+
|
|
99
|
+
const client = new SpearClient();
|
|
100
|
+
|
|
101
|
+
client.on("error", (err) => {
|
|
102
|
+
console.error("A handler failed:", err);
|
|
103
|
+
});
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
## Inline listeners still work
|
|
107
|
+
|
|
108
|
+
Because spearkit re-exports discord.js, the plain `client.on(...)` / `client.once(...)`
|
|
109
|
+
listeners work exactly as before — they are the same methods. Reach for them for
|
|
110
|
+
quick, inline, client-local listeners:
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
import { SpearClient } from "spearkit";
|
|
114
|
+
|
|
115
|
+
const client = new SpearClient();
|
|
116
|
+
client.on("guildCreate", (guild) => console.log(`Joined ${guild.name}`));
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Use `event()` when you want a listener that is **reusable and loadable** — a
|
|
120
|
+
self-contained module you can export, register from anywhere, or pick up via
|
|
121
|
+
`client.load(...)`. Note that inline `client.on` listeners do **not** get the
|
|
122
|
+
automatic error-routing that `event()` handlers do.
|
|
123
|
+
|
|
124
|
+
## The `EventRegistry`
|
|
125
|
+
|
|
126
|
+
`client.events` is an `EventRegistry`. The client attaches it automatically at
|
|
127
|
+
construction and again when you `register` an event, so you usually never call
|
|
128
|
+
its methods directly. They are available for advanced control:
|
|
129
|
+
|
|
130
|
+
| Member | Type | Description |
|
|
131
|
+
| ------ | ---- | ----------- |
|
|
132
|
+
| `add(...defs)` | `this` | Register one or more `EventDef`s (and attach them to already-attached clients). |
|
|
133
|
+
| `size` | `number` | Number of registered listeners. |
|
|
134
|
+
| `attachAll(client)` | `void` | Attach every registered listener to a client. |
|
|
135
|
+
| `detachAll(client)` | `void` | Detach every registered listener from a client. |
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { SpearClient, event } from "spearkit";
|
|
139
|
+
|
|
140
|
+
const client = new SpearClient();
|
|
141
|
+
client.events.add(event("warn", (info) => console.warn(info)));
|
|
142
|
+
|
|
143
|
+
console.log(client.events.size); // 1
|
|
144
|
+
|
|
145
|
+
// Detach all spearkit-managed listeners (e.g. before a hot reload).
|
|
146
|
+
client.events.detachAll(client);
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## See also
|
|
150
|
+
|
|
151
|
+
- [Client](./client.md) — registering events and the required intents.
|
|
152
|
+
- [File-based loading](./loading.md) — one event per file, auto-registered.
|