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