kythia-core 0.9.5-beta → 0.10.1-beta

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.
@@ -4,7 +4,7 @@
4
4
  * @file src/managers/AddonManager.js
5
5
  * @copyright © 2025 kenndeclouv
6
6
  * @assistant chaa & graa
7
- * @version 0.9.5-beta
7
+ * @version 0.10.0-beta
8
8
  *
9
9
  * @description
10
10
  * Handles all addon loading, command registration, and component management.
@@ -13,954 +13,1198 @@
13
13
  */
14
14
 
15
15
  const {
16
- SlashCommandBuilder,
17
- SlashCommandSubcommandBuilder,
18
- SlashCommandSubcommandGroupBuilder,
19
- Collection,
20
- ContextMenuCommandBuilder,
21
- ApplicationCommandType,
16
+ SlashCommandBuilder,
17
+ SlashCommandSubcommandBuilder,
18
+ SlashCommandSubcommandGroupBuilder,
19
+ Collection,
20
+ ContextMenuCommandBuilder,
21
+ ApplicationCommandType,
22
22
  } = require('discord.js');
23
- const path = require('path');
24
- const fs = require('fs');
23
+ const path = require('node:path');
24
+ const fs = require('node:fs');
25
25
 
26
26
  class AddonManager {
27
- /**
28
- * 🗝️ AddonManager Constructor
29
- * Initializes the addon manager with necessary collections and maps.
30
- * @param {Object} client - Discord client instance
31
- * @param {Object} container - Dependency container
32
- */
33
- constructor({ client, container }) {
34
- this.client = client;
35
- this.container = container;
36
- this.logger = this.container.logger;
37
-
38
- this.buttonHandlers = new Map();
39
- this.modalHandlers = new Map();
40
- this.selectMenuHandlers = new Map();
41
- this.autocompleteHandlers = new Map();
42
- this.commandCategoryMap = new Map();
43
- this.categoryToFeatureMap = new Map();
44
- this.embedDrafts = new Collection();
45
- this.eventHandlers = new Map();
46
- }
47
-
48
- /**
49
- * 🔘 Register Button Handler
50
- * Registers a handler function for a specific button customId.
51
- * @param {string} customId - The customId of the button
52
- * @param {Function} handler - The handler function to execute
53
- */
54
- registerButtonHandler(customId, handler) {
55
- if (this.buttonHandlers.has(customId)) {
56
- this.logger.warn(`[REGISTRATION] Warning: Button handler for [${customId}] already exists and will be overwritten.`);
57
- }
58
- this.buttonHandlers.set(customId, handler);
59
- }
60
- /**
61
- * 🔽 Register Select Menu Handler
62
- * Registers a handler function for a specific select menu customId prefix.
63
- * @param {string} customIdPrefix - The prefix of the select menu customId
64
- * @param {Function} handler - The handler function to execute
65
- */
66
- registerSelectMenuHandler(customIdPrefix, handler) {
67
- if (this.selectMenuHandlers.has(customIdPrefix)) {
68
- this.logger.warn(`[REGISTRATION] Warning: Select menu handler for [${customIdPrefix}] already exists and will be overwritten.`);
69
- }
70
- this.selectMenuHandlers.set(customIdPrefix, handler);
71
- }
72
-
73
- /**
74
- * 📝 Register Modal Handler
75
- * Registers a handler function for a modal, using a prefix of the customId.
76
- * @param {string} customIdPrefix - The prefix of the modal customId
77
- * @param {Function} handler - The handler function to execute
78
- */
79
- registerModalHandler(customIdPrefix, handler) {
80
- if (this.modalHandlers.has(customIdPrefix)) {
81
- this.logger.warn(`[REGISTRATION] Warning: Modal handler for [${customIdPrefix}] already exists and will be overwritten.`);
82
- }
83
- this.modalHandlers.set(customIdPrefix, handler);
84
- }
85
-
86
- /**
87
- * 📋 Register Autocomplete Handler
88
- * Registers a handler for autocomplete interactions for a specific command or subcommand.
89
- * @param {string} commandName - The command or subcommand key
90
- * @param {Function} handler - The autocomplete handler function
91
- */
92
- registerAutocompleteHandler(commandName, handler) {
93
- if (this.autocompleteHandlers.has(commandName)) {
94
- this.logger.warn(`[REGISTRATION] Warning: Autocomplete handler for [${commandName}] already exists and will be overwritten.`);
95
- }
96
- this.autocompleteHandlers.set(commandName, handler);
97
- }
98
-
99
- /**
100
- * 🔍 Check if module is a BaseCommand class
101
- * @param {any} module - The module to check
102
- * @returns {boolean} True if module is a class extending BaseCommand
103
- * @private
104
- */
105
- _isBaseCommandClass(module) {
106
- if (typeof module !== 'function') return false;
107
- if (!module.prototype) return false;
108
-
109
- const hasExecute = typeof module.prototype.execute === 'function';
110
- return hasExecute;
111
- }
112
-
113
- /**
114
- * 🏗️ Instantiate and prepare BaseCommand class
115
- * @param {Function} CommandClass - The command class to instantiate
116
- * @returns {Object} Command instance with proper structure
117
- * @private
118
- */
119
- _instantiateBaseCommand(CommandClass) {
120
- try {
121
- return new CommandClass(this.container);
122
- } catch (error) {
123
- this.logger.error(`Failed to instantiate BaseCommand class:`, error);
124
- throw error;
125
- }
126
- }
127
-
128
- /**
129
- * INTERNAL: Creates a Builder instance (Slash, Subcommand, Group) from a module's data property.
130
- * @param {Object} data - The data property (can be function, builder, or object)
131
- * @param {Class} BuilderClass - The d.js class to use (SlashCommandBuilder, etc.)
132
- * @returns {SlashCommandBuilder|SlashCommandSubcommandBuilder|SlashCommandSubcommandGroupBuilder}
133
- * @private
134
- */
135
- _createBuilderFromData(data, BuilderClass) {
136
- let builder = new BuilderClass();
137
-
138
- if (typeof data === 'function') {
139
- data(builder);
140
- } else if (data instanceof BuilderClass) {
141
- builder = data;
142
- } else if (typeof data === 'object') {
143
- builder.setName(data.name || 'unnamed');
144
- builder.setDescription(data.description || 'No description');
145
-
146
- if (BuilderClass === SlashCommandBuilder) {
147
- builder.setDescription(data.description || 'No description');
148
- if (data.permissions) {
149
- builder.setDefaultMemberPermissions(data.permissions);
150
- }
151
- if (data.guildOnly !== undefined) {
152
- builder.setDMPermission(!data.guildOnly);
153
- }
154
- } else if (BuilderClass === ContextMenuCommandBuilder) {
155
- builder.setType(data.type || ApplicationCommandType.User);
156
- if (data.permissions) {
157
- builder.setDefaultMemberPermissions(data.permissions);
158
- }
159
- if (data.guildOnly !== undefined) {
160
- builder.setDMPermission(!data.guildOnly);
161
- }
162
- }
163
- }
164
- return builder;
165
- }
166
-
167
- /**
168
- * 📝 Register Command Helper
169
- * Registers a single command file/module, adds it to the command collection, and prepares it for deployment.
170
- * @param {Object} module - The command module
171
- * @param {string} filePath - The file path of the command
172
- * @param {Set} commandNamesSet - Set of already registered command names
173
- * @param {Array} commandDataForDeployment - Array to collect command data for deployment
174
- * @param {Object} permissionDefaults - Permission defaults for the command
175
- * @param {Object} options - Additional options (e.g., folderName)
176
- * @returns {Object|null} Summary object for logging, or null if not registered
177
- */
178
- registerCommand(module, filePath, commandNamesSet, commandDataForDeployment, permissionDefaults = {}, options = {}) {
179
- if (this._isBaseCommandClass(module)) {
180
- module = this._instantiateBaseCommand(module);
181
- }
182
-
183
- if (!module || !module.data) return null;
184
-
185
- let builderClass;
186
-
187
- if (module.data instanceof ContextMenuCommandBuilder) {
188
- builderClass = ContextMenuCommandBuilder;
189
- } else {
190
- builderClass = SlashCommandBuilder;
191
- }
192
-
193
- let commandBuilder = this._createBuilderFromData(module.data, builderClass);
194
-
195
- const commandName = commandBuilder.name;
196
- const category = options.folderName || path.basename(path.dirname(filePath));
197
-
198
- const categoryDefaults = permissionDefaults[category] || {};
199
- const finalCommand = {
200
- ...categoryDefaults,
201
- ...module,
202
- };
203
-
204
- this.commandCategoryMap.set(commandName, category);
205
- if (commandNamesSet.has(commandName)) {
206
- throw new Error(`Duplicate command name detected: "${commandName}" in ${filePath}`);
207
- }
208
-
209
- commandNamesSet.add(commandName);
210
- this.client.commands.set(commandName, finalCommand);
211
- commandDataForDeployment.push(commandBuilder.toJSON());
212
-
213
- if (typeof finalCommand.autocomplete === 'function') {
214
- this.registerAutocompleteHandler(commandName, finalCommand.autocomplete);
215
- }
216
-
217
- return {
218
- type: 'single',
219
- name: commandName,
220
- folder: category,
221
- };
222
- }
223
-
224
- /**
225
- * 🧩 Load Addons & Register Commands/Events
226
- * Loads all addons from the addons directory, registers their commands, events, and components.
227
- * @param {Object} kythiaInstance - The main Kythia instance for addon registration
228
- * @returns {Promise<Array>} Array of command data for deployment
229
- */
230
- async loadAddons(kythiaInstance) {
231
- this.logger.info('🔌 Loading & Registering Kythia Addons...');
232
- const commandDataForDeployment = [];
233
- const addonsDir = path.join(this.container.appRoot, 'addons');
234
- if (!fs.existsSync(addonsDir)) return commandDataForDeployment;
235
-
236
- let addonFolders = fs.readdirSync(addonsDir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith('_'));
237
-
238
- let coreAddon = addonFolders.find((d) => d.name === 'core');
239
- let otherAddons = addonFolders.filter((d) => d.name !== 'core');
240
- if (coreAddon) {
241
- addonFolders = [coreAddon, ...otherAddons];
242
- }
243
-
244
- const commandNamesSet = new Set();
245
- const addonSummaries = [];
246
-
247
- for (const addon of addonFolders) {
248
- const addonDir = path.join(addonsDir, addon.name);
249
- let addonVersion = 'v0.0.0-alpha';
250
-
251
- try {
252
- const addonJsonPath = path.join(addonDir, 'addon.json');
253
- if (fs.existsSync(addonJsonPath)) {
254
- let addonJson;
255
- try {
256
- const addonJsonRaw = fs.readFileSync(addonJsonPath, 'utf8');
257
- addonJson = JSON.parse(addonJsonRaw);
258
- } catch (jsonErr) {
259
- this.logger.warn(`🔴 Failed to parse addon.json for ${addon.name}: ${jsonErr.message}`);
260
- continue;
261
- }
262
-
263
- addonVersion = addonJson.version || 'v0.0.0-alpha';
264
- if (addonJson.active === false) {
265
- this.logger.info(`🟠 Addon ${addon.name.toUpperCase()} disabled`);
266
- continue;
267
- }
268
- if (addonJson.featureFlag) {
269
- this.commandCategoryMap.set(addon.name, addon.name);
270
- this.categoryToFeatureMap.set(addon.name, addonJson.featureFlag);
271
- }
272
- } else {
273
- this.logger.warn(`🔴 Addon ${addon.name.toUpperCase()} is missing addon.json. Skipping.`);
274
- continue;
275
- }
276
- } catch (e) {
277
- this.logger.warn(`🔴 Error reading addon.json for ${addonDir}: ${e.message}`);
278
- continue;
279
- }
280
-
281
- try {
282
- const configAddons = this.container.kythiaConfig?.addons || {};
283
- if (configAddons.all?.active === false) {
284
- this.logger.info(`🟠 Addon ${addon.name.toUpperCase()} disabled via kythia config`);
285
- continue;
286
- } else if (configAddons[addon.name]?.active === false) {
287
- this.logger.info(`🟠 Addon ${addon.name.toUpperCase()} disabled via kythia config`);
288
- continue;
289
- }
290
- } catch (e) {
291
- this.logger.warn(`🔴 Error checking config for addon ${addon.name.toUpperCase()}: ${e.message}`);
292
- }
293
-
294
- let addonPermissionDefaults = {};
295
- const permissionsFilePath = path.join(addonDir, 'permissions.js');
296
-
297
- if (fs.existsSync(permissionsFilePath)) {
298
- try {
299
- addonPermissionDefaults = require(permissionsFilePath);
300
- this.logger.info(` └─> Found and loaded permission defaults for addon '${addon.name.toUpperCase()}'`);
301
- } catch (e) {
302
- this.logger.warn(` └─> Failed to load permissions.js for addon '${addon.name.toUpperCase()}': ${e.message}`);
303
- }
304
- }
305
-
306
- const loadedCommandsSummary = [];
307
- const loadedEventsSummary = [];
308
- const loadedRegisterSummary = [];
309
-
310
- const commandsPath = path.join(addonDir, 'commands');
311
- if (fs.existsSync(commandsPath)) {
312
- try {
313
- const commandsResult = await this._loadCommandsFromPath(
314
- commandsPath,
315
- addon,
316
- addonPermissionDefaults,
317
- commandNamesSet,
318
- commandDataForDeployment
319
- );
320
- loadedCommandsSummary.push(...commandsResult);
321
- } catch (error) {
322
- this.logger.error(`❌ Failed to load commands from addon "${addon.name}":`, error);
323
- }
324
- }
325
-
326
- const registerPath = path.join(addonDir, 'register.js');
327
- if (fs.existsSync(registerPath)) {
328
- try {
329
- const registration = require(registerPath);
330
- if (typeof registration.initialize === 'function') {
331
- const registrationSummary = await registration.initialize(kythiaInstance);
332
- if (Array.isArray(registrationSummary) && registrationSummary.length > 0) {
333
- loadedRegisterSummary.push(...registrationSummary);
334
- }
335
- }
336
- } catch (error) {
337
- this.logger.error(`❌ Failed to register components for [${addon.name}]:`, error);
338
- }
339
- }
340
-
341
- const eventsPath = path.join(addonDir, 'events');
342
- if (fs.existsSync(eventsPath)) {
343
- const eventFiles = fs.readdirSync(eventsPath).filter((file) => file.endsWith('.js'));
344
- for (const file of eventFiles) {
345
- const eventName = path.basename(file, '.js');
346
- try {
347
- const eventHandler = require(path.join(eventsPath, file));
348
- if (typeof eventHandler === 'function') {
349
- if (!this.eventHandlers.has(eventName)) {
350
- this.eventHandlers.set(eventName, []);
351
- }
352
- this.eventHandlers.get(eventName).push(eventHandler);
353
- loadedEventsSummary.push(eventName);
354
- }
355
- } catch (error) {
356
- this.logger.error(`❌ Failed to register event [${eventName}] for [${addon.name}]:`, error);
357
- }
358
- }
359
- }
360
-
361
- addonSummaries.push({
362
- name: addon.name,
363
- version: addonVersion,
364
- commands: loadedCommandsSummary,
365
- events: loadedEventsSummary,
366
- register: loadedRegisterSummary,
367
- });
368
- }
369
-
370
- this._logAddonSummary(addonSummaries);
371
- return commandDataForDeployment;
372
- }
373
-
374
- /**
375
- * Load commands from a specific path
376
- * @private
377
- */
378
- async _loadCommandsFromPath(commandsPath, addon, addonPermissionDefaults, commandNamesSet, commandDataForDeployment) {
379
- const loadedCommandsSummary = [];
380
- const isTopLevelCommandGroup = fs.existsSync(path.join(commandsPath, '_command.js'));
381
-
382
- if (isTopLevelCommandGroup) {
383
- return await this._loadTopLevelCommandGroup(
384
- commandsPath,
385
- addon,
386
- addonPermissionDefaults,
387
- commandNamesSet,
388
- commandDataForDeployment
389
- );
390
- } else {
391
- return await this._loadIndividualCommands(
392
- commandsPath,
393
- addon,
394
- addonPermissionDefaults,
395
- commandNamesSet,
396
- commandDataForDeployment
397
- );
398
- }
399
- }
400
-
401
- /**
402
- * Load top-level command group (supports BaseCommand classes)
403
- * @private
404
- */
405
- async _loadTopLevelCommandGroup(commandsPath, addon, addonPermissionDefaults, commandNamesSet, commandDataForDeployment) {
406
- const loadedCommandsSummary = [];
407
- let commandDef = require(path.join(commandsPath, '_command.js'));
408
-
409
- if (this._isBaseCommandClass(commandDef)) {
410
- commandDef = this._instantiateBaseCommand(commandDef);
411
- }
412
-
413
- let mainBuilder = this._createBuilderFromData(commandDef.data, SlashCommandBuilder);
414
-
415
- const mainCommandName = mainBuilder.name;
416
-
417
- if (commandDef.featureFlag) {
418
- this.commandCategoryMap.set(mainCommandName, addon.name);
419
- this.categoryToFeatureMap.set(addon.name, commandDef.featureFlag);
420
- }
421
- this.commandCategoryMap.set(mainCommandName, addon.name);
422
-
423
- if (commandNamesSet.has(mainCommandName)) throw new Error(`Duplicate command name: ${mainCommandName}`);
424
- commandNamesSet.add(mainCommandName);
425
-
426
- this.client.commands.set(mainCommandName, commandDef);
427
-
428
- if (typeof commandDef.autocomplete === 'function') {
429
- this.registerAutocompleteHandler(mainCommandName, commandDef.autocomplete);
430
- }
431
-
432
- const loadedSubcommandsSummary = [];
433
- const contents = fs.readdirSync(commandsPath, { withFileTypes: true });
434
-
435
- for (const item of contents) {
436
- const itemPath = path.join(commandsPath, item.name);
437
-
438
- if (item.isFile() && item.name.endsWith('.js') && !item.name.startsWith('_')) {
439
- let subModule = require(itemPath);
440
-
441
- const isSubcommand = subModule.subcommand === true || this._isBaseCommandClass(subModule);
442
-
443
- if (this._isBaseCommandClass(subModule)) {
444
- subModule = this._instantiateBaseCommand(subModule);
445
- }
446
-
447
- if (!isSubcommand) continue;
448
- if (!subModule.data) continue;
449
-
450
- const subBuilder = this._createBuilderFromData(subModule.data, SlashCommandSubcommandBuilder);
451
-
452
- mainBuilder.addSubcommand(subBuilder);
453
- this.client.commands.set(`${mainCommandName} ${subBuilder.name}`, subModule);
454
-
455
- if (typeof subModule.autocomplete === 'function') {
456
- this.registerAutocompleteHandler(`${mainCommandName} ${subBuilder.name}`, subModule.autocomplete);
457
- }
458
-
459
- loadedSubcommandsSummary.push(subBuilder.name);
460
- } else if (item.isDirectory()) {
461
- const groupDefPath = path.join(itemPath, '_group.js');
462
-
463
- if (!fs.existsSync(groupDefPath)) {
464
- continue;
465
- }
466
-
467
- try {
468
- let groupModule = require(groupDefPath);
469
-
470
- if (this._isBaseCommandClass(groupModule)) {
471
- groupModule = this._instantiateBaseCommand(groupModule);
472
- }
473
-
474
- if (!groupModule.data) continue;
475
-
476
- const groupBuilder = this._createBuilderFromData(groupModule.data, SlashCommandSubcommandGroupBuilder);
477
-
478
- const subcommandsInGroupSummary = [];
479
- const subCommandFiles = fs.readdirSync(itemPath).filter((f) => f.endsWith('.js') && !f.startsWith('_'));
480
-
481
- for (const file of subCommandFiles) {
482
- const subCommandPath = path.join(itemPath, file);
483
- let subModule = require(subCommandPath);
484
-
485
- if (this._isBaseCommandClass(subModule)) {
486
- subModule = this._instantiateBaseCommand(subModule);
487
- }
488
-
489
- if (!subModule.data) continue;
490
-
491
- const subBuilder = this._createBuilderFromData(subModule.data, SlashCommandSubcommandBuilder);
492
-
493
- groupBuilder.addSubcommand(subBuilder);
494
-
495
- const commandKey = `${mainCommandName} ${groupBuilder.name} ${subBuilder.name}`;
496
-
497
- this.client.commands.set(commandKey, subModule);
498
-
499
- if (typeof subModule.autocomplete === 'function') {
500
- this.registerAutocompleteHandler(commandKey, subModule.autocomplete);
501
- }
502
-
503
- subcommandsInGroupSummary.push(subBuilder.name);
504
- }
505
-
506
- mainBuilder.addSubcommandGroup(groupBuilder);
507
-
508
- loadedSubcommandsSummary.push({ group: groupBuilder.name, subcommands: subcommandsInGroupSummary });
509
- } catch (e) {
510
- this.logger.error(`❌ Failed to load subcommand group from ${itemPath}:`, e);
511
- }
512
- }
513
- }
514
-
515
- commandDataForDeployment.push(mainBuilder.toJSON());
516
- loadedCommandsSummary.push({ type: 'group', name: mainCommandName, subcommands: loadedSubcommandsSummary });
517
-
518
- return loadedCommandsSummary;
519
- }
520
-
521
- /**
522
- * Load individual commands (supports BaseCommand classes)
523
- * @private
524
- */
525
- async _loadIndividualCommands(commandsPath, addon, addonPermissionDefaults, commandNamesSet, commandDataForDeployment) {
526
- const loadedCommandsSummary = [];
527
- const commandItems = fs.readdirSync(commandsPath, { withFileTypes: true });
528
-
529
- for (const item of commandItems) {
530
- const itemPath = path.join(commandsPath, item.name);
531
-
532
- if (item.isDirectory() && fs.existsSync(path.join(itemPath, '_command.js'))) {
533
- let commandDef = require(path.join(itemPath, '_command.js'));
534
- if (this._isBaseCommandClass(commandDef)) {
535
- commandDef = this._instantiateBaseCommand(commandDef);
536
- }
537
- let mainBuilder = this._createBuilderFromData(commandDef.data, SlashCommandBuilder);
538
- const mainCommandName = mainBuilder.name;
539
-
540
- if (commandDef.featureFlag) {
541
- this.commandCategoryMap.set(mainCommandName, addon.name);
542
- this.categoryToFeatureMap.set(addon.name, commandDef.featureFlag);
543
- }
544
- if (commandNamesSet.has(mainCommandName)) throw new Error(`Duplicate name: ${mainCommandName}`);
545
- commandNamesSet.add(mainCommandName);
546
- this.client.commands.set(mainCommandName, commandDef);
547
- if (typeof commandDef.autocomplete === 'function') {
548
- this.registerAutocompleteHandler(mainCommandName, commandDef.autocomplete);
549
- }
550
-
551
- const subcommandsList = [];
552
- const groupContents = fs.readdirSync(itemPath, { withFileTypes: true });
553
-
554
- for (const content of groupContents) {
555
- const contentPath = path.join(itemPath, content.name);
556
-
557
- if (content.isFile() && content.name.endsWith('.js') && !content.name.startsWith('_')) {
558
- let subModule = require(contentPath);
559
- if (this._isBaseCommandClass(subModule)) {
560
- subModule = this._instantiateBaseCommand(subModule);
561
- }
562
- if (!subModule.data) continue;
563
- let subBuilder = this._createBuilderFromData(subModule.data, SlashCommandSubcommandBuilder);
564
- mainBuilder.addSubcommand(subBuilder);
565
- this.client.commands.set(`${mainCommandName} ${subBuilder.name}`, subModule);
566
- if (typeof subModule.autocomplete === 'function') {
567
- this.registerAutocompleteHandler(`${mainCommandName} ${subBuilder.name}`, subModule.autocomplete);
568
- }
569
- subcommandsList.push(subBuilder.name);
570
- } else if (content.isDirectory() && fs.existsSync(path.join(contentPath, '_group.js'))) {
571
- let groupDef = require(path.join(contentPath, '_group.js'));
572
- if (this._isBaseCommandClass(groupDef)) {
573
- groupDef = this._instantiateBaseCommand(groupDef);
574
- }
575
- let groupBuilder = this._createBuilderFromData(groupDef.data, SlashCommandSubcommandGroupBuilder);
576
- const subGroupList = [];
577
- const subGroupContents = fs.readdirSync(contentPath, { withFileTypes: true });
578
- for (const subSubItem of subGroupContents) {
579
- if (subSubItem.isFile() && subSubItem.name.endsWith('.js') && !subSubItem.name.startsWith('_')) {
580
- const subSubPath = path.join(contentPath, subSubItem.name);
581
- let subSubModule = require(subSubPath);
582
- if (this._isBaseCommandClass(subSubModule)) {
583
- subSubModule = this._instantiateBaseCommand(subSubModule);
584
- }
585
- if (!subSubModule.data) continue;
586
- let subSubBuilder = this._createBuilderFromData(subSubModule.data, SlashCommandSubcommandBuilder);
587
- groupBuilder.addSubcommand(subSubBuilder);
588
- this.client.commands.set(`${mainCommandName} ${groupBuilder.name} ${subSubBuilder.name}`, subSubModule);
589
- if (typeof subSubModule.autocomplete === 'function') {
590
- this.registerAutocompleteHandler(
591
- `${mainCommandName} ${groupBuilder.name} ${subSubBuilder.name}`,
592
- subSubModule.autocomplete
593
- );
594
- }
595
- subGroupList.push(subSubBuilder.name);
596
- }
597
- }
598
- mainBuilder.addSubcommandGroup(groupBuilder);
599
- subcommandsList.push({ group: groupBuilder.name, subcommands: subGroupList });
600
- }
601
- }
602
- commandDataForDeployment.push(mainBuilder.toJSON());
603
- loadedCommandsSummary.push({ type: 'group', name: mainCommandName, subcommands: subcommandsList });
604
- } else if (item.isFile() && item.name.endsWith('.js') && !item.name.startsWith('_')) {
605
- let commandModule = require(itemPath);
606
- let isClass = false;
607
- if (this._isBaseCommandClass(commandModule)) {
608
- commandModule = this._instantiateBaseCommand(commandModule);
609
- isClass = true;
610
- }
611
-
612
- if (!isClass && commandModule.subcommand) continue;
613
-
614
- let summarySlash = null;
615
- let summaryContext = null;
616
-
617
- if (commandModule.slashCommand) {
618
- const builder = commandModule.slashCommand;
619
- const name = builder.name;
620
- try {
621
- const allLocales = this.container.translator.getLocales();
622
- let nameLocalizations = {};
623
- let descriptionLocalizations = {};
624
- if (typeof allLocales.entries === 'function') {
625
- for (const [lang, translations] of allLocales.entries()) {
626
- const nameKey = `command_${name}_name`;
627
- const descKey = `command_${name}_desc`;
628
- if (translations[nameKey]) nameLocalizations[lang] = translations[nameKey];
629
- if (translations[descKey]) descriptionLocalizations[lang] = translations[descKey];
630
- }
631
- } else {
632
- for (const lang in allLocales) {
633
- const translations = allLocales[lang];
634
- const nameKey = `command_${name}_name`;
635
- const descKey = `command_${name}_desc`;
636
- if (translations[nameKey]) nameLocalizations[lang] = translations[nameKey];
637
- if (translations[descKey]) descriptionLocalizations[lang] = translations[descKey];
638
- }
639
- }
640
- if (Object.keys(nameLocalizations).length > 0) {
641
- builder.setNameLocalizations(nameLocalizations);
642
- }
643
- if (Object.keys(descriptionLocalizations).length > 0) {
644
- builder.setDescriptionLocalizations(descriptionLocalizations);
645
- }
646
- this._applySubcommandLocalizations(builder, name, allLocales);
647
- } catch (e) {
648
- this.logger.warn(`Failed to load localizations for command "${name}": ${e.message}`);
649
- }
650
-
651
- if (commandNamesSet.has(name)) {
652
- this.logger.warn(`Duplicate command name detected: "${name}" in ${itemPath}`);
653
- } else {
654
- commandNamesSet.add(name);
655
-
656
- this.client.commands.set(name, commandModule);
657
- }
658
- commandDataForDeployment.push(builder.toJSON());
659
- summarySlash = { type: 'single', name: name, folder: addon.name, kind: 'slash' };
660
- if (summarySlash) loadedCommandsSummary.push(summarySlash);
661
- this.commandCategoryMap.set(name, addon.name);
662
- }
663
-
664
- if (commandModule.contextMenuCommand) {
665
- const builder = commandModule.contextMenuCommand;
666
- const name = builder.name;
667
- if (commandNamesSet.has(name) && !commandModule.slashCommand) {
668
- this.logger.warn(`Duplicate command name detected: "${name}" in ${itemPath}`);
669
- } else {
670
- if (!commandNamesSet.has(name)) commandNamesSet.add(name);
671
-
672
- this.client.commands.set(name, commandModule);
673
- }
674
- commandDataForDeployment.push(builder.toJSON());
675
- summaryContext = { type: 'single', name: name, folder: addon.name, kind: 'contextMenu' };
676
- if (summaryContext) loadedCommandsSummary.push(summaryContext);
677
- }
678
-
679
- if (!isClass && !commandModule.slashCommand && !commandModule.contextMenuCommand) {
680
- const summary = this.registerCommand(
681
- commandModule,
682
- itemPath,
683
- commandNamesSet,
684
- commandDataForDeployment,
685
- addonPermissionDefaults,
686
- { folderName: addon.name }
687
- );
688
- if (summary) loadedCommandsSummary.push(summary);
689
- }
690
- } else if (item.isDirectory() && !item.name.startsWith('_')) {
691
- const files = fs.readdirSync(itemPath).filter((f) => f.endsWith('.js') && !f.startsWith('_'));
692
- for (const file of files) {
693
- const filePath = path.join(itemPath, file);
694
- let commandModule = require(filePath);
695
- let isClass = false;
696
- if (this._isBaseCommandClass(commandModule)) {
697
- commandModule = this._instantiateBaseCommand(commandModule);
698
- isClass = true;
699
- }
700
-
701
- if (!isClass && commandModule.subcommand) continue;
702
-
703
- let summarySlash = null;
704
- let summaryContext = null;
705
-
706
- if (commandModule.slashCommand) {
707
- const builder = commandModule.slashCommand;
708
- const name = builder.name;
709
- try {
710
- const allLocales = this.container.translator.getLocales();
711
- let nameLocalizations = {};
712
- let descriptionLocalizations = {};
713
- if (typeof allLocales.entries === 'function') {
714
- for (const [lang, translations] of allLocales.entries()) {
715
- const nameKey = `command_${name}_name`;
716
- const descKey = `command_${name}_desc`;
717
- if (translations[nameKey]) nameLocalizations[lang] = translations[nameKey];
718
- if (translations[descKey]) descriptionLocalizations[lang] = translations[descKey];
719
- }
720
- } else {
721
- for (const lang in allLocales) {
722
- const translations = allLocales[lang];
723
- const nameKey = `command_${name}_name`;
724
- const descKey = `command_${name}_desc`;
725
- if (translations[nameKey]) nameLocalizations[lang] = translations[nameKey];
726
- if (translations[descKey]) descriptionLocalizations[lang] = translations[descKey];
727
- }
728
- }
729
- if (Object.keys(nameLocalizations).length > 0) {
730
- builder.setNameLocalizations(nameLocalizations);
731
- }
732
- if (Object.keys(descriptionLocalizations).length > 0) {
733
- builder.setDescriptionLocalizations(descriptionLocalizations);
734
- }
735
- this._applySubcommandLocalizations(builder, name, allLocales);
736
- } catch (e) {
737
- this.logger.warn(`Failed to load localizations for command "${name}": ${e.message}`);
738
- }
739
- this.commandCategoryMap.set(name, item.name);
740
- if (commandNamesSet.has(name)) {
741
- this.logger.warn(`Duplicate slash command name detected: "${name}" in ${filePath}`);
742
- } else {
743
- commandNamesSet.add(name);
744
- this.client.commands.set(name, commandModule);
745
- commandDataForDeployment.push(builder.toJSON());
746
- summarySlash = { type: 'single', name: name, folder: item.name, kind: 'slash' };
747
- if (summarySlash) loadedCommandsSummary.push(summarySlash);
748
- }
749
- }
750
-
751
- if (commandModule.contextMenuCommand) {
752
- const builder = commandModule.contextMenuCommand;
753
- const name = builder.name;
754
- if (!this.client.commands.has(name)) {
755
- this.client.commands.set(name, commandModule);
756
- }
757
- commandDataForDeployment.push(builder.toJSON());
758
- summaryContext = { type: 'single', name: name, folder: item.name, kind: 'contextMenu' };
759
- if (summaryContext) loadedCommandsSummary.push(summaryContext);
760
- }
761
-
762
- if (!isClass && !commandModule.slashCommand && !commandModule.contextMenuCommand) {
763
- const summary = this.registerCommand(
764
- commandModule,
765
- filePath,
766
- commandNamesSet,
767
- commandDataForDeployment,
768
- addonPermissionDefaults,
769
- { folderName: item.name }
770
- );
771
- if (summary) loadedCommandsSummary.push(summary);
772
- }
773
- }
774
- }
775
- }
776
- return loadedCommandsSummary;
777
- }
778
-
779
- /**
780
- * Apply subcommand localizations
781
- * @private
782
- */
783
- _applySubcommandLocalizations(commandBuilder, commandName, allLocales) {
784
- if (Array.isArray(commandBuilder.options)) {
785
- for (const group of commandBuilder.options) {
786
- if (typeof SlashCommandSubcommandGroupBuilder !== 'undefined' && group instanceof SlashCommandSubcommandGroupBuilder) {
787
- const groupName = group.name;
788
-
789
- let groupDescLocalizations = {};
790
- if (typeof allLocales.entries === 'function') {
791
- for (const [lang, translations] of allLocales.entries()) {
792
- const groupDescKey = `command_${commandName}_${groupName}_group_desc`;
793
- if (translations[groupDescKey]) groupDescLocalizations[lang] = translations[groupDescKey];
794
- }
795
- } else {
796
- for (const lang in allLocales) {
797
- const translations = allLocales[lang];
798
- const groupDescKey = `command_${commandName}_${groupName}_group_desc`;
799
- if (translations[groupDescKey]) groupDescLocalizations[lang] = translations[groupDescKey];
800
- }
801
- }
802
- if (Object.keys(groupDescLocalizations).length > 0 && typeof group.setDescriptionLocalizations === 'function') {
803
- group.setDescriptionLocalizations(groupDescLocalizations);
804
- }
805
-
806
- if (Array.isArray(group.options)) {
807
- for (const sub of group.options) {
808
- const subName = sub.name;
809
-
810
- let subDescLocalizations = {};
811
- if (typeof allLocales.entries === 'function') {
812
- for (const [lang, translations] of allLocales.entries()) {
813
- const subDescKey = `command_${commandName}_${groupName}_${subName}_desc`;
814
- if (translations[subDescKey]) subDescLocalizations[lang] = translations[subDescKey];
815
- }
816
- } else {
817
- for (const lang in allLocales) {
818
- const translations = allLocales[lang];
819
- const subDescKey = `command_${commandName}_${groupName}_${subName}_desc`;
820
- if (translations[subDescKey]) subDescLocalizations[lang] = translations[subDescKey];
821
- }
822
- }
823
- if (Object.keys(subDescLocalizations).length > 0 && typeof sub.setDescriptionLocalizations === 'function') {
824
- sub.setDescriptionLocalizations(subDescLocalizations);
825
- }
826
-
827
- if (Array.isArray(sub.options)) {
828
- for (const opt of sub.options) {
829
- const optName = opt.name;
830
- let optDescLocalizations = {};
831
- if (typeof allLocales.entries === 'function') {
832
- for (const [lang, translations] of allLocales.entries()) {
833
- const optDescKey = `command_${commandName}_${groupName}_${subName}_option_${optName}`;
834
- if (translations[optDescKey]) optDescLocalizations[lang] = translations[optDescKey];
835
- }
836
- } else {
837
- for (const lang in allLocales) {
838
- const translations = allLocales[lang];
839
- const optDescKey = `command_${commandName}_${groupName}_${subName}_option_${optName}`;
840
- if (translations[optDescKey]) optDescLocalizations[lang] = translations[optDescKey];
841
- }
842
- }
843
- if (
844
- Object.keys(optDescLocalizations).length > 0 &&
845
- typeof opt.setDescriptionLocalizations === 'function'
846
- ) {
847
- opt.setDescriptionLocalizations(optDescLocalizations);
848
- }
849
- }
850
- }
851
- }
852
- }
853
- } else if (typeof SlashCommandSubcommandBuilder !== 'undefined' && group instanceof SlashCommandSubcommandBuilder) {
854
- const subName = group.name;
855
- let subDescLocalizations = {};
856
- if (typeof allLocales.entries === 'function') {
857
- for (const [lang, translations] of allLocales.entries()) {
858
- const subDescKey = `command_${commandName}_${subName}_desc`;
859
- if (translations[subDescKey]) subDescLocalizations[lang] = translations[subDescKey];
860
- }
861
- } else {
862
- for (const lang in allLocales) {
863
- const translations = allLocales[lang];
864
- const subDescKey = `command_${commandName}_${subName}_desc`;
865
- if (translations[subDescKey]) subDescLocalizations[lang] = translations[subDescKey];
866
- }
867
- }
868
- if (Object.keys(subDescLocalizations).length > 0 && typeof group.setDescriptionLocalizations === 'function') {
869
- group.setDescriptionLocalizations(subDescLocalizations);
870
- }
871
-
872
- if (Array.isArray(group.options)) {
873
- for (const opt of group.options) {
874
- const optName = opt.name;
875
- let optDescLocalizations = {};
876
- if (typeof allLocales.entries === 'function') {
877
- for (const [lang, translations] of allLocales.entries()) {
878
- const optDescKey = `command_${commandName}_${subName}_option_${optName}`;
879
- if (translations[optDescKey]) optDescLocalizations[lang] = translations[optDescKey];
880
- }
881
- } else {
882
- for (const lang in allLocales) {
883
- const translations = allLocales[lang];
884
- const optDescKey = `command_${commandName}_${subName}_option_${optName}`;
885
- if (translations[optDescKey]) optDescLocalizations[lang] = translations[optDescKey];
886
- }
887
- }
888
- if (Object.keys(optDescLocalizations).length > 0 && typeof opt.setDescriptionLocalizations === 'function') {
889
- opt.setDescriptionLocalizations(optDescLocalizations);
890
- }
891
- }
892
- }
893
- }
894
- }
895
- }
896
- }
897
-
898
- /**
899
- * Log addon summary
900
- * @private
901
- */
902
- _logAddonSummary(addonSummaries) {
903
- this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬[ Addon(s) Loaded ]▬▬▬▬▬▬▬▬▬▬▬▬▬');
904
- for (const addon of addonSummaries) {
905
- this.logger.info(`📦 ${addon.name} (v${addon.version})`);
906
- this.logger.info(' ⚙️ Command(s)');
907
- if (!addon.commands.length) {
908
- this.logger.info(' (no commands registered)');
909
- } else {
910
- for (const cmd of addon.commands) {
911
- if (cmd.type === 'group') {
912
- this.logger.info(` └─ /${cmd.name}`);
913
- for (const sub of cmd.subcommands) {
914
- if (typeof sub === 'string') {
915
- this.logger.info(` └─ ${sub}`);
916
- } else if (typeof sub === 'object' && sub.group) {
917
- this.logger.info(` └─ [${sub.group}]`);
918
- for (const subsub of sub.subcommands) {
919
- this.logger.info(` └─ ${subsub}`);
920
- }
921
- }
922
- }
923
- } else if (cmd.type === 'single') {
924
- let kindLabel = '';
925
- if (cmd.kind === 'slash') kindLabel = ' [slash]';
926
- else if (cmd.kind === 'contextMenu') kindLabel = ' [contextMenu]';
927
- if (cmd.folder) {
928
- this.logger.info(` └─ /${cmd.name} (${cmd.folder})${kindLabel}`);
929
- } else {
930
- this.logger.info(` └─ /${cmd.name}${kindLabel}`);
931
- }
932
- }
933
- }
934
- }
935
- if (addon.register && addon.register.length) {
936
- this.logger.info(' 🧩 Component(s)');
937
- for (const reg of addon.register) {
938
- this.logger.info(` ${reg}`);
939
- }
940
- }
941
- if (addon.events && addon.events.length) {
942
- this.logger.info(' 📢 Event(s)');
943
- for (const ev of addon.events) {
944
- this.logger.info(` └─ ${ev}`);
945
- }
946
- }
947
- }
948
- }
949
-
950
- /**
951
- * Get handler maps for other managers
952
- */
953
- getHandlers() {
954
- return {
955
- buttonHandlers: this.buttonHandlers,
956
- modalHandlers: this.modalHandlers,
957
- selectMenuHandlers: this.selectMenuHandlers,
958
- autocompleteHandlers: this.autocompleteHandlers,
959
- commandCategoryMap: this.commandCategoryMap,
960
- categoryToFeatureMap: this.categoryToFeatureMap,
961
- eventHandlers: this.eventHandlers,
962
- };
963
- }
27
+ /**
28
+ * 🗝️ AddonManager Constructor
29
+ * Initializes the addon manager with necessary collections and maps.
30
+ * @param {Object} client - Discord client instance
31
+ * @param {Object} container - Dependency container
32
+ */
33
+ constructor({ client, container }) {
34
+ this.client = client;
35
+ this.container = container;
36
+ this.logger = this.container.logger;
37
+
38
+ this.buttonHandlers = new Map();
39
+ this.modalHandlers = new Map();
40
+ this.selectMenuHandlers = new Map();
41
+ this.autocompleteHandlers = new Map();
42
+ this.commandCategoryMap = new Map();
43
+ this.categoryToFeatureMap = new Map();
44
+ this.embedDrafts = new Collection();
45
+ this.eventHandlers = new Map();
46
+ }
47
+
48
+ /**
49
+ * 🔘 Register Button Handler
50
+ * Registers a handler function for a specific button customId.
51
+ * @param {string} customId - The customId of the button
52
+ * @param {Function} handler - The handler function to execute
53
+ */
54
+ registerButtonHandler(customId, handler) {
55
+ if (this.buttonHandlers.has(customId)) {
56
+ this.logger.warn(
57
+ `[REGISTRATION] Warning: Button handler for [${customId}] already exists and will be overwritten.`,
58
+ );
59
+ }
60
+ this.buttonHandlers.set(customId, handler);
61
+ }
62
+ /**
63
+ * 🔽 Register Select Menu Handler
64
+ * Registers a handler function for a specific select menu customId prefix.
65
+ * @param {string} customIdPrefix - The prefix of the select menu customId
66
+ * @param {Function} handler - The handler function to execute
67
+ */
68
+ registerSelectMenuHandler(customIdPrefix, handler) {
69
+ if (this.selectMenuHandlers.has(customIdPrefix)) {
70
+ this.logger.warn(
71
+ `[REGISTRATION] Warning: Select menu handler for [${customIdPrefix}] already exists and will be overwritten.`,
72
+ );
73
+ }
74
+ this.selectMenuHandlers.set(customIdPrefix, handler);
75
+ }
76
+
77
+ /**
78
+ * 📝 Register Modal Handler
79
+ * Registers a handler function for a modal, using a prefix of the customId.
80
+ * @param {string} customIdPrefix - The prefix of the modal customId
81
+ * @param {Function} handler - The handler function to execute
82
+ */
83
+ registerModalHandler(customIdPrefix, handler) {
84
+ if (this.modalHandlers.has(customIdPrefix)) {
85
+ this.logger.warn(
86
+ `[REGISTRATION] Warning: Modal handler for [${customIdPrefix}] already exists and will be overwritten.`,
87
+ );
88
+ }
89
+ this.modalHandlers.set(customIdPrefix, handler);
90
+ }
91
+
92
+ /**
93
+ * 📋 Register Autocomplete Handler
94
+ * Registers a handler for autocomplete interactions for a specific command or subcommand.
95
+ * @param {string} commandName - The command or subcommand key
96
+ * @param {Function} handler - The autocomplete handler function
97
+ */
98
+ registerAutocompleteHandler(commandName, handler) {
99
+ if (this.autocompleteHandlers.has(commandName)) {
100
+ this.logger.warn(
101
+ `[REGISTRATION] Warning: Autocomplete handler for [${commandName}] already exists and will be overwritten.`,
102
+ );
103
+ }
104
+ this.autocompleteHandlers.set(commandName, handler);
105
+ }
106
+
107
+ /**
108
+ * 🔍 Check if module is a BaseCommand class
109
+ * @param {any} module - The module to check
110
+ * @returns {boolean} True if module is a class extending BaseCommand
111
+ * @private
112
+ */
113
+ _isBaseCommandClass(module) {
114
+ if (typeof module !== 'function') return false;
115
+ if (!module.prototype) return false;
116
+
117
+ const hasExecute = typeof module.prototype.execute === 'function';
118
+ return hasExecute;
119
+ }
120
+
121
+ /**
122
+ * 🏗️ Instantiate and prepare BaseCommand class
123
+ * @param {Function} CommandClass - The command class to instantiate
124
+ * @returns {Object} Command instance with proper structure
125
+ * @private
126
+ */
127
+ _instantiateBaseCommand(CommandClass) {
128
+ try {
129
+ return new CommandClass(this.container);
130
+ } catch (error) {
131
+ this.logger.error(`Failed to instantiate BaseCommand class:`, error);
132
+ throw error;
133
+ }
134
+ }
135
+
136
+ /**
137
+ * INTERNAL: Creates a Builder instance (Slash, Subcommand, Group) from a module's data property.
138
+ * @param {Object} data - The data property (can be function, builder, or object)
139
+ * @param {Class} BuilderClass - The d.js class to use (SlashCommandBuilder, etc.)
140
+ * @returns {SlashCommandBuilder|SlashCommandSubcommandBuilder|SlashCommandSubcommandGroupBuilder}
141
+ * @private
142
+ */
143
+ _createBuilderFromData(data, BuilderClass) {
144
+ let builder = new BuilderClass();
145
+
146
+ if (typeof data === 'function') {
147
+ data(builder);
148
+ } else if (data instanceof BuilderClass) {
149
+ builder = data;
150
+ } else if (typeof data === 'object') {
151
+ builder.setName(data.name || 'unnamed');
152
+ builder.setDescription(data.description || 'No description');
153
+
154
+ if (BuilderClass === SlashCommandBuilder) {
155
+ builder.setDescription(data.description || 'No description');
156
+ if (data.permissions) {
157
+ builder.setDefaultMemberPermissions(data.permissions);
158
+ }
159
+ if (data.guildOnly !== undefined) {
160
+ builder.setDMPermission(!data.guildOnly);
161
+ }
162
+ } else if (BuilderClass === ContextMenuCommandBuilder) {
163
+ builder.setType(data.type || ApplicationCommandType.User);
164
+ if (data.permissions) {
165
+ builder.setDefaultMemberPermissions(data.permissions);
166
+ }
167
+ if (data.guildOnly !== undefined) {
168
+ builder.setDMPermission(!data.guildOnly);
169
+ }
170
+ }
171
+ }
172
+ return builder;
173
+ }
174
+
175
+ /**
176
+ * 📝 Register Command Helper
177
+ * Registers a single command file/module, adds it to the command collection, and prepares it for deployment.
178
+ * @param {Object} module - The command module
179
+ * @param {string} filePath - The file path of the command
180
+ * @param {Set} commandNamesSet - Set of already registered command names
181
+ * @param {Array} commandDataForDeployment - Array to collect command data for deployment
182
+ * @param {Object} permissionDefaults - Permission defaults for the command
183
+ * @param {Object} options - Additional options (e.g., folderName)
184
+ * @returns {Object|null} Summary object for logging, or null if not registered
185
+ */
186
+ registerCommand(
187
+ module,
188
+ filePath,
189
+ commandNamesSet,
190
+ commandDataForDeployment,
191
+ permissionDefaults = {},
192
+ options = {},
193
+ ) {
194
+ if (this._isBaseCommandClass(module)) {
195
+ module = this._instantiateBaseCommand(module);
196
+ }
197
+
198
+ if (!module || !module.data) return null;
199
+
200
+ let builderClass;
201
+
202
+ if (module.data instanceof ContextMenuCommandBuilder) {
203
+ builderClass = ContextMenuCommandBuilder;
204
+ } else {
205
+ builderClass = SlashCommandBuilder;
206
+ }
207
+
208
+ const commandBuilder = this._createBuilderFromData(
209
+ module.data,
210
+ builderClass,
211
+ );
212
+
213
+ const commandName = commandBuilder.name;
214
+ const category =
215
+ options.folderName || path.basename(path.dirname(filePath));
216
+
217
+ const categoryDefaults = permissionDefaults[category] || {};
218
+ const finalCommand = {
219
+ ...categoryDefaults,
220
+ ...module,
221
+ };
222
+
223
+ this.commandCategoryMap.set(commandName, category);
224
+ if (commandNamesSet.has(commandName)) {
225
+ throw new Error(
226
+ `Duplicate command name detected: "${commandName}" in ${filePath}`,
227
+ );
228
+ }
229
+
230
+ commandNamesSet.add(commandName);
231
+ this.client.commands.set(commandName, finalCommand);
232
+ commandDataForDeployment.push(commandBuilder.toJSON());
233
+
234
+ if (typeof finalCommand.autocomplete === 'function') {
235
+ this.registerAutocompleteHandler(commandName, finalCommand.autocomplete);
236
+ }
237
+
238
+ return {
239
+ type: 'single',
240
+ name: commandName,
241
+ folder: category,
242
+ };
243
+ }
244
+
245
+ /**
246
+ * 🧩 Load Addons & Register Commands/Events
247
+ * Loads all addons from the addons directory, registers their commands, events, and components.
248
+ * @param {Object} kythiaInstance - The main Kythia instance for addon registration
249
+ * @returns {Promise<Array>} Array of command data for deployment
250
+ */
251
+ async loadAddons(kythiaInstance) {
252
+ this.logger.info('🔌 Loading & Registering Kythia Addons...');
253
+ const commandDataForDeployment = [];
254
+ const addonsDir = path.join(this.container.appRoot, 'addons');
255
+ if (!fs.existsSync(addonsDir)) return commandDataForDeployment;
256
+
257
+ let addonFolders = fs
258
+ .readdirSync(addonsDir, { withFileTypes: true })
259
+ .filter((d) => d.isDirectory() && !d.name.startsWith('_'));
260
+
261
+ const coreAddon = addonFolders.find((d) => d.name === 'core');
262
+ const otherAddons = addonFolders.filter((d) => d.name !== 'core');
263
+ if (coreAddon) {
264
+ addonFolders = [coreAddon, ...otherAddons];
265
+ }
266
+
267
+ const commandNamesSet = new Set();
268
+ const addonSummaries = [];
269
+
270
+ for (const addon of addonFolders) {
271
+ const addonDir = path.join(addonsDir, addon.name);
272
+ let addonVersion = 'v0.0.0-alpha';
273
+
274
+ try {
275
+ const addonJsonPath = path.join(addonDir, 'addon.json');
276
+ if (fs.existsSync(addonJsonPath)) {
277
+ let addonJson;
278
+ try {
279
+ const addonJsonRaw = fs.readFileSync(addonJsonPath, 'utf8');
280
+ addonJson = JSON.parse(addonJsonRaw);
281
+ } catch (jsonErr) {
282
+ this.logger.warn(
283
+ `🔴 Failed to parse addon.json for ${addon.name}: ${jsonErr.message}`,
284
+ );
285
+ continue;
286
+ }
287
+
288
+ addonVersion = addonJson.version || 'v0.0.0-alpha';
289
+ if (addonJson.active === false) {
290
+ this.logger.info(`🟠 Addon ${addon.name.toUpperCase()} disabled`);
291
+ continue;
292
+ }
293
+ if (addonJson.featureFlag) {
294
+ this.commandCategoryMap.set(addon.name, addon.name);
295
+ this.categoryToFeatureMap.set(addon.name, addonJson.featureFlag);
296
+ }
297
+ } else {
298
+ this.logger.warn(
299
+ `🔴 Addon ${addon.name.toUpperCase()} is missing addon.json. Skipping.`,
300
+ );
301
+ continue;
302
+ }
303
+ } catch (e) {
304
+ this.logger.warn(
305
+ `🔴 Error reading addon.json for ${addonDir}: ${e.message}`,
306
+ );
307
+ continue;
308
+ }
309
+
310
+ try {
311
+ const configAddons = this.container.kythiaConfig?.addons || {};
312
+ if (configAddons.all?.active === false) {
313
+ this.logger.info(
314
+ `🟠 Addon ${addon.name.toUpperCase()} disabled via kythia config`,
315
+ );
316
+ continue;
317
+ } else if (configAddons[addon.name]?.active === false) {
318
+ this.logger.info(
319
+ `🟠 Addon ${addon.name.toUpperCase()} disabled via kythia config`,
320
+ );
321
+ continue;
322
+ }
323
+ } catch (e) {
324
+ this.logger.warn(
325
+ `🔴 Error checking config for addon ${addon.name.toUpperCase()}: ${e.message}`,
326
+ );
327
+ }
328
+
329
+ let addonPermissionDefaults = {};
330
+ const permissionsFilePath = path.join(addonDir, 'permissions.js');
331
+
332
+ if (fs.existsSync(permissionsFilePath)) {
333
+ try {
334
+ addonPermissionDefaults = require(permissionsFilePath);
335
+ this.logger.info(
336
+ ` └─> Found and loaded permission defaults for addon '${addon.name.toUpperCase()}'`,
337
+ );
338
+ } catch (e) {
339
+ this.logger.warn(
340
+ ` └─> Failed to load permissions.js for addon '${addon.name.toUpperCase()}': ${e.message}`,
341
+ );
342
+ }
343
+ }
344
+
345
+ const loadedCommandsSummary = [];
346
+ const loadedEventsSummary = [];
347
+ const loadedRegisterSummary = [];
348
+
349
+ const commandsPath = path.join(addonDir, 'commands');
350
+ if (fs.existsSync(commandsPath)) {
351
+ try {
352
+ const commandsResult = await this._loadCommandsFromPath(
353
+ commandsPath,
354
+ addon,
355
+ addonPermissionDefaults,
356
+ commandNamesSet,
357
+ commandDataForDeployment,
358
+ );
359
+ loadedCommandsSummary.push(...commandsResult);
360
+ } catch (error) {
361
+ this.logger.error(
362
+ `❌ Failed to load commands from addon "${addon.name}":`,
363
+ error,
364
+ );
365
+ }
366
+ }
367
+
368
+ const registerPath = path.join(addonDir, 'register.js');
369
+ if (fs.existsSync(registerPath)) {
370
+ try {
371
+ const registration = require(registerPath);
372
+ if (typeof registration.initialize === 'function') {
373
+ const registrationSummary =
374
+ await registration.initialize(kythiaInstance);
375
+ if (
376
+ Array.isArray(registrationSummary) &&
377
+ registrationSummary.length > 0
378
+ ) {
379
+ loadedRegisterSummary.push(...registrationSummary);
380
+ }
381
+ }
382
+ } catch (error) {
383
+ this.logger.error(
384
+ `❌ Failed to register components for [${addon.name}]:`,
385
+ error,
386
+ );
387
+ }
388
+ }
389
+
390
+ const eventsPath = path.join(addonDir, 'events');
391
+ if (fs.existsSync(eventsPath)) {
392
+ const eventFiles = fs
393
+ .readdirSync(eventsPath)
394
+ .filter((file) => file.endsWith('.js'));
395
+ for (const file of eventFiles) {
396
+ const eventName = path.basename(file, '.js');
397
+ try {
398
+ const eventHandler = require(path.join(eventsPath, file));
399
+ if (typeof eventHandler === 'function') {
400
+ if (!this.eventHandlers.has(eventName)) {
401
+ this.eventHandlers.set(eventName, []);
402
+ }
403
+ this.eventHandlers.get(eventName).push(eventHandler);
404
+ loadedEventsSummary.push(eventName);
405
+ }
406
+ } catch (error) {
407
+ this.logger.error(
408
+ `❌ Failed to register event [${eventName}] for [${addon.name}]:`,
409
+ error,
410
+ );
411
+ }
412
+ }
413
+ }
414
+
415
+ addonSummaries.push({
416
+ name: addon.name,
417
+ version: addonVersion,
418
+ commands: loadedCommandsSummary,
419
+ events: loadedEventsSummary,
420
+ register: loadedRegisterSummary,
421
+ });
422
+ }
423
+
424
+ this._logAddonSummary(addonSummaries);
425
+ return commandDataForDeployment;
426
+ }
427
+
428
+ /**
429
+ * Load commands from a specific path
430
+ * @private
431
+ */
432
+ async _loadCommandsFromPath(
433
+ commandsPath,
434
+ addon,
435
+ addonPermissionDefaults,
436
+ commandNamesSet,
437
+ commandDataForDeployment,
438
+ ) {
439
+ const isTopLevelCommandGroup = fs.existsSync(
440
+ path.join(commandsPath, '_command.js'),
441
+ );
442
+
443
+ if (isTopLevelCommandGroup) {
444
+ return await this._loadTopLevelCommandGroup(
445
+ commandsPath,
446
+ addon,
447
+ addonPermissionDefaults,
448
+ commandNamesSet,
449
+ commandDataForDeployment,
450
+ );
451
+ } else {
452
+ return await this._loadIndividualCommands(
453
+ commandsPath,
454
+ addon,
455
+ addonPermissionDefaults,
456
+ commandNamesSet,
457
+ commandDataForDeployment,
458
+ );
459
+ }
460
+ }
461
+
462
+ /**
463
+ * Load top-level command group (supports BaseCommand classes)
464
+ * @private
465
+ */
466
+ async _loadTopLevelCommandGroup(
467
+ commandsPath,
468
+ addon,
469
+ commandNamesSet,
470
+ commandDataForDeployment,
471
+ ) {
472
+ const loadedCommandsSummary = [];
473
+ let commandDef = require(path.join(commandsPath, '_command.js'));
474
+
475
+ if (this._isBaseCommandClass(commandDef)) {
476
+ commandDef = this._instantiateBaseCommand(commandDef);
477
+ }
478
+
479
+ const mainBuilder = this._createBuilderFromData(
480
+ commandDef.data,
481
+ SlashCommandBuilder,
482
+ );
483
+
484
+ const mainCommandName = mainBuilder.name;
485
+
486
+ if (commandDef.featureFlag) {
487
+ this.commandCategoryMap.set(mainCommandName, addon.name);
488
+ this.categoryToFeatureMap.set(addon.name, commandDef.featureFlag);
489
+ }
490
+ this.commandCategoryMap.set(mainCommandName, addon.name);
491
+
492
+ if (commandNamesSet.has(mainCommandName))
493
+ throw new Error(`Duplicate command name: ${mainCommandName}`);
494
+ commandNamesSet.add(mainCommandName);
495
+
496
+ this.client.commands.set(mainCommandName, commandDef);
497
+
498
+ if (typeof commandDef.autocomplete === 'function') {
499
+ this.registerAutocompleteHandler(
500
+ mainCommandName,
501
+ commandDef.autocomplete,
502
+ );
503
+ }
504
+
505
+ const loadedSubcommandsSummary = [];
506
+ const contents = fs.readdirSync(commandsPath, { withFileTypes: true });
507
+
508
+ for (const item of contents) {
509
+ const itemPath = path.join(commandsPath, item.name);
510
+
511
+ if (
512
+ item.isFile() &&
513
+ item.name.endsWith('.js') &&
514
+ !item.name.startsWith('_')
515
+ ) {
516
+ let subModule = require(itemPath);
517
+
518
+ const isSubcommand =
519
+ subModule.subcommand === true || this._isBaseCommandClass(subModule);
520
+
521
+ if (this._isBaseCommandClass(subModule)) {
522
+ subModule = this._instantiateBaseCommand(subModule);
523
+ }
524
+
525
+ if (!isSubcommand) continue;
526
+ if (!subModule.data) continue;
527
+
528
+ const subBuilder = this._createBuilderFromData(
529
+ subModule.data,
530
+ SlashCommandSubcommandBuilder,
531
+ );
532
+
533
+ mainBuilder.addSubcommand(subBuilder);
534
+ this.client.commands.set(
535
+ `${mainCommandName} ${subBuilder.name}`,
536
+ subModule,
537
+ );
538
+
539
+ if (typeof subModule.autocomplete === 'function') {
540
+ this.registerAutocompleteHandler(
541
+ `${mainCommandName} ${subBuilder.name}`,
542
+ subModule.autocomplete,
543
+ );
544
+ }
545
+
546
+ loadedSubcommandsSummary.push(subBuilder.name);
547
+ } else if (item.isDirectory()) {
548
+ const groupDefPath = path.join(itemPath, '_group.js');
549
+
550
+ if (!fs.existsSync(groupDefPath)) {
551
+ continue;
552
+ }
553
+
554
+ try {
555
+ let groupModule = require(groupDefPath);
556
+
557
+ if (this._isBaseCommandClass(groupModule)) {
558
+ groupModule = this._instantiateBaseCommand(groupModule);
559
+ }
560
+
561
+ if (!groupModule.data) continue;
562
+
563
+ const groupBuilder = this._createBuilderFromData(
564
+ groupModule.data,
565
+ SlashCommandSubcommandGroupBuilder,
566
+ );
567
+
568
+ const subcommandsInGroupSummary = [];
569
+ const subCommandFiles = fs
570
+ .readdirSync(itemPath)
571
+ .filter((f) => f.endsWith('.js') && !f.startsWith('_'));
572
+
573
+ for (const file of subCommandFiles) {
574
+ const subCommandPath = path.join(itemPath, file);
575
+ let subModule = require(subCommandPath);
576
+
577
+ if (this._isBaseCommandClass(subModule)) {
578
+ subModule = this._instantiateBaseCommand(subModule);
579
+ }
580
+
581
+ if (!subModule.data) continue;
582
+
583
+ const subBuilder = this._createBuilderFromData(
584
+ subModule.data,
585
+ SlashCommandSubcommandBuilder,
586
+ );
587
+
588
+ groupBuilder.addSubcommand(subBuilder);
589
+
590
+ const commandKey = `${mainCommandName} ${groupBuilder.name} ${subBuilder.name}`;
591
+
592
+ this.client.commands.set(commandKey, subModule);
593
+
594
+ if (typeof subModule.autocomplete === 'function') {
595
+ this.registerAutocompleteHandler(
596
+ commandKey,
597
+ subModule.autocomplete,
598
+ );
599
+ }
600
+
601
+ subcommandsInGroupSummary.push(subBuilder.name);
602
+ }
603
+
604
+ mainBuilder.addSubcommandGroup(groupBuilder);
605
+
606
+ loadedSubcommandsSummary.push({
607
+ group: groupBuilder.name,
608
+ subcommands: subcommandsInGroupSummary,
609
+ });
610
+ } catch (e) {
611
+ this.logger.error(
612
+ `❌ Failed to load subcommand group from ${itemPath}:`,
613
+ e,
614
+ );
615
+ }
616
+ }
617
+ }
618
+
619
+ commandDataForDeployment.push(mainBuilder.toJSON());
620
+ loadedCommandsSummary.push({
621
+ type: 'group',
622
+ name: mainCommandName,
623
+ subcommands: loadedSubcommandsSummary,
624
+ });
625
+
626
+ return loadedCommandsSummary;
627
+ }
628
+
629
+ /**
630
+ * Load individual commands (supports BaseCommand classes)
631
+ * @private
632
+ */
633
+ async _loadIndividualCommands(
634
+ commandsPath,
635
+ addon,
636
+ addonPermissionDefaults,
637
+ commandNamesSet,
638
+ commandDataForDeployment,
639
+ ) {
640
+ const loadedCommandsSummary = [];
641
+ const commandItems = fs.readdirSync(commandsPath, { withFileTypes: true });
642
+
643
+ for (const item of commandItems) {
644
+ const itemPath = path.join(commandsPath, item.name);
645
+
646
+ if (
647
+ item.isDirectory() &&
648
+ fs.existsSync(path.join(itemPath, '_command.js'))
649
+ ) {
650
+ let commandDef = require(path.join(itemPath, '_command.js'));
651
+ if (this._isBaseCommandClass(commandDef)) {
652
+ commandDef = this._instantiateBaseCommand(commandDef);
653
+ }
654
+ const mainBuilder = this._createBuilderFromData(
655
+ commandDef.data,
656
+ SlashCommandBuilder,
657
+ );
658
+ const mainCommandName = mainBuilder.name;
659
+
660
+ if (commandDef.featureFlag) {
661
+ this.commandCategoryMap.set(mainCommandName, addon.name);
662
+ this.categoryToFeatureMap.set(addon.name, commandDef.featureFlag);
663
+ }
664
+ if (commandNamesSet.has(mainCommandName))
665
+ throw new Error(`Duplicate name: ${mainCommandName}`);
666
+ commandNamesSet.add(mainCommandName);
667
+ this.client.commands.set(mainCommandName, commandDef);
668
+ if (typeof commandDef.autocomplete === 'function') {
669
+ this.registerAutocompleteHandler(
670
+ mainCommandName,
671
+ commandDef.autocomplete,
672
+ );
673
+ }
674
+
675
+ const subcommandsList = [];
676
+ const groupContents = fs.readdirSync(itemPath, { withFileTypes: true });
677
+
678
+ for (const content of groupContents) {
679
+ const contentPath = path.join(itemPath, content.name);
680
+
681
+ if (
682
+ content.isFile() &&
683
+ content.name.endsWith('.js') &&
684
+ !content.name.startsWith('_')
685
+ ) {
686
+ let subModule = require(contentPath);
687
+ if (this._isBaseCommandClass(subModule)) {
688
+ subModule = this._instantiateBaseCommand(subModule);
689
+ }
690
+ if (!subModule.data) continue;
691
+ const subBuilder = this._createBuilderFromData(
692
+ subModule.data,
693
+ SlashCommandSubcommandBuilder,
694
+ );
695
+ mainBuilder.addSubcommand(subBuilder);
696
+ this.client.commands.set(
697
+ `${mainCommandName} ${subBuilder.name}`,
698
+ subModule,
699
+ );
700
+ if (typeof subModule.autocomplete === 'function') {
701
+ this.registerAutocompleteHandler(
702
+ `${mainCommandName} ${subBuilder.name}`,
703
+ subModule.autocomplete,
704
+ );
705
+ }
706
+ subcommandsList.push(subBuilder.name);
707
+ } else if (
708
+ content.isDirectory() &&
709
+ fs.existsSync(path.join(contentPath, '_group.js'))
710
+ ) {
711
+ let groupDef = require(path.join(contentPath, '_group.js'));
712
+ if (this._isBaseCommandClass(groupDef)) {
713
+ groupDef = this._instantiateBaseCommand(groupDef);
714
+ }
715
+ const groupBuilder = this._createBuilderFromData(
716
+ groupDef.data,
717
+ SlashCommandSubcommandGroupBuilder,
718
+ );
719
+ const subGroupList = [];
720
+ const subGroupContents = fs.readdirSync(contentPath, {
721
+ withFileTypes: true,
722
+ });
723
+ for (const subSubItem of subGroupContents) {
724
+ if (
725
+ subSubItem.isFile() &&
726
+ subSubItem.name.endsWith('.js') &&
727
+ !subSubItem.name.startsWith('_')
728
+ ) {
729
+ const subSubPath = path.join(contentPath, subSubItem.name);
730
+ let subSubModule = require(subSubPath);
731
+ if (this._isBaseCommandClass(subSubModule)) {
732
+ subSubModule = this._instantiateBaseCommand(subSubModule);
733
+ }
734
+ if (!subSubModule.data) continue;
735
+ const subSubBuilder = this._createBuilderFromData(
736
+ subSubModule.data,
737
+ SlashCommandSubcommandBuilder,
738
+ );
739
+ groupBuilder.addSubcommand(subSubBuilder);
740
+ this.client.commands.set(
741
+ `${mainCommandName} ${groupBuilder.name} ${subSubBuilder.name}`,
742
+ subSubModule,
743
+ );
744
+ if (typeof subSubModule.autocomplete === 'function') {
745
+ this.registerAutocompleteHandler(
746
+ `${mainCommandName} ${groupBuilder.name} ${subSubBuilder.name}`,
747
+ subSubModule.autocomplete,
748
+ );
749
+ }
750
+ subGroupList.push(subSubBuilder.name);
751
+ }
752
+ }
753
+ mainBuilder.addSubcommandGroup(groupBuilder);
754
+ subcommandsList.push({
755
+ group: groupBuilder.name,
756
+ subcommands: subGroupList,
757
+ });
758
+ }
759
+ }
760
+ commandDataForDeployment.push(mainBuilder.toJSON());
761
+ loadedCommandsSummary.push({
762
+ type: 'group',
763
+ name: mainCommandName,
764
+ subcommands: subcommandsList,
765
+ });
766
+ } else if (
767
+ item.isFile() &&
768
+ item.name.endsWith('.js') &&
769
+ !item.name.startsWith('_')
770
+ ) {
771
+ let commandModule = require(itemPath);
772
+ let isClass = false;
773
+ if (this._isBaseCommandClass(commandModule)) {
774
+ commandModule = this._instantiateBaseCommand(commandModule);
775
+ isClass = true;
776
+ }
777
+
778
+ if (!isClass && commandModule.subcommand) continue;
779
+
780
+ let summarySlash = null;
781
+ let summaryContext = null;
782
+
783
+ if (commandModule.slashCommand) {
784
+ const builder = commandModule.slashCommand;
785
+ const name = builder.name;
786
+ try {
787
+ const allLocales = this.container.translator.getLocales();
788
+ const nameLocalizations = {};
789
+ const descriptionLocalizations = {};
790
+ if (typeof allLocales.entries === 'function') {
791
+ for (const [lang, translations] of allLocales.entries()) {
792
+ const nameKey = `command_${name}_name`;
793
+ const descKey = `command_${name}_desc`;
794
+ if (translations[nameKey])
795
+ nameLocalizations[lang] = translations[nameKey];
796
+ if (translations[descKey])
797
+ descriptionLocalizations[lang] = translations[descKey];
798
+ }
799
+ } else {
800
+ for (const lang in allLocales) {
801
+ const translations = allLocales[lang];
802
+ const nameKey = `command_${name}_name`;
803
+ const descKey = `command_${name}_desc`;
804
+ if (translations[nameKey])
805
+ nameLocalizations[lang] = translations[nameKey];
806
+ if (translations[descKey])
807
+ descriptionLocalizations[lang] = translations[descKey];
808
+ }
809
+ }
810
+ if (Object.keys(nameLocalizations).length > 0) {
811
+ builder.setNameLocalizations(nameLocalizations);
812
+ }
813
+ if (Object.keys(descriptionLocalizations).length > 0) {
814
+ builder.setDescriptionLocalizations(descriptionLocalizations);
815
+ }
816
+ this._applySubcommandLocalizations(builder, name, allLocales);
817
+ } catch (e) {
818
+ this.logger.warn(
819
+ `Failed to load localizations for command "${name}": ${e.message}`,
820
+ );
821
+ }
822
+
823
+ if (commandNamesSet.has(name)) {
824
+ this.logger.warn(
825
+ `Duplicate command name detected: "${name}" in ${itemPath}`,
826
+ );
827
+ } else {
828
+ commandNamesSet.add(name);
829
+
830
+ this.client.commands.set(name, commandModule);
831
+ }
832
+ commandDataForDeployment.push(builder.toJSON());
833
+ summarySlash = {
834
+ type: 'single',
835
+ name: name,
836
+ folder: addon.name,
837
+ kind: 'slash',
838
+ };
839
+ if (summarySlash) loadedCommandsSummary.push(summarySlash);
840
+ this.commandCategoryMap.set(name, addon.name);
841
+ }
842
+
843
+ if (commandModule.contextMenuCommand) {
844
+ const builder = commandModule.contextMenuCommand;
845
+ const name = builder.name;
846
+ if (commandNamesSet.has(name) && !commandModule.slashCommand) {
847
+ this.logger.warn(
848
+ `Duplicate command name detected: "${name}" in ${itemPath}`,
849
+ );
850
+ } else {
851
+ if (!commandNamesSet.has(name)) commandNamesSet.add(name);
852
+
853
+ this.client.commands.set(name, commandModule);
854
+ }
855
+ commandDataForDeployment.push(builder.toJSON());
856
+ summaryContext = {
857
+ type: 'single',
858
+ name: name,
859
+ folder: addon.name,
860
+ kind: 'contextMenu',
861
+ };
862
+ if (summaryContext) loadedCommandsSummary.push(summaryContext);
863
+ }
864
+
865
+ if (
866
+ !isClass &&
867
+ !commandModule.slashCommand &&
868
+ !commandModule.contextMenuCommand
869
+ ) {
870
+ const summary = this.registerCommand(
871
+ commandModule,
872
+ itemPath,
873
+ commandNamesSet,
874
+ commandDataForDeployment,
875
+ addonPermissionDefaults,
876
+ { folderName: addon.name },
877
+ );
878
+ if (summary) loadedCommandsSummary.push(summary);
879
+ }
880
+ } else if (item.isDirectory() && !item.name.startsWith('_')) {
881
+ const files = fs
882
+ .readdirSync(itemPath)
883
+ .filter((f) => f.endsWith('.js') && !f.startsWith('_'));
884
+ for (const file of files) {
885
+ const filePath = path.join(itemPath, file);
886
+ let commandModule = require(filePath);
887
+ let isClass = false;
888
+ if (this._isBaseCommandClass(commandModule)) {
889
+ commandModule = this._instantiateBaseCommand(commandModule);
890
+ isClass = true;
891
+ }
892
+
893
+ if (!isClass && commandModule.subcommand) continue;
894
+
895
+ let summarySlash = null;
896
+ let summaryContext = null;
897
+
898
+ if (commandModule.slashCommand) {
899
+ const builder = commandModule.slashCommand;
900
+ const name = builder.name;
901
+ try {
902
+ const allLocales = this.container.translator.getLocales();
903
+ const nameLocalizations = {};
904
+ const descriptionLocalizations = {};
905
+ if (typeof allLocales.entries === 'function') {
906
+ for (const [lang, translations] of allLocales.entries()) {
907
+ const nameKey = `command_${name}_name`;
908
+ const descKey = `command_${name}_desc`;
909
+ if (translations[nameKey])
910
+ nameLocalizations[lang] = translations[nameKey];
911
+ if (translations[descKey])
912
+ descriptionLocalizations[lang] = translations[descKey];
913
+ }
914
+ } else {
915
+ for (const lang in allLocales) {
916
+ const translations = allLocales[lang];
917
+ const nameKey = `command_${name}_name`;
918
+ const descKey = `command_${name}_desc`;
919
+ if (translations[nameKey])
920
+ nameLocalizations[lang] = translations[nameKey];
921
+ if (translations[descKey])
922
+ descriptionLocalizations[lang] = translations[descKey];
923
+ }
924
+ }
925
+ if (Object.keys(nameLocalizations).length > 0) {
926
+ builder.setNameLocalizations(nameLocalizations);
927
+ }
928
+ if (Object.keys(descriptionLocalizations).length > 0) {
929
+ builder.setDescriptionLocalizations(descriptionLocalizations);
930
+ }
931
+ this._applySubcommandLocalizations(builder, name, allLocales);
932
+ } catch (e) {
933
+ this.logger.warn(
934
+ `Failed to load localizations for command "${name}": ${e.message}`,
935
+ );
936
+ }
937
+ this.commandCategoryMap.set(name, item.name);
938
+ if (commandNamesSet.has(name)) {
939
+ this.logger.warn(
940
+ `Duplicate slash command name detected: "${name}" in ${filePath}`,
941
+ );
942
+ } else {
943
+ commandNamesSet.add(name);
944
+ this.client.commands.set(name, commandModule);
945
+ commandDataForDeployment.push(builder.toJSON());
946
+ summarySlash = {
947
+ type: 'single',
948
+ name: name,
949
+ folder: item.name,
950
+ kind: 'slash',
951
+ };
952
+ if (summarySlash) loadedCommandsSummary.push(summarySlash);
953
+ }
954
+ }
955
+
956
+ if (commandModule.contextMenuCommand) {
957
+ const builder = commandModule.contextMenuCommand;
958
+ const name = builder.name;
959
+ if (!this.client.commands.has(name)) {
960
+ this.client.commands.set(name, commandModule);
961
+ }
962
+ commandDataForDeployment.push(builder.toJSON());
963
+ summaryContext = {
964
+ type: 'single',
965
+ name: name,
966
+ folder: item.name,
967
+ kind: 'contextMenu',
968
+ };
969
+ if (summaryContext) loadedCommandsSummary.push(summaryContext);
970
+ }
971
+
972
+ if (
973
+ !isClass &&
974
+ !commandModule.slashCommand &&
975
+ !commandModule.contextMenuCommand
976
+ ) {
977
+ const summary = this.registerCommand(
978
+ commandModule,
979
+ filePath,
980
+ commandNamesSet,
981
+ commandDataForDeployment,
982
+ addonPermissionDefaults,
983
+ { folderName: item.name },
984
+ );
985
+ if (summary) loadedCommandsSummary.push(summary);
986
+ }
987
+ }
988
+ }
989
+ }
990
+ return loadedCommandsSummary;
991
+ }
992
+
993
+ /**
994
+ * Apply subcommand localizations
995
+ * @private
996
+ */
997
+ _applySubcommandLocalizations(commandBuilder, commandName, allLocales) {
998
+ if (Array.isArray(commandBuilder.options)) {
999
+ for (const group of commandBuilder.options) {
1000
+ if (
1001
+ typeof SlashCommandSubcommandGroupBuilder !== 'undefined' &&
1002
+ group instanceof SlashCommandSubcommandGroupBuilder
1003
+ ) {
1004
+ const groupName = group.name;
1005
+
1006
+ const groupDescLocalizations = {};
1007
+ if (typeof allLocales.entries === 'function') {
1008
+ for (const [lang, translations] of allLocales.entries()) {
1009
+ const groupDescKey = `command_${commandName}_${groupName}_group_desc`;
1010
+ if (translations[groupDescKey])
1011
+ groupDescLocalizations[lang] = translations[groupDescKey];
1012
+ }
1013
+ } else {
1014
+ for (const lang in allLocales) {
1015
+ const translations = allLocales[lang];
1016
+ const groupDescKey = `command_${commandName}_${groupName}_group_desc`;
1017
+ if (translations[groupDescKey])
1018
+ groupDescLocalizations[lang] = translations[groupDescKey];
1019
+ }
1020
+ }
1021
+ if (
1022
+ Object.keys(groupDescLocalizations).length > 0 &&
1023
+ typeof group.setDescriptionLocalizations === 'function'
1024
+ ) {
1025
+ group.setDescriptionLocalizations(groupDescLocalizations);
1026
+ }
1027
+
1028
+ if (Array.isArray(group.options)) {
1029
+ for (const sub of group.options) {
1030
+ const subName = sub.name;
1031
+
1032
+ const subDescLocalizations = {};
1033
+ if (typeof allLocales.entries === 'function') {
1034
+ for (const [lang, translations] of allLocales.entries()) {
1035
+ const subDescKey = `command_${commandName}_${groupName}_${subName}_desc`;
1036
+ if (translations[subDescKey])
1037
+ subDescLocalizations[lang] = translations[subDescKey];
1038
+ }
1039
+ } else {
1040
+ for (const lang in allLocales) {
1041
+ const translations = allLocales[lang];
1042
+ const subDescKey = `command_${commandName}_${groupName}_${subName}_desc`;
1043
+ if (translations[subDescKey])
1044
+ subDescLocalizations[lang] = translations[subDescKey];
1045
+ }
1046
+ }
1047
+ if (
1048
+ Object.keys(subDescLocalizations).length > 0 &&
1049
+ typeof sub.setDescriptionLocalizations === 'function'
1050
+ ) {
1051
+ sub.setDescriptionLocalizations(subDescLocalizations);
1052
+ }
1053
+
1054
+ if (Array.isArray(sub.options)) {
1055
+ for (const opt of sub.options) {
1056
+ const optName = opt.name;
1057
+ const optDescLocalizations = {};
1058
+ if (typeof allLocales.entries === 'function') {
1059
+ for (const [lang, translations] of allLocales.entries()) {
1060
+ const optDescKey = `command_${commandName}_${groupName}_${subName}_option_${optName}`;
1061
+ if (translations[optDescKey])
1062
+ optDescLocalizations[lang] = translations[optDescKey];
1063
+ }
1064
+ } else {
1065
+ for (const lang in allLocales) {
1066
+ const translations = allLocales[lang];
1067
+ const optDescKey = `command_${commandName}_${groupName}_${subName}_option_${optName}`;
1068
+ if (translations[optDescKey])
1069
+ optDescLocalizations[lang] = translations[optDescKey];
1070
+ }
1071
+ }
1072
+ if (
1073
+ Object.keys(optDescLocalizations).length > 0 &&
1074
+ typeof opt.setDescriptionLocalizations === 'function'
1075
+ ) {
1076
+ opt.setDescriptionLocalizations(optDescLocalizations);
1077
+ }
1078
+ }
1079
+ }
1080
+ }
1081
+ }
1082
+ } else if (
1083
+ typeof SlashCommandSubcommandBuilder !== 'undefined' &&
1084
+ group instanceof SlashCommandSubcommandBuilder
1085
+ ) {
1086
+ const subName = group.name;
1087
+ const subDescLocalizations = {};
1088
+ if (typeof allLocales.entries === 'function') {
1089
+ for (const [lang, translations] of allLocales.entries()) {
1090
+ const subDescKey = `command_${commandName}_${subName}_desc`;
1091
+ if (translations[subDescKey])
1092
+ subDescLocalizations[lang] = translations[subDescKey];
1093
+ }
1094
+ } else {
1095
+ for (const lang in allLocales) {
1096
+ const translations = allLocales[lang];
1097
+ const subDescKey = `command_${commandName}_${subName}_desc`;
1098
+ if (translations[subDescKey])
1099
+ subDescLocalizations[lang] = translations[subDescKey];
1100
+ }
1101
+ }
1102
+ if (
1103
+ Object.keys(subDescLocalizations).length > 0 &&
1104
+ typeof group.setDescriptionLocalizations === 'function'
1105
+ ) {
1106
+ group.setDescriptionLocalizations(subDescLocalizations);
1107
+ }
1108
+
1109
+ if (Array.isArray(group.options)) {
1110
+ for (const opt of group.options) {
1111
+ const optName = opt.name;
1112
+ const optDescLocalizations = {};
1113
+ if (typeof allLocales.entries === 'function') {
1114
+ for (const [lang, translations] of allLocales.entries()) {
1115
+ const optDescKey = `command_${commandName}_${subName}_option_${optName}`;
1116
+ if (translations[optDescKey])
1117
+ optDescLocalizations[lang] = translations[optDescKey];
1118
+ }
1119
+ } else {
1120
+ for (const lang in allLocales) {
1121
+ const translations = allLocales[lang];
1122
+ const optDescKey = `command_${commandName}_${subName}_option_${optName}`;
1123
+ if (translations[optDescKey])
1124
+ optDescLocalizations[lang] = translations[optDescKey];
1125
+ }
1126
+ }
1127
+ if (
1128
+ Object.keys(optDescLocalizations).length > 0 &&
1129
+ typeof opt.setDescriptionLocalizations === 'function'
1130
+ ) {
1131
+ opt.setDescriptionLocalizations(optDescLocalizations);
1132
+ }
1133
+ }
1134
+ }
1135
+ }
1136
+ }
1137
+ }
1138
+ }
1139
+
1140
+ /**
1141
+ * Log addon summary
1142
+ * @private
1143
+ */
1144
+ _logAddonSummary(addonSummaries) {
1145
+ this.logger.info('▬▬▬▬▬▬▬▬▬▬▬▬▬[ Addon(s) Loaded ]▬▬▬▬▬▬▬▬▬▬▬▬▬');
1146
+ for (const addon of addonSummaries) {
1147
+ this.logger.info(`📦 ${addon.name} (v${addon.version})`);
1148
+ this.logger.info(' ⚙️ Command(s)');
1149
+ if (!addon.commands.length) {
1150
+ this.logger.info(' (no commands registered)');
1151
+ } else {
1152
+ for (const cmd of addon.commands) {
1153
+ if (cmd.type === 'group') {
1154
+ this.logger.info(` └─ /${cmd.name}`);
1155
+ for (const sub of cmd.subcommands) {
1156
+ if (typeof sub === 'string') {
1157
+ this.logger.info(` └─ ${sub}`);
1158
+ } else if (typeof sub === 'object' && sub.group) {
1159
+ this.logger.info(` └─ [${sub.group}]`);
1160
+ for (const subsub of sub.subcommands) {
1161
+ this.logger.info(` └─ ${subsub}`);
1162
+ }
1163
+ }
1164
+ }
1165
+ } else if (cmd.type === 'single') {
1166
+ let kindLabel = '';
1167
+ if (cmd.kind === 'slash') kindLabel = ' [slash]';
1168
+ else if (cmd.kind === 'contextMenu') kindLabel = ' [contextMenu]';
1169
+ if (cmd.folder) {
1170
+ this.logger.info(
1171
+ ` └─ /${cmd.name} (${cmd.folder})${kindLabel}`,
1172
+ );
1173
+ } else {
1174
+ this.logger.info(` └─ /${cmd.name}${kindLabel}`);
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+ if (addon.register?.length) {
1180
+ this.logger.info(' 🧩 Component(s)');
1181
+ for (const reg of addon.register) {
1182
+ this.logger.info(` ${reg}`);
1183
+ }
1184
+ }
1185
+ if (addon.events?.length) {
1186
+ this.logger.info(' 📢 Event(s)');
1187
+ for (const ev of addon.events) {
1188
+ this.logger.info(` └─ ${ev}`);
1189
+ }
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ /**
1195
+ * Get handler maps for other managers
1196
+ */
1197
+ getHandlers() {
1198
+ return {
1199
+ buttonHandlers: this.buttonHandlers,
1200
+ modalHandlers: this.modalHandlers,
1201
+ selectMenuHandlers: this.selectMenuHandlers,
1202
+ autocompleteHandlers: this.autocompleteHandlers,
1203
+ commandCategoryMap: this.commandCategoryMap,
1204
+ categoryToFeatureMap: this.categoryToFeatureMap,
1205
+ eventHandlers: this.eventHandlers,
1206
+ };
1207
+ }
964
1208
  }
965
1209
 
966
1210
  module.exports = AddonManager;