import { app, BrowserWindow, ipcMain, Menu, nativeTheme, shell, utilityProcess, } from 'electron'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const brandName = 'TrueGrowth'; const rendererUrl = process.env.VITE_DEV_SERVER_URL; const localApiPort = Number(process.env.TRUEGROWTH_LOCAL_API_PORT || 48177); const userDataPath = process.env.TRUEGROWTH_ELECTRON_USER_DATA || path.join(app.getPath('appData'), brandName); const isDev = Boolean(rendererUrl); const appRoot = app.isPackaged ? app.getAppPath() : path.resolve(__dirname, '../..'); const brandIconPath = resolveAppPath( 'apps/electron/assets/truegrowth-dock-icon.png' ); let mainWindow = null; let localApi = null; function readAppVersion() { try { const packageInfo = JSON.parse( fs.readFileSync(resolveAppPath('package.json'), 'utf8') ); return packageInfo.version || app.getVersion(); } catch { return app.getVersion(); } } const appVersion = readAppVersion(); function resolveAppPath(...segments) { return path.join(appRoot, ...segments); } function createDesktopLocalRendererEntry(indexHtmlPath) { const html = fs.readFileSync(indexHtmlPath, 'utf8'); const appAssetPattern = /https:\/\/cdn\.jsdelivr\.net\/npm\/aitu-app@[^/"'>\s]+\/([^"'<>\s]+)/g; return html.replace(appAssetPattern, (_url, assetPath) => `./${assetPath}`); } process.title = brandName; app.setName(brandName); app.setAppUserModelId('ai.truegrowth.desktop'); app.setPath('userData', userDataPath); process.env.TRUEGROWTH_ELECTRON_USER_DATA = userDataPath; process.env.TRUEGROWTH_RUNTIME_STATE_DIR = process.env.TRUEGROWTH_RUNTIME_STATE_DIR || path.join(userDataPath, 'runtime'); if (!isDev) { process.env.TRUEGROWTH_DESKTOP_PACKAGED = process.env.TRUEGROWTH_DESKTOP_PACKAGED || '1'; } process.env.TRUEGROWTH_DESKTOP_APP_ROOT = process.env.TRUEGROWTH_DESKTOP_APP_ROOT || appRoot; process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR = process.env.TRUEGROWTH_DESKTOP_RESOURCE_DIR || (app.isPackaged ? process.resourcesPath : appRoot); const hasSingleInstanceLock = app.requestSingleInstanceLock({ rendererUrl, userDataPath, }); if (!hasSingleInstanceLock) { app.quit(); process.exit(0); } app.on('second-instance', () => { if (!mainWindow) { return; } if (mainWindow.isMinimized()) { mainWindow.restore(); } mainWindow.focus(); mainWindow.show(); }); function getWindowState(window) { return { isFullScreen: window?.isFullScreen?.() ?? false, isMaximized: window?.isMaximized?.() ?? false, platform: process.platform, }; } function getSystemThemeState() { const theme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light'; return { theme, shouldUseDarkColors: theme === 'dark', source: 'system', }; } function sendWindowState(window) { if (!window || window.isDestroyed()) { return; } window.webContents.send('truegrowth-window:state', getWindowState(window)); } function sendSystemThemeState(window) { if (!window || window.isDestroyed()) { return; } const systemTheme = getSystemThemeState(); window.setBackgroundColor( systemTheme.theme === 'dark' ? '#060606' : '#f7f7f5' ); window.webContents.send('truegrowth-theme:system', systemTheme); } function getWindowFromIpcEvent(event) { const window = BrowserWindow.fromWebContents(event.sender); return window && !window.isDestroyed() ? window : null; } function installWindowControlsIpc() { ipcMain.handle('truegrowth-window:minimize', (event) => { const window = getWindowFromIpcEvent(event); window?.minimize(); return window ? getWindowState(window) : getWindowState(mainWindow); }); ipcMain.handle('truegrowth-window:toggle-maximize', (event) => { const window = getWindowFromIpcEvent(event); if (!window) { return getWindowState(mainWindow); } if (window.isFullScreen()) { window.setFullScreen(false); } else if (window.isMaximized()) { window.unmaximize(); } else { window.maximize(); } return getWindowState(window); }); ipcMain.handle('truegrowth-window:close', (event) => { const window = getWindowFromIpcEvent(event); window?.close(); return { closed: Boolean(window) }; }); ipcMain.handle('truegrowth-window:is-maximized', (event) => { const window = getWindowFromIpcEvent(event); return getWindowState(window || mainWindow); }); } function installThemeIpc() { ipcMain.handle('truegrowth-theme:get-system', () => getSystemThemeState()); nativeTheme.on('updated', () => { BrowserWindow.getAllWindows().forEach(sendSystemThemeState); }); } function installApplicationMenu() { const editMenu = [ { role: 'undo' }, { role: 'redo' }, { type: 'separator' }, { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' }, ]; const viewMenu = [ { role: 'reload' }, { role: 'forceReload' }, { role: 'toggleDevTools' }, { type: 'separator' }, { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' }, { type: 'separator' }, { role: 'togglefullscreen' }, ]; const windowMenu = [ { role: 'minimize' }, { role: 'zoom' }, ...(process.platform === 'darwin' ? [{ type: 'separator' }, { role: 'front' }] : [{ role: 'close' }]), ]; const template = [ ...(process.platform === 'darwin' ? [ { label: brandName, submenu: [ { role: 'about', label: `About ${brandName}` }, { type: 'separator' }, { role: 'services' }, { type: 'separator' }, { role: 'hide', label: `Hide ${brandName}` }, { role: 'hideOthers' }, { role: 'unhide' }, { type: 'separator' }, { role: 'quit', label: `Quit ${brandName}` }, ], }, ] : []), { label: 'File', submenu: [ process.platform === 'darwin' ? { role: 'close' } : { role: 'quit' }, ], }, { label: 'Edit', submenu: editMenu }, { label: 'View', submenu: viewMenu }, { label: 'Window', submenu: windowMenu }, { label: 'Help', submenu: [] }, ]; Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } function createWindow() { const initialSystemTheme = getSystemThemeState().theme; mainWindow = new BrowserWindow({ width: 1440, height: 920, minWidth: 1180, minHeight: 760, title: brandName, icon: brandIconPath, frame: false, autoHideMenuBar: true, backgroundColor: initialSystemTheme === 'dark' ? '#060606' : '#f7f7f5', webPreferences: { preload: resolveAppPath('apps/electron/preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: false, }, }); if (process.platform !== 'darwin') { mainWindow.setMenuBarVisibility(false); } mainWindow.on('closed', () => { mainWindow = null; }); [ 'enter-full-screen', 'leave-full-screen', 'maximize', 'resize', 'restore', 'unmaximize', ].forEach((eventName) => { mainWindow.on(eventName, () => sendWindowState(mainWindow)); }); mainWindow.webContents.once('did-finish-load', () => { sendWindowState(mainWindow); sendSystemThemeState(mainWindow); }); mainWindow.webContents.setWindowOpenHandler(({ url }) => { void shell.openExternal(url); return { action: 'deny' }; }); if (isDev && rendererUrl) { void mainWindow.loadURL(rendererUrl); return; } const indexHtmlPath = resolveAppPath('dist/apps/web/index.html'); const entryHtml = createDesktopLocalRendererEntry(indexHtmlPath); void mainWindow.loadURL( `data:text/html;charset=utf-8,${encodeURIComponent(entryHtml)}`, { baseURLForDataURL: `${pathToFileURL( path.dirname(indexHtmlPath) ).toString()}/`, } ); } async function startLocalApi() { const { createLocalApiServer } = await import( pathToFileURL(resolveAppPath('scripts/truegrowth-local-api.mjs')).toString() ); localApi = createLocalApiServer({ utilityProcess }); localApi.on('error', (error) => { if (error?.code === 'EADDRINUSE') { console.log( `TrueGrowth local API already available at http://127.0.0.1:${localApiPort}` ); return; } throw error; }); localApi.listen(localApiPort, '127.0.0.1', () => { console.log( `TrueGrowth local API listening at http://127.0.0.1:${localApiPort}` ); }); } app.whenReady().then(async () => { installApplicationMenu(); installWindowControlsIpc(); installThemeIpc(); if (process.platform === 'darwin' && app.dock) { app.dock.setIcon(brandIconPath); } app.setAboutPanelOptions({ applicationName: brandName, applicationVersion: appVersion, iconPath: brandIconPath, copyright: `Copyright © ${new Date().getFullYear()} TrueGrowth`, }); await startLocalApi(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit(); } }); app.on('before-quit', () => { localApi?.close(); });