refactor: enhance add-task fuzzy search and fix duplicate banner display

- **Remove hardcoded category system** in add-task that always matched 'Task management'
- **Eliminate arbitrary limits** in fuzzy search results (5→25 high relevance, 3→10 medium relevance, 8→20 detailed tasks)
- **Improve semantic weighting** in Fuse.js search (details=3, description=2, title=1.5) for better relevance
- **Fix duplicate banner issue** by removing console.clear() and redundant displayBanner() calls from UI functions
- **Enhance context generation** to rely on semantic similarity rather than rigid pattern matching
- **Preserve terminal history** to address GitHub issue #553 about eating terminal lines
- **Remove displayBanner() calls** from: displayHelp, displayNextTask, displayTaskById, displayComplexityReport, set-task-status, clear-subtasks, dependency-manager functions

The add-task system now provides truly relevant task context based on semantic similarity rather than arbitrary categories and limits, while maintaining a cleaner terminal experience.

Changes span: add-task.js, ui.js, set-task-status.js, clear-subtasks.js, list-tasks.js, dependency-manager.js

Closes #553
This commit is contained in:
Eyal Toledano
2025-06-07 20:23:55 -04:00
parent cc04d53720
commit b1390e4ddf
9 changed files with 3047 additions and 3094 deletions

View File

@@ -1,8 +1,8 @@
{ {
"models": { "models": {
"main": { "main": {
"provider": "openrouter", "provider": "anthropic",
"modelId": "qwen/qwen3-235b-a22b:free", "modelId": "claude-sonnet-4-20250514",
"maxTokens": 50000, "maxTokens": 50000,
"temperature": 0.2 "temperature": 0.2
}, },

View File

@@ -1,35 +0,0 @@
# Task ID: 97
# Title: Create Taskmaster Jingle Implementation
# Status: pending
# Dependencies: 95, 57, 3, 2
# Priority: medium
# Description: Develop a musical jingle system for Taskmaster that plays sound effects during key CLI interactions to enhance user experience.
# Details:
This task involves implementing a sound system that plays audio cues during Taskmaster CLI operations. Key implementation steps include:
1. Audio System Integration:
- Research and select appropriate audio library compatible with Node.js CLI applications
- Implement cross-platform audio playback (Windows, macOS, Linux)
- Create sound configuration options in .taskmasterconfig
2. Jingle Design:
- Define sound triggers for key events (task creation, completion, errors, etc.)
- Create or source appropriate sound files (WAV/MP3 format)
- Implement volume control and mute option in settings
3. CLI Integration:
- Add sound playback to core CLI commands (init, create, update, delete)
- Implement optional sound effects toggle via command line flags
- Ensure audio playback doesn't interfere with CLI performance
4. Documentation:
- Update user guide with sound configuration instructions
- Add troubleshooting section for audio playback issues
# Test Strategy:
1. Verify audio plays correctly during each supported CLI operation
2. Test sound configuration options across different platforms
3. Confirm volume control and mute functionality works as expected
4. Validate that audio playback doesn't affect CLI performance
5. Test edge cases (no audio hardware, invalid sound files, etc.)
6. Ensure sound effects can be disabled via configuration and CLI flags

View File

@@ -5871,22 +5871,6 @@
"parentTaskId": 96 "parentTaskId": 96
} }
] ]
},
{
"id": 97,
"title": "Create Taskmaster Jingle Implementation",
"description": "Develop a musical jingle system for Taskmaster that plays sound effects during key CLI interactions to enhance user experience.",
"details": "This task involves implementing a sound system that plays audio cues during Taskmaster CLI operations. Key implementation steps include:\n\n1. Audio System Integration:\n - Research and select appropriate audio library compatible with Node.js CLI applications\n - Implement cross-platform audio playback (Windows, macOS, Linux)\n - Create sound configuration options in .taskmasterconfig\n\n2. Jingle Design:\n - Define sound triggers for key events (task creation, completion, errors, etc.)\n - Create or source appropriate sound files (WAV/MP3 format)\n - Implement volume control and mute option in settings\n\n3. CLI Integration:\n - Add sound playback to core CLI commands (init, create, update, delete)\n - Implement optional sound effects toggle via command line flags\n - Ensure audio playback doesn't interfere with CLI performance\n\n4. Documentation:\n - Update user guide with sound configuration instructions\n - Add troubleshooting section for audio playback issues",
"testStrategy": "1. Verify audio plays correctly during each supported CLI operation\n2. Test sound configuration options across different platforms\n3. Confirm volume control and mute functionality works as expected\n4. Validate that audio playback doesn't affect CLI performance\n5. Test edge cases (no audio hardware, invalid sound files, etc.)\n6. Ensure sound effects can be disabled via configuration and CLI flags",
"status": "pending",
"dependencies": [
95,
57,
3,
2
],
"priority": "medium",
"subtasks": []
} }
] ]
} }

View File

