Compare commits

...

1 Commits

Author SHA1 Message Date
claude[bot]
57ed3a37e4 feat: implement intelligent scan command with ast-grep integration
- Add comprehensive project scanning with 4-phase analysis
- Integrate @ast-grep/cli for advanced syntax tree analysis
- Support AI-powered project understanding with fallback
- Generate structured JSON output with file/directory summaries
- Add configurable include/exclude patterns and scan depth
- Provide transparent logging for each analysis phase
- Create task-master scan command with full CLI options

This addresses issue #78 by enabling quick project structure analysis
for easier Task Master adoption on existing projects.

Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
2025-10-07 13:48:18 +00:00
8 changed files with 1385 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
---
"task-master-ai": minor
---
Add intelligent `scan` command for automated codebase analysis
Introduces a comprehensive project scanning feature that intelligently analyzes codebases using ast-grep and AI-powered analysis. The new `task-master scan` command provides:
- **Multi-phase Analysis**: Performs iterative scanning (project type identification → entry points → core structure → recursive deepening)
- **AST-grep Integration**: Uses ast-grep as an AI SDK tool for advanced code structure analysis
- **AI Enhancement**: Optional AI-powered analysis for intelligent project understanding
- **Structured Output**: Generates detailed JSON reports with file/directory summaries
- **Transparent Logging**: Clear progress indicators showing each analysis phase
- **Configurable Options**: Supports custom include/exclude patterns, scan depth, and output paths
This feature addresses the challenge of quickly understanding existing project structures when adopting Task Master, significantly streamlining initial setup and project onboarding.
Usage:
```bash
task-master scan --output=project_scan.json
task-master scan --include="*.js,*.ts" --exclude="*.test.*" --depth=3
task-master scan --no-ai # Skip AI analysis for faster results
```

View File

@@ -53,6 +53,7 @@
"license": "MIT WITH Commons-Clause",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^2.2.9",
"@ast-grep/cli": "^0.29.0",
"@ai-sdk/anthropic": "^1.2.10",
"@ai-sdk/azure": "^1.3.17",
"@ai-sdk/google": "^1.2.13",

View File

@@ -53,6 +53,8 @@ import {
validateStrength
} from './task-manager.js';
import { scanProject } from './task-manager/scan-project/index.js';
import {
moveTasksBetweenTags,
MoveTaskError,
@@ -5067,6 +5069,110 @@ Examples:
process.exit(1);
});
// scan command
programInstance
.command('scan')
.description('Intelligently scan and analyze the project codebase structure')
.option(
'--output <file>',
'Path to save scan results (JSON format)',
'project_scan.json'
)
.option(
'--include <patterns>',
'Comma-separated list of file patterns to include (e.g., "*.js,*.ts")'
)
.option(
'--exclude <patterns>',
'Comma-separated list of file patterns to exclude (e.g., "*.log,tmp/*")'
)
.option(
'--depth <number>',
'Maximum directory depth to scan',
'5'
)
.option('--debug', 'Enable debug output')
.option('--no-ai', 'Skip AI-powered analysis (faster but less detailed)')
.action(async (options) => {
try {
// Initialize TaskMaster to get project root
const taskMaster = initTaskMaster({});
const projectRoot = taskMaster.getProjectRoot();
if (!projectRoot) {
console.error(chalk.red('Error: Could not determine project root.'));
console.log(chalk.yellow('Make sure you are in a valid project directory.'));
process.exit(1);
}
console.log(chalk.blue(`🔍 Starting intelligent scan of project: ${projectRoot}`));
console.log(chalk.gray(`Output will be saved to: ${options.output}`));
// Parse options
const scanOptions = {
outputPath: path.isAbsolute(options.output)
? options.output
: path.join(projectRoot, options.output),
includeFiles: options.include ? options.include.split(',').map(s => s.trim()) : [],
excludeFiles: options.exclude ? options.exclude.split(',').map(s => s.trim()) : undefined,
scanDepth: parseInt(options.depth, 10),
debug: options.debug || false,
reportProgress: true,
skipAI: options.noAi || false
};
// Perform the scan
const spinner = ora('Scanning project structure...').start();
try {
const result = await scanProject(projectRoot, scanOptions);
spinner.stop();
if (result.success) {
console.log(chalk.green('✅ Project scan completed successfully!'));
console.log(chalk.cyan('\n📊 Scan Summary:'));
console.log(chalk.white(` Project Type: ${result.data.scanSummary.projectType}`));
console.log(chalk.white(` Total Files: ${result.data.stats.totalFiles}`));
console.log(chalk.white(` Languages: ${result.data.scanSummary.languages.join(', ')}`));
console.log(chalk.white(` Code Lines: ${result.data.scanSummary.codeMetrics.totalLines}`));
console.log(chalk.white(` Functions: ${result.data.scanSummary.codeMetrics.totalFunctions}`));
console.log(chalk.white(` Classes: ${result.data.scanSummary.codeMetrics.totalClasses}`));
if (result.data.scanSummary.recommendations.length > 0) {
console.log(chalk.yellow('\n💡 Recommendations:'));
result.data.scanSummary.recommendations.forEach(rec => {
console.log(chalk.white(`${rec}`));
});
}
console.log(chalk.green(`\n📄 Detailed results saved to: ${scanOptions.outputPath}`));
} else {
console.error(chalk.red('❌ Project scan failed:'));
console.error(chalk.red(` ${result.error.message}`));
if (scanOptions.debug && result.error.stack) {
console.error(chalk.gray(` ${result.error.stack}`));
}
process.exit(1);
}
} catch (error) {
spinner.stop();
console.error(chalk.red(`❌ Scan failed: ${error.message}`));
if (scanOptions.debug) {
console.error(chalk.gray(error.stack));
}
process.exit(1);
}
} catch (error) {
console.error(chalk.red(`Error initializing scan: ${error.message}`));
process.exit(1);
}
})
.on('error', function (err) {
console.error(chalk.red(`Error: ${err.message}`));
process.exit(1);
});
return programInstance;
}

