From ce7d4525b2295acea80022518b22165dbdb40461 Mon Sep 17 00:00:00 2001 From: syntaxbullet Date: Thu, 18 Dec 2025 16:36:23 +0100 Subject: [PATCH] feat: split `reload` command into `refresh` for command reloading and `update` for git-based bot restarts with update checking and confirmation. --- src/commands/admin/refresh.ts | 33 ++++++++++ src/commands/admin/reload.ts | 12 ++-- src/commands/admin/update.ts | 116 ++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 5 deletions(-) create mode 100644 src/commands/admin/refresh.ts create mode 100644 src/commands/admin/update.ts diff --git a/src/commands/admin/refresh.ts b/src/commands/admin/refresh.ts new file mode 100644 index 0000000..b7c9ddb --- /dev/null +++ b/src/commands/admin/refresh.ts @@ -0,0 +1,33 @@ +import { createCommand } from "@lib/utils"; +import { KyokoClient } from "@/lib/BotClient"; +import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags } from "discord.js"; +import { createErrorEmbed, createSuccessEmbed, createWarningEmbed } from "@lib/embeds"; + +export const refresh = createCommand({ + data: new SlashCommandBuilder() + .setName("refresh") + .setDescription("Reloads all commands and config without restarting") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + try { + const start = Date.now(); + await KyokoClient.loadCommands(true); + const duration = Date.now() - start; + + // Deploy commands + await KyokoClient.deployCommands(); + + const embed = createSuccessEmbed( + `Successfully reloaded ${KyokoClient.commands.size} commands in ${duration}ms.`, + "System Refreshed" + ); + + await interaction.editReply({ embeds: [embed] }); + } catch (error) { + console.error(error); + await interaction.editReply({ embeds: [createErrorEmbed("An error occurred while refreshing commands. Check console for details.", "Refresh Failed")] }); + } + } +}); \ No newline at end of file diff --git a/src/commands/admin/reload.ts b/src/commands/admin/reload.ts index 0e9c2c4..4b79319 100644 --- a/src/commands/admin/reload.ts +++ b/src/commands/admin/reload.ts @@ -30,14 +30,16 @@ export const reload = createCommand({ embeds: [createWarningEmbed("Pulling latest changes and restarting...", "Redeploy Initiated")] }); - const { stdout, stderr } = await execAsync("git pull"); + // Get current branch + const { stdout: branchName } = await execAsync("git rev-parse --abbrev-ref HEAD"); + const branch = branchName.trim(); - if (stderr && !stdout) { - throw new Error(stderr); - } + // Fetch and reset + await execAsync("git fetch --all"); + const { stdout } = await execAsync(`git reset --hard origin/${branch}`); await interaction.editReply({ - embeds: [createSuccessEmbed(`Git Pull Output:\n\`\`\`\n${stdout}\n\`\`\`\nRestarting process...`, "Update Successful")] + embeds: [createSuccessEmbed(`Git Reset Output:\n\`\`\`\n${stdout}\n\`\`\`\nRestarting process...`, "Update Successful")] }); // Write context for post-restart notification diff --git a/src/commands/admin/update.ts b/src/commands/admin/update.ts new file mode 100644 index 0000000..678d5c7 --- /dev/null +++ b/src/commands/admin/update.ts @@ -0,0 +1,116 @@ +import { createCommand } from "@lib/utils"; +import { SlashCommandBuilder, EmbedBuilder, PermissionFlagsBits, MessageFlags, ButtonBuilder, ButtonStyle, ActionRowBuilder, ComponentType } from "discord.js"; +import { createErrorEmbed, createSuccessEmbed, createWarningEmbed, createInfoEmbed } from "@lib/embeds"; + +export const update = createCommand({ + data: new SlashCommandBuilder() + .setName("update") + .setDescription("Check for updates and restart the bot") + .setDefaultMemberPermissions(PermissionFlagsBits.Administrator), + execute: async (interaction) => { + await interaction.deferReply({ flags: MessageFlags.Ephemeral }); + + const { exec } = await import("child_process"); + const { promisify } = await import("util"); + const { writeFile, appendFile } = await import("fs/promises"); + const execAsync = promisify(exec); + + try { + // Get current branch + const { stdout: branchName } = await execAsync("git rev-parse --abbrev-ref HEAD"); + const branch = branchName.trim(); + + await interaction.editReply({ + embeds: [createInfoEmbed("Fetching latest changes...", "Checking for Updates")] + }); + + // Fetch remote + await execAsync("git fetch --all"); + + // Check for potential changes + const { stdout: logOutput } = await execAsync(`git log HEAD..origin/${branch} --oneline`); + + if (!logOutput.trim()) { + await interaction.editReply({ + embeds: [createSuccessEmbed("The bot is already up to date.", "No Updates Found")] + }); + return; + } + + // Prepare confirmation UI + const confirmButton = new ButtonBuilder() + .setCustomId("confirm_update") + .setLabel("Update & Restart") + .setStyle(ButtonStyle.Success); + + const cancelButton = new ButtonBuilder() + .setCustomId("cancel_update") + .setLabel("Cancel") + .setStyle(ButtonStyle.Secondary); + + const row = new ActionRowBuilder() + .addComponents(confirmButton, cancelButton); + + const updateEmbed = createInfoEmbed( + `The following changes are available:\n\`\`\`\n${logOutput.substring(0, 1000)}${logOutput.length > 1000 ? "\n...and more" : ""}\n\`\`\`\n**Do you want to update and restart?**`, + "Updates Available" + ); + + const response = await interaction.editReply({ + embeds: [updateEmbed], + components: [row] + }); + + try { + const confirmation = await response.awaitMessageComponent({ + filter: (i) => i.user.id === interaction.user.id, + componentType: ComponentType.Button, + time: 30000 // 30 seconds timeout + }); + + if (confirmation.customId === "confirm_update") { + await confirmation.update({ + embeds: [createWarningEmbed("Applying updates and restarting...", "Update In Progress")], + components: [] + }); + + const { stdout } = await execAsync(`git reset --hard origin/${branch}`); + + await interaction.followUp({ + flags: MessageFlags.Ephemeral, + embeds: [createSuccessEmbed(`Git Reset Output:\n\`\`\`\n${stdout}\n\`\`\`\nRestarting process...`, "Update Successful")] + }); + + // Write context for post-restart notification + await writeFile(".restart_context.json", JSON.stringify({ + channelId: interaction.channelId, + userId: interaction.user.id, + timestamp: Date.now() + })); + + // Trigger restart + await appendFile("src/index.ts", " "); + + } else { + await confirmation.update({ + embeds: [createInfoEmbed("Update cancelled.", "Cancelled")], + components: [] + }); + } + + } catch (e) { + // Timeout + await interaction.editReply({ + embeds: [createWarningEmbed("Update confirmation timed out.", "Timed Out")], + components: [] + }); + } + + } catch (error) { + console.error(error); + await interaction.editReply({ + embeds: [createErrorEmbed(`Failed to check for updates:\n\`\`\`\n${error instanceof Error ? error.message : String(error)}\n\`\`\``, "Update Check Failed")] + }); + } + } +});