Compare commits

...

5 Commits

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

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

Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
2025-10-07 13:48:18 +00:00
github-actions[bot]
3b3dbabed1 Version Packages (#1255)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-27 08:56:38 +02:00
Ralph Khreish
af53525cbc fix: handle subtasks in getTask method (#1254)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-26 20:58:15 +02:00
github-actions[bot]
b7f32eac5a Version Packages (#1249)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
2025-09-26 01:06:52 +02:00
Ralph Khreish
044a7bfc98 fix: implement subtask status update functionality (#1248)
Co-authored-by: Ralph Khreish <Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: claude[bot] <209825114+claude[bot]@users.noreply.github.com>
2025-09-26 01:01:55 +02:00
16 changed files with 1758 additions and 55 deletions

View File

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

View File

@@ -1,5 +1,20 @@
# 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
### Patch Changes

View File

@@ -1,5 +1,19 @@
# 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
### Patch Changes

View File

@@ -3,7 +3,7 @@
"private": true,
"displayName": "TaskMaster",
"description": "A visual Kanban board interface for TaskMaster projects in VS Code",
"version": "0.25.2",
"version": "0.25.4",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
@@ -240,7 +240,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "0.27.1"
"task-master-ai": "0.27.3"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",

8
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.27.1",
"version": "0.27.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.27.1",
"version": "0.27.3",
"license": "MIT WITH Commons-Clause",
"workspaces": [
"apps/*",
@@ -357,9 +357,9 @@
}
},
"apps/extension": {
"version": "0.25.2",
"version": "0.25.4",
"dependencies": {
"task-master-ai": "0.27.1"
"task-master-ai": "0.27.3"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",

View File

@@ -1,6 +1,6 @@
{
"name": "task-master-ai",
"version": "0.27.1",
"version": "0.27.3",
"description": "A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.",
"main": "index.js",
"type": "module",
@@ -53,6 +53,7 @@
"license": "MIT WITH Commons-Clause",
"dependencies": {
"@ai-sdk/amazon-bedrock": "^2.2.9",
"@ast-grep/cli": "^0.29.0",
"@ai-sdk/anthropic": "^1.2.10",
"@ai-sdk/azure": "^1.3.17",
"@ai-sdk/google": "^1.2.13",

View File

@@ -3,7 +3,17 @@
* This file defines the contract for all storage implementations
*/
import type { Task, TaskMetadata } from '../types/index.js';
import type { Task, TaskMetadata, TaskStatus } from '../types/index.js';
/**
* Result type for updateTaskStatus operations
*/
export interface UpdateStatusResult {
success: boolean;
oldStatus: TaskStatus;
newStatus: TaskStatus;
taskId: string;
}
/**
* Interface for storage operations on tasks
@@ -54,6 +64,19 @@ export interface IStorage {
tag?: string
): Promise<void>;
/**
* Update task or subtask status by ID
* @param taskId - ID of the task or subtask (e.g., "1" or "1.2")
* @param newStatus - New status to set
* @param tag - Optional tag context for the task
* @returns Promise that resolves to update result with old and new status
*/
updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult>;
/**
* Delete a task by ID
* @param taskId - ID of the task to delete
@@ -191,6 +214,11 @@ export abstract class BaseStorage implements IStorage {
updates: Partial<Task>,
tag?: string
): Promise<void>;
abstract updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult>;
abstract deleteTask(taskId: string, tag?: string): Promise<void>;
abstract exists(tag?: string): Promise<boolean>;
abstract loadMetadata(tag?: string): Promise<TaskMetadata | null>;

View File

@@ -135,15 +135,28 @@ export class TaskService {
}
/**
* Get a single task by ID
* Get a single task by ID - delegates to storage layer
*/
async getTask(taskId: string, tag?: string): Promise<Task | null> {
const result = await this.getTaskList({
tag,
includeSubtasks: true
});
// Use provided tag or get active tag
const activeTag = tag || this.getActiveTag();
return result.tasks.find((t) => t.id === taskId) || null;
try {
// 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
);
}
}
/**
@@ -446,7 +459,7 @@ export class TaskService {
}
/**
* Update task status
* Update task status - delegates to storage layer which handles storage-specific logic
*/
async updateTaskStatus(
taskId: string | number,
@@ -468,49 +481,28 @@ export class TaskService {
// Use provided tag or get active tag
const activeTag = tag || this.getActiveTag();
const taskIdStr = String(taskId);
// TODO: For now, assume it's a regular task and just try to update directly
// In the future, we can add subtask support if needed
if (taskIdStr.includes('.')) {
throw new TaskMasterError(
'Subtask status updates not yet supported in API storage',
ERROR_CODES.NOT_IMPLEMENTED
);
}
// Get the current task to get old status (simple, direct approach)
let currentTask: Task | null;
try {
// Try to get the task directly
currentTask = await this.storage.loadTask(taskIdStr, activeTag);
// Delegate to storage layer which handles the specific logic for tasks vs subtasks
return await this.storage.updateTaskStatus(
taskIdStr,
newStatus,
activeTag
);
} catch (error) {
throw new TaskMasterError(
`Failed to load task ${taskIdStr}`,
ERROR_CODES.TASK_NOT_FOUND,
{ taskId: taskIdStr },
`Failed to update task status for ${taskIdStr}`,
ERROR_CODES.STORAGE_ERROR,
{
operation: 'updateTaskStatus',
resource: 'task',
taskId: taskIdStr,
newStatus,
tag: activeTag
},
error as Error
);
}
if (!currentTask) {
throw new TaskMasterError(
`Task ${taskIdStr} not found`,
ERROR_CODES.TASK_NOT_FOUND
);
}
const oldStatus = currentTask.status;
// Simple, direct update - just change the status
await this.storage.updateTask(taskIdStr, { status: newStatus }, activeTag);
return {
success: true,
oldStatus,
newStatus,
taskId: taskIdStr
};
}
}

View File

@@ -5,9 +5,15 @@
import type {
IStorage,
StorageStats
StorageStats,
UpdateStatusResult
} from '../interfaces/storage.interface.js';
import type { Task, TaskMetadata, TaskTag } from '../types/index.js';
import type {
Task,
TaskMetadata,
TaskTag,
TaskStatus
} from '../types/index.js';
import { ERROR_CODES, TaskMasterError } from '../errors/task-master-error.js';
import { TaskRepository } from '../repositories/task-repository.interface.js';
import { SupabaseTaskRepository } from '../repositories/supabase-task-repository.js';
@@ -485,6 +491,62 @@ export class ApiStorage implements IStorage {
}
}
/**
* Update task or subtask status by ID - for API storage
*/
async updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult> {
await this.ensureInitialized();
try {
const existingTask = await this.retryOperation(() =>
this.repository.getTask(this.projectId, taskId)
);
if (!existingTask) {
throw new Error(`Task ${taskId} not found`);
}
const oldStatus = existingTask.status;
if (oldStatus === newStatus) {
return {
success: true,
oldStatus,
newStatus,
taskId
};
}
// Update the task/subtask status
await this.retryOperation(() =>
this.repository.updateTask(this.projectId, taskId, {
status: newStatus,
updatedAt: new Date().toISOString()
})
);
// Note: Parent status auto-adjustment is handled by the backend API service
// which has its own business logic for managing task relationships
return {
success: true,
oldStatus,
newStatus,
taskId
};
} catch (error) {
throw new TaskMasterError(
'Failed to update task status via API',
ERROR_CODES.STORAGE_ERROR,
{ operation: 'updateTaskStatus', taskId, newStatus, tag },
error as Error
);
}
}
/**
* Get all available tags
*/

View File

@@ -2,10 +2,11 @@
* @fileoverview Refactored file-based storage implementation for Task Master
*/
import type { Task, TaskMetadata } from '../../types/index.js';
import type { Task, TaskMetadata, TaskStatus } from '../../types/index.js';
import type {
IStorage,
StorageStats
StorageStats,
UpdateStatusResult
} from '../../interfaces/storage.interface.js';
import { FormatHandler } from './format-handler.js';
import { FileOperations } from './file-operations.js';
@@ -104,9 +105,65 @@ export class FileStorage implements IStorage {
/**
* 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> {
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;
}
@@ -281,6 +338,156 @@ export class FileStorage implements IStorage {
await this.saveTasks(tasks, tag);
}
/**
* Update task or subtask status by ID - handles file storage logic with parent/subtask relationships
*/
async updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult> {
const tasks = await this.loadTasks(tag);
// Check if this is a subtask (contains a dot)
if (taskId.includes('.')) {
return this.updateSubtaskStatusInFile(tasks, taskId, newStatus, tag);
}
// Handle regular task update
const taskIndex = tasks.findIndex((t) => String(t.id) === String(taskId));
if (taskIndex === -1) {
throw new Error(`Task ${taskId} not found`);
}
const oldStatus = tasks[taskIndex].status;
if (oldStatus === newStatus) {
return {
success: true,
oldStatus,
newStatus,
taskId: String(taskId)
};
}
tasks[taskIndex] = {
...tasks[taskIndex],
status: newStatus,
updatedAt: new Date().toISOString()
};
await this.saveTasks(tasks, tag);
return {
success: true,
oldStatus,
newStatus,
taskId: String(taskId)
};
}
/**
* Update subtask status within file storage - handles parent status auto-adjustment
*/
private async updateSubtaskStatusInFile(
tasks: Task[],
subtaskId: string,
newStatus: TaskStatus,
tag?: string
): Promise<UpdateStatusResult> {
// Parse the subtask ID to get parent ID and subtask ID
const parts = subtaskId.split('.');
if (parts.length !== 2) {
throw new Error(
`Invalid subtask ID format: ${subtaskId}. Expected format: parentId.subtaskId`
);
}
const [parentId, subIdRaw] = parts;
const subId = subIdRaw.trim();
if (!/^\d+$/.test(subId)) {
throw new Error(
`Invalid subtask ID: ${subId}. Subtask ID must be a positive integer.`
);
}
const subtaskNumericId = Number(subId);
// Find the parent task
const parentTaskIndex = tasks.findIndex(
(t) => String(t.id) === String(parentId)
);
if (parentTaskIndex === -1) {
throw new Error(`Parent task ${parentId} not found`);
}
const parentTask = tasks[parentTaskIndex];
// Find the subtask within the parent task
const subtaskIndex = parentTask.subtasks.findIndex(
(st) => st.id === subtaskNumericId || String(st.id) === subId
);
if (subtaskIndex === -1) {
throw new Error(
`Subtask ${subtaskId} not found in parent task ${parentId}`
);
}
const oldStatus = parentTask.subtasks[subtaskIndex].status || 'pending';
if (oldStatus === newStatus) {
return {
success: true,
oldStatus,
newStatus,
taskId: subtaskId
};
}
const now = new Date().toISOString();
// Update the subtask status
parentTask.subtasks[subtaskIndex] = {
...parentTask.subtasks[subtaskIndex],
status: newStatus,
updatedAt: now
};
// Auto-adjust parent status based on subtask statuses
const subs = parentTask.subtasks;
let parentNewStatus = parentTask.status;
if (subs.length > 0) {
const norm = (s: any) => s.status || 'pending';
const isDoneLike = (s: any) => {
const st = norm(s);
return st === 'done' || st === 'completed';
};
const allDone = subs.every(isDoneLike);
const anyInProgress = subs.some((s) => norm(s) === 'in-progress');
const anyDone = subs.some(isDoneLike);
if (allDone) parentNewStatus = 'done';
else if (anyInProgress || anyDone) parentNewStatus = 'in-progress';
}
// Always bump updatedAt; update status only if changed
tasks[parentTaskIndex] = {
...parentTask,
...(parentNewStatus !== parentTask.status
? { status: parentNewStatus }
: {}),
updatedAt: now
};
await this.saveTasks(tasks, tag);
return {
success: true,
oldStatus,
newStatus,
taskId: subtaskId
};
}
/**
* Delete a task
*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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