@julanzw/ttoolbox-discordjs-framework 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -173,4 +173,97 @@ export declare abstract class Command {
173
173
  * Set the error reporter for this command
174
174
  */
175
175
  setErrorReporter(reporter: ErrorReporter): void;
176
+ /**
177
+ * Lifecycle hook called before command execution.
178
+ *
179
+ * Can be used for:
180
+ * - Custom validation
181
+ * - Logging/analytics
182
+ * - Loading user preferences
183
+ * - Rate limiting checks
184
+ * - And more...
185
+ *
186
+ * If this hook throws an error or returns false, execution is stopped.
187
+ *
188
+ * @param interaction - The command interaction
189
+ * @returns true to continue execution, false to stop (or throw an error)
190
+ *
191
+ * @example
192
+ * ```typescript
193
+ * protected async beforeExecute(interaction: ChatInputCommandInteraction): Promise<boolean> {
194
+ * // Log command usage
195
+ * console.log(`${interaction.user.tag} used ${this.name}`);
196
+ *
197
+ * // Check custom rate limit
198
+ * if (await this.isRateLimited(interaction.user.id)) {
199
+ * await interaction.reply('You are rate limited!');
200
+ * return false; // Stop execution
201
+ * }
202
+ *
203
+ * return true; // Continue
204
+ * }
205
+ * ```
206
+ */
207
+ protected beforeExecute?(interaction: ChatInputCommandInteraction, client: Client): Promise<boolean | void>;
208
+ /**
209
+ * Lifecycle hook called after successful command execution.
210
+ *
211
+ * Can be used for:
212
+ * - Analytics tracking
213
+ * - Cleanup tasks
214
+ * - Success logging
215
+ * - Updating usage statistics
216
+ * - And more...
217
+ *
218
+ * Note: This is **NOT** called if the command throws an error (use onError for that).
219
+ *
220
+ * @param interaction - The command interaction
221
+ *
222
+ * @example
223
+ * ```typescript
224
+ * protected async afterExecute(interaction: ChatInputCommandInteraction): Promise<void> {
225
+ * // Track command usage
226
+ * await analytics.track('command_used', {
227
+ * command: this.name,
228
+ * user: interaction.user.id,
229
+ * });
230
+ *
231
+ * // Update user stats
232
+ * await incrementCommandCount(interaction.user.id);
233
+ * }
234
+ * ```
235
+ */
236
+ protected afterExecute?(interaction: ChatInputCommandInteraction, client: Client): Promise<void>;
237
+ /**
238
+ * Lifecycle hook called when command execution fails.
239
+ *
240
+ * Can be used for:
241
+ * - Custom error handling
242
+ * - Error reporting to external services
243
+ * - User-friendly error messages
244
+ * - Cleanup after errors
245
+ * - And more...
246
+ *
247
+ * The error is rethrown after this hook.
248
+ *
249
+ * @param interaction - The command interaction
250
+ * @param error - The error that occurred
251
+ *
252
+ * @example
253
+ * ```typescript
254
+ * protected async onError(interaction: ChatInputCommandInteraction, error: Error): Promise<void> {
255
+ * // Send to Sentry
256
+ * Sentry.captureException(error, {
257
+ * tags: { command: this.name },
258
+ * user: { id: interaction.user.id },
259
+ * });
260
+ *
261
+ * // Custom error message
262
+ * if (error.message.includes('DATABASE')) {
263
+ * await interaction.reply('Database is temporarily unavailable. Try again later.');
264
+ * }
265
+ * }
266
+ * ```
267
+ */
268
+ protected onError?(interaction: ChatInputCommandInteraction, error: Error, client: Client): Promise<void>;
176
269
  }
@@ -1,4 +1,3 @@
1
- /* eslint-disable @typescript-eslint/no-unused-vars */
2
1
  import { SlashCommandBuilder, } from 'discord.js';
3
2
  import { safeReply } from '../utils/editAndReply.js';
4
3
  import { getPermissionsForLevel } from '../utils/permissions.js';
