@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
908 lines (794 loc) • 28.9 kB
JavaScript
/**
* TechnologyAnalyzer.js
*
* Identifies the technology stack, frameworks, and libraries used in the project
* by analyzing package files, imports, and project structure.
*/
import fs from 'node:fs/promises'
import path from 'node:path'
import chalk from 'chalk'
import { PackageAnalyzer } from '../utils/package-analyzer.js'
export class TechnologyAnalyzer {
constructor(options = {}) {
this.verbose = options.verbose
// 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?.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) &&
!this.libraries.includes('Data Science')
) {
this.libraries.push('Data Science')
}
// ML/AI libraries
if (
['tensorflow', 'keras', 'pytorch', 'scikit-learn', 'xgboost'].includes(packageName) &&
!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 && !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 = Number.parseInt(versionMatch[0], 10)
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 && !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) &&
!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') &&
!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 {
// 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 {
// 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 {
// 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) &&
!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(', ')}`))
}
}
}