UNPKG

acs-framework-cli

Version:

🚀 CLI inteligente para configurar automáticamente el Augmented Context Standard (ACS) Framework. Context-as-Code que convierte tu conocimiento en un activo versionado.

841 lines (708 loc) 29 kB
#!/usr/bin/env node /** * extract-facts.js - Extractor de hechos verificables del proyecto * * Extrae información real y verificable del proyecto para evitar * que la IA "delire" o invente información inexistente. */ const fs = require('fs-extra'); const path = require('path'); const { execSync } = require('child_process'); class ProjectFactsExtractor { constructor(targetDir = process.cwd()) { this.targetDir = targetDir; this.facts = { timestamp: new Date().toISOString(), project: {}, tech: {}, files: {}, git: {}, commands: {}, structure: {} }; } async extract() { console.log('🔍 Extrayendo hechos verificables del proyecto...'); try { await this.extractProjectBasics(); await this.extractTechStack(); await this.extractFileStructure(); await this.extractGitInfo(); await this.extractCommands(); await this.extractProjectStructure(); return this.facts; } catch (error) { console.error('Error extrayendo facts:', error.message); return this.facts; } } async extractProjectBasics() { // Detectar tipo de proyecto y nombre - AGNÓSTICO A TECNOLOGÍA const packageJsonPath = path.join(this.targetDir, 'package.json'); const composerJsonPath = path.join(this.targetDir, 'composer.json'); const requirementsPath = path.join(this.targetDir, 'requirements.txt'); const goModPath = path.join(this.targetDir, 'go.mod'); const cargoTomlPath = path.join(this.targetDir, 'Cargo.toml'); const pubspecYamlPath = path.join(this.targetDir, 'pubspec.yaml'); const gemfilePath = path.join(this.targetDir, 'Gemfile'); const pocoProjectPath = path.join(this.targetDir, '*.csproj'); const pomXmlPath = path.join(this.targetDir, 'pom.xml'); const buildGradlePath = path.join(this.targetDir, 'build.gradle'); this.facts.project.name = path.basename(this.targetDir); this.facts.project.detectedType = 'unknown'; // Flutter/Dart detection if (await fs.pathExists(pubspecYamlPath)) { try { const pubspec = await fs.readFile(pubspecYamlPath, 'utf8'); this.facts.project.detectedType = 'flutter'; // Extract Flutter/Dart info const nameMatch = pubspec.match(/name:\s*(.+)/); const versionMatch = pubspec.match(/version:\s*(.+)/); const descriptionMatch = pubspec.match(/description:\s*(.+)/); const sdkMatch = pubspec.match(/sdk:\s*['"](.+)['"]/); if (nameMatch) this.facts.project.name = nameMatch[1].trim(); if (versionMatch) this.facts.project.version = versionMatch[1].trim(); if (descriptionMatch) this.facts.project.description = descriptionMatch[1].trim(); if (sdkMatch) this.facts.project.dartSdk = sdkMatch[1].trim(); // Check if it's Flutter vs pure Dart if (pubspec.includes('flutter:')) { this.facts.project.detectedType = 'flutter'; this.facts.project.framework = 'Flutter'; } else { this.facts.project.detectedType = 'dart'; this.facts.project.framework = 'Dart'; } } catch (e) { this.facts.project.detectedType = 'flutter'; } } // Ruby on Rails detection if (await fs.pathExists(gemfilePath)) { try { const gemfile = await fs.readFile(gemfilePath, 'utf8'); this.facts.project.detectedType = 'ruby'; // Check if it's Rails specifically if (gemfile.includes('rails')) { this.facts.project.framework = 'Ruby on Rails'; this.facts.project.detectedType = 'rails'; } else { this.facts.project.framework = 'Ruby'; } // Extract Ruby version const rubyVersionMatch = gemfile.match(/ruby\s+['"](.+)['"]/); if (rubyVersionMatch) { this.facts.project.rubyVersion = rubyVersionMatch[1]; } } catch (e) { this.facts.project.detectedType = 'ruby'; } } // JavaScript/Node.js detection (includes Next.js, React, Vue, etc.) if (await fs.pathExists(packageJsonPath)) { const pkg = await this.safeJsonRead(packageJsonPath); if (pkg) { this.facts.project.name = pkg.name || this.facts.project.name; this.facts.project.version = pkg.version; this.facts.project.description = pkg.description; this.facts.project.detectedType = this.detectJSProjectType(pkg); this.facts.project.author = pkg.author; this.facts.project.license = pkg.license; this.facts.project.engines = pkg.engines; } } // PHP detection (includes Laravel, Symfony, etc.) if (await fs.pathExists(composerJsonPath)) { const composer = await this.safeJsonRead(composerJsonPath); if (composer) { this.facts.project.detectedType = 'php'; this.facts.project.phpVersion = composer.require?.php; this.facts.project.name = composer.name || this.facts.project.name; this.facts.project.description = composer.description; this.facts.project.version = composer.version; // Detect PHP frameworks if (composer.require && composer.require['laravel/framework']) { this.facts.project.framework = 'Laravel'; this.facts.project.detectedType = 'laravel'; } else if (composer.require && composer.require['symfony/symfony']) { this.facts.project.framework = 'Symfony'; this.facts.project.detectedType = 'symfony'; } } } // Python detection if (await fs.pathExists(requirementsPath)) { this.facts.project.detectedType = this.facts.project.detectedType === 'unknown' ? 'python' : this.facts.project.detectedType; } // Go detection if (await fs.pathExists(goModPath)) { this.facts.project.detectedType = 'go'; try { const goMod = await fs.readFile(goModPath, 'utf8'); const moduleMatch = goMod.match(/module\s+(.+)/); if (moduleMatch) { this.facts.project.goModule = moduleMatch[1]; } } catch (e) { // Silently ignore } } // Rust detection if (await fs.pathExists(cargoTomlPath)) { this.facts.project.detectedType = 'rust'; } // Java detection (Maven) if (await fs.pathExists(pomXmlPath)) { this.facts.project.detectedType = 'java'; this.facts.project.framework = 'Maven'; } // Java/Android detection (Gradle) if (await fs.pathExists(buildGradlePath)) { try { const gradle = await fs.readFile(buildGradlePath, 'utf8'); if (gradle.includes('com.android.application')) { this.facts.project.detectedType = 'android'; this.facts.project.framework = 'Android'; } else { this.facts.project.detectedType = 'java'; this.facts.project.framework = 'Gradle'; } } catch (e) { this.facts.project.detectedType = 'java'; } } } async extractTechStack() { const packageJsonPath = path.join(this.targetDir, 'package.json'); const packageLockPath = path.join(this.targetDir, 'package-lock.json'); const yarnLockPath = path.join(this.targetDir, 'yarn.lock'); const composerJsonPath = path.join(this.targetDir, 'composer.json'); const composerLockPath = path.join(this.targetDir, 'composer.lock'); const requirementsPath = path.join(this.targetDir, 'requirements.txt'); const pipfilePath = path.join(this.targetDir, 'Pipfile'); const pubspecYamlPath = path.join(this.targetDir, 'pubspec.yaml'); const gemfilePath = path.join(this.targetDir, 'Gemfile'); const gemfileLockPath = path.join(this.targetDir, 'Gemfile.lock'); // Flutter/Dart dependencies if (await fs.pathExists(pubspecYamlPath)) { try { const pubspec = await fs.readFile(pubspecYamlPath, 'utf8'); // Extract dependencies section const depsMatch = pubspec.match(/dependencies:\s*([\s\S]*?)(?=dev_dependencies:|flutter:|$)/); const devDepsMatch = pubspec.match(/dev_dependencies:\s*([\s\S]*?)(?=flutter:|$)/); if (depsMatch) { this.facts.tech.flutterDependencies = this.parseYamlDependencies(depsMatch[1]); } if (devDepsMatch) { this.facts.tech.flutterDevDependencies = this.parseYamlDependencies(devDepsMatch[1]); } // Check for pubspec.lock for exact versions const pubspecLockPath = path.join(this.targetDir, 'pubspec.lock'); if (await fs.pathExists(pubspecLockPath)) { const pubspecLock = await fs.readFile(pubspecLockPath, 'utf8'); this.facts.tech.flutterExactVersions = this.parseYamlLockFile(pubspecLock); } } catch (e) { // Silently ignore } } // Ruby/Rails dependencies if (await fs.pathExists(gemfilePath)) { try { const gemfile = await fs.readFile(gemfilePath, 'utf8'); this.facts.tech.rubyDependencies = this.parseGemfile(gemfile); // Extract from Gemfile.lock if exists if (await fs.pathExists(gemfileLockPath)) { const gemfileLock = await fs.readFile(gemfileLockPath, 'utf8'); this.facts.tech.rubyExactVersions = this.parseGemfileLock(gemfileLock); } } catch (e) { // Silently ignore } } // JavaScript/Node.js dependencies if (await fs.pathExists(packageJsonPath)) { const pkg = await this.safeJsonRead(packageJsonPath); if (pkg) { this.facts.tech.dependencies = pkg.dependencies || {}; this.facts.tech.devDependencies = pkg.devDependencies || {}; this.facts.tech.scripts = pkg.scripts || {}; // Detectar exactas desde lock files if (await fs.pathExists(packageLockPath)) { const lock = await this.safeJsonRead(packageLockPath); if (lock && lock.packages) { this.facts.tech.exactVersions = this.extractExactVersionsFromNpmLock(lock); } } } } // PHP dependencies if (await fs.pathExists(composerJsonPath)) { const composer = await this.safeJsonRead(composerJsonPath); if (composer) { this.facts.tech.phpDependencies = composer.require || {}; this.facts.tech.phpDevDependencies = composer['require-dev'] || {}; // Extract from composer.lock if exists if (await fs.pathExists(composerLockPath)) { const composerLock = await this.safeJsonRead(composerLockPath); if (composerLock && composerLock.packages) { this.facts.tech.phpExactVersions = this.extractExactVersionsFromComposerLock(composerLock); } } } } // Python dependencies if (await fs.pathExists(requirementsPath)) { try { const requirements = await fs.readFile(requirementsPath, 'utf8'); this.facts.tech.pythonDependencies = this.parseRequirementsTxt(requirements); } catch (e) { // Silently ignore } } // Detectar frameworks principales - AGNÓSTICO A TECNOLOGÍA this.facts.tech.detectedFrameworks = this.detectFrameworks(); } async extractFileStructure() { try { // Obtener tree del proyecto (solo directorios principales) const dirs = await this.getDirectoryStructure(); this.facts.structure.directories = dirs; // Detectar archivos de configuración importantes const configFiles = await this.detectConfigFiles(); this.facts.files.configFiles = configFiles; // Detectar archivos de testing const testFiles = await this.detectTestFiles(); this.facts.files.testFiles = testFiles; } catch (error) { this.facts.structure.error = error.message; } } async extractGitInfo() { try { // Git remotes const remoteOrigin = execSync('git remote get-url origin 2>/dev/null || echo ""', { cwd: this.targetDir, encoding: 'utf8' }).trim(); if (remoteOrigin) { this.facts.git.origin = remoteOrigin; this.facts.git.repoName = this.extractRepoNameFromUrl(remoteOrigin); } // Branch actual const currentBranch = execSync('git branch --show-current 2>/dev/null || echo ""', { cwd: this.targetDir, encoding: 'utf8' }).trim(); this.facts.git.currentBranch = currentBranch; // Tags recientes const tags = execSync('git tag --sort=-version:refname 2>/dev/null | head -5 || echo ""', { cwd: this.targetDir, encoding: 'utf8' }).trim().split('\n').filter(t => t); this.facts.git.recentTags = tags; // Último commit const lastCommit = execSync('git log -1 --format="%H|%s|%an|%ad" --date=iso 2>/dev/null || echo ""', { cwd: this.targetDir, encoding: 'utf8' }).trim(); if (lastCommit) { const [hash, message, author, date] = lastCommit.split('|'); this.facts.git.lastCommit = { hash, message, author, date }; } } catch (error) { this.facts.git.error = 'Not a git repository or git not available'; } } async extractCommands() { // Comandos desde package.json scripts if (this.facts.tech.scripts) { this.facts.commands.npm = Object.keys(this.facts.tech.scripts).map(script => `npm run ${script}`); } // Comandos básicos según el tipo de proyecto const commands = []; switch (this.facts.project.detectedType) { case 'next.js': commands.push('npm install', 'npm run dev', 'npm run build', 'npm start'); break; case 'react': commands.push('npm install', 'npm start', 'npm run build', 'npm test'); break; case 'vue': commands.push('npm install', 'npm run serve', 'npm run build'); break; case 'node': commands.push('npm install', 'node index.js', 'npm start'); break; case 'php': commands.push('composer install', 'php artisan serve'); break; case 'laravel': commands.push('composer install', 'php artisan key:generate', 'php artisan serve'); break; case 'python': commands.push('pip install -r requirements.txt', 'python manage.py runserver'); break; } this.facts.commands.detected = commands; } async extractProjectStructure() { // Detectar si es SPA, API, fullstack, etc. const hasPublicFolder = await fs.pathExists(path.join(this.targetDir, 'public')); const hasSrcFolder = await fs.pathExists(path.join(this.targetDir, 'src')); const hasAppFolder = await fs.pathExists(path.join(this.targetDir, 'app')); const hasApiFolder = await fs.pathExists(path.join(this.targetDir, 'api')); const hasRoutesFolder = await fs.pathExists(path.join(this.targetDir, 'routes')); const hasComponentsFolder = await fs.pathExists(path.join(this.targetDir, 'components')) || await fs.pathExists(path.join(this.targetDir, 'src/components')); // Detectar patrones adicionales para proyectos grandes const hasViewsFolder = await fs.pathExists(path.join(this.targetDir, 'views')) || await fs.pathExists(path.join(this.targetDir, 'resources/views')); const hasControllersFolder = await fs.pathExists(path.join(this.targetDir, 'controllers')) || await fs.pathExists(path.join(this.targetDir, 'app/Http/Controllers')); const hasModelsFolder = await fs.pathExists(path.join(this.targetDir, 'models')) || await fs.pathExists(path.join(this.targetDir, 'app/Models')); const hasMiddlewareFolder = await fs.pathExists(path.join(this.targetDir, 'middleware')) || await fs.pathExists(path.join(this.targetDir, 'app/Http/Middleware')); this.facts.structure.patterns = { hasPublicFolder, hasSrcFolder, hasAppFolder, hasApiFolder, hasRoutesFolder, hasComponentsFolder, hasViewsFolder, hasControllersFolder, hasModelsFolder, hasMiddlewareFolder }; // Inferir arquitectura con más precisión let inferredArchitecture = 'unknown'; let confidence = 'low'; if (hasApiFolder || hasRoutesFolder) { if (hasComponentsFolder || hasViewsFolder) { inferredArchitecture = 'fullstack'; confidence = 'high'; } else { inferredArchitecture = 'api'; confidence = 'medium'; } } else if (hasComponentsFolder && hasPublicFolder && !hasControllersFolder) { inferredArchitecture = 'spa'; confidence = 'high'; } else if (hasControllersFolder && hasModelsFolder && hasViewsFolder) { inferredArchitecture = 'monolithic-web'; confidence = 'high'; } else if (hasControllersFolder && hasModelsFolder && !hasViewsFolder) { inferredArchitecture = 'api'; confidence = 'medium'; } this.facts.project.inferredArchitecture = inferredArchitecture; this.facts.project.architectureConfidence = confidence; // Agregar sugerencias basadas en patrones detectados this.facts.project.suggestions = this.generateArchitectureSuggestions(); } // Utility methods async safeJsonRead(filePath) { try { const content = await fs.readFile(filePath, 'utf8'); return JSON.parse(content); } catch (error) { return null; } } detectJSProjectType(pkg) { const deps = { ...pkg.dependencies, ...pkg.devDependencies }; if (deps.next) return 'next.js'; if (deps.react && deps['react-scripts']) return 'create-react-app'; if (deps.react) return 'react'; if (deps.vue) return 'vue'; if (deps.angular || deps['@angular/core']) return 'angular'; if (deps.express) return 'express'; if (pkg.type === 'module' || deps.vite) return 'vite'; return 'node'; } detectFrameworks() { const frameworks = []; // JavaScript/Node.js frameworks const jsDeps = { ...this.facts.tech.dependencies, ...this.facts.tech.devDependencies }; if (jsDeps.react) frameworks.push('React'); if (jsDeps.vue) frameworks.push('Vue.js'); if (jsDeps.next) frameworks.push('Next.js'); if (jsDeps.nuxt) frameworks.push('Nuxt.js'); if (jsDeps.express) frameworks.push('Express'); if (jsDeps.fastify) frameworks.push('Fastify'); if (jsDeps.nest || jsDeps['@nestjs/core']) frameworks.push('NestJS'); if (jsDeps.angular || jsDeps['@angular/core']) frameworks.push('Angular'); if (jsDeps.svelte) frameworks.push('Svelte'); if (jsDeps.gatsby) frameworks.push('Gatsby'); // Flutter frameworks const flutterDeps = { ...this.facts.tech.flutterDependencies }; if (flutterDeps.flutter || this.facts.project.detectedType === 'flutter') { frameworks.push('Flutter'); } if (flutterDeps.cupertino_icons) frameworks.push('Flutter iOS'); if (flutterDeps.provider || flutterDeps.bloc || flutterDeps.riverpod) { frameworks.push('Flutter State Management'); } // PHP frameworks const phpDeps = { ...this.facts.tech.phpDependencies }; if (phpDeps['laravel/framework'] || this.facts.project.detectedType === 'laravel') { frameworks.push('Laravel'); } if (phpDeps['symfony/symfony'] || this.facts.project.detectedType === 'symfony') { frameworks.push('Symfony'); } if (phpDeps['codeigniter4/framework']) frameworks.push('CodeIgniter'); if (phpDeps['cakephp/cakephp']) frameworks.push('CakePHP'); // Ruby frameworks const rubyDeps = { ...this.facts.tech.rubyDependencies }; if (rubyDeps.rails || this.facts.project.detectedType === 'rails') { frameworks.push('Ruby on Rails'); } if (rubyDeps.sinatra) frameworks.push('Sinatra'); if (rubyDeps.hanami) frameworks.push('Hanami'); // Python frameworks const pythonDeps = { ...this.facts.tech.pythonDependencies }; if (pythonDeps.django) frameworks.push('Django'); if (pythonDeps.flask) frameworks.push('Flask'); if (pythonDeps.fastapi) frameworks.push('FastAPI'); if (pythonDeps.tornado) frameworks.push('Tornado'); // Based on project type detection if (this.facts.project.detectedType === 'android') frameworks.push('Android'); if (this.facts.project.detectedType === 'java') frameworks.push('Java'); if (this.facts.project.detectedType === 'go') frameworks.push('Go'); if (this.facts.project.detectedType === 'rust') frameworks.push('Rust'); return frameworks; } async getDirectoryStructure() { try { const items = await fs.readdir(this.targetDir); const dirs = []; const importantFiles = []; for (const item of items) { if (item.startsWith('.') && !['/.context', '.env', '.gitignore', '.git'].some(important => item.includes(important))) continue; const itemPath = path.join(this.targetDir, item); const stat = await fs.stat(itemPath); if (stat.isDirectory()) { // Solo agregar directorios relevantes, no node_modules gigantes if (!['node_modules', '.git', 'vendor', 'target', 'build', 'dist'].includes(item)) { dirs.push(item); } } else { // Capturar archivos importantes del root if ([ 'README.md', 'package.json', 'composer.json', 'requirements.txt', 'Dockerfile', 'docker-compose.yml', '.env.example', 'Makefile', 'tsconfig.json', 'babel.config.js', 'webpack.config.js' ].includes(item)) { importantFiles.push(item); } } } // Limitar a máximo 20 directorios para evitar overwhelm return { directories: dirs.slice(0, 20), rootFiles: importantFiles, totalDirectories: dirs.length }; } catch (error) { return { directories: [], rootFiles: [], error: error.message }; } } async detectConfigFiles() { const configFileNames = [ // JavaScript/Node.js 'package.json', 'tsconfig.json', 'babel.config.js', 'webpack.config.js', 'vite.config.js', 'next.config.js', 'nuxt.config.js', 'tailwind.config.js', 'postcss.config.js', '.eslintrc.js', '.eslintrc.json', 'prettier.config.js', '.prettierrc', // Flutter/Dart 'pubspec.yaml', 'pubspec.lock', 'analysis_options.yaml', // PHP 'composer.json', 'composer.lock', '.env', 'phpunit.xml', 'artisan', // Ruby/Rails 'Gemfile', 'Gemfile.lock', 'config.ru', 'Rakefile', 'database.yml', // Python 'requirements.txt', 'setup.py', 'pyproject.toml', 'Pipfile', 'Pipfile.lock', 'manage.py', 'settings.py', // Java 'pom.xml', 'build.gradle', 'gradle.properties', 'settings.gradle', // Go 'go.mod', 'go.sum', // Rust 'Cargo.toml', 'Cargo.lock', // General 'docker-compose.yml', 'Dockerfile', '.env.example', 'Makefile', '.gitignore', 'README.md', 'LICENSE' ]; const foundFiles = []; for (const fileName of configFileNames) { if (await fs.pathExists(path.join(this.targetDir, fileName))) { foundFiles.push(fileName); } } return foundFiles; } async detectTestFiles() { const testPatterns = [ // JavaScript/Node.js 'test', 'tests', '__tests__', 'spec', 'cypress', 'e2e', // Flutter/Dart 'test_driver', 'integration_test', // PHP 'tests', 'test', 'phpunit', // Ruby/Rails 'test', 'spec', 'features', // Python 'tests', 'test', 'pytest', // Java 'src/test', 'test', // General 'testing' ]; const foundDirs = []; for (const pattern of testPatterns) { if (await fs.pathExists(path.join(this.targetDir, pattern))) { foundDirs.push(pattern); } } // Also check for test files in root const testFiles = [ 'test.js', 'test.php', 'test.py', 'test.rb', 'test.dart' ]; for (const testFile of testFiles) { if (await fs.pathExists(path.join(this.targetDir, testFile))) { foundDirs.push(testFile); } } return foundDirs; } extractExactVersionsFromNpmLock(lock) { const exactVersions = {}; if (lock.packages) { Object.entries(lock.packages).forEach(([pkgPath, info]) => { if (pkgPath === '') return; // Root package const pkgName = pkgPath.replace(/^node_modules\//, ''); if (info.version && !pkgName.includes('/')) { exactVersions[pkgName] = info.version; } }); } return exactVersions; } parseRequirementsTxt(content) { const deps = {}; content.split('\n').forEach(line => { line = line.trim(); if (line && !line.startsWith('#')) { const match = line.match(/^([a-zA-Z0-9\-_]+)(==|>=|<=|>|<|~=)(.+)$/); if (match) { deps[match[1]] = match[3]; } } }); return deps; } generateArchitectureSuggestions() { const suggestions = []; const patterns = this.facts.structure.patterns; const tech = this.facts.tech; // Sugerencias basadas en patrones detectados if (patterns.hasControllersFolder && patterns.hasModelsFolder) { suggestions.push('MVC pattern detected - consider documenting controller-model relationships'); } if (patterns.hasApiFolder && patterns.hasComponentsFolder) { suggestions.push('Full-stack application - document API-frontend data flow'); } if (tech.dependencies && tech.dependencies.express && !patterns.hasViewsFolder) { suggestions.push('Express API detected - focus on endpoint documentation'); } if (tech.dependencies && (tech.dependencies.react || tech.dependencies.vue)) { suggestions.push('Frontend framework detected - document component hierarchy'); } // Sugerencias para proyectos grandes if (this.facts.structure.directories && this.facts.structure.directories.totalDirectories > 15) { suggestions.push('Large project detected - consider modular documentation approach'); } return suggestions; } extractRepoNameFromUrl(url) { // Extraer nombre del repo desde URL de git const match = url.match(/\/([^\/]+)\.git$/) || url.match(/\/([^\/]+)$/); return match ? match[1] : ''; } // Parsers for different technologies parseYamlDependencies(yamlSection) { const dependencies = {}; const lines = yamlSection.split('\n'); for (const line of lines) { const match = line.trim().match(/^(\w+):\s*(.+)/); if (match) { dependencies[match[1]] = match[2].replace(/^['"]|['"]$/g, ''); } } return dependencies; } parseYamlLockFile(lockContent) { const exactVersions = {}; const lines = lockContent.split('\n'); let currentPackage = null; for (const line of lines) { const packageMatch = line.match(/^\s+(\w+):$/); const versionMatch = line.match(/^\s+version:\s*"(.+)"/); if (packageMatch) { currentPackage = packageMatch[1]; } else if (versionMatch && currentPackage) { exactVersions[currentPackage] = versionMatch[1]; currentPackage = null; } } return exactVersions; } parseGemfile(gemfileContent) { const dependencies = {}; const lines = gemfileContent.split('\n'); for (const line of lines) { const match = line.trim().match(/gem\s+['"]([^'"]+)['"]\s*(?:,\s*['"]([^'"]+)['"])?/); if (match) { dependencies[match[1]] = match[2] || 'latest'; } } return dependencies; } parseGemfileLock(lockContent) { const exactVersions = {}; const lines = lockContent.split('\n'); let inSpecs = false; for (const line of lines) { if (line.trim() === 'specs:') { inSpecs = true; continue; } if (inSpecs && line.trim() === '') { inSpecs = false; continue; } if (inSpecs) { const match = line.trim().match(/^(\w+)\s*\(([^)]+)\)/); if (match) { exactVersions[match[1]] = match[2]; } } } return exactVersions; } extractExactVersionsFromComposerLock(composerLock) { const exactVersions = {}; if (composerLock.packages) { for (const pkg of composerLock.packages) { if (pkg.name && pkg.version) { exactVersions[pkg.name] = pkg.version; } } } return exactVersions; } } // CLI execution if (require.main === module) { const extractor = new ProjectFactsExtractor(); extractor.extract().then(facts => { console.log(JSON.stringify(facts, null, 2)); }).catch(error => { console.error('Error:', error.message); process.exit(1); }); } module.exports = ProjectFactsExtractor;