import { exec } from "child_process"; import { promisify } from "util"; import { writeFile, readFile, unlink } from "fs/promises"; import { Client, TextChannel } from "discord.js"; import { getPostRestartEmbed, getInstallingDependenciesEmbed } from "./update.view"; import type { PostRestartResult } from "./update.view"; const execAsync = promisify(exec); // Constants const STALE_CONTEXT_MS = 10 * 60 * 1000; // 10 minutes const RESTART_TRIGGER_FILE = ".restart_trigger"; export interface RestartContext { channelId: string; userId: string; timestamp: number; runMigrations: boolean; installDependencies: boolean; } export interface DependencyCheckResult { needsInstall: boolean; error?: Error; } 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 { const { stdout } = await execAsync(`git diff HEAD..origin/${branch} --name-only`); return { needsInstall: stdout.includes("package.json") }; } catch (e) { console.error("Failed to check dependencies:", e); return { needsInstall: false, error: e instanceof Error ? e : new Error(String(e)) }; } } 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 { if (process.env.RESTART_COMMAND) { // Run without awaiting - it may kill the process immediately exec(process.env.RESTART_COMMAND).unref(); } else { // Fallback to writing a trigger file (avoids polluting source code) try { await writeFile(RESTART_TRIGGER_FILE, Date.now().toString()); } catch (err) { console.error("Failed to write restart trigger:", err); } } } static async handlePostRestart(client: Client): Promise { try { const context = await this.loadRestartContext(); if (!context) return; if (this.isContextStale(context)) { await this.cleanupContext(); return; } const channel = await this.fetchNotificationChannel(client, context.channelId); if (!channel) { await this.cleanupContext(); return; } const result = await this.executePostRestartTasks(context, channel); await this.notifyPostRestartResult(channel, result); await this.cleanupContext(); } catch (e) { console.error("Failed to handle post-restart context:", e); } } // --- Private Helper Methods --- private static async loadRestartContext(): Promise { try { const contextData = await readFile(this.CONTEXT_FILE, "utf-8"); return JSON.parse(contextData) as RestartContext; } catch { return null; } } private static isContextStale(context: RestartContext): boolean { return Date.now() - context.timestamp > STALE_CONTEXT_MS; } private static async fetchNotificationChannel(client: Client, channelId: string): Promise { try { const channel = await client.channels.fetch(channelId); if (channel && channel.isSendable() && channel instanceof TextChannel) { return channel; } return null; } catch { return null; } } private static async executePostRestartTasks( context: RestartContext, channel: TextChannel ): Promise { const result: PostRestartResult = { installSuccess: true, installOutput: "", migrationSuccess: true, migrationOutput: "", ranInstall: context.installDependencies, ranMigrations: context.runMigrations }; // 1. Install Dependencies if needed if (context.installDependencies) { try { await channel.send({ embeds: [getInstallingDependenciesEmbed()] }); const { stdout } = await execAsync("bun install"); result.installOutput = stdout; } catch (err: unknown) { result.installSuccess = false; result.installOutput = err instanceof Error ? err.message : String(err); console.error("Dependency Install Failed:", err); } } // 2. Run Migrations if (context.runMigrations) { try { const { stdout } = await execAsync("bun x drizzle-kit migrate"); result.migrationOutput = stdout; } catch (err: unknown) { result.migrationSuccess = false; result.migrationOutput = err instanceof Error ? err.message : String(err); console.error("Migration Failed:", err); } } return result; } private static async notifyPostRestartResult(channel: TextChannel, result: PostRestartResult): Promise { await channel.send({ embeds: [getPostRestartEmbed(result)] }); } private static async cleanupContext(): Promise { try { await unlink(this.CONTEXT_FILE); } catch { // File may not exist, ignore } } }