Compare commits
1 Commits
ralph/fix/
...
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",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ai-sdk/amazon-bedrock": "^2.2.9",
|
"@ai-sdk/amazon-bedrock": "^2.2.9",
|
||||||
|
"@ast-grep/cli": "^0.29.0",
|
||||||
"@ai-sdk/anthropic": "^1.2.10",
|
"@ai-sdk/anthropic": "^1.2.10",
|
||||||
"@ai-sdk/azure": "^1.3.17",
|
"@ai-sdk/azure": "^1.3.17",
|
||||||
"@ai-sdk/google": "^1.2.13",
|
"@ai-sdk/google": "^1.2.13",
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ import {
|
|||||||
validateStrength
|
validateStrength
|
||||||
} from './task-manager.js';
|
} from './task-manager.js';
|
||||||
|
|
||||||
|
import { scanProject } from './task-manager/scan-project/index.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
moveTasksBetweenTags,
|
moveTasksBetweenTags,
|
||||||
MoveTaskError,
|
MoveTaskError,
|
||||||
@@ -5067,6 +5069,110 @@ Examples:
|
|||||||
process.exit(1);
|
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;
|
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