View File

@@ -0,0 +1,328 @@
/**
* AI-powered analysis for project scanning
*/
import { ScanLoggingConfig } from './scan-config.js';
// Dynamically import AI service with fallback
async function getAiService(options) {
try {
const { getAiService: aiService } = await import('../../ai-services-unified.js');
return aiService(options);
} catch (error) {
throw new Error(`AI service not available: ${error.message}`);
}
}
/**
* Analyze project structure using AI
* @param {Object} scanResults - Raw scan results
* @param {Object} config - Scan configuration
* @returns {Promise<Object>} AI-enhanced analysis
*/
export async function analyzeWithAI(scanResults, config) {
const logger = new ScanLoggingConfig(config.mcpLog, config.reportProgress);
logger.info('Starting AI-powered analysis...');
try {
// Step 1: Project Type Analysis
const projectTypeAnalysis = await analyzeProjectType(scanResults, config, logger);
// Step 2: Entry Points Analysis
const entryPointsAnalysis = await analyzeEntryPoints(scanResults, projectTypeAnalysis, config, logger);
// Step 3: Core Structure Analysis
const coreStructureAnalysis = await analyzeCoreStructure(scanResults, entryPointsAnalysis, config, logger);
// Step 4: Recursive Analysis (if needed)
const detailedAnalysis = await performDetailedAnalysis(scanResults, coreStructureAnalysis, config, logger);
// Combine all analyses
const enhancedAnalysis = {
projectType: projectTypeAnalysis,
entryPoints: entryPointsAnalysis,
coreStructure: coreStructureAnalysis,
detailed: detailedAnalysis,
summary: generateProjectSummary(scanResults, projectTypeAnalysis, coreStructureAnalysis)
};
logger.info('AI analysis completed successfully');
return enhancedAnalysis;
} catch (error) {
logger.error(`AI analysis failed: ${error.message}`);
throw error;
}
}
/**
* Step 1: Analyze project type using AI
* @param {Object} scanResults - Raw scan results
* @param {Object} config - Scan configuration
* @param {ScanLoggingConfig} logger - Logger instance
* @returns {Promise<Object>} Project type analysis
*/
async function analyzeProjectType(scanResults, config, logger) {
logger.info('[Scan #1]: Analyzing project type and structure...');
const prompt = `Given this root directory structure and files, identify the type of project and key characteristics:
Root files: ${JSON.stringify(scanResults.rootFiles, null, 2)}
Directory structure: ${JSON.stringify(scanResults.directories, null, 2)}
Please analyze:
1. Project type (e.g., Node.js, React, Laravel, Python, etc.)
2. Programming languages used
3. Frameworks and libraries
4. Build tools and configuration
5. Files or folders that should be excluded from further analysis (logs, binaries, etc.)
Respond with a JSON object containing your analysis.`;
try {
const aiService = getAiService({ projectRoot: config.projectRoot });
const response = await aiService.generateStructuredOutput({
prompt,
schema: {
type: 'object',
properties: {
projectType: { type: 'string' },
languages: { type: 'array', items: { type: 'string' } },
frameworks: { type: 'array', items: { type: 'string' } },
buildTools: { type: 'array', items: { type: 'string' } },
excludePatterns: { type: 'array', items: { type: 'string' } },
confidence: { type: 'number' },
reasoning: { type: 'string' }
}
}
});
logger.info(`[Scan #1]: Detected ${response.projectType} project`);
return response;
} catch (error) {
logger.warn(`[Scan #1]: AI analysis failed, using fallback detection`);
// Fallback to rule-based detection
return scanResults.projectType;
}
}
/**
* Step 2: Analyze entry points using AI
* @param {Object} scanResults - Raw scan results
* @param {Object} projectTypeAnalysis - Project type analysis
* @param {Object} config - Scan configuration
* @param {ScanLoggingConfig} logger - Logger instance
* @returns {Promise<Object>} Entry points analysis
*/
async function analyzeEntryPoints(scanResults, projectTypeAnalysis, config, logger) {
logger.info('[Scan #2]: Identifying main entry points and core files...');
const prompt = `Based on the project type "${projectTypeAnalysis.projectType}" and these files, identify the main entry points and core files:
Available files: ${JSON.stringify(scanResults.fileList.slice(0, 50), null, 2)}
Project type: ${projectTypeAnalysis.projectType}
Languages: ${JSON.stringify(projectTypeAnalysis.languages)}
Frameworks: ${JSON.stringify(projectTypeAnalysis.frameworks)}
Please identify:
1. Main entry points (files that start the application)
2. Configuration files
3. Core application files
4. Important directories to analyze further
Respond with a structured JSON object.`;
try {
const aiService = getAiService({ projectRoot: config.projectRoot });
const response = await aiService.generateStructuredOutput({
prompt,
schema: {
type: 'object',
properties: {
entryPoints: {
type: 'array',
items: {
type: 'object',
properties: {
path: { type: 'string' },
type: { type: 'string' },
description: { type: 'string' }
}
}
},
configFiles: { type: 'array', items: { type: 'string' } },
coreFiles: { type: 'array', items: { type: 'string' } },
importantDirectories: { type: 'array', items: { type: 'string' } }
}
}
});
logger.info(`[Scan #2]: Found ${response.entryPoints.length} entry points`);
return response;
} catch (error) {
logger.warn(`[Scan #2]: AI analysis failed, using basic detection`);
return {
entryPoints: scanResults.projectType.entryPoints.map(ep => ({ path: ep, type: 'main', description: 'Main entry point' })),
configFiles: [],
coreFiles: [],
importantDirectories: []
};
}
}
/**
* Step 3: Analyze core structure using AI
* @param {Object} scanResults - Raw scan results
* @param {Object} entryPointsAnalysis - Entry points analysis
* @param {Object} config - Scan configuration
* @param {ScanLoggingConfig} logger - Logger instance
* @returns {Promise<Object>} Core structure analysis
*/
async function analyzeCoreStructure(scanResults, entryPointsAnalysis, config, logger) {
logger.info('[Scan #3]: Analyzing core structure and key directories...');
const prompt = `Based on the entry points and project structure, analyze the core architecture:
Entry points: ${JSON.stringify(entryPointsAnalysis.entryPoints, null, 2)}
Important directories: ${JSON.stringify(entryPointsAnalysis.importantDirectories)}
File analysis: ${JSON.stringify(scanResults.detailedFiles.slice(0, 20), null, 2)}
Please analyze:
1. Directory-level summaries and purposes
2. File relationships and dependencies
3. Key architectural patterns
4. Data flow and component relationships
Respond with a structured analysis.`;
try {
const aiService = getAiService({ projectRoot: config.projectRoot });
const response = await aiService.generateStructuredOutput({
prompt,
schema: {
type: 'object',
properties: {
directories: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
purpose: { type: 'string' },
importance: { type: 'string' },
keyFiles: { type: 'array', items: { type: 'string' } },
description: { type: 'string' }
}
}
},
architecture: {
type: 'object',
properties: {
pattern: { type: 'string' },
layers: { type: 'array', items: { type: 'string' } },
dataFlow: { type: 'string' }
}
}
}
}
});
logger.info(`[Scan #3]: Analyzed ${Object.keys(response.directories || {}).length} directories`);
return response;
} catch (error) {
logger.warn(`[Scan #3]: AI analysis failed, using basic structure`);
return {
directories: {},
architecture: {
pattern: 'unknown',
layers: [],
dataFlow: 'unknown'
}
};
}
}
/**
* Step 4: Perform detailed analysis on specific files/directories
* @param {Object} scanResults - Raw scan results
* @param {Object} coreStructureAnalysis - Core structure analysis
* @param {Object} config - Scan configuration
* @param {ScanLoggingConfig} logger - Logger instance
* @returns {Promise<Object>} Detailed analysis
*/
async function performDetailedAnalysis(scanResults, coreStructureAnalysis, config, logger) {
logger.info('[Scan #4+]: Performing detailed file-level analysis...');
const importantFiles = scanResults.detailedFiles
.filter(file => file.functions?.length > 0 || file.classes?.length > 0)
.slice(0, 10); // Limit to most important files
if (importantFiles.length === 0) {
logger.info('No files requiring detailed analysis found');
return { files: {} };
}
const prompt = `Analyze these key files in detail:
${importantFiles.map(file => `
File: ${file.path}
Functions: ${JSON.stringify(file.functions)}
Classes: ${JSON.stringify(file.classes)}
Imports: ${JSON.stringify(file.imports)}
Size: ${file.size} bytes, ${file.lines} lines
`).join('\n')}
For each file, provide:
1. Purpose and responsibility
2. Key functions and their roles
3. Dependencies and relationships
4. Importance to the overall architecture
Respond with detailed analysis for each file.`;
try {
const aiService = getAiService({ projectRoot: config.projectRoot });
const response = await aiService.generateStructuredOutput({
prompt,
schema: {
type: 'object',
properties: {
files: {
type: 'object',
additionalProperties: {
type: 'object',
properties: {
purpose: { type: 'string' },
keyFunctions: { type: 'array', items: { type: 'string' } },
dependencies: { type: 'array', items: { type: 'string' } },
importance: { type: 'string' },
description: { type: 'string' }
}
}
}
}
}
});
logger.info(`[Scan #4+]: Detailed analysis completed for ${Object.keys(response.files || {}).length} files`);
return response;
} catch (error) {
logger.warn(`[Scan #4+]: Detailed analysis failed`);
return { files: {} };
}
}
/**
* Generate a comprehensive project summary
* @param {Object} scanResults - Raw scan results
* @param {Object} projectTypeAnalysis - Project type analysis
* @param {Object} coreStructureAnalysis - Core structure analysis
* @returns {Object} Project summary
*/
function generateProjectSummary(scanResults, projectTypeAnalysis, coreStructureAnalysis) {
return {
overview: `${projectTypeAnalysis.projectType} project with ${scanResults.stats.totalFiles} files across ${scanResults.stats.totalDirectories} directories`,
languages: projectTypeAnalysis.languages,
frameworks: projectTypeAnalysis.frameworks,
architecture: coreStructureAnalysis.architecture?.pattern || 'standard',
complexity: scanResults.stats.totalFiles > 100 ? 'high' : scanResults.stats.totalFiles > 50 ? 'medium' : 'low',
keyComponents: Object.keys(coreStructureAnalysis.directories || {}).slice(0, 5)
};
}

