@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/LICENSE +674 -0
- package/README.md +221 -212
- package/dist/index.d.mts +41 -123
- package/dist/index.d.ts +41 -123
- package/dist/index.js +160 -124
- package/dist/index.mjs +158 -117
- package/package.json +12 -8
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
|
|
113
|
-
constructor(extensions = [".js", ".
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
+
}
|