feat(lib): extract CommandLoader from BotClient
- Create dedicated CommandLoader class for command loading logic - Implement recursive directory scanning - Add category extraction from file paths - Add command validation and config-based filtering - Improve error handling with structured results - Enable better testability and separation of concerns
This commit is contained in:
110
src/lib/loaders/CommandLoader.ts
Normal file
110
src/lib/loaders/CommandLoader.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import { readdir } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import type { Command } from "@lib/types";
|
||||||
|
import { config } from "@lib/config";
|
||||||
|
import type { LoadResult, LoadError } from "./types";
|
||||||
|
import type { Client } from "../BotClient";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles loading commands from the file system
|
||||||
|
*/
|
||||||
|
export class CommandLoader {
|
||||||
|
private client: Client;
|
||||||
|
|
||||||
|
constructor(client: Client) {
|
||||||
|
this.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load commands from a directory recursively
|
||||||
|
*/
|
||||||
|
async loadFromDirectory(dir: string, reload: boolean = false): Promise<LoadResult> {
|
||||||
|
const result: LoadResult = { loaded: 0, skipped: 0, errors: [] };
|
||||||
|
await this.scanDirectory(dir, reload, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively scan directory for command files
|
||||||
|
*/
|
||||||
|
private async scanDirectory(dir: string, reload: boolean, result: LoadResult): Promise<void> {
|
||||||
|
try {
|
||||||
|
const files = await readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = join(dir, file.name);
|
||||||
|
|
||||||
|
if (file.isDirectory()) {
|
||||||
|
await this.scanDirectory(filePath, reload, result);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file.name.endsWith('.ts') && !file.name.endsWith('.js')) continue;
|
||||||
|
|
||||||
|
await this.loadCommandFile(filePath, reload, result);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error reading directory ${dir}:`, error);
|
||||||
|
result.errors.push({ file: dir, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load a single command file
|
||||||
|
*/
|
||||||
|
private async loadCommandFile(filePath: string, reload: boolean, result: LoadResult): Promise<void> {
|
||||||
|
try {
|
||||||
|
const importPath = reload ? `${filePath}?t=${Date.now()}` : filePath;
|
||||||
|
const commandModule = await import(importPath);
|
||||||
|
const commands = Object.values(commandModule);
|
||||||
|
|
||||||
|
if (commands.length === 0) {
|
||||||
|
console.warn(`⚠️ No commands found in ${filePath}`);
|
||||||
|
result.skipped++;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const category = this.extractCategory(filePath);
|
||||||
|
|
||||||
|
for (const command of commands) {
|
||||||
|
if (this.isValidCommand(command)) {
|
||||||
|
command.category = category;
|
||||||
|
|
||||||
|
const isEnabled = config.commands[command.data.name] !== false;
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
console.log(`🚫 Skipping disabled command: ${command.data.name}`);
|
||||||
|
result.skipped++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.client.commands.set(command.data.name, command);
|
||||||
|
console.log(`✅ Loaded command: ${command.data.name}`);
|
||||||
|
result.loaded++;
|
||||||
|
} else {
|
||||||
|
console.warn(`⚠️ Skipping invalid command in ${filePath}`);
|
||||||
|
result.skipped++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`❌ Failed to load command from ${filePath}:`, error);
|
||||||
|
result.errors.push({ file: filePath, error });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract category from file path
|
||||||
|
* e.g., /path/to/commands/admin/features.ts -> "admin"
|
||||||
|
*/
|
||||||
|
private extractCategory(filePath: string): string {
|
||||||
|
const pathParts = filePath.split('/');
|
||||||
|
return pathParts[pathParts.length - 2] ?? "uncategorized";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type guard to validate command structure
|
||||||
|
*/
|
||||||
|
private isValidCommand(command: any): command is Command {
|
||||||
|
return command && typeof command === 'object' && 'data' in command && 'execute' in command;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user