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...\nThe bot will run database migrations on next startup.", "Update In Progress")], components: [] }); // Write context BEFORE reset, because reset -> watcher restart await writeFile(".restart_context.json", JSON.stringify({ channelId: interaction.channelId, userId: interaction.user.id, timestamp: Date.now(), runMigrations: true })); const { stdout } = await execAsync(`git reset --hard origin/${branch}`); // In case we are not running with a watcher, or if no files were changed (unlikely given log check), // we might need to manually trigger restart. // But if files changed, watcher kicks in here or slightly after. // If we are here, we can try to force a touch or just exit. // Trigger restart just in case watcher didn't catch it or we are in a mode without watcher (though update implies source change) try { await appendFile("src/index.ts", " "); } catch (err) { console.error("Failed to touch triggers:", err); } // The process should die now or soon. // We do NOT run migrations here anymore. } 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")] }); } } });