@@ -116,7 +115,24 @@ export class Command {
116
115
  const error = this.validate(interaction);
117
116
  if (error)
118
117
  return await safeReply(interaction, error, true);
119
- await this.run(interaction, client);
118
+ if (this.beforeExecute) {
119
+ const shouldContinue = await this.beforeExecute(interaction, client);
120
+ if (shouldContinue === false) {
121
+ return;
122
+ }
123
+ }
124
+ try {
125
+ await this.run(interaction, client);
126
+ if (this.afterExecute) {
127
+ await this.afterExecute(interaction, client);
128
+ }
129
+ }
130
+ catch (err) {
131
+ if (this.onError) {
132
+ await this.onError(interaction, err, client);
133
+ }
134
+ throw err;
135
+ }
120
136
  });
121
137
  }
122
138
  /**
@@ -1,36 +1,105 @@
1
- import { ChatInputCommandInteraction, Client, RESTPostAPIChatInputApplicationCommandsJSONBody } from 'discord.js';
1
+ import { ChatInputCommandInteraction, Client, MessageContextMenuCommandInteraction, RESTPostAPIApplicationCommandsJSONBody, UserContextMenuCommandInteraction } from 'discord.js';
2
2
  import { ILogger } from '../types/logger.js';
3
3
  import { Command } from './Command.class.js';
4
4
  import { SubcommandGroup } from './SubcommandGroup.class.js';
5
5
  import { ErrorReporter } from '../utils/ErrorReporter.js';
6
+ import { MessageContextMenuCommand } from './MessageContextMenuCommand.class.js';
7
+ import { UserContextMenuCommand } from './UserContextMenuCommand.class.js';
8
+ import { LoadCommandsOptions } from '../types/loadCommands.js';
6
9
  export declare class CommandManager {
7
10
  private commands;
11
+ private userContextMenuCommands;
12
+ private messageContextMenuCommands;
8
13
  protected logger?: ILogger;
9
14
  protected errorReporter?: ErrorReporter;
10
15
  /**
16
+ * Register commands (accepts single, array, or mixed types)
17
+ * @param command - Command(s) to register
18
+ * @example
19
+ * ```typescript
20
+ * // Single command
21
+ * commandManager.registerCommand(new PingCommand());
22
+ *
23
+ * // Array of commands
24
+ * commandManager.registerCommand([cmd1, cmd2, cmd3]);
25
+ *
26
+ * // Mixed types
27
+ * commandManager.registerCommand([
28
+ * new PingCommand(),
29
+ * new UserContextMenu(),
30
+ * new SubcommandGroup(),
31
+ * ]);
32
+ * ```
33
+ */
34
+ registerCommand(command: Command | SubcommandGroup | UserContextMenuCommand | MessageContextMenuCommand | Array<Command | SubcommandGroup | UserContextMenuCommand | MessageContextMenuCommand | any>): void;
35
+ /**
36
+ * @experimental
37
+ * Automatically load and register all commands from a directory.
38
+ *
39
+ * This is a convenience wrapper around the loadCommands utility.
40
+ *
41
+ * @param dirPath - Absolute path to the directory containing commands
42
+ * @param options - Optional configuration
43
+ * @returns Number of commands successfully loaded
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * // Basic usage
48
+ * const count = await commandManager.loadFromDirectory('./src/commands');
49
+ * console.log(`Loaded ${count} commands`);
50
+ *
51
+ * // With options
52
+ * await commandManager.loadFromDirectory('./src/commands', {
53
+ * verbose: true,
54
+ * skipDirs: ['subcommands', 'test'],
55
+ * });
56
+ * ```
57
+ */
58
+ loadFromDirectory(dirPath: string, options?: LoadCommandsOptions): Promise<number>;
59
+ /**
60
+ * @deprecated Use `registerCommand()` instead
61
+ *
11
62
  * Register a single command or subcommand group
12
63
  */
13
64
  register(command: Command | SubcommandGroup): this;
14
65
  /**
66
+ * @deprecated Use `registerCommand()` with an array instead
67
+ *
15
68
  * Register multiple commands at once
16
69
  */
17
70
  registerMultiple(commands: Array<Command | SubcommandGroup>): this;
18
71
  /**
19
- * Get a specific command by name
72
+ * Get a specific slash command by name
20
73
  */
21
74
  get(name: string): Command | SubcommandGroup | undefined;
