stoatx 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,791 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Client: () => Client,
34
+ CommandRegistry: () => CommandRegistry,
35
+ Guard: () => Guard,
36
+ METADATA_KEYS: () => METADATA_KEYS,
37
+ On: () => On,
38
+ Once: () => Once,
39
+ SimpleCommand: () => SimpleCommand,
40
+ Stoat: () => Stoat,
41
+ buildSimpleCommandMetadata: () => buildSimpleCommandMetadata,
42
+ getEventsMetadata: () => getEventsMetadata,
43
+ getGuards: () => getGuards,
44
+ getSimpleCommands: () => getSimpleCommands,
45
+ isStoatClass: () => isStoatClass
46
+ });
47
+ module.exports = __toCommonJS(index_exports);
48
+
49
+ // src/decorators/Stoat.ts
50
+ var import_reflect_metadata = require("reflect-metadata");
51
+
52
+ // src/decorators/keys.ts
53
+ var METADATA_KEYS = {
54
+ IS_STOAT_CLASS: /* @__PURE__ */ Symbol("stoatx:stoat:isClass"),
55
+ SIMPLE_COMMANDS: /* @__PURE__ */ Symbol("stoatx:stoat:simpleCommands"),
56
+ GUARDS: "stoatx:command:guards",
57
+ EVENTS: /* @__PURE__ */ Symbol("stoatx:stoat:events")
58
+ };
59
+
60
+ // src/decorators/store.ts
61
+ var DecoratorStore = class _DecoratorStore {
62
+ constructor() {
63
+ /** Stoat classes with their SimpleCommand methods */
64
+ this.stoatClasses = /* @__PURE__ */ new Map();
65
+ /** Registered commands from @Stoat/@SimpleCommand decorators */
66
+ this.commands = [];
67
+ /** Whether the store has been initialized */
68
+ this.initialized = false;
69
+ }
70
+ static getInstance() {
71
+ if (!_DecoratorStore.instance) {
72
+ _DecoratorStore.instance = new _DecoratorStore();
73
+ }
74
+ return _DecoratorStore.instance;
75
+ }
76
+ /**
77
+ * Register a @Stoat decorated class
78
+ */
79
+ registerStoatClass(classConstructor) {
80
+ if (!this.stoatClasses.has(classConstructor)) {
81
+ const instance = new classConstructor();
82
+ this.stoatClasses.set(classConstructor, instance);
83
+ }
84
+ }
85
+ /**
86
+ * Get all registered Stoat classes with their instances
87
+ */
88
+ getStoatClasses() {
89
+ return this.stoatClasses;
90
+ }
91
+ /**
92
+ * Add a registered command
93
+ */
94
+ addCommand(command) {
95
+ this.commands.push(command);
96
+ }
97
+ /**
98
+ * Get all registered commands
99
+ */
100
+ getCommands() {
101
+ return this.commands;
102
+ }
103
+ /**
104
+ * Clear all registered classes (useful for testing)
105
+ */
106
+ clear() {
107
+ this.stoatClasses.clear();
108
+ this.commands = [];
109
+ this.initialized = false;
110
+ }
111
+ /**
112
+ * Mark as initialized
113
+ */
114
+ markInitialized() {
115
+ this.initialized = true;
116
+ }
117
+ /**
118
+ * Check if initialized
119
+ */
120
+ isInitialized() {
121
+ return this.initialized;
122
+ }
123
+ };
124
+ var decoratorStore = DecoratorStore.getInstance();
125
+
126
+ // src/decorators/Stoat.ts
127
+ function Stoat() {
128
+ return (target) => {
129
+ Reflect.defineMetadata(METADATA_KEYS.IS_STOAT_CLASS, true, target);
130
+ decoratorStore.registerStoatClass(target);
131
+ };
132
+ }
133
+ function isStoatClass(target) {
134
+ return Reflect.getMetadata(METADATA_KEYS.IS_STOAT_CLASS, target) === true;
135
+ }
136
+
137
+ // src/decorators/SimpleCommand.ts
138
+ var import_reflect_metadata2 = require("reflect-metadata");
139
+ function SimpleCommand(options = {}) {
140
+ return (target, propertyKey, descriptor) => {
141
+ const constructor = target.constructor;
142
+ const existingCommands = Reflect.getMetadata(METADATA_KEYS.SIMPLE_COMMANDS, constructor) || [];
143
+ existingCommands.push({
144
+ methodName: String(propertyKey),
145
+ options
146
+ });
147
+ Reflect.defineMetadata(METADATA_KEYS.SIMPLE_COMMANDS, existingCommands, constructor);
148
+ return descriptor;
149
+ };
150
+ }
151
+ function getSimpleCommands(target) {
152
+ return Reflect.getMetadata(METADATA_KEYS.SIMPLE_COMMANDS, target) || [];
153
+ }
154
+
155
+ // src/decorators/Guard.ts
156
+ var import_reflect_metadata3 = require("reflect-metadata");
157
+ function Guard(guardClass) {
158
+ return (target) => {
159
+ const existingGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, target) || [];
160
+ existingGuards.push(guardClass);
161
+ Reflect.defineMetadata(METADATA_KEYS.GUARDS, existingGuards, target);
162
+ };
163
+ }
164
+ function getGuards(target) {
165
+ return Reflect.getMetadata(METADATA_KEYS.GUARDS, target) || [];
166
+ }
167
+
168
+ // src/decorators/Events.ts
169
+ var import_reflect_metadata4 = require("reflect-metadata");
170
+ function createEventDecorator(event, type) {
171
+ return (target, propertyKey, descriptor) => {
172
+ const constructor = target.constructor;
173
+ const existingEvents = Reflect.getMetadata(METADATA_KEYS.EVENTS, constructor) || [];
174
+ existingEvents.push({
175
+ methodName: String(propertyKey),
176
+ event,
177
+ type
178
+ });
179
+ Reflect.defineMetadata(METADATA_KEYS.EVENTS, existingEvents, constructor);
180
+ return descriptor;
181
+ };
182
+ }
183
+ function On(event) {
184
+ return createEventDecorator(event, "on");
185
+ }
186
+ function Once(event) {
187
+ return createEventDecorator(event, "once");
188
+ }
189
+ function getEventsMetadata(target) {
190
+ return Reflect.getMetadata(METADATA_KEYS.EVENTS, target) || [];
191
+ }
192
+
193
+ // src/decorators/utils.ts
194
+ function buildSimpleCommandMetadata(options, methodName, category) {
195
+ return {
196
+ name: options.name ?? methodName.toLowerCase(),
197
+ description: options.description ?? "No description provided",
198
+ aliases: options.aliases ?? [],
199
+ permissions: options.permissions ?? [],
200
+ category: options.category ?? category ?? "uncategorized",
201
+ cooldown: options.cooldown ?? 0,
202
+ nsfw: options.nsfw ?? false,
203
+ ownerOnly: options.ownerOnly ?? false
204
+ };
205
+ }
206
+
207
+ // src/registry.ts
208
+ var path = __toESM(require("path"));
209
+ var fs = __toESM(require("fs/promises"));
210
+ var import_node_url = require("url");
211
+ var import_tinyglobby = require("tinyglobby");
212
+ var _CommandRegistry = class _CommandRegistry {
213
+ constructor(extensions = [".js", ".mjs", ".cjs"]) {
214
+ this.commands = /* @__PURE__ */ new Map();
215
+ this.aliases = /* @__PURE__ */ new Map();
216
+ this.registeredEvents = [];
217
+ this.processedStoatClasses = /* @__PURE__ */ new Set();
218
+ this.extensions = extensions;
219
+ }
220
+ /**
221
+ * Get the number of registered commands
222
+ */
223
+ get size() {
224
+ return this.commands.size;
225
+ }
226
+ /**
227
+ * Load commands from a directory using glob pattern matching
228
+ */
229
+ async loadFromDirectory(directory) {
230
+ const patterns = this.extensions.map((ext) => path.join(directory, "**", `*${ext}`).replace(/\\/g, "/"));
231
+ for (const pattern of patterns) {
232
+ const files = await (0, import_tinyglobby.glob)(pattern, {
233
+ ignore: ["**/*.d.ts", "**/*.test.ts", "**/*.spec.ts"],
234
+ absolute: true
235
+ });
236
+ for (const file of files) {
237
+ await this.loadFile(file, directory);
238
+ }
239
+ }
240
+ console.log(`[Stoatx] Loaded ${this.commands.size} command(s) and ${this.registeredEvents.length} event(s)`);
241
+ }
242
+ /**
243
+ * Auto-discover command files across one or more roots.
244
+ */
245
+ async autoDiscover(options = {}) {
246
+ const roots = options.roots?.length ? options.roots : [process.cwd()];
247
+ const includePatterns = options.include?.length ? options.include : this.getDefaultAutoDiscoveryPatterns();
248
+ const patterns = roots.flatMap(
249
+ (root) => includePatterns.map((pattern) => path.join(root, pattern).replace(/\\/g, "/"))
250
+ );
251
+ const files = await (0, import_tinyglobby.glob)(patterns, {
252
+ ignore: [..._CommandRegistry.DEFAULT_AUTO_DISCOVERY_IGNORES, ...options.ignore ?? []],
253
+ absolute: true
254
+ });
255
+ const uniqueFiles = [...new Set(files)];
256
+ let candidateFiles = 0;
257
+ for (const file of uniqueFiles) {
258
+ if (!await this.isLikelyCommandModule(file)) {
259
+ continue;
260
+ }
261
+ candidateFiles++;
262
+ const baseDir = roots.find((root) => {
263
+ const relative2 = path.relative(root, file);
264
+ return relative2 && !relative2.startsWith("..") && !path.isAbsolute(relative2);
265
+ }) ?? roots[0];
266
+ await this.loadFile(file, baseDir);
267
+ }
268
+ console.log(`[Stoatx] Loaded ${this.commands.size} command(s) and ${this.registeredEvents.length} event(s)`);
269
+ }
270
+ getDefaultAutoDiscoveryPatterns() {
271
+ return this.extensions.map((ext) => `**/*${ext}`);
272
+ }
273
+ async isLikelyCommandModule(filePath) {
274
+ try {
275
+ const source = await fs.readFile(filePath, "utf8");
276
+ return source.includes("Stoat") || source.includes("SimpleCommand") || source.includes("Command") || source.includes("stoatx:command");
277
+ } catch {
278
+ return true;
279
+ }
280
+ }
281
+ /**
282
+ * Register a command instance
283
+ */
284
+ register(instance, metadata, classConstructor, methodName) {
285
+ const name = metadata.name.toLowerCase();
286
+ if (this.commands.has(name)) {
287
+ console.warn(`[Stoatx] Duplicate command name: ${name}. Skipping...`);
288
+ return;
289
+ }
290
+ this.validateGuards(classConstructor, metadata.name);
291
+ this.commands.set(name, { instance, metadata, methodName, classConstructor });
292
+ for (const alias of metadata.aliases) {
293
+ const aliasLower = alias.toLowerCase();
294
+ if (this.aliases.has(aliasLower) || this.commands.has(aliasLower)) {
295
+ console.warn(`[Stoatx] Duplicate alias: ${aliasLower}. Skipping...`);
296
+ continue;
297
+ }
298
+ this.aliases.set(aliasLower, name);
299
+ }
300
+ }
301
+ /**
302
+ * Get a command by name or alias
303
+ */
304
+ get(name) {
305
+ const lowerName = name.toLowerCase();
306
+ const resolvedName = this.aliases.get(lowerName) ?? lowerName;
307
+ return this.commands.get(resolvedName);
308
+ }
309
+ /**
310
+ * Check if a command exists
311
+ */
312
+ has(name) {
313
+ const lowerName = name.toLowerCase();
314
+ return this.commands.has(lowerName) || this.aliases.has(lowerName);
315
+ }
316
+ /**
317
+ * Get all registered commands
318
+ */
319
+ getAll() {
320
+ return Array.from(this.commands.values());
321
+ }
322
+ /**
323
+ * Get all command metadata
324
+ */
325
+ getAllMetadata() {
326
+ return this.getAll().map((c) => c.metadata);
327
+ }
328
+ /**
329
+ * Get all registered events
330
+ */
331
+ getEvents() {
332
+ return this.registeredEvents;
333
+ }
334
+ /**
335
+ * Get commands grouped by category
336
+ */
337
+ getByCategory() {
338
+ const categories = /* @__PURE__ */ new Map();
339
+ for (const cmd of this.commands.values()) {
340
+ const category = cmd.metadata.category;
341
+ const existing = categories.get(category) ?? [];
342
+ existing.push(cmd);
343
+ categories.set(category, existing);
344
+ }
345
+ return categories;
346
+ }
347
+ /**
348
+ * Clear all commands
349
+ */
350
+ clear() {
351
+ this.commands.clear();
352
+ this.aliases.clear();
353
+ this.registeredEvents.length = 0;
354
+ this.processedStoatClasses.clear();
355
+ }
356
+ /**
357
+ * Iterate over commands
358
+ */
359
+ [Symbol.iterator]() {
360
+ return this.commands.entries();
361
+ }
362
+ /**
363
+ * Iterate over command values
364
+ */
365
+ values() {
366
+ return this.commands.values();
367
+ }
368
+ /**
369
+ * Iterate over command names
370
+ */
371
+ keys() {
372
+ return this.commands.keys();
373
+ }
374
+ /**
375
+ * Validate that all guards on a command implement the required methods
376
+ * @param commandClass
377
+ * @param commandName
378
+ * @private
379
+ */
380
+ validateGuards(commandClass, commandName) {
381
+ const guards = Reflect.getMetadata("stoatx:command:guards", commandClass) || [];
382
+ for (const GuardClass of guards) {
383
+ const guardInstance = new GuardClass();
384
+ if (typeof guardInstance.run !== "function") {
385
+ console.error(
386
+ `[Stoatx] FATAL: Guard "${GuardClass.name}" on command "${commandName}" does not have a run() method.`
387
+ );
388
+ process.exit(1);
389
+ }
390
+ if (typeof guardInstance.guardFail !== "function") {
391
+ console.error(
392
+ `[Stoatx] FATAL: Guard "${GuardClass.name}" on command "${commandName}" does not have a guardFail() method.`
393
+ );
394
+ console.error(`[Stoatx] All guards must implement guardFail() to handle failed checks.`);
395
+ process.exit(1);
396
+ }
397
+ }
398
+ }
399
+ /**
400
+ * Load commands from a single file
401
+ */
402
+ async loadFile(filePath, baseDir) {
403
+ try {
404
+ const knownStoatClasses = new Set(decoratorStore.getStoatClasses().keys());
405
+ const fileUrl = (0, import_node_url.pathToFileURL)(filePath).href;
406
+ await import(fileUrl);
407
+ const allStoatClasses = decoratorStore.getStoatClasses();
408
+ for (const [stoatClass, stoatInstance] of allStoatClasses.entries()) {
409
+ if (knownStoatClasses.has(stoatClass) || this.processedStoatClasses.has(stoatClass)) {
410
+ continue;
411
+ }
412
+ this.registerStoatClassCommands(stoatClass, stoatInstance, filePath, baseDir);
413
+ }
414
+ } catch (error) {
415
+ console.error(`[Stoatx] Failed to load command file: ${filePath}`, error);
416
+ }
417
+ }
418
+ registerStoatClassCommands(stoatClass, instance, filePath, baseDir) {
419
+ const simpleCommands = getSimpleCommands(stoatClass);
420
+ const events = getEventsMetadata(stoatClass);
421
+ const category = this.getCategoryFromPath(filePath, baseDir);
422
+ if (simpleCommands.length === 0 && events.length === 0) {
423
+ console.warn(
424
+ `[Stoatx] Class ${stoatClass.name} is decorated with @Stoat but has no @SimpleCommand, @On or @Once methods. Skipping...`
425
+ );
426
+ this.processedStoatClasses.add(stoatClass);
427
+ return;
428
+ }
429
+ for (const cmdDef of simpleCommands) {
430
+ const method = instance[cmdDef.methodName];
431
+ if (typeof method !== "function") {
432
+ console.warn(`[Stoatx] Method ${cmdDef.methodName} not found on ${stoatClass.name}. Skipping...`);
433
+ continue;
434
+ }
435
+ const metadata = buildSimpleCommandMetadata(cmdDef.options, cmdDef.methodName, category);
436
+ this.register(instance, metadata, stoatClass, cmdDef.methodName);
437
+ }
438
+ for (const eventDef of events) {
439
+ const method = instance[eventDef.methodName];
440
+ if (typeof method !== "function") {
441
+ console.warn(`[Stoatx] Method ${eventDef.methodName} not found on ${stoatClass.name}. Skipping...`);
442
+ continue;
443
+ }
444
+ this.registeredEvents.push({
445
+ instance,
446
+ methodName: eventDef.methodName,
447
+ event: eventDef.event,
448
+ type: eventDef.type
449
+ });
450
+ }
451
+ this.processedStoatClasses.add(stoatClass);
452
+ }
453
+ /**
454
+ * Derive category from file path relative to base directory
455
+ */
456
+ getCategoryFromPath(filePath, baseDir) {
457
+ const relative2 = path.relative(baseDir, filePath);
458
+ const parts = relative2.split(path.sep);
459
+ if (parts.length > 1) {
460
+ return parts[0];
461
+ }
462
+ return void 0;
463
+ }
464
+ };
465
+ _CommandRegistry.DEFAULT_AUTO_DISCOVERY_IGNORES = [
466
+ "**/node_modules/**",
467
+ "**/.git/**",
468
+ "**/*.d.ts",
469
+ "**/*.test.*",
470
+ "**/*.spec.*"
471
+ ];
472
+ var CommandRegistry = _CommandRegistry;
473
+
474
+ // src/handler.ts
475
+ var import_reflect_metadata5 = require("reflect-metadata");
476
+ var import_stoat = require("stoat.js");
477
+ var Client = class extends import_stoat.Client {
478
+ constructor(options) {
479
+ super();
480
+ this.handler = new StoatxHandler({ ...options, client: this });
481
+ this.on("messageCreate", async (message) => {
482
+ await this.handler.handle(message);
483
+ });
484
+ }
485
+ /**
486
+ * Initialize the StoatxHandler commands
487
+ */
488
+ async initCommands() {
489
+ await this.handler.init();
490
+ }
491
+ };
492
+ var StoatxHandler = class {
493
+ constructor(options) {
494
+ this.cooldowns = /* @__PURE__ */ new Map();
495
+ this.client = options.client;
496
+ this.commandsDir = options.commandsDir;
497
+ this.discoveryOptions = options.discovery;
498
+ this.prefixResolver = options.prefix;
499
+ this.owners = new Set(options.owners ?? []);
500
+ this.registry = new CommandRegistry(options.extensions);
501
+ this.disableMentionPrefix = options.disableMentionPrefix ?? false;
502
+ }
503
+ /**
504
+ * Initialize the handler - load all commands
505
+ */
506
+ async init() {
507
+ if (this.commandsDir) {
508
+ await this.registry.loadFromDirectory(this.commandsDir);
509
+ } else {
510
+ await this.registry.autoDiscover(this.discoveryOptions);
511
+ }
512
+ this.attachEvents();
513
+ }
514
+ /**
515
+ * Attach registered events to the client
516
+ */
517
+ attachEvents() {
518
+ const events = this.registry.getEvents();
519
+ for (const eventDef of events) {
520
+ const handler = async (...args) => {
521
+ try {
522
+ await eventDef.instance[eventDef.methodName](...args, this.client);
523
+ } catch (error) {
524
+ console.error(
525
+ `[Stoatx] Event Handler Error in @${eventDef.type === "on" ? "On" : "Once"}('${eventDef.event}'):`,
526
+ error
527
+ );
528
+ }
529
+ };
530
+ if (eventDef.type === "once") {
531
+ this.client.once(eventDef.event, handler);
532
+ } else {
533
+ this.client.on(eventDef.event, handler);
534
+ }
535
+ }
536
+ }
537
+ /**
538
+ * Parse a raw message into command context
539
+ */
540
+ async parseMessage(rawContent, message, meta) {
541
+ const prefix = await this.resolvePrefix(meta.serverId);
542
+ let usedPrefix = prefix;
543
+ let withoutPrefix = "";
544
+ if (rawContent.startsWith(prefix)) {
545
+ withoutPrefix = rawContent.slice(prefix.length).trim();
546
+ usedPrefix = prefix;
547
+ } else if (!this.disableMentionPrefix && rawContent.match(/^<@!?[\w]+>/)) {
548
+ const mentionMatch = rawContent.match(/^<@!?([\w]+)>\s*/);
549
+ if (mentionMatch) {
550
+ const mentionedId = mentionMatch[1];
551
+ const botId = this.client.user?.id;
552
+ if (botId && mentionedId === botId) {
553
+ usedPrefix = mentionMatch[0];
554
+ withoutPrefix = rawContent.slice(mentionMatch[0].length).trim();
555
+ } else {
556
+ }
557
+ }
558
+ }
559
+ if (!withoutPrefix) {
560
+ return null;
561
+ }
562
+ const [commandName, ...args] = withoutPrefix.split(/\s+/);
563
+ if (!commandName) {
564
+ return null;
565
+ }
566
+ return {
567
+ client: this.client,
568
+ content: rawContent,
569
+ authorId: meta.authorId,
570
+ channelId: meta.channelId,
571
+ serverId: meta.serverId,
572
+ args,
573
+ prefix: usedPrefix,
574
+ commandName: commandName.toLowerCase(),
575
+ reply: meta.reply,
576
+ message
577
+ };
578
+ }
579
+ /**
580
+ * Handle a message object using the configured message adapter
581
+ *
582
+ * @example
583
+ * ```ts
584
+ * // With message adapter configured
585
+ * client.on('messageCreate', (message) => {
586
+ * handler.handle(message);
587
+ * });
588
+ * ```
589
+ */
590
+ async handle(message) {
591
+ if (!message.channel || !message.author) {
592
+ return false;
593
+ }
594
+ if (message.author.bot) {
595
+ return false;
596
+ }
597
+ const rawContent = message.content;
598
+ const authorId = message.author.id;
599
+ const channelId = message.channel.id;
600
+ const serverId = message.server?.id;
601
+ const reply = async (content) => {
602
+ await message.channel.sendMessage(content);
603
+ };
604
+ return this.handleMessage(rawContent, message, {
605
+ authorId,
606
+ channelId,
607
+ serverId,
608
+ reply
609
+ });
610
+ }
611
+ /**
612
+ * Handle a raw message string with metadata
613
+ *
614
+ * @example
615
+ * ```ts
616
+ * // Manual usage without message adapter
617
+ * client.on('messageCreate', (message) => {
618
+ * handler.handleMessage(message.content, message, {
619
+ * authorId: message.author.id,
620
+ * channelId: message.channel.id,
621
+ * serverId: message.server?.id,
622
+ * reply: (content) => message.channel.sendMessage(content),
623
+ * });
624
+ * });
625
+ * ```
626
+ */
627
+ async handleMessage(rawContent, message, meta) {
628
+ const ctx = await this.parseMessage(rawContent, message, meta);
629
+ if (!ctx) {
630
+ return false;
631
+ }
632
+ return this.execute(ctx);
633
+ }
634
+ /**
635
+ * Execute a command with the given context
636
+ */
637
+ async execute(ctx) {
638
+ const registered = this.registry.get(ctx.commandName);
639
+ if (!registered) {
640
+ return false;
641
+ }
642
+ const { instance, metadata, methodName, classConstructor } = registered;
643
+ if (metadata.ownerOnly && !this.owners.has(ctx.authorId)) {
644
+ await ctx.reply("This command is owner-only.");
645
+ return false;
646
+ }
647
+ const guards = Reflect.getMetadata("stoatx:command:guards", classConstructor) || [];
648
+ for (const guardClass of guards) {
649
+ const guardInstance = new guardClass();
650
+ if (typeof guardInstance.run === "function") {
651
+ const guardResult = await guardInstance.run(ctx);
652
+ if (!guardResult) {
653
+ if (typeof guardInstance.guardFail === "function") {
654
+ await guardInstance.guardFail(ctx);
655
+ } else {
656
+ console.error("[Stoatx] Guard check failed but no guardFail method defined on", guardClass.name);
657
+ }
658
+ return false;
659
+ }
660
+ }
661
+ }
662
+ if (!this.checkCooldown(ctx.authorId, metadata)) {
663
+ const remaining = this.getRemainingCooldown(ctx.authorId, metadata);
664
+ if (typeof instance.onCooldown === "function") {
665
+ await instance.onCooldown(ctx, remaining);
666
+ } else {
667
+ await ctx.reply(`Please wait ${(remaining / 1e3).toFixed(1)} seconds before using this command again.`);
668
+ }
669
+ return false;
670
+ }
671
+ try {
672
+ await instance[methodName](ctx);
673
+ if (metadata.cooldown > 0) {
674
+ this.setCooldown(ctx.authorId, metadata);
675
+ }
676
+ return true;
677
+ } catch (error) {
678
+ if (typeof instance.onError === "function") {
679
+ await instance.onError(ctx, error);
680
+ } else {
681
+ console.error(`[Stoatx] Error in command ${metadata.name}:`, error);
682
+ await ctx.reply(`An error occurred: ${error.message}`);
683
+ }
684
+ return false;
685
+ }
686
+ }
687
+ /**
688
+ * Get the command registry
689
+ */
690
+ getRegistry() {
691
+ return this.registry;
692
+ }
693
+ /**
694
+ * Get a command by name or alias
695
+ */
696
+ getCommand(name) {
697
+ return this.registry.get(name);
698
+ }
699
+ /**
700
+ * Get all commands
701
+ */
702
+ getCommands() {
703
+ return this.registry.getAll();
704
+ }
705
+ /**
706
+ * Reload all commands
707
+ */
708
+ async reload() {
709
+ this.registry.clear();
710
+ this.cooldowns.clear();
711
+ if (this.commandsDir) {
712
+ await this.registry.loadFromDirectory(this.commandsDir);
713
+ return;
714
+ }
715
+ await this.registry.autoDiscover(this.discoveryOptions);
716
+ }
717
+ /**
718
+ * Check if a user is an owner
719
+ */
720
+ isOwner(userId) {
721
+ return this.owners.has(userId);
722
+ }
723
+ /**
724
+ * Add an owner
725
+ */
726
+ addOwner(userId) {
727
+ this.owners.add(userId);
728
+ }
729
+ /**
730
+ * Remove an owner
731
+ */
732
+ removeOwner(userId) {
733
+ this.owners.delete(userId);
734
+ }
735
+ /**
736
+ * Resolve the prefix for a context
737
+ */
738
+ async resolvePrefix(serverId) {
739
+ if (typeof this.prefixResolver === "function") {
740
+ return this.prefixResolver({ serverId });
741
+ }
742
+ return this.prefixResolver;
743
+ }
744
+ /**
745
+ * Check if user is on cooldown
746
+ */
747
+ checkCooldown(userId, metadata) {
748
+ if (metadata.cooldown <= 0) return true;
749
+ const commandCooldowns = this.cooldowns.get(metadata.name);
750
+ if (!commandCooldowns) return true;
751
+ const userCooldown = commandCooldowns.get(userId);
752
+ if (!userCooldown) return true;
753
+ return Date.now() >= userCooldown;
754
+ }
755
+ /**
756
+ * Get remaining cooldown time in ms
757
+ */
758
+ getRemainingCooldown(userId, metadata) {
759
+ const commandCooldowns = this.cooldowns.get(metadata.name);
760
+ if (!commandCooldowns) return 0;
761
+ const userCooldown = commandCooldowns.get(userId);
762
+ if (!userCooldown) return 0;
763
+ return Math.max(0, userCooldown - Date.now());
764
+ }
765
+ /**
766
+ * Set cooldown for a user
767
+ */
768
+ setCooldown(userId, metadata) {
769
+ if (!this.cooldowns.has(metadata.name)) {
770
+ this.cooldowns.set(metadata.name, /* @__PURE__ */ new Map());
771
+ }
772
+ const commandCooldowns = this.cooldowns.get(metadata.name);
773
+ commandCooldowns.set(userId, Date.now() + metadata.cooldown);
774
+ }
775
+ };
776
+ // Annotate the CommonJS export names for ESM import in node:
777
+ 0 && (module.exports = {
778
+ Client,
779
+ CommandRegistry,
780
+ Guard,
781
+ METADATA_KEYS,
782
+ On,
783
+ Once,
784
+ SimpleCommand,
785
+ Stoat,
786
+ buildSimpleCommandMetadata,
787
+ getEventsMetadata,
788
+ getGuards,
789
+ getSimpleCommands,
790
+ isStoatClass
791
+ });