Compare commits
1 Commits
docs/auto-
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
57ed3a37e4 |
23
.changeset/intelligent-scan-command.md
Normal file
23
.changeset/intelligent-scan-command.md
Normal 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
|
||||
```
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
328
scripts/modules/task-manager/scan-project/ai-analysis.js
Normal file
328
scripts/modules/task-manager/scan-project/ai-analysis.js
Normal 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)
|
||||
};
|
||||
}
|
||||
3
scripts/modules/task-manager/scan-project/index.js
Normal file
3
scripts/modules/task-manager/scan-project/index.js
Normal 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';
|
||||
61
scripts/modules/task-manager/scan-project/scan-config.js
Normal file
61
scripts/modules/task-manager/scan-project/scan-config.js
Normal 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');
|
||||
}
|
||||
}
|
||||
422
scripts/modules/task-manager/scan-project/scan-helpers.js
Normal file
422
scripts/modules/task-manager/scan-project/scan-helpers.js
Normal 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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
441
scripts/modules/task-manager/scan-project/scan-project.js
Normal file
441
scripts/modules/task-manager/scan-project/scan-project.js
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user