import * as fs from "fs"; import * as path from "path"; import { SlashCommandBuilder, SlashCommandSubcommandBuilder, SlashCommandSubcommandGroupBuilder, SlashCommandStringOption, SlashCommandIntegerOption, SlashCommandBooleanOption, SlashCommandUserOption, SlashCommandChannelOption, SlashCommandRoleOption, SlashCommandMentionableOption, SlashCommandNumberOption } from "discord.js"; import { DEV_MODE } from "../../utils/FrostSentinelInstances"; import { logger } from "../../utils/Logger"; function buildSlashCommand(commandData: any) { const cmd = new SlashCommandBuilder() .setName(commandData.name) .setDescription(commandData.description); if (commandData.options) { commandData.options.forEach((opt: any) => { switch (opt.type) { case 1: // SUB_COMMAND cmd.addSubcommand(sub => { sub.setName(opt.name) .setDescription(opt.description); if (opt.options) { opt.options.forEach((subOpt: any) => { addOptionToBuilder(sub, subOpt); }); } return sub; }); break; case 2: // SUB_COMMAND_GROUP cmd.addSubcommandGroup(group => { group.setName(opt.name) .setDescription(opt.description); if (opt.options) { opt.options.forEach((subcommand: any) => { group.addSubcommand(sub => { sub.setName(subcommand.name) .setDescription(subcommand.description); if (subcommand.options) { subcommand.options.forEach((subOpt: any) => { addOptionToBuilder(sub, subOpt); }); } return sub; }); }); } return group; }); break; default: // For root-level options (STRING, USER, CHANNEL, etc) addOptionToBuilder(cmd, opt); break; } }); } return cmd; } function addOptionToBuilder(builder: any, opt: any) { switch (opt.type) { case 3: // STRING builder.addStringOption((o: SlashCommandStringOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); if (opt.choices) { o.addChoices(...opt.choices.map((choice: any) => ({ name: choice.name, value: choice.value }))); } if (opt.autocomplete) { o.setAutocomplete(true); } return o; }); break; case 4: // INTEGER builder.addIntegerOption((o: SlashCommandIntegerOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); if (opt.choices) { o.addChoices(...opt.choices.map((choice: any) => ({ name: choice.name, value: choice.value }))); } if (typeof opt.minValue === "number") o.setMinValue(opt.minValue); if (typeof opt.maxValue === "number") o.setMaxValue(opt.maxValue); return o; }); break; case 5: // BOOLEAN builder.addBooleanOption((o: SlashCommandBooleanOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); return o; }); break; case 6: // USER builder.addUserOption((o: SlashCommandUserOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); return o; }); break; case 7: // CHANNEL builder.addChannelOption((o: SlashCommandChannelOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); if (opt.channelTypes) { o.addChannelTypes(opt.channelTypes); } return o; }); break; case 8: // ROLE builder.addRoleOption((o: SlashCommandRoleOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); return o; }); break; case 9: // MENTIONABLE (user or role) builder.addMentionableOption((o: SlashCommandMentionableOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); return o; }); break; case 10: // NUMBER (float) builder.addNumberOption((o: SlashCommandNumberOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); if (opt.choices) { o.addChoices(...opt.choices.map((choice: any) => ({ name: choice.name, value: choice.value }))); } if (typeof opt.minValue === "number") o.setMinValue(opt.minValue); if (typeof opt.maxValue === "number") o.setMaxValue(opt.maxValue); return o; }); break; case 11: // ATTACHMENT builder.addAttachmentOption((o: import("discord.js").SlashCommandAttachmentOption) => { o.setName(opt.name) .setDescription(opt.description) .setRequired(opt.required || false); return o; }); break; default: console.warn(`Unknown option type ${opt.type} for option ${opt.name}`); break; } } export function loadSlashCommands() { const slashCommands = new Map(); const isDevelopment = DEV_MODE || false; logger.info(`Loading slash commands. Development mode: ${isDevelopment}`); // Root commands folder (where Moderation, SystemCommands, etc. live) const rootPath = path.join(__dirname, "../commands"); if (!fs.existsSync(rootPath)) { logger.error(`Commands root folder not found: ${rootPath}`); return slashCommands; } // Helper: recursively walk through subfolders function walk(dir: string): string[] { const entries = fs.readdirSync(dir, { withFileTypes: true }); const files: string[] = []; for (const entry of entries) { const fullPath = path.join(dir, entry.name); if (entry.isDirectory()) { files.push(...walk(fullPath)); // recurse } else if (entry.isFile() && (entry.name.endsWith(".ts") || entry.name.endsWith(".js"))) { files.push(fullPath); } } return files; } const commandFiles = walk(rootPath); for (const file of commandFiles) { const commandModule = require(file); const { commandData, default: commandExecute } = commandModule; if (!commandData || !commandData.name) continue; if (commandData.commandStats && commandData.commandStats.enabled === false) { const reason = commandData.commandStats.reason || "No reason provided"; logger.warn(`Command "${commandData.name}" disabled. Reason: ${reason}`, "FrostSentinel.loadSlashCommands"); continue; } const slashCommand = buildSlashCommand(commandData); if (commandData.default_member_permissions) { slashCommand.setDefaultMemberPermissions(commandData.default_member_permissions); } if (isDevelopment) { slashCommand.setDMPermission(false); slashCommands.set(commandData.name, { command: slashCommand, execute: commandExecute, guildOnly: true, }); } else { if (commandData.dmPermission === false || commandData.guildOnly) { slashCommand.setDMPermission(false); } slashCommands.set(commandData.name, { command: slashCommand, execute: commandExecute, guildOnly: commandData.guildOnly || false, }); } }