@seedcord/core 0.10.0 → 0.11.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.
Files changed (63) hide show
  1. package/README.md +27 -9
  2. package/dist/CoordinatedShutdown-H5YjZn8A.d.mts +78 -0
  3. package/dist/CoordinatedStartup-CGnJrqcl.mjs +168 -0
  4. package/dist/CoordinatedStartup-CGnJrqcl.mjs.map +1 -0
  5. package/dist/{HmrModuleHandler-DOAtah3j.mjs → HmrModuleHandler-8ZC-UuZh.mjs} +9 -22
  6. package/dist/HmrModuleHandler-8ZC-UuZh.mjs.map +1 -0
  7. package/dist/PluginHost-B8kxLOeb.mjs +245 -0
  8. package/dist/PluginHost-B8kxLOeb.mjs.map +1 -0
  9. package/dist/{Plugin-Dhx_ivqj.d.mts → PluginHost-DB4j8unu.d.mts} +110 -17
  10. package/dist/PluginLifecycle-AhQ46NIr.mjs +158 -0
  11. package/dist/PluginLifecycle-AhQ46NIr.mjs.map +1 -0
  12. package/dist/ServerHost-CtmAhEXn.mjs +168 -0
  13. package/dist/ServerHost-CtmAhEXn.mjs.map +1 -0
  14. package/dist/ServerHost-DYeh0KTk.d.mts +40 -0
  15. package/dist/bindViteEnv-BgFewlZ0.d.mts +11 -0
  16. package/dist/bindViteEnv-DWq5rftJ.mjs +88 -0
  17. package/dist/bindViteEnv-DWq5rftJ.mjs.map +1 -0
  18. package/dist/env-internal.index.d.mts +2 -0
  19. package/dist/env-internal.index.mjs +3 -0
  20. package/dist/hmr.index.d.mts +0 -1
  21. package/dist/hmr.index.mjs +1 -1
  22. package/dist/{cursor-BTU5bGoo.d.mts → index-Du71Xx69.d.mts} +253 -6
  23. package/dist/index.d.mts +19 -84
  24. package/dist/index.mjs +17 -37
  25. package/dist/index.mjs.map +1 -1
  26. package/dist/internal.index.d.mts +186 -22
  27. package/dist/internal.index.mjs +714 -19
  28. package/dist/internal.index.mjs.map +1 -1
  29. package/dist/node-internal.index.d.mts +25 -70
  30. package/dist/node-internal.index.mjs +83 -297
  31. package/dist/node-internal.index.mjs.map +1 -1
  32. package/dist/node.index.d.mts +3 -2
  33. package/dist/node.index.mjs +53 -200
  34. package/dist/node.index.mjs.map +1 -1
  35. package/dist/{phases-D_zQBXga.d.mts → phases-FTM69J3a.d.mts} +4 -4
  36. package/dist/plugin.index.d.mts +2 -2
  37. package/dist/plugin.index.mjs +2 -2
  38. package/dist/publishDefault-D3dAyzZ9.mjs +7 -0
  39. package/dist/publishDefault-D3dAyzZ9.mjs.map +1 -0
  40. package/dist/subscribers-Ce3SWy23.mjs +1913 -0
  41. package/dist/subscribers-Ce3SWy23.mjs.map +1 -0
  42. package/package.json +24 -17
  43. package/dist/Bus-BxPa-Uvp.mjs +0 -866
  44. package/dist/Bus-BxPa-Uvp.mjs.map +0 -1
  45. package/dist/HmrModuleHandler-DOAtah3j.mjs.map +0 -1
  46. package/dist/Pluggable-CwKbLui3.d.mts +0 -162
  47. package/dist/Pluggable-afy2H0Dr.mjs +0 -339
  48. package/dist/Pluggable-afy2H0Dr.mjs.map +0 -1
  49. package/dist/Plugin-CmNW5n6a.mjs +0 -122
  50. package/dist/Plugin-CmNW5n6a.mjs.map +0 -1
  51. package/dist/contextMenuLeaves-COzDYa6c.d.mts +0 -17
  52. package/dist/cursor-CgNBnJfu.mjs +0 -802
  53. package/dist/cursor-CgNBnJfu.mjs.map +0 -1
  54. package/dist/devChannel-CQU7hq7Q.mjs +0 -14
  55. package/dist/devChannel-CQU7hq7Q.mjs.map +0 -1
  56. package/dist/index-B_-EjyvS.d.mts +0 -135
  57. package/dist/publishDefault-BuSZPbjT.mjs +0 -14
  58. package/dist/publishDefault-BuSZPbjT.mjs.map +0 -1
  59. package/dist/slashRouteLeaves-k1hJnXDO.mjs +0 -31
  60. package/dist/slashRouteLeaves-k1hJnXDO.mjs.map +0 -1
  61. package/dist/subscribers-xGy7a7xR.mjs +0 -167
  62. package/dist/subscribers-xGy7a7xR.mjs.map +0 -1
  63. package/dist/types-BG0BimVr.d.mts +0 -13
