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.
- package/.claude/skills/spearkit/SKILL.md +247 -0
- package/.claude/skills/spearkit/reference/cheatsheet.md +329 -0
- package/AGENTS.md +261 -0
- package/README.md +23 -3
- package/dist/index.cjs +599 -16
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +524 -2
- package/dist/index.d.ts +524 -2
- package/dist/index.js +576 -19
- package/dist/index.js.map +1 -1
- package/docs/README.md +72 -0
- package/docs/api-reference.md +777 -0
- package/docs/auto-defer.md +74 -0
- package/docs/client.md +245 -0
- package/docs/collectors.md +65 -0
- package/docs/commands.md +203 -0
- package/docs/components.md +281 -0
- package/docs/context-menus.md +121 -0
- package/docs/context.md +293 -0
- package/docs/cooldown.md +125 -0
- package/docs/env.md +130 -0
- package/docs/errors.md +73 -0
- package/docs/events.md +152 -0
- package/docs/getting-started.md +147 -0
- package/docs/guards.md +146 -0
- package/docs/loading.md +144 -0
- package/docs/logging.md +195 -0
- package/docs/messages.md +35 -0
- package/docs/migration.md +160 -0
- package/docs/options.md +163 -0
- package/docs/permissions.md +68 -0
- package/docs/plugins.md +116 -0
- package/docs/prefix.md +234 -0
- package/docs/scheduler.md +111 -0
- package/docs/shutdown.md +42 -0
- package/docs/store.md +90 -0
- package/docs/usage.md +188 -0
- package/llms-full.txt +4619 -0
- package/llms.txt +127 -0
- package/package.json +9 -3
package/docs/cooldown.md
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# Cooldowns
|
|
2
|
+
|
|
3
|
+
Rate-limit commands per user, per guild, per channel, or globally — with per-role
|
|
4
|
+
and per-user exemptions and overrides.
|
|
5
|
+
Cooldowns are enforced automatically by command dispatch: when an actor is
|
|
6
|
+
still on cooldown, spearkit replies (ephemerally) with a message and the
|
|
7
|
+
handler does not run.
|
|
8
|
+
|
|
9
|
+
## Per-command
|
|
10
|
+
|
|
11
|
+
Pass a number (milliseconds) or a full config to any command:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { command } from "spearkit";
|
|
15
|
+
|
|
16
|
+
export const daily = command({
|
|
17
|
+
name: "daily",
|
|
18
|
+
description: "Claim your daily reward",
|
|
19
|
+
cooldown: 86_400_000, // once per day, per user
|
|
20
|
+
run: (ctx) => ctx.reply("Reward claimed!"),
|
|
21
|
+
});
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Client-wide default
|
|
25
|
+
|
|
26
|
+
A default applies to every command; a command's own `cooldown` overrides it.
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { SpearClient } from "spearkit";
|
|
30
|
+
|
|
31
|
+
const client = new SpearClient({ cooldown: { duration: 3000 } });
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Scope
|
|
35
|
+
|
|
36
|
+
`scope` controls what the cooldown is keyed on. Default `"user"`.
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
command({
|
|
40
|
+
name: "announce",
|
|
41
|
+
description: "Post an announcement",
|
|
42
|
+
cooldown: { duration: 60_000, scope: "guild" }, // one per guild per minute
|
|
43
|
+
run: (ctx) => ctx.reply("Announced."),
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
| Scope | Keyed on |
|
|
48
|
+
| --- | --- |
|
|
49
|
+
| `user` | the invoking user (default) |
|
|
50
|
+
| `guild` | the guild |
|
|
51
|
+
| `channel` | the channel |
|
|
52
|
+
| `global` | everyone shares one bucket |
|
|
53
|
+
|
|
54
|
+
## Exemptions — who waits and who doesn't
|
|
55
|
+
|
|
56
|
+
`exempt` lists users and roles that bypass the cooldown entirely.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
command({
|
|
60
|
+
name: "purge",
|
|
61
|
+
description: "Bulk delete",
|
|
62
|
+
cooldown: {
|
|
63
|
+
duration: 10_000,
|
|
64
|
+
exempt: { roles: ["111111111111111111"], users: ["222222222222222222"] },
|
|
65
|
+
},
|
|
66
|
+
run: (ctx) => ctx.reply("Purged."),
|
|
67
|
+
});
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Per-role / per-user overrides
|
|
71
|
+
|
|
72
|
+
`overrides` gives specific roles or users a different duration (milliseconds).
|
|
73
|
+
A user override beats role overrides; among matching roles the most lenient
|
|
74
|
+
(shortest) duration wins. Use `0` to effectively disable the wait for them.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
command({
|
|
78
|
+
name: "search",
|
|
79
|
+
description: "Search the archive",
|
|
80
|
+
cooldown: {
|
|
81
|
+
duration: 10_000, // everyone else
|
|
82
|
+
overrides: {
|
|
83
|
+
roles: { "333333333333333333": 2_000 }, // VIP role: 2s
|
|
84
|
+
users: { "444444444444444444": 0 }, // this user: no wait
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
run: (ctx) => ctx.reply("Searching…"),
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## The message
|
|
92
|
+
|
|
93
|
+
`message` customises what blocked users see — a string, or a function of the
|
|
94
|
+
remaining milliseconds.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
command({
|
|
98
|
+
name: "spin",
|
|
99
|
+
description: "Spin the wheel",
|
|
100
|
+
cooldown: {
|
|
101
|
+
duration: 5_000,
|
|
102
|
+
message: (ms) => `Hold on — ${Math.ceil(ms / 1000)}s to go.`,
|
|
103
|
+
},
|
|
104
|
+
run: (ctx) => ctx.reply("🎡"),
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## The manager
|
|
109
|
+
|
|
110
|
+
`client.cooldowns` is the shared `CooldownManager` (also used by
|
|
111
|
+
[prefix commands](./prefix.md)). Use it directly for custom flows:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
const result = client.cooldowns.consume("vote", 5_000, {
|
|
115
|
+
userId: "1",
|
|
116
|
+
roleIds: [],
|
|
117
|
+
guildId: null,
|
|
118
|
+
channelId: null,
|
|
119
|
+
});
|
|
120
|
+
if (!result.allowed) console.log(`wait ${result.remaining}ms`);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`consume` records the use and returns `{ allowed: true }` or
|
|
124
|
+
`{ allowed: false, remaining }`. `peek` checks without recording; `reset` and
|
|
125
|
+
`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/errors.md
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Discord API errors
|
|
2
|
+
|
|
3
|
+
discord.js reports REST failures as `DiscordAPIError` with a numeric `code`
|
|
4
|
+
(`10008` "Unknown Message", `50013` "Missing Permissions", `50007` "Cannot send
|
|
5
|
+
DMs to this user", …). Catching *everything* turns recoverable failures — a
|
|
6
|
+
deleted message, a closed DM — into crashes or scary stack traces. spearkit gives
|
|
7
|
+
you named codes, a type-narrowing predicate, and a friendly explanation.
|
|
8
|
+
|
|
9
|
+
## Recognise and recover
|
|
10
|
+
|
|
11
|
+
`isDiscordError(err, code?)` narrows the throw and optionally matches a code
|
|
12
|
+
(or a list). Perfect for "ignore this one, re-throw the rest":
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { DiscordErrorCode, isDiscordError } from "spearkit";
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
await message.delete();
|
|
19
|
+
} catch (err) {
|
|
20
|
+
if (isDiscordError(err, DiscordErrorCode.UnknownMessage)) return; // already gone
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// match any of several codes
|
|
27
|
+
if (isDiscordError(err, [DiscordErrorCode.UnknownChannel, DiscordErrorCode.MissingAccess])) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Friendly messages
|
|
33
|
+
|
|
34
|
+
`explainDiscordError(err)` returns an end-user-appropriate sentence for a
|
|
35
|
+
recognised failure, or `null` otherwise (fall back to a generic message + log):
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { explainDiscordError } from "spearkit";
|
|
39
|
+
|
|
40
|
+
catch (err) {
|
|
41
|
+
await ctx.error(explainDiscordError(err) ?? "Something went wrong.");
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
spearkit already routes its own command/context-menu errors through
|
|
46
|
+
`explainDiscordError`, so a handler that throws `Missing Permissions` shows the
|
|
47
|
+
user *"I'm missing the permissions needed to do that."* instead of a generic
|
|
48
|
+
error.
|
|
49
|
+
|
|
50
|
+
## Named codes
|
|
51
|
+
|
|
52
|
+
`DiscordErrorCode` is a curated map of the codes bots actually hit:
|
|
53
|
+
|
|
54
|
+
| Name | Code | When |
|
|
55
|
+
| --- | --- | --- |
|
|
56
|
+
| `UnknownChannel` | 10003 | Channel gone/invisible |
|
|
57
|
+
| `UnknownMessage` | 10008 | Message deleted |
|
|
58
|
+
| `UnknownMember` | 10007 | Member left |
|
|
59
|
+
| `UnknownInteraction` | 10062 | Token expired (the 3s window) |
|
|
60
|
+
| `MissingAccess` | 50001 | No access to the resource |
|
|
61
|
+
| `CannotSendMessagesToThisUser` | 50007 | DMs closed / blocked |
|
|
62
|
+
| `MissingPermissions` | 50013 | Missing a permission |
|
|
63
|
+
| `InteractionHasAlreadyBeenAcknowledged` | 40060 | Double-acked |
|
|
64
|
+
|
|
65
|
+
(See the type for the full set — it mirrors discord.js' `RESTJSONErrorCodes`.)
|
|
66
|
+
|
|
67
|
+
## Transport & rate-limit errors
|
|
68
|
+
|
|
69
|
+
- `isHTTPError(err)` — a transport-level `HTTPError` (timeout, 5xx, aborted): an
|
|
70
|
+
HTTP status with no Discord JSON code.
|
|
71
|
+
- `isRateLimitError(err)` — a `DiscordAPIError` with HTTP status `429`.
|
|
72
|
+
`explainDiscordError` handles this case first, returning a "try again in a
|
|
73
|
+
moment" message.
|
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.
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# Getting started
|
|
2
|
+
|
|
3
|
+
spearkit is **discord.js++**: it re-exports the entire discord.js surface and adds a
|
|
4
|
+
fully type-safe layer for events, slash commands and interactive components. This
|
|
5
|
+
page takes you from an empty folder to a running bot that responds to a slash
|
|
6
|
+
command.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
spearkit sits alongside discord.js, so install both:
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install spearkit discord.js
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Everything in your code imports from `"spearkit"` — including the plain discord.js
|
|
17
|
+
symbols, which spearkit re-exports unchanged.
|
|
18
|
+
|
|
19
|
+
## Credentials you need
|
|
20
|
+
|
|
21
|
+
Create an application in the [Discord Developer Portal](https://discord.com/developers/applications)
|
|
22
|
+
and collect three values:
|
|
23
|
+
|
|
24
|
+
| Value | Where to find it | Used for |
|
|
25
|
+
| ----- | ---------------- | -------- |
|
|
26
|
+
| Bot token | Application → **Bot** → *Reset Token* | `client.start(token)` |
|
|
27
|
+
| Application id | Application → **General Information** → *Application ID* | command deployment (spearkit reads it from the client once ready) |
|
|
28
|
+
| Test guild id | Right-click your server in Discord (with Developer Mode on) → *Copy Server ID* | guild-scoped deploy |
|
|
29
|
+
|
|
30
|
+
Keep the token secret. The examples below read these from the environment
|
|
31
|
+
(`DISCORD_TOKEN`, `GUILD_ID`).
|
|
32
|
+
|
|
33
|
+
## Your first bot
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import { SpearClient, Intents, command, option, event } from "spearkit";
|
|
37
|
+
|
|
38
|
+
const client = new SpearClient({ intents: Intents.default });
|
|
39
|
+
|
|
40
|
+
const greet = command({
|
|
41
|
+
name: "greet",
|
|
42
|
+
description: "Greet someone",
|
|
43
|
+
options: {
|
|
44
|
+
who: option.user({ description: "Who to greet", required: true }),
|
|
45
|
+
},
|
|
46
|
+
run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // ctx.options.who: User
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const ready = event("clientReady", (c) => console.log(`Online as ${c.user.tag}`));
|
|
50
|
+
|
|
51
|
+
client.register(greet, ready);
|
|
52
|
+
|
|
53
|
+
await client.start(process.env.DISCORD_TOKEN);
|
|
54
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
What each step does:
|
|
58
|
+
|
|
59
|
+
1. **`new SpearClient({ intents })`** — a discord.js `Client` with command, event
|
|
60
|
+
and component routing wired up. `Intents.default` is `[Guilds]`, enough for
|
|
61
|
+
slash commands and interactions.
|
|
62
|
+
2. **`command({ ... })`** — defines a leaf slash command. Required options resolve
|
|
63
|
+
to their value type (`who` is a `User`); optional options would resolve to
|
|
64
|
+
`value | undefined`.
|
|
65
|
+
3. **`client.register(...)`** — routes each item to the matching registry
|
|
66
|
+
(commands, events, components) by its kind.
|
|
67
|
+
4. **`client.start(token)`** — logs in. With no argument it falls back to the
|
|
68
|
+
`DISCORD_TOKEN` environment variable.
|
|
69
|
+
5. **`client.deployCommands({ guildId })`** — pushes your command definitions to
|
|
70
|
+
Discord over the client's own authenticated REST connection. Must run after the
|
|
71
|
+
client is ready (i.e. after `start`).
|
|
72
|
+
|
|
73
|
+
### Guild vs global deploy
|
|
74
|
+
|
|
75
|
+
`deployCommands` takes an optional `guildId`:
|
|
76
|
+
|
|
77
|
+
- **Guild deploy** (`{ guildId }`) registers commands in a single server. Changes
|
|
78
|
+
appear **instantly** — ideal while developing.
|
|
79
|
+
- **Global deploy** (omit `guildId`) registers commands across every server the
|
|
80
|
+
bot is in. Propagation can take up to an hour.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
await client.deployCommands({ guildId: process.env.GUILD_ID }); // instant, one guild
|
|
84
|
+
await client.deployCommands(); // global, slow to propagate
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
You only need to deploy when your command *definitions* change (names,
|
|
88
|
+
descriptions, options) — not on every restart.
|
|
89
|
+
|
|
90
|
+
## Suggested project layout
|
|
91
|
+
|
|
92
|
+
As a bot grows, give each command, event and component its own file:
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
my-bot/
|
|
96
|
+
src/
|
|
97
|
+
index.ts # construct the client, register/load, start, deploy
|
|
98
|
+
commands/
|
|
99
|
+
greet.ts
|
|
100
|
+
ping.ts
|
|
101
|
+
events/
|
|
102
|
+
ready.ts
|
|
103
|
+
components/
|
|
104
|
+
vote.ts
|
|
105
|
+
package.json
|
|
106
|
+
tsconfig.json
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
A module exports a command, event or component as a default or named export:
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
// src/commands/ping.ts
|
|
113
|
+
import { command } from "spearkit";
|
|
114
|
+
|
|
115
|
+
export default command({
|
|
116
|
+
name: "ping",
|
|
117
|
+
description: "Check that the bot is alive",
|
|
118
|
+
run: (ctx) => ctx.reply(`Pong! ${ctx.client.ws.ping}ms`),
|
|
119
|
+
});
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
You can wire the pieces up explicitly with `register`, or let spearkit discover them
|
|
123
|
+
with `client.load` (see [File-based loading](./loading.md)).
|
|
124
|
+
|
|
125
|
+
## Running it
|
|
126
|
+
|
|
127
|
+
**With tsx** (run TypeScript directly, great for development):
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
npx tsx src/index.ts
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Compiled JavaScript** (for production):
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
npx tsc # emit JS into dist/ per your tsconfig
|
|
137
|
+
node dist/index.js
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Note that `client.load` imports **compiled JavaScript**, so if you use file-based
|
|
141
|
+
loading you must build before running the compiled output. Explicit `register`
|
|
142
|
+
calls work the same under `tsx` or `node`.
|
|
143
|
+
|
|
144
|
+
## See also
|
|
145
|
+
|
|
146
|
+
- [Client](./client.md) — `SpearClient`, intents, `register`, `start`, deployment.
|
|
147
|
+
- [Commands](./commands.md) — slash commands, subcommands, options, deployment.
|