import { SlashCommandBuilder, PermissionFlagsBits, EmbedBuilder } from 'discord.js'; /** * botUtils module - provides administrative bot control commands * Currently implements an owner-only exit command for graceful shutdown. */ // Define slash commands export const commands = [ { data: new SlashCommandBuilder() .setName('exit') .setDescription('Gracefully shutdown the bot (Owner only)') // Restrict to server administrators by default .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .setDMPermission(false) .addIntegerOption(option => option .setName('code') .setDescription('Exit code to use (default 0)') .setRequired(false) ), /** * Execute the exit command: only the configured owner can invoke. * @param {import('discord.js').CommandInteraction} interaction * @param {import('discord.js').Client} client */ async execute(interaction, client) { const ownerId = client.config.owner; // Check invoking user is the bot owner if (interaction.user.id !== String(ownerId)) { return interaction.reply({ content: 'Only the bot owner can shutdown the bot.', ephemeral: true }); } // Determine desired exit code (default 0) const exitCode = interaction.options.getInteger('code') ?? 0; // Validate exit code bounds if (exitCode < 0 || exitCode > 254) { return interaction.reply({ content: 'Exit code must be between 0 and 254 inclusive.', ephemeral: true }); } // Acknowledge before shutting down await interaction.reply({ content: `Shutting down with exit code ${exitCode}...`, ephemeral: true }); client.logger.info( `[cmd:exit] Shutdown initiated by owner ${interaction.user.tag} (${interaction.user.id}), exit code ${exitCode}` ); // Destroy Discord client and exit process try { await client.destroy(); } catch (err) { client.logger.error(`[cmd:exit] Error during client.destroy(): ${err}`); } process.exit(exitCode); } }, // /status: admin-only, shows current client info { data: new SlashCommandBuilder() .setName('status') .setDescription('Show this bot client status and process info') .setDefaultMemberPermissions(PermissionFlagsBits.Administrator) .setDMPermission(false) .addBooleanOption(option => option .setName('ephemeral') .setDescription('Whether the response should be ephemeral') .setRequired(false) ), async execute(interaction, client) { // Determine if response should be ephemeral (default true) const ephemeral = interaction.options.getBoolean('ephemeral') ?? true; await interaction.deferReply({ ephemeral }); // Process metrics const uptimeSec = process.uptime(); const hours = Math.floor(uptimeSec / 3600); const minutes = Math.floor((uptimeSec % 3600) / 60); const seconds = Math.floor(uptimeSec % 60); const uptime = `${hours}h ${minutes}m ${seconds}s`; const mem = process.memoryUsage(); const toMB = bytes => (bytes / 1024 / 1024).toFixed(2); const memoryInfo = `RSS: ${toMB(mem.rss)} MB, Heap: ${toMB(mem.heapUsed)}/${toMB(mem.heapTotal)} MB`; const cpu = process.cpuUsage(); const cpuInfo = `User: ${(cpu.user / 1000).toFixed(2)} ms, System: ${(cpu.system / 1000).toFixed(2)} ms`; const nodeVersion = process.version; const platform = `${process.platform} ${process.arch}`; // Client-specific stats const guildCount = client.guilds.cache.size; const userCount = client.guilds.cache.reduce((sum, g) => sum + (g.memberCount || 0), 0); const commandCount = client.commands.size; // List of loaded optional modules const loadedModules = client.modules ? Array.from(client.modules.keys()) : []; // Build embed for status // Determine if gitUtils module is loaded const gitLoaded = client.modules?.has('gitUtils'); let branch, build, statusBlock; if (gitLoaded) { const git = client.modules.get('gitUtils'); try { branch = await git.getBranch(); build = await git.getShortHash(); const statusRaw = await git.getStatusShort(); // Format status as fenced code block using template literals // Normalize each line with a leading space in a code fence // Prefix raw status output with a single space statusBlock = statusRaw ? '```\n ' + statusRaw + '\n```' : '```\n (clean)\n```'; } catch { branch = 'error'; build = 'error'; // Represent error status in code fence statusBlock = '```\n (error)\n```'; } } // Prepare module list as bullet points const moduleList = loadedModules.length > 0 ? loadedModules.map(m => `• ${m}`).join('\n') : 'None'; // Assemble fields const fields = []; // Client identification fields.push({ name: 'Client', value: client.config.id, inline: false }); // Performance metrics fields.push({ name: 'CPU Usage', value: cpuInfo, inline: false }); fields.push({ name: 'Memory', value: memoryInfo, inline: false }); // Environment fields.push({ name: 'Node.js', value: nodeVersion, inline: true }); fields.push({ name: 'Platform', value: platform, inline: true }); // Uptime fields.push({ name: 'Uptime', value: uptime, inline: true }); // Loaded modules fields.push({ name: 'Modules', value: moduleList, inline: false }); // Entity counts fields.push({ name: 'Commands', value: commandCount.toString(), inline: true }); fields.push({ name: 'Guilds', value: guildCount.toString(), inline: true }); fields.push({ name: 'Users', value: userCount.toString(), inline: true }); // Git reference and status if available if (gitLoaded) { fields.push({ name: 'Git Reference', value: `${branch}/${build}`, inline: false }); fields.push({ name: 'Git Status', value: statusBlock, inline: false }); } // Create embed const embed = new EmbedBuilder() .setAuthor({ name: 'ClientX', iconURL: client.user.displayAvatarURL() }) .setThumbnail(client.user.displayAvatarURL()) .addFields(fields) .setTimestamp(); await interaction.editReply({ embeds: [embed] }); client.logger.info(`[cmd:status] Returned status embed for client ${client.config.id}`); } } ]; // Module loaded logging export async function init(client, clientConfig) { client.logger.info('[module:botUtils] Module loaded'); }