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

553 lines 23.2 kB
/** * Enhanced Framework Detector * * Fixes critical framework misdetection issues, especially Phoenix LiveView * projects being incorrectly identified as Flutter. Provides accurate detection * through multiple detection methods and confidence scoring. */ import { UserFriendlyLogger } from './user-friendly-logger.js'; import * as fs from 'fs/promises'; import * as path from 'path'; export class EnhancedFrameworkDetector { logger; frameworkProfiles; constructor() { this.logger = new UserFriendlyLogger('FrameworkDetector'); this.frameworkProfiles = new Map(); this.initializeFrameworkProfiles(); } /** * Initialize comprehensive framework profiles with proper Phoenix LiveView support */ initializeFrameworkProfiles() { const profiles = [ // Phoenix LiveView (HIGH PRIORITY - should be detected first on port 4000) { name: 'phoenix-liveview', priority: 100, // Highest priority for port 4000 detectionFiles: ['mix.exs', 'config/config.exs', 'lib/', 'priv/'], requiredFiles: ['mix.exs'], // Must have mix.exs excludeFiles: ['pubspec.yaml', 'lib/main.dart'], // Exclude Flutter files httpHeaders: { 'server': /phoenix/i, 'x-powered-by': /phoenix/i }, domSelectors: ['[phx-', '[data-phx-', 'script[src*="phoenix"]'], portHints: [4000], packageJsonDeps: [] // Elixir project doesn't have package.json }, // Elixir/Phoenix (fallback for non-LiveView Phoenix) { name: 'phoenix', priority: 95, detectionFiles: ['mix.exs', 'config/config.exs', 'web/', 'priv/static/'], requiredFiles: ['mix.exs'], excludeFiles: ['pubspec.yaml', 'lib/main.dart'], portHints: [4000, 4001], packageJsonDeps: [] }, // Flutter Web (LOWER PRIORITY than Phoenix for port conflicts) { name: 'flutter-web', priority: 60, // Lower than Phoenix detectionFiles: ['pubspec.yaml', 'lib/main.dart', 'web/index.html'], requiredFiles: ['pubspec.yaml', 'lib/main.dart'], excludeFiles: ['mix.exs'], // Exclude Phoenix files httpHeaders: { 'content-type': /text\/html/ }, domSelectors: ['flutter-view', '#flutter_bootstrap', 'script[src*="flutter"]'], portHints: [3000, 8080], packageJsonDeps: [] }, // React { name: 'react', priority: 80, detectionFiles: ['src/App.js', 'src/App.tsx', 'public/index.html', 'package.json'], requiredFiles: ['package.json'], httpHeaders: { 'content-type': /text\/html/ }, domSelectors: ['#root', '[data-reactroot]', 'script[src*="react"]'], portHints: [3000], packageJsonDeps: ['react', 'react-dom'] }, // Next.js (Higher priority than React for detection) { name: 'nextjs', priority: 85, detectionFiles: ['next.config.js', 'next.config.mjs', 'pages/', 'app/', 'package.json'], requiredFiles: ['package.json'], httpHeaders: { 'x-powered-by': /next\.js/i }, domSelectors: ['#__next', 'script[src*="_next"]'], portHints: [3000], packageJsonDeps: ['next'] }, // Vue.js { name: 'vue', priority: 75, detectionFiles: ['src/main.js', 'src/main.ts', 'vue.config.js', 'package.json'], requiredFiles: ['package.json'], domSelectors: ['#app', '[data-v-', 'script[src*="vue"]'], portHints: [8080, 3000], packageJsonDeps: ['vue'] }, // Angular { name: 'angular', priority: 85, detectionFiles: ['angular.json', 'src/main.ts', 'package.json'], requiredFiles: ['angular.json', 'package.json'], domSelectors: ['app-root', '[ng-', 'script[src*="angular"]'], portHints: [4200], packageJsonDeps: ['@angular/core'] }, // Svelte/SvelteKit { name: 'svelte', priority: 70, detectionFiles: ['svelte.config.js', 'src/main.js', 'package.json'], requiredFiles: ['package.json'], domSelectors: ['script[src*="svelte"]'], portHints: [5000, 3000], packageJsonDeps: ['svelte'] }, // Rails { name: 'rails', priority: 90, detectionFiles: ['Gemfile', 'config/application.rb', 'app/', 'config/routes.rb'], requiredFiles: ['Gemfile'], httpHeaders: { 'server': /puma|unicorn|passenger/i }, portHints: [3000], packageJsonDeps: [] }, // Django { name: 'django', priority: 85, detectionFiles: ['manage.py', 'requirements.txt', 'settings.py'], requiredFiles: ['manage.py'], httpHeaders: { 'server': /django/i }, portHints: [8000], packageJsonDeps: [] } ]; for (const profile of profiles) { this.frameworkProfiles.set(profile.name, profile); } } /** * Enhanced framework detection with multiple methods and confidence scoring */ async detectFramework(url, page, // Playwright page for DOM analysis workingDirectory) { const urlObj = new URL(url); const port = parseInt(urlObj.port) || (urlObj.protocol === 'https:' ? 443 : 80); const workDir = workingDirectory || process.cwd(); this.logger.info(`🔍 Enhanced framework detection for ${url} (port ${port})`); // Detection Method 1: File System Analysis (Most Reliable) const fileSystemResults = await this.detectByFileSystem(workDir); // Detection Method 2: HTTP Headers Analysis const httpHeaderResults = await this.detectByHttpHeaders(url); // Detection Method 3: Port-based Heuristics const portHeuristicResults = this.detectByPortHeuristics(port); // Detection Method 4: DOM Analysis (if page available) const domResults = page ? await this.detectByDomAnalysis(page) : null; // Combine all detection methods with weighted scoring const combinedResults = this.combineDetectionResults(fileSystemResults, httpHeaderResults, portHeuristicResults, domResults); // Special handling for Phoenix LiveView on port 4000 const finalResult = this.applyPhoenixLiveViewCorrection(combinedResults, port, fileSystemResults); this.logger.info(`✅ Framework detected: ${finalResult.framework} (${(finalResult.confidence * 100).toFixed(1)}% confidence)`); return finalResult; } /** * File system-based framework detection */ async detectByFileSystem(workDir) { const results = new Map(); for (const [frameworkName, profile] of this.frameworkProfiles) { let confidence = 0; const evidence = []; // Check for detection files let filesFound = 0; for (const file of profile.detectionFiles) { try { const filePath = path.join(workDir, file); await fs.access(filePath); filesFound++; evidence.push(file); } catch { // File doesn't exist } } // Calculate base confidence from files found if (filesFound > 0) { confidence = (filesFound / profile.detectionFiles.length) * 0.6; // Base 60% for file detection } // Required files check (must all exist) if (profile.requiredFiles) { let requiredFound = 0; for (const requiredFile of profile.requiredFiles) { try { await fs.access(path.join(workDir, requiredFile)); requiredFound++; } catch { // Required file missing } } if (requiredFound === profile.requiredFiles.length) { confidence += 0.3; // Bonus for all required files } else if (requiredFound === 0) { confidence = 0; // No confidence if no required files } } // Exclude files check (none should exist) if (profile.excludeFiles) { let excludeFound = 0; for (const excludeFile of profile.excludeFiles) { try { await fs.access(path.join(workDir, excludeFile)); excludeFound++; } catch { // Exclude file doesn't exist (good) } } if (excludeFound > 0) { confidence *= 0.3; // Heavy penalty for conflicting files evidence.push(`Conflicting files: ${profile.excludeFiles.filter(async (f) => { try { await fs.access(path.join(workDir, f)); return true; } catch { return false; } }).join(', ')}`); } } // Package.json dependency check if (profile.packageJsonDeps && profile.packageJsonDeps.length > 0) { try { const packageJsonPath = path.join(workDir, 'package.json'); const packageJson = JSON.parse(await fs.readFile(packageJsonPath, 'utf-8')); const deps = { ...packageJson.dependencies, ...packageJson.devDependencies }; let depsFound = 0; for (const dep of profile.packageJsonDeps) { if (deps[dep]) { depsFound++; evidence.push(`Dependency: ${dep}`); } } if (depsFound > 0) { confidence += (depsFound / profile.packageJsonDeps.length) * 0.2; // Up to 20% bonus } } catch { // No package.json or parsing error } } if (confidence > 0) { results.set(frameworkName, { confidence, evidence }); } } return results; } /** * HTTP headers-based framework detection */ async detectByHttpHeaders(url) { const results = new Map(); try { const response = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(5000) }); const headers = {}; response.headers.forEach((value, key) => { headers[key.toLowerCase()] = value; }); for (const [frameworkName, profile] of this.frameworkProfiles) { if (!profile.httpHeaders) continue; let confidence = 0; const evidence = []; for (const [headerName, expectedValue] of Object.entries(profile.httpHeaders)) { const headerValue = headers[headerName.toLowerCase()]; if (headerValue) { if (expectedValue instanceof RegExp) { if (expectedValue.test(headerValue)) { confidence += 0.3; evidence.push(`${headerName}: ${headerValue}`); } } else if (headerValue.toLowerCase().includes(expectedValue.toLowerCase())) { confidence += 0.3; evidence.push(`${headerName}: ${headerValue}`); } } } if (confidence > 0) { results.set(frameworkName, { confidence, evidence }); } } } catch (error) { this.logger.warn(`HTTP headers detection failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } return results; } /** * Port-based heuristic detection */ detectByPortHeuristics(port) { const results = new Map(); for (const [frameworkName, profile] of this.frameworkProfiles) { if (!profile.portHints) continue; if (profile.portHints.includes(port)) { const confidence = 0.2; // Port hints give low confidence but useful for tie-breaking const evidence = [`Common port ${port} for ${frameworkName}`]; results.set(frameworkName, { confidence, evidence }); } } return results; } /** * DOM-based framework detection */ async detectByDomAnalysis(page) { if (!page) return null; const results = new Map(); try { for (const [frameworkName, profile] of this.frameworkProfiles) { if (!profile.domSelectors) continue; let confidence = 0; const evidence = []; for (const selector of profile.domSelectors) { try { const elements = await page.$$eval(selector, (els) => els.length); if (elements > 0) { confidence += 0.25; evidence.push(`DOM: ${selector} (${elements} elements)`); } } catch { // Selector failed, continue } } if (confidence > 0) { results.set(frameworkName, { confidence, evidence }); } } } catch (error) { this.logger.warn(`DOM analysis failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } return results; } /** * Combine detection results with weighted scoring */ combineDetectionResults(fileSystem, httpHeaders, portHeuristics, domAnalysis) { const combinedScores = new Map(); // Combine all detection methods const allSources = [ { source: fileSystem, method: 'file-system', weight: 1.0 }, { source: httpHeaders, method: 'http-headers', weight: 0.8 }, { source: portHeuristics, method: 'port-heuristic', weight: 0.3 }, { source: domAnalysis, method: 'dom-analysis', weight: 0.7 } ]; for (const { source, method, weight } of allSources) { if (!source) continue; for (const [framework, result] of source) { const existing = combinedScores.get(framework) || { confidence: 0, evidence: [], methods: [] }; existing.confidence += result.confidence * weight; existing.evidence.push(...result.evidence); existing.methods.push(method); combinedScores.set(framework, existing); } } // Apply priority bonuses for (const [framework, result] of combinedScores) { const profile = this.frameworkProfiles.get(framework); if (profile) { const priorityBonus = (profile.priority / 100) * 0.1; // Up to 10% bonus result.confidence += priorityBonus; } } // Find best match let bestFramework = 'unknown'; let bestConfidence = 0; let bestEvidence = []; let detectionMethod = 'hybrid'; for (const [framework, result] of combinedScores) { if (result.confidence > bestConfidence) { bestFramework = framework; bestConfidence = result.confidence; bestEvidence = result.evidence; // Determine primary detection method if (result.methods.includes('file-system')) { detectionMethod = 'file-system'; } else if (result.methods.includes('http-headers')) { detectionMethod = 'http-headers'; } else if (result.methods.includes('dom-analysis')) { detectionMethod = 'dom-analysis'; } else { detectionMethod = 'port-heuristic'; } } } // Generate alternative candidates const alternatives = Array.from(combinedScores.entries()) .filter(([framework]) => framework !== bestFramework) .sort((a, b) => b[1].confidence - a[1].confidence) .slice(0, 3) .map(([framework, result]) => ({ framework, confidence: result.confidence, reason: result.methods.join(', ') })); return { framework: bestFramework, confidence: Math.min(1.0, bestConfidence), // Cap at 100% detectionMethod, evidence: { filesFound: bestEvidence.filter(e => !e.includes(':')), httpHeaders: {}, portAnalysis: { port: 0, commonFrameworks: [] }, domIndicators: bestEvidence.filter(e => e.startsWith('DOM:')) }, alternativeCandidates: alternatives }; } /** * Apply Phoenix LiveView correction for port 4000 projects */ applyPhoenixLiveViewCorrection(result, port, fileSystemResults) { // Special case: If we're on port 4000 and have Phoenix files, strongly favor Phoenix if (port === 4000) { const phoenixResult = fileSystemResults.get('phoenix-liveview') || fileSystemResults.get('phoenix'); if (phoenixResult && phoenixResult.confidence > 0.3) { // Override detection if Phoenix is detected with reasonable confidence if (result.framework === 'flutter-web' || result.framework === 'unknown') { this.logger.info('🔧 Applying Phoenix LiveView correction for port 4000'); return { ...result, framework: 'phoenix-liveview', confidence: Math.max(0.8, phoenixResult.confidence), // Boost confidence detectionMethod: 'hybrid', evidence: { ...result.evidence, filesFound: phoenixResult.evidence } }; } } } return result; } /** * Quick framework detection for performance-critical scenarios */ async quickDetect(url, workingDirectory) { const urlObj = new URL(url); const port = parseInt(urlObj.port) || 80; const workDir = workingDirectory || process.cwd(); // Quick port-based detection first if (port === 4000) { // Check for Phoenix files quickly try { await fs.access(path.join(workDir, 'mix.exs')); return 'phoenix-liveview'; } catch { // No Phoenix files } } // Standard quick checks const quickChecks = [ { port: 3000, files: ['next.config.js'], framework: 'nextjs' }, { port: 3000, files: ['src/App.jsx', 'src/App.tsx'], framework: 'react' }, { port: 4200, files: ['angular.json'], framework: 'angular' }, { port: 8080, files: ['vue.config.js'], framework: 'vue' }, { port: 8080, files: ['pubspec.yaml'], framework: 'flutter-web' } ]; for (const check of quickChecks) { if (port === check.port) { for (const file of check.files) { try { await fs.access(path.join(workDir, file)); return check.framework; } catch { continue; } } } } return 'unknown'; } /** * Get framework-specific debugging recommendations */ getFrameworkSpecificRecommendations(framework) { const recommendations = { 'phoenix-liveview': [ 'Use Phoenix LiveView debugging tools', 'Monitor LiveView socket connections', 'Check for Elixir process errors in logs', 'Verify LiveView mount and update lifecycle' ], 'phoenix': [ 'Check Phoenix server logs', 'Verify Plug pipeline configuration', 'Monitor Ecto database connections', 'Review controller and view rendering' ], 'react': [ 'Use React Developer Tools', 'Check for React Strict Mode warnings', 'Monitor component re-renders', 'Verify prop types and state updates' ], 'nextjs': [ 'Check Next.js build output', 'Monitor SSR/SSG hydration', 'Verify API routes functionality', 'Review Next.js configuration' ], 'flutter-web': [ 'Use Flutter Inspector', 'Check Flutter Web console output', 'Verify Flutter Web build configuration', 'Monitor widget tree performance' ] }; return recommendations[framework] || [ 'Verify application server is running', 'Check console for JavaScript errors', 'Monitor network requests', 'Review application logs' ]; } } //# sourceMappingURL=enhanced-framework-detector.js.map