View File

@@ -0,0 +1,3 @@
// Main entry point for scan-project module
export { default } from './scan-project.js';
export { default as scanProject } from './scan-project.js';

View File

@@ -0,0 +1,61 @@
/**
* Configuration classes for project scanning functionality
*/
/**
* Configuration object for scan operations
*/
export class ScanConfig {
constructor({
projectRoot,
outputPath = null,
includeFiles = [],
excludeFiles = ['node_modules', '.git', 'dist', 'build', '*.log'],
scanDepth = 5,
mcpLog = false,
reportProgress = false,
debug = false
} = {}) {
this.projectRoot = projectRoot;
this.outputPath = outputPath;
this.includeFiles = includeFiles;
this.excludeFiles = excludeFiles;
this.scanDepth = scanDepth;
this.mcpLog = mcpLog;
this.reportProgress = reportProgress;
this.debug = debug;
}
}
/**
* Logging configuration for scan operations
*/
export class ScanLoggingConfig {
constructor(mcpLog = false, reportProgress = false) {
this.mcpLog = mcpLog;
this.reportProgress = reportProgress;
}
report(message, level = 'info') {
if (this.reportProgress || this.mcpLog) {
const prefix = this.mcpLog ? '[MCP]' : '[SCAN]';
console.log(`${prefix} ${level.toUpperCase()}: ${message}`);
}
}
debug(message) {
this.report(message, 'debug');
}
info(message) {
this.report(message, 'info');
}
warn(message) {
this.report(message, 'warn');
}
error(message) {
this.report(message, 'error');
}
}