@@ -1,15 +1,84 @@
1
- import { A as translateSerializationError, B as contextMenuRouteOf, C as NotInDm, E as OnCooldown, F as checkAckLegality, G as decodeComponentRoute, H as storeComponentRoute, I as sendTarget, K as NoticeCard, N as reportedWrite, P as AckTrace, R as interactionMiddlewareMetaOf, S as NotAllowed, T as NotOwner, U as storeInteractionRoute, V as interactionRoutesOf, W as ComponentDefsKey, _ as GateNotice, b as MissingRole, d as pickNotice, f as discardCommits, g as runCommits, h as runCheck, k as serializeReply, n as pageCursor, q as Notice, r as Silence, s as PermissionNames, t as PAGE_MAX, v as HasDangerousPermissions, w as NotInGuild, x as NeedsAny, y as MissingPermissions, z as areRoutes } from "./cursor-CgNBnJfu.mjs";
2
- import { C as InteractionRouteKeys, D as clearStore, E as accessorStore, N as bindBotColor, O as guardedAccessor, S as InteractionMiddlewareMetadataKey, T as WebhookUrlMetadataKey, _ as EventMetadataKey, a as RegisterSubscriber, c as VerifyWebhooks, g as CommandMetadataKey, h as sendFlags, i as RegisterDefaults, k as contextMenuLeaves, m as deferFlags, n as busLoggerOf, o as RegisteredCount, p as ReportThrottle, r as registrationFor, s as UnregisterSubscriber, v as EventMiddlewareMetadataKey, w as SubscribeMetadataKey, x as InteractionMetadataKey, y as GatedMetadataKey } from "./Bus-BxPa-Uvp.mjs";
3
- import { n as asError, t as PublishDefault } from "./publishDefault-BuSZPbjT.mjs";
4
- import { n as setDevChannel, t as getDevChannel } from "./devChannel-CQU7hq7Q.mjs";
5
- import { n as isCommandClass, t as slashRouteLeaves } from "./slashRouteLeaves-k1hJnXDO.mjs";
1
+ import { At as guardedAccessor, B as NotInGuild, Bt as isBuilderComponentClass, C as PermissionNames, Ct as warnLinks, D as pickNotice, Dt as linkCommands, Et as indexById, F as MissingPermissions, Ft as skippedNote, G as serializeReply, H as OnCooldown, Ht as Notice, I as MissingRole, It as contextMenuLeaves, K as translateSerializationError, L as NeedsAny, Lt as NoticeCard, M as runCommits, Mt as commandsRoute, N as GateNotice, Nt as globalFirst, O as discardCommits, Ot as accessorStore, P as HasDangerousPermissions, Pt as scopeIdsOf, Q as sendTarget, R as NotAllowed, St as installLinks, Tt as commandRefOf, V as NotOwner, Vt as bindBotColor, X as AckTrace, Y as reportedWrite, Z as checkAckLegality, _ as pageCursor, _t as SubscribeMetadataKey, a as RegisterSubscriber, at as ComponentDefsKey, c as VerifyWebhooks, dt as EventMiddlewareMetadataKey, et as interactionMiddlewareMetaOf, f as Subscriber, ft as GatedMetadataKey, g as PAGE_MAX, gt as InteractionRouteKeys, h as sendFlags, ht as InteractionMiddlewareMetadataKey, i as RegisterDefaults, it as storeInteractionRoute, j as runCheck, jt as GLOBAL_ID, kt as clearStore, lt as CommandMetadataKey, m as deferFlags, mt as InteractionMetadataKey, n as busLoggerOf, nt as interactionRoutesOf, o as RegisteredCount, ot as decodeComponentRoute, p as ReportThrottle, r as registrationFor, rt as storeComponentRoute, s as UnregisterSubscriber, st as ConfigGuilds, tt as contextMenuRouteOf, ut as EventMetadataKey, v as ResolvedEmoji, vt as WebhookUrlMetadataKey, wt as commandKey, y as Silence, yt as CommandInjector, z as NotInDm } from "./subscribers-Ce3SWy23.mjs";
2
+ import { t as PublishDefault } from "./publishDefault-D3dAyzZ9.mjs";
3
+ import { c as setDevChannel, i as FixedScope, n as attachmentsOf, r as sealAttachments, s as getDevChannel } from "./PluginHost-B8kxLOeb.mjs";
4
+ import { t as HmrModuleHandler } from "./HmrModuleHandler-8ZC-UuZh.mjs";
5
+ import { i as logUnknownFault, t as PluginLifecycle } from "./PluginLifecycle-AhQ46NIr.mjs";
6
+ import { t as CoordinatedStartup } from "./CoordinatedStartup-CGnJrqcl.mjs";
7
+ import { t as bindEnv } from "./bindViteEnv-DWq5rftJ.mjs";
6
8
  import "reflect-metadata";
9
+ import { SlashCommandBuilder } from "@discordjs/builders";
10
+ import { Routes } from "discord-api-types/v10";
7
11
  import { SeedcordErrorCode, paint } from "@seedcord/errors";
8
- import { SeedcordError } from "@seedcord/errors/internal";
9
- import { Logger } from "@seedcord/logger";
10
- import { timestampFromSnowflake } from "@seedcord/utils";
12
+ import { SeedcordError, SeedcordTypeError, throwSingleOrAggregate, validateDiscordToken } from "@seedcord/errors/internal";
13
+ import { LEVEL_COLOR, Logger, LoggerChannelRegistry } from "@seedcord/logger";
14
+ import { asError, botFolder, isPlainObject, pluralize, routeLeavesOf, unlessAborted } from "@seedcord/utils/internal";
15
+ import { filterCirculars, formatFilePath, importFiles, timestampFromSnowflake } from "@seedcord/utils";
11
16
  import { Envapter } from "envapt";
17
+ import { DiscordAPIError, HTTPError, RESTEvents } from "@discordjs/rest";
18
+ import { format } from "node:util";
19
+ import { CONSOLE_LEVEL, consoleRecord, routeConsole, traceError } from "@seedcord/logger/internal";
12
20
 
