embedia
Version:
Zero-configuration AI chatbot integration CLI - direct file copy with embedded API keys
308 lines (266 loc) • 9.82 kB
JavaScript
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;