View File

@@ -0,0 +1,422 @@
/**
* Helper functions for project scanning
*/
import fs from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import { ScanLoggingConfig } from './scan-config.js';
/**
* Execute ast-grep command to analyze files
* @param {string} projectRoot - Project root directory
* @param {string} pattern - AST pattern to search for
* @param {Array} files - Files to analyze
* @returns {Promise<Object>} AST analysis results
*/
export async function executeAstGrep(projectRoot, pattern, files = []) {
return new Promise((resolve, reject) => {
const astGrepPath = path.join(process.cwd(), 'node_modules/.bin/ast-grep');
const args = ['run', '--json'];
if (pattern) {
args.push('-p', pattern);
}
if (files.length > 0) {
args.push(...files);
}
const child = spawn(astGrepPath, args, {
cwd: projectRoot,
stdio: ['pipe', 'pipe', 'pipe']
});
let stdout = '';
let stderr = '';
child.stdout.on('data', (data) => {
stdout += data.toString();
});
child.stderr.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
if (code === 0) {
try {
const results = stdout ? JSON.parse(stdout) : [];
resolve(results);
} catch (error) {
reject(new Error(`Failed to parse ast-grep output: ${error.message}`));
}
} else {
reject(new Error(`ast-grep failed with code ${code}: ${stderr}`));
}
});
child.on('error', (error) => {
reject(new Error(`Failed to execute ast-grep: ${error.message}`));
});
});
}
/**
* Detect project type based on files in root directory
* @param {string} projectRoot - Project root directory
* @returns {Object} Project type information
*/
export function detectProjectType(projectRoot) {
const files = fs.readdirSync(projectRoot);
const projectType = {
type: 'unknown',
frameworks: [],
languages: [],
buildTools: [],
entryPoints: []
};
// Check for common project indicators
const indicators = {
'package.json': () => {
projectType.type = 'nodejs';
projectType.languages.push('javascript');
try {
const packageJson = JSON.parse(fs.readFileSync(path.join(projectRoot, 'package.json'), 'utf8'));
// Detect frameworks and libraries
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
if (deps.react) projectType.frameworks.push('react');
if (deps.next) projectType.frameworks.push('next.js');
if (deps.express) projectType.frameworks.push('express');
if (deps.typescript) projectType.languages.push('typescript');
// Find entry points
if (packageJson.main) projectType.entryPoints.push(packageJson.main);
if (packageJson.scripts?.start) {
const startScript = packageJson.scripts.start;
const match = startScript.match(/node\s+(\S+)/);
if (match) projectType.entryPoints.push(match[1]);
}
} catch (error) {
// Ignore package.json parsing errors
}
},
'pom.xml': () => {
projectType.type = 'java';
projectType.languages.push('java');
projectType.buildTools.push('maven');
},
'build.gradle': () => {
projectType.type = 'java';
projectType.languages.push('java');
projectType.buildTools.push('gradle');
},
'requirements.txt': () => {
projectType.type = 'python';
projectType.languages.push('python');
},
'Pipfile': () => {
projectType.type = 'python';
projectType.languages.push('python');
projectType.buildTools.push('pipenv');
},
'pyproject.toml': () => {
projectType.type = 'python';
projectType.languages.push('python');
},
'Cargo.toml': () => {
projectType.type = 'rust';
projectType.languages.push('rust');
projectType.buildTools.push('cargo');
},
'go.mod': () => {
projectType.type = 'go';
projectType.languages.push('go');
},
'composer.json': () => {
projectType.type = 'php';
projectType.languages.push('php');
},
'Gemfile': () => {
projectType.type = 'ruby';
projectType.languages.push('ruby');
}
};
// Check for indicators
for (const file of files) {
if (indicators[file]) {
indicators[file]();
}
}
return projectType;
}
/**
* Get file list based on include/exclude patterns
* @param {string} projectRoot - Project root directory
* @param {Array} includePatterns - Patterns to include
* @param {Array} excludePatterns - Patterns to exclude
* @param {number} maxDepth - Maximum directory depth to scan
* @returns {Array} List of files to analyze
*/
export function getFileList(projectRoot, includePatterns = [], excludePatterns = [], maxDepth = 5) {
const files = [];
function scanDirectory(dirPath, depth = 0) {
if (depth > maxDepth) return;
try {
const items = fs.readdirSync(dirPath, { withFileTypes: true });
for (const item of items) {
const fullPath = path.join(dirPath, item.name);
const relativePath = path.relative(projectRoot, fullPath);
// Check exclude patterns
if (shouldExclude(relativePath, excludePatterns)) {
continue;
}
if (item.isDirectory()) {
scanDirectory(fullPath, depth + 1);
} else if (item.isFile()) {
// Check include patterns (if specified)
if (includePatterns.length === 0 || shouldInclude(relativePath, includePatterns)) {
files.push(relativePath);
}
}
}
} catch (error) {
// Ignore permission errors and continue
}
}
scanDirectory(projectRoot);
return files;
}
/**
* Check if file should be excluded based on patterns
* @param {string} filePath - File path to check
* @param {Array} excludePatterns - Exclude patterns
* @returns {boolean} True if should be excluded
*/
function shouldExclude(filePath, excludePatterns) {
return excludePatterns.some(pattern => {
if (pattern.includes('*')) {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return regex.test(filePath);
}
return filePath.includes(pattern);
});
}
/**
* Check if file should be included based on patterns
* @param {string} filePath - File path to check
* @param {Array} includePatterns - Include patterns
* @returns {boolean} True if should be included
*/
function shouldInclude(filePath, includePatterns) {
return includePatterns.some(pattern => {
if (pattern.includes('*')) {
const regex = new RegExp(pattern.replace(/\*/g, '.*'));
return regex.test(filePath);
}
return filePath.includes(pattern);
});
}
/**
* Analyze file content to extract key information
* @param {string} filePath - Path to file
* @param {string} projectRoot - Project root
* @returns {Object} File analysis results
*/
export function analyzeFileContent(filePath, projectRoot) {
try {
const fullPath = path.join(projectRoot, filePath);
const content = fs.readFileSync(fullPath, 'utf8');
const ext = path.extname(filePath);
const analysis = {
path: filePath,
size: content.length,
lines: content.split('\n').length,
language: getLanguageFromExtension(ext),
functions: [],
classes: [],
imports: [],
exports: []
};
// Basic pattern matching for common constructs
switch (ext) {
case '.js':
case '.ts':
case '.jsx':
case '.tsx':
analyzeJavaScriptFile(content, analysis);
break;
case '.py':
analyzePythonFile(content, analysis);
break;
case '.java':
analyzeJavaFile(content, analysis);
break;
case '.go':
analyzeGoFile(content, analysis);
break;
}
return analysis;
} catch (error) {
return {
path: filePath,
error: error.message
};
}
}
/**
* Get programming language from file extension
* @param {string} ext - File extension
* @returns {string} Programming language
*/
function getLanguageFromExtension(ext) {
const langMap = {
'.js': 'javascript',
'.jsx': 'javascript',
'.ts': 'typescript',
'.tsx': 'typescript',
'.py': 'python',
'.java': 'java',
'.go': 'go',
'.rs': 'rust',
'.php': 'php',
'.rb': 'ruby',
'.cpp': 'cpp',
'.c': 'c',
'.cs': 'csharp'
};
return langMap[ext] || 'unknown';
}
/**
* Analyze JavaScript/TypeScript file content
* @param {string} content - File content
* @param {Object} analysis - Analysis object to populate
*/
function analyzeJavaScriptFile(content, analysis) {
// Extract function declarations
const functionRegex = /(?:function\s+(\w+)|const\s+(\w+)\s*=\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>)|(\w+)\s*:\s*(?:async\s+)?(?:function|\([^)]*\)\s*=>))/g;
let match;
while ((match = functionRegex.exec(content)) !== null) {
const functionName = match[1] || match[2] || match[3];
if (functionName) {
analysis.functions.push(functionName);
}
}
// Extract class declarations
const classRegex = /class\s+(\w+)/g;
while ((match = classRegex.exec(content)) !== null) {
analysis.classes.push(match[1]);
}
// Extract imports
const importRegex = /import\s+(?:.*?\s+from\s+)?['"]([^'"]+)['"]/g;
while ((match = importRegex.exec(content)) !== null) {
analysis.imports.push(match[1]);
}
// Extract exports
const exportRegex = /export\s+(?:default\s+)?(?:const\s+|function\s+|class\s+)?(\w+)/g;
while ((match = exportRegex.exec(content)) !== null) {
analysis.exports.push(match[1]);
}
}
/**
* Analyze Python file content
* @param {string} content - File content
* @param {Object} analysis - Analysis object to populate
*/
function analyzePythonFile(content, analysis) {
// Extract function definitions
const functionRegex = /def\s+(\w+)/g;
let match;
while ((match = functionRegex.exec(content)) !== null) {
analysis.functions.push(match[1]);
}
// Extract class definitions
const classRegex = /class\s+(\w+)/g;
while ((match = classRegex.exec(content)) !== null) {
analysis.classes.push(match[1]);
}
// Extract imports
const importRegex = /(?:import\s+(\w+)|from\s+(\w+)\s+import)/g;
while ((match = importRegex.exec(content)) !== null) {
analysis.imports.push(match[1] || match[2]);
}
}
/**
* Analyze Java file content
* @param {string} content - File content
* @param {Object} analysis - Analysis object to populate
*/
function analyzeJavaFile(content, analysis) {
// Extract method declarations
const methodRegex = /(?:public|private|protected|static|\s)*\s+\w+\s+(\w+)\s*\(/g;
let match;
while ((match = methodRegex.exec(content)) !== null) {
analysis.functions.push(match[1]);
}
// Extract class declarations
const classRegex = /(?:public\s+)?class\s+(\w+)/g;
while ((match = classRegex.exec(content)) !== null) {
analysis.classes.push(match[1]);
}
// Extract imports
const importRegex = /import\s+([^;]+);/g;
while ((match = importRegex.exec(content)) !== null) {
analysis.imports.push(match[1]);
}
}
/**
* Analyze Go file content
* @param {string} content - File content
* @param {Object} analysis - Analysis object to populate
*/
function analyzeGoFile(content, analysis) {
// Extract function declarations
const functionRegex = /func\s+(?:\([^)]*\)\s+)?(\w+)/g;
let match;
while ((match = functionRegex.exec(content)) !== null) {
analysis.functions.push(match[1]);
}
// Extract type/struct declarations
const typeRegex = /type\s+(\w+)\s+struct/g;
while ((match = typeRegex.exec(content)) !== null) {
analysis.classes.push(match[1]); // Treating structs as classes
}
// Extract imports
const importRegex = /import\s+(?:\([^)]+\)|"([^"]+)")/g;
while ((match = importRegex.exec(content)) !== null) {
if (match[1]) {
analysis.imports.push(match[1]);
}
}
}

