import { app, BrowserWindow, dialog, Menu } from "electron"; import * as path from "node:path"; import { registerIpcHandlers } from "./ipc/ipc_host"; import dotenv from "dotenv"; // @ts-ignore import started from "electron-squirrel-startup"; // import { updateElectronApp, UpdateSourceType } from "update-electron-app"; // Removed - MoreMinimore API dependency import log from "electron-log"; import { getSettingsFilePath, readSettings, writeSettings, } from "./main/settings"; import { handleSupabaseOAuthReturn } from "./supabase_admin/supabase_return_handler"; import { handleMoreMinimoreProReturn } from "./main/pro"; import { IS_TEST_BUILD } from "./ipc/utils/test_utils"; import { BackupManager } from "./backup_manager"; import { getDatabasePath, initializeDatabase } from "./db"; import { UserSettings } from "./lib/schemas"; import { handleNeonOAuthReturn } from "./neon_admin/neon_return_handler"; import { AddMcpServerConfigSchema, AddMcpServerPayload, AddPromptDataSchema, AddPromptPayload, } from "./ipc/deep_link_data"; import { startPerformanceMonitoring, stopPerformanceMonitoring, } from "./utils/performance_monitor"; import fs from "fs"; log.errorHandler.startCatching(); log.eventLogger.startLogging(); log.scope.labelPadding = false; const logger = log.scope("main"); // Load environment variables from .env file dotenv.config(); // Register IPC handlers before app is ready registerIpcHandlers(); // Handle creating/removing shortcuts on Windows when installing/uninstalling. if (started) { app.quit(); } // Decide the git directory depending on environment function resolveLocalGitDirectory() { if (!app.isPackaged) { // Dev: app.getAppPath() is the project root return path.join(app.getAppPath(), "node_modules/dugite/git"); } // Packaged app: git is bundled via extraResource return path.join(process.resourcesPath, "git"); } const gitDir = resolveLocalGitDirectory(); if (fs.existsSync(gitDir)) { process.env.LOCAL_GIT_DIRECTORY = gitDir; } // https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#main-process-mainjs if (process.defaultApp) { if (process.argv.length >= 2) { app.setAsDefaultProtocolClient("moreminimore", process.execPath, [ path.resolve(process.argv[1]), ]); } } else { app.setAsDefaultProtocolClient("moreminimore"); } export async function onReady() { try { const backupManager = new BackupManager({ settingsFile: getSettingsFilePath(), dbFile: getDatabasePath(), }); await backupManager.initialize(); } catch (e) { logger.error("Error initializing backup manager", e); } initializeDatabase(); const settings = readSettings(); // Check if app was force-closed if (settings.isRunning) { logger.warn("App was force-closed on previous run"); // Store performance data to send after window is created if (settings.lastKnownPerformance) { logger.warn("Last known performance:", settings.lastKnownPerformance); pendingForceCloseData = settings.lastKnownPerformance; } } // Set isRunning to true at startup writeSettings({ isRunning: true }); // Start performance monitoring startPerformanceMonitoring(); await onFirstRunMaybe(settings); createWindow(); logger.info("Auto-update disabled - removed MoreMinimore API dependency"); // Auto-update functionality removed to eliminate MoreMinimore API dependency // Users can manually update by downloading new releases from GitHub } export async function onFirstRunMaybe(settings: UserSettings) { if (!settings.hasRunBefore) { await promptMoveToApplicationsFolder(); writeSettings({ hasRunBefore: true, }); } if (IS_TEST_BUILD) { writeSettings({ isTestMode: true, }); } } /** * Ask the user if the app should be moved to the * applications folder. */ async function promptMoveToApplicationsFolder(): Promise { // Why not in e2e tests? // There's no way to stub this dialog in time, so we just skip it // in e2e testing mode. if (IS_TEST_BUILD) return; if (process.platform !== "darwin") return; if (app.isInApplicationsFolder()) return; logger.log("Prompting user to move to applications folder"); const { response } = await dialog.showMessageBox({ type: "question", buttons: ["Move to Applications Folder", "Do Not Move"], defaultId: 0, message: "Move to Applications Folder? (required for auto-update)", }); if (response === 0) { logger.log("User chose to move to applications folder"); app.moveToApplicationsFolder(); } else { logger.log("User chose not to move to applications folder"); } } declare global { const MAIN_WINDOW_VITE_DEV_SERVER_URL: string; } let mainWindow: BrowserWindow | null = null; let pendingForceCloseData: any = null; const createWindow = () => { // Create the browser window. mainWindow = new BrowserWindow({ width: process.env.NODE_ENV === "development" ? 1280 : 960, minWidth: 800, height: 700, minHeight: 500, titleBarStyle: "hidden", titleBarOverlay: false, trafficLightPosition: { x: 10, y: 8, }, webPreferences: { nodeIntegration: false, contextIsolation: true, preload: path.join(__dirname, "preload.js"), // transparent: true, }, // backgroundColor: "#00000001", // frame: false, }); // and load the index.html of the app. if (MAIN_WINDOW_VITE_DEV_SERVER_URL) { mainWindow.loadURL(MAIN_WINDOW_VITE_DEV_SERVER_URL); } else { mainWindow.loadFile( path.join(__dirname, "../renderer/main_window/index.html"), ); } if (process.env.NODE_ENV === "development") { // Open the DevTools. mainWindow.webContents.openDevTools(); } // Send force-close event if it was detected if (pendingForceCloseData) { mainWindow.webContents.once("did-finish-load", () => { mainWindow?.webContents.send("force-close-detected", { performanceData: pendingForceCloseData, }); pendingForceCloseData = null; }); } // Enable native context menu on right-click mainWindow.webContents.on("context-menu", (event, params) => { // Prevent any default behavior and show our own menu event.preventDefault(); const template: Electron.MenuItemConstructorOptions[] = []; if (params.isEditable) { template.push( { role: "undo" }, { role: "redo" }, { type: "separator" }, { role: "cut" }, { role: "copy" }, { role: "paste" }, { role: "delete" }, ); if (params.misspelledWord) { const suggestions: Electron.MenuItemConstructorOptions[] = params.dictionarySuggestions.slice(0, 5).map((suggestion) => ({ label: suggestion, click: () => { try { mainWindow?.webContents.replaceMisspelling(suggestion); } catch (error) { logger.error("Failed to replace misspelling:", error); } }, })); template.push( { type: "separator" }, { type: "submenu", label: `Correct "${params.misspelledWord}"`, submenu: suggestions, }, ); } template.push({ type: "separator" }, { role: "selectAll" }); } else { if (params.selectionText && params.selectionText.length > 0) { template.push({ role: "copy" }); } template.push({ role: "selectAll" }); } if (process.env.NODE_ENV === "development") { template.push( { type: "separator" }, { label: "Inspect Element", click: () => mainWindow?.webContents.inspectElement(params.x, params.y), }, ); } const menu = Menu.buildFromTemplate(template); menu.popup({ window: mainWindow! }); }); }; const gotTheLock = app.requestSingleInstanceLock(); if (!gotTheLock) { app.quit(); } else { app.on("second-instance", (_event, commandLine, _workingDirectory) => { // Someone tried to run a second instance, we should focus our window. if (mainWindow) { if (mainWindow.isMinimized()) mainWindow.restore(); mainWindow.focus(); } // the commandLine is array of strings in which last element is deep link url handleDeepLinkReturn(commandLine.pop()!); }); app.whenReady().then(onReady); } // Handle the protocol. In this case, we choose to show an Error Box. app.on("open-url", (event, url) => { handleDeepLinkReturn(url); }); function handleDeepLinkReturn(url: string) { // example url: "dyad://supabase-oauth-return?token=a&refreshToken=b" let parsed: URL; try { parsed = new URL(url); } catch { log.info("Invalid deep link URL", url); return; } // Intentionally do NOT log the full URL which may contain sensitive tokens. log.log( "Handling deep link: protocol", parsed.protocol, "hostname", parsed.hostname, ); if (parsed.protocol !== "moreminimore:") { dialog.showErrorBox( "Invalid Protocol", `Expected moreminimore://, got ${parsed.protocol}. Full URL: ${url}`, ); return; } if (parsed.hostname === "neon-oauth-return") { const token = parsed.searchParams.get("token"); const refreshToken = parsed.searchParams.get("refreshToken"); const expiresIn = Number(parsed.searchParams.get("expiresIn")); if (!token || !refreshToken || !expiresIn) { dialog.showErrorBox( "Invalid URL", "Expected token, refreshToken, and expiresIn", ); return; } handleNeonOAuthReturn({ token, refreshToken, expiresIn }); // Send message to renderer to trigger re-render mainWindow?.webContents.send("deep-link-received", { type: parsed.hostname, }); return; } if (parsed.hostname === "supabase-oauth-return") { const token = parsed.searchParams.get("token"); const refreshToken = parsed.searchParams.get("refreshToken"); const expiresIn = Number(parsed.searchParams.get("expiresIn")); if (!token || !refreshToken || !expiresIn) { dialog.showErrorBox( "Invalid URL", "Expected token, refreshToken, and expiresIn", ); return; } handleSupabaseOAuthReturn({ token, refreshToken, expiresIn }); // Send message to renderer to trigger re-render mainWindow?.webContents.send("deep-link-received", { type: parsed.hostname, }); return; } // dyad://dyad-pro-return?key=123&budget_reset_at=2025-05-26T16:31:13.492000Z&max_budget=100 if (parsed.hostname === "dyad-pro-return") { const apiKey = parsed.searchParams.get("key"); if (!apiKey) { dialog.showErrorBox("Invalid URL", "Expected key"); return; } handleMoreMinimoreProReturn({ apiKey, }); // Send message to renderer to trigger re-render mainWindow?.webContents.send("deep-link-received", { type: parsed.hostname, }); return; } // dyad://add-mcp-server?name=Chrome%20DevTools&config=eyJjb21tYW5kIjpudWxsLCJ0eXBlIjoic3RkaW8ifQ%3D%3D if (parsed.hostname === "add-mcp-server") { const name = parsed.searchParams.get("name"); const config = parsed.searchParams.get("config"); if (!name || !config) { dialog.showErrorBox("Invalid URL", "Expected name and config"); return; } try { const decodedConfigJson = atob(config); const decodedConfig = JSON.parse(decodedConfigJson); const parsedConfig = AddMcpServerConfigSchema.parse(decodedConfig); mainWindow?.webContents.send("deep-link-received", { type: parsed.hostname, payload: { name, config: parsedConfig, } as AddMcpServerPayload, }); } catch (error) { logger.error("Failed to parse add-mcp-server deep link:", error); dialog.showErrorBox( "Invalid MCP Server Configuration", "The deep link contains malformed configuration data. Please check the URL and try again.", ); } return; } // dyad://add-prompt?data= if (parsed.hostname === "add-prompt") { const data = parsed.searchParams.get("data"); if (!data) { dialog.showErrorBox("Invalid URL", "Expected data parameter"); return; } try { const decodedJson = atob(data); const decoded = JSON.parse(decodedJson); const parsedData = AddPromptDataSchema.parse(decoded); mainWindow?.webContents.send("deep-link-received", { type: parsed.hostname, payload: parsedData as AddPromptPayload, }); } catch (error) { logger.error("Failed to parse add-prompt deep link:", error); dialog.showErrorBox( "Invalid Prompt Data", "The deep link contains malformed data. Please check the URL and try again.", ); } return; } dialog.showErrorBox("Invalid deep link URL", url); } // Quit when all windows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on("window-all-closed", () => { if (process.platform !== "darwin") { app.quit(); } }); // Only set isRunning to false when the app is properly quit by the user app.on("will-quit", () => { logger.info("App is quitting, setting isRunning to false"); // Stop performance monitoring and capture final metrics stopPerformanceMonitoring(); writeSettings({ isRunning: false }); }); app.on("activate", () => { // On OS X it's common to re-create a window in the app when the // dock icon is clicked and there are no other windows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow(); } }); // In this file you can include the rest of your app's specific main process // code. You can also put them in separate files and import them here.