22
75
  /**
23
- * Get all registered commands
76
+ * Get a specific user context menu command by name
77
+ */
78
+ getUserContextMenu(name: string): UserContextMenuCommand | undefined;
79
+ /**
80
+ * Get a specific message context menu command by name
81
+ */
82
+ getMessageContextMenu(name: string): MessageContextMenuCommand | undefined;
83
+ /**
84
+ * Get all registered slash commands
24
85
  */
25
86
  getAll(): Array<Command | SubcommandGroup>;
26
87
  /**
27
- * Get all commands sorted alphabetically by name
88
+ * Get all registered user context menu commands
89
+ */
90
+ getAllUserContextMenus(): UserContextMenuCommand[];
91
+ /**
92
+ * Get all registered message context menu commands
93
+ */
94
+ getAllMessageContextMenus(): MessageContextMenuCommand[];
95
+ /**
96
+ * Get all slash commands sorted alphabetically by name
28
97
  */
29
98
  getAllSorted(): Array<Command | SubcommandGroup>;
30
99
  /**
31
100
  * Convert all commands to Discord JSON format for registration
32
101
  */
33
- toDiscordJSON(): RESTPostAPIChatInputApplicationCommandsJSONBody[];
102
+ toJSON(): RESTPostAPIApplicationCommandsJSONBody[];
34
103
  /**
35
104
  * Generate paginated help pages for display in help command
36
105
  * Returns a 2D array where each inner array is a page of command descriptions
@@ -48,25 +117,69 @@ export declare class CommandManager {
48
117
  */
49
118
  executeCommand(commandName: string, interaction: ChatInputCommandInteraction, client: Client): Promise<void>;
50
119
  /**
51
- * Get total number of registered commands
120
+ * Execute a user context menu command.
121
+ */
122
+ executeUserContextMenu(commandName: string, interaction: UserContextMenuCommandInteraction, client: Client): Promise<void>;
123
+ /**
124
+ * Execute a message context menu command.
125
+ */
126
+ executeMessageContextMenu(commandName: string, interaction: MessageContextMenuCommandInteraction, client: Client): Promise<void>;
127
+ /**
128
+ * Get total number of registered slash commands
52
129
  */
53
130
  get size(): number;
54
131
  /**
55
- * Check if a command exists
132
+ * Get total number of all registered commands
133
+ */
134
+ get totalSize(): number;
135
+ /**
136
+ * Check if a slash command exists
56
137
  */
57
138
  has(name: string): boolean;
58
139
  /**
59
- * Remove a command (useful for hot-reloading in dev)
140
+ * Check if a user context menu command exists
141
+ */
142
+ hasUserContextMenu(name: string): boolean;
143
+ /**
144
+ * Check if a message context menu command exists
145
+ */
146
+ hasMessageContextMenu(name: string): boolean;
147
+ /**
148
+ * Check if any command (slash, user context, or message context) exists
149
+ */
150
+ hasAny(name: string): boolean;
151
+ /**
152
+ * Remove a slash command (useful for hot-reloading in dev)
60
153
  */
61
154
  unregister(name: string): boolean;
62
155
  /**
63
- * Clear all commands
156
+ * Remove a user context menu command
157
+ */
158
+ unregisterUserContextMenu(name: string): boolean;
159
+ /**
160
+ * Remove a message context menu command
161
+ */
162
+ unregisterMessageContextMenu(name: string): boolean;
163
+ /**
164
+ * Clear all commands (slash commands, user context menus, and message context menus)
64
165
  */
65
166
  clear(): void;
66
167
  /**
67
- * Get command names as an array
168
+ * Get slash command names as an array
68
169
  */
69
170
  getCommandNames(): string[];
171
+ /**
172
+ * Get user context menu command names as an array
173
+ */
174
+ getUserContextMenuNames(): string[];
175
+ /**
176
+ * Get message context menu command names as an array
177
+ */
178
+ getMessageContextMenuNames(): string[];
179
+ /**
180
+ * Get all command names (slash + context menus) as an array
181
+ */
182
+ getAllCommandNames(): string[];
70
183
  setLogger(logger: ILogger): this;
