Compare commits
8 Commits
claude/iss
...
claude/iss
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b44d6a680 | ||
|
|
a0815ba50e | ||
|
|
7528399729 | ||
|
|
967335dfcf | ||
|
|
22bdee1892 | ||
|
|
ee822d9567 | ||
|
|
c0e856fdf0 | ||
|
|
cbbbae8ed6 |
@@ -1,23 +0,0 @@
|
|||||||
---
|
|
||||||
"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
|
|
||||||
```
|
|
||||||
8
.changeset/spotty-moments-trade.md
Normal file
8
.changeset/spotty-moments-trade.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
"task-master-ai": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix set-status for subtasks:
|
||||||
|
|
||||||
|
- Parent tasks are now set as `done` when subtasks are all `done`
|
||||||
|
- Parent tasks are now set as `in-progress` when at least one subtask is `in-progress` or `done`
|
||||||
15
CHANGELOG.md
15
CHANGELOG.md
@@ -1,20 +1,5 @@
|
|||||||
# task-master-ai
|
# task-master-ai
|
||||||
|
|
||||||
## 0.27.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1254](https://github.com/eyaltoledano/claude-task-master/pull/1254) [`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fixed issue where `tm show` command could not find subtasks using dotted notation IDs (e.g., '8.1').
|
|
||||||
- The command now properly searches within parent task subtasks and returns the correct subtask information.
|
|
||||||
|
|
||||||
## 0.27.2
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- [#1248](https://github.com/eyaltoledano/claude-task-master/pull/1248) [`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix set-status for subtasks:
|
|
||||||
- Parent tasks are now set as `done` when subtasks are all `done`
|
|
||||||
- Parent tasks are now set as `in-progress` when at least one subtask is `in-progress` or `done`
|
|
||||||
|
|
||||||
## 0.27.1
|
## 0.27.1
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -1,19 +1,5 @@
|
|||||||
# Change Log
|
# Change Log
|
||||||
|
|
||||||
## 0.25.4
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`af53525`](https://github.com/eyaltoledano/claude-task-master/commit/af53525cbc660a595b67d4bb90d906911c71f45d)]:
|
|
||||||
- task-master-ai@0.27.3
|
|
||||||
|
|
||||||
## 0.25.3
|
|
||||||
|
|
||||||
### Patch Changes
|
|
||||||
|
|
||||||
- Updated dependencies [[`044a7bf`](https://github.com/eyaltoledano/claude-task-master/commit/044a7bfc98049298177bc655cf341d7a8b6a0011)]:
|
|
||||||
- task-master-ai@0.27.2
|
|
||||||
|
|
||||||
## 0.25.2
|
## 0.25.2
|
||||||
|
|
||||||
### Patch Changes
|
### Patch Changes
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"displayName": "TaskMaster",
|
"displayName": "TaskMaster",
|
||||||
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
|
||||||
"version": "0.25.4",
|
"version": "0.25.2",
|
||||||
"publisher": "Hamster",
|
"publisher": "Hamster",
|
||||||
"icon": "assets/icon.png",
|
"icon": "assets/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
"check-types": "tsc --noEmit"
|
"check-types": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "0.27.3"
|
"task-master-ai": "0.27.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.3",
|
"version": "0.27.1",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.3",
|
"version": "0.27.1",
|
||||||
"license": "MIT WITH Commons-Clause",
|
"license": "MIT WITH Commons-Clause",
|
||||||
"workspaces": [
|
"workspaces": [
|
||||||
"apps/*",
|
"apps/*",
|
||||||
@@ -357,9 +357,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"apps/extension": {
|
"apps/extension": {
|
||||||
"version": "0.25.4",
|
"version": "0.25.2",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"task-master-ai": "0.27.3"
|
"task-master-ai": "0.27.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "task-master-ai",
|
"name": "task-master-ai",
|
||||||
"version": "0.27.3",
|
"version": "0.27.1",
|
||||||
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -53,7 +53,6 @@
|
|||||||
"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",
|
||||||
|
|||||||
@@ -135,28 +135,15 @@ export class TaskService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single task by ID - delegates to storage layer
|
* Get a single task by ID
|
||||||
*/
|
*/
|
||||||
async getTask(taskId: string, tag?: string): Promise<Task | null> {
|
async getTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||||
// Use provided tag or get active tag
|
const result = await this.getTaskList({
|
||||||
const activeTag = tag || this.getActiveTag();
|
tag,
|
||||||
|
includeSubtasks: true
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
return result.tasks.find((t) => t.id === taskId) || null;
|
||||||
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
|
|
||||||
return await this.storage.loadTask(String(taskId), activeTag);
|
|
||||||
} catch (error) {
|
|
||||||
throw new TaskMasterError(
|
|
||||||
`Failed to get task ${taskId}`,
|
|
||||||
ERROR_CODES.STORAGE_ERROR,
|
|
||||||
{
|
|
||||||
operation: 'getTask',
|
|
||||||
resource: 'task',
|
|
||||||
taskId: String(taskId),
|
|
||||||
tag: activeTag
|
|
||||||
},
|
|
||||||
error as Error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -105,65 +105,9 @@ export class FileStorage implements IStorage {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Load a single task by ID from the tasks.json file
|
* Load a single task by ID from the tasks.json file
|
||||||
* Handles both regular tasks and subtasks (with dotted notation like "1.2")
|
|
||||||
*/
|
*/
|
||||||
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
async loadTask(taskId: string, tag?: string): Promise<Task | null> {
|
||||||
const tasks = await this.loadTasks(tag);
|
const tasks = await this.loadTasks(tag);
|
||||||
|
|
||||||
// Check if this is a subtask (contains a dot)
|
|
||||||
if (taskId.includes('.')) {
|
|
||||||
const [parentId, subtaskId] = taskId.split('.');
|
|
||||||
const parentTask = tasks.find((t) => String(t.id) === parentId);
|
|
||||||
|
|
||||||
if (!parentTask || !parentTask.subtasks) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const subtask = parentTask.subtasks.find(
|
|
||||||
(st) => String(st.id) === subtaskId
|
|
||||||
);
|
|
||||||
if (!subtask) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toFullSubId = (maybeDotId: string | number): string => {
|
|
||||||
const depId = String(maybeDotId);
|
|
||||||
return depId.includes('.') ? depId : `${parentTask.id}.${depId}`;
|
|
||||||
};
|
|
||||||
const resolvedDependencies =
|
|
||||||
subtask.dependencies?.map((dep) => toFullSubId(dep)) ?? [];
|
|
||||||
|
|
||||||
// Return a Task-like object for the subtask with the full dotted ID
|
|
||||||
// Following the same pattern as findTaskById in utils.js
|
|
||||||
const subtaskResult = {
|
|
||||||
...subtask,
|
|
||||||
id: taskId, // Use the full dotted ID
|
|
||||||
title: subtask.title || `Subtask ${subtaskId}`,
|
|
||||||
description: subtask.description || '',
|
|
||||||
status: subtask.status || 'pending',
|
|
||||||
priority: subtask.priority || parentTask.priority || 'medium',
|
|
||||||
dependencies: resolvedDependencies,
|
|
||||||
details: subtask.details || '',
|
|
||||||
testStrategy: subtask.testStrategy || '',
|
|
||||||
subtasks: [],
|
|
||||||
tags: parentTask.tags || [],
|
|
||||||
assignee: subtask.assignee || parentTask.assignee,
|
|
||||||
complexity: subtask.complexity || parentTask.complexity,
|
|
||||||
createdAt: subtask.createdAt || parentTask.createdAt,
|
|
||||||
updatedAt: subtask.updatedAt || parentTask.updatedAt,
|
|
||||||
// Add reference to parent task for context (like utils.js does)
|
|
||||||
parentTask: {
|
|
||||||
id: parentTask.id,
|
|
||||||
title: parentTask.title,
|
|
||||||
status: parentTask.status
|
|
||||||
},
|
|
||||||
isSubtask: true
|
|
||||||
};
|
|
||||||
|
|
||||||
return subtaskResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle regular task lookup
|
|
||||||
return tasks.find((task) => String(task.id) === String(taskId)) || null;
|
return tasks.find((task) => String(task.id) === String(taskId)) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,8 +53,6 @@ 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,
|
||||||
@@ -5069,110 +5067,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,328 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
// Main entry point for scan-project module
|
|
||||||
export { default } from './scan-project.js';
|
|
||||||
export { default as scanProject } from './scan-project.js';
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,441 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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