import { exec } from "child_process"; import { promisify } from "util"; import { writeFile, readFile, unlink, appendFile } from "fs/promises"; import { Client, TextChannel } from "discord.js"; import { createSuccessEmbed } from "@lib/embeds"; const execAsync = promisify(exec); export interface RestartContext { channelId: string; userId: string; timestamp: number; runMigrations: boolean; installDependencies: boolean; } export class UpdateService { private static readonly CONTEXT_FILE = ".restart_context.json"; static async checkForUpdates(): Promise<{ hasUpdates: boolean; log: string; branch: string }> { const { stdout: branchName } = await execAsync("git rev-parse --abbrev-ref HEAD"); const branch = branchName.trim(); await execAsync("git fetch --all"); const { stdout: logOutput } = await execAsync(`git log HEAD..origin/${branch} --oneline`); return { hasUpdates: !!logOutput.trim(), log: logOutput.trim(), branch }; } static async performUpdate(branch: string): Promise { await execAsync(`git reset --hard origin/${branch}`); } static async checkDependencies(branch: string): Promise { try { // Check if package.json has changed between HEAD and upstream const { stdout } = await execAsync(`git diff HEAD..origin/${branch} --name-only`); return stdout.includes("package.json"); } catch (e) { console.error("Failed to check dependencies:", e); return false; } } static async installDependencies(): Promise { const { stdout } = await execAsync("bun install"); return stdout; } static async prepareRestartContext(context: RestartContext): Promise { await writeFile(this.CONTEXT_FILE, JSON.stringify(context)); } static async triggerRestart(): Promise { // Use custom restart command if available, otherwise fallback to touch if (process.env.RESTART_COMMAND) { // We run this without awaiting because it might kill the process immediately exec(process.env.RESTART_COMMAND).unref(); } else { // Fallback to touch try { await appendFile("src/index.ts", " "); } catch (err) { console.error("Failed to touch trigger:", err); } } } static async handlePostRestart(client: Client): Promise { try { const contextData = await readFile(this.CONTEXT_FILE, "utf-8"); const context: RestartContext = JSON.parse(contextData); if (Date.now() - context.timestamp > 10 * 60 * 1000) { // Ignore stale contexts (> 10 mins) return; } const channel = await client.channels.fetch(context.channelId); if (channel && channel.isSendable() && channel instanceof TextChannel) { let migrationOutput = ""; let migrationSuccess = true; let installOutput = ""; let installSuccess = true; // 1. Install Dependencies if needed (Post-Restart) if (context.installDependencies) { try { await channel.send({ embeds: [createSuccessEmbed("Installing dependencies...", "Post-Update Action")] }); const { stdout } = await execAsync("bun install"); installOutput = stdout; } catch (err: any) { installSuccess = false; installOutput = err.message; console.error("Dependency Install Failed:", err); } } // 2. Run Migrations if (context.runMigrations) { try { // Use drizzle-kit migrate // Apply migrations using drizzle-kit // We use `bun x` to run the local binary directly, avoiding docker-in-docker issues const { stdout: migOut } = await execAsync("bun x drizzle-kit migrate"); migrationOutput = migOut; } catch (err: any) { migrationSuccess = false; migrationOutput = err.message; } } await channel.send({ embeds: [ createSuccessEmbed( `System updated successfully. ${context.installDependencies ? `**Dependencies:** ${installSuccess ? "✅ Installed" : "❌ Failed"}\n` : ""} ${context.runMigrations ? `**Migrations:** ${migrationSuccess ? "✅ Applied" : "❌ Failed"}\n` : ""} ${installOutput ? `\n**Install Output:**\n\`\`\`\n${installOutput.substring(0, 500)}\n\`\`\`` : ""} ${migrationOutput ? `\n**Migration Output:**\n\`\`\`\n${migrationOutput.substring(0, 500)}\n\`\`\`` : ""}`, (migrationSuccess && installSuccess) ? "Update Complete" : "Update Completed with Errors" ) ] }); } await unlink(this.CONTEXT_FILE); } catch (e) { // No context or read error, ignore } } }