@marshmallow-stoat/mally 0.1.0 → 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.mjs CHANGED
@@ -1,30 +1,84 @@
1
- // src/types.ts
2
- var BaseCommand = class {
3
- /**
4
- * Optional: Called when an error occurs during command execution.
5
- * Override this method to provide custom error handling.
6
- */
7
- async onError(ctx, error) {
8
- await ctx.reply(`An error occurred: ${error.message}`);
9
- }
10
- };
11
-
12
1
  // src/decorators/Stoat.ts
13
2
  import "reflect-metadata";
14
3
 
15
4
  // src/decorators/keys.ts
16
5
  var METADATA_KEYS = {
17
- COMMAND_OPTIONS: /* @__PURE__ */ Symbol("mally:command:options"),
18
- IS_COMMAND: /* @__PURE__ */ Symbol("mally:command:isCommand"),
19
6
  IS_STOAT_CLASS: /* @__PURE__ */ Symbol("mally:stoat:isClass"),
20
7
  SIMPLE_COMMANDS: /* @__PURE__ */ Symbol("mally:stoat:simpleCommands"),
21
8
  GUARDS: "mally:command:guards"
22
9
  };
23
10
 
11
+ // src/decorators/store.ts
12
+ var DecoratorStore = class _DecoratorStore {
13
+ constructor() {
14
+ /** Stoat classes with their SimpleCommand methods */
15
+ this.stoatClasses = /* @__PURE__ */ new Map();
16
+ /** Registered commands from @Stoat/@SimpleCommand decorators */
17
+ this.commands = [];
18
+ /** Whether the store has been initialized */
19
+ this.initialized = false;
20
+ }
21
+ static getInstance() {
22
+ if (!_DecoratorStore.instance) {
23
+ _DecoratorStore.instance = new _DecoratorStore();
24
+ }
25
+ return _DecoratorStore.instance;
26
+ }
27
+ /**
28
+ * Register a @Stoat decorated class
29
+ */
30
+ registerStoatClass(classConstructor) {
31
+ if (!this.stoatClasses.has(classConstructor)) {
32
+ const instance = new classConstructor();
33
+ this.stoatClasses.set(classConstructor, instance);
34
+ }
35
+ }
36
+ /**
37
+ * Get all registered Stoat classes with their instances
38
+ */
39
+ getStoatClasses() {
40
+ return this.stoatClasses;
41
+ }
42
+ /**
43
+ * Add a registered command
44
+ */
45
+ addCommand(command) {
46
+ this.commands.push(command);
47
+ }
48
+ /**
49
+ * Get all registered commands
50
+ */
51
+ getCommands() {
52
+ return this.commands;
53
+ }
54
+ /**
55
+ * Clear all registered classes (useful for testing)
56
+ */
57
+ clear() {
58
+ this.stoatClasses.clear();
59
+ this.commands = [];
60
+ this.initialized = false;
61
+ }
62
+ /**
63
+ * Mark as initialized
64
+ */
65
+ markInitialized() {
66
+ this.initialized = true;
67
+ }
68
+ /**
69
+ * Check if initialized
70
+ */
71
+ isInitialized() {
72
+ return this.initialized;
73
+ }
74
+ };
75
+ var decoratorStore = DecoratorStore.getInstance();
76
+
24
77
  // src/decorators/Stoat.ts
25
78
  function Stoat() {
26
79
  return (target) => {
27
80
  Reflect.defineMetadata(METADATA_KEYS.IS_STOAT_CLASS, true, target);
81
+ decoratorStore.registerStoatClass(target);
28
82
  };
29
83
  }
