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.
@@ -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.
@@ -0,0 +1,142 @@
1
+ # File-based loading
2
+
3
+ Instead of importing and registering every handler by hand, you can keep one
4
+ command, event or component per file and let spearkit discover them. The loader
5
+ imports a directory, inspects each module's exports, and registers everything that
6
+ is a command, event or component.
7
+
8
+ ## `client.load`
9
+
10
+ ```ts
11
+ load(dir: string, options?: LoadOptions): Promise<number>
12
+ ```
13
+
14
+ `client.load` imports `dir` and registers every spearkit-registrable export it finds,
15
+ resolving to the number of items registered.
16
+
17
+ ```ts
18
+ import { fileURLToPath } from "node:url";
19
+ import { SpearClient, Intents } from "spearkit";
20
+
21
+ const here = fileURLToPath(new URL(".", import.meta.url));
22
+
23
+ const client = new SpearClient({ intents: Intents.default });
24
+
25
+ const loaded =
26
+ (await client.load(`${here}commands`)) +
27
+ (await client.load(`${here}events`)) +
28
+ (await client.load(`${here}components`));
29
+ console.log(`Loaded ${loaded} modules.`);
30
+
31
+ await client.start(process.env.DISCORD_TOKEN);
32
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
33
+ ```
34
+
35
+ ### What gets registered
36
+
37
+ For every imported file, spearkit walks **all** of its exports — default *and*
38
+ named — and registers each value that is a command (`command`, `commandGroup`),
39
+ an event (`event`), or a component (`button`, `stringSelect`, `modal`, …). Other
40
+ exports (helpers, constants, types) are ignored. So both of these are picked up:
41
+
42
+ ```ts
43
+ // default export
44
+ export default command({ name: "ping", description: "…", run: (ctx) => ctx.reply("pong") });
45
+ ```
46
+
47
+ ```ts
48
+ // named export
49
+ export const vote = button({ id: "vote:{choice}", label: "Vote", run: (ctx) => ctx.update(ctx.params.choice) });
50
+ ```
51
+
52
+ ### Options
53
+
54
+ ```ts
55
+ interface LoadOptions {
56
+ extensions?: readonly string[]; // default: [".js", ".mjs", ".cjs"]
57
+ recursive?: boolean; // default: true
58
+ }
59
+ ```
60
+
61
+ - **`extensions`** — which file extensions to import. By default the loader reads
62
+ `.js`, `.mjs` and `.cjs` — i.e. **compiled JavaScript**, not `.ts` source.
63
+ - **`recursive`** — by default the loader descends into subdirectories. Pass
64
+ `recursive: false` to load only the top level.
65
+
66
+ ```ts
67
+ await client.load(`${here}features`, { recursive: false });
68
+ ```
69
+
70
+ > The loader imports compiled JavaScript. **Build your TypeScript first**, then run
71
+ > (and load) the emitted output — `npx tsc && node dist/index.js`. Loading a
72
+ > directory of `.ts` source files will not match the default extensions.
73
+
74
+ ## Standalone helpers
75
+
76
+ `client.load` is the method form of `loadInto`. Both helpers are exported if you
77
+ want to collect or register modules separately.
78
+
79
+ ```ts
80
+ collectModules(dir: string, options?: LoadOptions): Promise<Registerable[]>
81
+ loadInto(client: SpearClient, dir: string, options?: LoadOptions): Promise<number>
82
+ ```
83
+
84
+ - **`collectModules`** imports a directory and returns the registrable exports it
85
+ found, without touching any client. Use it to inspect, filter, or combine
86
+ modules before registering.
87
+ - **`loadInto`** calls `collectModules` and then `client.register(...)` for you,
88
+ returning the count.
89
+
90
+ ```ts
91
+ import { collectModules, loadInto } from "spearkit";
92
+
93
+ // Inspect before registering:
94
+ const items = await collectModules(`${here}commands`);
95
+ console.log(`Found ${items.length} modules`);
96
+ client.register(...items);
97
+
98
+ // Or do both in one step:
99
+ const count = await loadInto(client, `${here}events`);
100
+ ```
101
+
102
+ ## Example layout
103
+
104
+ The `examples/file-based-loading` project keeps each handler in its own file:
105
+
106
+ ```
107
+ file-based-loading/
108
+ index.ts # construct client, load each folder, start, deploy
109
+ commands/
110
+ ping.ts # export default command({ ... })
111
+ echo.ts # export default command({ ... })
112
+ events/
113
+ ready.ts # export default event("clientReady", ...)
114
+ components/
115
+ vote.ts # export const vote = button({ ... })
116
+ ```
117
+
118
+ A command file looks like this:
119
+
120
+ ```ts
121
+ // commands/echo.ts
122
+ import { command, option } from "spearkit";
123
+
124
+ export default command({
125
+ name: "echo",
126
+ description: "Repeat a message",
127
+ options: {
128
+ text: option.string({ description: "What to say", required: true }),
129
+ loud: option.boolean({ description: "Shout it" }),
130
+ },
131
+ // text: string, loud: boolean | undefined
132
+ run: (ctx) => ctx.reply(ctx.options.loud ? ctx.options.text.toUpperCase() : ctx.options.text),
133
+ });
134
+ ```
135
+
136
+ Build the project, then run the compiled `index.js`; `client.load` will import the
137
+ emitted `.js` files and register `ping`, `echo`, `ready` and `vote` for you.
138
+
139
+ ## See also
140
+
141
+ - [Client](./client.md) — `load`, `register`, and the registries the loader writes to.
142
+ - [Events](./events.md) — the `event()` helper that event modules export.
@@ -0,0 +1,195 @@
1
+ # Logging
2
+
3
+ spearkit ships a small, dependency-free structured logger. Every client owns one
4
+ at `client.logger`, and spearkit routes its own command, component, event, and
5
+ gateway errors through it. You can use the same logger for your code, or build a
6
+ standalone one.
7
+
8
+ ## A first logger
9
+
10
+ ```ts
11
+ import { Logger } from "spearkit";
12
+
13
+ const log = new Logger(); // level "info", logs to the console
14
+ log.info("bot starting");
15
+ log.error("connection lost", { error: new Error("ECONNRESET") });
16
+ ```
17
+
18
+ A logger is constructed from `LoggerOptions`:
19
+
20
+ ```ts
21
+ import { Logger } from "spearkit";
22
+
23
+ const log = new Logger({
24
+ level: "debug", // minimum severity to emit; default "info"
25
+ scope: "worker", // a prefix attached to every entry
26
+ // sink: consoleSink (the default)
27
+ });
28
+ ```
29
+
30
+ ## Levels
31
+
32
+ There are four levels, lowest to highest: `debug`, `info`, `warn`, `error`. The
33
+ logger emits an entry only if its level is at or above the configured threshold.
34
+ The default threshold is **`info`**, so `debug` entries are suppressed until you
35
+ lower it. A fifth threshold, `"silent"`, suppresses everything.
36
+
37
+ ```ts
38
+ import { Logger } from "spearkit";
39
+
40
+ const log = new Logger(); // threshold "info"
41
+ log.debug("only visible at debug"); // suppressed by default
42
+ log.info("visible");
43
+
44
+ log.setLevel("debug"); // now debug entries are emitted too
45
+ log.enabled("debug"); // true
46
+ log.setLevel("silent"); // suppress everything
47
+ ```
48
+
49
+ | Method | Level | Use for |
50
+ | ------ | ----- | ------- |
51
+ | `log.debug(msg, opts?)` | `debug` | Verbose diagnostics, off by default. |
52
+ | `log.info(msg, opts?)` | `info` | Normal operational messages. |
53
+ | `log.warn(msg, opts?)` | `warn` | Recoverable problems worth attention. |
54
+ | `log.error(msg, opts?)` | `error` | Failures; attach the cause via `{ error }`. |
55
+
56
+ `log.level` reads the current threshold, `log.setLevel(level)` changes it, and
57
+ `log.enabled(level)` reports whether an entry of that level would be emitted —
58
+ handy to guard expensive message construction.
59
+
60
+ ## Scopes and child loggers
61
+
62
+ `log.child("scope")` returns a child logger whose entries carry an extra scope
63
+ segment. A child **shares its parent's threshold and sink**, so changing the
64
+ level on any logger in the tree affects them all.
65
+
66
+ ```ts
67
+ import { Logger } from "spearkit";
68
+
69
+ const log = new Logger({ scope: "app" });
70
+ const db = log.child("db"); // scope "app:db"
71
+ const cache = db.child("cache"); // scope "app:db:cache"
72
+
73
+ db.info("connected");
74
+ log.setLevel("debug"); // affects log, db, and cache
75
+ cache.debug("warm"); // now emitted
76
+ ```
77
+
78
+ spearkit uses this internally: the client creates `commands`, `components`,
79
+ `events`, `scheduler`, `prefix`, and `usage` children off `client.logger`, so
80
+ every subsystem's output is scoped and a single `setLevel` controls them all.
81
+
82
+ ## Structured `data` and `error`
83
+
84
+ Both arguments live in the optional second parameter (`LogOptions`):
85
+
86
+ ```ts
87
+ import { Logger } from "spearkit";
88
+
89
+ const log = new Logger();
90
+
91
+ log.info("command finished", {
92
+ data: { command: "ping", ms: 12, cached: true },
93
+ });
94
+
95
+ try {
96
+ throw new Error("kaboom");
97
+ } catch (cause) {
98
+ log.error("handler failed", {
99
+ error: cause instanceof Error ? cause : new Error(String(cause)),
100
+ data: { command: "purge", guildId: "123" },
101
+ });
102
+ }
103
+ ```
104
+
105
+ `data` is a flat record of primitives (`string | number | boolean | bigint |
106
+ null | undefined`). `error` is an `Error`; the default sink renders its stack.
107
+
108
+ ### Coercing unknown throws
109
+
110
+ A `catch` binding is `unknown`. `toError(value)` turns any thrown value into an
111
+ `Error` so it fits `{ error }`:
112
+
113
+ ```ts
114
+ import { Logger, toError } from "spearkit";
115
+
116
+ const log = new Logger();
117
+ try {
118
+ JSON.parse("{");
119
+ } catch (cause) {
120
+ log.error("parse failed", { error: toError(cause) });
121
+ }
122
+ ```
123
+
124
+ ## Custom sinks
125
+
126
+ A sink is `(entry: LogEntry) => void`. The default is `consoleSink`, which writes
127
+ human-readable lines to the console (stderr for `warn`/`error`). Pass your own to
128
+ route entries anywhere — JSON lines, a file, an aggregator:
129
+
130
+ ```ts
131
+ import { Logger, type LogEntry } from "spearkit";
132
+
133
+ const log = new Logger({
134
+ level: "debug",
135
+ sink: (entry: LogEntry) => {
136
+ process.stdout.write(
137
+ JSON.stringify({
138
+ level: entry.level,
139
+ message: entry.message,
140
+ scope: entry.scope,
141
+ at: entry.timestamp.toISOString(),
142
+ data: entry.data,
143
+ error: entry.error?.message,
144
+ }) + "\n",
145
+ );
146
+ },
147
+ });
148
+
149
+ log.child("commands").info("dispatched", { data: { name: "ping" } });
150
+ ```
151
+
152
+ A `LogEntry` is the fully-resolved record handed to the sink:
153
+
154
+ | Field | Type | Notes |
155
+ | ----- | ---- | ----- |
156
+ | `level` | `LogLevel` | One of `debug`/`info`/`warn`/`error`. |
157
+ | `message` | `string` | The log message. |
158
+ | `scope` | `string \| undefined` | The accumulated scope, if any. |
159
+ | `timestamp` | `Date` | When the entry was created. |
160
+ | `error` | `Error \| undefined` | The attached error, if any. |
161
+ | `data` | `Record<string, LogValue> \| undefined` | The structured metadata, if any. |
162
+
163
+ ## Configuring via the client
164
+
165
+ Pass `logger` to `SpearClient`. Give it `LoggerOptions` to build one, or a
166
+ `Logger` instance you already have:
167
+
168
+ ```ts
169
+ import { SpearClient, Logger } from "spearkit";
170
+
171
+ // Build from options:
172
+ const a = new SpearClient({ logger: { level: "debug" } });
173
+
174
+ // Or reuse an instance (e.g. one shared with non-Discord code):
175
+ const shared = new Logger({ level: "info", scope: "svc" });
176
+ const b = new SpearClient({ logger: shared });
177
+
178
+ a.logger.info("ready");
179
+ ```
180
+
181
+ The client logs all command, component, and event handler errors plus gateway
182
+ errors through `client.logger`. Set `level: "debug"` to see dispatch traces from
183
+ every subsystem:
184
+
185
+ ```ts
186
+ import { SpearClient } from "spearkit";
187
+
188
+ const client = new SpearClient({ logger: { level: "debug" } });
189
+ // client.logger.child("commands"), ".child('events')", etc. all log at debug now
190
+ ```
191
+
192
+ ## See also
193
+
194
+ - [Client](./client.md) — the `logger` and other construction options.
195
+ - [Environment & dotenv](./env.md) — load configuration before you start.
@@ -0,0 +1,160 @@
1
+ # Migrating from discord.js
2
+
3
+ spearkit re-exports the entire discord.js surface, so adopting it is not a rewrite —
4
+ it is a one-line import change followed by *optional*, incremental cleanup. You can
5
+ move to spearkit today and start using its ergonomic helpers whenever you like.
6
+
7
+ ## The drop-in story
8
+
9
+ Change `from "discord.js"` to `from "spearkit"`. Nothing else has to change: every
10
+ discord.js export is available under the same name with the same types.
11
+
12
+ ```ts
13
+ // before
14
+ import { Client, EmbedBuilder, GatewayIntentBits } from "discord.js";
15
+
16
+ // after — identical behaviour
17
+ import { Client, EmbedBuilder, GatewayIntentBits } from "spearkit";
18
+ ```
19
+
20
+ The full classic surface is there — builders, enums, the REST client, route
21
+ helpers, the `Events` map, and so on:
22
+
23
+ ```ts
24
+ import {
25
+ ActionRowBuilder,
26
+ ButtonBuilder,
27
+ ButtonStyle,
28
+ Client,
29
+ EmbedBuilder,
30
+ Events,
31
+ GatewayIntentBits,
32
+ REST,
33
+ Routes,
34
+ SlashCommandBuilder,
35
+ } from "spearkit";
36
+
37
+ const client = new Client({ intents: [GatewayIntentBits.Guilds] });
38
+
39
+ const pingCommand = new SlashCommandBuilder()
40
+ .setName("ping")
41
+ .setDescription("Replies with an embed and a button");
42
+
43
+ client.once(Events.ClientReady, (c) => {
44
+ console.log(`Ready as ${c.user.tag}`);
45
+ });
46
+
47
+ client.on(Events.InteractionCreate, async (interaction) => {
48
+ if (!interaction.isChatInputCommand()) return;
49
+ if (interaction.commandName !== "ping") return;
50
+
51
+ const embed = new EmbedBuilder().setTitle("Pong!").setDescription(`Latency: ${client.ws.ping}ms`);
52
+ const buttons = new ActionRowBuilder<ButtonBuilder>().addComponents(
53
+ new ButtonBuilder().setCustomId("again").setLabel("Again").setStyle(ButtonStyle.Primary),
54
+ );
55
+ await interaction.reply({ embeds: [embed], components: [buttons] });
56
+ });
57
+
58
+ async function deploy(token: string, appId: string): Promise<void> {
59
+ const rest = new REST().setToken(token);
60
+ await rest.put(Routes.applicationCommands(appId), { body: [pingCommand.toJSON()] });
61
+ }
62
+ ```
63
+
64
+ This file is 100% classic discord.js — only the import source changed. It keeps
65
+ working exactly as before.
66
+
67
+ ## Incremental adoption
68
+
69
+ Once your imports point at spearkit, you can convert pieces one at a time. There is no
70
+ big-bang migration; old and new styles coexist.
71
+
72
+ 1. **Swap the client.** Replace `new Client(...)` with `new SpearClient(...)`. It
73
+ *is* a discord.js `Client` (it extends it), so your existing `client.on`,
74
+ `client.once`, `client.ws`, `client.rest` code is unchanged — but now it also
75
+ routes interactions to spearkit's registries.
76
+
77
+ ```ts
78
+ import { SpearClient, Intents } from "spearkit";
79
+
80
+ const client = new SpearClient({ intents: Intents.default });
81
+ ```
82
+
83
+ 2. **Move commands to `command()`.** Replace a hand-written `SlashCommandBuilder`
84
+ plus its branch of the `interactionCreate` switch with a single co-located
85
+ definition. Option values become fully typed.
86
+ 3. **Move events to `event()`.** Replace `client.on(Events.X, ...)` listeners with
87
+ `event("x", ...)` definitions and register them.
88
+ 4. **Move components to spearkit builders.** Replace manual `ButtonBuilder` +
89
+ custom-id parsing with `button()`, `stringSelect()`, `modal()`, etc. — spearkit
90
+ routes them by custom-id namespace and decodes `{param}`s for you.
91
+
92
+ Convert at whatever pace suits you; un-migrated handlers keep running through your
93
+ existing `interactionCreate` listener.
94
+
95
+ ## Before and after
96
+
97
+ The classic approach hand-routes every interaction through one big switch and
98
+ parses custom ids by hand:
99
+
100
+ ```ts
101
+ // discord.js: one listener routes everything by hand
102
+ import { Client, Events, GatewayIntentBits } from "discord.js";
103
+
104
+ const client = new Client({ intents: [GatewayIntentBits.Guilds] });
105
+
106
+ client.on(Events.InteractionCreate, async (interaction) => {
107
+ if (interaction.isChatInputCommand()) {
108
+ if (interaction.commandName === "greet") {
109
+ const who = interaction.options.getUser("who", true);
110
+ await interaction.reply(`Hello ${who}!`);
111
+ }
112
+ } else if (interaction.isButton()) {
113
+ const [name, choice] = interaction.customId.split(":"); // manual parsing
114
+ if (name === "vote") {
115
+ await interaction.update({ content: `You chose ${choice}` });
116
+ }
117
+ }
118
+ });
119
+ ```
120
+
121
+ spearkit co-locates each command and component with its handler, and routes
122
+ interactions for you — no switch, no manual id parsing:
123
+
124
+ ```ts
125
+ // spearkit: each handler owns its definition; routing is automatic
126
+ import { SpearClient, Intents, command, option, button, row } from "spearkit";
127
+
128
+ const client = new SpearClient({ intents: Intents.default });
129
+
130
+ const greet = command({
131
+ name: "greet",
132
+ description: "Greet someone",
133
+ options: { who: option.user({ description: "Who to greet", required: true }) },
134
+ run: (ctx) => ctx.reply(`Hello ${ctx.options.who}!`), // who: User
135
+ });
136
+
137
+ const vote = button({
138
+ id: "vote:{choice}", // {choice} is a typed param
139
+ label: "Yes",
140
+ style: "Success",
141
+ run: (ctx) => ctx.update(`You chose ${ctx.params.choice}`), // choice: string
142
+ });
143
+
144
+ client.register(greet, vote);
145
+ await client.start(process.env.DISCORD_TOKEN);
146
+ await client.deployCommands({ guildId: process.env.GUILD_ID });
147
+
148
+ // build() requires exactly the params the id pattern declares:
149
+ await channel.send({ content: "Vote:", components: [row(vote.build({ choice: "yes" }))] });
150
+ ```
151
+
152
+ The option value (`who`) and the custom-id param (`choice`) are inferred from the
153
+ definitions — no casts, no `getUser`/`split` boilerplate, and no `interactionCreate`
154
+ switch to maintain.
155
+
156
+ ## See also
157
+
158
+ - [Getting started](./getting-started.md) — install spearkit and build a first bot.
159
+ - [Commands](./commands.md) — define slash commands with typed options.
160
+ - [Components](./components.md) — buttons, selects, modals and custom-id routing.