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; } 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 scheduleRestart(context: RestartContext): Promise { await writeFile(this.CONTEXT_FILE, JSON.stringify(context)); // 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; 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.runMigrations ? `\n\n**Migration Output:**\n\`\`\`\n${migrationOutput.substring(0, 1000)}\n\`\`\`` : ""}`, migrationSuccess ? "Update Complete" : "Update Complete (Migration Failed)" ) ] }); } await unlink(this.CONTEXT_FILE); } catch (e) { // No context or read error, ignore } } }