30
84
  function isStoatClass(target) {
@@ -49,35 +103,6 @@ function getSimpleCommands(target) {
49
103
  return Reflect.getMetadata(METADATA_KEYS.SIMPLE_COMMANDS, target) || [];
50
104
  }
51
105
 
52
- // src/decorators/Command.ts
53
- import "reflect-metadata";
54
- function Command(options = {}) {
55
- return (target) => {
56
- Reflect.defineMetadata(METADATA_KEYS.IS_COMMAND, true, target);
57
- Reflect.defineMetadata(METADATA_KEYS.COMMAND_OPTIONS, options, target);
58
- };
59
- }
60
- function isCommand(target) {
61
- return Reflect.getMetadata(METADATA_KEYS.IS_COMMAND, target) === true;
62
- }
63
- function getCommandOptions(target) {
64
- return Reflect.getMetadata(METADATA_KEYS.COMMAND_OPTIONS, target);
65
- }
66
- function buildCommandMetadata(target, options, category) {
67
- const className = target.name;
68
- const derivedName = className.replace(/Command$/i, "").toLowerCase();
69
- return {
70
- name: options.name ?? derivedName,
71
- description: options.description ?? "No description provided",
72
- aliases: options.aliases ?? [],
73
- permissions: options.permissions ?? [],
74
- category: options.category ?? category ?? "uncategorized",
75
- cooldown: options.cooldown ?? 0,
76
- nsfw: options.nsfw ?? false,
77
- ownerOnly: options.ownerOnly ?? false
78
- };
79
- }
80
-
81
106
  // src/decorators/Guard.ts
82
107
  import "reflect-metadata";
83
108
  function Guard(guardClass) {
@@ -107,12 +132,14 @@ function buildSimpleCommandMetadata(options, methodName, category) {
107
132
 
108
133
  // src/registry.ts
109
134
  import * as path from "path";
135
+ import * as fs from "fs/promises";
110
136
  import { pathToFileURL } from "url";
111
137
  import { glob } from "tinyglobby";
112
- var CommandRegistry = class {
113
- constructor(extensions = [".js", ".ts"]) {
138
+ var _CommandRegistry = class _CommandRegistry {
139
+ constructor(extensions = [".js", ".mjs", ".cjs"]) {
114
140
  this.commands = /* @__PURE__ */ new Map();
115
141
  this.aliases = /* @__PURE__ */ new Map();
142
+ this.processedStoatClasses = /* @__PURE__ */ new Set();
116
143
  this.extensions = extensions;
117
144
  }
118
145
  /**
@@ -137,6 +164,45 @@ var CommandRegistry = class {
137
164
  }
138
165
  console.log(`[Mally] Loaded ${this.commands.size} command(s)`);
139
166
  }
167
+ /**
168
+ * Auto-discover command files across one or more roots.
169
+ */
170
+ async autoDiscover(options = {}) {
171
+ const roots = options.roots?.length ? options.roots : [process.cwd()];
172
+ const includePatterns = options.include?.length ? options.include : this.getDefaultAutoDiscoveryPatterns();
173
+ const patterns = roots.flatMap(
174
+ (root) => includePatterns.map((pattern) => path.join(root, pattern).replace(/\\/g, "/"))
175
+ );
176
+ const files = await glob(patterns, {
177
+ ignore: [..._CommandRegistry.DEFAULT_AUTO_DISCOVERY_IGNORES, ...options.ignore ?? []],
178
+ absolute: true
179
+ });
180
+ const uniqueFiles = [...new Set(files)];
181
+ let candidateFiles = 0;
182
+ for (const file of uniqueFiles) {
183
+ if (!await this.isLikelyCommandModule(file)) {
184
+ continue;
185
+ }
186
+ candidateFiles++;
187
+ const baseDir = roots.find((root) => {
188
+ const relative2 = path.relative(root, file);
189
+ return relative2 && !relative2.startsWith("..") && !path.isAbsolute(relative2);
190
+ }) ?? roots[0];
191
+ await this.loadFile(file, baseDir);
192
+ }
193
+ console.log(`[Mally] Auto-discovered ${candidateFiles} candidate file(s), loaded ${this.commands.size} command(s)`);
194
+ }
195
+ getDefaultAutoDiscoveryPatterns() {
196
+ return this.extensions.map((ext) => `**/*${ext}`);
197
+ }
198
+ async isLikelyCommandModule(filePath) {
199
+ try {
200
+ const source = await fs.readFile(filePath, "utf8");
201
+ return source.includes("Stoat") || source.includes("SimpleCommand") || source.includes("Command") || source.includes("mally:command");
202
+ } catch {
203
+ return true;
204
+ }
205
+ }
140
206
  /**
141
207
  * Register a command instance
142
208
  */
@@ -147,9 +213,6 @@ var CommandRegistry = class {
147
213
  return;
148
214
  }
149
215
  this.validateGuards(classConstructor, metadata.name);
150
- if (!methodName) {
151
- this.validateCooldown(instance, metadata);
152
- }
153
216
  this.commands.set(name, { instance, metadata, methodName, classConstructor });
154
217
  for (const alias of metadata.aliases) {
155
218
  const aliasLower = alias.toLowerCase();
@@ -206,6 +269,7 @@ var CommandRegistry = class {
206
269
  clear() {
207
270
  this.commands.clear();
208
271
  this.aliases.clear();
272
+ this.processedStoatClasses.clear();
209
273
  }
210
274
  /**
211
275
  * Iterate over commands
@@ -250,77 +314,46 @@ var CommandRegistry = class {
250
314
  }
251
315
  }
252
316
  }
253
- /**
254
- * Validate that commands with cooldowns implement the onCooldown method
255
- * @param instance
256
- * @param metadata
257
- * @private
258
- */
259
- validateCooldown(instance, metadata) {
260
- if (metadata.cooldown > 0 && typeof instance.onCooldown !== "function") {
261
- console.error(
262
- `[Mally] FATAL: Command "${metadata.name}" has a cooldown of ${metadata.cooldown}ms but does not implement onCooldown() method.`
263
- );
264
- console.error(
265
- `[Mally] Commands with cooldowns must implement onCooldown(ctx, remaining) to handle cooldown messages.`
266
- );
267
- process.exit(1);
268
- }
269
- }
270
317
  /**
271
318
  * Load commands from a single file
272
319
  */
273
320
  async loadFile(filePath, baseDir) {
274
321
  try {
322
+ const knownStoatClasses = new Set(decoratorStore.getStoatClasses().keys());
275
323
  const fileUrl = pathToFileURL(filePath).href;
276
- const module = await import(fileUrl);
277
- for (const exportKey of Object.keys(module)) {
278
- const exported = module[exportKey];
279
- if (typeof exported !== "function") {
280
- continue;
281
- }
282
- if (isStoatClass(exported)) {
283
- const instance2 = new exported();
284
- const simpleCommands = getSimpleCommands(exported);
285
- const category2 = this.getCategoryFromPath(filePath, baseDir);
286
- if (simpleCommands.length === 0) {
287
- console.warn(
288
- `[Mally] Class ${exported.name} is decorated with @Stoat but has no @SimpleCommand methods. Skipping...`
289
- );
290
- continue;
291
- }
292
- for (const cmdDef of simpleCommands) {
293
- const method = instance2[cmdDef.methodName];
294
- if (typeof method !== "function") {
295
- console.warn(`[Mally] Method ${cmdDef.methodName} not found on ${exported.name}. Skipping...`);
296
- continue;
297
- }
298
- const metadata2 = buildSimpleCommandMetadata(cmdDef.options, cmdDef.methodName, category2);
299
- this.register(instance2, metadata2, exported, cmdDef.methodName);
300
- }
324
+ await import(fileUrl);
325
+ const allStoatClasses = decoratorStore.getStoatClasses();
326
+ for (const [stoatClass, stoatInstance] of allStoatClasses.entries()) {
327
+ if (knownStoatClasses.has(stoatClass) || this.processedStoatClasses.has(stoatClass)) {
301
328
  continue;
302
329
  }
303
- if (!isCommand(exported)) {
304
- continue;
305
- }
306
- const options = getCommandOptions(exported);
307
- if (!options) continue;
308
- const instance = new exported();
309
- if (typeof instance.run !== "function") {
310
- console.warn(
311
- `[Mally] Class ${exported.name} is decorated with @Command but does not implement run() method. Skipping...`
312
- );
313
- continue;
314
- }
315
- const category = this.getCategoryFromPath(filePath, baseDir);
316
- const metadata = buildCommandMetadata(exported, options, category);
317
- instance.metadata = metadata;
318
- this.register(instance, metadata, exported);
330
+ this.registerStoatClassCommands(stoatClass, stoatInstance, filePath, baseDir);
319
331
  }
320
332
  } catch (error) {
321
333
  console.error(`[Mally] Failed to load command file: ${filePath}`, error);
322
334
  }
323
335
  }
336
+ registerStoatClassCommands(stoatClass, instance, filePath, baseDir) {
337
+ const simpleCommands = getSimpleCommands(stoatClass);
338
+ const category = this.getCategoryFromPath(filePath, baseDir);
339
+ if (simpleCommands.length === 0) {
340
+ console.warn(
341
+ `[Mally] Class ${stoatClass.name} is decorated with @Stoat but has no @SimpleCommand methods. Skipping...`
342
+ );
343
+ this.processedStoatClasses.add(stoatClass);
344
+ return;
345
+ }
346
+ for (const cmdDef of simpleCommands) {
347
+ const method = instance[cmdDef.methodName];
348
+ if (typeof method !== "function") {
349
+ console.warn(`[Mally] Method ${cmdDef.methodName} not found on ${stoatClass.name}. Skipping...`);
350
+ continue;
351
+ }
352
+ const metadata = buildSimpleCommandMetadata(cmdDef.options, cmdDef.methodName, category);
353
+ this.register(instance, metadata, stoatClass, cmdDef.methodName);
354
+ }
355
+ this.processedStoatClasses.add(stoatClass);
356
+ }
324
357
  /**
325
358
  * Derive category from file path relative to base directory
326
359
  */
@@ -333,6 +366,14 @@ var CommandRegistry = class {
333
366
  return void 0;
334
367
  }
335
368
  };
369
+ _CommandRegistry.DEFAULT_AUTO_DISCOVERY_IGNORES = [
370
+ "**/node_modules/**",
371
+ "**/.git/**",
372
+ "**/*.d.ts",
373
+ "**/*.test.*",
374
+ "**/*.spec.*"
375
+ ];
376
+ var CommandRegistry = _CommandRegistry;
336
377
 
337
378
  // src/handler.ts
338
379
  import "reflect-metadata";
@@ -341,6 +382,7 @@ var MallyHandler = class {
341
382
  this.cooldowns = /* @__PURE__ */ new Map();
342
383
  this.client = options.client;
343
384
  this.commandsDir = options.commandsDir;
385
+ this.discoveryOptions = options.discovery;
344
386
  this.prefixResolver = options.prefix;
345
387
  this.owners = new Set(options.owners ?? []);
346
388
  this.registry = new CommandRegistry(options.extensions);
@@ -350,7 +392,11 @@ var MallyHandler = class {
350
392
  * Initialize the handler - load all commands
351
393
  */
352
394
  async init() {
353
- await this.registry.loadFromDirectory(this.commandsDir);
395
+ if (this.commandsDir) {
396
+ await this.registry.loadFromDirectory(this.commandsDir);
397
+ return;
398
+ }
399
+ await this.registry.autoDiscover(this.discoveryOptions);
354
400
  }
355
401
  /**
356
402
  * Parse a raw message into command context
@@ -487,11 +533,7 @@ var MallyHandler = class {
487
533
  return false;
488
534
  }
489
535
  try {
490
- if (methodName) {
491
- await instance[methodName](ctx);
492
- } else {
493
- await instance.run(ctx);
494
- }
536
+ await instance[methodName](ctx);
495
537
  if (metadata.cooldown > 0) {
496
538
  this.setCooldown(ctx.authorId, metadata);
497
539
  }
@@ -530,7 +572,11 @@ var MallyHandler = class {
530
572
  async reload() {
531
573
  this.registry.clear();
532
574
  this.cooldowns.clear();
533
- await this.registry.loadFromDirectory(this.commandsDir);
575
+ if (this.commandsDir) {
576
+ await this.registry.loadFromDirectory(this.commandsDir);
577
+ return;
578
+ }
579
+ await this.registry.autoDiscover(this.discoveryOptions);
534
580
  }
535
581
  /**
536
582
  * Check if a user is an owner
@@ -592,19 +638,14 @@ var MallyHandler = class {
592
638
  }
593
639
  };
594
640
  export {
595
- BaseCommand,
596
- Command,
597
641
  CommandRegistry,
598
642
  Guard,
599
643
  METADATA_KEYS,
600
644
  MallyHandler,
601
645
  SimpleCommand,
602
646
  Stoat,
603
- buildCommandMetadata,
604
647
  buildSimpleCommandMetadata,
605
- getCommandOptions,
606
648
  getGuards,
607
649
  getSimpleCommands,
608
- isCommand,
609
650
  isStoatClass
610
651
  };
package/package.json CHANGED
@@ -1,7 +1,11 @@
1
1
  {
2
2
  "name": "@marshmallow-stoat/mally",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "A high-performance, decorator-based command handler for the Stoat ecosystem.",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/Arsabutispik/marshmallow"
8
+ },
5
9
  "main": "./dist/index.js",
6
10
  "module": "./dist/index.mjs",
7
11
  "types": "./dist/index.d.ts",
@@ -19,12 +23,6 @@
19
23
  "publishConfig": {
20
24
  "access": "public"
21
25
  },
22
- "scripts": {
23
- "build": "tsup src/index.ts --format cjs,esm --dts",
24
- "dev": "tsup src/index.ts --format cjs,esm --watch --dts",
25
- "lint": "eslint src/**/*.ts",
26
- "format": "prettier --write src/**/*.ts"
27
- },
28
26
  "dependencies": {
29
27
  "reflect-metadata": "^0.2.2",
30
28
  "stoat.js": "^7.3.6",
@@ -38,5 +36,11 @@
38
36
  },
39
37
  "peerDependencies": {
40
38
  "reflect-metadata": "^0.2.0"
39
+ },
40
+ "scripts": {
41
+ "build": "tsup src/index.ts --format cjs,esm --dts",
42
+ "dev": "tsup src/index.ts --format cjs,esm --watch --dts",
43
+ "lint": "eslint src/**/*.ts",
44
+ "format": "prettier --write src/**/*.ts"
41
45
  }
42
- }
46
+ }