UNPKG

embedia

Version:

Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys

308 lines (266 loc) 9.82 kB
const fs = require('fs-extra'); const path = require('path'); const { glob } = require('glob'); class ConflictScanner { async scanProject(projectPath) { const conflicts = []; // Scan for multiple conflict types const scanners = [ this.scanForExistingChat, this.scanForAPIRoutes, this.scanForGlobalStyles, this.scanForPortConflicts, this.scanForDependencyConflicts, this.scanForEnvVarConflicts ]; for (const scanner of scanners) { const result = await scanner.call(this, projectPath); if (result && result.length > 0) { conflicts.push(...result); } } return this.prioritizeConflicts(conflicts); } async scanForExistingChat(projectPath) { const conflicts = []; const chatIndicators = [ // File patterns { pattern: /chat(bot)?\.(?:tsx?|jsx?)$/i, type: 'file' }, { pattern: /message\.(?:tsx?|jsx?)$/i, type: 'file' }, { pattern: /conversation\.(?:tsx?|jsx?)$/i, type: 'file' }, // Import patterns { pattern: /import.*[Cc]hat/, type: 'import' }, { pattern: /from\s+['"].*chat/, type: 'import' }, // Component usage { pattern: /<[Cc]hat\w*\s*\/?>/, type: 'jsx' }, { pattern: /use[Cc]hat/, type: 'hook' } ]; const files = await this.getAllSourceFiles(projectPath); const foundFiles = new Set(); for (const file of files) { try { const content = await fs.readFile(file, 'utf-8'); for (const indicator of chatIndicators) { if (indicator.pattern.test(content)) { const relativePath = path.relative(projectPath, file); if (!foundFiles.has(relativePath)) { foundFiles.add(relativePath); conflicts.push({ type: 'existing_chat', severity: 'warning', file: relativePath, pattern: indicator.pattern.toString(), indicatorType: indicator.type, message: `Found existing chat implementation in ${relativePath}`, resolution: 'Consider removing existing chat or integrating Embedia alongside' }); } break; // Only report once per file } } } catch (error) { // Ignore read errors } } return conflicts; } async scanForAPIRoutes(projectPath) { const conflicts = []; const apiRoutes = [ { path: 'app/api/chat/route.ts', type: 'app-router' }, { path: 'app/api/chat/route.js', type: 'app-router' }, { path: 'src/app/api/chat/route.ts', type: 'app-router' }, { path: 'src/app/api/chat/route.js', type: 'app-router' }, { path: 'app/api/embedia/route.ts', type: 'app-router' }, { path: 'app/api/embedia/route.js', type: 'app-router' }, { path: 'pages/api/chat.ts', type: 'pages-router' }, { path: 'pages/api/chat.js', type: 'pages-router' }, { path: 'pages/api/embedia.ts', type: 'pages-router' }, { path: 'pages/api/embedia.js', type: 'pages-router' }, { path: 'src/pages/api/chat.ts', type: 'pages-router' }, { path: 'src/pages/api/chat.js', type: 'pages-router' } ]; for (const route of apiRoutes) { const fullPath = path.join(projectPath, route.path); if (await fs.pathExists(fullPath)) { conflicts.push({ type: 'api_route_conflict', severity: 'error', file: route.path, routerType: route.type, message: `API route conflict: ${route.path} already exists`, resolution: 'Rename existing route or configure Embedia to use different endpoint' }); } } return conflicts; } async scanForGlobalStyles(projectPath) { const conflicts = []; const styleFiles = await glob('**/{globals,global,app}.{css,scss,sass}', { cwd: projectPath, ignore: ['node_modules/**', '.next/**', 'build/**'] }); for (const file of styleFiles) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); // Check for potentially conflicting global styles const problematicPatterns = [ { pattern: /\*\s*{[^}]*position:\s*fixed/s, issue: 'Global fixed positioning', severity: 'warning' }, { pattern: /body\s*{[^}]*overflow:\s*hidden/s, issue: 'Body overflow hidden', severity: 'warning' }, { pattern: /z-index:\s*(9{4,}|[0-9]{5,})/i, issue: 'Very high z-index values', severity: 'warning' }, { pattern: /\.(chat|message|conversation)[^{]*{/i, issue: 'Chat-related class names', severity: 'error' } ]; for (const { pattern, issue, severity } of problematicPatterns) { if (pattern.test(content)) { conflicts.push({ type: 'style_conflict', severity: severity, file: file, issue: issue, message: `Potential style conflict in ${file}: ${issue}`, resolution: 'Embedia uses scoped styles but global styles might interfere' }); } } } catch (error) { // Ignore read errors } } return conflicts; } async scanForPortConflicts(projectPath) { const conflicts = []; // Check if dev server ports are in use const devPorts = [3456, 3457]; // Embedia dev server ports // Check package.json scripts for port usage try { const packageJson = await fs.readJson(path.join(projectPath, 'package.json')); const scripts = packageJson.scripts || {}; for (const [name, script] of Object.entries(scripts)) { for (const port of devPorts) { if (script.includes(`${port}`)) { conflicts.push({ type: 'port_conflict', severity: 'info', script: name, port: port, message: `Port ${port} may be in use by script "${name}"`, resolution: 'Embedia dev server will use alternative ports if needed' }); } } } } catch (error) { // Ignore if package.json doesn't exist } return conflicts; } async scanForDependencyConflicts(projectPath) { const conflicts = []; try { const packageJson = await fs.readJson(path.join(projectPath, 'package.json')); const dependencies = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Check for conflicting versions const criticalDeps = { react: { min: '18.0.0', reason: 'Embedia requires React 18+ for createRoot API' }, 'react-dom': { min: '18.0.0', reason: 'Embedia requires ReactDOM 18+ for createRoot API' } }; for (const [dep, requirement] of Object.entries(criticalDeps)) { if (dependencies[dep]) { const version = dependencies[dep].replace(/[\^~]/, ''); const major = parseInt(version.split('.')[0]); const requiredMajor = parseInt(requirement.min.split('.')[0]); if (major < requiredMajor) { conflicts.push({ type: 'dependency_conflict', severity: 'error', dependency: dep, currentVersion: version, requiredVersion: requirement.min, message: `${dep} version ${version} is too old. ${requirement.reason}`, resolution: `Update ${dep} to version ${requirement.min} or higher` }); } } } } catch (error) { // Ignore if package.json doesn't exist } return conflicts; } async scanForEnvVarConflicts(projectPath) { const conflicts = []; const envFiles = ['.env', '.env.local', '.env.development', '.env.production']; for (const envFile of envFiles) { const envPath = path.join(projectPath, envFile); if (await fs.pathExists(envPath)) { try { const content = await fs.readFile(envPath, 'utf-8'); // Check if API keys are already defined const apiKeys = ['OPENAI_API_KEY', 'GEMINI_API_KEY', 'ANTHROPIC_API_KEY']; for (const key of apiKeys) { if (content.includes(`${key}=`)) { conflicts.push({ type: 'env_var_conflict', severity: 'info', file: envFile, variable: key, message: `${key} already defined in ${envFile}`, resolution: 'Existing API key will be used, no action needed' }); } } } catch (error) { // Ignore read errors } } } return conflicts; } async getAllSourceFiles(projectPath) { const patterns = ['**/*.{js,jsx,ts,tsx}']; const ignorePatterns = [ 'node_modules/**', '.next/**', 'build/**', 'dist/**', 'coverage/**', '.git/**' ]; const files = []; for (const pattern of patterns) { const matches = await glob(pattern, { cwd: projectPath, ignore: ignorePatterns, absolute: true }); files.push(...matches); } return files; } prioritizeConflicts(conflicts) { // Sort by severity: error > warning > info const severityOrder = { error: 0, warning: 1, info: 2 }; return conflicts.sort((a, b) => { return severityOrder[a.severity] - severityOrder[b.severity]; }); } } module.exports = ConflictScanner;