spearkit 0.2.0 → 0.3.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/dist/index.js CHANGED
@@ -1,13 +1,1237 @@
1
- import { GatewayIntentBits, MessageFlags, ApplicationCommandOptionType, REST, Routes, ButtonBuilder, ButtonStyle, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, ModalBuilder, ActionRowBuilder, Client, InteractionContextType, ApplicationCommandType, PermissionsBitField, TextInputStyle, TextInputBuilder } from 'discord.js';
1
+ import { ButtonStyle, GatewayIntentBits, EmbedBuilder, MessageFlags, ApplicationCommandType, ComponentType, ActionRowBuilder, ButtonBuilder, ApplicationCommandOptionType, REST, Routes, StringSelectMenuBuilder, UserSelectMenuBuilder, RoleSelectMenuBuilder, ChannelSelectMenuBuilder, MentionableSelectMenuBuilder, ModalBuilder, Client, PermissionsBitField, InteractionContextType, TextInputStyle, TextInputBuilder } from 'discord.js';
2
2
  export * from 'discord.js';
3
- import { readFileSync } from 'fs';
4
- import { join, dirname, extname } from 'path';
5
- import { mkdir, appendFile, readFile, readdir } from 'fs/promises';
3
+ import { readFile, readFileSync } from 'fs';
4
+ import { promisify } from 'util';
5
+ import { mkdir, appendFile, readFile as readFile$1, readdir } from 'fs/promises';
6
+ import { dirname, join, extname } from 'path';
6
7
  import { pathToFileURL } from 'url';
7
8
 
8
9
  // src/index.ts