@@ -3,9 +3,9 @@
* Manages task dependencies and relationships * Manages task dependencies and relationships
*/ */
import path from 'path'; import path from "path";
import chalk from 'chalk'; import chalk from "chalk";
import boxen from 'boxen'; import boxen from "boxen";
import { import {
log, log,
@@ -14,12 +14,12 @@ import {
taskExists, taskExists,
formatTaskId, formatTaskId,
findCycles, findCycles,
isSilentMode isSilentMode,
} from './utils.js'; } from "./utils.js";
import { displayBanner } from './ui.js'; import { displayBanner } from "./ui.js";
import { generateTaskFiles } from './task-manager.js'; import { generateTaskFiles } from "./task-manager.js";
/** /**
* Add a dependency to a task * Add a dependency to a task
@@ -28,17 +28,17 @@ import { generateTaskFiles } from './task-manager.js';
* @param {number|string} dependencyId - ID of the task to add as dependency * @param {number|string} dependencyId - ID of the task to add as dependency
*/ */
async function addDependency(tasksPath, taskId, dependencyId) { async function addDependency(tasksPath, taskId, dependencyId) {
log('info', `Adding dependency ${dependencyId} to task ${taskId}...`); log("info", `Adding dependency ${dependencyId} to task ${taskId}...`);
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json'); log("error", "No valid tasks found in tasks.json");
process.exit(1); process.exit(1);
} }
// Format the task and dependency IDs correctly // Format the task and dependency IDs correctly
const formattedTaskId = const formattedTaskId =
typeof taskId === 'string' && taskId.includes('.') typeof taskId === "string" && taskId.includes(".")
? taskId ? taskId
: parseInt(taskId, 10); : parseInt(taskId, 10);
@@ -47,7 +47,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Check if the dependency task or subtask actually exists // Check if the dependency task or subtask actually exists
if (!taskExists(data.tasks, formattedDependencyId)) { if (!taskExists(data.tasks, formattedDependencyId)) {
log( log(
'error', "error",
`Dependency target ${formattedDependencyId} does not exist in tasks.json` `Dependency target ${formattedDependencyId} does not exist in tasks.json`
); );
process.exit(1); process.exit(1);
@@ -57,20 +57,20 @@ async function addDependency(tasksPath, taskId, dependencyId) {
let targetTask = null; let targetTask = null;
let isSubtask = false; let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) { if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
// Handle dot notation for subtasks (e.g., "1.2") // Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId const [parentId, subtaskId] = formattedTaskId
.split('.') .split(".")
.map((id) => parseInt(id, 10)); .map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId); const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) { if (!parentTask) {
log('error', `Parent task ${parentId} not found.`); log("error", `Parent task ${parentId} not found.`);
process.exit(1); process.exit(1);
} }
if (!parentTask.subtasks) { if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`); log("error", `Parent task ${parentId} has no subtasks.`);
process.exit(1); process.exit(1);
} }
@@ -78,7 +78,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
isSubtask = true; isSubtask = true;
if (!targetTask) { if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`); log("error", `Subtask ${formattedTaskId} not found.`);
process.exit(1); process.exit(1);
} }
} else { } else {
@@ -86,7 +86,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
targetTask = data.tasks.find((t) => t.id === formattedTaskId); targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) { if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`); log("error", `Task ${formattedTaskId} not found.`);
process.exit(1); process.exit(1);
} }
} }
@@ -104,7 +104,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
}) })
) { ) {
log( log(
'warn', "warn",
`Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.` `Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`
); );
return; return;
@@ -112,7 +112,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Check if the task is trying to depend on itself - compare full IDs (including subtask parts) // Check if the task is trying to depend on itself - compare full IDs (including subtask parts)
if (String(formattedTaskId) === String(formattedDependencyId)) { if (String(formattedTaskId) === String(formattedDependencyId)) {
log('error', `Task ${formattedTaskId} cannot depend on itself.`); log("error", `Task ${formattedTaskId} cannot depend on itself.`);
process.exit(1); process.exit(1);
} }
@@ -121,30 +121,30 @@ async function addDependency(tasksPath, taskId, dependencyId) {
let isSelfDependency = false; let isSelfDependency = false;
if ( if (
typeof formattedTaskId === 'string' && typeof formattedTaskId === "string" &&
typeof formattedDependencyId === 'string' && typeof formattedDependencyId === "string" &&
formattedTaskId.includes('.') && formattedTaskId.includes(".") &&
formattedDependencyId.includes('.') formattedDependencyId.includes(".")
) { ) {
const [taskParentId] = formattedTaskId.split('.'); const [taskParentId] = formattedTaskId.split(".");
const [depParentId] = formattedDependencyId.split('.'); const [depParentId] = formattedDependencyId.split(".");
// Only treat it as a self-dependency if both the parent ID and subtask ID are identical // Only treat it as a self-dependency if both the parent ID and subtask ID are identical
isSelfDependency = formattedTaskId === formattedDependencyId; isSelfDependency = formattedTaskId === formattedDependencyId;
// Log for debugging // Log for debugging
log( log(
'debug', "debug",
`Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}` `Adding dependency between subtasks: ${formattedTaskId} depends on ${formattedDependencyId}`
); );
log( log(
'debug', "debug",
`Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}` `Parent IDs: ${taskParentId} and ${depParentId}, Self-dependency check: ${isSelfDependency}`
); );
} }
if (isSelfDependency) { if (isSelfDependency) {
log('error', `Subtask ${formattedTaskId} cannot depend on itself.`); log("error", `Subtask ${formattedTaskId} cannot depend on itself.`);
process.exit(1); process.exit(1);
} }
@@ -158,13 +158,13 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Sort dependencies numerically or by parent task ID first, then subtask ID // Sort dependencies numerically or by parent task ID first, then subtask ID
targetTask.dependencies.sort((a, b) => { targetTask.dependencies.sort((a, b) => {
if (typeof a === 'number' && typeof b === 'number') { if (typeof a === "number" && typeof b === "number") {
return a - b; return a - b;
} else if (typeof a === 'string' && typeof b === 'string') { } else if (typeof a === "string" && typeof b === "string") {
const [aParent, aChild] = a.split('.').map(Number); const [aParent, aChild] = a.split(".").map(Number);
const [bParent, bChild] = b.split('.').map(Number); const [bParent, bChild] = b.split(".").map(Number);
return aParent !== bParent ? aParent - bParent : aChild - bChild; return aParent !== bParent ? aParent - bParent : aChild - bChild;
} else if (typeof a === 'number') { } else if (typeof a === "number") {
return -1; // Numbers come before strings return -1; // Numbers come before strings
} else { } else {
return 1; // Strings come after numbers return 1; // Strings come after numbers
@@ -174,7 +174,7 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Save changes // Save changes
writeJSON(tasksPath, data); writeJSON(tasksPath, data);
log( log(
'success', "success",
`Added dependency ${formattedDependencyId} to task ${formattedTaskId}` `Added dependency ${formattedDependencyId} to task ${formattedTaskId}`
); );
@@ -186,9 +186,9 @@ async function addDependency(tasksPath, taskId, dependencyId) {
`Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`, `Task ${chalk.bold(formattedTaskId)} now depends on ${chalk.bold(formattedDependencyId)}`,
{ {
padding: 1, padding: 1,
borderColor: 'green', borderColor: "green",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1 } margin: { top: 1 },
} }
) )
); );
@@ -197,10 +197,10 @@ async function addDependency(tasksPath, taskId, dependencyId) {
// Generate updated task files // Generate updated task files
await generateTaskFiles(tasksPath, path.dirname(tasksPath)); await generateTaskFiles(tasksPath, path.dirname(tasksPath));
log('info', 'Task files regenerated with updated dependencies.'); log("info", "Task files regenerated with updated dependencies.");
} else { } else {
log( log(
'error', "error",
`Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.` `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`
); );
process.exit(1); process.exit(1);
@@ -214,18 +214,18 @@ async function addDependency(tasksPath, taskId, dependencyId) {
* @param {number|string} dependencyId - ID of the task to remove as dependency * @param {number|string} dependencyId - ID of the task to remove as dependency
*/ */
async function removeDependency(tasksPath, taskId, dependencyId) { async function removeDependency(tasksPath, taskId, dependencyId) {
log('info', `Removing dependency ${dependencyId} from task ${taskId}...`); log("info", `Removing dependency ${dependencyId} from task ${taskId}...`);
// Read tasks file // Read tasks file
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
log('error', 'No valid tasks found.'); log("error", "No valid tasks found.");
process.exit(1); process.exit(1);
} }
// Format the task and dependency IDs correctly // Format the task and dependency IDs correctly
const formattedTaskId = const formattedTaskId =
typeof taskId === 'string' && taskId.includes('.') typeof taskId === "string" && taskId.includes(".")
? taskId ? taskId
: parseInt(taskId, 10); : parseInt(taskId, 10);
@@ -235,20 +235,20 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
let targetTask = null; let targetTask = null;
let isSubtask = false; let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) { if (typeof formattedTaskId === "string" && formattedTaskId.includes(".")) {
// Handle dot notation for subtasks (e.g., "1.2") // Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId const [parentId, subtaskId] = formattedTaskId
.split('.') .split(".")
.map((id) => parseInt(id, 10)); .map((id) => parseInt(id, 10));
const parentTask = data.tasks.find((t) => t.id === parentId); const parentTask = data.tasks.find((t) => t.id === parentId);
if (!parentTask) { if (!parentTask) {
log('error', `Parent task ${parentId} not found.`); log("error", `Parent task ${parentId} not found.`);
process.exit(1); process.exit(1);
} }
if (!parentTask.subtasks) { if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`); log("error", `Parent task ${parentId} has no subtasks.`);
process.exit(1); process.exit(1);
} }
@@ -256,7 +256,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
isSubtask = true; isSubtask = true;
if (!targetTask) { if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`); log("error", `Subtask ${formattedTaskId} not found.`);
process.exit(1); process.exit(1);
} }
} else { } else {
@@ -264,7 +264,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
targetTask = data.tasks.find((t) => t.id === formattedTaskId); targetTask = data.tasks.find((t) => t.id === formattedTaskId);
if (!targetTask) { if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`); log("error", `Task ${formattedTaskId} not found.`);
process.exit(1); process.exit(1);
} }
} }
@@ -272,7 +272,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
// Check if the task has any dependencies // Check if the task has any dependencies
if (!targetTask.dependencies || targetTask.dependencies.length === 0) { if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
log( log(
'info', "info",
`Task ${formattedTaskId} has no dependencies, nothing to remove.` `Task ${formattedTaskId} has no dependencies, nothing to remove.`
); );
return; return;
@@ -287,10 +287,10 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
let depStr = String(dep); let depStr = String(dep);
// Special handling for numeric IDs that might be subtask references // Special handling for numeric IDs that might be subtask references
if (typeof dep === 'number' && dep < 100 && isSubtask) { if (typeof dep === "number" && dep < 100 && isSubtask) {
// It's likely a reference to another subtask in the same parent task // It's likely a reference to another subtask in the same parent task
// Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1) // Convert to full format for comparison (e.g., 2 -> "1.2" for a subtask in task 1)
const [parentId] = formattedTaskId.split('.'); const [parentId] = formattedTaskId.split(".");
depStr = `${parentId}.${dep}`; depStr = `${parentId}.${dep}`;
} }
@@ -299,7 +299,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
if (dependencyIndex === -1) { if (dependencyIndex === -1) {
log( log(
'info', "info",
`Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.` `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`
); );
return; return;
@@ -313,7 +313,7 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
// Success message // Success message
log( log(
'success', "success",
`Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}` `Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`
); );
@@ -325,9 +325,9 @@ async function removeDependency(tasksPath, taskId, dependencyId) {
`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`, `Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
{ {
padding: 1, padding: 1,
borderColor: 'green', borderColor: "green",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1 } margin: { top: 1 },
} }
) )
); );
@@ -358,8 +358,8 @@ function isCircularDependency(tasks, taskId, chain = []) {
let parentIdForSubtask = null; let parentIdForSubtask = null;
// Check if this is a subtask reference (e.g., "1.2") // Check if this is a subtask reference (e.g., "1.2")
if (taskIdStr.includes('.')) { if (taskIdStr.includes(".")) {
const [parentId, subtaskId] = taskIdStr.split('.').map(Number); const [parentId, subtaskId] = taskIdStr.split(".").map(Number);
const parentTask = tasks.find((t) => t.id === parentId); const parentTask = tasks.find((t) => t.id === parentId);
parentIdForSubtask = parentId; // Store parent ID if it's a subtask parentIdForSubtask = parentId; // Store parent ID if it's a subtask
@@ -385,7 +385,7 @@ function isCircularDependency(tasks, taskId, chain = []) {
return task.dependencies.some((depId) => { return task.dependencies.some((depId) => {
let normalizedDepId = String(depId); let normalizedDepId = String(depId);
// Normalize relative subtask dependencies // Normalize relative subtask dependencies
if (typeof depId === 'number' && parentIdForSubtask !== null) { if (typeof depId === "number" && parentIdForSubtask !== null) {
// If the current task is a subtask AND the dependency is a number, // If the current task is a subtask AND the dependency is a number,
// assume it refers to a sibling subtask. // assume it refers to a sibling subtask.
normalizedDepId = `${parentIdForSubtask}.${depId}`; normalizedDepId = `${parentIdForSubtask}.${depId}`;
@@ -413,9 +413,9 @@ function validateTaskDependencies(tasks) {
// Check for self-dependencies // Check for self-dependencies
if (String(depId) === String(task.id)) { if (String(depId) === String(task.id)) {
issues.push({ issues.push({
type: 'self', type: "self",
taskId: task.id, taskId: task.id,
message: `Task ${task.id} depends on itself` message: `Task ${task.id} depends on itself`,
}); });
return; return;
} }
@@ -423,10 +423,10 @@ function validateTaskDependencies(tasks) {
// Check if dependency exists // Check if dependency exists
if (!taskExists(tasks, depId)) { if (!taskExists(tasks, depId)) {
issues.push({ issues.push({
type: 'missing', type: "missing",
taskId: task.id, taskId: task.id,
dependencyId: depId, dependencyId: depId,
message: `Task ${task.id} depends on non-existent task ${depId}` message: `Task ${task.id} depends on non-existent task ${depId}`,
}); });
} }
}); });
@@ -434,9 +434,9 @@ function validateTaskDependencies(tasks) {
// Check for circular dependencies // Check for circular dependencies
if (isCircularDependency(tasks, task.id)) { if (isCircularDependency(tasks, task.id)) {
issues.push({ issues.push({
type: 'circular', type: "circular",
taskId: task.id, taskId: task.id,
message: `Task ${task.id} is part of a circular dependency chain` message: `Task ${task.id} is part of a circular dependency chain`,
}); });
} }
@@ -454,12 +454,12 @@ function validateTaskDependencies(tasks) {
// Check for self-dependencies in subtasks // Check for self-dependencies in subtasks
if ( if (
String(depId) === String(fullSubtaskId) || String(depId) === String(fullSubtaskId) ||
(typeof depId === 'number' && depId === subtask.id) (typeof depId === "number" && depId === subtask.id)
) { ) {
issues.push({ issues.push({
type: 'self', type: "self",
taskId: fullSubtaskId, taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} depends on itself` message: `Subtask ${fullSubtaskId} depends on itself`,
}); });
return; return;
} }
@@ -467,10 +467,10 @@ function validateTaskDependencies(tasks) {
// Check if dependency exists // Check if dependency exists
if (!taskExists(tasks, depId)) { if (!taskExists(tasks, depId)) {
issues.push({ issues.push({
type: 'missing', type: "missing",
taskId: fullSubtaskId, taskId: fullSubtaskId,
dependencyId: depId, dependencyId: depId,
message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}` message: `Subtask ${fullSubtaskId} depends on non-existent task/subtask ${depId}`,
}); });
} }
}); });
@@ -478,9 +478,9 @@ function validateTaskDependencies(tasks) {
// Check for circular dependencies in subtasks // Check for circular dependencies in subtasks
if (isCircularDependency(tasks, fullSubtaskId)) { if (isCircularDependency(tasks, fullSubtaskId)) {
issues.push({ issues.push({
type: 'circular', type: "circular",
taskId: fullSubtaskId, taskId: fullSubtaskId,
message: `Subtask ${fullSubtaskId} is part of a circular dependency chain` message: `Subtask ${fullSubtaskId} is part of a circular dependency chain`,
}); });
} }
}); });
@@ -489,7 +489,7 @@ function validateTaskDependencies(tasks) {
return { return {
valid: issues.length === 0, valid: issues.length === 0,
issues issues,
}; };
} }
@@ -508,13 +508,13 @@ function removeDuplicateDependencies(tasksData) {
const uniqueDeps = [...new Set(task.dependencies)]; const uniqueDeps = [...new Set(task.dependencies)];
return { return {
...task, ...task,
dependencies: uniqueDeps dependencies: uniqueDeps,
}; };
}); });
return { return {
...tasksData, ...tasksData,
tasks tasks,
}; };
} }
@@ -554,7 +554,7 @@ function cleanupSubtaskDependencies(tasksData) {
return { return {
...tasksData, ...tasksData,
tasks tasks,
}; };
} }
@@ -563,17 +563,12 @@ function cleanupSubtaskDependencies(tasksData) {
* @param {string} tasksPath - Path to tasks.json * @param {string} tasksPath - Path to tasks.json
*/ */
async function validateDependenciesCommand(tasksPath, options = {}) { async function validateDependenciesCommand(tasksPath, options = {}) {
// Only display banner if not in silent mode log("info", "Checking for invalid dependencies in task files...");
if (!isSilentMode()) {
displayBanner();
}
log('info', 'Checking for invalid dependencies in task files...');
// Read tasks data // Read tasks data
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json'); log("error", "No valid tasks found in tasks.json");
process.exit(1); process.exit(1);
} }
@@ -587,7 +582,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
}); });
log( log(
'info', "info",
`Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...` `Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`
); );
@@ -597,7 +592,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
if (!validationResult.valid) { if (!validationResult.valid) {
log( log(
'error', "error",
`Dependency validation failed. Found ${validationResult.issues.length} issue(s):` `Dependency validation failed. Found ${validationResult.issues.length} issue(s):`
); );
validationResult.issues.forEach((issue) => { validationResult.issues.forEach((issue) => {
@@ -605,7 +600,7 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
if (issue.dependencyId) { if (issue.dependencyId) {
errorMsg += ` (Dependency: ${issue.dependencyId})`; errorMsg += ` (Dependency: ${issue.dependencyId})`;
} }
log('error', errorMsg); // Log each issue as an error log("error", errorMsg); // Log each issue as an error
}); });
// Optionally exit if validation fails, depending on desired behavior // Optionally exit if validation fails, depending on desired behavior
@@ -616,22 +611,22 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
console.log( console.log(
boxen( boxen(
chalk.red(`Dependency Validation FAILED\n\n`) + chalk.red(`Dependency Validation FAILED\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` + `${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` + `${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
`${chalk.red('Issues found:')} ${validationResult.issues.length}`, // Display count from result `${chalk.red("Issues found:")} ${validationResult.issues.length}`, // Display count from result
{ {
padding: 1, padding: 1,
borderColor: 'red', borderColor: "red",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 } margin: { top: 1, bottom: 1 },
} }
) )
); );
} }
} else { } else {
log( log(
'success', "success",
'No invalid dependencies found - all dependencies are valid' "No invalid dependencies found - all dependencies are valid"
); );
// Show validation summary - only if not in silent mode // Show validation summary - only if not in silent mode
@@ -639,21 +634,21 @@ async function validateDependenciesCommand(tasksPath, options = {}) {
console.log( console.log(
boxen( boxen(
chalk.green(`All Dependencies Are Valid\n\n`) + chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` + `${chalk.cyan("Tasks checked:")} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` + `${chalk.cyan("Subtasks checked:")} ${subtaskCount}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`, `${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
{ {
padding: 1, padding: 1,
borderColor: 'green', borderColor: "green",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 } margin: { top: 1, bottom: 1 },
} }
) )
); );
} }
} }
} catch (error) { } catch (error) {
log('error', 'Error validating dependencies:', error); log("error", "Error validating dependencies:", error);
process.exit(1); process.exit(1);
} }
} }
@@ -691,18 +686,13 @@ function countAllDependencies(tasks) {
* @param {Object} options - Options object * @param {Object} options - Options object
*/ */
async function fixDependenciesCommand(tasksPath, options = {}) { async function fixDependenciesCommand(tasksPath, options = {}) {
// Only display banner if not in silent mode log("info", "Checking for and fixing invalid dependencies in tasks.json...");
if (!isSilentMode()) {
displayBanner();
}
log('info', 'Checking for and fixing invalid dependencies in tasks.json...');
try { try {
// Read tasks data // Read tasks data
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json'); log("error", "No valid tasks found in tasks.json");
process.exit(1); process.exit(1);
} }
@@ -716,7 +706,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
duplicateDependenciesRemoved: 0, duplicateDependenciesRemoved: 0,
circularDependenciesFixed: 0, circularDependenciesFixed: 0,
tasksFixed: 0, tasksFixed: 0,
subtasksFixed: 0 subtasksFixed: 0,
}; };
// First phase: Remove duplicate dependencies in tasks // First phase: Remove duplicate dependencies in tasks
@@ -728,7 +718,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const depIdStr = String(depId); const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) { if (uniqueDeps.has(depIdStr)) {
log( log(
'info', "info",
`Removing duplicate dependency from task ${task.id}: ${depId}` `Removing duplicate dependency from task ${task.id}: ${depId}`
); );
stats.duplicateDependenciesRemoved++; stats.duplicateDependenciesRemoved++;
@@ -750,12 +740,12 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const originalLength = subtask.dependencies.length; const originalLength = subtask.dependencies.length;
subtask.dependencies = subtask.dependencies.filter((depId) => { subtask.dependencies = subtask.dependencies.filter((depId) => {
let depIdStr = String(depId); let depIdStr = String(depId);
if (typeof depId === 'number' && depId < 100) { if (typeof depId === "number" && depId < 100) {
depIdStr = `${task.id}.${depId}`; depIdStr = `${task.id}.${depId}`;
} }
if (uniqueDeps.has(depIdStr)) { if (uniqueDeps.has(depIdStr)) {
log( log(
'info', "info",
`Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}` `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`
); );
stats.duplicateDependenciesRemoved++; stats.duplicateDependenciesRemoved++;
@@ -788,13 +778,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (task.dependencies && Array.isArray(task.dependencies)) { if (task.dependencies && Array.isArray(task.dependencies)) {
const originalLength = task.dependencies.length; const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter((depId) => { task.dependencies = task.dependencies.filter((depId) => {
const isSubtask = typeof depId === 'string' && depId.includes('.'); const isSubtask = typeof depId === "string" && depId.includes(".");
if (isSubtask) { if (isSubtask) {
// Check if the subtask exists // Check if the subtask exists
if (!validSubtaskIds.has(depId)) { if (!validSubtaskIds.has(depId)) {
log( log(
'info', "info",
`Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)` `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`
); );
stats.nonExistentDependenciesRemoved++; stats.nonExistentDependenciesRemoved++;
@@ -804,10 +794,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
} else { } else {
// Check if the task exists // Check if the task exists
const numericId = const numericId =
typeof depId === 'string' ? parseInt(depId, 10) : depId; typeof depId === "string" ? parseInt(depId, 10) : depId;
if (!validTaskIds.has(numericId)) { if (!validTaskIds.has(numericId)) {
log( log(
'info', "info",
`Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)` `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`
); );
stats.nonExistentDependenciesRemoved++; stats.nonExistentDependenciesRemoved++;
@@ -831,9 +821,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// First check for self-dependencies // First check for self-dependencies
const hasSelfDependency = subtask.dependencies.some((depId) => { const hasSelfDependency = subtask.dependencies.some((depId) => {
if (typeof depId === 'string' && depId.includes('.')) { if (typeof depId === "string" && depId.includes(".")) {
return depId === subtaskId; return depId === subtaskId;
} else if (typeof depId === 'number' && depId < 100) { } else if (typeof depId === "number" && depId < 100) {
return depId === subtask.id; return depId === subtask.id;
} }
return false; return false;
@@ -842,13 +832,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (hasSelfDependency) { if (hasSelfDependency) {
subtask.dependencies = subtask.dependencies.filter((depId) => { subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId = const normalizedDepId =
typeof depId === 'number' && depId < 100 typeof depId === "number" && depId < 100
? `${task.id}.${depId}` ? `${task.id}.${depId}`
: String(depId); : String(depId);
if (normalizedDepId === subtaskId) { if (normalizedDepId === subtaskId) {
log( log(
'info', "info",
`Removing self-dependency from subtask ${subtaskId}` `Removing self-dependency from subtask ${subtaskId}`
); );
stats.selfDependenciesRemoved++; stats.selfDependenciesRemoved++;
@@ -860,10 +850,10 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Then check for non-existent dependencies // Then check for non-existent dependencies
subtask.dependencies = subtask.dependencies.filter((depId) => { subtask.dependencies = subtask.dependencies.filter((depId) => {
if (typeof depId === 'string' && depId.includes('.')) { if (typeof depId === "string" && depId.includes(".")) {
if (!validSubtaskIds.has(depId)) { if (!validSubtaskIds.has(depId)) {
log( log(
'info', "info",
`Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)` `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`
); );
stats.nonExistentDependenciesRemoved++; stats.nonExistentDependenciesRemoved++;
@@ -874,7 +864,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Handle numeric dependencies // Handle numeric dependencies
const numericId = const numericId =
typeof depId === 'number' ? depId : parseInt(depId, 10); typeof depId === "number" ? depId : parseInt(depId, 10);
// Small numbers likely refer to subtasks in the same task // Small numbers likely refer to subtasks in the same task
if (numericId < 100) { if (numericId < 100) {
@@ -882,7 +872,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (!validSubtaskIds.has(fullSubtaskId)) { if (!validSubtaskIds.has(fullSubtaskId)) {
log( log(
'info', "info",
`Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}` `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`
); );
stats.nonExistentDependenciesRemoved++; stats.nonExistentDependenciesRemoved++;
@@ -895,7 +885,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
// Otherwise it's a task reference // Otherwise it's a task reference
if (!validTaskIds.has(numericId)) { if (!validTaskIds.has(numericId)) {
log( log(
'info', "info",
`Removing invalid task dependency from subtask ${subtaskId}: ${numericId}` `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`
); );
stats.nonExistentDependenciesRemoved++; stats.nonExistentDependenciesRemoved++;
@@ -914,7 +904,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
}); });
// Third phase: Check for circular dependencies // Third phase: Check for circular dependencies
log('info', 'Checking for circular dependencies...'); log("info", "Checking for circular dependencies...");
// Build the dependency map for subtasks // Build the dependency map for subtasks
const subtaskDependencyMap = new Map(); const subtaskDependencyMap = new Map();
@@ -925,9 +915,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) { if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const normalizedDeps = subtask.dependencies.map((depId) => { const normalizedDeps = subtask.dependencies.map((depId) => {
if (typeof depId === 'string' && depId.includes('.')) { if (typeof depId === "string" && depId.includes(".")) {
return depId; return depId;
} else if (typeof depId === 'number' && depId < 100) { } else if (typeof depId === "number" && depId < 100) {
return `${task.id}.${depId}`; return `${task.id}.${depId}`;
} }
return String(depId); return String(depId);
@@ -955,7 +945,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (cycleEdges.length > 0) { if (cycleEdges.length > 0) {
const [taskId, subtaskNum] = subtaskId const [taskId, subtaskNum] = subtaskId
.split('.') .split(".")
.map((part) => Number(part)); .map((part) => Number(part));
const task = data.tasks.find((t) => t.id === taskId); const task = data.tasks.find((t) => t.id === taskId);
@@ -966,9 +956,9 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
const originalLength = subtask.dependencies.length; const originalLength = subtask.dependencies.length;
const edgesToRemove = cycleEdges.map((edge) => { const edgesToRemove = cycleEdges.map((edge) => {
if (edge.includes('.')) { if (edge.includes(".")) {
const [depTaskId, depSubtaskId] = edge const [depTaskId, depSubtaskId] = edge
.split('.') .split(".")
.map((part) => Number(part)); .map((part) => Number(part));
if (depTaskId === taskId) { if (depTaskId === taskId) {
@@ -983,7 +973,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
subtask.dependencies = subtask.dependencies.filter((depId) => { subtask.dependencies = subtask.dependencies.filter((depId) => {
const normalizedDepId = const normalizedDepId =
typeof depId === 'number' && depId < 100 typeof depId === "number" && depId < 100
? `${taskId}.${depId}` ? `${taskId}.${depId}`
: String(depId); : String(depId);
@@ -992,7 +982,7 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
edgesToRemove.includes(normalizedDepId) edgesToRemove.includes(normalizedDepId)
) { ) {
log( log(
'info', "info",
`Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}` `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`
); );
stats.circularDependenciesFixed++; stats.circularDependenciesFixed++;
@@ -1015,13 +1005,13 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (dataChanged) { if (dataChanged) {
// Save the changes // Save the changes
writeJSON(tasksPath, data); writeJSON(tasksPath, data);
log('success', 'Fixed dependency issues in tasks.json'); log("success", "Fixed dependency issues in tasks.json");
// Regenerate task files // Regenerate task files
log('info', 'Regenerating task files to reflect dependency changes...'); log("info", "Regenerating task files to reflect dependency changes...");
await generateTaskFiles(tasksPath, path.dirname(tasksPath)); await generateTaskFiles(tasksPath, path.dirname(tasksPath));
} else { } else {
log('info', 'No changes needed to fix dependencies'); log("info", "No changes needed to fix dependencies");
} }
// Show detailed statistics report // Show detailed statistics report
@@ -1033,48 +1023,48 @@ async function fixDependenciesCommand(tasksPath, options = {}) {
if (!isSilentMode()) { if (!isSilentMode()) {
if (totalFixedAll > 0) { if (totalFixedAll > 0) {
log('success', `Fixed ${totalFixedAll} dependency issues in total!`); log("success", `Fixed ${totalFixedAll} dependency issues in total!`);
console.log( console.log(
boxen( boxen(
chalk.green(`Dependency Fixes Summary:\n\n`) + chalk.green(`Dependency Fixes Summary:\n\n`) +
`${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` + `${chalk.cyan("Invalid dependencies removed:")} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` + `${chalk.cyan("Self-dependencies removed:")} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` + `${chalk.cyan("Duplicate dependencies removed:")} ${stats.duplicateDependenciesRemoved}\n` +
`${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` + `${chalk.cyan("Circular dependencies fixed:")} ${stats.circularDependenciesFixed}\n\n` +
`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` + `${chalk.cyan("Tasks fixed:")} ${stats.tasksFixed}\n` +
`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`, `${chalk.cyan("Subtasks fixed:")} ${stats.subtasksFixed}\n`,
{ {
padding: 1, padding: 1,
borderColor: 'green', borderColor: "green",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 } margin: { top: 1, bottom: 1 },
} }
) )
); );
} else { } else {
log( log(
'success', "success",
'No dependency issues found - all dependencies are valid' "No dependency issues found - all dependencies are valid"
); );
console.log( console.log(
boxen( boxen(
chalk.green(`All Dependencies Are Valid\n\n`) + chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` + `${chalk.cyan("Tasks checked:")} ${data.tasks.length}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`, `${chalk.cyan("Total dependencies verified:")} ${countAllDependencies(data.tasks)}`,
{ {
padding: 1, padding: 1,
borderColor: 'green', borderColor: "green",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 } margin: { top: 1, bottom: 1 },
} }
) )
); );
} }
} }
} catch (error) { } catch (error) {
log('error', 'Error in fix-dependencies command:', error); log("error", "Error in fix-dependencies command:", error);
process.exit(1); process.exit(1);
} }
} }
@@ -1113,7 +1103,7 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
if (task.subtasks.length > 0) { if (task.subtasks.length > 0) {
const firstSubtask = task.subtasks[0]; const firstSubtask = task.subtasks[0];
log( log(
'debug', "debug",
`Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}` `Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`
); );
firstSubtask.dependencies = []; firstSubtask.dependencies = [];
@@ -1134,11 +1124,11 @@ function ensureAtLeastOneIndependentSubtask(tasksData) {
*/ */
function validateAndFixDependencies(tasksData, tasksPath = null) { function validateAndFixDependencies(tasksData, tasksPath = null) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) { if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
log('error', 'Invalid tasks data'); log("error", "Invalid tasks data");
return false; return false;
} }
log('debug', 'Validating and fixing dependencies...'); log("debug", "Validating and fixing dependencies...");
// Create a deep copy for comparison // Create a deep copy for comparison
const originalData = JSON.parse(JSON.stringify(tasksData)); const originalData = JSON.parse(JSON.stringify(tasksData));
@@ -1184,7 +1174,7 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
if (subtask.dependencies) { if (subtask.dependencies) {
subtask.dependencies = subtask.dependencies.filter((depId) => { subtask.dependencies = subtask.dependencies.filter((depId) => {
// Handle numeric subtask references // Handle numeric subtask references
if (typeof depId === 'number' && depId < 100) { if (typeof depId === "number" && depId < 100) {
const fullSubtaskId = `${task.id}.${depId}`; const fullSubtaskId = `${task.id}.${depId}`;
return taskExists(tasksData.tasks, fullSubtaskId); return taskExists(tasksData.tasks, fullSubtaskId);
} }
@@ -1220,9 +1210,9 @@ function validateAndFixDependencies(tasksData, tasksPath = null) {
if (tasksPath && changesDetected) { if (tasksPath && changesDetected) {
try { try {
writeJSON(tasksPath, tasksData); writeJSON(tasksPath, tasksData);
log('debug', 'Saved dependency fixes to tasks.json'); log("debug", "Saved dependency fixes to tasks.json");
} catch (error) { } catch (error) {
log('error', 'Failed to save dependency fixes to tasks.json', error); log("error", "Failed to save dependency fixes to tasks.json", error);
} }
} }
@@ -1239,5 +1229,5 @@ export {
removeDuplicateDependencies, removeDuplicateDependencies,
cleanupSubtaskDependencies, cleanupSubtaskDependencies,
ensureAtLeastOneIndependentSubtask, ensureAtLeastOneIndependentSubtask,
validateAndFixDependencies validateAndFixDependencies,
}; };

View File

@@ -1,40 +1,42 @@
import path from 'path'; import path from "path";
import chalk from 'chalk'; import chalk from "chalk";
import boxen from 'boxen'; import boxen from "boxen";
import Table from 'cli-table3'; import Table from "cli-table3";
import { z } from 'zod'; import { z } from "zod";
import Fuse from 'fuse.js'; // Import Fuse.js for advanced fuzzy search import Fuse from "fuse.js"; // Import Fuse.js for advanced fuzzy search
import { import {
displayBanner, displayBanner,
getStatusWithColor, getStatusWithColor,
startLoadingIndicator, startLoadingIndicator,
stopLoadingIndicator, stopLoadingIndicator,
displayAiUsageSummary succeedLoadingIndicator,
} from '../ui.js'; failLoadingIndicator,
import { readJSON, writeJSON, log as consoleLog, truncate } from '../utils.js'; displayAiUsageSummary,
import { generateObjectService } from '../ai-services-unified.js'; } from "../ui.js";
import { getDefaultPriority } from '../config-manager.js'; import { readJSON, writeJSON, log as consoleLog, truncate } from "../utils.js";
import generateTaskFiles from './generate-task-files.js'; import { generateObjectService } from "../ai-services-unified.js";
import { getDefaultPriority } from "../config-manager.js";
import generateTaskFiles from "./generate-task-files.js";
// Define Zod schema for the expected AI output object // Define Zod schema for the expected AI output object
const AiTaskDataSchema = z.object({ const AiTaskDataSchema = z.object({
title: z.string().describe('Clear, concise title for the task'), title: z.string().describe("Clear, concise title for the task"),
description: z description: z
.string() .string()
.describe('A one or two sentence description of the task'), .describe("A one or two sentence description of the task"),
details: z details: z
.string() .string()
.describe('In-depth implementation details, considerations, and guidance'), .describe("In-depth implementation details, considerations, and guidance"),
testStrategy: z testStrategy: z
.string() .string()
.describe('Detailed approach for verifying task completion'), .describe("Detailed approach for verifying task completion"),
dependencies: z dependencies: z
.array(z.number()) .array(z.number())
.optional() .optional()
.describe( .describe(
'Array of task IDs that this task depends on (must be completed before this task can start)' "Array of task IDs that this task depends on (must be completed before this task can start)"
) ),
}); });
/** /**
@@ -62,7 +64,7 @@ async function addTask(
dependencies = [], dependencies = [],
priority = null, priority = null,
context = {}, context = {},
outputFormat = 'text', // Default to text for CLI outputFormat = "text", // Default to text for CLI
manualTaskData = null, manualTaskData = null,
useResearch = false useResearch = false
) { ) {
@@ -74,27 +76,27 @@ async function addTask(
? mcpLog // Use MCP logger if provided ? mcpLog // Use MCP logger if provided
: { : {
// Create a wrapper around consoleLog for CLI // Create a wrapper around consoleLog for CLI
info: (...args) => consoleLog('info', ...args), info: (...args) => consoleLog("info", ...args),
warn: (...args) => consoleLog('warn', ...args), warn: (...args) => consoleLog("warn", ...args),
error: (...args) => consoleLog('error', ...args), error: (...args) => consoleLog("error", ...args),
debug: (...args) => consoleLog('debug', ...args), debug: (...args) => consoleLog("debug", ...args),
success: (...args) => consoleLog('success', ...args) success: (...args) => consoleLog("success", ...args),
}; };
const effectivePriority = priority || getDefaultPriority(projectRoot); const effectivePriority = priority || getDefaultPriority(projectRoot);
logFn.info( logFn.info(
`Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(', ') || 'None'}, Research: ${useResearch}, ProjectRoot: ${projectRoot}` `Adding new task with prompt: "${prompt}", Priority: ${effectivePriority}, Dependencies: ${dependencies.join(", ") || "None"}, Research: ${useResearch}, ProjectRoot: ${projectRoot}`
); );
let loadingIndicator = null; let loadingIndicator = null;
let aiServiceResponse = null; // To store the full response from AI service let aiServiceResponse = null; // To store the full response from AI service
// Create custom reporter that checks for MCP log // Create custom reporter that checks for MCP log
const report = (message, level = 'info') => { const report = (message, level = "info") => {
if (mcpLog) { if (mcpLog) {
mcpLog[level](message); mcpLog[level](message);
} else if (outputFormat === 'text') { } else if (outputFormat === "text") {
consoleLog(level, message); consoleLog(level, message);
} }
}; };
@@ -156,7 +158,7 @@ async function addTask(
title: task.title, title: task.title,
description: task.description, description: task.description,
status: task.status, status: task.status,
dependencies: dependencyData dependencies: dependencyData,
}; };
} }
@@ -166,14 +168,14 @@ async function addTask(
// If tasks.json doesn't exist or is invalid, create a new one // If tasks.json doesn't exist or is invalid, create a new one
if (!data || !data.tasks) { if (!data || !data.tasks) {
report('tasks.json not found or invalid. Creating a new one.', 'info'); report("tasks.json not found or invalid. Creating a new one.", "info");
// Create default tasks data structure // Create default tasks data structure
data = { data = {
tasks: [] tasks: [],
}; };
// Ensure the directory exists and write the new file // Ensure the directory exists and write the new file
writeJSON(tasksPath, data); writeJSON(tasksPath, data);
report('Created new tasks.json file with empty tasks array.', 'info'); report("Created new tasks.json file with empty tasks array.", "info");
} }
// Find the highest task ID to determine the next ID // Find the highest task ID to determine the next ID
@@ -182,13 +184,13 @@ async function addTask(
const newTaskId = highestId + 1; const newTaskId = highestId + 1;
// Only show UI box for CLI mode // Only show UI box for CLI mode
if (outputFormat === 'text') { if (outputFormat === "text") {
console.log( console.log(
boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), { boxen(chalk.white.bold(`Creating New Task #${newTaskId}`), {
padding: 1, padding: 1,
borderColor: 'blue', borderColor: "blue",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 } margin: { top: 1, bottom: 1 },
}) })
); );
} }
@@ -202,10 +204,10 @@ async function addTask(
if (invalidDeps.length > 0) { if (invalidDeps.length > 0) {
report( report(
`The following dependencies do not exist or are invalid: ${invalidDeps.join(', ')}`, `The following dependencies do not exist or are invalid: ${invalidDeps.join(", ")}`,
'warn' "warn"
); );
report('Removing invalid dependencies...', 'info'); report("Removing invalid dependencies...", "info");
dependencies = dependencies.filter( dependencies = dependencies.filter(
(depId) => !invalidDeps.includes(depId) (depId) => !invalidDeps.includes(depId)
); );
@@ -240,28 +242,28 @@ async function addTask(
// Check if manual task data is provided // Check if manual task data is provided
if (manualTaskData) { if (manualTaskData) {
report('Using manually provided task data', 'info'); report("Using manually provided task data", "info");
taskData = manualTaskData; taskData = manualTaskData;
report('DEBUG: Taking MANUAL task data path.', 'debug'); report("DEBUG: Taking MANUAL task data path.", "debug");
// Basic validation for manual data // Basic validation for manual data
if ( if (
!taskData.title || !taskData.title ||
typeof taskData.title !== 'string' || typeof taskData.title !== "string" ||
!taskData.description || !taskData.description ||
typeof taskData.description !== 'string' typeof taskData.description !== "string"
) { ) {
throw new Error( throw new Error(
'Manual task data must include at least a title and description.' "Manual task data must include at least a title and description."
); );
} }
} else { } else {
report('DEBUG: Taking AI task generation path.', 'debug'); report("DEBUG: Taking AI task generation path.", "debug");
// --- Refactored AI Interaction --- // --- Refactored AI Interaction ---
report(`Generating task data with AI with prompt:\n${prompt}`, 'info'); report(`Generating task data with AI with prompt:\n${prompt}`, "info");
// Create context string for task creation prompt // Create context string for task creation prompt
let contextTasks = ''; let contextTasks = "";
// Create a dependency map for better understanding of the task relationships // Create a dependency map for better understanding of the task relationships
const taskMap = {}; const taskMap = {};
@@ -272,18 +274,18 @@ async function addTask(
title: t.title, title: t.title,
description: t.description, description: t.description,
dependencies: t.dependencies || [], dependencies: t.dependencies || [],
status: t.status status: t.status,
}; };
}); });
// CLI-only feedback for the dependency analysis // CLI-only feedback for the dependency analysis
if (outputFormat === 'text') { if (outputFormat === "text") {
console.log( console.log(
boxen(chalk.cyan.bold('Task Context Analysis') + '\n', { boxen(chalk.cyan.bold("Task Context Analysis"), {
padding: { top: 0, bottom: 0, left: 1, right: 1 }, padding: { top: 0, bottom: 0, left: 1, right: 1 },
margin: { top: 0, bottom: 0 }, margin: { top: 0, bottom: 0 },
borderColor: 'cyan', borderColor: "cyan",
borderStyle: 'round' borderStyle: "round",
}) })
); );
} }
@@ -314,7 +316,7 @@ async function addTask(
const directDeps = data.tasks.filter((t) => const directDeps = data.tasks.filter((t) =>
numericDependencies.includes(t.id) numericDependencies.includes(t.id)
); );
contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join('\n')}`; contextTasks += `\n${directDeps.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`).join("\n")}`;
// Add an overview of indirect dependencies if present // Add an overview of indirect dependencies if present
const indirectDeps = dependentTasks.filter( const indirectDeps = dependentTasks.filter(
@@ -325,7 +327,7 @@ async function addTask(
contextTasks += `\n${indirectDeps contextTasks += `\n${indirectDeps
.slice(0, 5) .slice(0, 5)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`; .join("\n")}`;
if (indirectDeps.length > 5) { if (indirectDeps.length > 5) {
contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`; contextTasks += `\n- ... and ${indirectDeps.length - 5} more indirect dependencies`;
} }
@@ -336,15 +338,15 @@ async function addTask(
for (const depTask of uniqueDetailedTasks) { for (const depTask of uniqueDetailedTasks) {
const depthInfo = depthMap.get(depTask.id) const depthInfo = depthMap.get(depTask.id)
? ` (depth: ${depthMap.get(depTask.id)})` ? ` (depth: ${depthMap.get(depTask.id)})`
: ''; : "";
const isDirect = numericDependencies.includes(depTask.id) const isDirect = numericDependencies.includes(depTask.id)
? ' [DIRECT DEPENDENCY]' ? " [DIRECT DEPENDENCY]"
: ''; : "";
contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`; contextTasks += `\n\n------ Task ${depTask.id}${isDirect}${depthInfo}: ${depTask.title} ------\n`;
contextTasks += `Description: ${depTask.description}\n`; contextTasks += `Description: ${depTask.description}\n`;
contextTasks += `Status: ${depTask.status || 'pending'}\n`; contextTasks += `Status: ${depTask.status || "pending"}\n`;
contextTasks += `Priority: ${depTask.priority || 'medium'}\n`; contextTasks += `Priority: ${depTask.priority || "medium"}\n`;
// List its dependencies // List its dependencies
if (depTask.dependencies && depTask.dependencies.length > 0) { if (depTask.dependencies && depTask.dependencies.length > 0) {
@@ -354,7 +356,7 @@ async function addTask(
? `Task ${dId}: ${depDepTask.title}` ? `Task ${dId}: ${depDepTask.title}`
: `Task ${dId}`; : `Task ${dId}`;
}); });
contextTasks += `Dependencies: ${depDeps.join(', ')}\n`; contextTasks += `Dependencies: ${depDeps.join(", ")}\n`;
} else { } else {
contextTasks += `Dependencies: None\n`; contextTasks += `Dependencies: None\n`;
} }
@@ -363,7 +365,7 @@ async function addTask(
if (depTask.details) { if (depTask.details) {
const truncatedDetails = const truncatedDetails =
depTask.details.length > 400 depTask.details.length > 400
? depTask.details.substring(0, 400) + '... (truncated)' ? depTask.details.substring(0, 400) + "... (truncated)"
: depTask.details; : depTask.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`; contextTasks += `Implementation Details: ${truncatedDetails}\n`;
} }
@@ -371,19 +373,19 @@ async function addTask(
// Add dependency chain visualization // Add dependency chain visualization
if (dependencyGraphs.length > 0) { if (dependencyGraphs.length > 0) {
contextTasks += '\n\nDependency Chain Visualization:'; contextTasks += "\n\nDependency Chain Visualization:";
// Helper function to format dependency chain as text // Helper function to format dependency chain as text
function formatDependencyChain( function formatDependencyChain(
node, node,
prefix = '', prefix = "",
isLast = true, isLast = true,
depth = 0 depth = 0
) { ) {
if (depth > 3) return ''; // Limit depth to avoid excessive nesting if (depth > 3) return ""; // Limit depth to avoid excessive nesting
const connector = isLast ? '└── ' : '├── '; const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? ' ' : ''; const childPrefix = isLast ? " " : "";
let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`; let result = `\n${prefix}${connector}Task ${node.id}: ${node.title}`;
@@ -409,7 +411,7 @@ async function addTask(
} }
// Show dependency analysis in CLI mode // Show dependency analysis in CLI mode
if (outputFormat === 'text') { if (outputFormat === "text") {
if (directDeps.length > 0) { if (directDeps.length > 0) {
console.log(chalk.gray(` Explicitly specified dependencies:`)); console.log(chalk.gray(` Explicitly specified dependencies:`));
directDeps.forEach((t) => { directDeps.forEach((t) => {
@@ -449,14 +451,14 @@ async function addTask(
// Convert dependency graph to ASCII art for terminal // Convert dependency graph to ASCII art for terminal
function visualizeDependencyGraph( function visualizeDependencyGraph(
node, node,
prefix = '', prefix = "",
isLast = true, isLast = true,
depth = 0 depth = 0
) { ) {
if (depth > 2) return; // Limit depth for display if (depth > 2) return; // Limit depth for display
const connector = isLast ? '└── ' : '├── '; const connector = isLast ? "└── " : "├── ";
const childPrefix = isLast ? ' ' : ''; const childPrefix = isLast ? " " : "";
console.log( console.log(
chalk.blue( chalk.blue(
@@ -492,18 +494,18 @@ async function addTask(
includeScore: true, // Return match scores includeScore: true, // Return match scores
threshold: 0.4, // Lower threshold = stricter matching (range 0-1) threshold: 0.4, // Lower threshold = stricter matching (range 0-1)
keys: [ keys: [
{ name: 'title', weight: 2 }, // Title is most important { name: "title", weight: 1.5 }, // Title is most important
{ name: 'description', weight: 1.5 }, // Description is next { name: "description", weight: 2 }, // Description is very important
{ name: 'details', weight: 0.8 }, // Details is less important { name: "details", weight: 3 }, // Details is most important
// Search dependencies to find tasks that depend on similar things // Search dependencies to find tasks that depend on similar things
{ name: 'dependencyTitles', weight: 0.5 } { name: "dependencyTitles", weight: 0.5 },
], ],
// Sort matches by score (lower is better) // Sort matches by score (lower is better)
shouldSort: true, shouldSort: true,
// Allow searching in nested properties // Allow searching in nested properties
useExtendedSearch: true, useExtendedSearch: true,
// Return up to 15 matches // Return up to 50 matches
limit: 15 limit: 50,
}; };
// Prepare task data with dependencies expanded as titles for better semantic search // Prepare task data with dependencies expanded as titles for better semantic search
@@ -514,15 +516,15 @@ async function addTask(
? task.dependencies ? task.dependencies
.map((depId) => { .map((depId) => {
const depTask = data.tasks.find((t) => t.id === depId); const depTask = data.tasks.find((t) => t.id === depId);
return depTask ? depTask.title : ''; return depTask ? depTask.title : "";
}) })
.filter((title) => title) .filter((title) => title)
.join(' ') .join(" ")
: ''; : "";
return { return {
...task, ...task,
dependencyTitles dependencyTitles,
}; };
}); });
@@ -532,7 +534,7 @@ async function addTask(
// Extract significant words and phrases from the prompt // Extract significant words and phrases from the prompt
const promptWords = prompt const promptWords = prompt
.toLowerCase() .toLowerCase()
.replace(/[^\w\s-]/g, ' ') // Replace non-alphanumeric chars with spaces .replace(/[^\w\s-]/g, " ") // Replace non-alphanumeric chars with spaces
.split(/\s+/) .split(/\s+/)
.filter((word) => word.length > 3); // Words at least 4 chars .filter((word) => word.length > 3); // Words at least 4 chars
@@ -596,75 +598,40 @@ async function addTask(
// Get top N results for context // Get top N results for context
const relatedTasks = allRelevantTasks.slice(0, 8); const relatedTasks = allRelevantTasks.slice(0, 8);
// Also look for tasks with similar purposes or categories
const purposeCategories = [
{ pattern: /(command|cli|flag)/i, label: 'CLI commands' },
{ pattern: /(task|subtask|add)/i, label: 'Task management' },
{ pattern: /(dependency|depend)/i, label: 'Dependency handling' },
{ pattern: /(AI|model|prompt)/i, label: 'AI integration' },
{ pattern: /(UI|display|show)/i, label: 'User interface' },
{ pattern: /(schedule|time|cron)/i, label: 'Scheduling' }, // Added scheduling category
{ pattern: /(config|setting|option)/i, label: 'Configuration' } // Added configuration category
];
promptCategory = purposeCategories.find((cat) =>
cat.pattern.test(prompt)
);
const categoryTasks = promptCategory
? data.tasks
.filter(
(t) =>
promptCategory.pattern.test(t.title) ||
promptCategory.pattern.test(t.description) ||
(t.details && promptCategory.pattern.test(t.details))
)
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
.slice(0, 3)
: [];
// Format basic task overviews // Format basic task overviews
if (relatedTasks.length > 0) { if (relatedTasks.length > 0) {
contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks contextTasks = `\nRelevant tasks identified by semantic similarity:\n${relatedTasks
.map((t, i) => { .map((t, i) => {
const relevanceMarker = i < highRelevance.length ? '' : ''; const relevanceMarker = i < highRelevance.length ? "" : "";
return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`; return `- ${relevanceMarker}Task ${t.id}: ${t.title} - ${t.description}`;
}) })
.join('\n')}`; .join("\n")}`;
}
if (categoryTasks.length > 0) {
contextTasks += `\n\nTasks related to ${promptCategory.label}:\n${categoryTasks
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`;
} }
if ( if (
recentTasks.length > 0 && recentTasks.length > 0 &&
!contextTasks.includes('Recently created tasks') !contextTasks.includes("Recently created tasks")
) { ) {
contextTasks += `\n\nRecently created tasks:\n${recentTasks contextTasks += `\n\nRecently created tasks:\n${recentTasks
.filter((t) => !relatedTasks.some((rt) => rt.id === t.id)) .filter((t) => !relatedTasks.some((rt) => rt.id === t.id))
.slice(0, 3) .slice(0, 3)
.map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`) .map((t) => `- Task ${t.id}: ${t.title} - ${t.description}`)
.join('\n')}`; .join("\n")}`;
} }
// Add detailed information about the most relevant tasks // Add detailed information about the most relevant tasks
const allDetailedTasks = [ const allDetailedTasks = [...relatedTasks.slice(0, 25)];
...relatedTasks.slice(0, 5),
...categoryTasks.slice(0, 2)
];
uniqueDetailedTasks = Array.from( uniqueDetailedTasks = Array.from(
new Map(allDetailedTasks.map((t) => [t.id, t])).values() new Map(allDetailedTasks.map((t) => [t.id, t])).values()
).slice(0, 8); ).slice(0, 20);
if (uniqueDetailedTasks.length > 0) { if (uniqueDetailedTasks.length > 0) {
contextTasks += `\n\nDetailed information about relevant tasks:`; contextTasks += `\n\nDetailed information about relevant tasks:`;
for (const task of uniqueDetailedTasks) { for (const task of uniqueDetailedTasks) {
contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`; contextTasks += `\n\n------ Task ${task.id}: ${task.title} ------\n`;
contextTasks += `Description: ${task.description}\n`; contextTasks += `Description: ${task.description}\n`;
contextTasks += `Status: ${task.status || 'pending'}\n`; contextTasks += `Status: ${task.status || "pending"}\n`;
contextTasks += `Priority: ${task.priority || 'medium'}\n`; contextTasks += `Priority: ${task.priority || "medium"}\n`;
if (task.dependencies && task.dependencies.length > 0) { if (task.dependencies && task.dependencies.length > 0) {
// Format dependency list with titles // Format dependency list with titles
const depList = task.dependencies.map((depId) => { const depList = task.dependencies.map((depId) => {
@@ -673,13 +640,13 @@ async function addTask(
? `Task ${depId} (${depTask.title})` ? `Task ${depId} (${depTask.title})`
: `Task ${depId}`; : `Task ${depId}`;
}); });
contextTasks += `Dependencies: ${depList.join(', ')}\n`; contextTasks += `Dependencies: ${depList.join(", ")}\n`;
} }
// Add implementation details but truncate if too long // Add implementation details but truncate if too long
if (task.details) { if (task.details) {
const truncatedDetails = const truncatedDetails =
task.details.length > 400 task.details.length > 400
? task.details.substring(0, 400) + '... (truncated)' ? task.details.substring(0, 400) + "... (truncated)"
: task.details; : task.details;
contextTasks += `Implementation Details: ${truncatedDetails}\n`; contextTasks += `Implementation Details: ${truncatedDetails}\n`;
} }
@@ -687,7 +654,7 @@ async function addTask(
} }
// Add a concise view of the task dependency structure // Add a concise view of the task dependency structure
contextTasks += '\n\nSummary of task dependencies in the project:'; contextTasks += "\n\nSummary of task dependencies in the project:";
// Get pending/in-progress tasks that might be most relevant based on fuzzy search // Get pending/in-progress tasks that might be most relevant based on fuzzy search
// Prioritize tasks from our similarity search // Prioritize tasks from our similarity search
@@ -695,7 +662,7 @@ async function addTask(
const relevantPendingTasks = data.tasks const relevantPendingTasks = data.tasks
.filter( .filter(
(t) => (t) =>
(t.status === 'pending' || t.status === 'in-progress') && (t.status === "pending" || t.status === "in-progress") &&
// Either in our relevant set OR has relevant words in title/description // Either in our relevant set OR has relevant words in title/description
(relevantTaskIds.has(t.id) || (relevantTaskIds.has(t.id) ||
promptWords.some( promptWords.some(
@@ -709,24 +676,20 @@ async function addTask(
for (const task of relevantPendingTasks) { for (const task of relevantPendingTasks) {
const depsStr = const depsStr =
task.dependencies && task.dependencies.length > 0 task.dependencies && task.dependencies.length > 0
? task.dependencies.join(', ') ? task.dependencies.join(", ")
: 'None'; : "None";
contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`; contextTasks += `\n- Task ${task.id}: depends on [${depsStr}]`;
} }
// Additional analysis of common patterns // Additional analysis of common patterns
const similarPurposeTasks = promptCategory const similarPurposeTasks = data.tasks.filter((t) =>
? data.tasks.filter( prompt.toLowerCase().includes(t.title.toLowerCase())
(t) => );
promptCategory.pattern.test(t.title) ||
promptCategory.pattern.test(t.description)
)
: [];
let commonDeps = []; // Initialize commonDeps let commonDeps = []; // Initialize commonDeps
if (similarPurposeTasks.length > 0) { if (similarPurposeTasks.length > 0) {
contextTasks += `\n\nCommon patterns for ${promptCategory ? promptCategory.label : 'similar'} tasks:`; contextTasks += `\n\nCommon patterns for similar tasks:`;
// Collect dependencies from similar purpose tasks // Collect dependencies from similar purpose tasks
const similarDeps = similarPurposeTasks const similarDeps = similarPurposeTasks
@@ -743,10 +706,10 @@ async function addTask(
// Get most common dependencies for similar tasks // Get most common dependencies for similar tasks
commonDeps = Object.entries(depCounts) commonDeps = Object.entries(depCounts)
.sort((a, b) => b[1] - a[1]) .sort((a, b) => b[1] - a[1])
.slice(0, 5); .slice(0, 10);
if (commonDeps.length > 0) { if (commonDeps.length > 0) {
contextTasks += '\nMost common dependencies for similar tasks:'; contextTasks += "\nMost common dependencies for similar tasks:";
commonDeps.forEach(([depId, count]) => { commonDeps.forEach(([depId, count]) => {
const depTask = data.tasks.find((t) => t.id === parseInt(depId)); const depTask = data.tasks.find((t) => t.id === parseInt(depId));
if (depTask) { if (depTask) {
@@ -757,10 +720,10 @@ async function addTask(
} }
// Show fuzzy search analysis in CLI mode // Show fuzzy search analysis in CLI mode
if (outputFormat === 'text') { if (outputFormat === "text") {
console.log( console.log(
chalk.gray( chalk.gray(
` Fuzzy search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords` ` Context search across ${data.tasks.length} tasks using full prompt and ${promptWords.length} keywords`
) )
); );
@@ -768,7 +731,7 @@ async function addTask(
console.log( console.log(
chalk.gray(`\n High relevance matches (score < 0.25):`) chalk.gray(`\n High relevance matches (score < 0.25):`)
); );
highRelevance.slice(0, 5).forEach((t) => { highRelevance.slice(0, 25).forEach((t) => {
console.log( console.log(
chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`) chalk.yellow(` • ⭐ Task ${t.id}: ${truncate(t.title, 50)}`)
); );
@@ -779,24 +742,13 @@ async function addTask(
console.log( console.log(
chalk.gray(`\n Medium relevance matches (score < 0.4):`) chalk.gray(`\n Medium relevance matches (score < 0.4):`)
); );
mediumRelevance.slice(0, 3).forEach((t) => { mediumRelevance.slice(0, 10).forEach((t) => {
console.log( console.log(
chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`) chalk.green(` • Task ${t.id}: ${truncate(t.title, 50)}`)
); );
}); });
} }
if (promptCategory && categoryTasks.length > 0) {
console.log(
chalk.gray(`\n Tasks related to ${promptCategory.label}:`)
);
categoryTasks.forEach((t) => {
console.log(
chalk.magenta(` • Task ${t.id}: ${truncate(t.title, 50)}`)
);
});
}
// Show dependency patterns // Show dependency patterns
if (commonDeps && commonDeps.length > 0) { if (commonDeps && commonDeps.length > 0) {
console.log( console.log(
@@ -825,7 +777,7 @@ async function addTask(
const isHighRelevance = highRelevance.some( const isHighRelevance = highRelevance.some(
(ht) => ht.id === t.id (ht) => ht.id === t.id
); );
const relevanceIndicator = isHighRelevance ? '' : ''; const relevanceIndicator = isHighRelevance ? "" : "";
console.log( console.log(
chalk.cyan( chalk.cyan(
`${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}` `${relevanceIndicator}Task ${t.id}: ${truncate(t.title, 40)}`
@@ -853,26 +805,23 @@ async function addTask(
} }
// Add a visual transition to show we're moving to AI generation - only for CLI // Add a visual transition to show we're moving to AI generation - only for CLI
if (outputFormat === 'text') { if (outputFormat === "text") {
console.log( console.log(
boxen( boxen(
chalk.white.bold('AI Task Generation') + chalk.white.bold("AI Task Generation") +
`\n\n${chalk.gray('Analyzing context and generating task details using AI...')}` + `\n\n${chalk.gray("Analyzing context and generating task details using AI...")}` +
`\n${chalk.cyan('Context size: ')}${chalk.yellow(contextTasks.length.toLocaleString())} characters` + `\n${chalk.cyan("Context size: ")}${chalk.yellow(contextTasks.length.toLocaleString())} characters` +
`\n${chalk.cyan('Dependency detection: ')}${chalk.yellow(numericDependencies.length > 0 ? 'Explicit dependencies' : 'Auto-discovery mode')}` + `\n${chalk.cyan("Dependency detection: ")}${chalk.yellow(numericDependencies.length > 0 ? "Explicit dependencies" : "Auto-discovery mode")}` +
`\n${chalk.cyan('Detailed tasks: ')}${chalk.yellow( `\n${chalk.cyan("Detailed tasks: ")}${chalk.yellow(
numericDependencies.length > 0 numericDependencies.length > 0
? dependentTasks.length // Use length of tasks from explicit dependency path ? dependentTasks.length // Use length of tasks from explicit dependency path
: uniqueDetailedTasks.length // Use length of tasks from fuzzy search path : uniqueDetailedTasks.length // Use length of tasks from fuzzy search path
)}` + )}`,
(promptCategory
? `\n${chalk.cyan('Category detected: ')}${chalk.yellow(promptCategory.label)}`
: ''),
{ {
padding: { top: 0, bottom: 1, left: 1, right: 1 }, padding: { top: 0, bottom: 1, left: 1, right: 1 },
margin: { top: 1, bottom: 0 }, margin: { top: 1, bottom: 0 },
borderColor: 'white', borderColor: "white",
borderStyle: 'round' borderStyle: "round",
} }
) )
); );
@@ -882,15 +831,15 @@ async function addTask(
// System Prompt - Enhanced for dependency awareness // System Prompt - Enhanced for dependency awareness
const systemPrompt = const systemPrompt =
"You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" + "You are a helpful assistant that creates well-structured tasks for a software development project. Generate a single new task based on the user's description, adhering strictly to the provided JSON schema. Pay special attention to dependencies between tasks, ensuring the new task correctly references any tasks it depends on.\n\n" +
'When determining dependencies for a new task, follow these principles:\n' + "When determining dependencies for a new task, follow these principles:\n" +
'1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n' + "1. Select dependencies based on logical requirements - what must be completed before this task can begin.\n" +
'2. Prioritize task dependencies that are semantically related to the functionality being built.\n' + "2. Prioritize task dependencies that are semantically related to the functionality being built.\n" +
'3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n' + "3. Consider both direct dependencies (immediately prerequisite) and indirect dependencies.\n" +
'4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n' + "4. Avoid adding unnecessary dependencies - only include tasks that are genuinely prerequisite.\n" +
'5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n' + "5. Consider the current status of tasks - prefer completed tasks as dependencies when possible.\n" +
"6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" + "6. Pay special attention to foundation tasks (1-5) but don't automatically include them without reason.\n" +
'7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n' + "7. Recent tasks (higher ID numbers) may be more relevant for newer functionality.\n\n" +
'The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n'; "The dependencies array should contain task IDs (numbers) of prerequisite tasks.\n";
// Task Structure Description (for user prompt) // Task Structure Description (for user prompt)
const taskStructureDesc = ` const taskStructureDesc = `
@@ -904,7 +853,7 @@ async function addTask(
`; `;
// Add any manually provided details to the prompt for context // Add any manually provided details to the prompt for context
let contextFromArgs = ''; let contextFromArgs = "";
if (manualTaskData?.title) if (manualTaskData?.title)
contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`; contextFromArgs += `\n- Suggested Title: "${manualTaskData.title}"`;
if (manualTaskData?.description) if (manualTaskData?.description)
@@ -918,7 +867,7 @@ async function addTask(
const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project. const userPrompt = `You are generating the details for Task #${newTaskId}. Based on the user's request: "${prompt}", create a comprehensive new task for a software development project.
${contextTasks} ${contextTasks}
${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ''} ${contextFromArgs ? `\nConsider these additional details provided by the user:${contextFromArgs}` : ""}
Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on. Based on the information about existing tasks provided above, include appropriate dependencies in the "dependencies" array. Only include task IDs that this new task directly depends on.
@@ -929,15 +878,15 @@ async function addTask(
`; `;
// Start the loading indicator - only for text mode // Start the loading indicator - only for text mode
if (outputFormat === 'text') { if (outputFormat === "text") {
loadingIndicator = startLoadingIndicator( loadingIndicator = startLoadingIndicator(
`Generating new task with ${useResearch ? 'Research' : 'Main'} AI...\n` `Generating new task with ${useResearch ? "Research" : "Main"} AI... \n`
); );
} }
try { try {
const serviceRole = useResearch ? 'research' : 'main'; const serviceRole = useResearch ? "research" : "main";
report('DEBUG: Calling generateObjectService...', 'debug'); report("DEBUG: Calling generateObjectService...", "debug");
aiServiceResponse = await generateObjectService({ aiServiceResponse = await generateObjectService({
// Capture the full response // Capture the full response
@@ -945,17 +894,17 @@ async function addTask(
session: session, session: session,
projectRoot: projectRoot, projectRoot: projectRoot,
schema: AiTaskDataSchema, schema: AiTaskDataSchema,
objectName: 'newTaskData', objectName: "newTaskData",
systemPrompt: systemPrompt, systemPrompt: systemPrompt,
prompt: userPrompt, prompt: userPrompt,
commandName: commandName || 'add-task', // Use passed commandName or default commandName: commandName || "add-task", // Use passed commandName or default
outputType: outputType || (isMCP ? 'mcp' : 'cli') // Use passed outputType or derive outputType: outputType || (isMCP ? "mcp" : "cli"), // Use passed outputType or derive
}); });
report('DEBUG: generateObjectService returned successfully.', 'debug'); report("DEBUG: generateObjectService returned successfully.", "debug");
if (!aiServiceResponse || !aiServiceResponse.mainResult) { if (!aiServiceResponse || !aiServiceResponse.mainResult) {
throw new Error( throw new Error(
'AI service did not return the expected object structure.' "AI service did not return the expected object structure."
); );
} }
@@ -972,21 +921,37 @@ async function addTask(
) { ) {
taskData = aiServiceResponse.mainResult.object; taskData = aiServiceResponse.mainResult.object;
} else { } else {
throw new Error('AI service did not return a valid task object.'); throw new Error("AI service did not return a valid task object.");
} }
report('Successfully generated task data from AI.', 'success'); report("Successfully generated task data from AI.", "success");
// Success! Show checkmark
if (loadingIndicator) {
succeedLoadingIndicator(
loadingIndicator,
"Task generated successfully"
);
loadingIndicator = null; // Clear it
}
} catch (error) { } catch (error) {
// Failure! Show X
if (loadingIndicator) {
failLoadingIndicator(loadingIndicator, "AI generation failed");
loadingIndicator = null;
}
report( report(
`DEBUG: generateObjectService caught error: ${error.message}`, `DEBUG: generateObjectService caught error: ${error.message}`,
'debug' "debug"
); );
report(`Error generating task with AI: ${error.message}`, 'error'); report(`Error generating task with AI: ${error.message}`, "error");
if (loadingIndicator) stopLoadingIndicator(loadingIndicator);
throw error; // Re-throw error after logging throw error; // Re-throw error after logging
} finally { } finally {
report('DEBUG: generateObjectService finally block reached.', 'debug'); report("DEBUG: generateObjectService finally block reached.", "debug");
if (loadingIndicator) stopLoadingIndicator(loadingIndicator); // Ensure indicator stops // Clean up if somehow still running
if (loadingIndicator) {
stopLoadingIndicator(loadingIndicator);
}
} }
// --- End Refactored AI Interaction --- // --- End Refactored AI Interaction ---
} }
@@ -996,14 +961,14 @@ async function addTask(
id: newTaskId, id: newTaskId,
title: taskData.title, title: taskData.title,
description: taskData.description, description: taskData.description,
details: taskData.details || '', details: taskData.details || "",
testStrategy: taskData.testStrategy || '', testStrategy: taskData.testStrategy || "",
status: 'pending', status: "pending",
dependencies: taskData.dependencies?.length dependencies: taskData.dependencies?.length
? taskData.dependencies ? taskData.dependencies
: numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified : numericDependencies, // Use AI-suggested dependencies if available, fallback to manually specified
priority: effectivePriority, priority: effectivePriority,
subtasks: [] // Initialize with empty subtasks array subtasks: [], // Initialize with empty subtasks array
}; };
// Additional check: validate all dependencies in the AI response // Additional check: validate all dependencies in the AI response
@@ -1015,8 +980,8 @@ async function addTask(
if (!allValidDeps) { if (!allValidDeps) {
report( report(
'AI suggested invalid dependencies. Filtering them out...', "AI suggested invalid dependencies. Filtering them out...",
'warn' "warn"
); );
newTask.dependencies = taskData.dependencies.filter((depId) => { newTask.dependencies = taskData.dependencies.filter((depId) => {
const numDepId = parseInt(depId, 10); const numDepId = parseInt(depId, 10);
@@ -1028,48 +993,48 @@ async function addTask(
// Add the task to the tasks array // Add the task to the tasks array
data.tasks.push(newTask); data.tasks.push(newTask);
report('DEBUG: Writing tasks.json...', 'debug'); report("DEBUG: Writing tasks.json...", "debug");
// Write the updated tasks to the file // Write the updated tasks to the file
writeJSON(tasksPath, data); writeJSON(tasksPath, data);
report('DEBUG: tasks.json written.', 'debug'); report("DEBUG: tasks.json written.", "debug");
// Generate markdown task files // Generate markdown task files
report('Generating task files...', 'info'); report("Generating task files...", "info");
report('DEBUG: Calling generateTaskFiles...', 'debug'); report("DEBUG: Calling generateTaskFiles...", "debug");
// Pass mcpLog if available to generateTaskFiles // Pass mcpLog if available to generateTaskFiles
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog }); await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
report('DEBUG: generateTaskFiles finished.', 'debug'); report("DEBUG: generateTaskFiles finished.", "debug");
// Show success message - only for text output (CLI) // Show success message - only for text output (CLI)
if (outputFormat === 'text') { if (outputFormat === "text") {
const table = new Table({ const table = new Table({
head: [ head: [
chalk.cyan.bold('ID'), chalk.cyan.bold("ID"),
chalk.cyan.bold('Title'), chalk.cyan.bold("Title"),
chalk.cyan.bold('Description') chalk.cyan.bold("Description"),
], ],
colWidths: [5, 30, 50] // Adjust widths as needed colWidths: [5, 30, 50], // Adjust widths as needed
}); });
table.push([ table.push([
newTask.id, newTask.id,
truncate(newTask.title, 27), truncate(newTask.title, 27),
truncate(newTask.description, 47) truncate(newTask.description, 47),
]); ]);
console.log(chalk.green('✅ New task created successfully:')); console.log(chalk.green("✓ New task created successfully:"));
console.log(table.toString()); console.log(table.toString());
// Helper to get priority color // Helper to get priority color
const getPriorityColor = (p) => { const getPriorityColor = (p) => {
switch (p?.toLowerCase()) { switch (p?.toLowerCase()) {
case 'high': case "high":
return 'red'; return "red";
case 'low': case "low":
return 'gray'; return "gray";
case 'medium': case "medium":
default: default:
return 'yellow'; return "yellow";
} }
}; };
@@ -1093,49 +1058,49 @@ async function addTask(
}); });
// Prepare dependency display string // Prepare dependency display string
let dependencyDisplay = ''; let dependencyDisplay = "";
if (newTask.dependencies.length > 0) { if (newTask.dependencies.length > 0) {
dependencyDisplay = chalk.white('Dependencies:') + '\n'; dependencyDisplay = chalk.white("Dependencies:") + "\n";
newTask.dependencies.forEach((dep) => { newTask.dependencies.forEach((dep) => {
const isAiAdded = aiAddedDeps.includes(dep); const isAiAdded = aiAddedDeps.includes(dep);
const depType = isAiAdded ? chalk.yellow(' (AI suggested)') : ''; const depType = isAiAdded ? chalk.yellow(" (AI suggested)") : "";
dependencyDisplay += dependencyDisplay +=
chalk.white( chalk.white(
` - ${dep}: ${depTitles[dep] || 'Unknown task'}${depType}` ` - ${dep}: ${depTitles[dep] || "Unknown task"}${depType}`
) + '\n'; ) + "\n";
}); });
} else { } else {
dependencyDisplay = chalk.white('Dependencies: None') + '\n'; dependencyDisplay = chalk.white("Dependencies: None") + "\n";
} }
// Add info about removed dependencies if any // Add info about removed dependencies if any
if (aiRemovedDeps.length > 0) { if (aiRemovedDeps.length > 0) {
dependencyDisplay += dependencyDisplay +=
chalk.gray('\nUser-specified dependencies that were not used:') + chalk.gray("\nUser-specified dependencies that were not used:") +
'\n'; "\n";
aiRemovedDeps.forEach((dep) => { aiRemovedDeps.forEach((dep) => {
const depTask = data.tasks.find((t) => t.id === dep); const depTask = data.tasks.find((t) => t.id === dep);
const title = depTask ? truncate(depTask.title, 30) : 'Unknown task'; const title = depTask ? truncate(depTask.title, 30) : "Unknown task";
dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + '\n'; dependencyDisplay += chalk.gray(` - ${dep}: ${title}`) + "\n";
}); });
} }
// Add dependency analysis summary // Add dependency analysis summary
let dependencyAnalysis = ''; let dependencyAnalysis = "";
if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) { if (aiAddedDeps.length > 0 || aiRemovedDeps.length > 0) {
dependencyAnalysis = dependencyAnalysis =
'\n' + chalk.white.bold('Dependency Analysis:') + '\n'; "\n" + chalk.white.bold("Dependency Analysis:") + "\n";
if (aiAddedDeps.length > 0) { if (aiAddedDeps.length > 0) {
dependencyAnalysis += dependencyAnalysis +=
chalk.green( chalk.green(
`AI identified ${aiAddedDeps.length} additional dependencies` `AI identified ${aiAddedDeps.length} additional dependencies`
) + '\n'; ) + "\n";
} }
if (aiRemovedDeps.length > 0) { if (aiRemovedDeps.length > 0) {
dependencyAnalysis += dependencyAnalysis +=
chalk.yellow( chalk.yellow(
`AI excluded ${aiRemovedDeps.length} user-provided dependencies` `AI excluded ${aiRemovedDeps.length} user-provided dependencies`
) + '\n'; ) + "\n";
} }
} }
@@ -1143,32 +1108,32 @@ async function addTask(
console.log( console.log(
boxen( boxen(
chalk.white.bold(`Task ${newTaskId} Created Successfully`) + chalk.white.bold(`Task ${newTaskId} Created Successfully`) +
'\n\n' + "\n\n" +
chalk.white(`Title: ${newTask.title}`) + chalk.white(`Title: ${newTask.title}`) +
'\n' + "\n" +
chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) + chalk.white(`Status: ${getStatusWithColor(newTask.status)}`) +
'\n' + "\n" +
chalk.white( chalk.white(
`Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}` `Priority: ${chalk[getPriorityColor(newTask.priority)](newTask.priority)}`
) + ) +
'\n\n' + "\n\n" +
dependencyDisplay + dependencyDisplay +
dependencyAnalysis + dependencyAnalysis +
'\n' + "\n" +
chalk.white.bold('Next Steps:') + chalk.white.bold("Next Steps:") +
'\n' + "\n" +
chalk.cyan( chalk.cyan(
`1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details` `1. Run ${chalk.yellow(`task-master show ${newTaskId}`)} to see complete task details`
) + ) +
'\n' + "\n" +
chalk.cyan( chalk.cyan(
`2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it` `2. Run ${chalk.yellow(`task-master set-status --id=${newTaskId} --status=in-progress`)} to start working on it`
) + ) +
'\n' + "\n" +
chalk.cyan( chalk.cyan(
`3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks` `3. Run ${chalk.yellow(`task-master expand --id=${newTaskId}`)} to break it down into subtasks`
), ),
{ padding: 1, borderColor: 'green', borderStyle: 'round' } { padding: 1, borderColor: "green", borderStyle: "round" }
) )
); );
@@ -1176,19 +1141,19 @@ async function addTask(
if ( if (
aiServiceResponse && aiServiceResponse &&
aiServiceResponse.telemetryData && aiServiceResponse.telemetryData &&
(outputType === 'cli' || outputType === 'text') (outputType === "cli" || outputType === "text")
) { ) {
displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli'); displayAiUsageSummary(aiServiceResponse.telemetryData, "cli");
} }
} }
report( report(
`DEBUG: Returning new task ID: ${newTaskId} and telemetry.`, `DEBUG: Returning new task ID: ${newTaskId} and telemetry.`,
'debug' "debug"
); );
return { return {
newTaskId: newTaskId, newTaskId: newTaskId,
telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null telemetryData: aiServiceResponse ? aiServiceResponse.telemetryData : null,
}; };
} catch (error) { } catch (error) {
// Stop any loading indicator on error // Stop any loading indicator on error
@@ -1196,8 +1161,8 @@ async function addTask(
stopLoadingIndicator(loadingIndicator); stopLoadingIndicator(loadingIndicator);
} }
report(`Error adding task: ${error.message}`, 'error'); report(`Error adding task: ${error.message}`, "error");
if (outputFormat === 'text') { if (outputFormat === "text") {
console.error(chalk.red(`Error: ${error.message}`)); console.error(chalk.red(`Error: ${error.message}`));
} }
// In MCP mode, we let the direct function handler catch and format // In MCP mode, we let the direct function handler catch and format

View File

@@ -1,11 +1,11 @@
import path from 'path'; import path from "path";
import chalk from 'chalk'; import chalk from "chalk";
import boxen from 'boxen'; import boxen from "boxen";
import Table from 'cli-table3'; import Table from "cli-table3";
import { log, readJSON, writeJSON, truncate, isSilentMode } from '../utils.js'; import { log, readJSON, writeJSON, truncate, isSilentMode } from "../utils.js";
import { displayBanner } from '../ui.js'; import { displayBanner } from "../ui.js";
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from "./generate-task-files.js";
/** /**
* Clear subtasks from specified tasks * Clear subtasks from specified tasks
@@ -13,60 +13,58 @@ import generateTaskFiles from './generate-task-files.js';
* @param {string} taskIds - Task IDs to clear subtasks from * @param {string} taskIds - Task IDs to clear subtasks from
*/ */
function clearSubtasks(tasksPath, taskIds) { function clearSubtasks(tasksPath, taskIds) {
displayBanner(); log("info", `Reading tasks from ${tasksPath}...`);
log('info', `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
log('error', 'No valid tasks found.'); log("error", "No valid tasks found.");
process.exit(1); process.exit(1);
} }
if (!isSilentMode()) { if (!isSilentMode()) {
console.log( console.log(
boxen(chalk.white.bold('Clearing Subtasks'), { boxen(chalk.white.bold("Clearing Subtasks"), {
padding: 1, padding: 1,
borderColor: 'blue', borderColor: "blue",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 } margin: { top: 1, bottom: 1 },
}) })
); );
} }
// Handle multiple task IDs (comma-separated) // Handle multiple task IDs (comma-separated)
const taskIdArray = taskIds.split(',').map((id) => id.trim()); const taskIdArray = taskIds.split(",").map((id) => id.trim());
let clearedCount = 0; let clearedCount = 0;
// Create a summary table for the cleared subtasks // Create a summary table for the cleared subtasks
const summaryTable = new Table({ const summaryTable = new Table({
head: [ head: [
chalk.cyan.bold('Task ID'), chalk.cyan.bold("Task ID"),
chalk.cyan.bold('Task Title'), chalk.cyan.bold("Task Title"),
chalk.cyan.bold('Subtasks Cleared') chalk.cyan.bold("Subtasks Cleared"),
], ],
colWidths: [10, 50, 20], colWidths: [10, 50, 20],
style: { head: [], border: [] } style: { head: [], border: [] },
}); });
taskIdArray.forEach((taskId) => { taskIdArray.forEach((taskId) => {
const id = parseInt(taskId, 10); const id = parseInt(taskId, 10);
if (isNaN(id)) { if (isNaN(id)) {
log('error', `Invalid task ID: ${taskId}`); log("error", `Invalid task ID: ${taskId}`);
return; return;
} }
const task = data.tasks.find((t) => t.id === id); const task = data.tasks.find((t) => t.id === id);
if (!task) { if (!task) {
log('error', `Task ${id} not found`); log("error", `Task ${id} not found`);
return; return;
} }
if (!task.subtasks || task.subtasks.length === 0) { if (!task.subtasks || task.subtasks.length === 0) {
log('info', `Task ${id} has no subtasks to clear`); log("info", `Task ${id} has no subtasks to clear`);
summaryTable.push([ summaryTable.push([
id.toString(), id.toString(),
truncate(task.title, 47), truncate(task.title, 47),
chalk.yellow('No subtasks') chalk.yellow("No subtasks"),
]); ]);
return; return;
} }
@@ -74,12 +72,12 @@ function clearSubtasks(tasksPath, taskIds) {
const subtaskCount = task.subtasks.length; const subtaskCount = task.subtasks.length;
task.subtasks = []; task.subtasks = [];
clearedCount++; clearedCount++;
log('info', `Cleared ${subtaskCount} subtasks from task ${id}`); log("info", `Cleared ${subtaskCount} subtasks from task ${id}`);
summaryTable.push([ summaryTable.push([
id.toString(), id.toString(),
truncate(task.title, 47), truncate(task.title, 47),
chalk.green(`${subtaskCount} subtasks cleared`) chalk.green(`${subtaskCount} subtasks cleared`),
]); ]);
}); });
@@ -89,18 +87,18 @@ function clearSubtasks(tasksPath, taskIds) {
// Show summary table // Show summary table
if (!isSilentMode()) { if (!isSilentMode()) {
console.log( console.log(
boxen(chalk.white.bold('Subtask Clearing Summary:'), { boxen(chalk.white.bold("Subtask Clearing Summary:"), {
padding: { left: 2, right: 2, top: 0, bottom: 0 }, padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 }, margin: { top: 1, bottom: 0 },
borderColor: 'blue', borderColor: "blue",
borderStyle: 'round' borderStyle: "round",
}) })
); );
console.log(summaryTable.toString()); console.log(summaryTable.toString());
} }
// Regenerate task files to reflect changes // Regenerate task files to reflect changes
log('info', 'Regenerating task files...'); log("info", "Regenerating task files...");
generateTaskFiles(tasksPath, path.dirname(tasksPath)); generateTaskFiles(tasksPath, path.dirname(tasksPath));
// Success message // Success message
@@ -112,9 +110,9 @@ function clearSubtasks(tasksPath, taskIds) {
), ),
{ {
padding: 1, padding: 1,
borderColor: 'green', borderColor: "green",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1 } margin: { top: 1 },
} }
) )
); );
@@ -122,15 +120,15 @@ function clearSubtasks(tasksPath, taskIds) {
// Next steps suggestion // Next steps suggestion
console.log( console.log(
boxen( boxen(
chalk.white.bold('Next Steps:') + chalk.white.bold("Next Steps:") +
'\n\n' + "\n\n" +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master expand --id=<id>')} to generate new subtasks\n` + `${chalk.cyan("1.")} Run ${chalk.yellow("task-master expand --id=<id>")} to generate new subtasks\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master list --with-subtasks')} to verify changes`, `${chalk.cyan("2.")} Run ${chalk.yellow("task-master list --with-subtasks")} to verify changes`,
{ {
padding: 1, padding: 1,
borderColor: 'cyan', borderColor: "cyan",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1 } margin: { top: 1 },
} }
) )
); );
@@ -138,11 +136,11 @@ function clearSubtasks(tasksPath, taskIds) {
} else { } else {
if (!isSilentMode()) { if (!isSilentMode()) {
console.log( console.log(
boxen(chalk.yellow('No subtasks were cleared'), { boxen(chalk.yellow("No subtasks were cleared"), {
padding: 1, padding: 1,
borderColor: 'yellow', borderColor: "yellow",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1 } margin: { top: 1 },
}) })
); );
} }

View File

@@ -1,23 +1,23 @@
import chalk from 'chalk'; import chalk from "chalk";
import boxen from 'boxen'; import boxen from "boxen";
import Table from 'cli-table3'; import Table from "cli-table3";
import { import {
log, log,
readJSON, readJSON,
truncate, truncate,
readComplexityReport, readComplexityReport,
addComplexityToTask addComplexityToTask,
} from '../utils.js'; } from "../utils.js";
import findNextTask from './find-next-task.js'; import findNextTask from "./find-next-task.js";
import { import {
displayBanner, displayBanner,
getStatusWithColor, getStatusWithColor,
formatDependenciesWithStatus, formatDependenciesWithStatus,
getComplexityWithColor, getComplexityWithColor,
createProgressBar createProgressBar,
} from '../ui.js'; } from "../ui.js";
/** /**
* List all tasks * List all tasks
@@ -33,14 +33,9 @@ function listTasks(
statusFilter, statusFilter,
reportPath = null, reportPath = null,
withSubtasks = false, withSubtasks = false,
outputFormat = 'text' outputFormat = "text"
) { ) {
try { try {
// Only display banner for text output
if (outputFormat === 'text') {
displayBanner();
}
const data = readJSON(tasksPath); // Reads the whole tasks.json const data = readJSON(tasksPath); // Reads the whole tasks.json
if (!data || !data.tasks) { if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`); throw new Error(`No valid tasks found in ${tasksPath}`);
@@ -55,7 +50,7 @@ function listTasks(
// Filter tasks by status if specified // Filter tasks by status if specified
const filteredTasks = const filteredTasks =
statusFilter && statusFilter.toLowerCase() !== 'all' // <-- Added check for 'all' statusFilter && statusFilter.toLowerCase() !== "all" // <-- Added check for 'all'
? data.tasks.filter( ? data.tasks.filter(
(task) => (task) =>
task.status && task.status &&
@@ -66,7 +61,7 @@ function listTasks(
// Calculate completion statistics // Calculate completion statistics
const totalTasks = data.tasks.length; const totalTasks = data.tasks.length;
const completedTasks = data.tasks.filter( const completedTasks = data.tasks.filter(
(task) => task.status === 'done' || task.status === 'completed' (task) => task.status === "done" || task.status === "completed"
).length; ).length;
const completionPercentage = const completionPercentage =
totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0; totalTasks > 0 ? (completedTasks / totalTasks) * 100 : 0;
@@ -74,19 +69,19 @@ function listTasks(
// Count statuses for tasks // Count statuses for tasks
const doneCount = completedTasks; const doneCount = completedTasks;
const inProgressCount = data.tasks.filter( const inProgressCount = data.tasks.filter(
(task) => task.status === 'in-progress' (task) => task.status === "in-progress"
).length; ).length;
const pendingCount = data.tasks.filter( const pendingCount = data.tasks.filter(
(task) => task.status === 'pending' (task) => task.status === "pending"
).length; ).length;
const blockedCount = data.tasks.filter( const blockedCount = data.tasks.filter(
(task) => task.status === 'blocked' (task) => task.status === "blocked"
).length; ).length;
const deferredCount = data.tasks.filter( const deferredCount = data.tasks.filter(
(task) => task.status === 'deferred' (task) => task.status === "deferred"
).length; ).length;
const cancelledCount = data.tasks.filter( const cancelledCount = data.tasks.filter(
(task) => task.status === 'cancelled' (task) => task.status === "cancelled"
).length; ).length;
// Count subtasks and their statuses // Count subtasks and their statuses
@@ -102,22 +97,22 @@ function listTasks(
if (task.subtasks && task.subtasks.length > 0) { if (task.subtasks && task.subtasks.length > 0) {
totalSubtasks += task.subtasks.length; totalSubtasks += task.subtasks.length;
completedSubtasks += task.subtasks.filter( completedSubtasks += task.subtasks.filter(
(st) => st.status === 'done' || st.status === 'completed' (st) => st.status === "done" || st.status === "completed"
).length; ).length;
inProgressSubtasks += task.subtasks.filter( inProgressSubtasks += task.subtasks.filter(
(st) => st.status === 'in-progress' (st) => st.status === "in-progress"
).length; ).length;
pendingSubtasks += task.subtasks.filter( pendingSubtasks += task.subtasks.filter(
(st) => st.status === 'pending' (st) => st.status === "pending"
).length; ).length;
blockedSubtasks += task.subtasks.filter( blockedSubtasks += task.subtasks.filter(
(st) => st.status === 'blocked' (st) => st.status === "blocked"
).length; ).length;
deferredSubtasks += task.subtasks.filter( deferredSubtasks += task.subtasks.filter(
(st) => st.status === 'deferred' (st) => st.status === "deferred"
).length; ).length;
cancelledSubtasks += task.subtasks.filter( cancelledSubtasks += task.subtasks.filter(
(st) => st.status === 'cancelled' (st) => st.status === "cancelled"
).length; ).length;
} }
}); });
@@ -126,7 +121,7 @@ function listTasks(
totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0; totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
// For JSON output, return structured data // For JSON output, return structured data
if (outputFormat === 'json') { if (outputFormat === "json") {
// *** Modification: Remove 'details' field for JSON output *** // *** Modification: Remove 'details' field for JSON output ***
const tasksWithoutDetails = filteredTasks.map((task) => { const tasksWithoutDetails = filteredTasks.map((task) => {
// <-- USES filteredTasks! // <-- USES filteredTasks!
@@ -146,7 +141,7 @@ function listTasks(
return { return {
tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED tasks: tasksWithoutDetails, // <--- THIS IS THE ARRAY BEING RETURNED
filter: statusFilter || 'all', // Return the actual filter used filter: statusFilter || "all", // Return the actual filter used
stats: { stats: {
total: totalTasks, total: totalTasks,
completed: doneCount, completed: doneCount,
@@ -164,9 +159,9 @@ function listTasks(
blocked: blockedSubtasks, blocked: blockedSubtasks,
deferred: deferredSubtasks, deferred: deferredSubtasks,
cancelled: cancelledSubtasks, cancelled: cancelledSubtasks,
completionPercentage: subtaskCompletionPercentage completionPercentage: subtaskCompletionPercentage,
} },
} },
}; };
} }
@@ -174,22 +169,22 @@ function listTasks(
// Calculate status breakdowns as percentages of total // Calculate status breakdowns as percentages of total
const taskStatusBreakdown = { const taskStatusBreakdown = {
'in-progress': totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0, "in-progress": totalTasks > 0 ? (inProgressCount / totalTasks) * 100 : 0,
pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0, pending: totalTasks > 0 ? (pendingCount / totalTasks) * 100 : 0,
blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0, blocked: totalTasks > 0 ? (blockedCount / totalTasks) * 100 : 0,
deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0, deferred: totalTasks > 0 ? (deferredCount / totalTasks) * 100 : 0,
cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0 cancelled: totalTasks > 0 ? (cancelledCount / totalTasks) * 100 : 0,
}; };
const subtaskStatusBreakdown = { const subtaskStatusBreakdown = {
'in-progress': "in-progress":
totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0, totalSubtasks > 0 ? (inProgressSubtasks / totalSubtasks) * 100 : 0,
pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0, pending: totalSubtasks > 0 ? (pendingSubtasks / totalSubtasks) * 100 : 0,
blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0, blocked: totalSubtasks > 0 ? (blockedSubtasks / totalSubtasks) * 100 : 0,
deferred: deferred:
totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0, totalSubtasks > 0 ? (deferredSubtasks / totalSubtasks) * 100 : 0,
cancelled: cancelled:
totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0 totalSubtasks > 0 ? (cancelledSubtasks / totalSubtasks) * 100 : 0,
}; };
// Create progress bars with status breakdowns // Create progress bars with status breakdowns
@@ -207,21 +202,21 @@ function listTasks(
// Calculate dependency statistics // Calculate dependency statistics
const completedTaskIds = new Set( const completedTaskIds = new Set(
data.tasks data.tasks
.filter((t) => t.status === 'done' || t.status === 'completed') .filter((t) => t.status === "done" || t.status === "completed")
.map((t) => t.id) .map((t) => t.id)
); );
const tasksWithNoDeps = data.tasks.filter( const tasksWithNoDeps = data.tasks.filter(
(t) => (t) =>
t.status !== 'done' && t.status !== "done" &&
t.status !== 'completed' && t.status !== "completed" &&
(!t.dependencies || t.dependencies.length === 0) (!t.dependencies || t.dependencies.length === 0)
).length; ).length;
const tasksWithAllDepsSatisfied = data.tasks.filter( const tasksWithAllDepsSatisfied = data.tasks.filter(
(t) => (t) =>
t.status !== 'done' && t.status !== "done" &&
t.status !== 'completed' && t.status !== "completed" &&
t.dependencies && t.dependencies &&
t.dependencies.length > 0 && t.dependencies.length > 0 &&
t.dependencies.every((depId) => completedTaskIds.has(depId)) t.dependencies.every((depId) => completedTaskIds.has(depId))
@@ -229,8 +224,8 @@ function listTasks(
const tasksWithUnsatisfiedDeps = data.tasks.filter( const tasksWithUnsatisfiedDeps = data.tasks.filter(
(t) => (t) =>
t.status !== 'done' && t.status !== "done" &&
t.status !== 'completed' && t.status !== "completed" &&
t.dependencies && t.dependencies &&
t.dependencies.length > 0 && t.dependencies.length > 0 &&
!t.dependencies.every((depId) => completedTaskIds.has(depId)) !t.dependencies.every((depId) => completedTaskIds.has(depId))
@@ -283,7 +278,7 @@ function listTasks(
terminalWidth = process.stdout.columns; terminalWidth = process.stdout.columns;
} catch (e) { } catch (e) {
// Fallback if columns cannot be determined // Fallback if columns cannot be determined
log('debug', 'Could not determine terminal width, using default'); log("debug", "Could not determine terminal width, using default");
} }
// Ensure we have a reasonable default if detection fails // Ensure we have a reasonable default if detection fails
terminalWidth = terminalWidth || 80; terminalWidth = terminalWidth || 80;
@@ -293,35 +288,35 @@ function listTasks(
// Create dashboard content // Create dashboard content
const projectDashboardContent = const projectDashboardContent =
chalk.white.bold('Project Dashboard') + chalk.white.bold("Project Dashboard") +
'\n' + "\n" +
`Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` + `Tasks Progress: ${chalk.greenBright(taskProgressBar)} ${completionPercentage.toFixed(0)}%\n` +
`Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` + `Done: ${chalk.green(doneCount)} In Progress: ${chalk.blue(inProgressCount)} Pending: ${chalk.yellow(pendingCount)} Blocked: ${chalk.red(blockedCount)} Deferred: ${chalk.gray(deferredCount)} Cancelled: ${chalk.gray(cancelledCount)}\n\n` +
`Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` + `Subtasks Progress: ${chalk.cyan(subtaskProgressBar)} ${subtaskCompletionPercentage.toFixed(0)}%\n` +
`Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` + `Completed: ${chalk.green(completedSubtasks)}/${totalSubtasks} In Progress: ${chalk.blue(inProgressSubtasks)} Pending: ${chalk.yellow(pendingSubtasks)} Blocked: ${chalk.red(blockedSubtasks)} Deferred: ${chalk.gray(deferredSubtasks)} Cancelled: ${chalk.gray(cancelledSubtasks)}\n\n` +
chalk.cyan.bold('Priority Breakdown:') + chalk.cyan.bold("Priority Breakdown:") +
'\n' + "\n" +
`${chalk.red('•')} ${chalk.white('High priority:')} ${data.tasks.filter((t) => t.priority === 'high').length}\n` + `${chalk.red("•")} ${chalk.white("High priority:")} ${data.tasks.filter((t) => t.priority === "high").length}\n` +
`${chalk.yellow('•')} ${chalk.white('Medium priority:')} ${data.tasks.filter((t) => t.priority === 'medium').length}\n` + `${chalk.yellow("•")} ${chalk.white("Medium priority:")} ${data.tasks.filter((t) => t.priority === "medium").length}\n` +
`${chalk.green('•')} ${chalk.white('Low priority:')} ${data.tasks.filter((t) => t.priority === 'low').length}`; `${chalk.green("•")} ${chalk.white("Low priority:")} ${data.tasks.filter((t) => t.priority === "low").length}`;
const dependencyDashboardContent = const dependencyDashboardContent =
chalk.white.bold('Dependency Status & Next Task') + chalk.white.bold("Dependency Status & Next Task") +
'\n' + "\n" +
chalk.cyan.bold('Dependency Metrics:') + chalk.cyan.bold("Dependency Metrics:") +
'\n' + "\n" +
`${chalk.green('•')} ${chalk.white('Tasks with no dependencies:')} ${tasksWithNoDeps}\n` + `${chalk.green("•")} ${chalk.white("Tasks with no dependencies:")} ${tasksWithNoDeps}\n` +
`${chalk.green('•')} ${chalk.white('Tasks ready to work on:')} ${tasksReadyToWork}\n` + `${chalk.green("•")} ${chalk.white("Tasks ready to work on:")} ${tasksReadyToWork}\n` +
`${chalk.yellow('•')} ${chalk.white('Tasks blocked by dependencies:')} ${tasksWithUnsatisfiedDeps}\n` + `${chalk.yellow("•")} ${chalk.white("Tasks blocked by dependencies:")} ${tasksWithUnsatisfiedDeps}\n` +
`${chalk.magenta('•')} ${chalk.white('Most depended-on task:')} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray('None')}\n` + `${chalk.magenta("•")} ${chalk.white("Most depended-on task:")} ${mostDependedOnTask ? chalk.cyan(`#${mostDependedOnTaskId} (${maxDependents} dependents)`) : chalk.gray("None")}\n` +
`${chalk.blue('•')} ${chalk.white('Avg dependencies per task:')} ${avgDependenciesPerTask.toFixed(1)}\n\n` + `${chalk.blue("•")} ${chalk.white("Avg dependencies per task:")} ${avgDependenciesPerTask.toFixed(1)}\n\n` +
chalk.cyan.bold('Next Task to Work On:') + chalk.cyan.bold("Next Task to Work On:") +
'\n' + "\n" +
`ID: ${chalk.cyan(nextItem ? nextItem.id : 'N/A')} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow('No task available')} `ID: ${chalk.cyan(nextItem ? nextItem.id : "N/A")} - ${nextItem ? chalk.white.bold(truncate(nextItem.title, 40)) : chalk.yellow("No task available")}
` + ` +
`Priority: ${nextItem ? chalk.white(nextItem.priority || 'medium') : ''} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ''} `Priority: ${nextItem ? chalk.white(nextItem.priority || "medium") : ""} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ""}
` + ` +
`Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray('N/A')}`; `Complexity: ${nextItem && nextItem.complexityScore ? getComplexityWithColor(nextItem.complexityScore) : chalk.gray("N/A")}`;
// Calculate width for side-by-side display // Calculate width for side-by-side display
// Box borders, padding take approximately 4 chars on each side // Box borders, padding take approximately 4 chars on each side
@@ -341,23 +336,23 @@ function listTasks(
// Create boxen options with precise widths // Create boxen options with precise widths
const dashboardBox = boxen(projectDashboardContent, { const dashboardBox = boxen(projectDashboardContent, {
padding: 1, padding: 1,
borderColor: 'blue', borderColor: "blue",
borderStyle: 'round', borderStyle: "round",
width: boxContentWidth, width: boxContentWidth,
dimBorder: false dimBorder: false,
}); });
const dependencyBox = boxen(dependencyDashboardContent, { const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1, padding: 1,
borderColor: 'magenta', borderColor: "magenta",
borderStyle: 'round', borderStyle: "round",
width: boxContentWidth, width: boxContentWidth,
dimBorder: false dimBorder: false,
}); });
// Create a better side-by-side layout with exact spacing // Create a better side-by-side layout with exact spacing
const dashboardLines = dashboardBox.split('\n'); const dashboardLines = dashboardBox.split("\n");
const dependencyLines = dependencyBox.split('\n'); const dependencyLines = dependencyBox.split("\n");
// Make sure both boxes have the same height // Make sure both boxes have the same height
const maxHeight = Math.max(dashboardLines.length, dependencyLines.length); const maxHeight = Math.max(dashboardLines.length, dependencyLines.length);
@@ -367,35 +362,35 @@ function listTasks(
const combinedLines = []; const combinedLines = [];
for (let i = 0; i < maxHeight; i++) { for (let i = 0; i < maxHeight; i++) {
// Get the dashboard line (or empty string if we've run out of lines) // Get the dashboard line (or empty string if we've run out of lines)
const dashLine = i < dashboardLines.length ? dashboardLines[i] : ''; const dashLine = i < dashboardLines.length ? dashboardLines[i] : "";
// Get the dependency line (or empty string if we've run out of lines) // Get the dependency line (or empty string if we've run out of lines)
const depLine = i < dependencyLines.length ? dependencyLines[i] : ''; const depLine = i < dependencyLines.length ? dependencyLines[i] : "";
// Remove any trailing spaces from dashLine before padding to exact width // Remove any trailing spaces from dashLine before padding to exact width
const trimmedDashLine = dashLine.trimEnd(); const trimmedDashLine = dashLine.trimEnd();
// Pad the dashboard line to exactly halfWidth chars with no extra spaces // Pad the dashboard line to exactly halfWidth chars with no extra spaces
const paddedDashLine = trimmedDashLine.padEnd(halfWidth, ' '); const paddedDashLine = trimmedDashLine.padEnd(halfWidth, " ");
// Join the lines with no space in between // Join the lines with no space in between
combinedLines.push(paddedDashLine + depLine); combinedLines.push(paddedDashLine + depLine);
} }
// Join all lines and output // Join all lines and output
console.log(combinedLines.join('\n')); console.log(combinedLines.join("\n"));
} else { } else {
// Terminal too narrow, show boxes stacked vertically // Terminal too narrow, show boxes stacked vertically
const dashboardBox = boxen(projectDashboardContent, { const dashboardBox = boxen(projectDashboardContent, {
padding: 1, padding: 1,
borderColor: 'blue', borderColor: "blue",
borderStyle: 'round', borderStyle: "round",
margin: { top: 0, bottom: 1 } margin: { top: 0, bottom: 1 },
}); });
const dependencyBox = boxen(dependencyDashboardContent, { const dependencyBox = boxen(dependencyDashboardContent, {
padding: 1, padding: 1,
borderColor: 'magenta', borderColor: "magenta",
borderStyle: 'round', borderStyle: "round",
margin: { top: 0, bottom: 1 } margin: { top: 0, bottom: 1 },
}); });
// Display stacked vertically // Display stacked vertically
@@ -408,8 +403,8 @@ function listTasks(
boxen( boxen(
statusFilter statusFilter
? chalk.yellow(`No tasks with status '${statusFilter}' found`) ? chalk.yellow(`No tasks with status '${statusFilter}' found`)
: chalk.yellow('No tasks found'), : chalk.yellow("No tasks found"),
{ padding: 1, borderColor: 'yellow', borderStyle: 'round' } { padding: 1, borderColor: "yellow", borderStyle: "round" }
) )
); );
return; return;
@@ -458,12 +453,12 @@ function listTasks(
// Create a table with correct borders and spacing // Create a table with correct borders and spacing
const table = new Table({ const table = new Table({
head: [ head: [
chalk.cyan.bold('ID'), chalk.cyan.bold("ID"),
chalk.cyan.bold('Title'), chalk.cyan.bold("Title"),
chalk.cyan.bold('Status'), chalk.cyan.bold("Status"),
chalk.cyan.bold('Priority'), chalk.cyan.bold("Priority"),
chalk.cyan.bold('Dependencies'), chalk.cyan.bold("Dependencies"),
chalk.cyan.bold('Complexity') chalk.cyan.bold("Complexity"),
], ],
colWidths: [ colWidths: [
idWidth, idWidth,
@@ -471,21 +466,21 @@ function listTasks(
statusWidth, statusWidth,
priorityWidth, priorityWidth,
depsWidth, depsWidth,
complexityWidth // Added complexity column width complexityWidth, // Added complexity column width
], ],
style: { style: {
head: [], // No special styling for header head: [], // No special styling for header
border: [], // No special styling for border border: [], // No special styling for border
compact: false // Use default spacing compact: false, // Use default spacing
}, },
wordWrap: true, wordWrap: true,
wrapOnWordBoundary: true wrapOnWordBoundary: true,
}); });
// Process tasks for the table // Process tasks for the table
filteredTasks.forEach((task) => { filteredTasks.forEach((task) => {
// Format dependencies with status indicators (colored) // Format dependencies with status indicators (colored)
let depText = 'None'; let depText = "None";
if (task.dependencies && task.dependencies.length > 0) { if (task.dependencies && task.dependencies.length > 0) {
// Use the proper formatDependenciesWithStatus function for colored status // Use the proper formatDependenciesWithStatus function for colored status
depText = formatDependenciesWithStatus( depText = formatDependenciesWithStatus(
@@ -495,19 +490,19 @@ function listTasks(
complexityReport complexityReport
); );
} else { } else {
depText = chalk.gray('None'); depText = chalk.gray("None");
} }
// Clean up any ANSI codes or confusing characters // Clean up any ANSI codes or confusing characters
const cleanTitle = task.title.replace(/\n/g, ' '); const cleanTitle = task.title.replace(/\n/g, " ");
// Get priority color // Get priority color
const priorityColor = const priorityColor =
{ {
high: chalk.red, high: chalk.red,
medium: chalk.yellow, medium: chalk.yellow,
low: chalk.gray low: chalk.gray,
}[task.priority || 'medium'] || chalk.white; }[task.priority || "medium"] || chalk.white;
// Format status // Format status
const status = getStatusWithColor(task.status, true); const status = getStatusWithColor(task.status, true);
@@ -517,38 +512,38 @@ function listTasks(
task.id.toString(), task.id.toString(),
truncate(cleanTitle, titleWidth - 3), truncate(cleanTitle, titleWidth - 3),
status, status,
priorityColor(truncate(task.priority || 'medium', priorityWidth - 2)), priorityColor(truncate(task.priority || "medium", priorityWidth - 2)),
depText, depText,
task.complexityScore task.complexityScore
? getComplexityWithColor(task.complexityScore) ? getComplexityWithColor(task.complexityScore)
: chalk.gray('N/A') : chalk.gray("N/A"),
]); ]);
// Add subtasks if requested // Add subtasks if requested
if (withSubtasks && task.subtasks && task.subtasks.length > 0) { if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => { task.subtasks.forEach((subtask) => {
// Format subtask dependencies with status indicators // Format subtask dependencies with status indicators
let subtaskDepText = 'None'; let subtaskDepText = "None";
if (subtask.dependencies && subtask.dependencies.length > 0) { if (subtask.dependencies && subtask.dependencies.length > 0) {
// Handle both subtask-to-subtask and subtask-to-task dependencies // Handle both subtask-to-subtask and subtask-to-task dependencies
const formattedDeps = subtask.dependencies const formattedDeps = subtask.dependencies
.map((depId) => { .map((depId) => {
// Check if it's a dependency on another subtask // Check if it's a dependency on another subtask
if (typeof depId === 'number' && depId < 100) { if (typeof depId === "number" && depId < 100) {
const foundSubtask = task.subtasks.find( const foundSubtask = task.subtasks.find(
(st) => st.id === depId (st) => st.id === depId
); );
if (foundSubtask) { if (foundSubtask) {
const isDone = const isDone =
foundSubtask.status === 'done' || foundSubtask.status === "done" ||
foundSubtask.status === 'completed'; foundSubtask.status === "completed";
const isInProgress = foundSubtask.status === 'in-progress'; const isInProgress = foundSubtask.status === "in-progress";
// Use consistent color formatting instead of emojis // Use consistent color formatting instead of emojis
if (isDone) { if (isDone) {
return chalk.green.bold(`${task.id}.${depId}`); return chalk.green.bold(`${task.id}.${depId}`);
} else if (isInProgress) { } else if (isInProgress) {
return chalk.hex('#FFA500').bold(`${task.id}.${depId}`); return chalk.hex("#FFA500").bold(`${task.id}.${depId}`);
} else { } else {
return chalk.red.bold(`${task.id}.${depId}`); return chalk.red.bold(`${task.id}.${depId}`);
} }
@@ -560,22 +555,22 @@ function listTasks(
// Add complexity to depTask before checking status // Add complexity to depTask before checking status
addComplexityToTask(depTask, complexityReport); addComplexityToTask(depTask, complexityReport);
const isDone = const isDone =
depTask.status === 'done' || depTask.status === 'completed'; depTask.status === "done" || depTask.status === "completed";
const isInProgress = depTask.status === 'in-progress'; const isInProgress = depTask.status === "in-progress";
// Use the same color scheme as in formatDependenciesWithStatus // Use the same color scheme as in formatDependenciesWithStatus
if (isDone) { if (isDone) {
return chalk.green.bold(`${depId}`); return chalk.green.bold(`${depId}`);
} else if (isInProgress) { } else if (isInProgress) {
return chalk.hex('#FFA500').bold(`${depId}`); return chalk.hex("#FFA500").bold(`${depId}`);
} else { } else {
return chalk.red.bold(`${depId}`); return chalk.red.bold(`${depId}`);
} }
} }
return chalk.cyan(depId.toString()); return chalk.cyan(depId.toString());
}) })
.join(', '); .join(", ");
subtaskDepText = formattedDeps || chalk.gray('None'); subtaskDepText = formattedDeps || chalk.gray("None");
} }
// Add the subtask row without truncating dependencies // Add the subtask row without truncating dependencies
@@ -583,11 +578,11 @@ function listTasks(
`${task.id}.${subtask.id}`, `${task.id}.${subtask.id}`,
chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`), chalk.dim(`└─ ${truncate(subtask.title, titleWidth - 5)}`),
getStatusWithColor(subtask.status, true), getStatusWithColor(subtask.status, true),
chalk.dim('-'), chalk.dim("-"),
subtaskDepText, subtaskDepText,
subtask.complexityScore subtask.complexityScore
? chalk.gray(`${subtask.complexityScore}`) ? chalk.gray(`${subtask.complexityScore}`)
: chalk.gray('N/A') : chalk.gray("N/A"),
]); ]);
}); });
} }
@@ -597,12 +592,12 @@ function listTasks(
try { try {
console.log(table.toString()); console.log(table.toString());
} catch (err) { } catch (err) {
log('error', `Error rendering table: ${err.message}`); log("error", `Error rendering table: ${err.message}`);
// Fall back to simpler output // Fall back to simpler output
console.log( console.log(
chalk.yellow( chalk.yellow(
'\nFalling back to simple task list due to terminal width constraints:' "\nFalling back to simple task list due to terminal width constraints:"
) )
); );
filteredTasks.forEach((task) => { filteredTasks.forEach((task) => {
@@ -624,13 +619,13 @@ function listTasks(
const priorityColors = { const priorityColors = {
high: chalk.red.bold, high: chalk.red.bold,
medium: chalk.yellow, medium: chalk.yellow,
low: chalk.gray low: chalk.gray,
}; };
// Show next task box in a prominent color // Show next task box in a prominent color
if (nextItem) { if (nextItem) {
// Prepare subtasks section if they exist (Only tasks have .subtasks property) // Prepare subtasks section if they exist (Only tasks have .subtasks property)
let subtasksSection = ''; let subtasksSection = "";
// Check if the nextItem is a top-level task before looking for subtasks // Check if the nextItem is a top-level task before looking for subtasks
const parentTaskForSubtasks = data.tasks.find( const parentTaskForSubtasks = data.tasks.find(
(t) => String(t.id) === String(nextItem.id) (t) => String(t.id) === String(nextItem.id)
@@ -640,75 +635,75 @@ function listTasks(
parentTaskForSubtasks.subtasks && parentTaskForSubtasks.subtasks &&
parentTaskForSubtasks.subtasks.length > 0 parentTaskForSubtasks.subtasks.length > 0
) { ) {
subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`; subtasksSection = `\n\n${chalk.white.bold("Subtasks:")}\n`;
subtasksSection += parentTaskForSubtasks.subtasks subtasksSection += parentTaskForSubtasks.subtasks
.map((subtask) => { .map((subtask) => {
// Add complexity to subtask before display // Add complexity to subtask before display
addComplexityToTask(subtask, complexityReport); addComplexityToTask(subtask, complexityReport);
// Using a more simplified format for subtask status display // Using a more simplified format for subtask status display
const status = subtask.status || 'pending'; const status = subtask.status || "pending";
const statusColors = { const statusColors = {
done: chalk.green, done: chalk.green,
completed: chalk.green, completed: chalk.green,
pending: chalk.yellow, pending: chalk.yellow,
'in-progress': chalk.blue, "in-progress": chalk.blue,
deferred: chalk.gray, deferred: chalk.gray,
blocked: chalk.red, blocked: chalk.red,
cancelled: chalk.gray cancelled: chalk.gray,
}; };
const statusColor = const statusColor =
statusColors[status.toLowerCase()] || chalk.white; statusColors[status.toLowerCase()] || chalk.white;
// Ensure subtask ID is displayed correctly using parent ID from the original task object // Ensure subtask ID is displayed correctly using parent ID from the original task object
return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`; return `${chalk.cyan(`${parentTaskForSubtasks.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
}) })
.join('\n'); .join("\n");
} }
console.log( console.log(
boxen( boxen(
chalk.hex('#FF8800').bold( chalk.hex("#FF8800").bold(
// Use nextItem.id and nextItem.title // Use nextItem.id and nextItem.title
`🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}` `🔥 Next Task to Work On: #${nextItem.id} - ${nextItem.title}`
) + ) +
'\n\n' + "\n\n" +
// Use nextItem.priority, nextItem.status, nextItem.dependencies // Use nextItem.priority, nextItem.status, nextItem.dependencies
`${chalk.white('Priority:')} ${priorityColors[nextItem.priority || 'medium'](nextItem.priority || 'medium')} ${chalk.white('Status:')} ${getStatusWithColor(nextItem.status, true)}\n` + `${chalk.white("Priority:")} ${priorityColors[nextItem.priority || "medium"](nextItem.priority || "medium")} ${chalk.white("Status:")} ${getStatusWithColor(nextItem.status, true)}\n` +
`${chalk.white('Dependencies:')} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray('None')}\n\n` + `${chalk.white("Dependencies:")} ${nextItem.dependencies && nextItem.dependencies.length > 0 ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : chalk.gray("None")}\n\n` +
// Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this) // Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this)
// *** Fetching original item for description and details *** // *** Fetching original item for description and details ***
`${chalk.white('Description:')} ${getWorkItemDescription(nextItem, data.tasks)}` + `${chalk.white("Description:")} ${getWorkItemDescription(nextItem, data.tasks)}` +
subtasksSection + // <-- Subtasks are handled above now subtasksSection + // <-- Subtasks are handled above now
'\n\n' + "\n\n" +
// Use nextItem.id // Use nextItem.id
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` + `${chalk.cyan("Start working:")} ${chalk.yellow(`task-master set-status --id=${nextItem.id} --status=in-progress`)}\n` +
// Use nextItem.id // Use nextItem.id
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextItem.id}`)}`, `${chalk.cyan("View details:")} ${chalk.yellow(`task-master show ${nextItem.id}`)}`,
{ {
padding: { left: 2, right: 2, top: 1, bottom: 1 }, padding: { left: 2, right: 2, top: 1, bottom: 1 },
borderColor: '#FF8800', borderColor: "#FF8800",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 }, margin: { top: 1, bottom: 1 },
title: '⚡ RECOMMENDED NEXT TASK ⚡', title: "⚡ RECOMMENDED NEXT TASK ⚡",
titleAlignment: 'center', titleAlignment: "center",
width: terminalWidth - 4, width: terminalWidth - 4,
fullscreen: false fullscreen: false,
} }
) )
); );
} else { } else {
console.log( console.log(
boxen( boxen(
chalk.hex('#FF8800').bold('No eligible next task found') + chalk.hex("#FF8800").bold("No eligible next task found") +
'\n\n' + "\n\n" +
'All pending tasks have dependencies that are not yet completed, or all tasks are done.', "All pending tasks have dependencies that are not yet completed, or all tasks are done.",
{ {
padding: 1, padding: 1,
borderColor: '#FF8800', borderColor: "#FF8800",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1, bottom: 1 }, margin: { top: 1, bottom: 1 },
title: '⚡ NEXT TASK ⚡', title: "⚡ NEXT TASK ⚡",
titleAlignment: 'center', titleAlignment: "center",
width: terminalWidth - 4 // Use full terminal width minus a small margin width: terminalWidth - 4, // Use full terminal width minus a small margin
} }
) )
); );
@@ -717,28 +712,28 @@ function listTasks(
// Show next steps // Show next steps
console.log( console.log(
boxen( boxen(
chalk.white.bold('Suggested Next Steps:') + chalk.white.bold("Suggested Next Steps:") +
'\n\n' + "\n\n" +
`${chalk.cyan('1.')} Run ${chalk.yellow('task-master next')} to see what to work on next\n` + `${chalk.cyan("1.")} Run ${chalk.yellow("task-master next")} to see what to work on next\n` +
`${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks\n` + `${chalk.cyan("2.")} Run ${chalk.yellow("task-master expand --id=<id>")} to break down a task into subtasks\n` +
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master set-status --id=<id> --status=done')} to mark a task as complete`, `${chalk.cyan("3.")} Run ${chalk.yellow("task-master set-status --id=<id> --status=done")} to mark a task as complete`,
{ {
padding: 1, padding: 1,
borderColor: 'gray', borderColor: "gray",
borderStyle: 'round', borderStyle: "round",
margin: { top: 1 } margin: { top: 1 },
} }
) )
); );
} catch (error) { } catch (error) {
log('error', `Error listing tasks: ${error.message}`); log("error", `Error listing tasks: ${error.message}`);
if (outputFormat === 'json') { if (outputFormat === "json") {
// Return structured error for JSON output // Return structured error for JSON output
throw { throw {
code: 'TASK_LIST_ERROR', code: "TASK_LIST_ERROR",
message: error.message, message: error.message,
details: error.stack details: error.stack,
}; };
} }
@@ -749,18 +744,18 @@ function listTasks(
// *** Helper function to get description for task or subtask *** // *** Helper function to get description for task or subtask ***
function getWorkItemDescription(item, allTasks) { function getWorkItemDescription(item, allTasks) {
if (!item) return 'N/A'; if (!item) return "N/A";
if (item.parentId) { if (item.parentId) {
// It's a subtask // It's a subtask
const parent = allTasks.find((t) => t.id === item.parentId); const parent = allTasks.find((t) => t.id === item.parentId);
const subtask = parent?.subtasks?.find( const subtask = parent?.subtasks?.find(
(st) => `${parent.id}.${st.id}` === item.id (st) => `${parent.id}.${st.id}` === item.id
); );
return subtask?.description || 'No description available.'; return subtask?.description || "No description available.";
} else { } else {
// It's a top-level task // It's a top-level task
const task = allTasks.find((t) => String(t.id) === String(item.id)); const task = allTasks.find((t) => String(t.id) === String(item.id));
return task?.description || 'No description available.'; return task?.description || "No description available.";
} }
} }

View File

@@ -1,17 +1,17 @@
import path from 'path'; import path from "path";
import chalk from 'chalk'; import chalk from "chalk";
import boxen from 'boxen'; import boxen from "boxen";
import { log, readJSON, writeJSON, findTaskById } from '../utils.js'; import { log, readJSON, writeJSON, findTaskById } from "../utils.js";
import { displayBanner } from '../ui.js'; import { displayBanner } from "../ui.js";
import { validateTaskDependencies } from '../dependency-manager.js'; import { validateTaskDependencies } from "../dependency-manager.js";
import { getDebugFlag } from '../config-manager.js'; import { getDebugFlag } from "../config-manager.js";
import updateSingleTaskStatus from './update-single-task-status.js'; import updateSingleTaskStatus from "./update-single-task-status.js";
import generateTaskFiles from './generate-task-files.js'; import generateTaskFiles from "./generate-task-files.js";
import { import {
isValidTaskStatus, isValidTaskStatus,
TASK_STATUS_OPTIONS TASK_STATUS_OPTIONS,
} from '../../../src/constants/task-status.js'; } from "../../../src/constants/task-status.js";
/** /**
* Set the status of a task * Set the status of a task
@@ -25,7 +25,7 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
try { try {
if (!isValidTaskStatus(newStatus)) { if (!isValidTaskStatus(newStatus)) {
throw new Error( throw new Error(
`Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(', ')}` `Error: Invalid status value: ${newStatus}. Use one of: ${TASK_STATUS_OPTIONS.join(", ")}`
); );
} }
// Determine if we're in MCP mode by checking for mcpLog // Determine if we're in MCP mode by checking for mcpLog
@@ -33,25 +33,23 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
// Only display UI elements if not in MCP mode // Only display UI elements if not in MCP mode
if (!isMcpMode) { if (!isMcpMode) {
displayBanner();
console.log( console.log(
boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), { boxen(chalk.white.bold(`Updating Task Status to: ${newStatus}`), {
padding: 1, padding: 1,
borderColor: 'blue', borderColor: "blue",
borderStyle: 'round' borderStyle: "round",
}) })
); );
} }
log('info', `Reading tasks from ${tasksPath}...`); log("info", `Reading tasks from ${tasksPath}...`);
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
throw new Error(`No valid tasks found in ${tasksPath}`); throw new Error(`No valid tasks found in ${tasksPath}`);
} }
// Handle multiple task IDs (comma-separated) // Handle multiple task IDs (comma-separated)
const taskIds = taskIdInput.split(',').map((id) => id.trim()); const taskIds = taskIdInput.split(",").map((id) => id.trim());
const updatedTasks = []; const updatedTasks = [];
// Update each task // Update each task
@@ -64,13 +62,13 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
writeJSON(tasksPath, data); writeJSON(tasksPath, data);
// Validate dependencies after status update // Validate dependencies after status update
log('info', 'Validating dependencies after status update...'); log("info", "Validating dependencies after status update...");
validateTaskDependencies(data.tasks); validateTaskDependencies(data.tasks);
// Generate individual task files // Generate individual task files
log('info', 'Regenerating task files...'); log("info", "Regenerating task files...");
await generateTaskFiles(tasksPath, path.dirname(tasksPath), { await generateTaskFiles(tasksPath, path.dirname(tasksPath), {
mcpLog: options.mcpLog mcpLog: options.mcpLog,
}); });
// Display success message - only in CLI mode // Display success message - only in CLI mode
@@ -82,10 +80,10 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
console.log( console.log(
boxen( boxen(
chalk.white.bold(`Successfully updated task ${id} status:`) + chalk.white.bold(`Successfully updated task ${id} status:`) +
'\n' + "\n" +
`From: ${chalk.yellow(task ? task.status : 'unknown')}\n` + `From: ${chalk.yellow(task ? task.status : "unknown")}\n` +
`To: ${chalk.green(newStatus)}`, `To: ${chalk.green(newStatus)}`,
{ padding: 1, borderColor: 'green', borderStyle: 'round' } { padding: 1, borderColor: "green", borderStyle: "round" }
) )
); );
} }
@@ -96,11 +94,11 @@ async function setTaskStatus(tasksPath, taskIdInput, newStatus, options = {}) {
success: true, success: true,
updatedTasks: updatedTasks.map((id) => ({ updatedTasks: updatedTasks.map((id) => ({
id, id,
status: newStatus status: newStatus,
})) })),
}; };
} catch (error) { } catch (error) {
log('error', `Error setting task status: ${error.message}`); log("error", `Error setting task status: ${error.message}`);
// Only show error UI in CLI mode // Only show error UI in CLI mode
if (!options?.mcpLog) { if (!options?.mcpLog) {

View File

@@ -40,7 +40,7 @@ const warmGradient = gradient(["#fb8b24", "#e36414", "#9a031e"]);
function displayBanner() { function displayBanner() {
if (isSilentMode()) return; if (isSilentMode()) return;
console.clear(); // console.clear(); // Removing this to avoid clearing the terminal per command
const bannerText = figlet.textSync("Task Master", { const bannerText = figlet.textSync("Task Master", {
font: "Standard", font: "Standard",
horizontalLayout: "default", horizontalLayout: "default",
@@ -78,6 +78,8 @@ function displayBanner() {
* @returns {Object} Spinner object * @returns {Object} Spinner object
*/ */
function startLoadingIndicator(message) { function startLoadingIndicator(message) {
if (isSilentMode()) return null;
const spinner = ora({ const spinner = ora({
text: message, text: message,
color: "cyan", color: "cyan",
@@ -87,15 +89,75 @@ function startLoadingIndicator(message) {
} }
/** /**
* Stop a loading indicator * Stop a loading indicator (basic stop, no success/fail indicator)
* @param {Object} spinner - Spinner object to stop * @param {Object} spinner - Spinner object to stop
*/ */
function stopLoadingIndicator(spinner) { function stopLoadingIndicator(spinner) {
if (spinner && spinner.stop) { if (spinner && typeof spinner.stop === "function") {
spinner.stop(); spinner.stop();
} }
} }
/**
* Complete a loading indicator with success (shows checkmark)
* @param {Object} spinner - Spinner object to complete
* @param {string} message - Optional success message (defaults to current text)
*/
function succeedLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.succeed === "function") {
if (message) {
spinner.succeed(message);
} else {
spinner.succeed();
}
}
}
/**
* Complete a loading indicator with failure (shows X)
* @param {Object} spinner - Spinner object to fail
* @param {string} message - Optional failure message (defaults to current text)
*/
function failLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.fail === "function") {
if (message) {
spinner.fail(message);
} else {
spinner.fail();
}
}
}
/**
* Complete a loading indicator with warning (shows warning symbol)
* @param {Object} spinner - Spinner object to warn
* @param {string} message - Optional warning message (defaults to current text)
*/
function warnLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.warn === "function") {
if (message) {
spinner.warn(message);
} else {
spinner.warn();
}
}
}
/**
* Complete a loading indicator with info (shows info symbol)
* @param {Object} spinner - Spinner object to complete with info
* @param {string} message - Optional info message (defaults to current text)
*/
function infoLoadingIndicator(spinner, message = null) {
if (spinner && typeof spinner.info === "function") {
if (message) {
spinner.info(message);
} else {
spinner.info();
}
}
}
/** /**
* Create a colored progress bar * Create a colored progress bar
* @param {number} percent - The completion percentage * @param {number} percent - The completion percentage
@@ -232,14 +294,14 @@ function getStatusWithColor(status, forTable = false) {
} }
const statusConfig = { const statusConfig = {
done: { color: chalk.green, icon: "", tableIcon: "✓" }, done: { color: chalk.green, icon: "", tableIcon: "✓" },
completed: { color: chalk.green, icon: "", tableIcon: "✓" }, completed: { color: chalk.green, icon: "", tableIcon: "✓" },
pending: { color: chalk.yellow, icon: "⏱️", tableIcon: "⏱" }, pending: { color: chalk.yellow, icon: "", tableIcon: "⏱" },
"in-progress": { color: chalk.hex("#FFA500"), icon: "🔄", tableIcon: "►" }, "in-progress": { color: chalk.hex("#FFA500"), icon: "🔄", tableIcon: "►" },
deferred: { color: chalk.gray, icon: "⏱️", tableIcon: "⏱" }, deferred: { color: chalk.gray, icon: "x", tableIcon: "⏱" },
blocked: { color: chalk.red, icon: "", tableIcon: "✗" }, blocked: { color: chalk.red, icon: "!", tableIcon: "✗" },
review: { color: chalk.magenta, icon: "👀", tableIcon: "👁" }, review: { color: chalk.magenta, icon: "?", tableIcon: "?" },
cancelled: { color: chalk.gray, icon: "❌", tableIcon: "" }, cancelled: { color: chalk.gray, icon: "❌", tableIcon: "x" },
}; };
const config = statusConfig[status.toLowerCase()] || { const config = statusConfig[status.toLowerCase()] || {
@@ -383,8 +445,6 @@ function formatDependenciesWithStatus(
* Display a comprehensive help guide * Display a comprehensive help guide
*/ */
function displayHelp() { function displayHelp() {
displayBanner();
// Get terminal width - moved to top of function to make it available throughout // Get terminal width - moved to top of function to make it available throughout
const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect
@@ -767,8 +827,6 @@ function truncateString(str, maxLength) {
* @param {string} tasksPath - Path to the tasks.json file * @param {string} tasksPath - Path to the tasks.json file
*/ */
async function displayNextTask(tasksPath, complexityReportPath = null) { async function displayNextTask(tasksPath, complexityReportPath = null) {
displayBanner();
// Read the tasks file // Read the tasks file
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
@@ -1039,8 +1097,6 @@ async function displayTaskById(
complexityReportPath = null, complexityReportPath = null,
statusFilter = null statusFilter = null
) { ) {
displayBanner();
// Read the tasks file // Read the tasks file
const data = readJSON(tasksPath); const data = readJSON(tasksPath);
if (!data || !data.tasks) { if (!data || !data.tasks) {
@@ -1495,8 +1551,6 @@ async function displayTaskById(
* @param {string} reportPath - Path to the complexity report file * @param {string} reportPath - Path to the complexity report file
*/ */
async function displayComplexityReport(reportPath) { async function displayComplexityReport(reportPath) {
displayBanner();
// Check if the report exists // Check if the report exists
if (!fs.existsSync(reportPath)) { if (!fs.existsSync(reportPath)) {
console.log( console.log(
@@ -2094,4 +2148,8 @@ export {
displayModelConfiguration, displayModelConfiguration,
displayAvailableModels, displayAvailableModels,
displayAiUsageSummary, displayAiUsageSummary,
succeedLoadingIndicator,
failLoadingIndicator,
warnLoadingIndicator,
infoLoadingIndicator,
}; };