Compare commits

...

19 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
github-actions[bot]
51a351760c Version Packages (#1243)
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-24 19:21:21 +02:00
Ralph Khreish
732b2c61ad Merge pull request #1233 from eyaltoledano/ralph/chore/fix.ci.failure.main 2025-09-24 17:10:42 +02:00
Ralph Khreish
4c2801d5eb chore: run format and fix CI 2025-09-24 11:00:04 +02:00
Ralph Khreish
c911608f60 chore: last round of touchups and bug fixes 2025-09-24 10:57:17 +02:00
github-actions[bot]
8f1497407f chore: rc version bump 2025-09-23 20:43:32 +00:00
Ralph Khreish
10b64ec6f5 chore: re-enter rc mode for a last pre-release 2025-09-23 22:38:55 +02:00
Ralph Khreish
1a1879483b chore: do final test 2025-09-23 21:47:37 +02:00
Ralph Khreish
d691cbb7ae chore: CI fix format 2025-09-23 20:27:41 +02:00
Ralph Khreish
1b7c9637a5 chore: fix CI and tsdown config 2025-09-23 20:24:11 +02:00
Ralph Khreish
9ff5f158d5 chore: fix format 2025-09-23 19:27:57 +02:00
Ralph Khreish
b2ff06e8c5 fix: CI and unit tests 2025-09-23 19:26:02 +02:00
Ralph Khreish
6438f6c7c8 chore: exit pre-release mode and format 2025-09-23 11:46:20 +02:00
Ralph Khreish
6bbd777552 chore: fix --version weird error 2025-09-23 11:45:46 +02:00
github-actions[bot]
100482722f chore: rc version bump 2025-09-23 09:10:24 +00:00
108 changed files with 1901 additions and 189 deletions

View File

@@ -1,5 +0,0 @@
---
"task-master-ai": patch
---
Fix module not found for new 0.27.0 release

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,41 @@
# 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
- [#1232](https://github.com/eyaltoledano/claude-task-master/pull/1232) [`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix module not found for new 0.27.0 release
- [#1233](https://github.com/eyaltoledano/claude-task-master/pull/1233) [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix Zed MCP configuration by adding required "source" property
- Add "source": "custom" property to task-master-ai server in Zed settings.json
## 0.27.1-rc.1
### Patch Changes
- [#1233](https://github.com/eyaltoledano/claude-task-master/pull/1233) [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - One last testing final final
## 0.27.1-rc.0
### Patch Changes
- [#1232](https://github.com/eyaltoledano/claude-task-master/pull/1232) [`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e) Thanks [@Crunchyman-ralph](https://github.com/Crunchyman-ralph)! - Fix module not found for new 0.27.0 release
## 0.27.0
### Minor Changes

View File

@@ -1,5 +1,12 @@
# @tm/cli
## null
### Patch Changes
- Updated dependencies []:
- @tm/core@null
## 0.27.0
### Patch Changes

View File

@@ -18,7 +18,8 @@ export * as ui from './utils/ui.js';
export {
checkForUpdate,
performAutoUpdate,
displayUpgradeNotification
displayUpgradeNotification,
compareVersions
} from './utils/auto-update.js';
// Re-export commonly used types from tm-core

View File

@@ -7,7 +7,6 @@ import https from 'https';
import chalk from 'chalk';
import ora from 'ora';
import boxen from 'boxen';
import packageJson from '../../../../package.json' with { type: 'json' };
export interface UpdateInfo {
currentVersion: string;
@@ -16,15 +15,18 @@ export interface UpdateInfo {
}
/**
* Get current version from package.json
* Get current version from build-time injected environment variable
*/
function getCurrentVersion(): string {
try {
return packageJson.version;
} catch (error) {
console.warn('Could not read package.json for version info');
return '0.0.0';
// Version is injected at build time via TM_PUBLIC_VERSION
const version = process.env.TM_PUBLIC_VERSION;
if (version && version !== 'unknown') {
return version;
}
// Fallback for development or if injection failed
console.warn('Could not read version from TM_PUBLIC_VERSION, using fallback');
return '0.0.0';
}
/**
@@ -33,7 +35,7 @@ function getCurrentVersion(): string {
* @param v2 - Second version
* @returns -1 if v1 < v2, 0 if v1 = v2, 1 if v1 > v2
*/
function compareVersions(v1: string, v2: string): number {
export function compareVersions(v1: string, v2: string): number {
const toParts = (v: string) => {
const [core, pre = ''] = v.split('-', 2);
const nums = core.split('.').map((n) => Number.parseInt(n, 10) || 0);

View File

@@ -1,5 +1,7 @@
# docs
## 0.0.4
## 0.0.3
## 0.0.2

View File

@@ -1,6 +1,6 @@
{
"name": "docs",
"version": "0.0.3",
"version": "0.0.4",
"private": true,
"description": "Task Master documentation powered by Mintlify",
"scripts": {

View File

@@ -1,5 +1,40 @@
# 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
- Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e), [`c911608`](https://github.com/eyaltoledano/claude-task-master/commit/c911608f60454253f4e024b57ca84e5a5a53f65c), [`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
- task-master-ai@0.27.1
## 0.25.2-rc.1
### Patch Changes
- Updated dependencies [[`1a18794`](https://github.com/eyaltoledano/claude-task-master/commit/1a1879483b86c118a4e46c02cbf4acebfcf6bcf9)]:
- task-master-ai@0.27.1-rc.1
## 0.25.2-rc.0
### Patch Changes
- Updated dependencies [[`f487736`](https://github.com/eyaltoledano/claude-task-master/commit/f487736670ef8c484059f676293777eabb249c9e)]:
- task-master-ai@0.27.1-rc.0
## 0.25.0
### Minor 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.1",
"version": "0.25.4",
"publisher": "Hamster",
"icon": "assets/icon.png",
"engines": {
@@ -240,7 +240,7 @@
"check-types": "tsc --noEmit"
},
"dependencies": {
"task-master-ai": "*"
"task-master-ai": "0.27.3"
},
"devDependencies": {
"@dnd-kit/core": "^6.3.1",

10
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "task-master-ai",
"version": "0.27.0",
"version": "0.27.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "task-master-ai",
"version": "0.27.0",
"version": "0.27.3",
"license": "MIT WITH Commons-Clause",
"workspaces": [
"apps/*",
@@ -351,15 +351,15 @@
}
},
"apps/docs": {
"version": "0.0.3",
"version": "0.0.4",
"devDependencies": {
"mintlify": "^4.2.111"
}
},
"apps/extension": {
"version": "0.25.1",
"version": "0.25.4",
"dependencies": {
"task-master-ai": "*"
"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.0",
"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

@@ -1,3 +1,5 @@
# @tm/build-config
## null
## 1.0.1

View File

@@ -1,5 +1,7 @@
# Changelog
## null
## 0.26.1
All notable changes to the @task-master/tm-core package will be documented in this file.

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

@@ -16,8 +16,6 @@
import fs from 'fs';
import path from 'path';
import readline from 'readline';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import chalk from 'chalk';
import figlet from 'figlet';
import boxen from 'boxen';
@@ -49,9 +47,6 @@ import {
GITIGNORE_FILE
} from '../src/constants/paths.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Define log levels
const LOG_LEVELS = {
debug: 0,
@@ -619,13 +614,7 @@ function createProjectStructure(
// Copy .gitignore with GitTasks preference
try {
const gitignoreTemplatePath = path.join(
__dirname,
'..',
'assets',
'gitignore'
);
const templateContent = fs.readFileSync(gitignoreTemplatePath, 'utf8');
const templateContent = readAsset('gitignore', 'utf8');
manageGitignoreFile(
path.join(targetDir, GITIGNORE_FILE),
templateContent,

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;
}
@@ -5077,27 +5183,9 @@ Examples:
function setupCLI() {
// Create a new program instance
const programInstance = new Command()
.name('dev')
.name('task-master')
.description('AI-driven development task management')
.version(() => {
// Read version directly from package.json ONLY
try {
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(
fs.readFileSync(packageJsonPath, 'utf8')
);
return packageJson.version;
}
} catch (error) {
// Silently fall back to 'unknown'
log(
'warn',
'Could not read package.json for version info in .version()'
);
}
return 'unknown'; // Default fallback if package.json fails
})
.version(process.env.TM_PUBLIC_VERSION || 'unknown')
.helpOption('-h, --help', 'Display help')
.addHelpCommand(false); // Disable default help command
@@ -5126,8 +5214,9 @@ function setupCLI() {
*/
async function runCLI(argv = process.argv) {
try {
// Display banner if not in a pipe
if (process.stdout.isTTY) {
// Display banner if not in a pipe (except for init command which has its own banner)
const isInitCommand = argv.includes('init');
if (process.stdout.isTTY && !isInitCommand) {
displayBanner();
}

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');
}
}

Some files were not shown because too many files have changed in this diff Show More