@marshmallow-stoat/mally 0.1.2 → 0.2.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 CHANGED
@@ -30,51 +30,100 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- BaseCommand: () => BaseCommand,
34
- Command: () => Command,
35
33
  CommandRegistry: () => CommandRegistry,
36
34
  Guard: () => Guard,
37
35
  METADATA_KEYS: () => METADATA_KEYS,
38
36
  MallyHandler: () => MallyHandler,
39
37
  SimpleCommand: () => SimpleCommand,
40
38
  Stoat: () => Stoat,
41
- buildCommandMetadata: () => buildCommandMetadata,
42
39
  buildSimpleCommandMetadata: () => buildSimpleCommandMetadata,
43
- getCommandOptions: () => getCommandOptions,
44
40
  getGuards: () => getGuards,
45
41
  getSimpleCommands: () => getSimpleCommands,
46
- isCommand: () => isCommand,
47
42
  isStoatClass: () => isStoatClass
48
43
  });
49
44
  module.exports = __toCommonJS(index_exports);
50
45
 
51
- // src/types.ts
52
- var BaseCommand = class {
53
- /**
54
- * Optional: Called when an error occurs during command execution.
55
- * Override this method to provide custom error handling.
56
- */
57
- async onError(ctx, error) {
58
- await ctx.reply(`An error occurred: ${error.message}`);
59
- }
60
- };
61
-
62
46
  // src/decorators/Stoat.ts
63
47
  var import_reflect_metadata = require("reflect-metadata");
64
48
 
65
49
  // src/decorators/keys.ts
66
50
  var METADATA_KEYS = {
67
- COMMAND_OPTIONS: /* @__PURE__ */ Symbol("mally:command:options"),
68
- IS_COMMAND: /* @__PURE__ */ Symbol("mally:command:isCommand"),
69
51
  IS_STOAT_CLASS: /* @__PURE__ */ Symbol("mally:stoat:isClass"),
70
52
  SIMPLE_COMMANDS: /* @__PURE__ */ Symbol("mally:stoat:simpleCommands"),
71
53
  GUARDS: "mally:command:guards"
72
54
  };
73
55
 
56
+ // src/decorators/store.ts
57
+ var DecoratorStore = class _DecoratorStore {
58
+ constructor() {
59
+ /** Stoat classes with their SimpleCommand methods */
60
+ this.stoatClasses = /* @__PURE__ */ new Map();
61
+ /** Registered commands from @Stoat/@SimpleCommand decorators */
62
+ this.commands = [];
63
+ /** Whether the store has been initialized */
64
+ this.initialized = false;
65
+ }
66
+ static getInstance() {
67
+ if (!_DecoratorStore.instance) {
68
+ _DecoratorStore.instance = new _DecoratorStore();
69
+ }
70
+ return _DecoratorStore.instance;
71
+ }
72
+ /**
73
+ * Register a @Stoat decorated class
74
+ */
75
+ registerStoatClass(classConstructor) {
76
+ if (!this.stoatClasses.has(classConstructor)) {
77
+ const instance = new classConstructor();
78
+ this.stoatClasses.set(classConstructor, instance);
79
+ }
80
+ }
81
+ /**
82
+ * Get all registered Stoat classes with their instances
83
+ */
84
+ getStoatClasses() {
85
+ return this.stoatClasses;
86
+ }
87
+ /**
88
+ * Add a registered command
89
+ */
90
+ addCommand(command) {
91
+ this.commands.push(command);
92
+ }
93
+ /**
94
+ * Get all registered commands
95
+ */
96
+ getCommands() {
97
+ return this.commands;
98
+ }
99
+ /**
100
+ * Clear all registered classes (useful for testing)
101
+ */
102
+ clear() {
103
+ this.stoatClasses.clear();
104
+ this.commands = [];
105
+ this.initialized = false;
106
+ }
107
+ /**
108
+ * Mark as initialized
109
+ */
110
+ markInitialized() {
111
+ this.initialized = true;
112
+ }
113
+ /**
114
+ * Check if initialized
115
+ */
116
+ isInitialized() {
117
+ return this.initialized;
118
+ }
119
+ };
120
+ var decoratorStore = DecoratorStore.getInstance();
121
+
74
122
  // src/decorators/Stoat.ts
75
123
  function Stoat() {
76
124
  return (target) => {
77
125
  Reflect.defineMetadata(METADATA_KEYS.IS_STOAT_CLASS, true, target);
126
+ decoratorStore.registerStoatClass(target);
78
127
  };
79
128
  }
