UNPKG

@vibe-dev-kit/cli

Version:

Advanced Command-line toolkit that analyzes your codebase and deploys project-aware rules, memories, commands and agents to any AI coding assistant - VDK is the world's first Vibe Development Kit

881 lines (768 loc) 28.8 kB
/** * TechnologyAnalyzer.js * * Identifies the technology stack, frameworks, and libraries used in the project * by analyzing package files, imports, and project structure. */ import chalk from 'chalk'; import fs from 'fs/promises'; import path from 'path'; import { PackageAnalyzer } from '../utils/package-analyzer.js'; export class TechnologyAnalyzer { constructor(options = {}) { this.verbose = options.verbose || false; // Initialize tech stack storage this.primaryLanguages = []; this.frameworks = []; this.libraries = []; this.buildTools = []; this.linters = []; this.testingFrameworks = []; this.foundPackages = []; this.stacks = []; } /** * Analyzes the project to identify technologies used * @param {Object} projectStructure - Project structure from ProjectScanner * @returns {Object} Technology stack information */ async analyzeTechnologies(projectStructure) { if (this.verbose) { console.log(chalk.gray('Starting technology stack analysis...')); } // Reset tech stack storage for a clean analysis this.resetTechnologyStack(); try { // Identify primary languages based on file extensions this.identifyPrimaryLanguages(projectStructure); // Analyze package files (package.json, requirements.txt, etc.) await this.analyzePackageFiles(projectStructure); // Analyze specific framework files and indicators await this.analyzeFrameworkIndicators(projectStructure); // Detect technology stacks and integrations this.detectTechnologyStacks(); // Return combined technology stack results return { primaryLanguages: this.primaryLanguages, frameworks: this.frameworks, libraries: this.libraries, buildTools: this.buildTools, linters: this.linters, testingFrameworks: this.testingFrameworks, stacks: this.stacks || [], }; } catch (error) { if (this.verbose) { console.error(chalk.red(`Error in technology analysis: ${error.message}`)); console.error(chalk.gray(error.stack)); } throw new Error(`Technology analysis failed: ${error.message}`); } } /** * Resets technology stack storage for a clean analysis */ resetTechnologyStack() { this.primaryLanguages = []; this.frameworks = []; this.libraries = []; this.buildTools = []; this.linters = []; this.testingFrameworks = []; } /** * Identifies primary languages based on file extensions * @param {Object} projectStructure - Project structure data */ identifyPrimaryLanguages(projectStructure) { if (this.verbose) { console.log(chalk.gray('Identifying primary languages...')); } // Count files by extension const extensionCount = new Map(); let totalFiles = 0; // Create a mapping of extensions to languages const languageMap = { js: 'javascript', jsx: 'javascript', ts: 'typescript', tsx: 'typescript', py: 'python', rb: 'ruby', php: 'php', java: 'java', go: 'go', rs: 'rust', c: 'c', cpp: 'cpp', h: 'c', hpp: 'cpp', cs: 'csharp', swift: 'swift', kt: 'kotlin', scala: 'scala', clj: 'clojure', ex: 'elixir', exs: 'elixir', html: 'html', css: 'css', scss: 'sass', sass: 'sass', less: 'less', json: 'json', xml: 'xml', yaml: 'yaml', yml: 'yaml', md: 'markdown', sql: 'sql', }; // Count files by extension for (const file of projectStructure.files) { // Skip files that are in the project's node_modules, build, or dist directories if ( file.path.includes('/node_modules/') || file.path.includes('/dist/') || file.path.includes('/build/') ) { continue; } // Get file extension without the dot const ext = path.extname(file.path).slice(1).toLowerCase(); if (!ext) continue; extensionCount.set(ext, (extensionCount.get(ext) || 0) + 1); totalFiles++; } if (totalFiles === 0) return; // Calculate language percentages const languagePercentage = new Map(); for (const [ext, count] of extensionCount.entries()) { if (languageMap[ext]) { const language = languageMap[ext]; languagePercentage.set( language, (languagePercentage.get(language) || 0) + (count / totalFiles) * 100 ); } } // Filter for primary languages (more than 5%) for (const [language, percentage] of languagePercentage.entries()) { if (percentage > 5) { this.primaryLanguages.push(language); } } // Sort languages by percentage this.primaryLanguages.sort((a, b) => { return languagePercentage.get(b) - languagePercentage.get(a); }); if (this.verbose) { console.log(chalk.gray(`Primary languages detected: ${this.primaryLanguages.join(', ')}`)); } } /** * Analyzes package files for dependencies * @param {Object} projectStructure - Project structure data */ async analyzePackageFiles(projectStructure) { if (this.verbose) { console.log(chalk.gray('Analyzing package files for dependencies...')); } // Get project root const projectRoot = projectStructure.projectPath; // Use our enhanced package analyzer if (this.verbose) { console.log(chalk.gray('Using enhanced package analyzer...')); } try { // Analyze package.json files const packageAnalysis = await PackageAnalyzer.analyzeDependencies(projectRoot); // Update tech stacks from the analysis if (packageAnalysis && packageAnalysis.technologies) { // Add libraries from different categories if (packageAnalysis.technologies.ui && packageAnalysis.technologies.ui.length > 0) { this.libraries.push(...packageAnalysis.technologies.ui); } if ( packageAnalysis.technologies.backend_services && packageAnalysis.technologies.backend_services.length > 0 ) { this.libraries.push(...packageAnalysis.technologies.backend_services); } if ( packageAnalysis.technologies.databases && packageAnalysis.technologies.databases.length > 0 ) { this.libraries.push(...packageAnalysis.technologies.databases); } if ( packageAnalysis.technologies.stateManagement && packageAnalysis.technologies.stateManagement.length > 0 ) { this.libraries.push(...packageAnalysis.technologies.stateManagement); } if ( packageAnalysis.technologies.dataFetching && packageAnalysis.technologies.dataFetching.length > 0 ) { this.libraries.push(...packageAnalysis.technologies.dataFetching); } if (packageAnalysis.technologies.forms && packageAnalysis.technologies.forms.length > 0) { this.libraries.push(...packageAnalysis.technologies.forms); } if ( packageAnalysis.technologies.styling && packageAnalysis.technologies.styling.length > 0 ) { this.libraries.push(...packageAnalysis.technologies.styling); } if (packageAnalysis.technologies.api && packageAnalysis.technologies.api.length > 0) { this.libraries.push(...packageAnalysis.technologies.api); } // Add frameworks if ( packageAnalysis.technologies.frontend && packageAnalysis.technologies.frontend.length > 0 ) { this.frameworks.push(...packageAnalysis.technologies.frontend); } if ( packageAnalysis.technologies.backend && packageAnalysis.technologies.backend.length > 0 ) { this.frameworks.push(...packageAnalysis.technologies.backend); } if ( packageAnalysis.technologies.serverless && packageAnalysis.technologies.serverless.length > 0 ) { this.frameworks.push(...packageAnalysis.technologies.serverless); } if (packageAnalysis.technologies.mobile && packageAnalysis.technologies.mobile.length > 0) { this.frameworks.push(...packageAnalysis.technologies.mobile); } // Add build tools if ( packageAnalysis.technologies.buildTools && packageAnalysis.technologies.buildTools.length > 0 ) { this.buildTools.push(...packageAnalysis.technologies.buildTools); } // Add testing frameworks if ( packageAnalysis.technologies.testing && packageAnalysis.technologies.testing.length > 0 ) { this.testingFrameworks.push(...packageAnalysis.technologies.testing); } } } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Warning: Error analyzing package.json: ${error.message}`)); } } // Check for Python projects (requirements.txt, setup.py) if (this.primaryLanguages.includes('python')) { const requirementsFiles = projectStructure.files .filter((file) => file.name === 'requirements.txt') .map((file) => file.path); const setupFiles = projectStructure.files .filter((file) => file.name === 'setup.py') .map((file) => file.path); // Analyze requirements.txt files for (const requirementsPath of requirementsFiles) { try { const content = await fs.readFile(requirementsPath, 'utf8'); const lines = content.split('\n').map((line) => line.trim()); for (const line of lines) { // Skip empty lines and comments if (!line || line.startsWith('#')) continue; // Extract package name (ignoring version) const packageName = line.split('==')[0].split('>=')[0].split('<=')[0].trim(); // Check for common frameworks and libraries if (packageName === 'django') this.frameworks.push('Django'); if (packageName === 'flask') this.frameworks.push('Flask'); if (packageName === 'fastapi') this.frameworks.push('FastAPI'); if (packageName === 'tornado') this.frameworks.push('Tornado'); if (packageName === 'pyramid') this.frameworks.push('Pyramid'); // Testing libraries if (packageName === 'pytest') this.testingFrameworks.push('pytest'); if (packageName === 'unittest') this.testingFrameworks.push('unittest'); // Common data science libraries if (['numpy', 'pandas', 'matplotlib', 'seaborn', 'scipy'].includes(packageName)) { if (!this.libraries.includes('Data Science')) { this.libraries.push('Data Science'); } } // ML/AI libraries if ( ['tensorflow', 'keras', 'pytorch', 'scikit-learn', 'xgboost'].includes(packageName) ) { if (!this.libraries.includes('Machine Learning')) { this.libraries.push('Machine Learning'); } } } } catch (error) { if (this.verbose) { console.warn( chalk.yellow(`Warning: Error analyzing requirements.txt: ${error.message}`) ); } } } } // Check for Ruby projects (Gemfile) if (this.primaryLanguages.includes('ruby')) { const gemFiles = projectStructure.files .filter((file) => file.name === 'Gemfile') .map((file) => file.path); for (const gemfilePath of gemFiles) { try { const content = await fs.readFile(gemfilePath, 'utf8'); const lines = content.split('\n').map((line) => line.trim()); for (const line of lines) { // Skip empty lines and comments if (!line || line.startsWith('#')) continue; // Check for common frameworks and libraries if (line.includes("'rails'") || line.includes('"rails"')) { this.frameworks.push('Ruby on Rails'); } if (line.includes("'sinatra'") || line.includes('"sinatra"')) { this.frameworks.push('Sinatra'); } if (line.includes("'rspec'") || line.includes('"rspec"')) { this.testingFrameworks.push('RSpec'); } } } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Warning: Error analyzing Gemfile: ${error.message}`)); } } } } } /** * Analyze specific framework indicators in the project structure * @param {Object} projectStructure - Project structure from ProjectScanner */ async analyzeFrameworkIndicators(projectStructure) { if (this.verbose) { console.log(chalk.gray('Analyzing framework indicators...')); } // Check for React const reactFiles = projectStructure.files .filter((file) => file.name.endsWith('.jsx') || file.name.endsWith('.tsx')) .map((file) => file.path); if (reactFiles.length > 0 && !this.frameworks.includes('React')) { this.frameworks.push('React'); } // Enhanced Next.js detection // Check for Next.js config files const nextjsConfigFiles = projectStructure.files .filter( (file) => file.name === 'next.config.js' || file.name === 'next.config.mjs' || file.name === 'next.config.ts' ) .map((file) => file.path); // Check for Next.js app directory structure (Next.js 13+) const hasAppDir = projectStructure.directories.some( (dir) => dir.name === 'app' && (dir.path.includes('/src/app') || dir.path.endsWith('/app')) ); // Check for Next.js pages directory structure (traditional Next.js) const hasPagesDir = projectStructure.directories.some( (dir) => dir.name === 'pages' && (dir.path.includes('/src/pages') || dir.path.endsWith('/pages')) ); // Look for Next.js special files like page.js, layout.js in app directory const nextSpecialFileNames = ['page', 'layout', 'loading', 'error', 'not-found', 'route']; const hasNextAppPageFiles = projectStructure.files.some((file) => { const fileName = path.basename(file.name, path.extname(file.name)); return file.path.includes('/app/') && nextSpecialFileNames.includes(fileName); }); // Check package.json for Next.js dependency const hasNextDependency = this.libraries.includes('next'); // If any Next.js indicators are found, mark as Next.js project // Note: hasPagesDir alone is not sufficient since Astro also uses pages directories if ( nextjsConfigFiles.length > 0 || hasNextDependency || (hasAppDir && hasNextAppPageFiles) || (hasPagesDir && (hasNextDependency || nextjsConfigFiles.length > 0)) ) { if (!this.frameworks.includes('Next.js')) { this.frameworks.push('Next.js'); // Log detailed detection reason if in verbose mode if (this.verbose) { if (nextjsConfigFiles.length > 0) console.log(chalk.gray(' - Next.js detected: Found next.config.js')); if (hasNextDependency) console.log(chalk.gray(' - Next.js detected: Found next dependency')); if (hasAppDir && hasNextAppPageFiles) console.log( chalk.gray(' - Next.js detected: Found app directory with Next.js page files') ); if (hasPagesDir) console.log(chalk.gray(' - Next.js detected: Found pages directory')); } } // If app directory is detected, add Next.js App Router as framework variant if (hasAppDir && hasNextAppPageFiles) { if (!this.frameworks.includes('Next.js App Router')) { this.frameworks.push('Next.js App Router'); if (this.verbose) { console.log(chalk.gray(' - Next.js App Router detected')); } } } // Try to detect Next.js version from package.json try { const packageJsonFiles = projectStructure.files .filter((file) => file.name === 'package.json') .map((file) => file.path); if (packageJsonFiles.length > 0) { const packageJsonContent = await fs.readFile(packageJsonFiles[0], 'utf8'); const packageData = JSON.parse(packageJsonContent); // Check for Next.js version const nextVersion = packageData.dependencies?.next || packageData.devDependencies?.next; if (nextVersion) { // Extract major version const versionMatch = nextVersion.match(/[0-9]+/); if (versionMatch) { const majorVersion = parseInt(versionMatch[0]); if (majorVersion >= 14) { if (!this.frameworks.includes('Next.js 14+')) { this.frameworks.push('Next.js 14+'); if (this.verbose) { console.log(chalk.gray(` - Next.js 14+ detected (version: ${nextVersion})`)); } } } else if (majorVersion >= 13) { if (!this.frameworks.includes('Next.js 13+')) { this.frameworks.push('Next.js 13+'); if (this.verbose) { console.log(chalk.gray(` - Next.js 13+ detected (version: ${nextVersion})`)); } } } } } } } catch (err) { // Silently handle any errors in version detection if (this.verbose) { console.log(chalk.yellow(`Error detecting Next.js version: ${err.message}`)); } } } // Check for Vue.js const vueFiles = projectStructure.files .filter((file) => file.name.endsWith('.vue')) .map((file) => file.path); if (vueFiles.length > 0 && !this.frameworks.includes('Vue.js')) { this.frameworks.push('Vue.js'); } // Check for Angular const angularFiles = projectStructure.files .filter((file) => file.name === 'angular.json') .map((file) => file.path); if (angularFiles.length > 0 && !this.frameworks.includes('Angular')) { this.frameworks.push('Angular'); } // Check for Django const djangoFiles = projectStructure.files .filter((file) => file.name === 'manage.py' || file.name === 'settings.py') .map((file) => file.path); if (djangoFiles.length > 0 && !this.frameworks.includes('Django')) { this.frameworks.push('Django'); } // Check for shadcn/ui const hasShadcnUIComponents = projectStructure.directories.some( (dir) => dir.name === 'components' && dir.path.includes('/ui/') ); const hasShadcnUIIndicators = projectStructure.files.some( (file) => file.name === 'components.json' || (file.path.includes('/ui/') && (file.path.includes('/button/') || file.path.includes('/dialog/'))) ); const hasShadcnDependencies = [ 'class-variance-authority', 'clsx', 'cmdk', 'lucide-react', 'tailwind-merge', 'tailwindcss-animate', ].some((dep) => this.libraries.includes(dep)); if ((hasShadcnUIComponents && hasShadcnUIIndicators) || hasShadcnDependencies) { if (!this.frameworks.includes('shadcn/ui')) { this.frameworks.push('shadcn/ui'); if (this.verbose) { console.log(chalk.gray(' - shadcn/ui detected')); } } } // Check for Supabase const hasSupabaseDependencies = [ '@supabase/supabase-js', '@supabase/auth-helpers-nextjs', '@supabase/auth-helpers-react', '@supabase/auth-ui-react', ].some((dep) => this.libraries.includes(dep)); const hasSupabaseConfig = projectStructure.files.some( (file) => file.name === 'supabase.ts' || file.name === 'supabase.js' || file.path.includes('/supabase/') || file.path.includes('/lib/supabase') ); if (hasSupabaseDependencies || hasSupabaseConfig) { if (!this.frameworks.includes('Supabase')) { this.frameworks.push('Supabase'); if (this.verbose) { console.log(chalk.gray(' - Supabase detected')); } } // If using both Next.js and Supabase, add the stack if (this.frameworks.includes('Next.js')) { if (!this.frameworks.includes('Supabase-Next.js Stack')) { this.frameworks.push('Supabase-Next.js Stack'); if (this.verbose) { console.log(chalk.gray(' - Supabase-Next.js Stack detected')); } } } } // Check for Flask const flaskFiles = projectStructure.files .filter((file) => { // Check for app.py or similar with imports for Flask if (file.name === 'app.py' || file.name === 'wsgi.py' || file.name === 'main.py') { return true; } return false; }) .map((file) => file.path); if (flaskFiles.length > 0) { // Sample a few files to check for Flask imports const sampleSize = Math.min(flaskFiles.length, 3); let hasFlask = false; for (let i = 0; i < sampleSize; i++) { try { const content = await fs.readFile(flaskFiles[i], 'utf8'); if (content.includes('from flask import')) { hasFlask = true; break; } } catch (error) { // Ignore errors reading files } } if (hasFlask && !this.frameworks.includes('Flask')) { this.frameworks.push('Flask'); } } // Check for Express.js if ( (this.libraries.includes('express') || this.libraries.includes('express.js')) && !this.frameworks.includes('Express.js') ) { this.frameworks.push('Express.js'); } // Check for iOS/Swift projects const xcodeProjectFiles = projectStructure.files .filter((file) => file.name.endsWith('.xcodeproj') || file.name.endsWith('.xcworkspace')) .map((file) => file.path); if (xcodeProjectFiles.length > 0) { this.frameworks.push('Xcode'); // Check for Swift UI const swiftFiles = projectStructure.files .filter((file) => file.extension === 'swift') .map((file) => file.path); if (swiftFiles.length > 0) { // Sample a few Swift files to check for SwiftUI imports const sampleSize = Math.min(swiftFiles.length, 5); let hasSwiftUI = false; for (let i = 0; i < sampleSize; i++) { try { const content = await fs.readFile(swiftFiles[i], 'utf8'); if (content.includes('import SwiftUI')) { hasSwiftUI = true; break; } } catch (error) { // Ignore errors reading files } } if (hasSwiftUI) { this.frameworks.push('SwiftUI'); } } } // Detect Android projects const androidManifestFiles = projectStructure.files .filter((file) => file.name === 'AndroidManifest.xml') .map((file) => file.path); if (androidManifestFiles.length > 0) { this.frameworks.push('Android'); } // Detect Flutter const pubspecFiles = projectStructure.files .filter((file) => file.name === 'pubspec.yaml') .map((file) => file.path); if (pubspecFiles.length > 0) { for (const pubspecPath of pubspecFiles) { try { const content = await fs.readFile(pubspecPath, 'utf8'); if (content.includes('flutter:')) { this.frameworks.push('Flutter'); break; } } catch (error) { // Ignore errors reading files } } } // Check for Tailwind CSS const hasTailwindDependencies = [ 'tailwindcss', '@tailwindcss/forms', '@tailwindcss/typography', '@tailwindcss/aspect-ratio', '@tailwindcss/container-queries', 'tailwind-merge', 'tailwindcss-animate', ].some((dep) => this.libraries.includes(dep)); const hasTailwindConfig = projectStructure.files.some( (file) => file.name === 'tailwind.config.js' || file.name === 'tailwind.config.ts' || file.name === 'tailwind.config.cjs' || file.name === 'tailwind.config.mjs' ); if (hasTailwindDependencies || hasTailwindConfig) { if (!this.frameworks.includes('Tailwind CSS')) { this.frameworks.push('Tailwind CSS'); if (this.verbose) { console.log(chalk.gray(' - Tailwind CSS detected')); } } } // Ensure we don't have duplicates this.primaryLanguages = [...new Set(this.primaryLanguages)]; this.frameworks = [...new Set(this.frameworks)]; this.libraries = [...new Set(this.libraries)]; this.buildTools = [...new Set(this.buildTools)]; this.linters = [...new Set(this.linters)]; this.testingFrameworks = [...new Set(this.testingFrameworks)]; } /** * Detects common technology stacks based on identified frameworks and libraries * This method analyzes the combination of detected technologies to identify * well-known technology stacks and patterns */ detectTechnologyStacks() { if (this.verbose) { console.log(chalk.gray('Detecting technology stacks...')); } this.stacks = []; // MERN Stack (MongoDB, Express, React, Node.js) if ( this.frameworks.includes('React') && this.frameworks.includes('Express.js') && this.libraries.includes('mongodb') ) { this.stacks.push('MERN Stack'); } // MEAN Stack (MongoDB, Express, Angular, Node.js) if ( this.frameworks.includes('Angular') && this.frameworks.includes('Express.js') && this.libraries.includes('mongodb') ) { this.stacks.push('MEAN Stack'); } // Next.js Enterprise Stack if ( this.frameworks.includes('Next.js') && (this.libraries.includes('typescript') || this.primaryLanguages.includes('typescript')) ) { this.stacks.push('NextJS Enterprise Stack'); } // Supabase + Next.js Stack (already detected in framework indicators) if (this.frameworks.includes('Supabase-Next.js Stack')) { this.stacks.push('Supabase + Next.js'); } // tRPC Full-Stack if ( this.libraries.includes('@trpc/server') || this.libraries.includes('@trpc/client') || this.libraries.includes('@trpc/react-query') ) { this.stacks.push('tRPC Full-Stack'); } // Laravel + Vue Stack if (this.frameworks.includes('Laravel') && this.frameworks.includes('Vue.js')) { this.stacks.push('Laravel + Vue'); } // Django REST + React Stack if ( this.frameworks.includes('Django') && this.frameworks.includes('React') && this.libraries.includes('djangorestframework') ) { this.stacks.push('Django REST + React'); } // Spring Boot + React Stack if (this.frameworks.includes('Spring Boot') && this.frameworks.includes('React')) { this.stacks.push('Spring Boot + React'); } // Astro Content Stack if (this.frameworks.includes('Astro')) { this.stacks.push('Astro Content Stack'); } // React Native Mobile Stack if (this.frameworks.includes('React Native')) { this.stacks.push('React Native Mobile'); } // E-commerce Stack indicators const ecommerceIndicators = [ 'stripe', 'shopify', 'woocommerce', 'magento', 'commerce.js', '@stripe/stripe-js', 'paypal', ]; if (ecommerceIndicators.some((indicator) => this.libraries.includes(indicator))) { this.stacks.push('Ecommerce Stack'); } // JAMstack (JavaScript, APIs, Markup) const jamstackFrameworks = ['Gatsby', 'Next.js', 'Nuxt.js', 'Astro', 'Eleventy']; if (jamstackFrameworks.some((framework) => this.frameworks.includes(framework))) { this.stacks.push('JAMstack'); } // Serverless Stack const serverlessIndicators = [ 'aws-lambda', 'vercel', 'netlify-functions', '@vercel/node', 'serverless', 'aws-cdk', ]; if (serverlessIndicators.some((indicator) => this.libraries.includes(indicator))) { this.stacks.push('Serverless Stack'); } // Full-Stack TypeScript if (this.primaryLanguages.includes('typescript') && this.frameworks.length > 1) { this.stacks.push('Full-Stack TypeScript'); } // Remove duplicates this.stacks = [...new Set(this.stacks)]; if (this.verbose && this.stacks.length > 0) { console.log(chalk.gray(`Technology stacks detected: ${this.stacks.join(', ')}`)); } } }