71
184
  /**
72
185
  * Set the error reporter for all commands
@@ -1,42 +1,148 @@
1
+ import { Command } from './Command.class.js';
1
2
  import { SubcommandGroup } from './SubcommandGroup.class.js';
3
+ import { MessageContextMenuCommand } from './MessageContextMenuCommand.class.js';
4
+ import { UserContextMenuCommand } from './UserContextMenuCommand.class.js';
5
+ import { loadCommands } from '../utils/loadCommands.js';
2
6
  export class CommandManager {
3
7
  constructor() {
4
8
  this.commands = new Map();
9
+ this.userContextMenuCommands = new Map();
10
+ this.messageContextMenuCommands = new Map();
5
11
  }
6
12
  /**
7
- * Register a single command or subcommand group
13
+ * Register commands (accepts single, array, or mixed types)
14
+ * @param command - Command(s) to register
15
+ * @example
16
+ * ```typescript
17
+ * // Single command
18
+ * commandManager.registerCommand(new PingCommand());
19
+ *
20
+ * // Array of commands
21
+ * commandManager.registerCommand([cmd1, cmd2, cmd3]);
22
+ *
23
+ * // Mixed types
24
+ * commandManager.registerCommand([
25
+ * new PingCommand(),
26
+ * new UserContextMenu(),
27
+ * new SubcommandGroup(),
28
+ * ]);
29
+ * ```
8
30
  */
