UNPKG

aios-core

Version:

Synkra AIOS: AI-Orchestrated System for Full Stack Development - Core Framework

600 lines (523 loc) 16.5 kB
/** * Tech Stack Detector - Detects project technology stack before workflow execution * * DETERMINISTIC: All operations use file system checks (fs-extra), * no AI involvement in detection. * * Responsibilities: * - Detect database configuration (Supabase, PostgreSQL, MongoDB, etc.) * - Detect frontend framework (React, Vue, Angular, etc.) * - Detect backend framework (Express, Fastify, etc.) * - Detect build tools and styling (Vite, Tailwind, etc.) * - Determine which workflow phases are applicable * * @module core/orchestration/tech-stack-detector * @version 1.0.0 */ const fs = require('fs-extra'); const path = require('path'); /** * @typedef {Object} DatabaseProfile * @property {string|null} type - Database type (supabase, postgresql, mongodb, mysql, sqlite) * @property {boolean} hasSchema - Whether schema files exist * @property {boolean} hasMigrations - Whether migration files exist * @property {boolean} hasRLS - Whether RLS policies are configured * @property {boolean} envVarsConfigured - Whether DB env vars are set */ /** * @typedef {Object} FrontendProfile * @property {string|null} framework - Frontend framework (react, vue, angular, svelte) * @property {string|null} buildTool - Build tool (vite, webpack, esbuild) * @property {string|null} styling - Styling approach (tailwind, css, scss, styled-components) * @property {string|null} componentLibrary - UI component library (shadcn, mui, chakra) */ /** * @typedef {Object} BackendProfile * @property {string|null} type - Backend type (express, fastify, nest, edge-functions) * @property {boolean} hasAPI - Whether API routes exist */ /** * @typedef {Object} TechStackProfile * @property {boolean} hasDatabase * @property {boolean} hasFrontend * @property {boolean} hasBackend * @property {boolean} hasTypeScript * @property {boolean} hasTests * @property {DatabaseProfile} database * @property {FrontendProfile} frontend * @property {BackendProfile} backend * @property {number[]} applicablePhases * @property {number} confidence * @property {string} detectedAt */ /** * Detects project technology stack for workflow phase determination */ class TechStackDetector { /** * @param {string} projectRoot - Project root directory */ constructor(projectRoot) { this.projectRoot = projectRoot; this._packageJsonCache = null; } /** * Main detection method - runs before workflow phases * DETERMINISTIC: All detection uses fs operations * @returns {Promise<TechStackProfile>} */ async detect() { const profile = this._createEmptyProfile(); // Run all detectors await this._detectDatabase(profile); await this._detectFrontend(profile); await this._detectBackend(profile); await this._detectTypeScript(profile); await this._detectTests(profile); // Compute applicable phases based on detection this._computeApplicablePhases(profile); // Calculate detection confidence this._calculateConfidence(profile); profile.detectedAt = new Date().toISOString(); return profile; } /** * Create empty profile structure * @private * @returns {TechStackProfile} */ _createEmptyProfile() { return { // Core detection flags hasDatabase: false, hasFrontend: false, hasBackend: false, hasTypeScript: false, hasTests: false, // Database details database: { type: null, hasSchema: false, hasMigrations: false, hasRLS: false, envVarsConfigured: false, }, // Frontend details frontend: { framework: null, buildTool: null, styling: null, componentLibrary: null, }, // Backend details backend: { type: null, hasAPI: false, }, // Computed fields applicablePhases: [], confidence: 0, detectedAt: null, }; } /** * Load and cache package.json * @private * @returns {Promise<Object|null>} */ async _loadPackageJson() { if (this._packageJsonCache !== null) { return this._packageJsonCache; } const packageJsonPath = path.join(this.projectRoot, 'package.json'); if (await fs.pathExists(packageJsonPath)) { try { this._packageJsonCache = await fs.readJson(packageJsonPath); } catch { this._packageJsonCache = null; } } else { this._packageJsonCache = null; } return this._packageJsonCache; } /** * Get all dependencies from package.json * @private * @returns {Promise<Object>} */ async _getAllDependencies() { const pkg = await this._loadPackageJson(); if (!pkg) return {}; return { ...pkg.dependencies, ...pkg.devDependencies, }; } /** * Detect database configuration * @private * @param {TechStackProfile} profile */ async _detectDatabase(profile) { const deps = await this._getAllDependencies(); // Check for Supabase directory const supabasePath = path.join(this.projectRoot, 'supabase'); if (await fs.pathExists(supabasePath)) { profile.hasDatabase = true; profile.database.type = 'supabase'; // Check for migrations const migrationsPath = path.join(supabasePath, 'migrations'); profile.database.hasMigrations = await fs.pathExists(migrationsPath); profile.database.hasSchema = profile.database.hasMigrations; // Check for RLS in migrations if (profile.database.hasMigrations) { try { const migrations = await fs.readdir(migrationsPath); for (const file of migrations) { if (file.endsWith('.sql')) { const content = await fs.readFile(path.join(migrationsPath, file), 'utf8'); if ( content.includes('ENABLE ROW LEVEL SECURITY') || content.includes('CREATE POLICY') ) { profile.database.hasRLS = true; break; } } } } catch { // Ignore read errors } } } // Check for Prisma const prismaPath = path.join(this.projectRoot, 'prisma'); if (await fs.pathExists(prismaPath)) { profile.hasDatabase = true; if (!profile.database.type) { profile.database.type = 'postgresql'; } profile.database.hasSchema = await fs.pathExists(path.join(prismaPath, 'schema.prisma')); } // Check package.json dependencies if (deps['@supabase/supabase-js']) { profile.hasDatabase = true; if (!profile.database.type) { profile.database.type = 'supabase'; } } if (deps['pg'] || deps['postgres'] || deps['@prisma/client']) { profile.hasDatabase = true; if (!profile.database.type) { profile.database.type = 'postgresql'; } } if (deps['mongoose'] || deps['mongodb']) { profile.hasDatabase = true; // Only set type if not already detected (preserve first detection) if (!profile.database.type) { profile.database.type = 'mongodb'; } } if (deps['mysql'] || deps['mysql2']) { profile.hasDatabase = true; if (!profile.database.type) { profile.database.type = 'mysql'; } } if (deps['better-sqlite3'] || deps['sqlite3']) { profile.hasDatabase = true; if (!profile.database.type) { profile.database.type = 'sqlite'; } } // Check for environment variables await this._checkDatabaseEnvVars(profile); } /** * Check for database environment variables * @private * @param {TechStackProfile} profile */ async _checkDatabaseEnvVars(profile) { const envFiles = ['.env', '.env.local', '.env.example']; for (const envFile of envFiles) { const envPath = path.join(this.projectRoot, envFile); if (await fs.pathExists(envPath)) { try { const content = await fs.readFile(envPath, 'utf8'); if ( content.includes('SUPABASE_URL') || content.includes('DATABASE_URL') || content.includes('POSTGRES_') || content.includes('MONGODB_URI') || content.includes('MYSQL_') ) { profile.database.envVarsConfigured = true; break; } } catch { // Ignore read errors } } } } /** * Detect frontend framework and tools * @private * @param {TechStackProfile} profile */ async _detectFrontend(profile) { const deps = await this._getAllDependencies(); // Framework detection if (deps['react'] || deps['react-dom']) { profile.hasFrontend = true; profile.frontend.framework = 'react'; } else if (deps['vue']) { profile.hasFrontend = true; profile.frontend.framework = 'vue'; } else if (deps['@angular/core']) { profile.hasFrontend = true; profile.frontend.framework = 'angular'; } else if (deps['svelte']) { profile.hasFrontend = true; profile.frontend.framework = 'svelte'; } else if (deps['next']) { profile.hasFrontend = true; profile.frontend.framework = 'react'; // Next.js uses React } else if (deps['nuxt']) { profile.hasFrontend = true; profile.frontend.framework = 'vue'; // Nuxt uses Vue } // Build tool detection if (deps['vite']) { profile.frontend.buildTool = 'vite'; } else if (deps['webpack']) { profile.frontend.buildTool = 'webpack'; } else if (deps['esbuild']) { profile.frontend.buildTool = 'esbuild'; } else if (deps['parcel']) { profile.frontend.buildTool = 'parcel'; } // Styling detection if (deps['tailwindcss']) { profile.frontend.styling = 'tailwind'; } else if (deps['styled-components']) { profile.frontend.styling = 'styled-components'; } else if (deps['@emotion/react'] || deps['@emotion/styled']) { profile.frontend.styling = 'emotion'; } else if (deps['sass'] || deps['node-sass']) { profile.frontend.styling = 'scss'; } // Component library detection if (await fs.pathExists(path.join(this.projectRoot, 'src/components/ui'))) { profile.frontend.componentLibrary = 'shadcn'; } else if (deps['@mui/material'] || deps['@material-ui/core']) { profile.frontend.componentLibrary = 'mui'; } else if (deps['@chakra-ui/react']) { profile.frontend.componentLibrary = 'chakra'; } else if (deps['antd']) { profile.frontend.componentLibrary = 'antd'; } // Check for src directory as fallback indicator if (!profile.hasFrontend) { const srcPath = path.join(this.projectRoot, 'src'); if (await fs.pathExists(srcPath)) { const srcFiles = await fs.readdir(srcPath).catch(() => []); const hasUIFiles = srcFiles.some( (f) => f.endsWith('.jsx') || f.endsWith('.tsx') || f.endsWith('.vue') || f === 'App.jsx' || f === 'App.tsx', ); if (hasUIFiles) { profile.hasFrontend = true; } } } } /** * Detect backend framework * @private * @param {TechStackProfile} profile */ async _detectBackend(profile) { const deps = await this._getAllDependencies(); // Express if (deps['express']) { profile.hasBackend = true; profile.backend.type = 'express'; } // Fastify if (deps['fastify']) { profile.hasBackend = true; profile.backend.type = 'fastify'; } // NestJS if (deps['@nestjs/core']) { profile.hasBackend = true; profile.backend.type = 'nest'; } // Hono if (deps['hono']) { profile.hasBackend = true; profile.backend.type = 'hono'; } // Check for Supabase Edge Functions const edgeFunctionsPath = path.join(this.projectRoot, 'supabase', 'functions'); if (await fs.pathExists(edgeFunctionsPath)) { profile.hasBackend = true; if (!profile.backend.type) { profile.backend.type = 'edge-functions'; } } // Check for API routes const apiPaths = [ path.join(this.projectRoot, 'api'), path.join(this.projectRoot, 'src/api'), path.join(this.projectRoot, 'pages/api'), // Next.js path.join(this.projectRoot, 'app/api'), // Next.js App Router ]; for (const apiPath of apiPaths) { if (await fs.pathExists(apiPath)) { profile.backend.hasAPI = true; break; } } } /** * Detect TypeScript usage * @private * @param {TechStackProfile} profile */ async _detectTypeScript(profile) { const deps = await this._getAllDependencies(); // Check for TypeScript dependency if (deps['typescript']) { profile.hasTypeScript = true; } // Check for tsconfig.json const tsconfigPath = path.join(this.projectRoot, 'tsconfig.json'); if (await fs.pathExists(tsconfigPath)) { profile.hasTypeScript = true; } } /** * Detect test framework * @private * @param {TechStackProfile} profile */ async _detectTests(profile) { const deps = await this._getAllDependencies(); // Check for test frameworks const testFrameworks = [ 'jest', 'vitest', 'mocha', 'jasmine', '@testing-library/react', 'cypress', 'playwright', ]; for (const framework of testFrameworks) { if (deps[framework]) { profile.hasTests = true; break; } } // Check for test directories const testPaths = [ path.join(this.projectRoot, 'tests'), path.join(this.projectRoot, 'test'), path.join(this.projectRoot, '__tests__'), path.join(this.projectRoot, 'src/__tests__'), ]; for (const testPath of testPaths) { if (await fs.pathExists(testPath)) { profile.hasTests = true; break; } } } /** * Compute which workflow phases are applicable * @private * @param {TechStackProfile} profile */ _computeApplicablePhases(profile) { // Phase 1 (System Architecture) - Always applicable profile.applicablePhases.push(1); // Phase 2 (Database) - Only if hasDatabase if (profile.hasDatabase) { profile.applicablePhases.push(2); } // Phase 3 (Frontend/UX) - Only if hasFrontend if (profile.hasFrontend) { profile.applicablePhases.push(3); } // Phases 4-10 (Consolidation, Validation, Planning) - Always applicable // But their content adapts based on what was collected profile.applicablePhases.push(4, 5, 6, 7, 8, 9, 10); } /** * Calculate detection confidence score * @private * @param {TechStackProfile} profile */ _calculateConfidence(profile) { let confidence = 50; // Base confidence // Add confidence for each detection if (profile.hasDatabase) { confidence += 10; if (profile.database.type) confidence += 5; if (profile.database.envVarsConfigured) confidence += 5; } if (profile.hasFrontend) { confidence += 10; if (profile.frontend.framework) confidence += 5; if (profile.frontend.buildTool) confidence += 3; if (profile.frontend.styling) confidence += 2; } if (profile.hasBackend) { confidence += 5; if (profile.backend.type) confidence += 3; } if (profile.hasTypeScript) { confidence += 3; } if (profile.hasTests) { confidence += 2; } profile.confidence = Math.min(100, confidence); } /** * Get a human-readable summary of the detection * @param {TechStackProfile} profile * @returns {string} */ static getSummary(profile) { const parts = []; if (profile.hasFrontend) { const fw = profile.frontend.framework || 'unknown'; const style = profile.frontend.styling ? ` + ${profile.frontend.styling}` : ''; parts.push(`Frontend: ${fw}${style}`); } if (profile.hasDatabase) { const db = profile.database.type || 'unknown'; const rls = profile.database.hasRLS ? ' (RLS)' : ''; parts.push(`Database: ${db}${rls}`); } if (profile.hasBackend) { const be = profile.backend.type || 'unknown'; parts.push(`Backend: ${be}`); } if (profile.hasTypeScript) { parts.push('TypeScript'); } return parts.length > 0 ? parts.join(' | ') : 'No stack detected'; } } module.exports = TechStackDetector;