@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/README.md +32 -23
- 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 +1 -1
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
|
|
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
|
|
163
|
-
constructor(extensions = [".js", ".
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
});
|