// ============================================================================ // Agent 1: Project Scanner // Scans and maps the complete project structure // ============================================================================ import * as path from 'path'; import { AgentState, ProjectStructure, FrameworkInfo, ProjectFile, TestRunner, E2ERunner, CoverageTool, } from '../types'; import { scanDirectory, getDirectories, findEntryPoints, findConfigFiles, detectPackageManager, loadPackageJson, countLines, } from '../utils/file-utils'; import { logger } from '../utils/logger'; export async function scannerAgent(state: AgentState): Promise> { const startTime = Date.now(); logger.agentStart('scanner'); try { const projectPath = state.projectPath; logger.agent('scanner', 'Scanning project directory', projectPath); // 1. Scan all files const files = scanDirectory(projectPath); logger.agent('scanner', `${files.length} files found`); // 2. Get directory structure const directories = getDirectories(projectPath); logger.agent('scanner', `${directories.length} directories found`); // 3. Find entry points const entryPoints = findEntryPoints(projectPath); logger.agent('scanner', `${entryPoints.length} entry points found`, entryPoints.join(', ')); // 4. Find config files const configFiles = findConfigFiles(projectPath); logger.agent('scanner', `${configFiles.length} configuration files`, configFiles.join(', ')); // 5. Detect package manager const packageManager = detectPackageManager(projectPath); logger.agent('scanner', 'Package Manager', packageManager); // 6. Detect framework const framework = detectFramework(projectPath); logger.agent('scanner', 'Framework detected', `${framework.name} (${framework.type})`); // 7. Count lines and languages const sourceFiles = files.filter(f => f.type === 'source'); let totalLines = 0; const languages: Record = {}; for (const file of sourceFiles) { const lines = countLines(file.path); totalLines += lines; languages[file.language] = (languages[file.language] || 0) + 1; } logger.agent('scanner', `${totalLines} lines of code in ${sourceFiles.length} source files`); // 8. Build structure const pkg = loadPackageJson(projectPath); const projectStructure: ProjectStructure = { rootPath: projectPath, name: pkg?.name || path.basename(projectPath), framework, packageManager, files, directories, entryPoints, configFiles, totalFiles: files.length, totalLines, languages, }; // Log summary logger.table( ['Category', 'Count'], [ ['Source files', String(sourceFiles.length)], ['Test files', String(files.filter(f => f.type === 'test').length)], ['Configuration', String(files.filter(f => f.type === 'config').length)], ['Code lines', String(totalLines)], ['Directories', String(directories.length)], ] ); // Auto-detect test tooling from project if not explicitly configured const autoConfig: Partial = {}; if (framework.testRunner) { autoConfig.testRunner = framework.testRunner; logger.agent('scanner', 'Test Runner detected', framework.testRunner); } if (framework.e2eRunner) { autoConfig.e2eRunner = framework.e2eRunner; logger.agent('scanner', 'E2E Runner detected', framework.e2eRunner); } if (framework.coverageTool) { autoConfig.coverageTool = framework.coverageTool; logger.agent('scanner', 'Coverage Tool detected', framework.coverageTool); } // Merge auto-detected config (only if user hasn't explicitly set via CLI/env) const updatedConfig = { ...state.config, // Only override if the current value is the default testRunner: state.config.testRunner === 'vitest' && framework.testRunner ? framework.testRunner : state.config.testRunner, e2eRunner: state.config.e2eRunner === 'playwright' && framework.e2eRunner ? framework.e2eRunner : state.config.e2eRunner, coverageTool: state.config.coverageTool === 'v8' && framework.coverageTool ? framework.coverageTool : state.config.coverageTool, }; logger.agentComplete('scanner', Date.now() - startTime); return { projectStructure, config: updatedConfig, agentLog: [ ...state.agentLog, { agent: 'scanner', timestamp: new Date().toISOString(), action: 'Project scanned', details: `${files.length} files, ${totalLines} lines, Framework: ${framework.name}`, duration: Date.now() - startTime, status: 'complete', }, ], }; } catch (error) { const errMsg = error instanceof Error ? error.message : String(error); logger.agentError('scanner', errMsg); return { errors: [...state.errors, `Scanner: ${errMsg}`], agentLog: [ ...state.agentLog, { agent: 'scanner', timestamp: new Date().toISOString(), action: 'Error', details: errMsg, status: 'error', }, ], }; } } function detectFramework(projectPath: string): FrameworkInfo { const pkg = loadPackageJson(projectPath); if (!pkg) { return { name: 'unknown', version: '', type: 'unknown', features: [] }; } const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; const depNames = Object.keys(allDeps); // Detect framework const frameworks: { name: string; packages: string[]; type: FrameworkInfo['type'] }[] = [ { name: 'Next.js', packages: ['next'], type: 'fullstack' }, { name: 'NestJS', packages: ['@nestjs/core'], type: 'backend' }, { name: 'Express', packages: ['express'], type: 'backend' }, { name: 'Fastify', packages: ['fastify'], type: 'backend' }, { name: 'Koa', packages: ['koa'], type: 'backend' }, { name: 'Hapi', packages: ['@hapi/hapi'], type: 'backend' }, { name: 'React', packages: ['react'], type: 'frontend' }, { name: 'Vue', packages: ['vue'], type: 'frontend' }, { name: 'Angular', packages: ['@angular/core'], type: 'frontend' }, { name: 'Svelte', packages: ['svelte'], type: 'frontend' }, { name: 'Nuxt', packages: ['nuxt'], type: 'fullstack' }, { name: 'Remix', packages: ['@remix-run/node'], type: 'fullstack' }, { name: 'Electron', packages: ['electron'], type: 'fullstack' }, ]; let detectedFramework: FrameworkInfo = { name: 'Node.js', version: '', type: 'library', features: [] }; for (const fw of frameworks) { if (fw.packages.some(p => depNames.includes(p))) { detectedFramework = { name: fw.name, version: allDeps[fw.packages[0]] || '', type: fw.type, features: [], }; break; } } // Detect test framework & test runner const testFrameworks = ['vitest', 'jest', 'mocha', 'ava', 'tap', 'jasmine']; detectedFramework.testFramework = testFrameworks.find(t => depNames.includes(t)); // Detect test runner (preferred order: vitest > jest > node:test) if (depNames.includes('vitest')) { detectedFramework.testRunner = 'vitest'; } else if (depNames.includes('jest') || depNames.includes('ts-jest')) { detectedFramework.testRunner = 'jest'; } else { // Default to vitest for new projects (fastest, best TS support) detectedFramework.testRunner = 'vitest'; } // Detect E2E runner if (depNames.includes('@playwright/test') || depNames.includes('playwright')) { detectedFramework.e2eRunner = 'playwright'; } else if (depNames.includes('supertest')) { detectedFramework.e2eRunner = 'supertest'; } else { // Default: playwright for frontend/fullstack, supertest for backend detectedFramework.e2eRunner = ['frontend', 'fullstack'].includes(detectedFramework.type) ? 'playwright' : 'supertest'; } // Detect coverage tool if (depNames.includes('c8')) { detectedFramework.coverageTool = 'c8'; } else if (depNames.includes('nyc') || depNames.includes('istanbul')) { detectedFramework.coverageTool = 'istanbul'; } else { // Default: v8 (native, fastest) detectedFramework.coverageTool = 'v8'; } // Detect build tool const buildTools = ['vite', 'webpack', 'rollup', 'esbuild', 'parcel', 'turbo', 'swc']; detectedFramework.buildTool = buildTools.find(t => depNames.includes(t)); // Detect features const features: string[] = []; if (depNames.includes('typescript')) features.push('TypeScript'); if (depNames.includes('prisma') || depNames.includes('@prisma/client')) features.push('Prisma'); if (depNames.includes('typeorm')) features.push('TypeORM'); if (depNames.includes('mongoose')) features.push('Mongoose'); if (depNames.includes('sequelize')) features.push('Sequelize'); if (depNames.includes('graphql') || depNames.includes('@apollo/server')) features.push('GraphQL'); if (depNames.includes('socket.io')) features.push('WebSockets'); if (depNames.includes('redis') || depNames.includes('ioredis')) features.push('Redis'); if (depNames.includes('passport')) features.push('Passport Auth'); if (depNames.includes('jsonwebtoken') || depNames.includes('jose')) features.push('JWT'); if (depNames.includes('bcrypt') || depNames.includes('bcryptjs')) features.push('Password Hashing'); if (depNames.includes('helmet')) features.push('Helmet Security'); if (depNames.includes('cors')) features.push('CORS'); if (depNames.includes('joi') || depNames.includes('zod') || depNames.includes('yup')) features.push('Validation'); if (depNames.includes('winston') || depNames.includes('pino')) features.push('Logging'); if (depNames.includes('bull') || depNames.includes('bullmq')) features.push('Job Queue'); if (depNames.includes('swagger-ui-express') || depNames.includes('@nestjs/swagger')) features.push('Swagger/OpenAPI'); if (depNames.includes('@playwright/test') || depNames.includes('playwright')) features.push('Playwright'); if (depNames.includes('vitest')) features.push('Vitest'); if (depNames.includes('@stryker-mutator/core')) features.push('Stryker Mutation Testing'); if (depNames.includes('c8')) features.push('c8 Coverage'); if (depNames.includes('eslint-plugin-security')) features.push('ESLint Security'); if (depNames.includes('pactum')) features.push('PactumJS'); detectedFramework.features = features; return detectedFramework; }