21
+ //#region src/subscribers/SubscriberLoader.ts
22
+ var SubscriberLoader = class {
23
+ bus;
24
+ directory;
25
+ isInitialized = false;
26
+ hmrHandler;
27
+ constructor(bus, directory) {
28
+ this.bus = bus;
29
+ this.directory = directory;
30
+ if (!this.directory) return;
31
+ if (!Envapter.isDevelopment && !Envapter.isTest) return;
32
+ this.hmrHandler = new HmrModuleHandler({
33
+ handlersDir: this.directory,
34
+ isHandler: this.isSubscriber.bind(this),
35
+ registerHandler: this.registerSubscriber.bind(this),
36
+ unregisterHandler: this.unregisterSubscriber.bind(this),
37
+ getArtifacts: this.getArtifacts.bind(this),
38
+ logger: busLoggerOf(this.bus)
39
+ });
40
+ }
41
+ /** @internal */
42
+ async init() {
43
+ if (this.isInitialized) return;
44
+ this.isInitialized = true;
45
+ this.bus[RegisterDefaults]();
46
+ const { directory } = this;
47
+ if (directory) {
48
+ busLoggerOf(this.bus).debug(paint.mute(directory));
49
+ await this.load(directory);
50
+ busLoggerOf(this.bus).utils.list([`${paint.iris.bold(this.bus[RegisteredCount])} subscribers`], paint.mint.bold("Loaded"), "debug");
51
+ }
52
+ if (!Envapter.isTest) await this.bus[VerifyWebhooks]();
53
+ }
54
+ /** @internal */
55
+ async onHmr(event) {
56
+ if (this.hmrHandler) await this.hmrHandler.handle(event);
57
+ }
58
+ async load(dir) {
59
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const value of Object.values(imported)) {
60
+ if (!this.isSubscriber(value)) continue;
61
+ this.registerSubscriber(value);
62
+ this.hmrHandler?.trackHandler(fullPath, value);
63
+ busLoggerOf(this.bus).utils.registration(value.name, relativePath, void 0, "trace");
64
+ }
65
+ }
66
+ registerSubscriber(ctor) {
67
+ this.bus[RegisterSubscriber](registrationFor(ctor));
68
+ }
69
+ unregisterSubscriber(ctor, artifacts) {
70
+ this.bus[UnregisterSubscriber](ctor, artifacts);
71
+ }
72
+ getArtifacts(ctor) {
73
+ return [Reflect.getMetadata(SubscribeMetadataKey, ctor).subscriber];
74
+ }
75
+ isSubscriber(value) {
76
+ if (typeof value !== "function") return false;
77
+ return value.prototype instanceof Subscriber && Reflect.hasMetadata(SubscribeMetadataKey, value);
78
+ }
79
+ };
80
+
81
+ //#endregion
13
82
  //#region src/decorators/brands.ts
14
83
  const SlashRouteBrand = Symbol("seedcord:brand:slash-route");
15
84
  const AutocompleteRouteBrand = Symbol("seedcord:brand:autocomplete-route");