10
+ var DEFAULT_EMBED_COLORS = {
11
+ error: 15747655,
12
+ success: 4437378,
13
+ info: 3447003,
14
+ warn: 16361509
15
+ };
16
+ var DEFAULT_EMBED_ICONS = {
17
+ error: "\u26D4",
18
+ success: "\u2705",
19
+ info: "\u2139\uFE0F",
20
+ warn: "\u26A0\uFE0F"
21
+ };
22
+ var Embeds = class {
23
+ /** The resolved colors for every preset. */
24
+ colors;
25
+ /** The resolved icons for every preset. */
26
+ icons;
27
+ constructor(options = {}) {
28
+ this.colors = { ...DEFAULT_EMBED_COLORS, ...options.colors };
29
+ this.icons = { ...DEFAULT_EMBED_ICONS, ...options.icons };
30
+ }
31
+ /** Red preset — something went wrong. */
32
+ error(input) {
33
+ return this.build("error", input);
34
+ }
35
+ /** Green preset — something succeeded. */
36
+ success(input) {
37
+ return this.build("success", input);
38
+ }
39
+ /** Blue preset — neutral information. */
40
+ info(input) {
41
+ return this.build("info", input);
42
+ }
43
+ /** Yellow preset — caution. */
44
+ warn(input) {
45
+ return this.build("warn", input);
46
+ }
47
+ /** Build an embed at a chosen level. */
48
+ build(level, input) {
49
+ const builder = new EmbedBuilder().setColor(this.colors[level]);
50
+ const icon = this.icons[level];
51
+ const prefix = icon.length > 0 ? `${icon} ` : "";
52
+ if (typeof input === "string") {
53
+ builder.setDescription(`${prefix}${input}`);
54
+ return builder;
55
+ }
56
+ if (input.title !== void 0) builder.setTitle(input.title);
57
+ if (input.description !== void 0) {
58
+ builder.setDescription(`${prefix}${input.description}`);
59
+ }
60
+ if (input.fields !== void 0) builder.addFields(...input.fields);
61
+ if (input.footer !== void 0) builder.setFooter(input.footer);
62
+ if (input.author !== void 0) builder.setAuthor(input.author);
63
+ if (input.url !== void 0) builder.setURL(input.url);
64
+ if (input.timestamp !== void 0) {
65
+ builder.setTimestamp(
66
+ input.timestamp instanceof Date ? input.timestamp : new Date(input.timestamp)
67
+ );
68
+ }
69
+ if (input.thumbnail !== void 0) builder.setThumbnail(input.thumbnail.url);
70
+ if (input.image !== void 0) builder.setImage(input.image.url);
71
+ return builder;
72
+ }
73
+ };
74
+ var defaultEmbeds = new Embeds();
75
+
76
+ // src/lock.ts
77
+ var KeyedLock = class {
78
+ entries = /* @__PURE__ */ new Map();
79
+ defaultTtl;
80
+ sweepTimer;
81
+ constructor(options = {}) {
82
+ this.defaultTtl = options.ttl ?? 6e4;
83
+ const sweep = options.sweep ?? 15e3;
84
+ if (sweep > 0) {
85
+ this.sweepTimer = setInterval(() => this.sweep(), sweep);
86
+ if (typeof this.sweepTimer.unref === "function") this.sweepTimer.unref();
87
+ }
88
+ }
89
+ /** Try to acquire `key`. Returns a release function, or `null` if already held. */
90
+ tryAcquire(key, ttl = this.defaultTtl) {
91
+ const existing = this.entries.get(key);
92
+ if (existing !== void 0 && Date.now() - existing.createdAt < existing.ttl) {
93
+ return null;
94
+ }
95
+ this.entries.set(key, { createdAt: Date.now(), ttl });
96
+ let released = false;
97
+ return () => {
98
+ if (released) return;
99
+ released = true;
100
+ this.entries.delete(key);
101
+ };
102
+ }
103
+ /** Whether `key` is currently held and not expired. */
104
+ isHeld(key) {
105
+ const entry = this.entries.get(key);
106
+ return entry !== void 0 && Date.now() - entry.createdAt < entry.ttl;
107
+ }
108
+ /**
109
+ * Run `fn` while holding `key`. If the key is already held, calls `onBusy`
110
+ * (or returns `undefined`) without ever calling `fn`. Always releases on
111
+ * return or throw.
112
+ */
113
+ async run(key, fn, options = {}) {
114
+ const release = this.tryAcquire(key, options.ttl ?? this.defaultTtl);
115
+ if (release === null) {
116
+ return options.onBusy !== void 0 ? await options.onBusy() : void 0;
117
+ }
118
+ try {
119
+ return await fn();
120
+ } finally {
121
+ release();
122
+ }
123
+ }
124
+ /** Number of currently-tracked leases (including expired-but-unswept). */
125
+ get size() {
126
+ return this.entries.size;
127
+ }
128
+ /** Drop all known leases and stop the sweep timer. */
129
+ dispose() {
130
+ this.entries.clear();
131
+ if (this.sweepTimer !== void 0) clearInterval(this.sweepTimer);
132
+ }
133
+ /** Manually remove a single key without running anything. */
134
+ forget(key) {
135
+ return this.entries.delete(key);
136
+ }
137
+ sweep() {
138
+ const now = Date.now();
139
+ for (const [key, entry] of this.entries.entries()) {
140
+ if (now - entry.createdAt > entry.ttl) this.entries.delete(key);
141
+ }
142
+ }
143
+ };
144
+
145
+ // src/safe-fetch.ts
146
+ var DEFAULT_TIMEOUT_MS = 5e3;
147
+ async function withTimeout(promise, ms) {
148
+ let timer;
149
+ try {
150
+ const timeout = new Promise((resolve) => {
151
+ timer = setTimeout(() => resolve(null), ms);
152
+ });
153
+ return await Promise.race([promise.catch(() => null), timeout]);
154
+ } finally {
155
+ if (timer !== void 0) clearTimeout(timer);
156
+ }
157
+ }
158
+ async function fetchMember(guild, userId, options = {}) {
159
+ if (guild == null || userId == null || userId.length === 0) return null;
160
+ if (options.cache !== false && options.force !== true) {
161
+ const cached = guild.members.cache.get(userId);
162
+ if (cached !== void 0) return cached;
163
+ }
164
+ return withTimeout(
165
+ guild.members.fetch({ user: userId, force: options.force ?? false }),
166
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
167
+ );
168
+ }
169
+ async function fetchChannel(client, channelId, options = {}) {
170
+ if (client == null || channelId == null || channelId.length === 0) return null;
171
+ if (options.cache !== false && options.force !== true) {
172
+ const cached = client.channels.cache.get(channelId);
173
+ if (cached !== void 0) return cached;
174
+ }
175
+ return withTimeout(
176
+ client.channels.fetch(channelId, { force: options.force ?? false }),
177
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
178
+ );
179
+ }
180
+ async function fetchMessage(messages, messageId, options = {}) {
181
+ if (messages == null || messageId == null || messageId.length === 0) return null;
182
+ if (options.cache !== false && options.force !== true) {
183
+ const cached = messages.cache.get(messageId);
184
+ if (cached !== void 0) return cached;
185
+ }
186
+ return withTimeout(
187
+ messages.fetch({ message: messageId, force: options.force ?? false }),
188
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
189
+ );
190
+ }
191
+ async function fetchUser(client, userId, options = {}) {
192
+ if (client == null || userId == null || userId.length === 0) return null;
193
+ if (options.cache !== false && options.force !== true) {
194
+ const cached = client.users.cache.get(userId);
195
+ if (cached !== void 0) return cached;
196
+ }
197
+ return withTimeout(
198
+ client.users.fetch(userId, { force: options.force ?? false }),
199
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
200
+ );
201
+ }
202
+ async function fetchGuild(client, guildId, options = {}) {
203
+ if (client == null || guildId == null || guildId.length === 0) return null;
204
+ if (options.cache !== false && options.force !== true) {
205
+ const cached = client.guilds.cache.get(guildId);
206
+ if (cached !== void 0) return cached;
207
+ }
208
+ return withTimeout(
209
+ client.guilds.fetch({ guild: guildId, force: options.force ?? false }),
210
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
211
+ );
212
+ }
213
+ async function fetchRole(guild, roleId, options = {}) {
214
+ if (guild == null || roleId == null || roleId.length === 0) return null;
215
+ if (options.cache !== false && options.force !== true) {
216
+ const cached = guild.roles.cache.get(roleId);
217
+ if (cached !== void 0) return cached;
218
+ }
219
+ return withTimeout(
220
+ guild.roles.fetch(roleId, { force: options.force ?? false }),
221
+ options.timeoutMs ?? DEFAULT_TIMEOUT_MS
222
+ );
223
+ }
224
+ async function safeTry(op) {
225
+ try {
226
+ return await op();
227
+ } catch {
228
+ return null;
229
+ }
230
+ }
231
+ async function withSafeTimeout(promise, timeoutMs) {
232
+ return withTimeout(promise, timeoutMs);
233
+ }
234
+ var safeFetch = {
235
+ member: fetchMember,
236
+ channel: fetchChannel,
237
+ message: fetchMessage,
238
+ user: fetchUser,
239
+ guild: fetchGuild,
240
+ role: fetchRole,
241
+ try: safeTry
242
+ };
243
+
244
+ // src/format.ts
245
+ var EN = {
246
+ week: ["week", "weeks"],
247
+ day: ["day", "days"],
248
+ hour: ["hour", "hours"],
249
+ minute: ["minute", "minutes"],
250
+ second: ["second", "seconds"],
251
+ separator: " ",
252
+ zero: "0 seconds"
253
+ };
254
+ var TR = {
255
+ week: ["hafta", "hafta"],
256
+ day: ["g\xFCn", "g\xFCn"],
257
+ hour: ["saat", "saat"],
258
+ minute: ["dakika", "dakika"],
259
+ second: ["saniye", "saniye"],
260
+ separator: " ",
261
+ zero: "0 saniye"
262
+ };
263
+ var LABELS = {
264
+ en: EN,
265
+ "en-US": EN,
266
+ "en-GB": EN,
267
+ tr: TR,
268
+ "tr-TR": TR
269
+ };
270
+ var UNIT_MS = {
271
+ week: 7 * 864e5,
272
+ day: 864e5,
273
+ hour: 36e5,
274
+ minute: 6e4,
275
+ second: 1e3
276
+ };
277
+ var UNIT_ORDER = ["week", "day", "hour", "minute", "second"];
278
+ function resolveLabels(locale) {
279
+ if (typeof locale === "object" && locale !== null) return locale;
280
+ if (locale === void 0) return EN;
281
+ return LABELS[locale] ?? LABELS[locale.split("-")[0] ?? ""] ?? EN;
282
+ }
283
+ function formatDuration(ms, options = {}) {
284
+ const labels = resolveLabels(options.locale);
285
+ if (!Number.isFinite(ms) || ms <= 0) return labels.zero;
286
+ const limit = options.largest ?? 2;
287
+ const units = options.units ?? UNIT_ORDER;
288
+ const parts = [];
289
+ let remaining = Math.floor(ms);
290
+ for (const unit of units) {
291
+ if (parts.length >= limit) break;
292
+ const size = UNIT_MS[unit];
293
+ if (remaining < size) continue;
294
+ const value = Math.floor(remaining / size);
295
+ remaining -= value * size;
296
+ const word = value === 1 ? labels[unit][0] : labels[unit][1];
297
+ parts.push(`${value} ${word}`);
298
+ }
299
+ return parts.length > 0 ? parts.join(labels.separator) : labels.zero;
300
+ }
301
+ var DURATION_PATTERN = /(\d+(?:\.\d+)?)\s*(milliseconds|millisecond|seconds|minutes|saniye|dakika|minute|second|weeks|hours|hafta|saat|week|hour|days|day|gün|gun|min|sec|ms|wk|hr|dk|m|s|h|d|w)/gi;
302
+ var SHORT_TO_MS = {
303
+ ms: 1,
304
+ millisecond: 1,
305
+ milliseconds: 1,
306
+ s: 1e3,
307
+ sec: 1e3,
308
+ second: 1e3,
309
+ seconds: 1e3,
310
+ saniye: 1e3,
311
+ m: 6e4,
312
+ min: 6e4,
313
+ minute: 6e4,
314
+ minutes: 6e4,
315
+ dakika: 6e4,
316
+ dk: 6e4,
317
+ h: 36e5,
318
+ hr: 36e5,
319
+ hour: 36e5,
320
+ hours: 36e5,
321
+ saat: 36e5,
322
+ d: 864e5,
323
+ day: 864e5,
324
+ days: 864e5,
325
+ g\u00FCn: 864e5,
326
+ gun: 864e5,
327
+ w: 6048e5,
328
+ wk: 6048e5,
329
+ week: 6048e5,
330
+ weeks: 6048e5,
331
+ hafta: 6048e5
332
+ };
333
+ function parseDuration(input) {
334
+ const trimmed = input.trim().toLowerCase();
335
+ if (trimmed.length === 0) return null;
336
+ DURATION_PATTERN.lastIndex = 0;
337
+ let total = 0;
338
+ let matched = false;
339
+ for (; ; ) {
340
+ const match = DURATION_PATTERN.exec(trimmed);
341
+ if (match === null) break;
342
+ matched = true;
343
+ const value = Number(match[1]);
344
+ const unit = match[2] ?? "";
345
+ const ms = SHORT_TO_MS[unit];
346
+ if (ms !== void 0 && Number.isFinite(value)) total += value * ms;
347
+ }
348
+ return matched ? total : null;
349
+ }
350
+ function toEpochSeconds(date) {
351
+ return Math.floor((date instanceof Date ? date.getTime() : date) / 1e3);
352
+ }
353
+ function discordTimestamp(date, style = "f") {
354
+ return `<t:${toEpochSeconds(date)}:${style}>`;
355
+ }
356
+ function relativeTimestamp(date) {
357
+ return discordTimestamp(date, "R");
358
+ }
359
+
360
+ // src/cache.ts
361
+ var MemoryCache = class {
362
+ store = /* @__PURE__ */ new Map();
363
+ /** Total number of stored (possibly expired) entries — primarily for tests. */
364
+ get size() {
365
+ return this.store.size;
366
+ }
367
+ async get(key) {
368
+ const entry = this.store.get(key);
369
+ if (entry === void 0) return void 0;
370
+ if (entry.expiresAt !== void 0 && entry.expiresAt <= Date.now()) {
371
+ this.store.delete(key);
372
+ return void 0;
373
+ }
374
+ return entry.value;
375
+ }
376
+ async set(key, value, options) {
377
+ const ttl = options?.ttl;
378
+ this.store.set(key, {
379
+ value,
380
+ expiresAt: ttl !== void 0 && ttl > 0 ? Date.now() + ttl : void 0
381
+ });
382
+ }
383
+ async delete(key) {
384
+ return this.store.delete(key);
385
+ }
386
+ async has(key) {
387
+ return await this.get(key) !== void 0;
388
+ }
389
+ async increment(key, delta = 1, options) {
390
+ const current = await this.get(key) ?? 0;
391
+ const next = current + delta;
392
+ const existing = this.store.get(key);
393
+ const ttl = options?.ttl;
394
+ if (ttl !== void 0 && ttl > 0) {
395
+ await this.set(key, next, { ttl });
396
+ } else if (existing?.expiresAt !== void 0) {
397
+ this.store.set(key, { value: next, expiresAt: existing.expiresAt });
398
+ } else {
399
+ await this.set(key, next);
400
+ }
401
+ return next;
402
+ }
403
+ async rateLimit(key, options) {
404
+ const now = Date.now();
405
+ const bucketKey = `__rl__:${key}`;
406
+ const entry = this.store.get(bucketKey);
407
+ const expired = entry === void 0 || entry.expiresAt === void 0 || entry.expiresAt <= now;
408
+ if (expired) {
409
+ const resetAt2 = now + options.windowMs;
410
+ this.store.set(bucketKey, { value: 1, expiresAt: resetAt2 });
411
+ return { allowed: true, remaining: Math.max(0, options.limit - 1), resetAt: resetAt2 };
412
+ }
413
+ const count = entry.value;
414
+ const resetAt = entry.expiresAt ?? now;
415
+ if (count >= options.limit) {
416
+ return { allowed: false, remaining: 0, resetAt };
417
+ }
418
+ this.store.set(bucketKey, { value: count + 1, expiresAt: resetAt });
419
+ return { allowed: true, remaining: Math.max(0, options.limit - count - 1), resetAt };
420
+ }
421
+ async clear() {
422
+ this.store.clear();
423
+ }
424
+ };
425
+ function createCache() {
426
+ return new MemoryCache();
427
+ }
428
+ var readFileAsync = promisify(readFile);
429
+ function loadConfig(options) {
430
+ const text = readFileSync(options.file, options.encoding ?? "utf8");
431
+ const parser = options.parser ?? JSON.parse;
432
+ const parsed = parser(text);
433
+ return options.schema !== void 0 ? options.schema(parsed) : parsed;
434
+ }
435
+ async function loadConfigAsync(options) {
436
+ const text = await readFileAsync(options.file, options.encoding ?? "utf8");
437
+ const parser = options.parser ?? JSON.parse;
438
+ const parsed = parser(text);
439
+ return options.schema !== void 0 ? options.schema(parsed) : parsed;
440
+ }
441
+ function lookup(table, resourceName = "key") {
442
+ return (key) => {
443
+ const value = table[key];
444
+ if (value === void 0) {
445
+ throw new Error(`spearkit: ${resourceName} "${String(key)}" not found in config`);
446
+ }
447
+ return value;
448
+ };
449
+ }
450
+ function lookupOptional(table) {
451
+ return (key) => table[key];
452
+ }
453
+ function denied(reason) {
454
+ return { allowed: false, reason };
455
+ }
456
+ async function runGuards(ctx, guards) {
457
+ if (guards === void 0 || guards.length === 0) return { allowed: true };
458
+ for (const guard2 of guards) {
459
+ const result = await guard2(ctx);
460
+ if (result === true) continue;
461
+ if (result === false) return { allowed: false, reason: void 0 };
462
+ if (result.allowed === false) return { allowed: false, reason: result.reason };
463
+ }
464
+ return { allowed: true };
465
+ }
466
+ function guildOnly(reason = "This can only be used in a server.") {
467
+ return (ctx) => ctx.guildId !== null ? true : denied(reason);
468
+ }
469
+ function dmOnly(reason = "This can only be used in DMs.") {
470
+ return (ctx) => ctx.guildId === null ? true : denied(reason);
471
+ }
472
+ function memberRoleIds(member) {
473
+ if (member === null) return [];
474
+ const roles = member.roles;
475
+ if (Array.isArray(roles)) return roles;
476
+ return [...roles.cache.keys()];
477
+ }
478
+ function memberPermissionsBitField(member) {
479
+ if (member === null) return null;
480
+ const perms = member.permissions;
481
+ if (perms instanceof PermissionsBitField) return perms;
482
+ if (typeof perms === "string") return new PermissionsBitField(BigInt(perms));
483
+ return null;
484
+ }
485
+ function requireAnyRole(roleIds, reason = "You don't have permission to use this.") {
486
+ const set = new Set(roleIds);
487
+ return (ctx) => {
488
+ const ids = memberRoleIds(ctx.member);
489
+ return ids.some((id) => set.has(id)) ? true : denied(reason);
490
+ };
491
+ }
492
+ function requireAllRoles(roleIds, reason = "You're missing one of the required roles.") {
493
+ return (ctx) => {
494
+ const ids = new Set(memberRoleIds(ctx.member));
495
+ return roleIds.every((id) => ids.has(id)) ? true : denied(reason);
496
+ };
497
+ }
498
+ function requireOwner(ownerIds, reason = "This is owner-only.") {
499
+ const set = new Set(ownerIds);
500
+ return (ctx) => set.has(ctx.user.id) ? true : denied(reason);
501
+ }
502
+ function requireUserPermissions(permission, reason = "You don't have permission to use this.") {
503
+ return (ctx) => {
504
+ const bits = memberPermissionsBitField(ctx.member);
505
+ if (bits === null) return denied(reason);
506
+ return bits.has(permission) ? true : denied(reason);
507
+ };
508
+ }
509
+ function requireBotPermissions(permission, reason = "I don't have permission to do that here.") {
510
+ return async (ctx) => {
511
+ const guild = ctx.guild;
512
+ if (guild === null) return denied(reason);
513
+ const me = guild.members.me ?? await guild.members.fetchMe().catch(() => null);
514
+ if (me === null) return denied(reason);
515
+ return me.permissions.has(permission) ? true : denied(reason);
516
+ };
517
+ }
518
+ function guard(predicate) {
519
+ return predicate;
520
+ }
521
+ function withEphemeralFlag(flags) {
522
+ if (flags == null) return MessageFlags.Ephemeral;
523
+ if (typeof flags === "number" || typeof flags === "bigint") {
524
+ return Number(flags) | MessageFlags.Ephemeral;
525
+ }
526
+ if (Array.isArray(flags)) return [...flags, MessageFlags.Ephemeral];
527
+ return [flags, MessageFlags.Ephemeral];
528
+ }
529
+ function normalizeReply(input) {
530
+ if (typeof input === "string") return { content: input };
531
+ const { ephemeral, ...rest } = input;
532
+ if (ephemeral) return { ...rest, flags: withEphemeralFlag(rest.flags) };
533
+ return rest;
534
+ }
535
+ function normalizeEdit(input) {
536
+ if (typeof input === "string") return { content: input };
537
+ const { ephemeral: _ephemeral, flags: _flags, ...rest } = input;
538
+ return rest;
539
+ }
540
+ function asEphemeral(input) {
541
+ if (typeof input === "string") return { content: input, ephemeral: true };
542
+ return { ...input, ephemeral: true };
543
+ }
544
+ var BaseContext = class {
545
+ constructor(interaction) {
546
+ this.interaction = interaction;
547
+ }
548
+ interaction;
549
+ get client() {
550
+ return this.interaction.client;
551
+ }
552
+ get user() {
553
+ return this.interaction.user;
554
+ }
555
+ get member() {
556
+ return this.interaction.member;
557
+ }
558
+ get guild() {
559
+ return this.interaction.guild;
560
+ }
561
+ get guildId() {
562
+ return this.interaction.guildId;
563
+ }
564
+ get channel() {
565
+ return this.interaction.channel;
566
+ }
567
+ get channelId() {
568
+ return this.interaction.channelId;
569
+ }
570
+ get locale() {
571
+ return this.interaction.locale;
572
+ }
573
+ /** Whether the interaction is already deferred. */
574
+ get deferred() {
575
+ return this.interaction.deferred;
576
+ }
577
+ /** Whether the interaction already received an initial response. */
578
+ get replied() {
579
+ return this.interaction.replied;
580
+ }
581
+ /** Send the initial response to the interaction. */
582
+ reply(input) {
583
+ return this.interaction.reply(normalizeReply(input));
584
+ }
585
+ /** Reply, but always hidden to everyone except the invoking user. */
586
+ replyEphemeral(input) {
587
+ return this.reply(asEphemeral(input));
588
+ }
589
+ /** Acknowledge now and respond later via {@link editReply}. */
590
+ defer(options = {}) {
591
+ return this.interaction.deferReply(
592
+ options.ephemeral ? { flags: MessageFlags.Ephemeral } : {}
593
+ );
594
+ }
595
+ /** Edit the original (or deferred) response. */
596
+ editReply(input) {
597
+ return this.interaction.editReply(normalizeEdit(input));
598
+ }
599
+ /** Add an additional message after the initial response. */
600
+ followUp(input) {
601
+ return this.interaction.followUp(normalizeReply(input));
602
+ }
603
+ /**
604
+ * State-aware send: replies, edits a deferred response, or follows up —
605
+ * whichever is valid given the current interaction state. The single method
606
+ * most handlers ever need.
607
+ */
608
+ async send(input) {
609
+ if (this.interaction.deferred) {
610
+ await this.editReply(input);
611
+ } else if (this.interaction.replied) {
612
+ await this.followUp(input);
613
+ } else {
614
+ await this.reply(input);
615
+ }
616
+ }
617
+ /** Get the configured {@link Embeds} factory — `client.embeds` or the default. */
618
+ getEmbeds() {
619
+ return this.interaction.client.embeds ?? defaultEmbeds;
620
+ }
621
+ /** State-aware send of a red error embed. Defaults to ephemeral. */
622
+ error(input, options = {}) {
623
+ return this.sendPreset("error", input, { ephemeral: options.ephemeral ?? true });
624
+ }
625
+ /** State-aware send of a green success embed. */
626
+ success(input, options = {}) {
627
+ return this.sendPreset("success", input, options);
628
+ }
629
+ /** State-aware send of a blue info embed. */
630
+ info(input, options = {}) {
631
+ return this.sendPreset("info", input, options);
632
+ }
633
+ /** State-aware send of a yellow warn embed. */
634
+ warn(input, options = {}) {
635
+ return this.sendPreset("warn", input, options);
636
+ }
637
+ /** Initial-reply variant of {@link error} (always `reply`, never `editReply`/`followUp`). */
638
+ replyError(input, options = {}) {
639
+ return this.replyPreset("error", input, { ephemeral: options.ephemeral ?? true });
640
+ }
641
+ /** Initial-reply variant of {@link success}. */
642
+ replySuccess(input, options = {}) {
643
+ return this.replyPreset("success", input, options);
644
+ }
645
+ /** Initial-reply variant of {@link info}. */
646
+ replyInfo(input, options = {}) {
647
+ return this.replyPreset("info", input, options);
648
+ }
649
+ /** Initial-reply variant of {@link warn}. */
650
+ replyWarn(input, options = {}) {
651
+ return this.replyPreset("warn", input, options);
652
+ }
653
+ sendPreset(level, input, options) {
654
+ const embed = this.getEmbeds().build(level, input);
655
+ return this.send({ embeds: [embed], ephemeral: options.ephemeral });
656
+ }
657
+ replyPreset(level, input, options) {
658
+ const embed = this.getEmbeds().build(level, input);
659
+ return this.reply({ embeds: [embed], ephemeral: options.ephemeral });
660
+ }
661
+ };
662
+
663
+ // src/cooldown.ts
664
+ function normalizeCooldown(input) {
665
+ return typeof input === "number" ? { duration: input } : input;
666
+ }
667
+ function scopeKey(scope, actor) {
668
+ switch (scope) {
669
+ case "guild":
670
+ return `g:${actor.guildId ?? "dm"}`;
671
+ case "channel":
672
+ return `c:${actor.channelId ?? "dm"}`;
673
+ case "global":
674
+ return "global";
675
+ case "user":
676
+ return `u:${actor.userId}`;
677
+ }
678
+ }
679
+ function effectiveDuration(config, actor) {
680
+ if (config.exempt?.users?.includes(actor.userId) === true) return null;
681
+ if (config.exempt?.roles?.some((roleId) => actor.roleIds.includes(roleId)) === true) return null;
682
+ const userOverride = config.overrides?.users?.[actor.userId];
683
+ if (userOverride !== void 0) return userOverride;
684
+ const roleOverrides = config.overrides?.roles;
685
+ if (roleOverrides !== void 0) {
686
+ let best;
687
+ for (const roleId of actor.roleIds) {
688
+ const candidate = roleOverrides[roleId];
689
+ if (candidate !== void 0) best = best === void 0 ? candidate : Math.min(best, candidate);
690
+ }
691
+ if (best !== void 0) return best;
692
+ }
693
+ return config.duration;
694
+ }
695
+ function keyFor(bucket, config, actor) {
696
+ return `${bucket}|${scopeKey(config.scope ?? "user", actor)}`;
697
+ }
698
+ var CooldownManager = class {
699
+ hits = /* @__PURE__ */ new Map();
700
+ /** Number of tracked buckets. */
701
+ get size() {
702
+ return this.hits.size;
703
+ }
704
+ /**
705
+ * Check whether `actor` may use `bucket`, recording the use when allowed.
706
+ * Exempt actors and non-positive durations are always allowed (no record).
707
+ */
708
+ consume(bucket, input, actor, now = Date.now()) {
709
+ const config = normalizeCooldown(input);
710
+ const duration = effectiveDuration(config, actor);
711
+ if (duration === null || duration <= 0) return { allowed: true };
712
+ const key = keyFor(bucket, config, actor);
713
+ const last = this.hits.get(key);
714
+ if (last !== void 0 && now - last < duration) {
715
+ return { allowed: false, remaining: duration - (now - last) };
716
+ }
717
+ this.hits.set(key, now);
718
+ return { allowed: true };
719
+ }
720
+ /** Like {@link consume} but never records — a read-only check. */
721
+ peek(bucket, input, actor, now = Date.now()) {
722
+ const config = normalizeCooldown(input);
723
+ const duration = effectiveDuration(config, actor);
724
+ if (duration === null || duration <= 0) return { allowed: true };
725
+ const last = this.hits.get(keyFor(bucket, config, actor));
726
+ if (last !== void 0 && now - last < duration) {
727
+ return { allowed: false, remaining: duration - (now - last) };
728
+ }
729
+ return { allowed: true };
730
+ }
731
+ /** Clear a single actor's cooldown for a bucket. Returns whether one existed. */
732
+ reset(bucket, actor, scope = "user") {
733
+ return this.hits.delete(`${bucket}|${scopeKey(scope, actor)}`);
734
+ }
735
+ /** Drop every tracked cooldown. */
736
+ clear() {
737
+ this.hits.clear();
738
+ }
739
+ };
740
+ function formatCooldownMessage(config, remainingMs) {
741
+ if (typeof config.message === "function") return config.message(remainingMs);
742
+ if (typeof config.message === "string") return config.message;
743
+ const seconds = Math.max(1, Math.ceil(remainingMs / 1e3));
744
+ return `You're on cooldown \u2014 try again in ${seconds}s.`;
745
+ }
746
+
747
+ // src/context-menus.ts
748
+ var UserContextMenuContext = class extends BaseContext {
749
+ /** The user the menu was invoked on. */
750
+ get targetUser() {
751
+ return this.interaction.targetUser;
752
+ }
753
+ /** The member version of the target, if available. */
754
+ get targetMember() {
755
+ return this.interaction.targetMember;
756
+ }
757
+ };
758
+ var MessageContextMenuContext = class extends BaseContext {
759
+ /** The message the menu was invoked on. */
760
+ get targetMessage() {
761
+ return this.interaction.targetMessage;
762
+ }
763
+ };
764
+ function baseJSON(meta, type) {
765
+ return {
766
+ type,
767
+ name: meta.name,
768
+ name_localizations: meta.nameLocalizations,
769
+ nsfw: meta.nsfw,
770
+ default_member_permissions: meta.defaultMemberPermissions == null ? meta.defaultMemberPermissions : new PermissionsBitField(meta.defaultMemberPermissions).bitfield.toString(),
771
+ contexts: meta.guildOnly ? [InteractionContextType.Guild] : void 0
772
+ };
773
+ }
774
+ function userCommand(config) {
775
+ const cooldown = config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0;
776
+ return {
777
+ kind: "userMenu",
778
+ name: config.name,
779
+ cooldown,
780
+ guards: config.guards,
781
+ toJSON: () => baseJSON(config, ApplicationCommandType.User),
782
+ execute: async (interaction) => {
783
+ await config.run(new UserContextMenuContext(interaction));
784
+ }
785
+ };
786
+ }
787
+ function messageCommand(config) {
788
+ const cooldown = config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0;
789
+ return {
790
+ kind: "messageMenu",
791
+ name: config.name,
792
+ cooldown,
793
+ guards: config.guards,
794
+ toJSON: () => baseJSON(config, ApplicationCommandType.Message),
795
+ execute: async (interaction) => {
796
+ await config.run(new MessageContextMenuContext(interaction));
797
+ }
798
+ };
799
+ }
800
+ var ContextMenuRegistry = class {
801
+ users = /* @__PURE__ */ new Map();
802
+ messages = /* @__PURE__ */ new Map();
803
+ logger;
804
+ cooldowns;
805
+ defaultCooldown;
806
+ defaultGuards = [];
807
+ onUsage;
808
+ /** Register one or more context-menu commands. */
809
+ add(...commands) {
810
+ for (const command2 of commands) {
811
+ if (command2.kind === "userMenu") this.users.set(command2.name, command2);
812
+ else this.messages.set(command2.name, command2);
813
+ }
814
+ return this;
815
+ }
816
+ /** Total number of registered context-menu commands. */
817
+ get size() {
818
+ return this.users.size + this.messages.size;
819
+ }
820
+ /** Every registered command, both kinds. */
821
+ all() {
822
+ return [...this.users.values(), ...this.messages.values()];
823
+ }
824
+ /** Serialise every command for the REST `applicationCommands` PUT body. */
825
+ toJSON() {
826
+ return this.all().map((c) => c.toJSON());
827
+ }
828
+ setLogger(logger) {
829
+ this.logger = logger;
830
+ return this;
831
+ }
832
+ setCooldowns(manager, defaultCooldown) {
833
+ this.cooldowns = manager;
834
+ this.defaultCooldown = defaultCooldown;
835
+ return this;
836
+ }
837
+ setDefaultGuards(guards) {
838
+ this.defaultGuards = guards;
839
+ return this;
840
+ }
841
+ setUsageHook(hook) {
842
+ this.onUsage = hook;
843
+ return this;
844
+ }
845
+ /** Dispatch a user-target interaction. */
846
+ async handleUser(interaction) {
847
+ const command2 = this.users.get(interaction.commandName);
848
+ if (command2 === void 0) return;
849
+ await this.dispatch(command2, interaction);
850
+ }
851
+ /** Dispatch a message-target interaction. */
852
+ async handleMessage(interaction) {
853
+ const command2 = this.messages.get(interaction.commandName);
854
+ if (command2 === void 0) return;
855
+ await this.dispatch(command2, interaction);
856
+ }
857
+ async dispatch(command2, interaction) {
858
+ this.logger?.debug("contextMenu", {
859
+ data: { kind: command2.kind, name: command2.name, user: interaction.user.id }
860
+ });
861
+ const cooldown = command2.cooldown ?? this.defaultCooldown;
862
+ if (cooldown !== void 0 && this.cooldowns !== void 0) {
863
+ const result = this.cooldowns.consume(
864
+ `${command2.kind}:${command2.name}`,
865
+ cooldown,
866
+ actorOf(interaction)
867
+ );
868
+ if (!result.allowed) {
869
+ await replyCooldown(interaction, cooldown, result.remaining);
870
+ return;
871
+ }
872
+ }
873
+ const guards = combineGuards(this.defaultGuards, command2.guards);
874
+ if (guards.length > 0) {
875
+ const guardResult = await runGuards(interaction, guards);
876
+ if (!guardResult.allowed) {
877
+ this.logger?.debug("contextMenu denied", {
878
+ data: { name: command2.name, user: interaction.user.id, reason: guardResult.reason ?? "" }
879
+ });
880
+ await replyDenied(interaction, guardResult.reason);
881
+ return;
882
+ }
883
+ }
884
+ const start = Date.now();
885
+ try {
886
+ if (command2.kind === "userMenu") {
887
+ await command2.execute(interaction);
888
+ } else {
889
+ await command2.execute(interaction);
890
+ }
891
+ this.onUsage?.({
892
+ type: "command",
893
+ name: command2.name,
894
+ detail: command2.kind,
895
+ outcome: "success",
896
+ durationMs: Date.now() - start,
897
+ userId: interaction.user.id,
898
+ userTag: interaction.user.tag,
899
+ guildId: interaction.guildId,
900
+ channelId: interaction.channelId,
901
+ timestamp: /* @__PURE__ */ new Date()
902
+ });
903
+ } catch (error) {
904
+ const err = error instanceof Error ? error : new Error(String(error));
905
+ this.onUsage?.({
906
+ type: "command",
907
+ name: command2.name,
908
+ detail: command2.kind,
909
+ outcome: "error",
910
+ errorMessage: err.message,
911
+ durationMs: Date.now() - start,
912
+ userId: interaction.user.id,
913
+ userTag: interaction.user.tag,
914
+ guildId: interaction.guildId,
915
+ channelId: interaction.channelId,
916
+ timestamp: /* @__PURE__ */ new Date()
917
+ });
918
+ interaction.client.emit("error", err);
919
+ try {
920
+ if (!interaction.replied && !interaction.deferred) {
921
+ await interaction.reply({
922
+ content: "Something went wrong.",
923
+ flags: MessageFlags.Ephemeral
924
+ });
925
+ }
926
+ } catch {
927
+ }
928
+ }
929
+ }
930
+ };
931
+ function combineGuards(defaults, own) {
932
+ if (own === void 0 || own.length === 0) return defaults;
933
+ if (defaults.length === 0) return own;
934
+ return [...defaults, ...own];
935
+ }
936
+ function actorOf(interaction) {
937
+ const member = interaction.member;
938
+ let roleIds = [];
939
+ if (member !== null) {
940
+ const roles = member.roles;
941
+ roleIds = Array.isArray(roles) ? roles : [...roles.cache.keys()];
942
+ }
943
+ return {
944
+ userId: interaction.user.id,
945
+ roleIds,
946
+ guildId: interaction.guildId,
947
+ channelId: interaction.channelId
948
+ };
949
+ }
950
+ function clientEmbeds(client) {
951
+ return client.embeds ?? defaultEmbeds;
952
+ }
953
+ async function replyCooldown(interaction, config, remaining) {
954
+ const content = formatCooldownMessage(config, remaining);
955
+ try {
956
+ if (interaction.deferred) await interaction.editReply({ content });
957
+ else if (interaction.replied) await interaction.followUp({ content, flags: MessageFlags.Ephemeral });
958
+ else await interaction.reply({ content, flags: MessageFlags.Ephemeral });
959
+ } catch {
960
+ }
961
+ }
962
+ async function replyDenied(interaction, reason) {
963
+ const embeds = clientEmbeds(interaction.client);
964
+ const text = reason ?? "You don't have permission to use this.";
965
+ try {
966
+ const payload = { embeds: [embeds.error(text)], flags: MessageFlags.Ephemeral };
967
+ if (interaction.deferred) await interaction.editReply({ embeds: payload.embeds });
968
+ else if (interaction.replied) await interaction.followUp(payload);
969
+ else await interaction.reply(payload);
970
+ } catch {
971
+ }
972
+ }
9
973
 