View File

@@ -0,0 +1,441 @@
/**
* Main scan-project functionality
* Implements intelligent project scanning with AI-driven analysis and ast-grep integration
*/
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { ScanConfig, ScanLoggingConfig } from './scan-config.js';
import {
detectProjectType,
getFileList,
analyzeFileContent,
executeAstGrep
} from './scan-helpers.js';
import { analyzeWithAI } from './ai-analysis.js';
/**
* Main scan project function
* @param {string} projectRoot - Project root directory
* @param {Object} options - Scan options
* @returns {Promise<Object>} Scan results
*/
export default async function scanProject(projectRoot, options = {}) {
const config = new ScanConfig({
projectRoot,
outputPath: options.outputPath,
includeFiles: options.includeFiles || [],
excludeFiles: options.excludeFiles || ['node_modules', '.git', 'dist', 'build', '*.log'],
scanDepth: options.scanDepth || 5,
mcpLog: options.mcpLog || false,
reportProgress: options.reportProgress !== false, // Default to true
debug: options.debug || false
});
const logger = new ScanLoggingConfig(config.mcpLog, config.reportProgress);
logger.info('Starting intelligent project scan...');
try {
// Phase 1: Initial project discovery
logger.info('Phase 1: Discovering project structure...');
const initialScan = await performInitialScan(config, logger);
// Phase 2: File-level analysis
logger.info('Phase 2: Analyzing individual files...');
const fileAnalysis = await performFileAnalysis(config, initialScan, logger);
// Phase 3: AST-grep enhanced analysis
logger.info('Phase 3: Performing AST analysis...');
const astAnalysis = await performASTAnalysis(config, fileAnalysis, logger);
// Phase 4: AI-powered analysis (optional)
let aiAnalysis = null;
if (!options.skipAI) {
logger.info('Phase 4: Enhancing with AI analysis...');
try {
aiAnalysis = await analyzeWithAI({
...initialScan,
...fileAnalysis,
...astAnalysis
}, config);
} catch (error) {
logger.warn(`AI analysis failed, continuing without it: ${error.message}`);
aiAnalysis = {
projectType: { confidence: 0 },
coreStructure: { architecture: { pattern: 'unknown' } },
summary: { complexity: 'unknown' }
};
}
} else {
logger.info('Phase 4: Skipping AI analysis...');
aiAnalysis = {
projectType: { confidence: 0 },
coreStructure: { architecture: { pattern: 'unknown' } },
summary: { complexity: 'unknown' }
};
}
// Phase 5: Generate final output
const finalResults = {
timestamp: new Date().toISOString(),
projectRoot: config.projectRoot,
scanConfig: {
excludeFiles: config.excludeFiles,
scanDepth: config.scanDepth
},
...initialScan,
...fileAnalysis,
...astAnalysis,
aiAnalysis,
scanSummary: generateScanSummary(initialScan, fileAnalysis, aiAnalysis)
};
// Save results if output path is specified
if (config.outputPath) {
await saveResults(finalResults, config.outputPath, logger);
}
logger.info('Project scan completed successfully');
return {
success: true,
data: finalResults
};
} catch (error) {
logger.error(`Scan failed: ${error.message}`);
return {
success: false,
error: {
message: error.message,
stack: config.debug ? error.stack : undefined
}
};
}
}
/**
* Phase 1: Perform initial project discovery
* @param {ScanConfig} config - Scan configuration
* @param {ScanLoggingConfig} logger - Logger instance
* @returns {Promise<Object>} Initial scan results
*/
async function performInitialScan(config, logger) {
logger.info('[Initial Scan]: Discovering project type and structure...');
// Detect project type
const projectType = detectProjectType(config.projectRoot);
logger.info(`[Initial Scan]: Detected ${projectType.type} project`);
// Get root-level files
const rootFiles = fs.readdirSync(config.projectRoot)
.filter(item => {
const fullPath = path.join(config.projectRoot, item);
return fs.statSync(fullPath).isFile();
});
// Get directory structure (first level)
const directories = fs.readdirSync(config.projectRoot)
.filter(item => {
const fullPath = path.join(config.projectRoot, item);
return fs.statSync(fullPath).isDirectory() &&
!config.excludeFiles.includes(item);
})
.map(dir => {
const dirPath = path.join(config.projectRoot, dir);
try {
const files = fs.readdirSync(dirPath);
return {
name: dir,
path: dirPath,
fileCount: files.length,
files: files.slice(0, 10) // Sample of files
};
} catch (error) {
return {
name: dir,
path: dirPath,
error: 'Access denied'
};
}
});
// Get complete file list for scanning
const fileList = getFileList(
config.projectRoot,
config.includeFiles,
config.excludeFiles,
config.scanDepth
);
// Calculate basic statistics
const stats = {
totalFiles: fileList.length,
totalDirectories: directories.length,
rootFiles: rootFiles.length,
languages: [...new Set(fileList.map(f => {
const ext = path.extname(f);
return ext ? ext.substring(1) : 'unknown';
}))],
largestFiles: fileList
.map(f => {
try {
const fullPath = path.join(config.projectRoot, f);
const stats = fs.statSync(fullPath);
return { path: f, size: stats.size };
} catch {
return { path: f, size: 0 };
}
})
.sort((a, b) => b.size - a.size)
.slice(0, 10)
};
logger.info(`[Initial Scan]: Found ${stats.totalFiles} files in ${stats.totalDirectories} directories`);
return {
projectType,
rootFiles,
directories,
fileList,
stats
};
}
/**
* Phase 2: Perform detailed file analysis
* @param {ScanConfig} config - Scan configuration
* @param {Object} initialScan - Initial scan results
* @param {ScanLoggingConfig} logger - Logger instance
* @returns {Promise<Object>} File analysis results
*/
async function performFileAnalysis(config, initialScan, logger) {
logger.info('[File Analysis]: Analyzing file contents...');
const { fileList, projectType } = initialScan;
// Filter files for detailed analysis (avoid binary files, focus on source code)
const sourceExtensions = ['.js', '.ts', '.jsx', '.tsx', '.py', '.java', '.go', '.rs', '.php', '.rb', '.cpp', '.c', '.cs'];
const sourceFiles = fileList.filter(file => {
const ext = path.extname(file);
return sourceExtensions.includes(ext) || projectType.entryPoints.includes(file);
}).slice(0, 100); // Limit to prevent excessive processing
logger.info(`[File Analysis]: Analyzing ${sourceFiles.length} source files...`);
// Analyze files
const detailedFiles = sourceFiles.map(file => {
try {
return analyzeFileContent(file, config.projectRoot);
} catch (error) {
logger.warn(`[File Analysis]: Failed to analyze ${file}: ${error.message}`);
return { path: file, error: error.message };
}
}).filter(result => !result.error);
// Group by language
const byLanguage = detailedFiles.reduce((acc, file) => {
const lang = file.language || 'unknown';
if (!acc[lang]) acc[lang] = [];
acc[lang].push(file);
return acc;
}, {});
// Extract key statistics
const codeStats = {
totalLines: detailedFiles.reduce((sum, f) => sum + (f.lines || 0), 0),
totalFunctions: detailedFiles.reduce((sum, f) => sum + (f.functions?.length || 0), 0),
totalClasses: detailedFiles.reduce((sum, f) => sum + (f.classes?.length || 0), 0),
languageBreakdown: Object.keys(byLanguage).map(lang => ({
language: lang,
files: byLanguage[lang].length,
lines: byLanguage[lang].reduce((sum, f) => sum + (f.lines || 0), 0)
}))
};
logger.info(`[File Analysis]: Analyzed ${detailedFiles.length} files, ${codeStats.totalLines} lines, ${codeStats.totalFunctions} functions`);
return {
detailedFiles,
byLanguage,
codeStats
};
}
/**
* Phase 3: Perform AST-grep enhanced analysis
* @param {ScanConfig} config - Scan configuration
* @param {Object} fileAnalysis - File analysis results
* @param {ScanLoggingConfig} logger - Logger instance
* @returns {Promise<Object>} AST analysis results
*/
async function performASTAnalysis(config, fileAnalysis, logger) {
logger.info('[AST Analysis]: Performing syntax tree analysis...');
const { detailedFiles } = fileAnalysis;
// Select files for AST analysis (focus on main source files)
const astTargetFiles = detailedFiles
.filter(file => file.functions?.length > 0 || file.classes?.length > 0)
.slice(0, 20) // Limit for performance
.map(file => file.path);
if (astTargetFiles.length === 0) {
logger.info('[AST Analysis]: No suitable files found for AST analysis');
return { astResults: {} };
}
logger.info(`[AST Analysis]: Analyzing ${astTargetFiles.length} files with ast-grep...`);
const astResults = {};
// Define common patterns to search for
const patterns = {
functions: {
javascript: 'function $_($$$) { $$$ }',
typescript: 'function $_($$$): $_ { $$$ }',
python: 'def $_($$$): $$$',
java: '$_ $_($$$ args) { $$$ }'
},
classes: {
javascript: 'class $_ { $$$ }',
typescript: 'class $_ { $$$ }',
python: 'class $_: $$$',
java: 'class $_ { $$$ }'
},
imports: {
javascript: 'import $_ from $_',
typescript: 'import $_ from $_',
python: 'import $_',
java: 'import $_;'
}
};
// Run AST analysis for different languages
for (const [language, files] of Object.entries(fileAnalysis.byLanguage || {})) {
if (patterns.functions[language] && files.length > 0) {
try {
logger.debug(`[AST Analysis]: Analyzing ${language} files...`);
const langFiles = files.map(f => f.path).filter(path => astTargetFiles.includes(path));
if (langFiles.length > 0) {
// Run ast-grep for functions
const functionResults = await executeAstGrep(
config.projectRoot,
patterns.functions[language],
langFiles
);
// Run ast-grep for classes
const classResults = await executeAstGrep(
config.projectRoot,
patterns.classes[language],
langFiles
);
astResults[language] = {
functions: functionResults || [],
classes: classResults || [],
files: langFiles
};
}
} catch (error) {
logger.warn(`[AST Analysis]: AST analysis failed for ${language}: ${error.message}`);
// Continue with other languages
}
}
}
const totalMatches = Object.values(astResults).reduce((sum, lang) =>
sum + (lang.functions?.length || 0) + (lang.classes?.length || 0), 0);
logger.info(`[AST Analysis]: Found ${totalMatches} AST matches across ${Object.keys(astResults).length} languages`);
return { astResults };
}
/**
* Generate scan summary
* @param {Object} initialScan - Initial scan results
* @param {Object} fileAnalysis - File analysis results
* @param {Object} aiAnalysis - AI analysis results
* @returns {Object} Scan summary
*/
function generateScanSummary(initialScan, fileAnalysis, aiAnalysis) {
return {
overview: `Scanned ${initialScan.stats.totalFiles} files across ${initialScan.stats.totalDirectories} directories`,
projectType: initialScan.projectType.type,
languages: initialScan.stats.languages,
codeMetrics: {
totalLines: fileAnalysis.codeStats?.totalLines || 0,
totalFunctions: fileAnalysis.codeStats?.totalFunctions || 0,
totalClasses: fileAnalysis.codeStats?.totalClasses || 0
},
aiInsights: {
confidence: aiAnalysis.projectType?.confidence || 0,
architecture: aiAnalysis.coreStructure?.architecture?.pattern || 'unknown',
complexity: aiAnalysis.summary?.complexity || 'unknown'
},
recommendations: generateRecommendations(initialScan, fileAnalysis, aiAnalysis)
};
}
/**
* Generate recommendations based on scan results
* @param {Object} initialScan - Initial scan results
* @param {Object} fileAnalysis - File analysis results
* @param {Object} aiAnalysis - AI analysis results
* @returns {Array} List of recommendations
*/
function generateRecommendations(initialScan, fileAnalysis, aiAnalysis) {
const recommendations = [];
// Size-based recommendations
if (initialScan.stats.totalFiles > 500) {
recommendations.push('Consider using a monorepo management tool for large codebase');
}
// Language-specific recommendations
const jsFiles = fileAnalysis.byLanguage?.javascript?.length || 0;
const tsFiles = fileAnalysis.byLanguage?.typescript?.length || 0;
if (jsFiles > tsFiles && jsFiles > 10) {
recommendations.push('Consider migrating JavaScript files to TypeScript for better type safety');
}
// Documentation recommendations
const readmeExists = initialScan.rootFiles.some(f => f.toLowerCase().includes('readme'));
if (!readmeExists) {
recommendations.push('Add a README.md file to document the project');
}
// Testing recommendations
const hasTests = initialScan.fileList.some(f => f.includes('test') || f.includes('spec'));
if (!hasTests) {
recommendations.push('Consider adding unit tests to improve code quality');
}
return recommendations;
}
/**
* Save scan results to file
* @param {Object} results - Scan results
* @param {string} outputPath - Output file path
* @param {ScanLoggingConfig} logger - Logger instance
*/
async function saveResults(results, outputPath, logger) {
try {
// Ensure output directory exists
const outputDir = path.dirname(outputPath);
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
// Write results to file
fs.writeFileSync(outputPath, JSON.stringify(results, null, 2));
logger.info(`Scan results saved to: ${outputPath}`);
} catch (error) {
logger.error(`Failed to save results: ${error.message}`);
throw error;
}
}