@@ -49,14 +118,11 @@ var MiddlewareRegistry = class {
49
118
  /**
50
119
  * Adds a decorated middleware class and returns what it registered with. Returns `undefined` for an
51
120
  * undecorated class or one already registered.
52
- *
53
- * @throws A **SeedcordError** If another class with the same name is registered.
54
121
  */
55
122
  register(ctor) {
56
123
  const registration = this.registrationOf(ctor);
57
124
  if (!registration) return void 0;
58
125
  if (this.entries.some((entry) => entry.ctor === ctor)) return void 0;
59
- if (this.entries.some((entry) => entry.ctor.name === ctor.name)) throw new SeedcordError(SeedcordErrorCode.DuplicateMiddleware, [ctor.name]);
60
126
  this.entries.push({
61
127
  ctor,
62
128
  priority: registration.priority,
@@ -143,31 +209,534 @@ function isEmojiTuple(value) {
143
209
  return Array.isArray(value) && value.length === 2 && typeof value[0] === "string" && typeof value[1] === "string";
144
210
  }
145
211
 
212
+ //#endregion
213
+ //#region src/miscellaneous/currentApplication.ts
214
+ async function currentApplication(rest) {
215
+ return await rest.get(Routes.currentApplication());
216
+ }
217
+
218
+ //#endregion
219
+ //#region src/miscellaneous/readBotToken.ts
220
+ function readBotToken() {
221
+ return validateDiscordToken(Envapter.get("DISCORD_BOT_TOKEN"));
222
+ }
223
+
224
+ //#endregion
225
+ //#region src/miscellaneous/resolveEmojis.ts
226
+ function byName(list) {
227
+ const map = /* @__PURE__ */ new Map();
228
+ for (const entry of list) if (entry.name) map.set(entry.name, entry);
229
+ return map;
230
+ }
231
+ var EmojiResolver = class {
232
+ rest;
233
+ applicationId;
234
+ resolved = accessorStore();
235
+ problems = [];
236
+ guilds = /* @__PURE__ */ new Map();
237
+ application;
238
+ constructor(rest, applicationId) {
239
+ this.rest = rest;
240
+ this.applicationId = applicationId;
241
+ }
242
+ async resolve(configured) {
243
+ for (const [key, value] of Object.entries(configured)) if (isEmojiTuple(value)) await this.fromGuild(key, value);
244
+ else if (typeof value === "string") await this.fromApplication(key, value);
245
+ else this.problems.push(new SeedcordTypeError(SeedcordErrorCode.ConfigEmojiInvalid, [key]));
246
+ throwSingleOrAggregate(this.problems, SeedcordErrorCode.ConfigEmojiUnresolved);
247
+ return this.resolved;
248
+ }
249
+ async fromApplication(key, name) {
250
+ this.application ??= await this.applicationEmojis();
251
+ if (Error.isError(this.application)) {
252
+ this.unreadable(key, name, "the application", this.application, `Check that ${paint.bold("DISCORD_BOT_TOKEN")} is still valid.`);
253
+ return;
254
+ }
255
+ this.pick(key, name, this.application, "the application");
256
+ }
257
+ async applicationEmojis() {
258
+ try {
259
+ return byName((await this.rest.get(Routes.applicationEmojis(this.applicationId))).items);
260
+ } catch (error) {
261
+ return asError(error);
262
+ }
263
+ }
264
+ async fromGuild(key, [name, guildId]) {
265
+ let guild = this.guilds.get(guildId);
266
+ if (!guild) {
267
+ guild = await this.guildEmojis(guildId);
268
+ this.guilds.set(guildId, guild);
269
+ }
270
+ if (Error.isError(guild)) {
271
+ this.unreadable(key, name, `guild ${guildId}`, guild, "Check that the bot is in that guild.");
272
+ return;
273
+ }
274
+ this.pick(key, name, guild, `guild ${guildId}`);
275
+ }
276
+ async guildEmojis(guildId) {
277
+ try {
278
+ return byName(await this.rest.get(Routes.guildEmojis(guildId)));
279
+ } catch (error) {
280
+ return asError(error);
281
+ }
282
+ }
283
+ pick(key, name, source, where) {
284
+ const found = source.get(name);
285
+ if (!found?.id) {
286
+ this.problems.push(new SeedcordError(SeedcordErrorCode.ConfigEmojiNotFound, [
287
+ key,
288
+ name,
289
+ where
290
+ ]));
291
+ return;
292
+ }
293
+ this.resolved[key] = new ResolvedEmoji(name, found.id, found.animated ?? false);
294
+ }
295
+ unreadable(key, name, where, cause, fix) {
296
+ this.problems.push(new SeedcordError(SeedcordErrorCode.ConfigEmojiSourceUnreadable, [
297
+ key,
298
+ name,
299
+ where,
300
+ cause.message,
301
+ fix
302
+ ], { cause }));
303
+ }
304
+ };
305
+ function resolveEmojis(rest, applicationId, configured) {
306
+ return new EmojiResolver(rest, applicationId).resolve(configured);
307
+ }
308
+
309
+ //#endregion
310
+ //#region src/bakedIds.ts
311
+ const BAKED_IDS_KEY = "seedcord:core:baked-ids";
312
+
313
+ //#endregion
314
+ //#region src/commands/guildSummaries.ts
315
+ async function guildName(rest, guildId) {
316
+ try {
317
+ return (await rest.get(Routes.guild(guildId))).name;
318
+ } catch {
319
+ return guildId;
320
+ }
321
+ }
322
+ function guildSummaries(rest, guildIds, knownNames = /* @__PURE__ */ new Map()) {
323
+ return Promise.all(Iterator.from(guildIds).map(async (id) => ({
324
+ id,
325
+ name: knownNames.get(id) ?? await guildName(rest, id)
326
+ })));
327
+ }
328
+
329
+ //#endregion
330
+ //#region src/commands/DeployReporter.ts
331
+ var DeployReporter = class {
332
+ logger;
333
+ guildNames = /* @__PURE__ */ new Map();
334
+ constructor(logger) {
335
+ this.logger = logger;
336
+ }
337
+ async readGuildNames(rest, scopes, signal) {
338
+ if (!getDevChannel()) return;
339
+ const unread = scopes.filter((scope) => scope !== "global" && !this.guildNames.has(scope));
340
+ for (const { id, name } of await unlessAborted(guildSummaries(rest, unread), signal)) this.guildNames.set(id, name);
341
+ }
342
+ skipped(scopes) {
343
+ if (scopes.length > 0) this.logger.info(skippedNote(scopes, (guildId) => this.guildLabel(guildId)));
344
+ }
345
+ deployed(scope, commands) {
346
+ const { utils } = this.logger;
347
+ const count = paint.iris.bold(commands.length);
348
+ const tag = pluralize(commands.length, "command");
349
+ const title = scope === "global" ? `Deployed ${count} global ${tag}` : `Deployed ${count} ${tag} to ${this.guildLabel(scope)}`;
350
+ utils.block(title, utils.wrap(commands.map((command) => command.name)), "info");
351
+ }
352
+ guildLabel(guildId) {
353
+ return paint.sky.bold(this.guildNames.get(guildId) ?? guildId);
354
+ }
355
+ };
356
+
357
+ //#endregion
358
+ //#region src/commands/deployTarget.ts
359
+ function resolveDeployTarget(meta, configured, commandName) {
360
+ if (meta.scope === "global") return { scope: "global" };
361
+ if (meta.scope === "config") return configured.length > 0 ? guildTarget(configured) : { scope: "global" };
362
+ if (configured.length === 0 && meta.guilds.includes("config")) throw new SeedcordTypeError(SeedcordErrorCode.CoreCommandGuildsEmpty, [commandName]);
363
+ return guildTarget(meta.guilds.flatMap((target) => target === "config" ? configured : target));
364
+ }
365
+ function guildTarget(guilds) {
366
+ return {
367
+ scope: "guild",
368
+ guilds: [...new Set(guilds)]
369
+ };
370
+ }
371
+
372
+ //#endregion
373
+ //#region src/commands/isCommandClass.ts
374
+ /** @internal */
375
+ function isCommandClass(value) {
376
+ return isBuilderComponentClass(value) && Reflect.hasMetadata(CommandMetadataKey, value);
377
+ }
378
+
379
+ //#endregion
380
+ //#region src/commands/LastDeploy.ts
381
+ const ANSWER_TIMEOUT_MS = 5e3;
382
+ var LastDeploy = class {
383
+ saved;
384
+ read(applicationId, signal) {
385
+ this.saved ??= askSeedcordDev(applicationId);
386
+ return unlessAborted(this.saved, signal);
387
+ }
388
+ save(deployed) {
389
+ this.saved = Promise.resolve(deployed);
390
+ getDevChannel()?.send("seedcord:commands-deployed", deployed);
391
+ }
392
+ };
393
+ function askSeedcordDev(applicationId) {
394
+ const cli = getDevChannel();
395
+ if (!cli) return Promise.resolve(void 0);
396
+ const { promise, resolve } = Promise.withResolvers();
397
+ const settle = (deployed) => {
398
+ clearTimeout(timer);
399
+ cli.off("seedcord:last-deploy", onAnswer);
400
+ resolve(deployed);
401
+ };
402
+ const onAnswer = ({ deployed }) => settle(deployed);
403
+ const timer = setTimeout(() => settle(void 0), ANSWER_TIMEOUT_MS);
404
+ cli.on("seedcord:last-deploy", onAnswer);
405
+ cli.send("seedcord:read-deployed", { applicationId });
406
+ return promise;
407
+ }
408
+
409
+ //#endregion
410
+ //#region src/commands/planDeploy.ts
411
+ const HEX = 16;
412
+ function byCodePoint(a, b) {
413
+ return a < b ? -1 : Number(a > b);
414
+ }
415
+ async function hashOf(commands) {
416
+ const sorted = commands.toSorted((a, b) => byCodePoint(commandKey(a), commandKey(b)));
417
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(JSON.stringify(sorted)));
418
+ return [...new Uint8Array(digest)].map((byte) => byte.toString(HEX).padStart(2, "0")).join("");
419
+ }
420
+ async function scopeHashes(scopes) {
421
+ const hashed = [...scopes].map(async ([scope, commands]) => [scope, await hashOf(commands)]);
422
+ return Object.fromEntries(await Promise.all(hashed));
423
+ }
424
+ function sameCommandNames(inCode, deployed) {
425
+ const onDiscord = new Set(deployed?.values().map(commandKey));
426
+ return new Set(inCode.map(commandKey)).symmetricDifference(onDiscord).size === 0;
427
+ }
428
+ async function planDeploy(scopes, last, readUnchanged) {
429
+ const hashes = await scopeHashes(scopes);
430
+ const unchanged = globalFirst(scopes).filter(([scope]) => hashes[scope] === last?.[scope]);
431
+ const onDiscord = await readUnchanged(unchanged.map(([scope]) => scope));
432
+ const skipped = new Set(unchanged.filter(([scope, commands]) => sameCommandNames(commands, onDiscord.get(scope))).map(([scope]) => scope));
433
+ return {
434
+ hashes,
435
+ skippedHashes: Object.fromEntries(Object.entries(hashes).filter(([scope]) => skipped.has(scope))),
436
+ toPut: new Map(scopes.entries().filter(([scope]) => !skipped.has(scope))),
437
+ skipped: [...skipped],
438
+ onDiscord
439
+ };
440
+ }
441
+
442
+ //#endregion
443
+ //#region src/commands/putCommands.ts
444
+ const MS_PER_SECOND = 1e3;
445
+ async function putCommands(scopes, options) {
446
+ const result = /* @__PURE__ */ new Map();
447
+ for (const [scope, commands] of globalFirst(scopes)) {
448
+ result.set(scope, indexById(await putScope(scope, commands, options)));
449
+ options.onDeployed?.(scope, commands);
450
+ }
451
+ return result;
452
+ }
453
+ async function putScope(scope, commands, options) {
454
+ try {
455
+ return await put(commandsRoute(options.applicationId, scope), commands, options);
456
+ } catch (error) {
457
+ if (options.signal?.aborted) throw error;
458
+ const refusal = [asError(error).message, statusOf(error)];
459
+ throw scope === "global" ? new SeedcordError(SeedcordErrorCode.CoreCommandGlobalDeployFailed, refusal, { cause: error }) : new SeedcordError(SeedcordErrorCode.CoreCommandGuildDeployFailed, [scope, ...refusal], { cause: error });
460
+ }
461
+ }
462
+ function statusOf(error) {
463
+ return error instanceof DiscordAPIError || error instanceof HTTPError ? error.status : void 0;
464
+ }
465
+ async function put(route, body, { rest, logger, signal }) {
466
+ const reportWait = (info) => {
467
+ if (info.method !== "PUT" || !info.url.endsWith(route)) return;
468
+ const seconds = Math.ceil(info.timeToReset / MS_PER_SECOND);
469
+ logger.warn(`Discord rate-limited the command deploy. It continues in ${paint.amber(`${seconds}s`)}.`);
470
+ };
471
+ rest.on(RESTEvents.RateLimited, reportWait);
472
+ try {
473
+ return await unlessAborted(rest.put(route, {
474
+ body,
475
+ signal
476
+ }), signal);
477
+ } finally {
478
+ rest.off(RESTEvents.RateLimited, reportWait);
479
+ }
480
+ }
481
+
482
+ //#endregion
483
+ //#region src/commands/readCommands.ts
484
+ async function readCommandsAt(scope, { rest, applicationId, signal }) {
485
+ const route = commandsRoute(applicationId, scope);
486
+ try {
487
+ return indexById(await rest.get(route, { signal }));
488
+ } catch (error) {
489
+ if (signal?.aborted) throw error;
490
+ const reason = asError(error).message;
491
+ throw new SeedcordError(SeedcordErrorCode.CoreDiscordReadFailed, scope === "global" ? ["the commands deployed globally", reason] : [
492
+ `the commands deployed to guild ${paint.sky(scope)}`,
493
+ reason,
494
+ `Check that ${paint.bold("DISCORD_BOT_TOKEN")} is still valid and that the bot is in that guild.`
495
+ ], { cause: error });
496
+ }
497
+ }
498
+ async function readCommands(scopes, options) {
499
+ const read = Iterator.from(scopes).map(async (scope) => [scope, await readCommandsAt(scope, options)]);
500
+ return new Map(await Promise.all(read));
501
+ }
502
+
503
+ //#endregion
504
+ //#region src/commands/slashRouteLeaves.ts
505
+ /**
506
+ * dedupes route keys since the same builder appears once per guild.
507
+ * these come from the same {@link routeLeavesOf} that `seedcord codegen` reads, so the boot-time
508
+ * check compares against the generated registry's keys.
509
+ *
510
+ * @internal
511
+ */
512
+ function slashRouteLeaves(commands) {
513
+ const leaves = /* @__PURE__ */ new Set();
514
+ for (const command of commands) {
515
+ if (!(command instanceof SlashCommandBuilder)) continue;
516
+ for (const leaf of routeLeavesOf(command.toJSON())) leaves.add(leaf.route);
517
+ }
518
+ return leaves;
519
+ }
520
+
521
+ //#endregion
522
+ //#region src/commands/CommandRegistry.ts
523
+ const COMMAND_DEPLOY_TIMEOUT_MS = 75e3;
524
+ /**
525
+ * Scans the command directory, builds each command, and PUTs the global and guild scopes to Discord.
526
+ *
527
+ * @internal
528
+ */
529
+ var CommandRegistry = class {
530
+ core;
531
+ logger = new Logger("Commands", { channel: "commands" });
532
+ isInitialised = false;
533
+ scopedCommands = /* @__PURE__ */ new Map();
534
+ ctorToCommand = /* @__PURE__ */ new Map();
535
+ loading = false;
536
+ loadedCommands = [];
537
+ hmrHandler;
538
+ pendingEvents = /* @__PURE__ */ new Map();
539
+ injector = new CommandInjector();
540
+ lastDeploy = new LastDeploy();
541
+ reporter = new DeployReporter(this.logger);
542
+ dir;
543
+ configuredGuilds;
544
+ constructor(core) {
545
+ this.core = core;
546
+ const { commands } = core.config.bot;
547
+ if (!commands.path) throw new SeedcordError(SeedcordErrorCode.CoreControllerPathMissing, ["CommandRegistry", "commands"]);
548
+ this.dir = commands.path;
549
+ this.configuredGuilds = commands.guilds ?? [];
550
+ if (!Envapter.isDevelopment && !Envapter.isTest) return;
551
+ this.hmrHandler = new HmrModuleHandler({
552
+ handlersDir: this.dir,
553
+ isHandler: isCommandClass,
554
+ registerHandler: this.registerCommand.bind(this),
555
+ unregisterHandler: this.unregisterCommand.bind(this),
556
+ getArtifacts: this.getArtifacts.bind(this),
557
+ logger: this.logger
558
+ });
559
+ }
560
+ getArtifacts(ctor) {
561
+ return this.ctorToCommand.get(ctor);
562
+ }
563
+ /** @internal */
564
+ async init() {
565
+ if (this.isInitialised) return;
566
+ this.isInitialised = true;
567
+ this.loading = true;
568
+ this.loadedCommands.length = 0;
569
+ try {
570
+ await this.loadCommands(this.dir);
571
+ } finally {
572
+ this.loading = false;
573
+ }
574
+ this.reportLoad();
575
+ getDevChannel()?.on("seedcord:refresh-commands", (data) => {
576
+ this.refresh(data.shouldRefresh);
577
+ });
578
+ }
579
+ reportLoad() {
580
+ const { utils } = this.logger;
581
+ utils.summary("Loaded commands", {
582
+ global: this.scopedCommands.get("global")?.length ?? 0,
583
+ "guild groups": [...this.scopedCommands.keys()].filter((scope) => scope !== GLOBAL_ID).length
584
+ }, "debug");
585
+ let slash = 0;
586
+ let menu = 0;
587
+ for (const command of this.loadedCommands) if (command.kind === "slash command") slash++;
588
+ else menu++;
589
+ utils.block("Loaded commands", [...utils.entries(this.loadedCommands), ...utils.counts({
590
+ "slash commands": slash,
591
+ "context menus": menu
592
+ })], "debug");
593
+ }
594
+ /** @internal */
595
+ async refresh(shouldRefresh = true) {
596
+ if (!shouldRefresh) {
597
+ this.logger.debug(paint.italic("Command refresh cancelled."));
598
+ this.pendingEvents.clear();
599
+ return;
600
+ }
601
+ this.logger.debug(paint.italic("Refreshing commands..."));
602
+ for (const event of this.pendingEvents.values()) await this.hmrHandler?.handle(event);
603
+ this.pendingEvents.clear();
604
+ await this.setCommands();
605
+ }
606
+ /** @internal */
607
+ async onHmr(event) {
608
+ if (botFolder(this.dir).holds(event.file)) {
609
+ this.pendingEvents.delete(event.file);
610
+ this.pendingEvents.set(event.file, event);
611
+ getDevChannel()?.send("seedcord:commands-update-prompt", { files: [...this.pendingEvents.keys()].map((f) => formatFilePath(f)) });
612
+ } else await this.hmrHandler?.handle(event);
613
+ }
614
+ async loadCommands(dir) {
615
+ for await (const { fullPath, relativePath, imported } of importFiles(dir)) for (const exported of Object.values(imported)) {
616
+ if (!isCommandClass(exported)) continue;
617
+ this.registerCommand(exported, relativePath);
618
+ this.hmrHandler?.trackHandler(fullPath, exported);
619
+ }
620
+ }
621
+ registerCommand(Ctor, rel) {
622
+ const meta = Reflect.getMetadata(CommandMetadataKey, Ctor);
623
+ if (!meta) return;
624
+ const comp = new Ctor().component;
625
+ const kind = comp instanceof SlashCommandBuilder ? "slash command" : "context menu";
626
+ const target = resolveDeployTarget(meta, this.configuredGuilds, Ctor.name);
627
+ for (const scope of scopeIdsOf(target)) {
628
+ const inScope = this.scopedCommands.get(scope);
629
+ if (inScope) inScope.push(comp);
630
+ else this.scopedCommands.set(scope, [comp]);
631
+ }
632
+ this.ctorToCommand.set(Ctor, {
633
+ name: comp.name,
634
+ ...target
635
+ });
636
+ if (this.loading) this.loadedCommands.push({
637
+ name: comp.name,
638
+ from: formatFilePath(rel),
639
+ kind
640
+ });
641
+ }
642
+ unregisterCommand(ctor, artifacts) {
643
+ const info = artifacts ?? this.ctorToCommand.get(ctor);
644
+ if (!info) return;
645
+ for (const scope of scopeIdsOf(info)) {
646
+ const inScope = this.scopedCommands.get(scope) ?? [];
647
+ const idx = inScope.findIndex((c) => c.name === info.name);
648
+ if (idx !== -1) inScope.splice(idx, 1);
649
+ }
650
+ this.ctorToCommand.delete(ctor);
651
+ }
652
+ /** @internal */
653
+ async deploy(signal, { routes, check }) {
654
+ await this.init();
655
+ check?.(this.allCommands());
656
+ await this.setCommands(signal);
657
+ routes?.warnUnhandledRoutes(this.routeLeaves());
658
+ routes?.warnUnhandledContextMenuRoutes(this.contextMenuLeaves());
659
+ }
660
+ /** @internal */
661
+ async setCommands(signal) {
662
+ const { rest, applicationId } = this.core;
663
+ const last = await this.lastDeploy.read(applicationId, signal);
664
+ const plan = await planDeploy(this.commandBodies(), last?.scopes, (unchanged) => readCommands(unchanged, {
665
+ rest,
666
+ applicationId,
667
+ signal
668
+ }));
669
+ await this.reporter.readGuildNames(rest, [...plan.toPut.keys(), ...plan.skipped], signal);
670
+ if (plan.toPut.size > 0) this.lastDeploy.save({
671
+ applicationId,
672
+ scopes: plan.skippedHashes
673
+ });
674
+ const put = await putCommands(plan.toPut, {
675
+ rest,
676
+ applicationId,
677
+ logger: this.logger,
678
+ signal,
679
+ onDeployed: (scope, commands) => this.reporter.deployed(scope, commands)
680
+ });
681
+ this.reporter.skipped(plan.skipped);
682
+ const result = new Map([...plan.onDiscord, ...put]);
683
+ this.lastDeploy.save({
684
+ applicationId,
685
+ scopes: plan.hashes
686
+ });
687
+ this.injector.inject(result, this.allCommands());
688
+ this.core.bus[PublishDefault]("commandsDeployed", deployedPayload(result));
689
+ return result;
690
+ }
691
+ commandBodies() {
692
+ const deployable = this.scopedCommands.entries().filter(([scope, commands]) => scope !== "global" || commands.length > 0);
693
+ return new Map(deployable.map(([scope, commands]) => [scope, commands.map((c) => c.toJSON())]));
694
+ }
695
+ allCommands() {
696
+ return [...this.scopedCommands.values()].flat();
697
+ }
698
+ routeLeaves() {
699
+ return slashRouteLeaves(this.allCommands());
700
+ }
701
+ contextMenuLeaves() {
702
+ return contextMenuLeaves(this.allCommands().map((command) => command.toJSON()));
703
+ }
704
+ };
705
+ function deployedPayload(result) {
706
+ const guilds = {};
707
+ for (const [scope, deployed] of result) if (scope !== "global") guilds[scope] = [...deployed.values()];
708
+ return {
709
+ global: [...result.get("global")?.values() ?? []],
710
+ guilds
711
+ };
712
+ }
713
+
146
714
  //#endregion
147
715
  //#region src/hmr/wrapHot.ts
148
716
  /** @internal */
149
717
  function wrapHot(hot) {
150
718
  return {
151
719
  send: (event, data) => hot.send(event, data),
152
- on: (event, cb) => hot.on(event, cb)
720
+ on: (event, cb) => hot.on(event, cb),
721
+ off: (event, cb) => hot.off(event, cb)
153
722
  };
154
723
  }
155
724
 
156
725
  //#endregion
157
726
  //#region src/hmr/HmrManager.ts
727
+ function connectDevChannel() {
728
+ if (import.meta.hot) setDevChannel(wrapHot(import.meta.hot));
729
+ return getDevChannel();
730
+ }
158
731
  var HmrManager = class {
159
732
  logger = new Logger("HMR", { channel: "hmr" });
160
733
  listeners = /* @__PURE__ */ new Set();
161
- /** @internal */
162
- init() {
163
- if (!import.meta.hot) return;
164
- const channel = wrapHot(import.meta.hot);
165
- setDevChannel(channel);
734
+ init(channel) {
735
+ if (!channel) return;
166
736
  if (!(Envapter.isDevelopment || Envapter.isTest)) return;
167
737
  this.logger.debug("Enabled");
168
738
  channel.on("seedcord:hmr", (payload) => void this.handleUpdate(payload));
169
739
  }
170
- /** @internal */
171
740
  register(listener) {
172
741
  this.listeners.add(listener);
173
742
  }
@@ -183,6 +752,132 @@ var HmrManager = class {
183
752
  }
184
753
  };
185
754
 
755
+ //#endregion
756
+ //#region src/hmr/wire.ts
757
+ function toWireError(thrown) {
758
+ return encodeError(thrown, /* @__PURE__ */ new Set());
759
+ }
760
+ function encodeError(thrown, sent) {
761
+ if (!Error.isError(thrown)) return {
762
+ name: "Error",
763
+ message: String(thrown)
764
+ };
765
+ sent.add(thrown);
766
+ const unsent = (value) => !Error.isError(value) || !sent.has(value);
767
+ const { cause } = thrown;
768
+ return {
769
+ name: thrown.name,
770
+ message: thrown.message,
771
+ stack: thrown.stack,
772
+ code: Reflect.get(thrown, "code"),
773
+ cause: cause !== void 0 && unsent(cause) ? encodeError(cause, sent) : void 0,
774
+ errors: thrown instanceof AggregateError ? thrown.errors.filter(unsent).map((member) => encodeError(member, sent)) : void 0
775
+ };
776
+ }
777
+ function fromWireError({ name, message, stack, code, cause, errors }) {
778
+ const options = cause && { cause: fromWireError(cause) };
779
+ const error = errors ? new AggregateError(errors.map(fromWireError), message, options) : new Error(message, options);
780
+ error.name = name;
781
+ if (stack !== void 0) error.stack = stack;
782
+ if (code !== void 0) Reflect.set(error, "code", code);
783
+ return error;
784
+ }
785
+ function functionText(fn) {
786
+ const parent = Object.getPrototypeOf(fn);
787
+ if (Function.prototype.toString.call(fn).startsWith("class")) {
788
+ const extended = typeof parent === "function" && parent.name ? ` extends ${parent.name}` : "";
789
+ return `[class ${fn.name || "(anonymous)"}${extended}]`;
790
+ }
791
+ const kind = parent === null ? "Function (null prototype)" : fn.constructor.name;
792
+ return fn.name ? `[${kind}: ${fn.name}]` : `[${kind} (anonymous)]`;
793
+ }
794
+ function toWireArg(arg) {
795
+ if (Error.isError(arg)) return { error: toWireError(arg) };
796
+ switch (typeof arg) {
797
+ case "undefined":
798
+ case "symbol": return { value: String(arg) };
799
+ case "bigint": return { value: `${arg}n` };
800
+ case "function": return { value: functionText(arg) };
801
+ default: return { value: filterCirculars(arg, { mode: "json" }) };
802
+ }
803
+ }
804
+ function fromWireArg(arg) {
805
+ return "error" in arg ? fromWireError(arg.error) : arg.value;
806
+ }
807
+ function toWireLog({ args, ...record }) {
808
+ return {
809
+ ...record,
810
+ args: args?.map(toWireArg)
811
+ };
812
+ }
813
+ function fromWireLog({ args, ...record }) {
814
+ return args ? {
815
+ ...record,
816
+ args: args.map(fromWireArg)
817
+ } : record;
818
+ }
819
+ const DEV_LOG_MARKER = "seedcord:log ";
820
+ function toDevLogLine(record) {
821
+ return `${DEV_LOG_MARKER}${JSON.stringify(toWireLog(record))}`;
822
+ }
823
+ function isLogLevel(value) {
824
+ return typeof value === "string" && Object.hasOwn(LEVEL_COLOR, value);
825
+ }
826
+ function isWireError(value) {
827
+ return isPlainObject(value) && typeof value.name === "string" && typeof value.message === "string" && (value.stack === void 0 || typeof value.stack === "string") && (value.cause === void 0 || isWireError(value.cause)) && (value.errors === void 0 || Array.isArray(value.errors) && value.errors.every(isWireError));
828
+ }
829
+ function isWireArg(value) {
830
+ if (!isPlainObject(value)) return false;
831
+ return "error" in value ? isWireError(value.error) : "value" in value;
832
+ }
833
+ function isWireArgs(value) {
834
+ return value === void 0 || Array.isArray(value) && value.every(isWireArg);
835
+ }
836
+ function isWireLogRecord(value) {
837
+ return isPlainObject(value) && isLogLevel(value.level) && typeof value.message === "string" && typeof value.label === "string" && typeof value.channel === "string" && typeof value.timestamp === "number" && isWireArgs(value.args);
838
+ }
839
+ function fromDevLogLine(line) {
840
+ if (!line.startsWith(DEV_LOG_MARKER)) return void 0;
841
+ let parsed;
842
+ try {
843
+ parsed = JSON.parse(line.slice(13));
844
+ } catch {
845
+ return;
846
+ }
847
+ return isWireLogRecord(parsed) ? fromWireLog(parsed) : void 0;
848
+ }
849
+
850
+ //#endregion
851
+ //#region src/hmr/forwardLogsToCli.ts
852
+ const WORKER_ROUTED = [
853
+ "debug",
854
+ "dir",
855
+ "dirxml",
856
+ "error",
857
+ "info",
858
+ "log",
859
+ "table",
860
+ "trace",
861
+ "warn"
862
+ ];
863
+ function forwardLogsToCli() {
864
+ const registry = LoggerChannelRegistry.instance;
865
+ const print = console.log.bind(console);
866
+ registry.installSink({
867
+ kind: "capture",
868
+ onLog: (record) => print(toDevLogLine(record))
869
+ }, { muteConsole: true });
870
+ routeConsole(WORKER_ROUTED, (method, args, invoked) => {
871
+ const level = CONSOLE_LEVEL[method];
872
+ if (method === "trace") {
873
+ registry.dispatch(consoleRecord(level, "", [traceError(format(...args), invoked)]));
874
+ return;
875
+ }
876
+ const [first, ...rest] = args;
877
+ registry.dispatch(typeof first === "string" ? consoleRecord(level, first, rest) : consoleRecord(level, "", args));
878
+ });
879
+ }
880
+
186
881
  //#endregion
187
882
  //#region src/dispatch/dispatchReport.ts
188
883
  let dispatchLogger;
@@ -274,5 +969,5 @@ async function runAfter(ran, result, logger) {
274
969
  }
275
970
 
276
971
  //#endregion
277
- export { AckTrace, AutocompleteRouteBrand, CommandMetadataKey, ComponentDefsBrand, ComponentDefsKey, ComponentKindBrand, ContextMenuKindBrand, ContextMenuNamesBrand, EventMetadataKey, EventMiddlewareMetadataKey, GateNotice, GatedMetadataKey, HasDangerousPermissions, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, InteractionRouteKeys, MiddlewareKindsBrand, MiddlewareRegistry, MissingPermissions, MissingRole, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, PAGE_MAX, PermissionNames, PublishDefault, RegisterDefaults, RegisterSubscriber, RegisteredCount, ReportThrottle, SlashRouteBrand, SubscribeMetadataKey, UnregisterSubscriber, VerifyWebhooks, WebhookUrlMetadataKey, accessorStore, areRoutes, asError, bindBotColor, busLoggerOf, checkAckLegality, clearStore, contextMenuLeaves, contextMenuRouteOf, decodeComponentRoute, deferFlags, eventResultFor, getDevChannel, guardedAccessor, interactionMiddleware, interactionMiddlewareMetaOf, interactionRoutesOf, isCommandClass, isEmojiTuple, outcomeFor, pageCursor, pickNotice, queuedMsFor, registrationFor, reportDispatch, reportedWrite, resultFor, runAfter, runGates, runHandlerGates, sendFlags, sendTarget, serializeReply, setDevChannel, slashRouteLeaves, slowGateMonitor, storeComponentRoute, storeInteractionRoute, translateSerializationError, wrapHot };
972
+ export { AckTrace, AutocompleteRouteBrand, BAKED_IDS_KEY, COMMAND_DEPLOY_TIMEOUT_MS, CommandMetadataKey, CommandRegistry, ComponentDefsBrand, ComponentDefsKey, ComponentKindBrand, ContextMenuKindBrand, ContextMenuNamesBrand, CoordinatedStartup, EventMetadataKey, EventMiddlewareMetadataKey, FixedScope, GLOBAL_ID, GateNotice, GatedMetadataKey, HasDangerousPermissions, HmrManager, InteractionMetadataKey, InteractionMiddlewareMetadataKey, InteractionRouteKeys, MiddlewareKindsBrand, MiddlewareRegistry, MissingPermissions, MissingRole, NeedsAny, NotAllowed, NotInDm, NotInGuild, NotOwner, NoticeCard, OnCooldown, PAGE_MAX, PermissionNames, PluginLifecycle, PublishDefault, RegisterDefaults, RegisterSubscriber, RegisteredCount, ReportThrottle, SlashRouteBrand, SubscribeMetadataKey, SubscriberLoader, UnregisterSubscriber, VerifyWebhooks, WebhookUrlMetadataKey, accessorStore, attachmentsOf, bindBotColor, bindEnv, busLoggerOf, checkAckLegality, clearStore, commandKey, commandRefOf, commandsRoute, connectDevChannel, contextMenuLeaves, contextMenuRouteOf, currentApplication, decodeComponentRoute, deferFlags, eventResultFor, forwardLogsToCli, fromDevLogLine, fromWireError, getDevChannel, globalFirst, guardedAccessor, guildSummaries, indexById, installLinks, interactionMiddleware, interactionMiddlewareMetaOf, interactionRoutesOf, isCommandClass, isEmojiTuple, linkCommands, logUnknownFault, outcomeFor, pageCursor, pickNotice, planDeploy, putCommands, queuedMsFor, readBotToken, readCommands, readCommandsAt, registrationFor, reportDispatch, reportedWrite, resolveDeployTarget, resolveEmojis, resultFor, runAfter, runGates, runHandlerGates, scopeIdsOf, sealAttachments, sendFlags, sendTarget, serializeReply, setDevChannel, skippedNote, slashRouteLeaves, slowGateMonitor, storeComponentRoute, storeInteractionRoute, toWireError, translateSerializationError, warnLinks, wrapHot };
278
973
  //# sourceMappingURL=internal.index.mjs.map