9
- register(command) {
10
- if (this.logger) {
31
+ registerCommand(command) {
32
+ if (Array.isArray(command)) {
33
+ for (const cmd of command) {
34
+ this.registerCommand(cmd);
35
+ }
36
+ return;
37
+ }
38
+ if (this.logger && 'setLogger' in command) {
11
39
  command.setLogger(this.logger);
12
40
  }
13
- if (this.errorReporter) {
41
+ if (this.errorReporter && 'setErrorReporter' in command) {
14
42
  command.setErrorReporter(this.errorReporter);
15
43
  }
16
- this.commands.set(command.name, command);
44
+ if (command instanceof Command || command instanceof SubcommandGroup) {
45
+ this.commands.set(command.name, command);
46
+ }
47
+ else if (command instanceof UserContextMenuCommand) {
48
+ this.userContextMenuCommands.set(command.name, command);
49
+ }
50
+ else if (command instanceof MessageContextMenuCommand) {
51
+ this.messageContextMenuCommands.set(command.name, command);
52
+ }
53
+ else {
54
+ this.logger?.warn(`Attempted to register invalid command type: ${command?.constructor?.name || 'unknown'}`, 'command-manager');
55
+ }
56
+ }
57
+ /**
58
+ * @experimental
59
+ * Automatically load and register all commands from a directory.
60
+ *
61
+ * This is a convenience wrapper around the loadCommands utility.
62
+ *
63
+ * @param dirPath - Absolute path to the directory containing commands
64
+ * @param options - Optional configuration
65
+ * @returns Number of commands successfully loaded
66
+ *
67
+ * @example
68
+ * ```typescript
69
+ * // Basic usage
70
+ * const count = await commandManager.loadFromDirectory('./src/commands');
71
+ * console.log(`Loaded ${count} commands`);
72
+ *
73
+ * // With options
74
+ * await commandManager.loadFromDirectory('./src/commands', {
75
+ * verbose: true,
76
+ * skipDirs: ['subcommands', 'test'],
77
+ * });
78
+ * ```
79
+ */
80
+ async loadFromDirectory(dirPath, options = {}) {
81
+ const commands = await loadCommands(dirPath, options);
82
+ this.registerCommand(commands);
83
+ const validCount = commands.filter(cmd => cmd instanceof Command ||
84
+ cmd instanceof SubcommandGroup ||
85
+ cmd instanceof UserContextMenuCommand ||
86
+ cmd instanceof MessageContextMenuCommand).length;
87
+ this.logger?.info(`Auto-loaded ${validCount} commands from ${dirPath}`, 'command-manager');
88
+ return validCount;
89
+ }
90
+ /**
91
+ * @deprecated Use `registerCommand()` instead
92
+ *
93
+ * Register a single command or subcommand group
94
+ */
95
+ register(command) {
96
+ this.registerCommand(command);
17
97
  return this;
18
98
  }
19
99
  /**
100
+ * @deprecated Use `registerCommand()` with an array instead
101
+ *
20
102
  * Register multiple commands at once
21
103
  */
22
104
  registerMultiple(commands) {
23
- commands.forEach((cmd) => this.register(cmd));
105
+ this.registerCommand(commands);
24
106
  return this;
25
107
  }
26
108
  /**
27
- * Get a specific command by name
109
+ * Get a specific slash command by name
28
110
  */
29
111
  get(name) {
30
112
  return this.commands.get(name);
31
113
  }
32
114
  /**
33
- * Get all registered commands
115
+ * Get a specific user context menu command by name
116
+ */
117
+ getUserContextMenu(name) {
118
+ return this.userContextMenuCommands.get(name);
119
+ }
120
+ /**
121
+ * Get a specific message context menu command by name
122
+ */
123
+ getMessageContextMenu(name) {
124
+ return this.messageContextMenuCommands.get(name);
125
+ }
126
+ /**
127
+ * Get all registered slash commands
34
128
  */
35
129
  getAll() {
36
130
  return Array.from(this.commands.values());
37
131
  }
38
132
  /**
39
- * Get all commands sorted alphabetically by name
133
+ * Get all registered user context menu commands
134
+ */
135
+ getAllUserContextMenus() {
136
+ return Array.from(this.userContextMenuCommands.values());
137
+ }
138
+ /**
139
+ * Get all registered message context menu commands
140
+ */
141
+ getAllMessageContextMenus() {
142
+ return Array.from(this.messageContextMenuCommands.values());
143
+ }
144
+ /**
145
+ * Get all slash commands sorted alphabetically by name
40
146
  */
41
147
  getAllSorted() {
42
148
  return this.getAll().sort((a, b) => a.name.localeCompare(b.name));
@@ -44,13 +150,26 @@ export class CommandManager {
44
150
  /**
45
151
  * Convert all commands to Discord JSON format for registration
46
152
  */
47
- toDiscordJSON() {
48
- return this.getAll().map((cmd) => {
153
+ toJSON() {
154
+ const slashCommands = Array.from(this.commands.values()).map((cmd) => {
49
155
  if (process.env.ENV === 'dev') {
50
156
  console.log(`Registering: ${cmd.name}`);
51
157
  }
52
158
  return cmd.toJSON();
53
159
  });
160
+ const userContextMenus = Array.from(this.userContextMenuCommands.values()).map((cmd) => {
161
+ if (process.env.ENV === 'dev') {
162
+ console.log(`Registering user context menu: ${cmd.name}`);
163
+ }
164
+ return cmd.toJSON();
165
+ });
166
+ const messageContextMenus = Array.from(this.messageContextMenuCommands.values()).map((cmd) => {
167
+ if (process.env.ENV === 'dev') {
168
+ console.log(`Registering message context menu: ${cmd.name}`);
169
+ }
170
+ return cmd.toJSON();
171
+ });
172
+ return [...slashCommands, ...userContextMenus, ...messageContextMenus];
54
173
  }
55
174
  /**
56
175
  * Generate paginated help pages for display in help command
@@ -112,41 +231,131 @@ export class CommandManager {
112
231
  await command.execute(interaction, client);
113
232
  }
114
233
  /**
115
- * Get total number of registered commands
234
+ * Execute a user context menu command.
235
+ */
236
+ async executeUserContextMenu(commandName, interaction, client) {
237
+ const command = this.userContextMenuCommands.get(commandName);
238
+ if (!command) {
239
+ throw new Error(`User context menu command not found: ${commandName}`);
240
+ }
241
+ await command.execute(interaction, client);
242
+ }
243
+ /**
244
+ * Execute a message context menu command.
245
+ */
246
+ async executeMessageContextMenu(commandName, interaction, client) {
247
+ const command = this.messageContextMenuCommands.get(commandName);
248
+ if (!command) {
249
+ throw new Error(`Message context menu command not found: ${commandName}`);
250
+ }
251
+ await command.execute(interaction, client);
252
+ }
253
+ /**
254
+ * Get total number of registered slash commands
116
255
  */
117
256
  get size() {
118
257
  return this.commands.size;
119
258
  }
120
259
  /**
121
- * Check if a command exists
260
+ * Get total number of all registered commands
261
+ */
262
+ get totalSize() {
263
+ return this.commands.size +
264
+ this.userContextMenuCommands.size +
265
+ this.messageContextMenuCommands.size;
266
+ }
267
+ /**
268
+ * Check if a slash command exists
122
269
  */
123
270
  has(name) {
124
271
  return this.commands.has(name);
125
272
  }
126
273
  /**
127
- * Remove a command (useful for hot-reloading in dev)
274
+ * Check if a user context menu command exists
275
+ */
276
+ hasUserContextMenu(name) {
277
+ return this.userContextMenuCommands.has(name);
278
+ }
279
+ /**
280
+ * Check if a message context menu command exists
281
+ */
282
+ hasMessageContextMenu(name) {
283
+ return this.messageContextMenuCommands.has(name);
284
+ }
285
+ /**
286
+ * Check if any command (slash, user context, or message context) exists
287
+ */
288
+ hasAny(name) {
289
+ return this.commands.has(name) ||
290
+ this.userContextMenuCommands.has(name) ||
291
+ this.messageContextMenuCommands.has(name);
292
+ }
293
+ /**
294
+ * Remove a slash command (useful for hot-reloading in dev)
128
295
  */
129
296
  unregister(name) {
130
297
  return this.commands.delete(name);
131
298
  }
132
299
  /**
133
- * Clear all commands
300
+ * Remove a user context menu command
301
+ */
302
+ unregisterUserContextMenu(name) {
303
+ return this.userContextMenuCommands.delete(name);
304
+ }
305
+ /**
306
+ * Remove a message context menu command
307
+ */
308
+ unregisterMessageContextMenu(name) {
309
+ return this.messageContextMenuCommands.delete(name);
310
+ }
311
+ /**
312
+ * Clear all commands (slash commands, user context menus, and message context menus)
134
313
  */
135
314
  clear() {
136
315
  this.commands.clear();
316
+ this.userContextMenuCommands.clear();
317
+ this.messageContextMenuCommands.clear();
137
318
  }
138
319
  /**
139
- * Get command names as an array
320
+ * Get slash command names as an array
140
321
  */
141
322
  getCommandNames() {
142
323
  return Array.from(this.commands.keys());
143
324
  }
325
+ /**
326
+ * Get user context menu command names as an array
327
+ */
328
+ getUserContextMenuNames() {
329
+ return Array.from(this.userContextMenuCommands.keys());
330
+ }
331
+ /**
332
+ * Get message context menu command names as an array
333
+ */
334
+ getMessageContextMenuNames() {
335
+ return Array.from(this.messageContextMenuCommands.keys());
336
+ }
337
+ /**
338
+ * Get all command names (slash + context menus) as an array
339
+ */
340
+ getAllCommandNames() {
341
+ return [
342
+ ...this.getCommandNames(),
343
+ ...this.getUserContextMenuNames(),
344
+ ...this.getMessageContextMenuNames(),
345
+ ];
346
+ }
144
347
  setLogger(logger) {
145
348
  this.logger = logger;
146
349
  // Inject logger into all already-registered commands
147
350
  for (const command of this.commands.values()) {
148
351
  command.setLogger(logger);
149
352
  }
353
+ for (const command of this.userContextMenuCommands.values()) {
354
+ command.setLogger(logger);
355
+ }
356
+ for (const command of this.messageContextMenuCommands.values()) {
357
+ command.setLogger(logger);
358
+ }
150
359
  return this;
151
360
  }
152
361
  /**
@@ -157,6 +366,12 @@ export class CommandManager {
157
366
  for (const command of this.commands.values()) {
158
367
  command.setErrorReporter(reporter);
159
368
  }
369
+ for (const command of this.userContextMenuCommands.values()) {
370
+ command.setErrorReporter(reporter);
371
+ }
372
+ for (const command of this.messageContextMenuCommands.values()) {
373
+ command.setErrorReporter(reporter);
374
+ }
160
375
  return this;
161
376
  }
162
377
  }