10
- // src/logger.ts
974
+ // src/prefix-args.ts
975
+ var SNOWFLAKE_RE = /^\d{15,21}$/;
976
+ var USER_MENTION_RE = /^<@!?(\d{15,21})>$/;
977
+ var CHANNEL_MENTION_RE = /^<#(\d{15,21})>$/;
978
+ var ROLE_MENTION_RE = /^<@&(\d{15,21})>$/;
979
+ function extractSnowflake(input) {
980
+ if (SNOWFLAKE_RE.test(input)) return input;
981
+ const m = USER_MENTION_RE.exec(input) ?? CHANNEL_MENTION_RE.exec(input) ?? ROLE_MENTION_RE.exec(input);
982
+ return m === null ? null : m[1] ?? null;
983
+ }
984
+ var PrefixArgsBuilder = class _PrefixArgsBuilder {
985
+ specs;
986
+ /** @internal */
987
+ constructor(specs = []) {
988
+ this.specs = specs;
989
+ }
990
+ /** A raw string token. */
991
+ string(name, options) {
992
+ return this.push({ name, kind: "string", required: options?.required ?? false, defaultValue: options?.default });
993
+ }
994
+ /** A whole integer. */
995
+ integer(name, options) {
996
+ return this.push({ name, kind: "integer", required: options?.required ?? false, defaultValue: options?.default });
997
+ }
998
+ /** A floating-point number. */
999
+ number(name, options) {
1000
+ return this.push({ name, kind: "number", required: options?.required ?? false, defaultValue: options?.default });
1001
+ }
1002
+ /** A boolean (`true`/`yes`/`1`/`on` vs `false`/`no`/`0`/`off`). */
1003
+ boolean(name, options) {
1004
+ return this.push({ name, kind: "boolean", required: options?.required ?? false, defaultValue: options?.default });
1005
+ }
1006
+ /** A Discord snowflake id — accepts raw ids and `<@u>` / `<#c>` / `<@&r>` mentions. */
1007
+ snowflake(name, options) {
1008
+ return this.push({ name, kind: "snowflake", required: options?.required ?? false, defaultValue: options?.default });
1009
+ }
1010
+ /** A duration like `"1h30m"` or `"1 saat"` parsed to milliseconds. */
1011
+ duration(name, options) {
1012
+ return this.push({ name, kind: "duration", required: options?.required ?? false, defaultValue: options?.default });
1013
+ }
1014
+ /** The remainder of the message (everything after previous args). */
1015
+ rest(name, options) {
1016
+ return this.push({ name, kind: "rest", required: options?.required ?? false, defaultValue: options?.default });
1017
+ }
1018
+ push(spec) {
1019
+ return new _PrefixArgsBuilder([...this.specs, spec]);
1020
+ }
1021
+ /** Compile this builder into a parser. */
1022
+ compile() {
1023
+ const specs = this.specs;
1024
+ return {
1025
+ specs,
1026
+ parse(tokens, rest) {
1027
+ const out = {};
1028
+ let idx = 0;
1029
+ for (let i = 0; i < specs.length; i++) {
1030
+ const spec = specs[i];
1031
+ if (spec.kind === "rest") {
1032
+ const tail = idx === 0 ? rest : tokens.slice(idx).join(" ");
1033
+ if (tail.length === 0) {
1034
+ if (spec.required) return { ok: false, arg: spec.name, reason: `missing required argument "${spec.name}"` };
1035
+ out[spec.name] = spec.defaultValue;
1036
+ } else {
1037
+ out[spec.name] = tail;
1038
+ }
1039
+ idx = tokens.length;
1040
+ continue;
1041
+ }
1042
+ const token = tokens[idx];
1043
+ if (token === void 0) {
1044
+ if (spec.required) {
1045
+ return { ok: false, arg: spec.name, reason: `missing required argument "${spec.name}"` };
1046
+ }
1047
+ out[spec.name] = spec.defaultValue;
1048
+ continue;
1049
+ }
1050
+ const parsed = coerce(spec, token);
1051
+ if (parsed.ok === false) return { ok: false, arg: spec.name, reason: parsed.reason };
1052
+ out[spec.name] = parsed.value;
1053
+ idx += 1;
1054
+ }
1055
+ return { ok: true, values: out };
1056
+ }
1057
+ };
1058
+ }
1059
+ };
1060
+ function coerce(spec, token) {
1061
+ switch (spec.kind) {
1062
+ case "string":
1063
+ return { ok: true, value: token };
1064
+ case "integer": {
1065
+ const n = Number(token);
1066
+ if (!Number.isInteger(n)) return { ok: false, reason: `"${token}" is not an integer` };
1067
+ return { ok: true, value: n };
1068
+ }
1069
+ case "number": {
1070
+ const n = Number(token);
1071
+ if (!Number.isFinite(n)) return { ok: false, reason: `"${token}" is not a number` };
1072
+ return { ok: true, value: n };
1073
+ }
1074
+ case "boolean": {
1075
+ const low = token.toLowerCase();
1076
+ if (["true", "1", "yes", "on"].includes(low)) return { ok: true, value: true };
1077
+ if (["false", "0", "no", "off"].includes(low)) return { ok: true, value: false };
1078
+ return { ok: false, reason: `"${token}" is not a boolean` };
1079
+ }
1080
+ case "snowflake": {
1081
+ const id = extractSnowflake(token);
1082
+ if (id === null) return { ok: false, reason: `"${token}" is not a snowflake or mention` };
1083
+ return { ok: true, value: id };
1084
+ }
1085
+ case "duration": {
1086
+ const ms = parseDuration(token);
1087
+ if (ms === null) return { ok: false, reason: `"${token}" is not a duration` };
1088
+ return { ok: true, value: ms };
1089
+ }
1090
+ default:
1091
+ return { ok: false, reason: `unknown arg kind for "${spec.name}"` };
1092
+ }
1093
+ }
1094
+ function prefixArgs() {
1095
+ return new PrefixArgsBuilder();
1096
+ }
1097
+ function normalisePayload(render) {
1098
+ if (typeof render.setColor === "function" && typeof render.toJSON === "function") {
1099
+ return { embeds: [render] };
1100
+ }
1101
+ if (Array.isArray(render)) {
1102
+ return { embeds: render };
1103
+ }
1104
+ return render;
1105
+ }
1106
+ function controlsRow(page, pages, ns, controls, labels) {
1107
+ const buttons = [];
1108
+ if (controls === "first-prev-next-last") {
1109
+ buttons.push(
1110
+ new ButtonBuilder().setCustomId(`${ns}:first`).setStyle(ButtonStyle.Secondary).setLabel(labels.first).setDisabled(page === 0)
1111
+ );
1112
+ }
1113
+ buttons.push(
1114
+ new ButtonBuilder().setCustomId(`${ns}:prev`).setStyle(ButtonStyle.Primary).setLabel(labels.prev).setDisabled(page === 0),
1115
+ new ButtonBuilder().setCustomId(`${ns}:next`).setStyle(ButtonStyle.Primary).setLabel(labels.next).setDisabled(page >= pages - 1)
1116
+ );
1117
+ if (controls === "first-prev-next-last") {
1118
+ buttons.push(
1119
+ new ButtonBuilder().setCustomId(`${ns}:last`).setStyle(ButtonStyle.Secondary).setLabel(labels.last).setDisabled(page >= pages - 1)
1120
+ );
1121
+ }
1122
+ return new ActionRowBuilder().addComponents(...buttons);
1123
+ }
1124
+ function resolveLabels2(input) {
1125
+ return {
1126
+ first: input?.first ?? "\xAB",
1127
+ prev: input?.prev ?? "\u2039",
1128
+ next: input?.next ?? "\u203A",
1129
+ last: input?.last ?? "\xBB"
1130
+ };
1131
+ }
1132
+ async function buildPaginatorPage(items, page, options) {
1133
+ const pageSize = options.pageSize ?? 10;
1134
+ const pages = Math.max(1, Math.ceil(items.length / pageSize));
1135
+ const ns = options.namespace ?? "spk-page";
1136
+ const controls = options.controls ?? "prev-next";
1137
+ const labels = resolveLabels2(options.labels);
1138
+ const slice = items.slice(page * pageSize, page * pageSize + pageSize);
1139
+ const body = await options.render(slice, { page, pages });
1140
+ const payload = normalisePayload(body);
1141
+ const components = pages > 1 ? [controlsRow(page, pages, ns, controls, labels)] : [];
1142
+ return { payload: { ...payload, components }, pages };
1143
+ }
1144
+ async function paginate(interaction, items, options) {
1145
+ const pageSize = options.pageSize ?? 10;
1146
+ const ns = options.namespace ?? "spk-page";
1147
+ const controls = options.controls ?? "prev-next";
1148
+ const labels = resolveLabels2(options.labels);
1149
+ const allowedUser = options.user ?? interaction.user.id;
1150
+ let page = 0;
1151
+ const buildPage = async () => {
1152
+ return buildPaginatorPage(items, page, { ...options, pageSize, namespace: ns, controls, labels });
1153
+ };
1154
+ const { payload: initial, pages } = await buildPage();
1155
+ const sent = interaction.deferred ? await interaction.editReply(initial) : (await interaction.reply({
1156
+ ...initial,
1157
+ flags: options.ephemeral === true ? 64 : void 0
1158
+ }), await interaction.fetchReply());
1159
+ if (pages <= 1) return;
1160
+ const collector = sent.createMessageComponentCollector({
1161
+ componentType: ComponentType.Button,
1162
+ time: options.timeoutMs ?? 5 * 6e4,
1163
+ filter: (i) => i.user.id === allowedUser && i.customId.startsWith(`${ns}:`)
1164
+ });
1165
+ collector.on("collect", async (button2) => {
1166
+ const action = button2.customId.slice(ns.length + 1);
1167
+ if (action === "first") page = 0;
1168
+ else if (action === "prev") page = Math.max(0, page - 1);
1169
+ else if (action === "next") page = Math.min(pages - 1, page + 1);
1170
+ else if (action === "last") page = pages - 1;
1171
+ const next = await buildPage();
1172
+ await button2.update(next.payload).catch(() => void 0);
1173
+ });
1174
+ collector.on("end", async () => {
1175
+ const disabledRow = controlsRow(page, pages, ns, controls, labels);
1176
+ for (const c of disabledRow.components) c.setDisabled(true);
1177
+ const { payload: final } = await buildPage();
1178
+ await interaction.editReply({ ...final, components: [disabledRow] }).catch(() => void 0);
1179
+ });
1180
+ }
1181
+ var STYLE_MAP = {
1182
+ Primary: ButtonStyle.Primary,
1183
+ Secondary: ButtonStyle.Secondary,
1184
+ Success: ButtonStyle.Success,
1185
+ Danger: ButtonStyle.Danger
1186
+ };
1187
+ function clientEmbeds2(client) {
1188
+ return client.embeds ?? defaultEmbeds;
1189
+ }
1190
+ async function confirm(interaction, options) {
1191
+ const ns = options.namespace ?? "spk-confirm";
1192
+ const confirmLabel = options.confirm?.label ?? "Confirm";
1193
+ const cancelLabel = options.cancel?.label ?? "Cancel";
1194
+ const confirmStyle = STYLE_MAP[options.confirm?.style ?? "Success"];
1195
+ const cancelStyle = STYLE_MAP[options.cancel?.style ?? "Secondary"];
1196
+ const user = options.user ?? interaction.user.id;
1197
+ const ephemeral = options.ephemeral !== false;
1198
+ const embeds = clientEmbeds2(interaction.client);
1199
+ const promptEmbed = embeds.info(
1200
+ options.title !== void 0 ? { title: options.title, description: options.body } : options.body
1201
+ );
1202
+ const row2 = new ActionRowBuilder().addComponents(
1203
+ new ButtonBuilder().setCustomId(`${ns}:yes`).setLabel(confirmLabel).setStyle(confirmStyle),
1204
+ new ButtonBuilder().setCustomId(`${ns}:no`).setLabel(cancelLabel).setStyle(cancelStyle)
1205
+ );
1206
+ const payload = { embeds: [promptEmbed], components: [row2] };
1207
+ const sent = interaction.deferred ? await interaction.editReply(payload) : (await interaction.reply({
1208
+ ...payload,
1209
+ flags: ephemeral ? 64 : void 0
1210
+ }), await interaction.fetchReply());
1211
+ return new Promise((resolve) => {
1212
+ const collector = sent.createMessageComponentCollector({
1213
+ componentType: ComponentType.Button,
1214
+ time: options.timeoutMs ?? 3e4,
1215
+ max: 1,
1216
+ filter: (i) => i.user.id === user && i.customId.startsWith(`${ns}:`)
1217
+ });
1218
+ let outcome = null;
1219
+ collector.on("collect", async (button2) => {
1220
+ const action = button2.customId.slice(ns.length + 1);
1221
+ outcome = {
1222
+ confirmed: action === "yes",
1223
+ reason: action === "yes" ? "confirm" : "cancel",
1224
+ interaction: button2
1225
+ };
1226
+ await button2.deferUpdate().catch(() => void 0);
1227
+ });
1228
+ collector.on("end", async () => {
1229
+ for (const c of row2.components) c.setDisabled(true);
1230
+ await interaction.editReply({ embeds: [promptEmbed], components: [row2] }).catch(() => void 0);
1231
+ resolve(outcome ?? { confirmed: false, reason: "timeout" });
1232
+ });
1233
+ });
1234
+ }
11
1235
  var RANK = {
12
1236
  debug: 10,
13
1237
  info: 20,
@@ -30,6 +1254,71 @@ function consoleSink(entry) {
30
1254
  write(line);
31
1255
  if (entry.error !== void 0) write(entry.error.stack ?? String(entry.error));
32
1256
  }
1257
+ function jsonlSink(path, options = {}) {
1258
+ const min = options.minLevel ?? "debug";
1259
+ let dirReady = false;
1260
+ let chain = Promise.resolve();
1261
+ return (entry) => {
1262
+ if (RANK[entry.level] < RANK[min]) return;
1263
+ const record = {
1264
+ ...entry,
1265
+ timestamp: entry.timestamp.toISOString(),
1266
+ error: entry.error ? { name: entry.error.name, message: entry.error.message, stack: entry.error.stack } : void 0
1267
+ };
1268
+ const line = `${JSON.stringify(record)}
1269
+ `;
1270
+ chain = chain.then(async () => {
1271
+ try {
1272
+ if (!dirReady) {
1273
+ await mkdir(dirname(path), { recursive: true });
1274
+ dirReady = true;
1275
+ }
1276
+ await appendFile(path, line, "utf8");
1277
+ } catch {
1278
+ }
1279
+ });
1280
+ };
1281
+ }
1282
+ function webhookSink(options) {
1283
+ const min = options.minLevel ?? "warn";
1284
+ return (entry) => {
1285
+ if (RANK[entry.level] < RANK[min]) return;
1286
+ const color = entry.level === "error" ? 15747655 : entry.level === "warn" ? 16361509 : 3447003;
1287
+ const fields = [];
1288
+ if (entry.scope !== void 0) fields.push({ name: "Scope", value: entry.scope, inline: true });
1289
+ if (entry.data !== void 0) {
1290
+ for (const [k, v] of Object.entries(entry.data)) {
1291
+ fields.push({ name: k, value: formatValue(v).slice(0, 1e3), inline: true });
1292
+ }
1293
+ }
1294
+ const desc = entry.error?.stack !== void 0 ? `${entry.message}
1295
+ \`\`\`
1296
+ ${entry.error.stack.slice(0, 1800)}
1297
+ \`\`\`` : entry.message;
1298
+ const body = {
1299
+ username: options.username ?? "spearkit",
1300
+ embeds: [
1301
+ {
1302
+ title: `[${entry.level.toUpperCase()}] ${entry.message.slice(0, 240)}`,
1303
+ description: desc.slice(0, 4e3),
1304
+ color,
1305
+ timestamp: entry.timestamp.toISOString(),
1306
+ fields: fields.slice(0, 25)
1307
+ }
1308
+ ]
1309
+ };
1310
+ void fetch(options.url, {
1311
+ method: "POST",
1312
+ headers: { "content-type": "application/json" },
1313
+ body: JSON.stringify(body)
1314
+ }).catch(() => void 0);
1315
+ };
1316
+ }
1317
+ function resolveTransports(options) {
1318
+ if (options.transports !== void 0 && options.transports.length > 0) return options.transports;
1319
+ if (options.sink !== void 0) return [options.sink];
1320
+ return [consoleSink];
1321
+ }
33
1322
  var Logger = class _Logger {
34
1323
  state;
35
1324
  /** The scope prefix applied to every entry, if any. */
@@ -37,7 +1326,7 @@ var Logger = class _Logger {
37
1326
  constructor(options = {}) {
38
1327
  this.state = {
39
1328
  threshold: options.level ?? "info",
40
- sink: options.sink ?? consoleSink
1329
+ transports: resolveTransports(options)
41
1330
  };
42
1331
  this.scope = options.scope;
43
1332
  }
@@ -50,6 +1339,16 @@ var Logger = class _Logger {
50
1339
  this.state.threshold = level;
51
1340
  return this;
52
1341
  }
1342
+ /** Replace the transport list for this logger and every child sharing its state. */
1343
+ setTransports(transports) {
1344
+ this.state.transports = transports;
1345
+ return this;
1346
+ }
1347
+ /** Append a transport to the existing list. */
1348
+ addTransport(sink) {
1349
+ this.state.transports = [...this.state.transports, sink];
1350
+ return this;
1351
+ }
53
1352
  /** Whether an entry of `level` would currently be emitted. */
54
1353
  enabled(level) {
55
1354
  return RANK[level] >= RANK[this.state.threshold];
@@ -64,14 +1363,20 @@ var Logger = class _Logger {
64
1363
  /** Emit an entry at an explicit level. */
65
1364
  log(level, message, options) {
66
1365
  if (!this.enabled(level)) return;
67
- this.state.sink({
1366
+ const entry = {
68
1367
  level,
69
1368
  message,
70
1369
  scope: this.scope,
71
1370
  timestamp: /* @__PURE__ */ new Date(),
72
1371
  error: options?.error,
73
1372
  data: options?.data
74
- });
1373
+ };
1374
+ for (const sink of this.state.transports) {
1375
+ try {
1376
+ sink(entry);
1377
+ } catch {
1378
+ }
1379
+ }
75
1380
  }
76
1381
  /** Verbose diagnostics, off by default. */
77
1382
  debug(message, options) {
@@ -164,98 +1469,14 @@ function envRequire(key) {
164
1469
  if (value === void 0) {
165
1470
  throw new Error(`spearkit: required environment variable "${key}" is missing or empty`);
166
1471
  }
167
- return value;
168
- }
169
- var env = {
170
- string: envString,
171
- number: envNumber,
172
- boolean: envBoolean,
173
- require: envRequire
174
- };
175
-
176
- // src/cooldown.ts
177
- function normalizeCooldown(input) {
178
- return typeof input === "number" ? { duration: input } : input;
179
- }
180
- function scopeKey(scope, actor) {
181
- switch (scope) {
182
- case "guild":
183
- return `g:${actor.guildId ?? "dm"}`;
184
- case "channel":
185
- return `c:${actor.channelId ?? "dm"}`;
186
- case "global":
187
- return "global";
188
- case "user":
189
- return `u:${actor.userId}`;
190
- }
191
- }
192
- function effectiveDuration(config, actor) {
193
- if (config.exempt?.users?.includes(actor.userId) === true) return null;
194
- if (config.exempt?.roles?.some((roleId) => actor.roleIds.includes(roleId)) === true) return null;
195
- const userOverride = config.overrides?.users?.[actor.userId];
196
- if (userOverride !== void 0) return userOverride;
197
- const roleOverrides = config.overrides?.roles;
198
- if (roleOverrides !== void 0) {
199
- let best;
200
- for (const roleId of actor.roleIds) {
201
- const candidate = roleOverrides[roleId];
202
- if (candidate !== void 0) best = best === void 0 ? candidate : Math.min(best, candidate);
203
- }
204
- if (best !== void 0) return best;
205
- }
206
- return config.duration;
207
- }
208
- function keyFor(bucket, config, actor) {
209
- return `${bucket}|${scopeKey(config.scope ?? "user", actor)}`;
210
- }
211
- var CooldownManager = class {
212
- hits = /* @__PURE__ */ new Map();
213
- /** Number of tracked buckets. */
214
- get size() {
215
- return this.hits.size;
216
- }
217
- /**
218
- * Check whether `actor` may use `bucket`, recording the use when allowed.
219
- * Exempt actors and non-positive durations are always allowed (no record).
220
- */
221
- consume(bucket, input, actor, now = Date.now()) {
222
- const config = normalizeCooldown(input);
223
- const duration = effectiveDuration(config, actor);
224
- if (duration === null || duration <= 0) return { allowed: true };
225
- const key = keyFor(bucket, config, actor);
226
- const last = this.hits.get(key);
227
- if (last !== void 0 && now - last < duration) {
228
- return { allowed: false, remaining: duration - (now - last) };
229
- }
230
- this.hits.set(key, now);
231
- return { allowed: true };
232
- }
233
- /** Like {@link consume} but never records — a read-only check. */
234
- peek(bucket, input, actor, now = Date.now()) {
235
- const config = normalizeCooldown(input);
236
- const duration = effectiveDuration(config, actor);
237
- if (duration === null || duration <= 0) return { allowed: true };
238
- const last = this.hits.get(keyFor(bucket, config, actor));
239
- if (last !== void 0 && now - last < duration) {
240
- return { allowed: false, remaining: duration - (now - last) };
241
- }
242
- return { allowed: true };
243
- }
244
- /** Clear a single actor's cooldown for a bucket. Returns whether one existed. */
245
- reset(bucket, actor, scope = "user") {
246
- return this.hits.delete(`${bucket}|${scopeKey(scope, actor)}`);
247
- }
248
- /** Drop every tracked cooldown. */
249
- clear() {
250
- this.hits.clear();
251
- }
252
- };
253
- function formatCooldownMessage(config, remainingMs) {
254
- if (typeof config.message === "function") return config.message(remainingMs);
255
- if (typeof config.message === "string") return config.message;
256
- const seconds = Math.max(1, Math.ceil(remainingMs / 1e3));
257
- return `You're on cooldown \u2014 try again in ${seconds}s.`;
1472
+ return value;
258
1473
  }
1474
+ var env = {
1475
+ string: envString,
1476
+ number: envNumber,
1477
+ boolean: envBoolean,
1478
+ require: envRequire
1479
+ };
259
1480
 
260
1481
  // src/scheduler.ts
261
1482
  var ALIASES = {
@@ -348,7 +1569,7 @@ var CronExpression = class {
348
1569
  const date = new Date(from.getTime());
349
1570
  date.setSeconds(0, 0);
350
1571
  date.setMinutes(date.getMinutes() + 1);
351
- for (let guard = 0; guard < 1e5; guard++) {
1572
+ for (let guard2 = 0; guard2 < 1e5; guard2++) {
352
1573
  if (!this.months.has(date.getMonth() + 1)) {
353
1574
  date.setMonth(date.getMonth() + 1, 1);
354
1575
  date.setHours(0, 0, 0, 0);
@@ -398,6 +1619,7 @@ var TaskScheduler = class {
398
1619
  running = false;
399
1620
  client;
400
1621
  logger;
1622
+ reconcilers = [];
401
1623
  /** Number of registered tasks. */
402
1624
  get size() {
403
1625
  return this.tasks.size;
@@ -428,12 +1650,93 @@ var TaskScheduler = class {
428
1650
  this.cancel(name);
429
1651
  return this.tasks.delete(name);
430
1652
  }
1653
+ /**
1654
+ * Schedule a one-shot job: run `fn` once after `ms` milliseconds, then forget.
1655
+ * Returns a cancel handle. Replaces hand-rolled `setTimeout` calls for things
1656
+ * like "remind the moderator in 10 minutes if no claim happened".
1657
+ */
1658
+ delay(name, ms, fn) {
1659
+ const key = `delay:${name}`;
1660
+ this.cancel(key);
1661
+ const timer = setTimeout(async () => {
1662
+ this.timers.delete(key);
1663
+ try {
1664
+ await fn();
1665
+ } catch (error) {
1666
+ this.logger?.error(`delay "${name}" failed`, { error: toError(error) });
1667
+ }
1668
+ }, Math.max(0, ms));
1669
+ if (typeof timer.unref === "function") timer.unref();
1670
+ this.timers.set(key, timer);
1671
+ return {
1672
+ cancel: () => {
1673
+ const had = this.timers.has(key);
1674
+ this.cancel(key);
1675
+ return had;
1676
+ }
1677
+ };
1678
+ }
1679
+ /**
1680
+ * Schedule a series of follow-up fires from a single start point. Each
1681
+ * delay is measured from "now"; the callback receives the index of the
1682
+ * fire. Generalises the 10s/30s/60s retry pattern in real bots.
1683
+ */
1684
+ followUp(name, delays, fn) {
1685
+ const keys = delays.map((_, i) => `followUp:${name}:${i}`);
1686
+ for (const key of keys) this.cancel(key);
1687
+ delays.forEach((delay, i) => {
1688
+ const key = keys[i];
1689
+ const timer = setTimeout(async () => {
1690
+ this.timers.delete(key);
1691
+ try {
1692
+ await fn(i);
1693
+ } catch (error) {
1694
+ this.logger?.error(`followUp "${name}" fire ${i} failed`, { error: toError(error) });
1695
+ }
1696
+ }, Math.max(0, delay));
1697
+ if (typeof timer.unref === "function") timer.unref();
1698
+ this.timers.set(key, timer);
1699
+ });
1700
+ return {
1701
+ cancel: () => {
1702
+ let any = false;
1703
+ for (const key of keys) {
1704
+ if (this.timers.has(key)) any = true;
1705
+ this.cancel(key);
1706
+ }
1707
+ return any;
1708
+ }
1709
+ };
1710
+ }
1711
+ /**
1712
+ * Register a once-on-ready reconciler — runs the first time the scheduler
1713
+ * starts (typically when the client becomes ready) and never again. Use
1714
+ * for restart-recovery work like closing orphaned voice sessions or
1715
+ * reapplying cached channel state.
1716
+ */
1717
+ reconcile(name, fn) {
1718
+ if (this.running && this.client !== void 0) {
1719
+ void this.runReconciler(name, fn, this.client);
1720
+ } else {
1721
+ this.reconcilers.push({ name, run: fn });
1722
+ }
1723
+ }
1724
+ async runReconciler(name, run, client) {
1725
+ this.logger?.debug("reconcile", { data: { name } });
1726
+ try {
1727
+ await run(client);
1728
+ } catch (error) {
1729
+ this.logger?.error(`reconciler "${name}" failed`, { error: toError(error) });
1730
+ }
1731
+ }
431
1732
  /** Start every task. Safe to call once; later calls are ignored. */
432
1733
  start(client) {
433
1734
  if (this.running) return;
434
1735
  this.client = client;
435
1736
  this.running = true;
436
1737
  for (const task2 of this.tasks.values()) this.begin(task2);
1738
+ const pending = this.reconcilers.splice(0);
1739
+ for (const { name, run } of pending) void this.runReconciler(name, run, client);
437
1740
  }
438
1741
  /** Stop the scheduler and cancel every pending timer. */
439
1742
  stop() {
@@ -487,28 +1790,33 @@ var TaskScheduler = class {
487
1790
 
488
1791
  // src/prefix.ts
489
1792
  function prefixCommand(config) {
1793
+ const parser = config.args !== void 0 ? config.args(prefixArgs()).compile() : void 0;
490
1794
  return {
491
1795
  kind: "prefixCommand",
492
1796
  name: config.name,
493
1797
  aliases: config.aliases ?? [],
494
1798
  description: config.description,
495
1799
  cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
1800
+ guards: config.guards,
1801
+ parser,
496
1802
  run: async (ctx) => {
497
1803
  await config.run(ctx);
498
1804
  }
499
1805
  };
500
1806
  }
501
1807
  var PrefixContext = class {
502
- constructor(message, commandName, args, rest) {
1808
+ constructor(message, commandName, args, rest, options = {}) {
503
1809
  this.message = message;
504
1810
  this.commandName = commandName;
505
1811
  this.args = args;
506
1812
  this.rest = rest;
1813
+ this.options = options;
507
1814
  }
508
1815
  message;
509
1816
  commandName;
510
1817
  args;
511
1818
  rest;
1819
+ options;
512
1820
  get client() {
513
1821
  return this.message.client;
514
1822
  }
@@ -578,6 +1886,7 @@ var PrefixRegistry = class {
578
1886
  cooldowns;
579
1887
  defaultCooldown;
580
1888
  errorHandler;
1889
+ defaultGuards = [];
581
1890
  onUsage;
582
1891
  /** Configure prefixes and matching behaviour. */
583
1892
  setOptions(input) {
@@ -600,6 +1909,11 @@ var PrefixRegistry = class {
600
1909
  this.defaultCooldown = defaultCooldown;
601
1910
  return this;
602
1911
  }
1912
+ /** Guards that run before every prefix command's own guards. */
1913
+ setDefaultGuards(guards) {
1914
+ this.defaultGuards = guards;
1915
+ return this;
1916
+ }
603
1917
  /** Set the handler used when a prefix command throws. */
604
1918
  onError(handler) {
605
1919
  this.errorHandler = handler;
@@ -665,12 +1979,44 @@ var PrefixRegistry = class {
665
1979
  return true;
666
1980
  }
667
1981
  }
1982
+ const guards = combineGuards2(this.defaultGuards, command2.guards);
1983
+ if (guards.length > 0) {
1984
+ const guardCtx = guardContextFromMessage(message);
1985
+ const guardResult = await runGuards(guardCtx, guards);
1986
+ if (!guardResult.allowed) {
1987
+ this.logger?.debug("prefix denied", {
1988
+ data: {
1989
+ command: command2.name,
1990
+ user: message.author.id,
1991
+ reason: guardResult.reason ?? ""
1992
+ }
1993
+ });
1994
+ await replyDeniedMessage(message, guardResult.reason);
1995
+ return true;
1996
+ }
1997
+ }
668
1998
  const args = rest.length > 0 ? rest.split(/\s+/) : [];
1999
+ let options = {};
2000
+ if (command2.parser !== void 0) {
2001
+ const parsed = command2.parser.parse(args, rest);
2002
+ if (!parsed.ok) {
2003
+ this.logger?.debug("prefix arg error", {
2004
+ data: { command: command2.name, user: message.author.id, arg: parsed.arg, reason: parsed.reason }
2005
+ });
2006
+ const embeds = clientEmbeds3(message.client);
2007
+ await message.reply({ embeds: [embeds.error(`Argument \`${parsed.arg}\`: ${parsed.reason}`)] }).catch(() => void 0);
2008
+ return true;
2009
+ }
2010
+ options = parsed.values;
2011
+ }
2012
+ const start = Date.now();
669
2013
  try {
670
- await command2.run(new PrefixContext(message, name, args, rest));
2014
+ await command2.run(new PrefixContext(message, name, args, rest, options));
671
2015
  this.onUsage?.({
672
2016
  type: "prefix",
673
2017
  name: command2.name,
2018
+ outcome: "success",
2019
+ durationMs: Date.now() - start,
674
2020
  userId: message.author.id,
675
2021
  userTag: message.author.tag,
676
2022
  guildId: message.guildId,
@@ -680,11 +2026,46 @@ var PrefixRegistry = class {
680
2026
  } catch (error) {
681
2027
  const err = toError(error);
682
2028
  this.logger?.error(`prefix command "${command2.name}" failed`, { error: err });
2029
+ this.onUsage?.({
2030
+ type: "prefix",
2031
+ name: command2.name,
2032
+ outcome: "error",
2033
+ errorMessage: err.message,
2034
+ durationMs: Date.now() - start,
2035
+ userId: message.author.id,
2036
+ userTag: message.author.tag,
2037
+ guildId: message.guildId,
2038
+ channelId: message.channelId,
2039
+ timestamp: /* @__PURE__ */ new Date()
2040
+ });
683
2041
  if (this.errorHandler !== void 0) await this.errorHandler(err, message);
684
2042
  }
685
2043
  return true;
686
2044
  }
687
2045
  };
2046
+ function combineGuards2(defaults, own) {
2047
+ if (own === void 0 || own.length === 0) return defaults;
2048
+ if (defaults.length === 0) return own;
2049
+ return [...defaults, ...own];
2050
+ }
2051
+ function guardContextFromMessage(message) {
2052
+ return {
2053
+ client: message.client,
2054
+ user: message.author,
2055
+ member: message.member,
2056
+ guild: message.guild,
2057
+ guildId: message.guildId,
2058
+ channelId: message.channelId
2059
+ };
2060
+ }
2061
+ function clientEmbeds3(client) {
2062
+ return client.embeds ?? defaultEmbeds;
2063
+ }
2064
+ async function replyDeniedMessage(message, reason) {
2065
+ const embeds = clientEmbeds3(message.client);
2066
+ const text = reason ?? "You don't have permission to use this.";
2067
+ await message.reply({ embeds: [embeds.error(text)] }).catch(() => void 0);
2068
+ }
688
2069
  var MemoryUsageStore = class {
689
2070
  constructor(limit = Number.POSITIVE_INFINITY) {
690
2071
  this.limit = limit;
@@ -725,7 +2106,7 @@ var JsonFileUsageStore = class {
725
2106
  async all() {
726
2107
  let content;
727
2108
  try {
728
- content = await readFile(this.path, "utf8");
2109
+ content = await readFile$1(this.path, "utf8");
729
2110
  } catch {
730
2111
  return [];
731
2112
  }
@@ -799,107 +2180,6 @@ var UsageTracker = class {
799
2180
  }
800
2181
  }
801
2182
  };
802
- function withEphemeralFlag(flags) {
803
- if (flags == null) return MessageFlags.Ephemeral;
804
- if (typeof flags === "number" || typeof flags === "bigint") {
805
- return Number(flags) | MessageFlags.Ephemeral;
806
- }
807
- if (Array.isArray(flags)) return [...flags, MessageFlags.Ephemeral];
808
- return [flags, MessageFlags.Ephemeral];
809
- }
810
- function normalizeReply(input) {
811
- if (typeof input === "string") return { content: input };
812
- const { ephemeral, ...rest } = input;
813
- if (ephemeral) return { ...rest, flags: withEphemeralFlag(rest.flags) };
814
- return rest;
815
- }
816
- function normalizeEdit(input) {
817
- if (typeof input === "string") return { content: input };
818
- const { ephemeral: _ephemeral, flags: _flags, ...rest } = input;
819
- return rest;
820
- }
821
- function asEphemeral(input) {
822
- if (typeof input === "string") return { content: input, ephemeral: true };
823
- return { ...input, ephemeral: true };
824
- }
825
- var BaseContext = class {
826
- constructor(interaction) {
827
- this.interaction = interaction;
828
- }
829
- interaction;
830
- get client() {
831
- return this.interaction.client;
832
- }
833
- get user() {
834
- return this.interaction.user;
835
- }
836
- get member() {
837
- return this.interaction.member;
838
- }
839
- get guild() {
840
- return this.interaction.guild;
841
- }
842
- get guildId() {
843
- return this.interaction.guildId;
844
- }
845
- get channel() {
846
- return this.interaction.channel;
847
- }
848
- get channelId() {
849
- return this.interaction.channelId;
850
- }
851
- get locale() {
852
- return this.interaction.locale;
853
- }
854
- /** Whether the interaction is already deferred. */
855
- get deferred() {
856
- return this.interaction.deferred;
857
- }
858
- /** Whether the interaction already received an initial response. */
859
- get replied() {
860
- return this.interaction.replied;
861
- }
862
- /** Send the initial response to the interaction. */
863
- reply(input) {
864
- return this.interaction.reply(normalizeReply(input));
865
- }
866
- /** Reply, but always hidden to everyone except the invoking user. */
867
- replyEphemeral(input) {
868
- return this.reply(asEphemeral(input));
869
- }
870
- /** Acknowledge now and respond later via {@link editReply}. */
871
- defer(options = {}) {
872
- return this.interaction.deferReply(
873
- options.ephemeral ? { flags: MessageFlags.Ephemeral } : {}
874
- );
875
- }
876
- /** Edit the original (or deferred) response. */
877
- editReply(input) {
878
- return this.interaction.editReply(normalizeEdit(input));
879
- }
880
- /** Add an additional message after the initial response. */
881
- followUp(input) {
882
- return this.interaction.followUp(normalizeReply(input));
883
- }
884
- /**
885
- * State-aware send: replies, edits a deferred response, or follows up —
886
- * whichever is valid given the current interaction state. The single method
887
- * most handlers ever need.
888
- */
889
- async send(input) {
890
- if (this.interaction.deferred) {
891
- await this.editReply(input);
892
- } else if (this.interaction.replied) {
893
- await this.followUp(input);
894
- } else {
895
- await this.reply(input);
896
- }
897
- }
898
- /** State-aware ephemeral error message. */
899
- error(message) {
900
- return this.send(asEphemeral(message));
901
- }
902
- };
903
2183
  function makeOption(type, config) {
904
2184
  return { type, ...config, required: config.required ?? false };
905
2185
  }
@@ -1081,6 +2361,8 @@ var SlashCommand = class {
1081
2361
  autocompleter;
1082
2362
  /** Resolved cooldown configuration for this command, if any. */
1083
2363
  cooldown;
2364
+ /** Resolved guard list for this command, if any. */
2365
+ guards;
1084
2366
  /** @internal */
1085
2367
  constructor(spec) {
1086
2368
  this.name = spec.name;
@@ -1089,6 +2371,7 @@ var SlashCommand = class {
1089
2371
  this.executor = spec.executor;
1090
2372
  this.autocompleter = spec.autocompleter;
1091
2373
  this.cooldown = spec.cooldown;
2374
+ this.guards = spec.guards;
1092
2375
  }
1093
2376
  /** Serialise to the discord REST chat-input command payload. */
1094
2377
  toJSON() {
@@ -1123,7 +2406,7 @@ function makeAutocompleter(options) {
1123
2406
  if (!interaction.responded) await ctx.respond(choices);
1124
2407
  };
1125
2408
  }
1126
- function baseJSON(meta, options) {
2409
+ function baseJSON2(meta, options) {
1127
2410
  return {
1128
2411
  type: ApplicationCommandType.ChatInput,
1129
2412
  name: meta.name,
@@ -1163,11 +2446,12 @@ function command(config) {
1163
2446
  };
1164
2447
  return new SlashCommand({
1165
2448
  name: config.name,
1166
- json: baseJSON(config, leafOptionsJSON(options)),
2449
+ json: baseJSON2(config, leafOptionsJSON(options)),
1167
2450
  hasAutocomplete: optionsHaveAutocomplete(options),
1168
2451
  executor,
1169
2452
  autocompleter: makeAutocompleter(options),
1170
- cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0
2453
+ cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
2454
+ guards: config.guards
1171
2455
  });
1172
2456
  }
1173
2457
  function subcommand(config) {
@@ -1235,11 +2519,12 @@ function commandGroup(config) {
1235
2519
  };
1236
2520
  return new SlashCommand({
1237
2521
  name: config.name,
1238
- json: baseJSON(config, options),
2522
+ json: baseJSON2(config, options),
1239
2523
  hasAutocomplete,
1240
2524
  executor,
1241
2525
  autocompleter,
1242
- cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0
2526
+ cooldown: config.cooldown !== void 0 ? normalizeCooldown(config.cooldown) : void 0,
2527
+ guards: config.guards
1243
2528
  });
1244
2529
  }
1245
2530
  var CommandRegistry = class {
@@ -1248,6 +2533,7 @@ var CommandRegistry = class {
1248
2533
  logger;
1249
2534
  cooldowns;
1250
2535
  defaultCooldown;
2536
+ defaultGuards = [];
1251
2537
  onUsage;
1252
2538
  /** Register one or more commands. Later registrations override by name. */
1253
2539
  add(...commands) {
@@ -1290,6 +2576,11 @@ var CommandRegistry = class {
1290
2576
  this.defaultCooldown = defaultCooldown;
1291
2577
  return this;
1292
2578
  }
2579
+ /** Guards that run before every command's own guards. */
2580
+ setDefaultGuards(guards) {
2581
+ this.defaultGuards = guards;
2582
+ return this;
2583
+ }
1293
2584
  /** Attach a hook called after each successful command execution. */
1294
2585
  setUsageHook(hook) {
1295
2586
  this.onUsage = hook;
@@ -1308,17 +2599,35 @@ var CommandRegistry = class {
1308
2599
  });
1309
2600
  const cooldown = command2.cooldown ?? this.defaultCooldown;
1310
2601
  if (cooldown !== void 0 && this.cooldowns !== void 0) {
1311
- const result = this.cooldowns.consume(command2.name, cooldown, actorOf(interaction));
2602
+ const result = this.cooldowns.consume(command2.name, cooldown, actorOf2(interaction));
1312
2603
  if (!result.allowed) {
1313
2604
  await this.replyCooldown(interaction, cooldown, result.remaining);
1314
2605
  return;
1315
2606
  }
1316
2607
  }
2608
+ const guards = combineGuards3(this.defaultGuards, command2.guards);
2609
+ if (guards.length > 0) {
2610
+ const guardResult = await runGuards(interaction, guards);
2611
+ if (!guardResult.allowed) {
2612
+ this.logger?.debug("command denied", {
2613
+ data: {
2614
+ command: command2.name,
2615
+ user: interaction.user.id,
2616
+ reason: guardResult.reason ?? ""
2617
+ }
2618
+ });
2619
+ await this.replyDenied(interaction, guardResult.reason);
2620
+ return;
2621
+ }
2622
+ }
2623
+ const start = Date.now();
1317
2624
  try {
1318
2625
  await command2.execute(interaction);
1319
2626
  this.onUsage?.({
1320
2627
  type: "command",
1321
2628
  name: command2.name,
2629
+ outcome: "success",
2630
+ durationMs: Date.now() - start,
1322
2631
  userId: interaction.user.id,
1323
2632
  userTag: interaction.user.tag,
1324
2633
  guildId: interaction.guildId,
@@ -1327,6 +2636,18 @@ var CommandRegistry = class {
1327
2636
  });
1328
2637
  } catch (error) {
1329
2638
  const err = error instanceof Error ? error : new Error(String(error));
2639
+ this.onUsage?.({
2640
+ type: "command",
2641
+ name: command2.name,
2642
+ outcome: "error",
2643
+ errorMessage: err.message,
2644
+ durationMs: Date.now() - start,
2645
+ userId: interaction.user.id,
2646
+ userTag: interaction.user.tag,
2647
+ guildId: interaction.guildId,
2648
+ channelId: interaction.channelId,
2649
+ timestamp: /* @__PURE__ */ new Date()
2650
+ });
1330
2651
  if (this.errorHandler !== void 0) {
1331
2652
  await this.errorHandler(err, interaction);
1332
2653
  } else {
@@ -1388,8 +2709,28 @@ var CommandRegistry = class {
1388
2709
  } catch {
1389
2710
  }
1390
2711
  }
2712
+ async replyDenied(interaction, reason) {
2713
+ const embeds = clientEmbeds4(interaction.client);
2714
+ const text = reason ?? "You don't have permission to use this.";
2715
+ try {
2716
+ const payload = { embeds: [embeds.error(text)], flags: MessageFlags.Ephemeral };
2717
+ if (interaction.deferred) await interaction.editReply({ embeds: payload.embeds });
2718
+ else if (interaction.replied) await interaction.followUp(payload);
2719
+ else await interaction.reply(payload);
2720
+ } catch {
2721
+ }
2722
+ }
1391
2723
  };
1392
- function actorOf(interaction) {
2724
+ function combineGuards3(defaults, own) {
2725
+ if (own === void 0 || own.length === 0) return defaults;
2726
+ if (defaults.length === 0) return own;
2727
+ return [...defaults, ...own];
2728
+ }
2729
+ function clientEmbeds4(client) {
2730
+ const host = client;
2731
+ return host.embeds ?? defaultEmbeds;
2732
+ }
2733
+ function actorOf2(interaction) {
1393
2734
  const member = interaction.member;
1394
2735
  let roleIds = [];
1395
2736
  if (member !== null) {
@@ -1641,6 +2982,7 @@ var ComponentRegistry = class {
1641
2982
  errorHandler;
1642
2983
  logger;
1643
2984
  onUsage;
2985
+ defaultGuards = [];
1644
2986
  /** Register one or more components. Later registrations override by namespace. */
1645
2987
  add(...defs) {
1646
2988
  for (const def of defs) {
@@ -1685,6 +3027,11 @@ var ComponentRegistry = class {
1685
3027
  this.onUsage = hook;
1686
3028
  return this;
1687
3029
  }
3030
+ /** Guards that run before every component's own guards. */
3031
+ setDefaultGuards(guards) {
3032
+ this.defaultGuards = guards;
3033
+ return this;
3034
+ }
1688
3035
  /** Total number of registered components. */
1689
3036
  get size() {
1690
3037
  return this.buttons.size + this.stringSelects.size + this.userSelects.size + this.roleSelects.size + this.channelSelects.size + this.mentionableSelects.size + this.modals.size;
@@ -1720,13 +3067,27 @@ var ComponentRegistry = class {
1720
3067
  async exec(route, interaction) {
1721
3068
  if (route === void 0) return false;
1722
3069
  this.logger?.debug("component", { data: { customId: interaction.customId } });
3070
+ const guards = combineGuards4(this.defaultGuards, route.guards);
3071
+ if (guards.length > 0) {
3072
+ const guardResult = await runGuards(interaction, guards);
3073
+ if (!guardResult.allowed) {
3074
+ this.logger?.debug("component denied", {
3075
+ data: { customId: interaction.customId, reason: guardResult.reason ?? "" }
3076
+ });
3077
+ await replyDeniedComponent(interaction, guardResult.reason);
3078
+ return true;
3079
+ }
3080
+ }
1723
3081
  const { values } = parseCustomId(interaction.customId);
1724
3082
  const params = paramsFromValues(route.paramNames, values);
3083
+ const start = Date.now();
1725
3084
  try {
1726
3085
  await route.handle(interaction, params);
1727
3086
  this.onUsage?.({
1728
3087
  type: "component",
1729
3088
  name: route.namespace,
3089
+ outcome: "success",
3090
+ durationMs: Date.now() - start,
1730
3091
  userId: interaction.user.id,
1731
3092
  userTag: interaction.user.tag,
1732
3093
  guildId: interaction.guildId,
@@ -1735,6 +3096,18 @@ var ComponentRegistry = class {
1735
3096
  });
1736
3097
  } catch (error) {
1737
3098
  const err = toError3(error);
3099
+ this.onUsage?.({
3100
+ type: "component",
3101
+ name: route.namespace,
3102
+ outcome: "error",
3103
+ errorMessage: err.message,
3104
+ durationMs: Date.now() - start,
3105
+ userId: interaction.user.id,
3106
+ userTag: interaction.user.tag,
3107
+ guildId: interaction.guildId,
3108
+ channelId: interaction.channelId,
3109
+ timestamp: /* @__PURE__ */ new Date()
3110
+ });
1738
3111
  if (this.errorHandler !== void 0) {
1739
3112
  await this.errorHandler(err, interaction);
1740
3113
  } else {
@@ -1750,6 +3123,25 @@ var ComponentRegistry = class {
1750
3123
  function namespaceOf(customId) {
1751
3124
  return parseCustomId(customId).namespace;
1752
3125
  }
3126
+ function combineGuards4(defaults, own) {
3127
+ if (own === void 0 || own.length === 0) return defaults;
3128
+ if (defaults.length === 0) return own;
3129
+ return [...defaults, ...own];
3130
+ }
3131
+ function clientEmbeds5(client) {
3132
+ return client.embeds ?? defaultEmbeds;
3133
+ }
3134
+ async function replyDeniedComponent(interaction, reason) {
3135
+ const embeds = clientEmbeds5(interaction.client);
3136
+ const text = reason ?? "You don't have permission to use this.";
3137
+ try {
3138
+ const payload = { embeds: [embeds.error(text)], flags: MessageFlags.Ephemeral };
3139
+ if (interaction.deferred) await interaction.editReply({ embeds: payload.embeds });
3140
+ else if (interaction.replied) await interaction.followUp(payload);
3141
+ else await interaction.reply(payload);
3142
+ } catch {
3143
+ }
3144
+ }
1753
3145
  function resolveButtonStyle(input) {
1754
3146
  if (input === void 0) return ButtonStyle.Secondary;
1755
3147
  return typeof input === "number" ? input : ButtonStyle[input];
@@ -1761,6 +3153,7 @@ function button(config) {
1761
3153
  kind: "button",
1762
3154
  namespace: compiled.namespace,
1763
3155
  paramNames: compiled.paramNames,
3156
+ guards: config.guards,
1764
3157
  async handle(interaction, params) {
1765
3158
  await config.run(new ButtonContext(interaction, params));
1766
3159
  },
@@ -1793,6 +3186,7 @@ function stringSelect(config) {
1793
3186
  kind: "stringSelect",
1794
3187
  namespace: compiled.namespace,
1795
3188
  paramNames: compiled.paramNames,
3189
+ guards: config.guards,
1796
3190
  async handle(interaction, params) {
1797
3191
  await config.run(new StringSelectContext(interaction, params));
1798
3192
  },
@@ -1810,6 +3204,7 @@ function userSelect(config) {
1810
3204
  kind: "userSelect",
1811
3205
  namespace: compiled.namespace,
1812
3206
  paramNames: compiled.paramNames,
3207
+ guards: config.guards,
1813
3208
  async handle(interaction, params) {
1814
3209
  await config.run(new UserSelectContext(interaction, params));
1815
3210
  },
@@ -1827,6 +3222,7 @@ function roleSelect(config) {
1827
3222
  kind: "roleSelect",
1828
3223
  namespace: compiled.namespace,
1829
3224
  paramNames: compiled.paramNames,
3225
+ guards: config.guards,
1830
3226
  async handle(interaction, params) {
1831
3227
  await config.run(new RoleSelectContext(interaction, params));
1832
3228
  },
@@ -1844,6 +3240,7 @@ function channelSelect(config) {
1844
3240
  kind: "channelSelect",
1845
3241
  namespace: compiled.namespace,
1846
3242
  paramNames: compiled.paramNames,
3243
+ guards: config.guards,
1847
3244
  async handle(interaction, params) {
1848
3245
  await config.run(new ChannelSelectContext(interaction, params));
1849
3246
  },
@@ -1862,6 +3259,7 @@ function mentionableSelect(config) {
1862
3259
  kind: "mentionableSelect",
1863
3260
  namespace: compiled.namespace,
1864
3261
  paramNames: compiled.paramNames,
3262
+ guards: config.guards,
1865
3263
  async handle(interaction, params) {
1866
3264
  await config.run(new MentionableSelectContext(interaction, params));
1867
3265
  },
@@ -1904,6 +3302,7 @@ function modal(config) {
1904
3302
  kind: "modal",
1905
3303
  namespace: compiled.namespace,
1906
3304
  paramNames: compiled.paramNames,
3305
+ guards: config.guards,
1907
3306
  async handle(interaction, params) {
1908
3307
  const fields = {};
1909
3308
  for (const key of fieldKeys) {
@@ -2010,40 +3409,54 @@ var SpearClient = class extends Client {
2010
3409
  prefix = new PrefixRegistry();
2011
3410
  /** Usage tracker: records who used what to a store and/or a Discord channel. */
2012
3411
  usage = new UsageTracker();
3412
+ /** Preset embed factory used by `ctx.error/success/info/warn` and available to your code. */
3413
+ embeds;
3414
+ /** User- and message-context-menu command registry. */
3415
+ contextMenus = new ContextMenuRegistry();
2013
3416
  envConfig;
2014
3417
  constructor(options = {}) {
2015
- const { intents, logger, dotenv, cooldown, prefix, usage, ...rest } = options;
3418
+ const { intents, logger, dotenv, cooldown, prefix, usage, embeds, guards, ...rest } = options;
2016
3419
  super({ ...rest, intents: intents ?? Intents.default });
3420
+ this.embeds = embeds instanceof Embeds ? embeds : new Embeds(embeds);
2017
3421
  this.envConfig = dotenv === false ? false : dotenv === void 0 || dotenv === true ? {} : dotenv;
2018
3422
  this.logger = logger instanceof Logger ? logger : new Logger(logger);
2019
- this.commands.setLogger(this.logger.child("commands"));
2020
3423
  const defaultCooldown = cooldown !== void 0 ? normalizeCooldown(cooldown) : void 0;
3424
+ this.commands.setLogger(this.logger.child("commands"));
2021
3425
  this.commands.setCooldowns(this.cooldowns, defaultCooldown);
2022
3426
  this.components.setLogger(this.logger.child("components"));
2023
- this.events.attachAll(this);
2024
- this.on("interactionCreate", (interaction) => this.route(interaction));
2025
- this.on("error", (error) => this.logger.error("client error", { error: toError(error) }));
2026
- this.scheduler.setLogger(this.logger.child("scheduler"));
2027
- this.once("clientReady", () => this.scheduler.start(this));
3427
+ this.contextMenus.setLogger(this.logger.child("contextMenus"));
3428
+ this.contextMenus.setCooldowns(this.cooldowns, defaultCooldown);
2028
3429
  this.prefix.setLogger(this.logger.child("prefix"));
2029
3430
  this.prefix.setCooldowns(this.cooldowns, defaultCooldown);
2030
3431
  if (prefix !== void 0) this.prefix.setOptions(prefix);
2031
- this.on("messageCreate", (message) => {
2032
- void this.prefix.handle(message);
2033
- });
3432
+ this.scheduler.setLogger(this.logger.child("scheduler"));
3433
+ if (guards !== void 0 && guards.length > 0) {
3434
+ this.commands.setDefaultGuards(guards);
3435
+ this.contextMenus.setDefaultGuards(guards);
3436
+ this.components.setDefaultGuards(guards);
3437
+ this.prefix.setDefaultGuards(guards);
3438
+ }
2034
3439
  this.usage.setClient(this).setLogger(this.logger.child("usage"));
2035
3440
  if (usage !== void 0) {
2036
3441
  if (usage.store !== void 0) this.usage.setStore(usage.store);
2037
3442
  if (usage.channel !== void 0) this.usage.reportTo(usage.channel, usage.format);
2038
3443
  const onUsage = (event2) => this.usage.track(event2);
2039
3444
  this.commands.setUsageHook(onUsage);
3445
+ this.contextMenus.setUsageHook(onUsage);
2040
3446
  this.components.setUsageHook(onUsage);
2041
3447
  this.prefix.setUsageHook(onUsage);
2042
3448
  }
3449
+ this.events.attachAll(this);
3450
+ this.on("interactionCreate", (interaction) => this.route(interaction));
3451
+ this.on("error", (error) => this.logger.error("client error", { error: toError(error) }));
3452
+ this.once("clientReady", () => this.scheduler.start(this));
3453
+ this.on("messageCreate", (message) => {
3454
+ void this.prefix.handle(message);
3455
+ });
2043
3456
  }
2044
3457
  /**
2045
- * Register commands, events and components in one call. Each item is routed
2046
- * to the matching registry based on its kind.
3458
+ * Register commands, events, components, scheduled tasks, prefix commands
3459
+ * and context menus in one call. Each item is routed to its matching registry.
2047
3460
  */
2048
3461
  register(...items) {
2049
3462
  for (const item of items) {
@@ -2055,6 +3468,10 @@ var SpearClient = class extends Client {
2055
3468
  this.scheduler.add(item);
2056
3469
  } else if (item.kind === "prefixCommand") {
2057
3470
  this.prefix.add(item);
3471
+ } else if (item.kind === "userMenu") {
3472
+ this.contextMenus.add(item);
3473
+ } else if (item.kind === "messageMenu") {
3474
+ this.contextMenus.add(item);
2058
3475
  } else {
2059
3476
  this.components.add(item);
2060
3477
  }
@@ -2089,8 +3506,9 @@ var SpearClient = class extends Client {
2089
3506
  return this;
2090
3507
  }
2091
3508
  /**
2092
- * Push the registered slash commands to discord using the client's own
2093
- * authenticated REST connection. Call after the client is ready.
3509
+ * Push the registered slash commands to Discord using the client's REST
3510
+ * connection. Slash-only use {@link deployAllCommands} to include context
3511
+ * menus in the same request.
2094
3512
  */
2095
3513
  async deployCommands(options = {}) {
2096
3514
  const applicationId = this.application?.id ?? this.user?.id;
@@ -2099,14 +3517,36 @@ var SpearClient = class extends Client {
2099
3517
  }
2100
3518
  return this.commands.deploy({ rest: this.rest, applicationId, guildId: options.guildId });
2101
3519
  }
2102
- async route(interaction) {
2103
- if (interaction.isChatInputCommand()) {
2104
- await this.commands.handle(interaction);
2105
- } else if (interaction.isAutocomplete()) {
2106
- await this.commands.handleAutocomplete(interaction);
2107
- } else {
2108
- await this.components.handle(interaction);
3520
+ /**
3521
+ * Deploy slash commands AND context menus together to Discord in a single
3522
+ * PUT. Use this once you mix `userCommand` / `messageCommand` with `command`.
3523
+ */
3524
+ /**
3525
+ * Deploy slash commands AND context menus together. Supports a `diff`
3526
+ * strategy that fetches the remote set first and skips the PUT when
3527
+ * nothing changed, and a `dryRun` flag that returns the body without
3528
+ * touching Discord (perfect for CI deploy gates).
3529
+ *
3530
+ * @returns the API's DeployResult on PUT, or a skipped report when
3531
+ * `dryRun: true` is set OR `strategy: "diff"` finds no changes.
3532
+ */
3533
+ async deployAllCommands(options = {}) {
3534
+ const applicationId = options.applicationId ?? this.application?.id ?? this.user?.id;
3535
+ if (applicationId == null) {
3536
+ throw new Error("spearkit: deployAllCommands() must run after the client is ready");
3537
+ }
3538
+ const body = [...this.commands.toJSON(), ...this.contextMenus.toJSON()];
3539
+ const route = options.guildId !== void 0 ? Routes.applicationGuildCommands(applicationId, options.guildId) : Routes.applicationCommands(applicationId);
3540
+ if (options.dryRun === true) {
3541
+ return { skipped: true, reason: "dry-run", body };
3542
+ }
3543
+ if (options.strategy === "diff") {
3544
+ const remote = await this.rest.get(route);
3545
+ if (commandsEqual(body, remote)) {
3546
+ return { skipped: true, reason: "no-changes", body };
3547
+ }
2109
3548
  }
3549
+ return await this.rest.put(route, { body });
2110
3550
  }
2111
3551
  /** Define and register a scheduled task in one call. */
2112
3552
  schedule(config) {
@@ -2119,13 +3559,66 @@ var SpearClient = class extends Client {
2119
3559
  this.scheduler.stop();
2120
3560
  await super.destroy();
2121
3561
  }
3562
+ async route(interaction) {
3563
+ if (interaction.isChatInputCommand()) {
3564
+ await this.commands.handle(interaction);
3565
+ } else if (interaction.isAutocomplete()) {
3566
+ await this.commands.handleAutocomplete(interaction);
3567
+ } else if (interaction.isUserContextMenuCommand()) {
3568
+ await this.contextMenus.handleUser(interaction);
3569
+ } else if (interaction.isMessageContextMenuCommand()) {
3570
+ await this.contextMenus.handleMessage(interaction);
3571
+ } else {
3572
+ await this.components.handle(interaction);
3573
+ }
3574
+ }
2122
3575
  };
3576
+ function commandsEqual(local, remote) {
3577
+ if (local.length !== remote.length) return false;
3578
+ const lset = new Set(local.map(commandHash));
3579
+ for (const r of remote) {
3580
+ if (!lset.has(commandHash(r))) return false;
3581
+ }
3582
+ return true;
3583
+ }
3584
+ function commandHash(cmd) {
3585
+ const c = cmd;
3586
+ return JSON.stringify({
3587
+ name: c.name,
3588
+ type: c.type ?? 1,
3589
+ description: c.description ?? "",
3590
+ nsfw: c.nsfw === true,
3591
+ default_member_permissions: c.default_member_permissions ?? null,
3592
+ contexts: Array.isArray(c.contexts) ? [...c.contexts].sort((a, b) => a - b) : null,
3593
+ options: normaliseOptions(c.options)
3594
+ });
3595
+ }
3596
+ function normaliseOptions(options) {
3597
+ if (!Array.isArray(options)) return [];
3598
+ return options.map((o) => {
3599
+ const opt = o;
3600
+ return {
3601
+ name: opt.name,
3602
+ type: opt.type,
3603
+ description: opt.description ?? "",
3604
+ required: opt.required === true,
3605
+ choices: Array.isArray(opt.choices) ? [...opt.choices].map((ch) => ({ name: ch.name, value: ch.value })).sort((a, b) => String(a.name).localeCompare(String(b.name))) : null,
3606
+ options: normaliseOptions(opt.options),
3607
+ channel_types: Array.isArray(opt.channel_types) ? [...opt.channel_types].sort((a, b) => a - b) : null,
3608
+ min_value: opt.min_value ?? null,
3609
+ max_value: opt.max_value ?? null,
3610
+ min_length: opt.min_length ?? null,
3611
+ max_length: opt.max_length ?? null,
3612
+ autocomplete: opt.autocomplete === true
3613
+ };
3614
+ }).sort((a, b) => String(a.name).localeCompare(String(b.name)));
3615
+ }
2123
3616
 
2124
3617
  // src/plugin.ts
2125
3618
  function definePlugin(plugin) {
2126
3619
  return plugin;
2127
3620
  }
2128
3621
 
2129
- export { AutocompleteContext, BaseContext, ButtonContext, ChannelSelectContext, CommandContext, CommandRegistry, ComponentRegistry, CooldownManager, CronExpression, EventRegistry, Intents, JsonFileUsageStore, Logger, MAX_CUSTOM_ID_LENGTH, MemoryUsageStore, MentionableSelectContext, MessageComponentContext, ModalContext, PrefixContext, PrefixRegistry, RoleSelectContext, SlashCommand, SpearClient, StringSelectContext, TaskScheduler, UsageTracker, UserSelectContext, asEphemeral, buildCustomId, button, channelSelect, collectModules, command, commandGroup, compilePattern, consoleSink, cron, definePlugin, effectiveDuration, env, event, formatCooldownMessage, formatUsage, linkButton, loadEnv, loadInto, mentionableSelect, modal, normalizeCooldown, normalizeReply, option, optionsHaveAutocomplete, paramsFromValues, parseCustomId, parseEnv, prefixCommand, readOption, roleSelect, row, stringSelect, subcommand, subcommandGroup, task, textInput, toAPIOption, toError, userSelect };
3622
+ export { AutocompleteContext, BaseContext, ButtonContext, ChannelSelectContext, CommandContext, CommandRegistry, ComponentRegistry, ContextMenuRegistry, CooldownManager, CronExpression, DEFAULT_EMBED_COLORS, DEFAULT_EMBED_ICONS, Embeds, EventRegistry, Intents, JsonFileUsageStore, KeyedLock, Logger, MAX_CUSTOM_ID_LENGTH, MemoryCache, MemoryUsageStore, MentionableSelectContext, MessageComponentContext, MessageContextMenuContext, ModalContext, PrefixArgsBuilder, PrefixContext, PrefixRegistry, RoleSelectContext, SlashCommand, SpearClient, StringSelectContext, TaskScheduler, UsageTracker, UserContextMenuContext, UserSelectContext, asEphemeral, buildCustomId, buildPaginatorPage, button, channelSelect, collectModules, command, commandGroup, compilePattern, confirm, consoleSink, createCache, cron, defaultEmbeds, definePlugin, denied, discordTimestamp, dmOnly, effectiveDuration, env, event, fetchChannel, fetchGuild, fetchMember, fetchMessage, fetchRole, fetchUser, formatCooldownMessage, formatDuration, formatUsage, guard, guildOnly, jsonlSink, linkButton, loadConfig, loadConfigAsync, loadEnv, loadInto, lookup, lookupOptional, mentionableSelect, messageCommand, modal, normalizeCooldown, normalizeReply, option, optionsHaveAutocomplete, paginate, paramsFromValues, parseCustomId, parseDuration, parseEnv, prefixArgs, prefixCommand, readOption, relativeTimestamp, requireAllRoles, requireAnyRole, requireBotPermissions, requireOwner, requireUserPermissions, roleSelect, row, runGuards, safeFetch, safeTry, stringSelect, subcommand, subcommandGroup, task, textInput, toAPIOption, toError, userCommand, userSelect, webhookSink, withSafeTimeout };
2130
3623
  //# sourceMappingURL=index.js.map
2131
3624
  //# sourceMappingURL=index.js.map