import './instrument'; // MUST be the first import to ensure Sentry initializes before app ready import { app, BrowserWindow, dialog, shell } from 'electron'; import type { MessageBoxOptions } from 'electron'; import path from 'path'; import fs from 'fs'; import squirrelStartup from 'electron-squirrel-startup'; import { EnvHttpProxyAgent, setGlobalDispatcher } from 'undici'; import { ipcMain } from 'electron/main'; import { ipcContext } from '@/ipc/context'; import { IPC_CHANNELS } from './constants'; import { updateElectronApp, UpdateSourceType } from 'update-electron-app'; import { logger } from './utils/logger'; import { getExpectedInstallRoot, getInstallNoticeText, isRunningFromExpectedInstallDir as isRunningFromExpectedInstallDirUtil, resolveInstallNoticeLanguage, } from './utils/installNotice'; import { CloudAccountRepo } from './ipc/database/cloudHandler'; import { initDatabase } from './ipc/database/handler'; import { CloudMonitorService } from './services/CloudMonitorService'; // Static Imports to fix Bundle Resolution Errors import { AuthServer } from './ipc/cloud/authServer'; import { bootstrapNestServer, stopNestServer } from './server/main'; import { initTray, setTrayLanguage, destroyTray } from './ipc/tray/handler'; import { rpcHandler } from './ipc/handler'; import { ConfigManager } from './ipc/config/manager'; import { AppConfig } from './types/config'; import { isAutoStartLaunch, syncAutoStart } from './utils/autoStart'; import { safeStringifyPacket } from './utils/sensitiveDataMasking'; const packetLogPath = path.join(app.getPath('userData'), 'orpc_packets.log'); function logPacket(data: any) { try { fs.appendFileSync( packetLogPath, `[${new Date().toISOString()}] ${safeStringifyPacket(data)}\n`, ); } catch (e) { if (e instanceof Error) { logger.error('Failed to append ORPC packet log', e); } } } ipcMain.on(IPC_CHANNELS.CHANGE_LANGUAGE, (event, lang) => { logger.info(`IPC: Received CHANGE_LANGUAGE: ${lang}`); setTrayLanguage(lang); }); app.disableHardwareAcceleration(); if (process.platform === 'linux') { app.commandLine.appendSwitch('disable-gpu'); app.commandLine.appendSwitch('disable-gpu-compositing'); logger.info('Applied Linux GPU-safe Chromium switches for Antigravity Manager startup'); } if (squirrelStartup) { app.quit(); process.exit(0); } const inDevelopment = process.env.NODE_ENV === 'development'; const debugHttpProxy = process.env.http_proxy?.trim() || process.env.HTTP_PROXY?.trim(); const debugHttpsProxy = process.env.https_proxy?.trim() || process.env.HTTPS_PROXY?.trim(); const debugNoProxy = process.env.no_proxy?.trim() || process.env.NO_PROXY?.trim(); const debugProxyServer = process.env.ELECTRON_PROXY_SERVER?.trim() || debugHttpsProxy || debugHttpProxy; const debugProxyBypassList = process.env.ELECTRON_PROXY_BYPASS_LIST?.trim() || ';localhost;127.0.0.1;::1'; function configureDebugProxy() { if (debugHttpProxy || debugHttpsProxy) { setGlobalDispatcher( new EnvHttpProxyAgent({ httpProxy: debugHttpProxy, httpsProxy: debugHttpsProxy, noProxy: debugNoProxy, }), ); logger.info( `[Debug Proxy] Node fetch proxy enabled (http: ${debugHttpProxy ?? 'none'}, https: ${debugHttpsProxy ?? 'none'}, no_proxy: ${debugNoProxy ?? 'none'})`, ); } if (!debugProxyServer) { return; } // Route Chromium traffic through the local debug proxy while keeping local dev servers direct. app.commandLine.appendSwitch('proxy-server', debugProxyServer); app.commandLine.appendSwitch('proxy-bypass-list', debugProxyBypassList); logger.info( `[Debug Proxy] Electron proxy enabled: ${debugProxyServer} (bypass: ${debugProxyBypassList})`, ); } configureDebugProxy(); // Keep a global reference of the window object, if you don't, the window will // be closed automatically when the JavaScript object is garbage collected. let globalMainWindow: BrowserWindow | null = null; // let tray: Tray | null = null; // Moved to tray/handler.ts let isQuitting = false; let startupConfig: AppConfig | null = null; let shouldStartHidden = false; let hasShownInstallNotice = false; function isRunningFromExpectedInstallDir() { return isRunningFromExpectedInstallDirUtil({ platform: process.platform, isPackaged: app.isPackaged, localAppData: process.env.LOCALAPPDATA, appName: app.getName(), execPath: process.execPath, }); } function showWindowsInstallNoticeIfNeeded() { if (hasShownInstallNotice) { return; } if (isRunningFromExpectedInstallDir()) { return; } const expectedRoot = getExpectedInstallRoot({ platform: process.platform, localAppData: process.env.LOCALAPPDATA, appName: app.getName(), }); if (!expectedRoot) { return; } hasShownInstallNotice = true; const language = resolveInstallNoticeLanguage({ configLanguage: startupConfig?.language, locale: app.getLocale(), }); const text = getInstallNoticeText(language); const options: MessageBoxOptions = { type: 'info', title: text.title, message: text.message, detail: `${text.detailPrefix}${expectedRoot}`, buttons: [...text.buttons], defaultId: 1, }; const showPromise = globalMainWindow ? dialog.showMessageBox(globalMainWindow, options) : dialog.showMessageBox(options); showPromise.then(({ response }) => { if (response === 0) { shell.openPath(expectedRoot); } }); } const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; if (isDev) { app.setName('Antigravity Manager Dev'); } const gotSingleInstanceLock = app.requestSingleInstanceLock(); if (!gotSingleInstanceLock) { app.quit(); process.exit(0); } else { app.on('second-instance', () => { logger.info('Second instance detected, focusing existing window'); if (app.isReady()) { createWindow({ startHidden: false }); return; } app.whenReady().then(() => { createWindow({ startHidden: false }); }); }); } process.on('exit', (code) => { logger.info(`Process exit event triggered with code: ${code}`); }); process.on('before-exit', (code) => { logger.info(`Process before-exit event triggered with code: ${code}`); logger.info(`Process before-exit event triggered with code: ${code}`); }); // let tray: Tray | null = null; // Moved to tray/handler.ts function createWindow({ startHidden }: { startHidden: boolean }) { if (globalMainWindow && !globalMainWindow.isDestroyed()) { if (startHidden) { globalMainWindow.hide(); return; } if (globalMainWindow.isMinimized()) { globalMainWindow.restore(); } if (!globalMainWindow.isVisible()) { globalMainWindow.show(); } globalMainWindow.focus(); return; } logger.info('createWindow: start'); const preload = path.join(__dirname, 'preload.js'); logger.info(`createWindow: preload path: ${preload}`); logger.info('createWindow: attempting to create BrowserWindow'); const mainWindow = new BrowserWindow({ width: 1200, height: 800, show: !startHidden, autoHideMenuBar: true, webPreferences: { devTools: inDevelopment, contextIsolation: true, nodeIntegration: true, nodeIntegrationInSubFrames: false, preload: preload, }, // Use process.cwd() in dev to find the icon reliably icon: inDevelopment ? path.join(process.cwd(), 'src/assets/icon.png') : path.join(__dirname, '../assets/icon.png'), }); globalMainWindow = mainWindow; logger.info('createWindow: BrowserWindow instance created'); if (startHidden) { mainWindow.hide(); logger.info('createWindow: startHidden enabled, window hidden'); } logger.info('createWindow: setting main window in ipcContext'); ipcContext.setMainWindow(mainWindow); logger.info('createWindow: setMainWindow done'); if (inDevelopment && MAIN_WINDOW_VITE_DEV_SERVER_URL) { const devUrl = MAIN_WINDOW_VITE_DEV_SERVER_URL; logger.info(`createWindow: waiting for Vite dev server at ${devUrl}`); // Wait for Vite to be ready before loading const waitForVite = async (url: string, maxRetries = 30, delay = 500) => { for (let i = 0; i < maxRetries; i++) { try { const response = await fetch(url); if (response.ok) { logger.info(`createWindow: Vite server ready after ${i * delay}ms`); return true; } } catch (e) { // Server not ready yet } await new Promise((resolve) => setTimeout(resolve, delay)); } logger.error('createWindow: Vite server did not start in time'); return false; }; waitForVite(devUrl).then((ready) => { if (mainWindow.isDestroyed()) { logger.warn('createWindow: BrowserWindow destroyed before Vite URL load'); return; } if (ready) { logger.info(`createWindow: loading URL ${devUrl}`); mainWindow.loadURL(devUrl); } else { logger.error('createWindow: Failed to connect to Vite server, loading anyway'); mainWindow.loadURL(devUrl); } }); } else { logger.info('createWindow: loading file index.html'); mainWindow.loadFile(path.join(__dirname, `../renderer/${MAIN_WINDOW_VITE_NAME}/index.html`)); } logger.info('Window created'); showWindowsInstallNoticeIfNeeded(); mainWindow.on('close', (event) => { if (!isQuitting) { event.preventDefault(); mainWindow.hide(); logger.info('Window close intercepted -> Minimized to tray'); return false; } logger.info('Window close event triggered (Quitting)'); }); mainWindow.on('closed', () => { logger.info('Window closed event triggered'); globalMainWindow = null; }); mainWindow.webContents.on('render-process-gone', (event, details) => { logger.error('Renderer process gone:', details); }); mainWindow.webContents.on('did-fail-load', (event, errorCode, errorDescription, validatedURL) => { logger.error(`Page failed to load: ${errorCode} - ${errorDescription} - URL: ${validatedURL}`); }); mainWindow.webContents.on('did-finish-load', () => { logger.info('Page finished loading successfully'); }); mainWindow.webContents.on('console-message', (details) => { const { level, message, lineNumber, sourceId } = details; logger.info(`[Renderer Console][${level}] ${message} (${sourceId}:${lineNumber})`); }); mainWindow.on('focus', () => { CloudMonitorService.handleAppFocus(); }); } app.on('child-process-gone', (event, details) => { logger.error('Child process gone:', details); }); app.on('before-quit', () => { isQuitting = true; logger.info('App before-quit event triggered - isQuitting set to true'); }); app.on('will-quit', (event) => { logger.info('App will quit event triggered'); try { destroyTray(); } catch (err) { logger.error('Failed to destroy tray during will-quit', err); } }); app.on('quit', (event, exitCode) => { logger.info(`App quit event triggered with code: ${exitCode}`); }); /* async function installExtensions() { try { const result = await installExtension(REACT_DEVELOPER_TOOLS); logger.info(`Extensions installed successfully: ${result.name}`); } catch { logger.error('Failed to install extensions'); } } */ function checkForUpdates() { updateElectronApp({ updateSource: { type: UpdateSourceType.ElectronPublicUpdateService, repo: 'Draculabo/AntigravityManager', }, }); } async function setupORPC() { ipcMain.on(IPC_CHANNELS.START_ORPC_SERVER, (event) => { logger.info('IPC: Received START_ORPC_SERVER'); const [port] = event.ports; // Debug: Inspect raw messages port.on('message', (msgEvent) => { try { const data = msgEvent.data; logPacket(data); } catch (e) { logger.debug('[RAW ORPC MSG] (unparseable)', msgEvent.data); } }); port.start(); logger.info('IPC: Server port started'); try { rpcHandler.upgrade(port); logger.info('IPC: rpcHandler upgraded successfully'); } catch (error) { logger.error('IPC: Failed to upgrade rpcHandler', error); } }); } process.on('uncaughtException', (error) => { logger.error('Uncaught Exception:', error); }); process.on('unhandledRejection', (reason) => { logger.error('Unhandled Rejection:', reason); }); app .whenReady() .then(async () => { logger.info('Step: Initialize CloudAccountRepo'); try { await CloudAccountRepo.init(); } catch (e) { logger.error('Startup: Failed to initialize CloudAccountRepo', e); // We might want to exit here or show a dialog, but for now we proceed // though functionality will be broken. } logger.info('Step: Initialize Antigravity DB (WAL Mode)'); initDatabase(); }) .then(() => { logger.info('Step: Load Config'); const config = ConfigManager.loadConfig(); startupConfig = config; syncAutoStart(config); shouldStartHidden = isAutoStartLaunch() && config.auto_startup; if (shouldStartHidden) { logger.info('Startup: Auto-start detected, window will start hidden'); } }) .then(() => { logger.info('Step: setupORPC'); return setupORPC(); }) .then(async () => { logger.info('Step: createWindow'); await createWindow({ startHidden: shouldStartHidden }); }) .then(() => { logger.info('Step: installExtensions (SKIPPED)'); // return installExtensions(); }) .then(() => { logger.info('Step: checkForUpdates'); checkForUpdates(); }) .then(async () => { // Initialize Cloud Monitor if enabled try { // Start OAuth Server AuthServer.start(); // Gateway Server (NestJS) - auto-start if enabled const config = startupConfig || ConfigManager.loadConfig(); if (config.proxy?.auto_start) { const port = config.proxy?.port || 8045; // Default to a valid ProxyConfig object if null, although loadConfig ensures defaults if (config.proxy) { await bootstrapNestServer(config.proxy); } logger.info(`NestJS Proxy: Auto-started on port ${port}`); } const enabled = CloudAccountRepo.getSetting('auto_switch_enabled', false); if (enabled) { logger.info('Startup: Auto-Switch enabled, starting monitor...'); CloudMonitorService.start(); } else { logger.info( 'Startup: Auto-Switch disabled, running one-time quota and AI credits sync...', ); await CloudMonitorService.poll(); } } catch (e) { logger.error('Startup: Failed to initialize services', e); } }) .then(async () => { logger.info('Step: Startup Complete'); if (globalMainWindow) { initTray(globalMainWindow); } }) .catch((error) => { logger.error('Failed to start application:', error); app.quit(); }); //osX only app.on('window-all-closed', () => { logger.info('Window all closed event triggered'); stopNestServer(); // Stop server if (process.platform !== 'darwin') { app.quit(); } // Keep app running for tray }); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow({ startHidden: false }); } }); //osX only ends