80
129
  function isStoatClass(target) {
@@ -99,37 +148,8 @@ function getSimpleCommands(target) {
99
148
  return Reflect.getMetadata(METADATA_KEYS.SIMPLE_COMMANDS, target) || [];
100
149
  }
101
150
 
102
- // src/decorators/Command.ts
103
- var import_reflect_metadata3 = require("reflect-metadata");
104
- function Command(options = {}) {
105
- return (target) => {
106
- Reflect.defineMetadata(METADATA_KEYS.IS_COMMAND, true, target);
107
- Reflect.defineMetadata(METADATA_KEYS.COMMAND_OPTIONS, options, target);
108
- };
109
- }
110
- function isCommand(target) {
111
- return Reflect.getMetadata(METADATA_KEYS.IS_COMMAND, target) === true;
112
- }
113
- function getCommandOptions(target) {
114
- return Reflect.getMetadata(METADATA_KEYS.COMMAND_OPTIONS, target);
115
- }
116
- function buildCommandMetadata(target, options, category) {
117
- const className = target.name;
118
- const derivedName = className.replace(/Command$/i, "").toLowerCase();
119
- return {
120
- name: options.name ?? derivedName,
121
- description: options.description ?? "No description provided",
122
- aliases: options.aliases ?? [],
123
- permissions: options.permissions ?? [],
124
- category: options.category ?? category ?? "uncategorized",
125
- cooldown: options.cooldown ?? 0,
126
- nsfw: options.nsfw ?? false,
127
- ownerOnly: options.ownerOnly ?? false
128
- };
129
- }
130
-
131
151
  // src/decorators/Guard.ts
132
- var import_reflect_metadata4 = require("reflect-metadata");
152
+ var import_reflect_metadata3 = require("reflect-metadata");
133
153
  function Guard(guardClass) {
134
154
  return (target) => {
135
155
  const existingGuards = Reflect.getMetadata(METADATA_KEYS.GUARDS, target) || [];
@@ -157,12 +177,14 @@ function buildSimpleCommandMetadata(options, methodName, category) {
157
177
 
158
178
  // src/registry.ts
159
179
  var path = __toESM(require("path"));
180
+ var fs = __toESM(require("fs/promises"));
160
181
  var import_node_url = require("url");
161
182
  var import_tinyglobby = require("tinyglobby");
162
- var CommandRegistry = class {
163
- constructor(extensions = [".js", ".ts"]) {
183
+ var _CommandRegistry = class _CommandRegistry {
184
+ constructor(extensions = [".js", ".mjs", ".cjs"]) {
164
185
  this.commands = /* @__PURE__ */ new Map();
165
186
  this.aliases = /* @__PURE__ */ new Map();
187
+ this.processedStoatClasses = /* @__PURE__ */ new Set();
166
188
  this.extensions = extensions;
167
189
  }
168
190
  /**
@@ -187,6 +209,45 @@ var CommandRegistry = class {
187
209
  }
188
210
  console.log(`[Mally] Loaded ${this.commands.size} command(s)`);
189
211
  }
212
+ /**
213
+ * Auto-discover command files across one or more roots.
214
+ */
215
+ async autoDiscover(options = {}) {
216
+ const roots = options.roots?.length ? options.roots : [process.cwd()];
217
+ const includePatterns = options.include?.length ? options.include : this.getDefaultAutoDiscoveryPatterns();
218
+ const patterns = roots.flatMap(
219
+ (root) => includePatterns.map((pattern) => path.join(root, pattern).replace(/\\/g, "/"))
220
+ );
221
+ const files = await (0, import_tinyglobby.glob)(patterns, {
222
+ ignore: [..._CommandRegistry.DEFAULT_AUTO_DISCOVERY_IGNORES, ...options.ignore ?? []],
223
+ absolute: true
224
+ });
225
+ const uniqueFiles = [...new Set(files)];
226
+ let candidateFiles = 0;
227
+ for (const file of uniqueFiles) {
228
+ if (!await this.isLikelyCommandModule(file)) {
229
+ continue;
230
+ }
231
+ candidateFiles++;
232
+ const baseDir = roots.find((root) => {
233
+ const relative2 = path.relative(root, file);
234
+ return relative2 && !relative2.startsWith("..") && !path.isAbsolute(relative2);
235
+ }) ?? roots[0];
236
+ await this.loadFile(file, baseDir);
237
+ }
238
+ console.log(`[Mally] Auto-discovered ${candidateFiles} candidate file(s), loaded ${this.commands.size} command(s)`);
239
+ }
240
+ getDefaultAutoDiscoveryPatterns() {
241
+ return this.extensions.map((ext) => `**/*${ext}`);
242
+ }
243
+ async isLikelyCommandModule(filePath) {
244
+ try {
245
+ const source = await fs.readFile(filePath, "utf8");
246
+ return source.includes("Stoat") || source.includes("SimpleCommand") || source.includes("Command") || source.includes("mally:command");
247
+ } catch {
248
+ return true;
249
+ }
250
+ }
190
251
  /**
191
252
  * Register a command instance
192
253
  */
@@ -197,9 +258,6 @@ var CommandRegistry = class {
197
258
  return;
198
259
  }
199
260
  this.validateGuards(classConstructor, metadata.name);
200
- if (!methodName) {
201
- this.validateCooldown(instance, metadata);
202
- }
203
261
  this.commands.set(name, { instance, metadata, methodName, classConstructor });
204
262
  for (const alias of metadata.aliases) {
205
263
  const aliasLower = alias.toLowerCase();
@@ -256,6 +314,7 @@ var CommandRegistry = class {
256
314
  clear() {
257
315
  this.commands.clear();
258
316
  this.aliases.clear();
317
+ this.processedStoatClasses.clear();
259
318
  }
260
319
  /**
261
320
  * Iterate over commands
@@ -300,77 +359,46 @@ var CommandRegistry = class {
300
359
  }
301
360
  }
302
361
  }
303
- /**
304
- * Validate that commands with cooldowns implement the onCooldown method
305
- * @param instance
306
- * @param metadata
307
- * @private
308
- */
309
- validateCooldown(instance, metadata) {
310
- if (metadata.cooldown > 0 && typeof instance.onCooldown !== "function") {
311
- console.error(
312
- `[Mally] FATAL: Command "${metadata.name}" has a cooldown of ${metadata.cooldown}ms but does not implement onCooldown() method.`
313
- );
314
- console.error(
315
- `[Mally] Commands with cooldowns must implement onCooldown(ctx, remaining) to handle cooldown messages.`
316
- );
317
- process.exit(1);
318
- }
319
- }
320
362
  /**
321
363
  * Load commands from a single file
322
364
  */
323
365
  async loadFile(filePath, baseDir) {
324
366
  try {
367
+ const knownStoatClasses = new Set(decoratorStore.getStoatClasses().keys());
325
368
  const fileUrl = (0, import_node_url.pathToFileURL)(filePath).href;
326
- const module2 = await import(fileUrl);
327
- for (const exportKey of Object.keys(module2)) {
328
- const exported = module2[exportKey];
329
- if (typeof exported !== "function") {
330
- continue;
331
- }
332
- if (isStoatClass(exported)) {
333
- const instance2 = new exported();
334
- const simpleCommands = getSimpleCommands(exported);
335
- const category2 = this.getCategoryFromPath(filePath, baseDir);
336
- if (simpleCommands.length === 0) {
337
- console.warn(
338
- `[Mally] Class ${exported.name} is decorated with @Stoat but has no @SimpleCommand methods. Skipping...`
339
- );
340
- continue;
341
- }
342
- for (const cmdDef of simpleCommands) {
343
- const method = instance2[cmdDef.methodName];
344
- if (typeof method !== "function") {
345
- console.warn(`[Mally] Method ${cmdDef.methodName} not found on ${exported.name}. Skipping...`);
346
- continue;
347
- }
348
- const metadata2 = buildSimpleCommandMetadata(cmdDef.options, cmdDef.methodName, category2);
349
- this.register(instance2, metadata2, exported, cmdDef.methodName);
350
- }
351
- continue;
352
- }
353
- if (!isCommand(exported)) {
354
- continue;
355
- }
356
- const options = getCommandOptions(exported);
357
- if (!options) continue;
358
- const instance = new exported();
359
- if (typeof instance.run !== "function") {
360
- console.warn(
361
- `[Mally] Class ${exported.name} is decorated with @Command but does not implement run() method. Skipping...`
362
- );
369
+ await import(fileUrl);
370
+ const allStoatClasses = decoratorStore.getStoatClasses();
371
+ for (const [stoatClass, stoatInstance] of allStoatClasses.entries()) {
372
+ if (knownStoatClasses.has(stoatClass) || this.processedStoatClasses.has(stoatClass)) {
363
373
  continue;
364
374
  }
365
- const category = this.getCategoryFromPath(filePath, baseDir);
366
- const metadata = buildCommandMetadata(exported, options, category);
367
- instance.metadata = metadata;
368
- this.register(instance, metadata, exported);
375
+ this.registerStoatClassCommands(stoatClass, stoatInstance, filePath, baseDir);
369
376
  }
370
377
  } catch (error) {
371
378
  console.error(`[Mally] Failed to load command file: ${filePath}`, error);
372
379
  }
373
380
  }
381
+ registerStoatClassCommands(stoatClass, instance, filePath, baseDir) {
382
+ const simpleCommands = getSimpleCommands(stoatClass);
383
+ const category = this.getCategoryFromPath(filePath, baseDir);
384
+ if (simpleCommands.length === 0) {
385
+ console.warn(
386
+ `[Mally] Class ${stoatClass.name} is decorated with @Stoat but has no @SimpleCommand methods. Skipping...`
387
+ );
388
+ this.processedStoatClasses.add(stoatClass);
389
+ return;
390
+ }
391
+ for (const cmdDef of simpleCommands) {
392
+ const method = instance[cmdDef.methodName];
393
+ if (typeof method !== "function") {
394
+ console.warn(`[Mally] Method ${cmdDef.methodName} not found on ${stoatClass.name}. Skipping...`);
395
+ continue;
396
+ }
397
+ const metadata = buildSimpleCommandMetadata(cmdDef.options, cmdDef.methodName, category);
398
+ this.register(instance, metadata, stoatClass, cmdDef.methodName);
399
+ }
400
+ this.processedStoatClasses.add(stoatClass);
401
+ }
374
402
  /**
375
403
  * Derive category from file path relative to base directory
376
404
  */
@@ -383,14 +411,23 @@ var CommandRegistry = class {
383
411
  return void 0;
384
412
  }
385
413
  };
414
+ _CommandRegistry.DEFAULT_AUTO_DISCOVERY_IGNORES = [
415
+ "**/node_modules/**",
416
+ "**/.git/**",
417
+ "**/*.d.ts",
418
+ "**/*.test.*",
419
+ "**/*.spec.*"
420
+ ];
421
+ var CommandRegistry = _CommandRegistry;
386
422
 
387
423
  // src/handler.ts
388
- var import_reflect_metadata5 = require("reflect-metadata");
424
+ var import_reflect_metadata4 = require("reflect-metadata");
389
425
  var MallyHandler = class {
390
426
  constructor(options) {
391
427
  this.cooldowns = /* @__PURE__ */ new Map();
392
428
  this.client = options.client;
393
429
  this.commandsDir = options.commandsDir;
430
+ this.discoveryOptions = options.discovery;
394
431
  this.prefixResolver = options.prefix;
395
432
  this.owners = new Set(options.owners ?? []);
396
433
  this.registry = new CommandRegistry(options.extensions);
@@ -400,7 +437,11 @@ var MallyHandler = class {
400
437
  * Initialize the handler - load all commands
401
438
  */
402
439
  async init() {
403
- await this.registry.loadFromDirectory(this.commandsDir);
440
+ if (this.commandsDir) {
441
+ await this.registry.loadFromDirectory(this.commandsDir);
442
+ return;
443
+ }
444
+ await this.registry.autoDiscover(this.discoveryOptions);
404
445
  }
405
446
  /**
406
447
  * Parse a raw message into command context
@@ -537,11 +578,7 @@ var MallyHandler = class {
537
578
  return false;
538
579
  }
539
580
  try {
540
- if (methodName) {
541
- await instance[methodName](ctx);
542
- } else {
543
- await instance.run(ctx);
544
- }
581
+ await instance[methodName](ctx);
545
582
  if (metadata.cooldown > 0) {
546
583
  this.setCooldown(ctx.authorId, metadata);
547
584
  }
@@ -580,7 +617,11 @@ var MallyHandler = class {
580
617
  async reload() {
581
618
  this.registry.clear();
582
619
  this.cooldowns.clear();
583
- await this.registry.loadFromDirectory(this.commandsDir);
620
+ if (this.commandsDir) {
621
+ await this.registry.loadFromDirectory(this.commandsDir);
622
+ return;
623
+ }
624
+ await this.registry.autoDiscover(this.discoveryOptions);
584
625
  }
585
626
  /**
586
627
  * Check if a user is an owner
@@ -643,19 +684,14 @@ var MallyHandler = class {
643
684
  };
644
685
  // Annotate the CommonJS export names for ESM import in node:
645
686
  0 && (module.exports = {
646
- BaseCommand,
647
- Command,
648
687
  CommandRegistry,
649
688
  Guard,
650
689
  METADATA_KEYS,
651
690
  MallyHandler,
652
691
  SimpleCommand,
653
692
  Stoat,
654
- buildCommandMetadata,
655
693
  buildSimpleCommandMetadata,
656
- getCommandOptions,
657
694
  getGuards,
658
695
  getSimpleCommands,
659
- isCommand,
660
696
  isStoatClass
661
697
  });