Skip to main content
Glama
snowfort-ai

Snowfort Circuit MCP

Official
by snowfort-ai
electron-driver.js48.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.ElectronDriver = void 0; // Import from playwright-core (will be overridden with project version) const playwright_core_1 = require("playwright-core"); const crypto_1 = require("crypto"); const fs_1 = require("fs"); const child_process_1 = require("child_process"); const util_1 = require("util"); const path = __importStar(require("path")); const os = __importStar(require("os")); const execAsync = (0, util_1.promisify)(child_process_1.exec); class ElectronDriver { devServerProcesses = new Map(); async killProcessesOnPorts(ports) { const promises = ports.map(async (port) => { try { // Use lsof to find processes using the port const { stdout } = await execAsync(`lsof -ti:${port}`); const pids = stdout.trim().split('\n').filter(pid => pid); if (pids.length > 0) { // Kill each process await Promise.all(pids.map(pid => execAsync(`kill -9 ${pid}`).catch(() => { // Ignore errors - process might already be dead }))); return `Killed ${pids.length} process(es) on port ${port}`; } return `No processes found on port ${port}`; } catch (error) { // Port not in use or lsof failed return `Port ${port} was free or couldn't check`; } }); const results = await Promise.all(promises); // Results are logged by the caller for debugging } async getWindowType(window) { try { const title = await window.title(); const url = await window.url(); if (title && (title.includes('DevTools') || title.includes('Developer Tools') || title.startsWith('chrome-extension://') || url.startsWith('devtools://'))) { return 'devtools'; } if (title === 'about:blank' || !title) { return 'other'; } return 'main'; } catch { return 'other'; } } async getMainWindow(electronApp, timeout = 10000) { const startTime = Date.now(); while (Date.now() - startTime < timeout) { const windows = await electronApp.windows(); for (const window of windows) { const windowType = await this.getWindowType(window); if (windowType === 'main') { return window; } } // Wait a bit before trying again await new Promise(resolve => setTimeout(resolve, 100)); } // Fallback to firstWindow if no main window found return await electronApp.firstWindow(); } async startDevServer(projectPath, startScript) { return new Promise((resolve, reject) => { const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'; const devServerProcess = (0, child_process_1.spawn)(npmCmd, ['run', startScript], { cwd: projectPath, stdio: 'pipe', shell: true, }); let output = ''; let errorOutput = ''; let resolved = false; // Set a timeout for server startup with progress updates let progressCount = 0; const progressInterval = setInterval(() => { if (!resolved) { progressCount++; // Log progress every 5 seconds if (progressCount % 5 === 0) { console.error(`[Dev Server] Waiting for startup... ${progressCount}s elapsed`); if (output.trim()) { console.error(`[Dev Server] Last output: ${output.split('\n').slice(-3).join('\n')}`); } } } }, 1000); const timeout = setTimeout(() => { if (!resolved) { resolved = true; clearInterval(progressInterval); devServerProcess.kill(); reject(new Error(`Dev server startup timeout after 30 seconds. Last output: ${output}`)); } }, 30000); devServerProcess.stdout?.on('data', (data) => { const chunk = data.toString(); output += chunk; // Look for common "ready" patterns if (!resolved && (chunk.includes('Compiled successfully') || chunk.includes('webpack compiled successfully') || chunk.includes('Webpack compiled successfully') || chunk.includes('Main window is ready') || chunk.includes('Electron Forge webpack output') || chunk.includes('App ready') || chunk.includes('Dev server is running') || // Electron Forge specific patterns chunk.includes('✔ Compiling Renderer Process Code') || chunk.includes('✔ Launching Application') || chunk.includes('✔ Running preStart hook') || chunk.includes('STARTUP_URL=') || chunk.includes('Renderer webpack configuration') || // App-specific patterns chunk.includes('Remote debugging enabled') || chunk.includes('Starting electron app') || chunk.includes('Electron app started'))) { resolved = true; clearTimeout(timeout); clearInterval(progressInterval); // Give it a moment for the server to fully stabilize setTimeout(() => resolve(devServerProcess), 2000); // Increased to 2s for Forge } }); devServerProcess.stderr?.on('data', (data) => { errorOutput += data.toString(); }); devServerProcess.on('error', (error) => { if (!resolved) { resolved = true; clearTimeout(timeout); clearInterval(progressInterval); reject(new Error(`Failed to start dev server: ${error.message}`)); } }); devServerProcess.on('exit', (code) => { if (!resolved) { resolved = true; clearTimeout(timeout); clearInterval(progressInterval); if (code !== 0) { reject(new Error(`Dev server exited with code ${code}. Error output: ${errorOutput}`)); } } }); }); } async getElectronInstance(projectPath) { const debugInfo = []; if (!projectPath) { debugInfo.push(`[PLAYWRIGHT-DEBUG] No project path provided, using NPX default`); return { electron: playwright_core_1._electron, debugInfo }; } try { const projectPlaywrightDir = path.join(projectPath, 'node_modules', 'playwright-core'); debugInfo.push(`[PLAYWRIGHT-DEBUG] Checking project directory: ${projectPlaywrightDir}`); // Check if the directory exists await fs_1.promises.access(projectPlaywrightDir); debugInfo.push(`[PLAYWRIGHT-DEBUG] ✓ Project Playwright directory found`); // ESM requires importing specific files, not directories const projectPlaywrightPath = path.join(projectPlaywrightDir, 'index.js'); debugInfo.push(`[PLAYWRIGHT-DEBUG] Importing specific file: ${projectPlaywrightPath}`); // Try multiple import strategies let projectPlaywright = null; // Strategy 1: ESM dynamic import (specific file) try { debugInfo.push(`[PLAYWRIGHT-DEBUG] Trying ESM dynamic import of index.js...`); projectPlaywright = await Promise.resolve(`${projectPlaywrightPath}`).then(s => __importStar(require(s))); debugInfo.push(`[PLAYWRIGHT-DEBUG] ✓ ESM dynamic import succeeded`); } catch (esmError) { debugInfo.push(`[PLAYWRIGHT-DEBUG] ✗ ESM dynamic import failed: ${esmError}`); // Strategy 2: Try alternative import methods if needed debugInfo.push(`[PLAYWRIGHT-DEBUG] ✗ ESM dynamic import failed, checking error...`); throw esmError; // For now, just throw to see if there are other issues } // Check if _electron is available (handle both CommonJS and ESM patterns) let electronInstance = null; if (projectPlaywright) { // Try direct access first (CommonJS pattern) if (projectPlaywright._electron) { electronInstance = projectPlaywright._electron; debugInfo.push(`[PLAYWRIGHT-DEBUG] ✓ Found _electron at top level (CommonJS style)`); } // Try default export (ESM pattern) else if (projectPlaywright.default && projectPlaywright.default._electron) { electronInstance = projectPlaywright.default._electron; debugInfo.push(`[PLAYWRIGHT-DEBUG] ✓ Found _electron in default export (ESM style)`); } // Try module.exports (mixed pattern) else if (projectPlaywright['module.exports'] && projectPlaywright['module.exports']._electron) { electronInstance = projectPlaywright['module.exports']._electron; debugInfo.push(`[PLAYWRIGHT-DEBUG] ✓ Found _electron in module.exports`); } else { debugInfo.push(`[PLAYWRIGHT-DEBUG] ✗ Project Playwright loaded but missing _electron property`); debugInfo.push(`[PLAYWRIGHT-DEBUG] Available top-level properties: ${Object.keys(projectPlaywright).join(', ')}`); if (projectPlaywright.default) { debugInfo.push(`[PLAYWRIGHT-DEBUG] Available default properties: ${Object.keys(projectPlaywright.default).slice(0, 10).join(', ')}...`); } } } if (electronInstance) { debugInfo.push(`[PLAYWRIGHT-DEBUG] ✓ Successfully using project's Playwright!`); return { electron: electronInstance, debugInfo }; } } catch (error) { debugInfo.push(`[PLAYWRIGHT-DEBUG] ✗ Failed to load project Playwright: ${error}`); } debugInfo.push(`[PLAYWRIGHT-DEBUG] Falling back to NPX-cached Playwright`); return { electron: playwright_core_1._electron, debugInfo }; } async launch(opts) { if (!opts.app) { throw new Error("App path is required for Electron driver"); } const mode = opts.mode || 'auto'; if (mode === 'auto') { return await this.autoLaunch(opts); } else if (mode === 'development') { return await this.launchDevelopment(opts); } else { return await this.launchPackaged(opts); } } async autoLaunch(opts) { try { // Try packaged first (most common) if (await this.isPackagedApp(opts.app)) { return await this.launchPackaged(opts); } // Try development mode if we have a project path or if app looks like a directory if (opts.projectPath || await this.looksLikeProject(opts.app)) { const projectPath = opts.projectPath || opts.app; if (await this.hasPackageJson(projectPath)) { return await this.launchDevelopment({ ...opts, projectPath }); } } // Fallback to packaged launch return await this.launchPackaged(opts); } catch (error) { throw new Error(`Auto-launch failed: ${error instanceof Error ? error.message : String(error)}`); } } async launchDevelopment(opts) { const projectPath = opts.projectPath || opts.app; const { electron } = await this.getElectronInstance(projectPath); // Validate project path exists try { await fs_1.promises.stat(projectPath); } catch (error) { throw new Error(`Project path does not exist or is not accessible: ${projectPath}. Error: ${error}`); } // Check for package.json const packageJsonPath = path.join(projectPath, 'package.json'); let packageJsonContent; try { await fs_1.promises.stat(packageJsonPath); packageJsonContent = JSON.parse(await fs_1.promises.readFile(packageJsonPath, 'utf8')); } catch (error) { throw new Error(`No valid package.json found in ${projectPath}. Error: ${error}`); } // Check for Electron Forge project const isForgeProject = await this.isElectronForgeProject(projectPath, packageJsonContent); if (isForgeProject) { return await this.launchElectronForgeProject(opts, projectPath, packageJsonContent); } // Find Electron executable let electronPath; let execDebugInfo = []; if (opts.electronPath) { electronPath = opts.electronPath; execDebugInfo.push(`[EXEC-DEBUG] Using provided electronPath: ${electronPath}`); } else { const execResult = await this.findElectronExecutable(projectPath); electronPath = execResult.path; execDebugInfo = execResult.debugInfo; } // Validate Electron executable try { await fs_1.promises.stat(electronPath); } catch (error) { throw new Error(`Electron executable not accessible: ${electronPath}. Error: ${error}`); } // Prepare launch configuration // IMPORTANT: Do NOT pass env parameter unless absolutely necessary // Playwright's electron.launch replaces the entire environment when env is provided const launchConfig = { executablePath: electronPath, args: ['.'], cwd: projectPath, timeout: opts.timeout || 30000, }; // Add DevTools control if requested if (opts.disableDevtools) { launchConfig.args = [...(launchConfig.args || []), '--disable-dev-tools']; } // Only add env if user explicitly provided environment variables if (opts.env && Object.keys(opts.env).length > 0) { // Must include process.env to avoid breaking Electron launchConfig.env = { ...process.env, ...opts.env, NODE_ENV: 'development' }; } // Launch Electron application const electronApp = await electron.launch(launchConfig); // Wait for main window const mainWindow = await this.getMainWindow(electronApp); const windows = new Map(); if (mainWindow) { windows.set("main", mainWindow); } // Create and return session const session = { id: (0, crypto_1.randomUUID)(), electronApp, mainWindow, windows, options: { ...opts, compressScreenshots: opts.compressScreenshots ?? true }, }; return session; } async launchPackaged(opts) { const { electron } = await this.getElectronInstance(); // Build launch config without env by default const launchConfig = { executablePath: opts.app, args: opts.args || [], cwd: opts.cwd, timeout: opts.timeout || 30000, }; // Add DevTools control if requested if (opts.disableDevtools) { launchConfig.args = [...launchConfig.args, '--disable-dev-tools']; } // Only add env if explicitly provided and non-empty if (opts.env && Object.keys(opts.env).length > 0) { launchConfig.env = { ...process.env, ...opts.env }; } const electronApp = await electron.launch(launchConfig); const mainWindow = await this.getMainWindow(electronApp); const windows = new Map(); if (mainWindow) { windows.set("main", mainWindow); } const session = { id: (0, crypto_1.randomUUID)(), electronApp, mainWindow, windows, options: { ...opts, compressScreenshots: opts.compressScreenshots ?? true }, }; return session; } async isElectronForgeProject(projectPath, packageJsonContent) { // Check for Forge configuration in package.json const hasForgeConfig = packageJsonContent.config && packageJsonContent.config.forge; if (hasForgeConfig) { return true; } // Check for Forge dev dependencies const devDeps = packageJsonContent.devDependencies || {}; const hasForgeDevDep = Object.keys(devDeps).some(dep => dep.includes('@electron-forge/')); if (hasForgeDevDep) { return true; } // Check for Forge scripts const scripts = packageJsonContent.scripts || {}; const hasForgeScript = Object.values(scripts).some((script) => typeof script === 'string' && script.includes('electron-forge')); if (hasForgeScript) { return true; } // Check for Forge config files const forgeFiles = ['forge.config.js', 'forge.config.ts']; for (const file of forgeFiles) { try { await fs_1.promises.access(path.join(projectPath, file)); return true; } catch { // File doesn't exist } } return false; } async launchElectronForgeProject(opts, projectPath, packageJsonContent) { let electronApp; let devServerProcess; let allDebugInfo = []; try { // Start dev server if startScript is provided if (opts.startScript) { allDebugInfo.push(`[FORGE-DEBUG] Starting dev server with script: ${opts.startScript}`); try { devServerProcess = await this.startDevServer(projectPath, opts.startScript); allDebugInfo.push(`[FORGE-DEBUG] ✓ Dev server started successfully`); this.devServerProcesses.set(projectPath, devServerProcess); } catch (devServerError) { allDebugInfo.push(`[FORGE-DEBUG] ✗ Failed to start dev server: ${devServerError}`); // Check if it's a port conflict and auto-recovery is enabled const errorMessage = devServerError instanceof Error ? devServerError.message : String(devServerError); const isPortConflict = errorMessage.includes('EADDRINUSE') || errorMessage.includes('address already in use'); const shouldRetry = opts.killPortConflicts !== false; // Default to true unless explicitly disabled if (isPortConflict && shouldRetry) { allDebugInfo.push(`[FORGE-DEBUG] Port conflict detected - attempting to kill existing processes and retry`); try { // Try to extract the specific port from the error message const portMatch = errorMessage.match(/:(\d+)/); const conflictingPort = portMatch ? parseInt(portMatch[1]) : null; if (conflictingPort) { allDebugInfo.push(`[FORGE-DEBUG] Detected port conflict on port ${conflictingPort}, killing processes on that port only`); await this.killProcessesOnPorts([conflictingPort]); } else { // Fallback to common Electron Forge ports if we can't detect the specific port allDebugInfo.push(`[FORGE-DEBUG] Could not detect specific port, trying common Electron Forge ports`); await this.killProcessesOnPorts([9000, 9001, 9002]); } allDebugInfo.push(`[FORGE-DEBUG] Killed processes on conflicting ports, retrying dev server start`); // Wait a moment for ports to be freed await new Promise(resolve => setTimeout(resolve, 2000)); // Retry starting the dev server devServerProcess = await this.startDevServer(projectPath, opts.startScript); allDebugInfo.push(`[FORGE-DEBUG] ✓ Dev server started successfully after port cleanup`); this.devServerProcesses.set(projectPath, devServerProcess); } catch (retryError) { allDebugInfo.push(`[FORGE-DEBUG] ✗ Retry failed: ${retryError}`); throw new Error(`Failed to start dev server with '${opts.startScript}' even after port cleanup: ${errorMessage}`); } } else { const hint = isPortConflict ? ' (set killPortConflicts: false to disable auto-recovery)' : ''; throw new Error(`Failed to start dev server with '${opts.startScript}': ${errorMessage}${hint}`); } } } allDebugInfo.push(`[PLAYWRIGHT-DEBUG] Attempting to use project's Playwright...`); const { electron, debugInfo: playwrightDebugInfo } = await this.getElectronInstance(projectPath); allDebugInfo.push(...playwrightDebugInfo); // Find Electron executable let electronPath; let execDebugInfo = []; if (opts.electronPath) { electronPath = opts.electronPath; execDebugInfo.push(`[EXEC-DEBUG] Using provided electronPath: ${electronPath}`); } else { const execResult = await this.findElectronExecutable(projectPath); electronPath = execResult.path; execDebugInfo = execResult.debugInfo; } allDebugInfo.push(...execDebugInfo); // Add execution context debugging allDebugInfo.push(`[CONTEXT-DEBUG] === EXECUTION CONTEXT ===`); allDebugInfo.push(`[CONTEXT-DEBUG] MCP process.cwd(): ${process.cwd()}`); allDebugInfo.push(`[CONTEXT-DEBUG] MCP module context: ESM/CommonJS`); allDebugInfo.push(`[CONTEXT-DEBUG] Node.js version: ${process.version}`); allDebugInfo.push(`[CONTEXT-DEBUG] Platform: ${process.platform} ${process.arch}`); // Get Playwright version (simplified for compatibility) try { // Use dynamic import without JSON attributes to avoid compatibility issues const playwrightModule = await Promise.resolve().then(() => __importStar(require('playwright-core'))); allDebugInfo.push(`[CONTEXT-DEBUG] Playwright module loaded successfully`); // Try to get version from the module itself if available if ('version' in playwrightModule) { allDebugInfo.push(`[CONTEXT-DEBUG] Playwright version: ${playwrightModule.version}`); } else { allDebugInfo.push(`[CONTEXT-DEBUG] Playwright version: not exposed in module`); } } catch (e) { allDebugInfo.push(`[CONTEXT-DEBUG] Playwright module load failed: ${e}`); } // Simplified context detection allDebugInfo.push(`[CONTEXT-DEBUG] Module context: Running from NPX cache (different from project)`); allDebugInfo.push(`[CONTEXT-DEBUG] Key insight: NPX-cached Playwright vs project Playwright`); allDebugInfo.push(`[CONTEXT-DEBUG] === END CONTEXT ===`); // Prepare Forge-specific launch configuration // CRITICAL: Do NOT pass env parameter unless user explicitly needs it // Playwright's electron.launch completely replaces process.env when env is specified const launchConfig = { executablePath: electronPath, args: ['.'], // Use current directory for Forge projects cwd: projectPath, timeout: opts.timeout || 60000, // Longer timeout for Forge projects (webpack can be slow) }; // Add DevTools control if requested if (opts.disableDevtools) { launchConfig.args = [...(launchConfig.args || []), '--disable-dev-tools']; } // Only add env if user explicitly provided environment variables if (opts.env && Object.keys(opts.env).length > 0) { // Must include full process.env to preserve system paths and variables launchConfig.env = { ...process.env, ...opts.env, NODE_ENV: 'development' }; } // Collect detailed launch parameters for debugging allDebugInfo.push(`[FORGE-DEBUG] Launch Config:`); allDebugInfo.push(` executablePath: ${launchConfig.executablePath}`); allDebugInfo.push(` args: ${JSON.stringify(launchConfig.args)}`); allDebugInfo.push(` cwd: ${launchConfig.cwd}`); allDebugInfo.push(` timeout: ${launchConfig.timeout}`); allDebugInfo.push(` env keys: ${launchConfig.env ? Object.keys(launchConfig.env).join(', ') : 'none (using default process.env)'}`); // Verify executable exists and is accessible try { const { promises: fs } = await Promise.resolve().then(() => __importStar(require('fs'))); await fs.access(electronPath); allDebugInfo.push(`[FORGE-DEBUG] ✓ Electron executable verified: ${electronPath}`); } catch (accessError) { allDebugInfo.push(`[FORGE-DEBUG] ✗ Electron executable access failed: ${accessError}`); throw new Error(`Electron executable not accessible: ${electronPath}. Debug: ${allDebugInfo.join('; ')}`); } // Verify project directory allDebugInfo.push(`[FORGE-DEBUG] Project directory: ${projectPath}`); allDebugInfo.push(`[FORGE-DEBUG] Working directory: ${process.cwd()}`); // Test basic Playwright functionality first allDebugInfo.push(`[FORGE-DEBUG] Testing basic Playwright functionality...`); try { // Simple test - just check if _electron exists and has launch method if (typeof electron.launch === 'function') { allDebugInfo.push(`[FORGE-DEBUG] ✓ Playwright _electron.launch is available`); } else { allDebugInfo.push(`[FORGE-DEBUG] ✗ Playwright _electron.launch is not a function: ${typeof electron.launch}`); } } catch (testError) { allDebugInfo.push(`[FORGE-DEBUG] ✗ Basic Playwright test failed: ${testError}`); } // Launch the Forge application allDebugInfo.push(`[FORGE-DEBUG] Attempting electron.launch...`); try { electronApp = await electron.launch(launchConfig); allDebugInfo.push(`[FORGE-DEBUG] ✓ electron.launch succeeded`); } catch (launchError) { allDebugInfo.push(`[FORGE-DEBUG] ✗ electron.launch failed: ${launchError}`); // If relative path failed, try with absolute path as fallback if (electronPath.startsWith('./')) { allDebugInfo.push(`[FORGE-DEBUG] Retrying with absolute path...`); const absolutePath = path.resolve(projectPath, electronPath.substring(2)); allDebugInfo.push(`[FORGE-DEBUG] Absolute path: ${absolutePath}`); const retryConfig = { ...launchConfig, executablePath: absolutePath }; allDebugInfo.push(`[FORGE-DEBUG] Retry config: executablePath=${retryConfig.executablePath}`); try { electronApp = await electron.launch(retryConfig); allDebugInfo.push(`[FORGE-DEBUG] ✓ Absolute path retry succeeded!`); } catch (retryError) { allDebugInfo.push(`[FORGE-DEBUG] ✗ Absolute path retry also failed: ${retryError}`); throw launchError; // Throw original error } } else { throw launchError; } } // Ensure electronApp was successfully created if (!electronApp) { throw new Error('Failed to create Electron application instance'); } // Wait for main window with extended patience const mainWindow = await this.getMainWindow(electronApp); const windows = new Map(); if (mainWindow) { windows.set("main", mainWindow); } // Create session const session = { id: (0, crypto_1.randomUUID)(), electronApp, mainWindow, windows, options: { ...opts, compressScreenshots: opts.compressScreenshots ?? true }, devServerProcess, }; return session; } catch (error) { // Add error details to debug info allDebugInfo.push(`[FORGE-DEBUG] ✗ Launch failed with error: ${error instanceof Error ? error.message : String(error)}`); allDebugInfo.push(`[FORGE-DEBUG] Error stack: ${error instanceof Error ? error.stack : 'No stack available'}`); // Cleanup on failure if (electronApp) { try { await electronApp.close(); } catch (cleanupError) { allDebugInfo.push(`[FORGE-DEBUG] Cleanup error: ${cleanupError}`); } } // Kill dev server if it was started if (devServerProcess) { try { devServerProcess.kill(); this.devServerProcesses.delete(projectPath); } catch (killError) { allDebugInfo.push(`[FORGE-DEBUG] Failed to kill dev server: ${killError}`); } } // Provide helpful error messages for common Forge issues with debug info const debugSummary = allDebugInfo.join('\n'); if (error instanceof Error) { if (error.message.includes('timeout')) { throw new Error(`Forge project launch timeout. This often happens when:\n1. Webpack compilation is taking too long\n2. The main process is waiting for renderer\n3. Try increasing timeout or check Forge webpack config\n\nDEBUG INFO:\n${debugSummary}\n\nOriginal error: ${error.message}`); } else if (error.message.includes('spawn') || error.message.includes('Process failed to launch')) { throw new Error(`Forge project spawn error. This often happens when:\n1. Electron executable path is wrong\n2. Main file path is incorrect\n3. File permissions issue\n4. Missing dependencies or environment variables\n\nDEBUG INFO:\n${debugSummary}\n\nOriginal error: ${error.message}`); } } throw new Error(`Failed to launch Electron Forge project: ${error instanceof Error ? error.message : String(error)}\n\nDEBUG INFO:\n${debugSummary}`); } } async isPackagedApp(appPath) { try { const stats = await fs_1.promises.stat(appPath); // Check for common packaged app patterns if (stats.isFile()) { return appPath.endsWith('.exe') || appPath.endsWith('.app') || appPath.includes('electron'); } if (stats.isDirectory()) { // Check if it's a .app bundle (macOS) if (appPath.endsWith('.app')) { return true; } // Check for typical packaged structure try { await fs_1.promises.access(path.join(appPath, 'resources', 'app.asar')); return true; } catch { return false; } } return false; } catch { return false; } } async looksLikeProject(appPath) { try { const stats = await fs_1.promises.stat(appPath); return stats.isDirectory(); } catch { return false; } } async hasPackageJson(projectPath) { try { await fs_1.promises.access(path.join(projectPath, 'package.json')); return true; } catch { return false; } } async findElectronExecutable(projectPath) { const debugInfo = []; debugInfo.push(`[EXEC-DEBUG] Finding Electron executable for project: ${projectPath}`); // Try relative path first (matches working manual test: './node_modules/.bin/electron') const relativeElectron = './node_modules/.bin/electron'; debugInfo.push(`[EXEC-DEBUG] Checking relative path: ${relativeElectron}`); try { await fs_1.promises.access(relativeElectron); debugInfo.push(`[EXEC-DEBUG] ✓ Found relative electron: ${relativeElectron}`); return { path: relativeElectron, debugInfo }; } catch (e) { debugInfo.push(`[EXEC-DEBUG] ✗ Relative path failed: ${e}`); } // Check local node_modules with absolute path const localElectron = path.join(projectPath, 'node_modules', '.bin', 'electron'); debugInfo.push(`[EXEC-DEBUG] Checking local bin: ${localElectron}`); try { await fs_1.promises.access(localElectron); debugInfo.push(`[EXEC-DEBUG] ✓ Found local bin electron: ${localElectron}`); return { path: localElectron, debugInfo }; } catch (e) { debugInfo.push(`[EXEC-DEBUG] ✗ Local bin not found: ${e}`); } // Check platform-specific paths const platform = os.platform(); let electronBinary; if (platform === 'win32') { electronBinary = path.join(projectPath, 'node_modules', 'electron', 'dist', 'electron.exe'); } else if (platform === 'darwin') { electronBinary = path.join(projectPath, 'node_modules', 'electron', 'dist', 'Electron.app', 'Contents', 'MacOS', 'Electron'); } else { electronBinary = path.join(projectPath, 'node_modules', 'electron', 'dist', 'electron'); } debugInfo.push(`[EXEC-DEBUG] Checking platform binary: ${electronBinary}`); try { await fs_1.promises.access(electronBinary); debugInfo.push(`[EXEC-DEBUG] ✓ Found platform binary: ${electronBinary}`); return { path: electronBinary, debugInfo }; } catch (e) { debugInfo.push(`[EXEC-DEBUG] ✗ Platform binary not found: ${e}`); } // Try global electron debugInfo.push(`[EXEC-DEBUG] Checking global electron`); try { const { stdout } = await execAsync('which electron'); const globalElectron = stdout.trim(); if (globalElectron) { debugInfo.push(`[EXEC-DEBUG] ✓ Found global electron: ${globalElectron}`); return { path: globalElectron, debugInfo }; } } catch (e) { debugInfo.push(`[EXEC-DEBUG] ✗ Global electron not found: ${e}`); } debugInfo.push(`[EXEC-DEBUG] ✗ No Electron executable found anywhere`); throw new Error(`Electron executable not found. Please install electron in ${projectPath} or globally. Debug info: ${debugInfo.join('; ')}`); } async screenshot(session, path, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); const compress = electronSession.options?.compressScreenshots !== false; const quality = electronSession.options?.screenshotQuality || 50; let screenshotPath; if (compress) { screenshotPath = path || `electron-screenshot-${Date.now()}.jpeg`; await window.screenshot({ path: screenshotPath, type: 'jpeg', quality: quality }); } else { screenshotPath = path || `electron-screenshot-${Date.now()}.png`; await window.screenshot({ path: screenshotPath, type: 'png' }); } return screenshotPath; } async snapshot(session, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); const snapshot = await window.accessibility.snapshot(); const enhancedSnapshot = this.enhanceSnapshotWithRefs(snapshot); return JSON.stringify(enhancedSnapshot, null, 2); } enhanceSnapshotWithRefs(snapshot) { if (!snapshot) return snapshot; let refCounter = 1; const addRefs = (node) => { if (!node || typeof node !== 'object') return node; const enhanced = { ...node }; if (node.role && node.role !== 'WebArea' && node.role !== 'RootWebArea') { enhanced.ref = `e${refCounter++}`; } if (node.children && Array.isArray(node.children)) { enhanced.children = node.children.map(addRefs); } return enhanced; }; return addRefs(snapshot); } async click(session, selector, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.click(selector); } async type(session, selector, text, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.fill(selector, text); } async evaluate(session, script, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); return await window.evaluate(script); } async waitForSelector(session, selector, timeout, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.waitForSelector(selector, { timeout }); } async close(session) { const electronSession = session; await electronSession.electronApp.close(); // Clean up dev server if it exists if (electronSession.devServerProcess) { try { electronSession.devServerProcess.kill(); // Remove from tracking map if we know the project path if (electronSession.options?.projectPath || electronSession.options?.app) { const projectPath = electronSession.options.projectPath || electronSession.options.app; this.devServerProcesses.delete(projectPath); } } catch (error) { // Log but don't throw - app is already closed console.error('Failed to kill dev server process:', error); } } } getWindow(session, windowId) { if (windowId && session.windows.has(windowId)) { return session.windows.get(windowId); } if (session.mainWindow) { return session.mainWindow; } throw new Error(`No window found for ID: ${windowId || 'main'}`); } // Additional methods for all the other MCP tools... async hover(session, selector, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.hover(selector); } async drag(session, sourceSelector, targetSelector, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.dragAndDrop(sourceSelector, targetSelector); } async key(session, key, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.keyboard.press(key); } async select(session, selector, value, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.selectOption(selector, value); } async upload(session, selector, filePath, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.setInputFiles(selector, filePath); } async back(session, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.goBack(); } async forward(session, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.goForward(); } async refresh(session, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.reload(); } async content(session, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); return await window.content(); } async textContent(session, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); return await window.textContent('body') || ''; } // Electron-specific methods async invokeIPC(session, channel, ...args) { const electronSession = session; return await electronSession.electronApp.evaluate(({ ipcMain }, { channel, args }) => { return new Promise((resolve) => { ipcMain.handle('__mcp_test', () => { const { ipcRenderer } = require('electron'); return ipcRenderer.invoke(channel, ...args); }); resolve(require('electron').ipcMain.emit('__mcp_test')); }); }, { channel, args }); } async getWindows(session) { const electronSession = session; const windows = await electronSession.electronApp.windows(); const windowInfo = []; for (let index = 0; index < windows.length; index++) { const window = windows[index]; const type = await this.getWindowType(window); let title = ''; try { title = await window.title(); } catch { title = 'Unknown'; } windowInfo.push({ id: `window-${index}`, type: type, title: title }); } return windowInfo; } async writeFile(session, filePath, content) { await fs_1.promises.writeFile(filePath, content, 'utf8'); } async readFile(session, filePath) { return await fs_1.promises.readFile(filePath, 'utf8'); } async keyboardPress(session, key, modifiers, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); if (modifiers && modifiers.length > 0) { const modString = modifiers.join('+'); await window.keyboard.press(`${modString}+${key}`); } else { await window.keyboard.press(key); } } async clickByText(session, text, exact = false, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); if (exact) { await window.click(`text="${text}"`); } else { await window.click(`text=${text}`); } } async addLocatorHandler(session, selector, action, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.addLocatorHandler(window.locator(selector), async () => { if (action === 'click') { await window.click(selector); } else if (action === 'dismiss') { await window.keyboard.press('Escape'); } else if (action === 'accept') { await window.keyboard.press('Enter'); } }); } async clickByRole(session, role, name, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); if (name) { await window.click(`role=${role}[name="${name}"]`); } else { await window.click(`role=${role}`); } } async clickNth(session, selector, index, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.click(`${selector} >> nth=${index}`); } async keyboardType(session, text, delay, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.keyboard.type(text, { delay }); } async waitForLoadState(session, state, windowId) { const electronSession = session; const window = this.getWindow(electronSession, windowId); await window.waitForLoadState(state || 'load'); } } exports.ElectronDriver = ElectronDriver; //# sourceMappingURL=electron-driver.js.map

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/snowfort-ai/circuit-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server