const { migrate } = require("./services/db"); const { ensureDefaults, getSetting, setSetting } = require("./services/settings"); const { createWebServer } = require("./web/server"); const { startBot, stopBot } = require("./services/discord"); const { startTwitchBot, stopTwitchBot } = require("./services/twitch"); const { startYouTubeBot, stopYouTubeBot } = require("./services/youtube"); const pluginService = require("./services/plugins"); const { loadEnabled } = pluginService; const stopPlugins = typeof pluginService.stopPlugins === "function" ? pluginService.stopPlugins : async () => {}; const { checkForUpdates, completePendingBranchDeployment, pullUpdates, registerRestartHandler, requestRestart } = require("./services/updater"); const { createCommandRouter } = require("./services/command-router"); const { registerTopCommand } = require("./services/top"); const logger = require("./services/logger"); const { isPlatformEnabled } = require("./services/platforms"); const { registerCorePlaceholders } = require("./services/placeholders"); const { overlayConnectorManager } = require("./services/overlay-connectors"); const { cleanupSnapshots } = require("./services/update-manager"); const { twitchEventSubManager } = require("./services/twitch-eventsub"); const { isSafeModeRequested, markStartupVerification } = require("./services/recovery-mode"); async function main() { migrate(); ensureDefaults(); const configuredRemote = getSetting("git_remote", "origin"); const configuredBranch = getSetting("git_branch", "main"); const completedDeployment = await completePendingBranchDeployment(configuredRemote, configuredBranch); if (completedDeployment) { console.log(`Completed deployment of ${completedDeployment.deployment.branch}; restarting with synchronized core and plugin code.`); requestRestart({ delayMs: 0 }); return; } registerCorePlaceholders(); logger.hookConsole(); const runtimeLog = logger.createLogger("core:runtime", { category: "lifecycle" }); const logCleanup = logger.cleanupLogs({ maxAgeDays: getSetting("log_retention_days", logger.DEFAULT_MAX_AGE_DAYS), maxEntries: getSetting("log_retention_max_entries", logger.DEFAULT_MAX_ENTRIES) }); runtimeLog.info("Lumi startup initiated", { version: require("../package.json").version, node: process.version, platform: process.platform, log_entries_removed: logCleanup.removed }, { event: "startup" }); try { cleanupSnapshots(); } catch (error) { console.warn(`Snapshot cleanup could not complete: ${error.message}`); } const safeModeRequested = isSafeModeRequested(); const startupMarker = markStartupVerification(); if (startupMarker?.status === "stale") { console.warn("Recovery marker detected from incomplete update; start with LUMI_SAFE_MODE=1 for recovery tools."); } const settingsApi = { getSetting, setSetting }; const commandRouter = createCommandRouter({ settings: settingsApi }); registerTopCommand({ commandRouter, settings: settingsApi }); let discordClient = null; let twitchClient = null; let youtubeClient = null; if (!safeModeRequested && isPlatformEnabled("discord")) { try { discordClient = await startBot({ commandRouter }); } catch (error) { console.error("Discord bot failed to start", error); } } if (!safeModeRequested && isPlatformEnabled("twitch")) { try { twitchClient = await startTwitchBot({ commandRouter }); } catch (error) { console.error("Twitch bot failed to start", error); } try { await twitchEventSubManager.start(); } catch (error) { console.error("Twitch event alerts failed to start", error); } } if (!safeModeRequested && isPlatformEnabled("youtube")) { try { youtubeClient = await startYouTubeBot({ commandRouter }); } catch (error) { console.error("YouTube bot failed to start", error); } } const app = createWebServer({ discordClient, commandRouter, loadPlugins: (appInstance, web, webhooks) => { if (safeModeRequested) return; loadEnabled({ app: appInstance, discordClient, twitchClient, youtubeClient, settings: settingsApi, web, webhooks, commandRouter }); } }); if (!safeModeRequested) { overlayConnectorManager.start().catch((error) => { console.error("OBS overlay connectors failed to start", error); }); } const port = Number(process.env.PORT || 3000); const webServer = app.listen(port, () => { runtimeLog.run({ event: "web_ready" }, () => { console.log(`WebUI listening on http://localhost:${port}`, { port }); }); }); app.locals.lumiUpgradeRegistry?.attach(webServer); const autoUpdateEnabled = getSetting("auto_update_enabled", false); const intervalMinutes = getSetting("auto_update_interval_minutes", 60); if (!safeModeRequested && autoUpdateEnabled) { const intervalMs = Math.max(5, Number(intervalMinutes)) * 60 * 1000; let autoUpdateRunning = false; setInterval(async () => { if (autoUpdateRunning) return; autoUpdateRunning = true; try { const remote = getSetting("git_remote", "origin"); const branch = getSetting("git_branch", "main"); if (checkForUpdates(remote, branch)) { await pullUpdates(remote, branch); requestRestart(); } } catch (error) { console.error("Auto-update failed", error); } finally { autoUpdateRunning = false; } }, intervalMs); } let shuttingDown = false; const shutdown = async (exitCode = 0) => { if (shuttingDown) { return; } shuttingDown = true; app.locals.lumiUpgradeRegistry?.close(); runtimeLog.info("Lumi shutdown started", { exit_code: exitCode }, { event: exitCode === 10 ? "restart" : "shutdown" }); const closeWebServer = new Promise((resolve) => webServer.close(resolve)); for (const stop of [ () => overlayConnectorManager.stop(), () => twitchEventSubManager.stop(), () => stopPlugins(), () => stopBot(), () => stopTwitchBot(), () => stopYouTubeBot() ]) { try { await stop(); } catch (error) { runtimeLog.warn("A service did not stop cleanly", { error: error.message }, { event: "shutdown_service_error" }); } } await Promise.race([ closeWebServer, new Promise((resolve) => setTimeout(resolve, 2500)) ]); webServer.closeAllConnections?.(); process.exit(Number(exitCode) === 10 ? 10 : 0); }; registerRestartHandler(() => shutdown(10)); process.on("SIGINT", () => shutdown(0)); process.on("SIGTERM", () => shutdown(0)); } main();