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
JavaScript
/**
* 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;