UNPKG

bmad-method-mcp

Version:

Breakthrough Method of Agile AI-driven Development with Enhanced MCP Integration

314 lines (270 loc) 8.35 kB
const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); /** * Auto Web UI Launcher for BMad MCP Server * Automatically starts the web UI when MCP server connects to Claude Desktop * Similar to Serena's auto-dashboard feature */ class AutoWebUILauncher { constructor(logger = console) { this.logger = logger; this.webUIProcess = null; this.webUIPort = 5173; // Default Vite dev server port this.enabled = true; this.autoOpen = process.env.BMAD_AUTO_OPEN_WEBUI !== 'false'; } /** * Start the web UI automatically when MCP server initializes */ async autoStartWebUI() { if (!this.enabled) { this.logger.debug('Auto Web UI launch disabled'); return; } try { // Check if web UI is already running if (await this.isWebUIRunning()) { this.logger.info('🌐 Web UI already running, skipping auto-start'); return; } // Find web UI directory const webUIPath = this.findWebUIPath(); if (!webUIPath) { this.logger.warn('Web UI directory not found, skipping auto-start'); return; } // Start web UI in background await this.startWebUIProcess(webUIPath); // Auto-open in browser if enabled if (this.autoOpen) { setTimeout(() => { this.openWebUI(); }, 3000); // Wait 3 seconds for web UI to start } } catch (error) { this.logger.error('Failed to auto-start Web UI:', error.message); } } /** * Find the web UI directory relative to MCP server */ findWebUIPath() { const possiblePaths = [ path.join(__dirname, '..', 'web-ui'), // tools/mcp-server/../web-ui path.join(__dirname, '..', '..', 'web-ui'), // project root web-ui path.join(process.cwd(), 'web-ui'), // current directory web-ui path.join(process.cwd(), 'tools', 'web-ui'), // tools/web-ui from project root ]; for (const webUIPath of possiblePaths) { if (fs.existsSync(webUIPath) && fs.existsSync(path.join(webUIPath, 'package.json'))) { this.logger.debug(`Found Web UI at: ${webUIPath}`); return webUIPath; } } return null; } /** * Start the web UI development server */ async startWebUIProcess(webUIPath) { return new Promise((resolve, reject) => { this.logger.info('🚀 Auto-starting Web UI...'); // Determine if we need to install dependencies first const nodeModulesPath = path.join(webUIPath, 'node_modules'); const needsInstall = !fs.existsSync(nodeModulesPath); let command, args; if (needsInstall) { this.logger.info('📦 Installing Web UI dependencies...'); command = 'npm'; args = ['install']; } else { command = 'npm'; args = ['run', 'dev']; } // Start the process this.webUIProcess = spawn(command, args, { cwd: webUIPath, stdio: ['ignore', 'pipe', 'pipe'], detached: false, env: { ...process.env, PORT: this.webUIPort.toString(), BROWSER: 'none' // Prevent auto-opening multiple browsers } }); let isResolved = false; // Handle process output this.webUIProcess.stdout.on('data', (data) => { const output = data.toString(); this.logger.debug(`Web UI: ${output.trim()}`); // Check for successful startup indicators if (output.includes('Local:') || output.includes('localhost') || output.includes('ready')) { if (!isResolved && !needsInstall) { this.logger.info(`✅ Web UI started on http://localhost:${this.webUIPort}`); isResolved = true; resolve(); } } // If we just finished installing, start dev server if (needsInstall && (output.includes('added') || output.includes('packages'))) { this.logger.info('📦 Dependencies installed, starting dev server...'); this.webUIProcess.kill(); // Start dev server this.webUIProcess = spawn('npm', ['run', 'dev'], { cwd: webUIPath, stdio: ['ignore', 'pipe', 'pipe'], detached: false, env: { ...process.env, PORT: this.webUIPort.toString(), BROWSER: 'none' } }); // Re-attach listeners this.setupProcessListeners(resolve, reject); } }); this.setupProcessListeners(resolve, reject); // Timeout for startup setTimeout(() => { if (!isResolved) { this.logger.info('✅ Web UI startup initiated (backgrounded)'); resolve(); } }, needsInstall ? 30000 : 10000); // Longer timeout if installing }); } /** * Setup process event listeners */ setupProcessListeners(resolve, reject) { this.webUIProcess.stderr.on('data', (data) => { const error = data.toString(); // Filter out common non-error messages if (!error.includes('ExperimentalWarning') && !error.includes('DeprecationWarning') && !error.includes('punycode')) { this.logger.debug(`Web UI error: ${error.trim()}`); } }); this.webUIProcess.on('error', (error) => { this.logger.error('Web UI process error:', error.message); reject(error); }); this.webUIProcess.on('exit', (code, signal) => { if (code !== 0 && code !== null) { this.logger.warn(`Web UI process exited with code ${code}`); } this.webUIProcess = null; }); } /** * Check if web UI is already running */ async isWebUIRunning() { return new Promise((resolve) => { const http = require('http'); const req = http.request({ hostname: 'localhost', port: this.webUIPort, path: '/', method: 'GET', timeout: 2000 }, (res) => { resolve(true); }); req.on('error', () => { resolve(false); }); req.on('timeout', () => { req.destroy(); resolve(false); }); req.end(); }); } /** * Open web UI in default browser */ openWebUI() { const url = `http://localhost:${this.webUIPort}`; const { exec } = require('child_process'); // Cross-platform browser opening const command = process.platform === 'win32' ? `start ${url}` : process.platform === 'darwin' ? `open ${url}` : `xdg-open ${url}`; exec(command, (error) => { if (error) { this.logger.debug('Could not auto-open Web UI:', error.message); this.logger.info(`🌐 Web UI available at: ${url}`); } else { this.logger.info('🌐 Web UI opened in browser'); } }); } /** * Stop the web UI process */ async stopWebUI() { if (this.webUIProcess) { this.logger.info('🛑 Stopping Web UI...'); return new Promise((resolve) => { this.webUIProcess.on('exit', () => { this.logger.info('✅ Web UI stopped'); this.webUIProcess = null; resolve(); }); // Try graceful shutdown first this.webUIProcess.kill('SIGTERM'); // Force kill after 5 seconds if still running setTimeout(() => { if (this.webUIProcess) { this.webUIProcess.kill('SIGKILL'); this.webUIProcess = null; resolve(); } }, 5000); }); } } /** * Enable or disable auto-start */ setEnabled(enabled) { this.enabled = enabled; } /** * Enable or disable auto-open browser */ setAutoOpen(autoOpen) { this.autoOpen = autoOpen; } /** * Get web UI URL */ getWebUIUrl() { return `http://localhost:${this.webUIPort}`; } /** * Check if web UI process is running */ isProcessRunning() { return this.webUIProcess !== null && !this.webUIProcess.killed; } /** * Get web UI status */ getStatus() { return { enabled: this.enabled, processRunning: this.isProcessRunning(), url: this.getWebUIUrl(), port: this.webUIPort, autoOpen: this.autoOpen }; } } module.exports = AutoWebUILauncher;