/**
* Electron Main Process Entry Point
*
* This is a standalone Electron UI app that can be:
* 1. Launched directly by user - Shows UI with history
* 2. Launched by MCP server with --show-diagram=<id> to display a specific diagram
*
* The MCP protocol is handled by a separate lightweight Node.js process.
*/
import { app, Menu, BrowserWindow } from 'electron';
import path from 'path';
import { fileURLToPath } from 'url';
import { createMainWindow, getMainWindow, ensureWindowVisible } from './window.js';
import { createMenu } from './menu.js';
import { initializeIPCHandlers, cleanupIPCHandlers } from './ipc/handlers.js';
import { IPC_CHANNELS } from './ipc/channels.js';
import { HistoryService } from '../shared/historyService.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Check if running in development mode
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
// Parse command line arguments
function parseArgs(): { dataPath?: string; disableAnalytics: boolean; showDiagramId?: string } {
const args = process.argv.slice(2);
let dataPath: string | undefined;
let disableAnalytics = false;
let showDiagramId: string | undefined;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--data-path' && args[i + 1]) {
dataPath = args[i + 1];
i++;
}
if (args[i] === '--disable-analytics') {
disableAnalytics = true;
}
if (args[i] === '--show-diagram' && args[i + 1]) {
showDiagramId = args[i + 1];
i++;
}
}
return { dataPath, disableAnalytics, showDiagramId };
}
async function initialize(): Promise<void> {
const { dataPath, disableAnalytics, showDiagramId } = parseArgs();
console.log('[Main] Starting Electron UI', { dataPath, showDiagramId, isDev });
// Create history service
const historyService = new HistoryService(dataPath);
// Initialize IPC handlers
initializeIPCHandlers(dataPath, historyService, { disableAnalytics });
// Set up menu
const menu = createMenu();
Menu.setApplicationMenu(menu);
// Create main window
const preloadPath = path.join(__dirname, 'preload.js');
createMainWindow(preloadPath, isDev, true);
// Ensure window is visible on screen
ensureWindowVisible();
// If launched with --show-diagram, send the diagram to the renderer once it's ready
if (showDiagramId) {
const mainWindow = getMainWindow();
if (mainWindow) {
mainWindow.webContents.once('did-finish-load', async () => {
try {
// Load the diagram from history
const diagrams = await historyService.getDiagrams();
const diagram = diagrams.find(d => d.id === showDiagramId);
if (diagram) {
console.log(`[Main] Loading diagram: ${diagram.title} (${showDiagramId})`);
// Send to renderer to display
mainWindow.webContents.send(IPC_CHANNELS.MCP_DIAGRAM_UPDATE, {
diagram: diagram.diagram,
title: diagram.title,
id: diagram.id,
});
} else {
console.warn(`[Main] Diagram not found: ${showDiagramId}`);
}
} catch (error) {
console.error('[Main] Failed to load diagram:', error);
}
});
}
}
}
// App lifecycle handlers
app.whenReady().then(initialize);
app.on('window-all-closed', () => {
// On macOS, apps typically stay running even when all windows are closed
// Only clean up handlers if we're actually quitting
if (process.platform !== 'darwin') {
cleanupIPCHandlers();
app.quit();
}
});
app.on('before-quit', () => {
// Clean up handlers when app is actually quitting (e.g., Cmd+Q on macOS)
cleanupIPCHandlers();
});
app.on('activate', () => {
// On macOS, re-create window when dock icon is clicked
if (BrowserWindow.getAllWindows().length === 0) {
const preloadPath = path.join(__dirname, 'preload.js');
createMainWindow(preloadPath, isDev);
}
});
// Handle second instance (single instance lock)
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
// Another instance is already running
// Pass the diagram ID to the existing instance and quit
app.quit();
} else {
app.on('second-instance', async (_event, commandLine, _workingDirectory) => {
let mainWindow = getMainWindow();
// If no window exists (e.g., closed on macOS), create one
if (!mainWindow) {
const preloadPath = path.join(__dirname, 'preload.js');
mainWindow = createMainWindow(preloadPath, isDev, true);
}
// Restore if minimized
if (mainWindow.isMinimized()) {
mainWindow.restore();
}
// Show and focus
mainWindow.show();
mainWindow.focus();
// Check if the second instance was launched with --show-diagram
// Support both formats: --show-diagram <id> and --show-diagram=<id>
let diagramId: string | undefined;
// Check for --show-diagram=<id> format
const equalsArg = commandLine.find(arg => arg.startsWith('--show-diagram='));
if (equalsArg) {
diagramId = equalsArg.split('=')[1];
} else {
// Check for --show-diagram <id> format
const showDiagramIndex = commandLine.indexOf('--show-diagram');
if (showDiagramIndex !== -1 && commandLine[showDiagramIndex + 1]) {
diagramId = commandLine[showDiagramIndex + 1];
}
}
if (diagramId) {
console.log(`[Main] Second instance requested diagram: ${diagramId}`);
// Load and display the diagram
const { dataPath } = parseArgs();
const historyService = new HistoryService(dataPath);
// Wait for window to be ready if it was just created
const sendDiagram = async () => {
try {
const diagrams = await historyService.getDiagrams();
const diagram = diagrams.find(d => d.id === diagramId);
if (diagram && mainWindow) {
mainWindow.webContents.send(IPC_CHANNELS.MCP_DIAGRAM_UPDATE, {
diagram: diagram.diagram,
title: diagram.title,
id: diagram.id,
});
}
} catch (error) {
console.error('[Main] Failed to load diagram:', error);
}
};
// If the page is already loaded, send immediately; otherwise wait
if (mainWindow.webContents.isLoading()) {
mainWindow.webContents.once('did-finish-load', sendDiagram);
} else {
sendDiagram();
}
}
});
}
// Handle uncaught exceptions
process.on('uncaughtException', (error) => {
console.error('Uncaught exception:', error);
});
process.on('unhandledRejection', (reason, promise) => {
console.error('Unhandled rejection at:', promise, 'reason:', reason);
});