UNPKG

ai-debug-local-mcp

Version:

🎯 ENHANCED AI GUIDANCE v4.1.2: Dramatically improved tool descriptions help AI users choose the right tools instead of 'close enough' options. Ultra-fast keyboard automation (10x speed), universal recording, multi-ecosystem debugging support, and compreh

490 lines 19.5 kB
/** * Intelligent Server Readiness & Auto-Startup System * * Addresses the common AI pattern where inject_debugging is attempted before * the development server is running, causing inefficient retry cycles. * * Features: * - Proactive server readiness detection * - Intelligent auto-startup with framework detection * - Development server health monitoring * - Zero-wait injection through predictive readiness */ import { exec, spawn } from 'child_process'; import { promisify } from 'util'; import { UserFriendlyLogger } from './user-friendly-logger.js'; import * as fs from 'fs/promises'; import * as path from 'path'; const execAsync = promisify(exec); export class IntelligentServerReadiness { logger; frameworkConfigs; serverProcesses; readinessCache; constructor() { this.logger = new UserFriendlyLogger('ServerReadiness'); this.frameworkConfigs = new Map(); this.serverProcesses = new Map(); this.readinessCache = new Map(); this.initializeFrameworkConfigs(); } /** * Main method: Check if server is ready, auto-start if needed */ async ensureServerReadiness(url, options = {}) { const startTime = performance.now(); const { autoStart = true, maxWaitMs = 30000, framework = 'auto' } = options; try { // Step 1: Quick readiness check (cached if available) let readinessResult = await this.checkServerReadiness(url); if (readinessResult.isReady) { return { ...readinessResult, readinessTimeMs: performance.now() - startTime, recommendedAction: 'proceed' }; } // Step 2: Detect framework if not specified const detectedFramework = framework === 'auto' ? await this.detectProjectFramework(options.workingDirectory) : framework; // Step 3: Auto-start server if enabled and framework detected if (autoStart && detectedFramework && detectedFramework !== 'unknown') { this.logger.info(`🚀 Auto-starting ${detectedFramework} development server...`); const startupResult = await this.autoStartDevelopmentServer(detectedFramework, url, options.workingDirectory); if (startupResult.success) { // Wait for server to be ready readinessResult = await this.waitForServerReadiness(url, maxWaitMs - (performance.now() - startTime)); return { ...readinessResult, autoStarted: true, readinessTimeMs: performance.now() - startTime, recommendedAction: readinessResult.isReady ? 'proceed' : 'wait', serverInfo: startupResult.serverInfo }; } } // Step 4: Provide guidance if auto-start failed or disabled const recommendedAction = this.getRecommendedAction(detectedFramework, readinessResult); return { ...readinessResult, framework: detectedFramework, readinessTimeMs: performance.now() - startTime, recommendedAction }; } catch (error) { this.logger.error(`Server readiness check failed: ${error instanceof Error ? error.message : 'Unknown error'}`); return { isReady: false, url, framework: 'unknown', autoStarted: false, readinessTimeMs: performance.now() - startTime, recommendedAction: 'check_config' }; } } /** * Check if server is ready to accept debugging connections */ async checkServerReadiness(url) { // Check cache first const cached = this.readinessCache.get(url); if (cached && (Date.now() - cached.timestamp) < cached.ttl) { return { isReady: cached.ready, url, framework: 'cached', autoStarted: false }; } try { // Extract port from URL const urlObj = new URL(url); const port = parseInt(urlObj.port) || (urlObj.protocol === 'https:' ? 443 : 80); // Check if port is listening const isListening = await this.isPortListening(port); if (!isListening) { this.cacheReadinessResult(url, false, 5000); // Cache failure for 5s return { isReady: false, url, framework: 'unknown', autoStarted: false }; } // Try HTTP request to verify server responds const isResponding = await this.testHttpResponse(url); // Cache result this.cacheReadinessResult(url, isResponding, isResponding ? 30000 : 5000); return { isReady: isResponding, url, framework: 'unknown', // Will be detected separately autoStarted: false }; } catch (error) { this.cacheReadinessResult(url, false, 5000); return { isReady: false, url, framework: 'unknown', autoStarted: false }; } } /** * Auto-start development server based on detected framework */ async autoStartDevelopmentServer(framework, targetUrl, workingDirectory) { const config = this.frameworkConfigs.get(framework); if (!config) { return { success: false, error: `No configuration found for framework: ${framework}` }; } const urlObj = new URL(targetUrl); const targetPort = parseInt(urlObj.port) || config.defaultPort; const workDir = workingDirectory || process.cwd(); try { // Find the best start command for the detected framework const startCommand = await this.selectBestStartCommand(config, workDir); if (!startCommand) { return { success: false, error: 'No suitable start command found' }; } this.logger.info(`⚡ Starting ${framework} server with: ${startCommand}`); // Start the server process const serverProcess = await this.spawnServerProcess(startCommand, workDir, targetPort); if (serverProcess) { const serverInfo = { pid: serverProcess.pid, port: targetPort, status: 'starting', startupTimeMs: Date.now() }; // Track the process this.serverProcesses.set(targetUrl, { pid: serverInfo.pid, port: serverInfo.port, status: 'starting', startTime: Date.now() }); // Give the server a moment to start await new Promise(resolve => setTimeout(resolve, 2000)); return { success: true, serverInfo: { pid: serverInfo.pid, port: serverInfo.port, status: serverInfo.status, startupTimeMs: serverInfo.startupTimeMs } }; } return { success: false, error: 'Failed to spawn server process' }; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown startup error' }; } } /** * Wait for server to become ready with intelligent polling */ async waitForServerReadiness(url, maxWaitMs) { const startTime = Date.now(); const pollInterval = 500; // Check every 500ms while ((Date.now() - startTime) < maxWaitMs) { const readinessCheck = await this.checkServerReadiness(url); if (readinessCheck.isReady) { this.logger.success(`✅ Server ready after ${Date.now() - startTime}ms`); return readinessCheck; } // Exponential backoff for polling const waitTime = Math.min(pollInterval * Math.pow(1.2, (Date.now() - startTime) / 1000), 2000); await new Promise(resolve => setTimeout(resolve, waitTime)); } this.logger.warn(`⏰ Server readiness timeout after ${maxWaitMs}ms`); return { isReady: false, url, framework: 'timeout', autoStarted: false }; } /** * Detect project framework from file system */ async detectProjectFramework(workingDirectory) { const workDir = workingDirectory || process.cwd(); try { // Check for framework-specific files for (const [framework, config] of this.frameworkConfigs) { for (const detectionFile of config.detectionFiles) { const filePath = path.join(workDir, detectionFile); try { await fs.access(filePath); this.logger.info(`📦 Detected ${framework} framework (found ${detectionFile})`); return framework; } catch { // File doesn't exist, continue checking } } } // Check package.json for additional clues const packageJsonPath = path.join(workDir, 'package.json'); try { const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); const frameworkFromDeps = this.detectFrameworkFromPackageJson(packageJson); if (frameworkFromDeps) { this.logger.info(`📦 Detected ${frameworkFromDeps} from package.json dependencies`); return frameworkFromDeps; } } catch { // No package.json or parsing error } return 'unknown'; } catch (error) { this.logger.warn(`Failed to detect framework: ${error instanceof Error ? error.message : 'Unknown error'}`); return 'unknown'; } } /** * Initialize framework configurations */ initializeFrameworkConfigs() { const configs = [ { framework: 'react', detectionFiles: ['src/App.js', 'src/App.tsx', 'public/index.html'], startCommands: ['npm start', 'yarn start', 'pnpm start'], defaultPort: 3000, readinessPath: '/', startupTimeoutMs: 15000, healthCheckEndpoints: ['/', '/static/js/', '/favicon.ico'] }, { framework: 'next.js', detectionFiles: ['next.config.js', 'next.config.mjs', 'pages/', 'app/'], startCommands: ['npm run dev', 'yarn dev', 'pnpm dev', 'next dev'], defaultPort: 3000, readinessPath: '/', startupTimeoutMs: 20000, healthCheckEndpoints: ['/', '/_next/', '/api/'] }, { framework: 'vue', detectionFiles: ['src/main.js', 'src/main.ts', 'vue.config.js'], startCommands: ['npm run serve', 'yarn serve', 'pnpm serve', 'npm run dev'], defaultPort: 8080, readinessPath: '/', startupTimeoutMs: 15000, healthCheckEndpoints: ['/', '/js/', '/css/'] }, { framework: 'angular', detectionFiles: ['angular.json', 'src/main.ts', '.angular-cli.json'], startCommands: ['ng serve', 'npm start', 'yarn start'], defaultPort: 4200, readinessPath: '/', startupTimeoutMs: 25000, healthCheckEndpoints: ['/', '/main.js', '/polyfills.js'] }, { framework: 'svelte', detectionFiles: ['svelte.config.js', 'src/main.js', 'rollup.config.js'], startCommands: ['npm run dev', 'yarn dev', 'pnpm dev'], defaultPort: 5000, readinessPath: '/', startupTimeoutMs: 12000, healthCheckEndpoints: ['/', '/build/', '/global.css'] }, { framework: 'flutter', detectionFiles: ['pubspec.yaml', 'lib/main.dart', 'web/index.html'], startCommands: ['flutter run -d web-server', 'flutter run -d chrome'], defaultPort: 3000, readinessPath: '/', startupTimeoutMs: 30000, healthCheckEndpoints: ['/', '/main.dart.js', '/flutter.js'] }, { framework: 'phoenix', detectionFiles: ['mix.exs', 'config/config.exs', 'lib/'], startCommands: ['mix phx.server', 'iex -S mix phx.server'], defaultPort: 4000, readinessPath: '/', startupTimeoutMs: 20000, healthCheckEndpoints: ['/', '/assets/', '/phoenix/'] } ]; for (const config of configs) { this.frameworkConfigs.set(config.framework, config); } } /** * Select the best start command for the detected environment */ async selectBestStartCommand(config, workDir) { // Check which package managers are available const packageManagers = await this.detectAvailablePackageManagers(workDir); for (const command of config.startCommands) { // Check if the command is available if (command.startsWith('npm') && packageManagers.includes('npm')) { return command; } if (command.startsWith('yarn') && packageManagers.includes('yarn')) { return command; } if (command.startsWith('pnpm') && packageManagers.includes('pnpm')) { return command; } if (command.startsWith('flutter') && await this.isCommandAvailable('flutter')) { return command; } if (command.startsWith('mix') && await this.isCommandAvailable('mix')) { return command; } if (command.startsWith('ng') && await this.isCommandAvailable('ng')) { return command; } } return null; } /** * Spawn server process with proper error handling */ async spawnServerProcess(command, workDir, port) { const [cmd, ...args] = command.split(' '); const serverProcess = spawn(cmd, args, { cwd: workDir, detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...process.env, PORT: port.toString(), NODE_ENV: 'development' } }); // Capture startup output for debugging let startupOutput = ''; serverProcess.stdout?.on('data', (data) => { startupOutput += data.toString(); }); serverProcess.stderr?.on('data', (data) => { startupOutput += data.toString(); }); // Handle process errors serverProcess.on('error', (error) => { this.logger.error(`Server process error: ${error.message}`); }); return serverProcess; } // Helper methods async isPortListening(port) { try { const { stdout } = await execAsync(`lsof -i :${port}`); return stdout.trim().length > 0; } catch { return false; } } async testHttpResponse(url) { try { const response = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(5000) }); return response.status < 500; // Accept any non-server-error status } catch { return false; } } cacheReadinessResult(url, ready, ttlMs) { this.readinessCache.set(url, { ready, timestamp: Date.now(), ttl: ttlMs }); } async detectAvailablePackageManagers(workDir) { const managers = []; const checks = [ { name: 'npm', file: 'package-lock.json' }, { name: 'yarn', file: 'yarn.lock' }, { name: 'pnpm', file: 'pnpm-lock.yaml' } ]; for (const { name, file } of checks) { try { await fs.access(path.join(workDir, file)); managers.push(name); } catch { // File doesn't exist } } // If no lock files found, check if commands are available if (managers.length === 0) { for (const name of ['npm', 'yarn', 'pnpm']) { if (await this.isCommandAvailable(name)) { managers.push(name); } } } return managers; } async isCommandAvailable(command) { try { await execAsync(`which ${command}`); return true; } catch { return false; } } detectFrameworkFromPackageJson(packageJson) { const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; if (dependencies['next']) return 'next.js'; if (dependencies['react']) return 'react'; if (dependencies['vue']) return 'vue'; if (dependencies['@angular/core']) return 'angular'; if (dependencies['svelte']) return 'svelte'; if (dependencies['phoenix']) return 'phoenix'; return null; } getRecommendedAction(framework, readinessResult) { if (readinessResult.isReady) return 'proceed'; if (framework === 'unknown') return 'check_config'; return 'start_server'; } /** * Get current server readiness metrics */ getReadinessMetrics() { const activeProcesses = Array.from(this.serverProcesses.values()).length; const avgStartupTime = activeProcesses > 0 ? Array.from(this.serverProcesses.values()) .reduce((sum, proc) => sum + (Date.now() - proc.startTime), 0) / activeProcesses : 0; return { cachedUrls: this.readinessCache.size, activeProcesses, frameworksSupported: this.frameworkConfigs.size, averageStartupTime: avgStartupTime }; } /** * Cleanup server processes */ async cleanup() { for (const [url, processInfo] of this.serverProcesses) { try { process.kill(processInfo.pid, 'SIGTERM'); this.logger.info(`🛑 Stopped server process ${processInfo.pid} for ${url}`); } catch (error) { // Process might already be stopped } } this.serverProcesses.clear(); this.readinessCache.clear(); } } //# sourceMappingURL=intelligent-server-readiness.js.map