UNPKG

embedia

Version:

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

711 lines (598 loc) 21.4 kB
const fs = require('fs-extra'); const path = require('path'); const { execSync } = require('child_process'); const logger = require('../utils/logger'); /** * ValidationService - Tests integration success and provides feedback * Ensures the chatbot actually works after CLI integration */ class ValidationService { constructor() { this.validationResults = { passed: [], failed: [], warnings: [], score: 0, maxScore: 0 }; } /** * Run comprehensive validation of the integration * @param {string} projectDirectory - Project directory * @param {Object} projectInfo - Project information * @param {Object} integrationResults - Results from auto-integrator * @returns {Promise<Object>} Validation results */ async validateIntegration(projectDirectory, projectInfo, integrationResults) { try { logger.info('🔍 Validating chatbot integration...'); this.projectDirectory = projectDirectory; this.projectInfo = projectInfo; // Run validation tests await this.validateFileStructure(); await this.validateImports(); await this.validateApiEndpoint(); await this.validateEnvironment(); await this.validateDependencies(); await this.validateConfiguration(); await this.validateBuildProcess(); // Calculate final score this.calculateScore(); // Generate recommendations const recommendations = this.generateRecommendations(); logger.success(`✅ Validation completed - Score: ${this.validationResults.score}/${this.validationResults.maxScore}`); return { ...this.validationResults, recommendations, success: this.validationResults.score >= this.validationResults.maxScore * 0.8 }; } catch (error) { this.validationResults.failed.push({ test: 'validation_process', error: error.message }); throw error; } } /** * Validate file structure */ async validateFileStructure() { const tests = [ { name: 'Components directory exists', check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components)) }, { name: 'Chatbot component exists', check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/Chatbot.tsx')) }, { name: 'Chat hook exists', check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/useChat.ts')) }, { name: 'Types file exists', check: () => fs.pathExists(path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/types.ts')) }, { name: 'API endpoint exists', check: () => this.checkApiEndpoint() } ]; await this.runTests('File Structure', tests); } /** * Check if API endpoint exists in correct location */ async checkApiEndpoint() { try { const { routerType } = this.projectInfo; const apiPath = this.projectInfo.paths?.api || 'api'; if (routerType === 'app' || routerType === 'hybrid') { return fs.pathExists(path.join(this.projectDirectory, apiPath, 'chat/route.ts')); } else if (routerType === 'pages') { return fs.pathExists(path.join(this.projectDirectory, apiPath, 'chat.ts')); } else { // For other project types, check for general API endpoint return fs.pathExists(path.join(this.projectDirectory, apiPath, 'chat.ts')); } } catch (error) { logger.warn(`API endpoint check failed: ${error.message}`); return false; } } /** * Validate imports and syntax */ async validateImports() { const tests = [ { name: 'Chatbot component syntax valid', check: () => this.validateFileContent( path.join(this.projectInfo.paths.components, 'AutoChat/Chatbot.tsx'), ['import React', 'export default'] ) }, { name: 'Hook imports are correct', check: () => this.validateHookImports() }, { name: 'Layout integration exists', check: () => this.validateLayoutIntegration() } ]; await this.runTests('Import Validation', tests); } /** * Validate hook imports with flexible import pattern matching */ async validateHookImports() { const hookPath = path.join(this.projectDirectory, this.projectInfo.paths.components, 'AutoChat/useChat.ts'); if (!(await fs.pathExists(hookPath))) { return false; } const content = await fs.readFile(hookPath, 'utf8'); // More flexible React import validation const hasReactImports = content.includes("import") && content.includes("react") || content.includes("useState") || content.includes("useEffect") || content.includes("useCallback") || content.includes("useRef"); // Check for type imports (supports both relative and absolute imports) const hasTypeImports = content.includes("import") && content.includes("types") || content.includes("Message") || content.includes("interface") || content.includes("type Message"); // Check for proper export (either named or default) const hasValidExport = content.includes('export const useChat') || content.includes('export default') || content.includes('export function useChat') || content.includes('export { useChat }'); // Check that essential hook functionality exists const hasHookLogic = content.includes('useState') && content.includes('fetch') && (content.includes('/api/chat') || content.includes('api/chat')); return hasReactImports && hasValidExport && hasHookLogic; } /** * Validate layout integration */ async validateLayoutIntegration() { if (!this.projectInfo.paths.layout) { return false; } const layoutPath = path.join(this.projectDirectory, this.projectInfo.paths.layout); if (!(await fs.pathExists(layoutPath))) { return false; } const content = await fs.readFile(layoutPath, 'utf8'); // Check for chatbot import and component return content.includes('AutoChat/Chatbot') && content.includes('<Chatbot'); } /** * Validate API endpoint functionality */ async validateApiEndpoint() { const tests = [ { name: 'API route file is valid JavaScript/TypeScript', check: () => this.validateApiSyntax() }, { name: 'API route handles POST requests', check: () => this.validateApiContent() }, { name: 'API route uses correct imports', check: () => this.validateApiImports() } ]; await this.runTests('API Validation', tests); } /** * Validate API syntax */ async validateApiSyntax() { const apiPath = this.getApiPath(); if (!(await fs.pathExists(path.join(this.projectDirectory, apiPath)))) { return false; } const content = await fs.readFile(path.join(this.projectDirectory, apiPath), 'utf8'); // Basic syntax checks return content.includes('export') && (content.includes('POST') || content.includes('req.method')) && !this.hasSyntaxErrors(content); } /** * Get API path based on router type */ getApiPath() { const { routerType, type } = this.projectInfo; let apiPath = this.projectInfo.paths?.api || 'api'; // For scaffolded App Router projects, ensure correct API path if (type === 'empty' && routerType === 'app') { apiPath = 'src/app/api'; } if (routerType === 'app' || routerType === 'hybrid') { return path.join(apiPath, 'chat/route.ts'); } else { return path.join(apiPath, 'chat.ts'); } } /** * Basic syntax error detection */ hasSyntaxErrors(content) { const commonErrors = [ 'SyntaxError', 'Unexpected token', 'Unexpected end of input', 'Missing closing', 'Unclosed' ]; return commonErrors.some(error => content.includes(error)); } /** * Validate API content */ async validateApiContent() { const apiPath = path.join(this.projectDirectory, this.getApiPath()); if (!(await fs.pathExists(apiPath))) { return false; } const content = await fs.readFile(apiPath, 'utf8'); // Check for different router types const { routerType } = this.projectInfo; if (routerType === 'pages') { // Pages Router validation - more flexible const hasHandler = content.includes('handler') || content.includes('export default'); const hasPostMethod = content.includes('req.method') && content.includes('POST'); const hasAiIntegration = content.includes('@google/generative-ai') || content.includes('GoogleGenerativeAI') || content.includes('genai'); const hasResponse = content.includes('res.status') || content.includes('res.json'); return hasHandler && hasPostMethod && hasAiIntegration && hasResponse; } else { // App Router validation - more flexible for async patterns const hasPostExport = content.includes('export async function POST') || content.includes('export function POST'); const hasRequestParam = content.includes('request') || content.includes('Request'); const hasAiIntegration = content.includes('@google/generative-ai') || content.includes('GoogleGenerativeAI') || content.includes('genai'); const hasResponseJson = content.includes('Response.json') || content.includes('new Response') || content.includes('json()'); const hasAsyncAwait = content.includes('async') && content.includes('await'); return hasPostExport && hasRequestParam && hasAiIntegration && (hasResponseJson || hasAsyncAwait); } } /** * Validate API imports */ async validateApiImports() { const apiPath = path.join(this.projectDirectory, this.getApiPath()); if (!(await fs.pathExists(apiPath))) { return false; } const content = await fs.readFile(apiPath, 'utf8'); // Check for correct Google AI package import return content.includes('@google/generative-ai') || content.includes('GoogleGenerativeAI') || content.includes('genai'); } /** * Validate environment setup */ async validateEnvironment() { const tests = [ { name: '.env.local exists', check: () => fs.pathExists(path.join(this.projectDirectory, '.env.local')) }, { name: 'API key is configured', check: () => this.checkApiKeyExists() }, { name: 'Public variables are set', check: () => this.checkPublicVars() }, { name: '.gitignore excludes .env.local', check: () => this.checkGitignore() } ]; await this.runTests('Environment', tests); } /** * Check if API key exists in .env.local */ async checkApiKeyExists() { return await this.validateApiKeyConfiguration(); } /** * Enhanced API key validation - checks multiple env files and providers */ async validateApiKeyConfiguration() { const envPaths = ['.env.local', '.env', '.env.example']; for (const envFile of envPaths) { const envPath = path.join(this.projectDirectory, envFile); if (await fs.pathExists(envPath)) { const content = await fs.readFile(envPath, 'utf8'); // Check for key existence (not just placeholder) const hasGeminiKey = content.includes('GEMINI_API_KEY=') && !content.includes('your_gemini_api_key_here'); const hasOpenaiKey = content.includes('OPENAI_API_KEY=') && !content.includes('your_openai_api_key_here'); if (hasGeminiKey || hasOpenaiKey) { return true; } } } return false; // Only fail if no valid keys found } /** * Check public environment variables */ async checkPublicVars() { const envPath = path.join(this.projectDirectory, '.env.local'); if (!(await fs.pathExists(envPath))) { return false; } const content = await fs.readFile(envPath, 'utf8'); return content.includes('NEXT_PUBLIC_CHATBOT_ENABLED'); } /** * Check if .gitignore excludes .env.local */ async checkGitignore() { const gitignorePath = path.join(this.projectDirectory, '.gitignore'); if (!(await fs.pathExists(gitignorePath))) { return true; // Not a git project, that's ok } const content = await fs.readFile(gitignorePath, 'utf8'); return content.includes('.env.local'); } /** * Validate dependencies */ async validateDependencies() { const tests = [ { name: 'package.json exists', check: () => fs.pathExists(path.join(this.projectDirectory, 'package.json')) }, { name: 'Required dependencies installed', check: () => this.checkRequiredDependencies() }, { name: 'node_modules exists', check: () => fs.pathExists(path.join(this.projectDirectory, 'node_modules')) } ]; await this.runTests('Dependencies', tests); } /** * Check required dependencies */ async checkRequiredDependencies() { const packageJsonPath = path.join(this.projectDirectory, 'package.json'); if (!(await fs.pathExists(packageJsonPath))) { return false; } const packageJson = await fs.readJson(packageJsonPath); const allDeps = { ...packageJson.dependencies, ...packageJson.devDependencies }; // Check for AI dependencies const requiredAiDeps = ['@google/generative-ai', '@google/genai', 'openai']; const hasAiDep = requiredAiDeps.some(dep => allDeps[dep]); if (!hasAiDep) { return false; } // Verify node_modules existence (indicates successful install) const nodeModulesPath = path.join(this.projectDirectory, 'node_modules'); const nodeModulesExists = await fs.pathExists(nodeModulesPath); return nodeModulesExists; } /** * Validate configuration files */ async validateConfiguration() { const tests = [ { name: 'TypeScript configuration', check: () => this.validateTypeScriptConfig() }, { name: 'Tailwind configuration', check: () => this.validateTailwindConfig() } ]; await this.runTests('Configuration', tests); } /** * Validate TypeScript configuration */ async validateTypeScriptConfig() { if (!this.projectInfo.config.typescript) { return true; // Not a TypeScript project } const tsConfigPath = path.join(this.projectDirectory, 'tsconfig.json'); if (!(await fs.pathExists(tsConfigPath))) { return false; } const tsConfig = await fs.readJson(tsConfigPath); const includes = tsConfig.include || []; return includes.some(include => include.includes(this.projectInfo.paths.components)); } /** * Validate Tailwind configuration */ async validateTailwindConfig() { if (!this.projectInfo.config.tailwind) { return true; // Not using Tailwind } const configFiles = ['tailwind.config.js', 'tailwind.config.ts', 'tailwind.config.mjs']; for (const configFile of configFiles) { const configPath = path.join(this.projectDirectory, configFile); if (await fs.pathExists(configPath)) { const content = await fs.readFile(configPath, 'utf8'); return content.includes(this.projectInfo.paths.components); } } return false; } /** * Validate build process */ async validateBuildProcess() { const tests = [ { name: 'Project can be built', check: () => this.testBuild() } ]; await this.runTests('Build Process', tests, false); // Non-critical } /** * Test if project can build (optional, may be slow) */ async testBuild() { try { // This is optional and may be slow, so we'll skip it for now // execSync('npm run build', { cwd: this.projectDirectory, stdio: 'pipe' }); return true; } catch (error) { return false; } } /** * Validate file content */ async validateFileContent(relativePath, requiredStrings) { const fullPath = path.join(this.projectDirectory, relativePath); if (!(await fs.pathExists(fullPath))) { return false; } const content = await fs.readFile(fullPath, 'utf8'); return requiredStrings.every(str => content.includes(str)); } /** * Run a set of tests */ async runTests(category, tests, critical = true) { logger.info(`🔍 Testing: ${category}`); for (const test of tests) { try { const result = await test.check(); if (result) { this.validationResults.passed.push({ category, test: test.name, critical }); logger.debug(`✅ ${test.name}`); } else { const failure = { category, test: test.name, critical }; if (critical) { this.validationResults.failed.push(failure); logger.warn(`❌ ${test.name}`); } else { this.validationResults.warnings.push(failure); logger.debug(`⚠️ ${test.name}`); } } this.validationResults.maxScore++; if (result) { this.validationResults.score++; } } catch (error) { this.validationResults.failed.push({ category, test: test.name, error: error.message, critical }); this.validationResults.maxScore++; logger.error(`❌ ${test.name}: ${error.message}`); } } } /** * Calculate final validation score */ calculateScore() { const { passed, failed, warnings } = this.validationResults; // Weight critical tests more heavily const criticalPassed = passed.filter(p => p.critical !== false).length; const criticalFailed = failed.filter(f => f.critical !== false).length; const criticalTotal = criticalPassed + criticalFailed; if (criticalTotal === 0) { this.validationResults.score = this.validationResults.maxScore; return; } // Critical tests are 80% of score, non-critical are 20% const criticalScore = (criticalPassed / criticalTotal) * 0.8; const nonCriticalScore = ((passed.length - criticalPassed) / Math.max(1, this.validationResults.maxScore - criticalTotal)) * 0.2; this.validationResults.score = Math.round((criticalScore + nonCriticalScore) * this.validationResults.maxScore); } /** * Generate recommendations based on validation results */ generateRecommendations() { const recommendations = []; const { failed, warnings } = this.validationResults; // Critical failures failed.forEach(failure => { if (failure.category === 'File Structure') { recommendations.push(`📁 ${failure.test} - Check file placement and naming`); } else if (failure.category === 'Environment') { recommendations.push(`🔑 ${failure.test} - Review environment variable setup`); } else if (failure.category === 'Dependencies') { recommendations.push(`📦 ${failure.test} - Run 'npm install' to install missing packages`); } else if (failure.category === 'API Validation') { recommendations.push(`🚀 ${failure.test} - Check API endpoint implementation`); } else { recommendations.push(`⚠️ ${failure.test} - Requires manual attention`); } }); // Add general recommendations if (this.validationResults.score < this.validationResults.maxScore) { recommendations.push('🔍 Run integration again if issues persist'); recommendations.push('📖 Check the integration guide for manual steps'); } if (this.validationResults.score >= this.validationResults.maxScore * 0.8) { recommendations.push('🎉 Integration looks good! Try running: npm run dev'); } return recommendations; } /** * Get validation summary */ getSummary() { const { passed, failed, warnings, score, maxScore } = this.validationResults; return { success: score >= maxScore * 0.8, score: `${score}/${maxScore}`, percentage: Math.round((score / maxScore) * 100), counts: { passed: passed.length, failed: failed.length, warnings: warnings.length }, status: score >= maxScore * 0.9 ? 'excellent' : score >= maxScore * 0.8 ? 'good' : score >= maxScore * 0.6 ? 'needs work' : 'poor' }; } } module.exports = ValidationService;