UNPKG

embedia

Version:

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

263 lines (224 loc) 8.81 kB
const fs = require('fs-extra'); const path = require('path'); const { glob } = require('glob'); class ProjectAnalyzer { async analyzeProject(projectPath) { const analysis = { framework: await this.detectFramework(projectPath), routerType: await this.detectRouterType(projectPath), structure: await this.mapProjectStructure(projectPath), styling: await this.detectStylingSystem(projectPath), packageManager: await this.detectPackageManager(projectPath), typescript: await this.isTypeScriptProject(projectPath), conflicts: await this.detectPotentialConflicts(projectPath), reactVersion: await this.analyzeReactSetup(projectPath) }; return analysis; } async readPackageJson(projectPath) { try { const packageJsonPath = path.join(projectPath, 'package.json'); if (await fs.pathExists(packageJsonPath)) { return await fs.readJson(packageJsonPath); } } catch (error) { console.error('Error reading package.json:', error); } return {}; } async detectFramework(projectPath) { const packageJson = await this.readPackageJson(projectPath); if (packageJson.dependencies?.next || packageJson.devDependencies?.next) { const version = packageJson.dependencies?.next || packageJson.devDependencies?.next; return { name: 'next', version: version.replace(/[\^~]/, ''), majorVersion: parseInt(version.match(/\d+/)[0]) }; } // Check for other frameworks if (packageJson.dependencies?.gatsby) return { name: 'gatsby', version: packageJson.dependencies.gatsby }; if (packageJson.dependencies?.['@remix-run/react']) return { name: 'remix', version: packageJson.dependencies['@remix-run/react'] }; if (packageJson.dependencies?.nuxt) return { name: 'nuxt', version: packageJson.dependencies.nuxt }; if (packageJson.dependencies?.vite && packageJson.dependencies?.react) return { name: 'vite', version: packageJson.dependencies.vite }; if (packageJson.dependencies?.['react-scripts']) return { name: 'create-react-app', version: packageJson.dependencies['react-scripts'] }; // Check for plain React if (packageJson.dependencies?.react) return { name: 'react', version: packageJson.dependencies.react }; return { name: 'unknown', version: null }; } async detectRouterType(projectPath) { // For Next.js projects const hasAppDir = await fs.pathExists(path.join(projectPath, 'app')) || await fs.pathExists(path.join(projectPath, 'src/app')); const hasPagesDir = await fs.pathExists(path.join(projectPath, 'pages')) || await fs.pathExists(path.join(projectPath, 'src/pages')); if (hasAppDir && !hasPagesDir) return 'app'; if (!hasAppDir && hasPagesDir) return 'pages'; if (hasAppDir && hasPagesDir) return 'hybrid'; return 'unknown'; } async detectStylingSystem(projectPath) { const systems = { tailwind: false, cssModules: false, styledComponents: false, emotion: false, sass: false, cssInJs: false }; // Check for Tailwind if (await fs.pathExists(path.join(projectPath, 'tailwind.config.js')) || await fs.pathExists(path.join(projectPath, 'tailwind.config.ts'))) { systems.tailwind = true; } // Check package.json for styling libraries const packageJson = await this.readPackageJson(projectPath); if (packageJson.dependencies?.['styled-components']) systems.styledComponents = true; if (packageJson.dependencies?.['@emotion/react']) systems.emotion = true; if (packageJson.dependencies?.sass || packageJson.devDependencies?.sass) systems.sass = true; // Check for CSS modules by looking at import patterns const hasModules = await this.searchForPattern(projectPath, /\.module\.(css|scss|sass)/); if (hasModules.length > 0) systems.cssModules = true; return systems; } async detectPackageManager(projectPath) { if (await fs.pathExists(path.join(projectPath, 'yarn.lock'))) return 'yarn'; if (await fs.pathExists(path.join(projectPath, 'pnpm-lock.yaml'))) return 'pnpm'; if (await fs.pathExists(path.join(projectPath, 'bun.lockb'))) return 'bun'; return 'npm'; } async isTypeScriptProject(projectPath) { const hasTsConfig = await fs.pathExists(path.join(projectPath, 'tsconfig.json')); const packageJson = await this.readPackageJson(projectPath); const hasTypeScript = !!(packageJson.devDependencies?.typescript || packageJson.dependencies?.typescript); return { isTypeScript: hasTsConfig || hasTypeScript, hasTsConfig, version: packageJson.devDependencies?.typescript || packageJson.dependencies?.typescript }; } async mapProjectStructure(projectPath) { const structure = { srcDir: await fs.pathExists(path.join(projectPath, 'src')), componentsPath: null, apiPath: null, publicPath: null }; // Find components directory const componentPaths = ['components', 'src/components']; for (const compPath of componentPaths) { if (await fs.pathExists(path.join(projectPath, compPath))) { structure.componentsPath = compPath; break; } } // Find API directory const apiPaths = ['app/api', 'src/app/api', 'pages/api', 'src/pages/api']; for (const apiPath of apiPaths) { if (await fs.pathExists(path.join(projectPath, apiPath))) { structure.apiPath = apiPath; break; } } // Find public directory if (await fs.pathExists(path.join(projectPath, 'public'))) { structure.publicPath = 'public'; } return structure; } async searchForPattern(projectPath, pattern) { const files = await glob('**/*.{js,jsx,ts,tsx}', { cwd: projectPath, ignore: ['node_modules/**', '.next/**', 'build/**', 'dist/**'] }); const matches = []; for (const file of files) { try { const content = await fs.readFile(path.join(projectPath, file), 'utf-8'); if (pattern.test(content)) { matches.push(file); } } catch (error) { // Ignore read errors } } return matches; } async detectPotentialConflicts(projectPath) { const conflicts = []; // Check for existing chat implementations const chatPatterns = [ /import.*[Cc]hat/, /[Cc]hatbot/, /[Mm]essag(e|ing)/, /[Cc]onversation/ ]; for (const pattern of chatPatterns) { const files = await this.searchForPattern(projectPath, pattern); if (files.length > 0) { conflicts.push({ type: 'existing_chat', severity: 'warning', message: 'Found existing chat implementation', files: files.slice(0, 5) // Limit to first 5 matches }); } } // Check for API route conflicts const apiPaths = [ 'app/api/chat', 'src/app/api/chat', 'pages/api/chat', 'app/api/embedia', 'pages/api/embedia' ]; for (const apiPath of apiPaths) { if (await fs.pathExists(path.join(projectPath, apiPath))) { conflicts.push({ type: 'api_route_conflict', severity: 'error', message: `API route already exists at ${apiPath}`, path: apiPath }); } } // Check for global style conflicts const globalStyleFiles = ['globals.css', 'global.css', 'app.css']; for (const styleFile of globalStyleFiles) { const stylePath = path.join(projectPath, 'styles', styleFile); if (await fs.pathExists(stylePath)) { const content = await fs.readFile(stylePath, 'utf-8'); if (content.includes('position: fixed') && content.includes('z-index')) { conflicts.push({ type: 'style_conflict', severity: 'warning', message: 'Global styles may conflict with chat widget', file: `styles/${styleFile}` }); } } } return conflicts; } async analyzeReactSetup(projectPath) { const packageJson = await this.readPackageJson(projectPath); const react = packageJson.dependencies?.react || packageJson.devDependencies?.react; if (!react) { return { hasReact: false, requiresInstall: true }; } const version = react.replace(/[\^~]/, ''); const major = parseInt(version.split('.')[0]); return { hasReact: true, version: version, majorVersion: major, isCompatible: major >= 18, usesNewJSXTransform: major >= 17, requiresInstall: false }; } } module.exports = ProjectAnalyzer;