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

387 lines (339 loc) 11.3 kB
/** * ProjectScanner.js * * Core component responsible for traversing the project directory structure * and gathering information about files, directories, and their relationships. */ import chalk from 'chalk'; import fs from 'fs/promises'; import { glob } from 'glob'; import path from 'path'; import { GitIgnoreParser } from '../utils/gitignore-parser.js'; export class ProjectScanner { constructor(options = {}) { this.projectPath = options.projectPath || process.cwd(); this.ignorePatterns = options.ignorePatterns || [ '**/node_modules/**', '**/dist/**', '**/build/**', '**/.git/**', '**/.next/**', '**/coverage/**', '**/*.d.ts', ]; this.useGitIgnore = options.useGitIgnore !== false; // Default to true this.deepScan = options.deepScan || false; this.verbose = options.verbose || false; // Initialize data structures for project information this.fileTypes = {}; this.fileExtensions = new Set(); this.directoryStructure = {}; this.files = []; this.directories = []; } /** * Scan project directory and analyze structure * @param {string} projectPath - Path to project directory * @param {Object} options - Scanning options * @returns {Object} Project analysis results */ async scanProject(projectPath, options = {}) { const startTime = Date.now(); try { console.log(chalk.blue(`🔍 Scanning project at: ${projectPath}`)); // Validate project directory exists try { await fs.access(projectPath); } catch { throw new Error(`Project directory does not exist: ${projectPath}`); } const stats = await fs.stat(projectPath); if (!stats.isDirectory()) { throw new Error(`Path is not a directory: ${projectPath}`); } // Update project path if provided if (projectPath) { this.projectPath = projectPath; } // Update options if provided if (options.ignorePatterns) { this.ignorePatterns = options.ignorePatterns; } if (options.useGitIgnore !== undefined) { this.useGitIgnore = options.useGitIgnore; } if (options.deep !== undefined) { this.deepScan = options.deep; } if (this.verbose) { console.log(chalk.gray(`Ignored patterns: ${this.ignorePatterns.join(', ')}`)); } // Reset data structures for a clean scan this.fileTypes = {}; this.fileExtensions = new Set(); this.directoryStructure = {}; this.files = []; this.directories = []; // If enabled, add gitignore patterns to our ignore list let effectiveIgnorePatterns = [...this.ignorePatterns]; if (this.useGitIgnore) { try { const gitIgnorePatterns = await GitIgnoreParser.parseGitIgnore(this.projectPath); if (gitIgnorePatterns.length > 0) { effectiveIgnorePatterns = [...effectiveIgnorePatterns, ...gitIgnorePatterns]; if (this.verbose) { console.log(chalk.gray(`Added ${gitIgnorePatterns.length} patterns from .gitignore`)); } } } catch (error) { if (this.verbose) { console.warn(chalk.yellow(`Warning: Error parsing .gitignore: ${error.message}`)); } } } // Get all files in the project, respecting ignore patterns const allFiles = await glob('**/*', { cwd: this.projectPath, ignore: effectiveIgnorePatterns, dot: true, nodir: false, absolute: true, }); // Analyze each file/directory for (const filePath of allFiles) { try { const stats = await fs.stat(filePath); const relPath = path.relative(this.projectPath, filePath); if (stats.isDirectory()) { this.directories.push({ path: filePath, relativePath: relPath, name: path.basename(filePath), depth: relPath.split(path.sep).length, parentPath: path.dirname(filePath), }); } else { // File properties const ext = path.extname(filePath).substring(1); // Remove the dot const fileInfo = { path: filePath, relativePath: relPath, name: path.basename(filePath), extension: ext, size: stats.size, type: this.determineFileType(filePath), modifiedTime: stats.mtime, parentPath: path.dirname(filePath), }; this.files.push(fileInfo); // Track extension statistics this.fileExtensions.add(ext); // Track file type statistics const fileType = fileInfo.type; this.fileTypes[fileType] = (this.fileTypes[fileType] || 0) + 1; } } catch (error) { if (this.verbose) { console.warn( chalk.yellow(`Warning: Error analyzing file ${filePath}: ${error.message}`) ); } } } // If doing a deep scan, analyze relationships between files if (this.deepScan) { await this.analyzeRelationships(); } // Build directory structure representation this.buildDirectoryStructure(); const result = { projectPath, projectName: path.basename(projectPath), timestamp: new Date().toISOString(), scanDuration: Date.now() - startTime, files: this.files, directories: this.directories, fileTypes: this.fileTypes, fileExtensions: Array.from(this.fileExtensions), directoryStructure: this.directoryStructure, }; console.log(chalk.green(`✅ Project scan completed in ${result.scanDuration}ms`)); return result; } catch (error) { console.log(chalk.red(`❌ Project scan failed: ${error.message}`)); // Return a minimal structure instead of crashing return { projectPath, projectName: path.basename(projectPath), timestamp: new Date().toISOString(), scanDuration: Date.now() - startTime, error: error.message, files: [], directories: [], fileTypes: {}, fileExtensions: [], directoryStructure: {}, }; } } /** * Analyzes relationships between files (imports, dependencies, etc.) * Only performed during deep scans */ async analyzeRelationships() { if (this.verbose) { console.log(chalk.gray('Analyzing file relationships (deep scan)...')); } // Implementation would involve parsing files for import statements, // require() calls, etc., and creating a dependency graph for (const file of this.files) { file.imports = []; file.importedBy = []; } } /** * Builds a hierarchical representation of the directory structure */ buildDirectoryStructure() { if (this.verbose) { console.log(chalk.gray('Building directory structure representation...')); } // Create the root node this.directoryStructure = { name: path.basename(this.projectPath), path: this.projectPath, type: 'directory', children: {}, }; // Group files by parent directory const filesByDir = {}; for (const file of this.files) { const dirPath = path.dirname(file.relativePath); if (!filesByDir[dirPath]) { filesByDir[dirPath] = []; } filesByDir[dirPath].push(file); } // Helper function to add a path to the structure const addPathToStructure = (relativePath, isDirectory = false, fileInfo = null) => { if (relativePath === '.') return; // Skip the root directory const parts = relativePath.split(path.sep); let current = this.directoryStructure.children; // Build the path in the structure for (let i = 0; i < parts.length; i++) { const part = parts[i]; if (!part) continue; // Skip empty parts const isLastPart = i === parts.length - 1; if (!current[part]) { if (isLastPart && !isDirectory) { // This is a file current[part] = { name: part, type: 'file', extension: fileInfo ? fileInfo.extension : '', fileType: fileInfo ? fileInfo.type : 'unknown', size: fileInfo ? fileInfo.size : 0, }; } else { // This is a directory current[part] = { name: part, type: 'directory', children: {}, }; } } if (!isLastPart || isDirectory) { current = current[part].children; } } }; // Add directories to the structure for (const dir of this.directories) { addPathToStructure(dir.relativePath, true); } // Add files to the structure for (const file of this.files) { addPathToStructure(file.relativePath, false, file); } } /** * Determines the type of a file based on its extension or content * @param {string} filePath - Path to the file * @returns {string} Type of file */ determineFileType(filePath) { const ext = path.extname(filePath).toLowerCase(); const fileName = path.basename(filePath).toLowerCase(); // Configuration files if ( [ 'package.json', 'package-lock.json', 'yarn.lock', 'tsconfig.json', 'jsconfig.json', '.prettierrc', '.eslintrc', '.babelrc', 'webpack.config.js', 'babel.config.js', 'jest.config.js', 'vite.config.js', 'rollup.config.js', ].includes(fileName) ) { return 'config'; } // Documentation files if ( [ 'readme.md', 'license', 'license.md', 'license.txt', 'contributing.md', 'changelog.md', ].includes(fileName) || ext === '.md' ) { return 'documentation'; } // Source code by language const codeExtensions = { '.js': 'javascript', '.jsx': 'javascript-react', '.ts': 'typescript', '.tsx': 'typescript-react', '.py': 'python', '.rb': 'ruby', '.java': 'java', '.go': 'go', '.cs': 'csharp', '.php': 'php', '.swift': 'swift', '.kt': 'kotlin', '.rs': 'rust', '.dart': 'dart', '.c': 'c', '.cpp': 'cpp', '.h': 'c-header', '.hpp': 'cpp-header', }; if (codeExtensions[ext]) { return codeExtensions[ext]; } // Web assets if (['.html', '.htm'].includes(ext)) return 'html'; if (['.css', '.scss', '.sass', '.less'].includes(ext)) return 'stylesheet'; if (['.svg', '.png', '.jpg', '.jpeg', '.gif', '.webp'].includes(ext)) return 'image'; if (['.woff', '.woff2', '.ttf', '.eot', '.otf'].includes(ext)) return 'font'; if (['.json', '.jsonc'].includes(ext)) return 'json'; if (['.xml', '.xsl'].includes(ext)) return 'xml'; if (['.yml', '.yaml'].includes(ext)) return 'yaml'; if (['.toml'].includes(ext)) return 'toml'; if (['.csv', '.tsv'].includes(ext)) return 'tabular-data'; // Fallback to 'unknown' type return 'unknown'; } }