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

410 lines (361 loc) 11.4 kB
/** * ProjectScanner.js * * Core component responsible for traversing the project directory structure * and gathering information about files, directories, and their relationships. */ import fs from 'node:fs/promises' import path from 'node:path' import chalk from 'chalk' import { glob } from 'glob' 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 this.verbose = options.verbose // 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' } }