claude-task-master/scripts/modules/dependency-manager.js

1326 lines
49 KiB
JavaScript
Raw Normal View History

Refactor: Modularize Task Master CLI into Modules Directory Simplified the Task Master CLI by organizing code into modules within the directory. **Why:** - **Better Organization:** Code is now grouped by function (AI, commands, dependencies, tasks, UI, utilities). - **Easier to Maintain:** Smaller modules are simpler to update and fix. - **Scalable:** New features can be added more easily in a structured way. **What Changed:** - Moved code from single _____ _ __ __ _ |_ _|_ _ ___| | __ | \/ | __ _ ___| |_ ___ _ __ | |/ _` / __| |/ / | |\/| |/ _` / __| __/ _ \ '__| | | (_| \__ \ < | | | | (_| \__ \ || __/ | |_|\__,_|___/_|\_\ |_| |_|\__,_|___/\__\___|_| by https://x.com/eyaltoledano ╭────────────────────────────────────────────╮ │ │ │ Version: 0.9.16 Project: Task Master │ │ │ ╰────────────────────────────────────────────╯ ╭─────────────────────╮ │ │ │ Task Master CLI │ │ │ ╰─────────────────────╯ ╭───────────────────╮ │ Task Generation │ ╰───────────────────╯ parse-prd --input=<file.txt> [--tasks=10] Generate tasks from a PRD document generate Create individual task files from tasks… ╭───────────────────╮ │ Task Management │ ╰───────────────────╯ list [--status=<status>] [--with-subtas… List all tasks with their status set-status --id=<id> --status=<status> Update task status (done, pending, etc.) update --from=<id> --prompt="<context>" Update tasks based on new requirements add-task --prompt="<text>" [--dependencies=… Add a new task using AI add-dependency --id=<id> --depends-on=<id> Add a dependency to a task remove-dependency --id=<id> --depends-on=<id> Remove a dependency from a task ╭──────────────────────────╮ │ Task Analysis & Detail │ ╰──────────────────────────╯ analyze-complexity [--research] [--threshold=5] Analyze tasks and generate expansion re… complexity-report [--file=<path>] Display the complexity analysis report expand --id=<id> [--num=5] [--research] [… Break down tasks into detailed subtasks expand --all [--force] [--research] Expand all pending tasks with subtasks clear-subtasks --id=<id> Remove subtasks from specified tasks ╭─────────────────────────────╮ │ Task Navigation & Viewing │ ╰─────────────────────────────╯ next Show the next task to work on based on … show <id> Display detailed information about a sp… ╭─────────────────────────╮ │ Dependency Management │ ╰─────────────────────────╯ validate-dependenci… Identify invalid dependencies without f… fix-dependencies Fix invalid dependencies automatically ╭─────────────────────────╮ │ Environment Variables │ ╰─────────────────────────╯ ANTHROPIC_API_KEY Your Anthropic API key Required MODEL Claude model to use Default: claude-3-7-sonn… MAX_TOKENS Maximum tokens for responses Default: 4000 TEMPERATURE Temperature for model responses Default: 0.7 PERPLEXITY_API_KEY Perplexity API key for research Optional PERPLEXITY_MODEL Perplexity model to use Default: sonar-small-onl… DEBUG Enable debug logging Default: false LOG_LEVEL Console output level (debug,info,warn,error) Default: info DEFAULT_SUBTASKS Default number of subtasks to generate Default: 3 DEFAULT_PRIORITY Default task priority Default: medium PROJECT_NAME Project name displayed in UI Default: Task Master file into these new modules: - : AI interactions (Claude, Perplexity) - : CLI command definitions (Commander.js) - : Task dependency handling - : Core task operations (create, list, update, etc.) - : User interface elements (display, formatting) - : Utility functions and configuration - : Exports all modules - Replaced direct use of _____ _ __ __ _ |_ _|_ _ ___| | __ | \/ | __ _ ___| |_ ___ _ __ | |/ _` / __| |/ / | |\/| |/ _` / __| __/ _ \ '__| | | (_| \__ \ < | | | | (_| \__ \ || __/ | |_|\__,_|___/_|\_\ |_| |_|\__,_|___/\__\___|_| by https://x.com/eyaltoledano ╭────────────────────────────────────────────╮ │ │ │ Version: 0.9.16 Project: Task Master │ │ │ ╰────────────────────────────────────────────╯ ╭─────────────────────╮ │ │ │ Task Master CLI │ │ │ ╰─────────────────────╯ ╭───────────────────╮ │ Task Generation │ ╰───────────────────╯ parse-prd --input=<file.txt> [--tasks=10] Generate tasks from a PRD document generate Create individual task files from tasks… ╭───────────────────╮ │ Task Management │ ╰───────────────────╯ list [--status=<status>] [--with-subtas… List all tasks with their status set-status --id=<id> --status=<status> Update task status (done, pending, etc.) update --from=<id> --prompt="<context>" Update tasks based on new requirements add-task --prompt="<text>" [--dependencies=… Add a new task using AI add-dependency --id=<id> --depends-on=<id> Add a dependency to a task remove-dependency --id=<id> --depends-on=<id> Remove a dependency from a task ╭──────────────────────────╮ │ Task Analysis & Detail │ ╰──────────────────────────╯ analyze-complexity [--research] [--threshold=5] Analyze tasks and generate expansion re… complexity-report [--file=<path>] Display the complexity analysis report expand --id=<id> [--num=5] [--research] [… Break down tasks into detailed subtasks expand --all [--force] [--research] Expand all pending tasks with subtasks clear-subtasks --id=<id> Remove subtasks from specified tasks ╭─────────────────────────────╮ │ Task Navigation & Viewing │ ╰─────────────────────────────╯ next Show the next task to work on based on … show <id> Display detailed information about a sp… ╭─────────────────────────╮ │ Dependency Management │ ╰─────────────────────────╯ validate-dependenci… Identify invalid dependencies without f… fix-dependencies Fix invalid dependencies automatically ╭─────────────────────────╮ │ Environment Variables │ ╰─────────────────────────╯ ANTHROPIC_API_KEY Your Anthropic API key Required MODEL Claude model to use Default: claude-3-7-sonn… MAX_TOKENS Maximum tokens for responses Default: 4000 TEMPERATURE Temperature for model responses Default: 0.7 PERPLEXITY_API_KEY Perplexity API key for research Optional PERPLEXITY_MODEL Perplexity model to use Default: sonar-small-onl… DEBUG Enable debug logging Default: false LOG_LEVEL Console output level (debug,info,warn,error) Default: info DEFAULT_SUBTASKS Default number of subtasks to generate Default: 3 DEFAULT_PRIORITY Default task priority Default: medium PROJECT_NAME Project name displayed in UI Default: Task Master with the global command (see ). - Updated documentation () to reflect the new command. **Benefits:** Code is now cleaner, easier to work with, and ready for future growth. Use the command (or ) to run the CLI. See for command details.
2025-03-23 23:19:37 -04:00
/**
* dependency-manager.js
* Manages task dependencies and relationships
*/
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import { Anthropic } from '@anthropic-ai/sdk';
import {
log,
readJSON,
writeJSON,
taskExists,
formatTaskId,
findCycles
} from './utils.js';
import { displayBanner } from './ui.js';
import { generateTaskFiles } from './task-manager.js';
// Initialize Anthropic client
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
});
/**
* Add a dependency to a task
* @param {string} tasksPath - Path to the tasks.json file
* @param {number|string} taskId - ID of the task to add dependency to
* @param {number|string} dependencyId - ID of the task to add as dependency
*/
async function addDependency(tasksPath, taskId, dependencyId) {
log('info', `Adding dependency ${dependencyId} to task ${taskId}...`);
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId = typeof taskId === 'string' && taskId.includes('.')
? taskId : parseInt(taskId, 10);
const formattedDependencyId = formatTaskId(dependencyId);
// Check if the dependency task or subtask actually exists
if (!taskExists(data.tasks, formattedDependencyId)) {
log('error', `Dependency target ${formattedDependencyId} does not exist in tasks.json`);
process.exit(1);
}
// Find the task to update
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId.split('.').map(id => parseInt(id, 10));
const parentTask = data.tasks.find(t => t.id === parentId);
if (!parentTask) {
log('error', `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
targetTask = parentTask.subtasks.find(s => s.id === subtaskId);
isSubtask = true;
if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
// Regular task (not a subtask)
targetTask = data.tasks.find(t => t.id === formattedTaskId);
if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
// Initialize dependencies array if it doesn't exist
if (!targetTask.dependencies) {
targetTask.dependencies = [];
}
// Check if dependency already exists
if (targetTask.dependencies.some(d => {
// Convert both to strings for comparison to handle both numeric and string IDs
return String(d) === String(formattedDependencyId);
})) {
log('warn', `Dependency ${formattedDependencyId} already exists in task ${formattedTaskId}.`);
return;
}
// Check if the task is trying to depend on itself
if (String(formattedTaskId) === String(formattedDependencyId)) {
log('error', `Task ${formattedTaskId} cannot depend on itself.`);
process.exit(1);
}
// Check for circular dependencies
let dependencyChain = [formattedTaskId];
if (!isCircularDependency(data.tasks, formattedDependencyId, dependencyChain)) {
// Add the dependency
targetTask.dependencies.push(formattedDependencyId);
// Sort dependencies numerically or by parent task ID first, then subtask ID
targetTask.dependencies.sort((a, b) => {
if (typeof a === 'number' && typeof b === 'number') {
return a - b;
} else if (typeof a === 'string' && typeof b === 'string') {
const [aParent, aChild] = a.split('.').map(Number);
const [bParent, bChild] = b.split('.').map(Number);
return aParent !== bParent ? aParent - bParent : aChild - bChild;
} else if (typeof a === 'number') {
return -1; // Numbers come before strings
} else {
return 1; // Strings come after numbers
}
});
// Save changes
writeJSON(tasksPath, data);
log('success', `Added dependency ${formattedDependencyId} to task ${formattedTaskId}`);
// Generate updated task files
await generateTaskFiles(tasksPath, 'tasks');
log('info', 'Task files regenerated with updated dependencies.');
} else {
log('error', `Cannot add dependency ${formattedDependencyId} to task ${formattedTaskId} as it would create a circular dependency.`);
process.exit(1);
}
}
/**
* Remove a dependency from a task
* @param {string} tasksPath - Path to the tasks.json file
* @param {number|string} taskId - ID of the task to remove dependency from
* @param {number|string} dependencyId - ID of the task to remove as dependency
*/
async function removeDependency(tasksPath, taskId, dependencyId) {
log('info', `Removing dependency ${dependencyId} from task ${taskId}...`);
// Read tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', "No valid tasks found.");
process.exit(1);
}
// Format the task and dependency IDs correctly
const formattedTaskId = typeof taskId === 'string' && taskId.includes('.')
? taskId : parseInt(taskId, 10);
const formattedDependencyId = formatTaskId(dependencyId);
// Find the task to update
let targetTask = null;
let isSubtask = false;
if (typeof formattedTaskId === 'string' && formattedTaskId.includes('.')) {
// Handle dot notation for subtasks (e.g., "1.2")
const [parentId, subtaskId] = formattedTaskId.split('.').map(id => parseInt(id, 10));
const parentTask = data.tasks.find(t => t.id === parentId);
if (!parentTask) {
log('error', `Parent task ${parentId} not found.`);
process.exit(1);
}
if (!parentTask.subtasks) {
log('error', `Parent task ${parentId} has no subtasks.`);
process.exit(1);
}
targetTask = parentTask.subtasks.find(s => s.id === subtaskId);
isSubtask = true;
if (!targetTask) {
log('error', `Subtask ${formattedTaskId} not found.`);
process.exit(1);
}
} else {
// Regular task (not a subtask)
targetTask = data.tasks.find(t => t.id === formattedTaskId);
if (!targetTask) {
log('error', `Task ${formattedTaskId} not found.`);
process.exit(1);
}
}
// Check if the task has any dependencies
if (!targetTask.dependencies || targetTask.dependencies.length === 0) {
log('info', `Task ${formattedTaskId} has no dependencies, nothing to remove.`);
return;
}
// Normalize the dependency ID for comparison to handle different formats
const normalizedDependencyId = String(formattedDependencyId);
// Check if the dependency exists by comparing string representations
const dependencyIndex = targetTask.dependencies.findIndex(dep => {
// Convert both to strings for comparison
let depStr = String(dep);
// Special handling for numeric IDs that might be subtask references
if (typeof dep === 'number' && dep < 100 && isSubtask) {
// 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)
const [parentId] = formattedTaskId.split('.');
depStr = `${parentId}.${dep}`;
}
return depStr === normalizedDependencyId;
});
if (dependencyIndex === -1) {
log('info', `Task ${formattedTaskId} does not depend on ${formattedDependencyId}, no changes made.`);
return;
}
// Remove the dependency
targetTask.dependencies.splice(dependencyIndex, 1);
// Save the updated tasks
writeJSON(tasksPath, data);
// Success message
log('success', `Removed dependency: Task ${formattedTaskId} no longer depends on ${formattedDependencyId}`);
// Display a more visually appealing success message
console.log(boxen(
chalk.green(`Successfully removed dependency:\n\n`) +
`Task ${chalk.bold(formattedTaskId)} no longer depends on ${chalk.bold(formattedDependencyId)}`,
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
));
// Regenerate task files
await generateTaskFiles(tasksPath, 'tasks');
}
/**
* Check if adding a dependency would create a circular dependency
* @param {Array} tasks - All tasks
* @param {number|string} dependencyId - ID of the dependency being added
* @param {Array} chain - Current dependency chain being checked
* @returns {boolean} - True if circular dependency would be created, false otherwise
*/
function isCircularDependency(tasks, dependencyId, chain = []) {
// Convert chain elements and dependencyId to strings for consistent comparison
const chainStrs = chain.map(id => String(id));
const depIdStr = String(dependencyId);
// If the dependency is already in the chain, it would create a circular dependency
if (chainStrs.includes(depIdStr)) {
log('error', `Circular dependency detected: ${chainStrs.join(' -> ')} -> ${depIdStr}`);
return true;
}
// Check if this is a subtask dependency (e.g., "1.2")
const isSubtask = depIdStr.includes('.');
// Find the task or subtask by ID
let dependencyTask = null;
let dependencySubtask = null;
if (isSubtask) {
// Parse parent and subtask IDs
const [parentId, subtaskId] = depIdStr.split('.').map(id => isNaN(id) ? id : Number(id));
const parentTask = tasks.find(t => t.id === parentId);
if (parentTask && parentTask.subtasks) {
dependencySubtask = parentTask.subtasks.find(s => s.id === Number(subtaskId));
// For a subtask, we need to check dependencies of both the subtask and its parent
if (dependencySubtask && dependencySubtask.dependencies && dependencySubtask.dependencies.length > 0) {
// Recursively check each of the subtask's dependencies
const newChain = [...chainStrs, depIdStr];
const hasCircular = dependencySubtask.dependencies.some(depId => {
// Handle relative subtask references (e.g., numeric IDs referring to subtasks in the same parent task)
const normalizedDepId = typeof depId === 'number' && depId < 100
? `${parentId}.${depId}`
: depId;
return isCircularDependency(tasks, normalizedDepId, newChain);
});
if (hasCircular) return true;
}
// Also check if parent task has dependencies that could create a cycle
if (parentTask.dependencies && parentTask.dependencies.length > 0) {
// If any of the parent's dependencies create a cycle, return true
const newChain = [...chainStrs, depIdStr];
if (parentTask.dependencies.some(depId => isCircularDependency(tasks, depId, newChain))) {
return true;
}
}
return false;
}
} else {
// Regular task (not a subtask)
const depId = isNaN(dependencyId) ? dependencyId : Number(dependencyId);
dependencyTask = tasks.find(t => t.id === depId);
// If task not found or has no dependencies, there's no circular dependency
if (!dependencyTask || !dependencyTask.dependencies || dependencyTask.dependencies.length === 0) {
return false;
}
// Recursively check each of the dependency's dependencies
const newChain = [...chainStrs, depIdStr];
if (dependencyTask.dependencies.some(depId => isCircularDependency(tasks, depId, newChain))) {
return true;
}
// Also check for cycles through subtasks of this task
if (dependencyTask.subtasks && dependencyTask.subtasks.length > 0) {
for (const subtask of dependencyTask.subtasks) {
if (subtask.dependencies && subtask.dependencies.length > 0) {
// Check if any of this subtask's dependencies create a cycle
const subtaskId = `${dependencyTask.id}.${subtask.id}`;
const newSubtaskChain = [...chainStrs, depIdStr, subtaskId];
for (const subDepId of subtask.dependencies) {
// Handle relative subtask references
const normalizedDepId = typeof subDepId === 'number' && subDepId < 100
? `${dependencyTask.id}.${subDepId}`
: subDepId;
if (isCircularDependency(tasks, normalizedDepId, newSubtaskChain)) {
return true;
}
}
}
}
}
}
return false;
}
/**
* Validate and clean up task dependencies to ensure they only reference existing tasks
* @param {Array} tasks - Array of tasks to validate
* @param {string} tasksPath - Optional path to tasks.json to save changes
* @returns {boolean} - True if any changes were made to dependencies
*/
function validateTaskDependencies(tasks, tasksPath = null) {
// Create a set of valid task IDs for fast lookup
const validTaskIds = new Set(tasks.map(t => t.id));
// Create a set of valid subtask IDs (in the format "parentId.subtaskId")
const validSubtaskIds = new Set();
tasks.forEach(task => {
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
validSubtaskIds.add(`${task.id}.${subtask.id}`);
});
}
});
// Flag to track if any changes were made
let changesDetected = false;
// Validate all tasks and their dependencies
tasks.forEach(task => {
if (task.dependencies && Array.isArray(task.dependencies)) {
// First check for and remove duplicate dependencies
const uniqueDeps = new Set();
const uniqueDependencies = task.dependencies.filter(depId => {
// Convert to string for comparison to handle both numeric and string IDs
const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) {
log('warn', `Removing duplicate dependency from task ${task.id}: ${depId}`);
changesDetected = true;
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
// If we removed duplicates, update the array
if (uniqueDependencies.length !== task.dependencies.length) {
task.dependencies = uniqueDependencies;
changesDetected = true;
}
const validDependencies = uniqueDependencies.filter(depId => {
const isSubtask = typeof depId === 'string' && depId.includes('.');
if (isSubtask) {
// Check if the subtask exists
if (!validSubtaskIds.has(depId)) {
log('warn', `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`);
return false;
}
return true;
} else {
// Check if the task exists
const numericId = typeof depId === 'string' ? parseInt(depId, 10) : depId;
if (!validTaskIds.has(numericId)) {
log('warn', `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`);
return false;
}
return true;
}
});
// Update the task's dependencies array
if (validDependencies.length !== uniqueDependencies.length) {
task.dependencies = validDependencies;
changesDetected = true;
}
}
// Validate subtask dependencies
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
// First check for and remove duplicate dependencies
const uniqueDeps = new Set();
const uniqueDependencies = subtask.dependencies.filter(depId => {
// Convert to string for comparison to handle both numeric and string IDs
const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) {
log('warn', `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`);
changesDetected = true;
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
// If we removed duplicates, update the array
if (uniqueDependencies.length !== subtask.dependencies.length) {
subtask.dependencies = uniqueDependencies;
changesDetected = true;
}
// Check for and remove self-dependencies
const subtaskId = `${task.id}.${subtask.id}`;
const selfDependencyIndex = subtask.dependencies.findIndex(depId => {
return String(depId) === String(subtaskId);
});
if (selfDependencyIndex !== -1) {
log('warn', `Removing self-dependency from subtask ${subtaskId} (subtask cannot depend on itself)`);
subtask.dependencies.splice(selfDependencyIndex, 1);
changesDetected = true;
}
// Then validate remaining dependencies
const validSubtaskDeps = subtask.dependencies.filter(depId => {
const isSubtask = typeof depId === 'string' && depId.includes('.');
if (isSubtask) {
// Check if the subtask exists
if (!validSubtaskIds.has(depId)) {
log('warn', `Removing invalid subtask dependency from subtask ${task.id}.${subtask.id}: ${depId} (subtask does not exist)`);
return false;
}
return true;
} else {
// Check if the task exists
const numericId = typeof depId === 'string' ? parseInt(depId, 10) : depId;
if (!validTaskIds.has(numericId)) {
log('warn', `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`);
return false;
}
return true;
}
});
// Update the subtask's dependencies array
if (validSubtaskDeps.length !== subtask.dependencies.length) {
subtask.dependencies = validSubtaskDeps;
changesDetected = true;
}
}
});
}
});
// Save changes if tasksPath is provided and changes were detected
if (tasksPath && changesDetected) {
try {
const data = readJSON(tasksPath);
if (data) {
data.tasks = tasks;
writeJSON(tasksPath, data);
log('info', 'Updated tasks.json to remove invalid and duplicate dependencies');
}
} catch (error) {
log('error', 'Failed to save changes to tasks.json', error);
}
}
return changesDetected;
}
/**
* Validate dependencies in task files
* @param {string} tasksPath - Path to tasks.json
*/
async function validateDependenciesCommand(tasksPath) {
displayBanner();
log('info', 'Checking for invalid dependencies in task files...');
// Read tasks data
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
// Count of tasks and subtasks for reporting
const taskCount = data.tasks.length;
let subtaskCount = 0;
data.tasks.forEach(task => {
if (task.subtasks && Array.isArray(task.subtasks)) {
subtaskCount += task.subtasks.length;
}
});
log('info', `Analyzing dependencies for ${taskCount} tasks and ${subtaskCount} subtasks...`);
// Track validation statistics
const stats = {
nonExistentDependenciesRemoved: 0,
selfDependenciesRemoved: 0,
tasksFixed: 0,
subtasksFixed: 0
};
// Monkey patch the log function to capture warnings and count fixes
const originalLog = log;
const warnings = [];
log = function(level, ...args) {
if (level === 'warn') {
warnings.push(args.join(' '));
// Count the type of fix based on the warning message
const msg = args.join(' ');
if (msg.includes('self-dependency')) {
stats.selfDependenciesRemoved++;
} else if (msg.includes('invalid')) {
stats.nonExistentDependenciesRemoved++;
}
// Count if it's a task or subtask being fixed
if (msg.includes('from subtask')) {
stats.subtasksFixed++;
} else if (msg.includes('from task')) {
stats.tasksFixed++;
}
}
// Call the original log function
return originalLog(level, ...args);
};
// Run validation
try {
const changesDetected = validateTaskDependencies(data.tasks, tasksPath);
// Create a detailed report
if (changesDetected) {
log('success', 'Invalid dependencies were removed from tasks.json');
// Show detailed stats in a nice box
console.log(boxen(
chalk.green(`Dependency Validation Results:\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.cyan('Non-existent dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}`,
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
// Show all warnings in a collapsible list if there are many
if (warnings.length > 0) {
console.log(chalk.yellow('\nDetailed fixes:'));
warnings.forEach(warning => {
console.log(` ${warning}`);
});
}
// Regenerate task files to reflect the changes
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
log('info', 'Task files regenerated to reflect dependency changes');
} else {
log('success', 'No invalid dependencies found - all dependencies are valid');
// Show validation summary
console.log(boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${taskCount}\n` +
`${chalk.cyan('Subtasks checked:')} ${subtaskCount}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
}
} finally {
// Restore the original log function
log = originalLog;
}
}
/**
* Helper function to count all dependencies across tasks and subtasks
* @param {Array} tasks - All tasks
* @returns {number} - Total number of dependencies
*/
function countAllDependencies(tasks) {
let count = 0;
tasks.forEach(task => {
// Count main task dependencies
if (task.dependencies && Array.isArray(task.dependencies)) {
count += task.dependencies.length;
}
// Count subtask dependencies
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
count += subtask.dependencies.length;
}
});
}
});
return count;
}
/**
* Fixes invalid dependencies in tasks.json
* @param {string} tasksPath - Path to tasks.json
*/
async function fixDependenciesCommand(tasksPath) {
displayBanner();
log('info', 'Checking for and fixing invalid dependencies in tasks.json...');
try {
// Read tasks data
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', 'No valid tasks found in tasks.json');
process.exit(1);
}
// Create a deep copy of the original data for comparison
const originalData = JSON.parse(JSON.stringify(data));
// Track fixes for reporting
const stats = {
nonExistentDependenciesRemoved: 0,
selfDependenciesRemoved: 0,
duplicateDependenciesRemoved: 0,
circularDependenciesFixed: 0,
tasksFixed: 0,
subtasksFixed: 0
};
// First phase: Remove duplicate dependencies in tasks
data.tasks.forEach(task => {
if (task.dependencies && Array.isArray(task.dependencies)) {
const uniqueDeps = new Set();
const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter(depId => {
const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) {
log('info', `Removing duplicate dependency from task ${task.id}: ${depId}`);
stats.duplicateDependenciesRemoved++;
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
if (task.dependencies.length < originalLength) {
stats.tasksFixed++;
}
}
// Check for duplicates in subtasks
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const uniqueDeps = new Set();
const originalLength = subtask.dependencies.length;
subtask.dependencies = subtask.dependencies.filter(depId => {
let depIdStr = String(depId);
if (typeof depId === 'number' && depId < 100) {
depIdStr = `${task.id}.${depId}`;
}
if (uniqueDeps.has(depIdStr)) {
log('info', `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`);
stats.duplicateDependenciesRemoved++;
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
if (subtask.dependencies.length < originalLength) {
stats.subtasksFixed++;
}
}
});
}
});
// Create validity maps for tasks and subtasks
const validTaskIds = new Set(data.tasks.map(t => t.id));
const validSubtaskIds = new Set();
data.tasks.forEach(task => {
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
validSubtaskIds.add(`${task.id}.${subtask.id}`);
});
}
});
// Second phase: Remove invalid task dependencies (non-existent tasks)
data.tasks.forEach(task => {
if (task.dependencies && Array.isArray(task.dependencies)) {
const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter(depId => {
const isSubtask = typeof depId === 'string' && depId.includes('.');
if (isSubtask) {
// Check if the subtask exists
if (!validSubtaskIds.has(depId)) {
log('info', `Removing invalid subtask dependency from task ${task.id}: ${depId} (subtask does not exist)`);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
} else {
// Check if the task exists
const numericId = typeof depId === 'string' ? parseInt(depId, 10) : depId;
if (!validTaskIds.has(numericId)) {
log('info', `Removing invalid task dependency from task ${task.id}: ${depId} (task does not exist)`);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
}
});
if (task.dependencies.length < originalLength) {
stats.tasksFixed++;
}
}
// Check subtask dependencies for invalid references
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const originalLength = subtask.dependencies.length;
const subtaskId = `${task.id}.${subtask.id}`;
// First check for self-dependencies
const hasSelfDependency = subtask.dependencies.some(depId => {
if (typeof depId === 'string' && depId.includes('.')) {
return depId === subtaskId;
} else if (typeof depId === 'number' && depId < 100) {
return depId === subtask.id;
}
return false;
});
if (hasSelfDependency) {
subtask.dependencies = subtask.dependencies.filter(depId => {
const normalizedDepId = typeof depId === 'number' && depId < 100
? `${task.id}.${depId}`
: String(depId);
if (normalizedDepId === subtaskId) {
log('info', `Removing self-dependency from subtask ${subtaskId}`);
stats.selfDependenciesRemoved++;
return false;
}
return true;
});
}
// Then check for non-existent dependencies
subtask.dependencies = subtask.dependencies.filter(depId => {
if (typeof depId === 'string' && depId.includes('.')) {
if (!validSubtaskIds.has(depId)) {
log('info', `Removing invalid subtask dependency from subtask ${subtaskId}: ${depId} (subtask does not exist)`);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
}
// Handle numeric dependencies
const numericId = typeof depId === 'number' ? depId : parseInt(depId, 10);
// Small numbers likely refer to subtasks in the same task
if (numericId < 100) {
const fullSubtaskId = `${task.id}.${numericId}`;
if (!validSubtaskIds.has(fullSubtaskId)) {
log('info', `Removing invalid subtask dependency from subtask ${subtaskId}: ${numericId}`);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
}
// Otherwise it's a task reference
if (!validTaskIds.has(numericId)) {
log('info', `Removing invalid task dependency from subtask ${subtaskId}: ${numericId}`);
stats.nonExistentDependenciesRemoved++;
return false;
}
return true;
});
if (subtask.dependencies.length < originalLength) {
stats.subtasksFixed++;
}
}
});
}
});
// Third phase: Check for circular dependencies
log('info', 'Checking for circular dependencies...');
// Build the dependency map for subtasks
const subtaskDependencyMap = new Map();
data.tasks.forEach(task => {
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
const subtaskId = `${task.id}.${subtask.id}`;
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const normalizedDeps = subtask.dependencies.map(depId => {
if (typeof depId === 'string' && depId.includes('.')) {
return depId;
} else if (typeof depId === 'number' && depId < 100) {
return `${task.id}.${depId}`;
}
return String(depId);
});
subtaskDependencyMap.set(subtaskId, normalizedDeps);
} else {
subtaskDependencyMap.set(subtaskId, []);
}
});
}
});
// Check for and fix circular dependencies
for (const [subtaskId, dependencies] of subtaskDependencyMap.entries()) {
const visited = new Set();
const recursionStack = new Set();
// Detect cycles
const cycleEdges = findCycles(subtaskId, subtaskDependencyMap, visited, recursionStack);
if (cycleEdges.length > 0) {
const [taskId, subtaskNum] = subtaskId.split('.').map(part => Number(part));
const task = data.tasks.find(t => t.id === taskId);
if (task && task.subtasks) {
const subtask = task.subtasks.find(st => st.id === subtaskNum);
if (subtask && subtask.dependencies) {
const originalLength = subtask.dependencies.length;
const edgesToRemove = cycleEdges.map(edge => {
if (edge.includes('.')) {
const [depTaskId, depSubtaskId] = edge.split('.').map(part => Number(part));
if (depTaskId === taskId) {
return depSubtaskId;
}
return edge;
}
return Number(edge);
});
subtask.dependencies = subtask.dependencies.filter(depId => {
const normalizedDepId = typeof depId === 'number' && depId < 100
? `${taskId}.${depId}`
: String(depId);
if (edgesToRemove.includes(depId) || edgesToRemove.includes(normalizedDepId)) {
log('info', `Breaking circular dependency: Removing ${normalizedDepId} from subtask ${subtaskId}`);
stats.circularDependenciesFixed++;
return false;
}
return true;
});
if (subtask.dependencies.length < originalLength) {
stats.subtasksFixed++;
}
}
}
}
}
// Check if any changes were made by comparing with original data
const dataChanged = JSON.stringify(data) !== JSON.stringify(originalData);
if (dataChanged) {
// Save the changes
writeJSON(tasksPath, data);
log('success', 'Fixed dependency issues in tasks.json');
// Regenerate task files
log('info', 'Regenerating task files to reflect dependency changes...');
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
} else {
log('info', 'No changes needed to fix dependencies');
}
// Show detailed statistics report
const totalFixedAll = stats.nonExistentDependenciesRemoved +
stats.selfDependenciesRemoved +
stats.duplicateDependenciesRemoved +
stats.circularDependenciesFixed;
if (totalFixedAll > 0) {
log('success', `Fixed ${totalFixedAll} dependency issues in total!`);
console.log(boxen(
chalk.green(`Dependency Fixes Summary:\n\n`) +
`${chalk.cyan('Invalid dependencies removed:')} ${stats.nonExistentDependenciesRemoved}\n` +
`${chalk.cyan('Self-dependencies removed:')} ${stats.selfDependenciesRemoved}\n` +
`${chalk.cyan('Duplicate dependencies removed:')} ${stats.duplicateDependenciesRemoved}\n` +
`${chalk.cyan('Circular dependencies fixed:')} ${stats.circularDependenciesFixed}\n\n` +
`${chalk.cyan('Tasks fixed:')} ${stats.tasksFixed}\n` +
`${chalk.cyan('Subtasks fixed:')} ${stats.subtasksFixed}\n`,
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
} else {
log('success', 'No dependency issues found - all dependencies are valid');
console.log(boxen(
chalk.green(`All Dependencies Are Valid\n\n`) +
`${chalk.cyan('Tasks checked:')} ${data.tasks.length}\n` +
`${chalk.cyan('Total dependencies verified:')} ${countAllDependencies(data.tasks)}`,
{ padding: 1, borderColor: 'green', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
}
} catch (error) {
log('error', "Error in fix-dependencies command:", error);
process.exit(1);
}
}
/**
* Clean up subtask dependencies by removing references to non-existent subtasks/tasks
* @param {Object} tasksData - The tasks data object with tasks array
* @returns {boolean} - True if any changes were made
*/
function cleanupSubtaskDependencies(tasksData) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
return false;
}
log('debug', 'Cleaning up subtask dependencies...');
let changesDetected = false;
let duplicatesRemoved = 0;
// Create validity maps for fast lookup
const validTaskIds = new Set(tasksData.tasks.map(t => t.id));
const validSubtaskIds = new Set();
// Create a dependency map for cycle detection
const subtaskDependencyMap = new Map();
// Populate the validSubtaskIds set
tasksData.tasks.forEach(task => {
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
validSubtaskIds.add(`${task.id}.${subtask.id}`);
});
}
});
// Clean up each task's subtasks
tasksData.tasks.forEach(task => {
if (!task.subtasks || !Array.isArray(task.subtasks)) {
return;
}
task.subtasks.forEach(subtask => {
if (!subtask.dependencies || !Array.isArray(subtask.dependencies)) {
return;
}
const originalLength = subtask.dependencies.length;
const subtaskId = `${task.id}.${subtask.id}`;
// First remove duplicate dependencies
const uniqueDeps = new Set();
subtask.dependencies = subtask.dependencies.filter(depId => {
// Convert to string for comparison, handling special case for subtask references
let depIdStr = String(depId);
// For numeric IDs that are likely subtask references in the same parent task
if (typeof depId === 'number' && depId < 100) {
depIdStr = `${task.id}.${depId}`;
}
if (uniqueDeps.has(depIdStr)) {
log('debug', `Removing duplicate dependency from subtask ${subtaskId}: ${depId}`);
duplicatesRemoved++;
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
// Then filter invalid dependencies
subtask.dependencies = subtask.dependencies.filter(depId => {
// Handle string dependencies with dot notation
if (typeof depId === 'string' && depId.includes('.')) {
if (!validSubtaskIds.has(depId)) {
log('debug', `Removing invalid subtask dependency from ${subtaskId}: ${depId}`);
return false;
}
if (depId === subtaskId) {
log('debug', `Removing self-dependency from ${subtaskId}`);
return false;
}
return true;
}
// Handle numeric dependencies
const numericId = typeof depId === 'number' ? depId : parseInt(depId, 10);
// Small numbers likely refer to subtasks in the same task
if (numericId < 100) {
const fullSubtaskId = `${task.id}.${numericId}`;
if (fullSubtaskId === subtaskId) {
log('debug', `Removing self-dependency from ${subtaskId}`);
return false;
}
if (!validSubtaskIds.has(fullSubtaskId)) {
log('debug', `Removing invalid subtask dependency from ${subtaskId}: ${numericId}`);
return false;
}
return true;
}
// Otherwise it's a task reference
if (!validTaskIds.has(numericId)) {
log('debug', `Removing invalid task dependency from ${subtaskId}: ${numericId}`);
return false;
}
return true;
});
if (subtask.dependencies.length < originalLength) {
changesDetected = true;
}
// Build dependency map for cycle detection
subtaskDependencyMap.set(subtaskId, subtask.dependencies.map(depId => {
if (typeof depId === 'string' && depId.includes('.')) {
return depId;
} else if (typeof depId === 'number' && depId < 100) {
return `${task.id}.${depId}`;
}
return String(depId);
}));
});
});
// Break circular dependencies in subtasks
tasksData.tasks.forEach(task => {
if (!task.subtasks || !Array.isArray(task.subtasks)) {
return;
}
task.subtasks.forEach(subtask => {
const subtaskId = `${task.id}.${subtask.id}`;
// Skip if no dependencies
if (!subtask.dependencies || !Array.isArray(subtask.dependencies) || subtask.dependencies.length === 0) {
return;
}
// Detect cycles for this subtask
const visited = new Set();
const recursionStack = new Set();
const cyclesToBreak = findCycles(subtaskId, subtaskDependencyMap, visited, recursionStack);
if (cyclesToBreak.length > 0) {
const originalLength = subtask.dependencies.length;
// Format cycle paths for removal
const edgesToRemove = cyclesToBreak.map(edge => {
if (edge.includes('.')) {
const [depTaskId, depSubtaskId] = edge.split('.').map(Number);
if (depTaskId === task.id) {
return depSubtaskId; // Return just subtask ID if in the same task
}
return edge; // Full subtask ID string
}
return Number(edge); // Task ID
});
// Remove dependencies that cause cycles
subtask.dependencies = subtask.dependencies.filter(depId => {
const normalizedDepId = typeof depId === 'number' && depId < 100
? `${task.id}.${depId}`
: String(depId);
if (edgesToRemove.includes(depId) || edgesToRemove.includes(normalizedDepId)) {
log('debug', `Breaking circular dependency: Removing ${normalizedDepId} from ${subtaskId}`);
return false;
}
return true;
});
if (subtask.dependencies.length < originalLength) {
changesDetected = true;
}
}
});
});
if (changesDetected) {
log('debug', `Cleaned up subtask dependencies (removed ${duplicatesRemoved} duplicates and fixed circular references)`);
}
return changesDetected;
}
/**
* Ensure at least one subtask in each task has no dependencies
* @param {Object} tasksData - The tasks data object with tasks array
* @returns {boolean} - True if any changes were made
*/
function ensureAtLeastOneIndependentSubtask(tasksData) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
return false;
}
let changesDetected = false;
tasksData.tasks.forEach(task => {
if (!task.subtasks || !Array.isArray(task.subtasks) || task.subtasks.length === 0) {
return;
}
// Check if any subtask has no dependencies
const hasIndependentSubtask = task.subtasks.some(st =>
!st.dependencies || !Array.isArray(st.dependencies) || st.dependencies.length === 0
);
if (!hasIndependentSubtask) {
// Find the first subtask and clear its dependencies
if (task.subtasks.length > 0) {
const firstSubtask = task.subtasks[0];
log('debug', `Ensuring at least one independent subtask: Clearing dependencies for subtask ${task.id}.${firstSubtask.id}`);
firstSubtask.dependencies = [];
changesDetected = true;
}
}
});
return changesDetected;
}
/**
* Remove duplicate dependencies from tasks and subtasks
* @param {Object} tasksData - The tasks data object with tasks array
* @returns {boolean} - True if any changes were made
*/
function removeDuplicateDependencies(tasksData) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
return false;
}
let changesDetected = false;
tasksData.tasks.forEach(task => {
// Remove duplicates from main task dependencies
if (task.dependencies && Array.isArray(task.dependencies)) {
const uniqueDeps = new Set();
const originalLength = task.dependencies.length;
task.dependencies = task.dependencies.filter(depId => {
const depIdStr = String(depId);
if (uniqueDeps.has(depIdStr)) {
log('debug', `Removing duplicate dependency from task ${task.id}: ${depId}`);
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
if (task.dependencies.length < originalLength) {
changesDetected = true;
}
}
// Remove duplicates from subtask dependencies
if (task.subtasks && Array.isArray(task.subtasks)) {
task.subtasks.forEach(subtask => {
if (subtask.dependencies && Array.isArray(subtask.dependencies)) {
const uniqueDeps = new Set();
const originalLength = subtask.dependencies.length;
subtask.dependencies = subtask.dependencies.filter(depId => {
// Convert to string for comparison, handling special case for subtask references
let depIdStr = String(depId);
// For numeric IDs that are likely subtask references in the same parent task
if (typeof depId === 'number' && depId < 100) {
depIdStr = `${task.id}.${depId}`;
}
if (uniqueDeps.has(depIdStr)) {
log('debug', `Removing duplicate dependency from subtask ${task.id}.${subtask.id}: ${depId}`);
return false;
}
uniqueDeps.add(depIdStr);
return true;
});
if (subtask.dependencies.length < originalLength) {
changesDetected = true;
}
}
});
}
});
return changesDetected;
}
/**
* Validate and fix dependencies across all tasks and subtasks
* This function is designed to be called after any task modification
* @param {Object} tasksData - The tasks data object with tasks array
* @param {string} tasksPath - Optional path to save the changes
* @returns {boolean} - True if any changes were made
*/
function validateAndFixDependencies(tasksData, tasksPath = null) {
if (!tasksData || !tasksData.tasks || !Array.isArray(tasksData.tasks)) {
log('error', 'Invalid tasks data');
return false;
}
log('debug', 'Validating and fixing dependencies...');
let changesDetected = false;
// 1. Remove duplicate dependencies from tasks and subtasks
const hasDuplicates = removeDuplicateDependencies(tasksData);
if (hasDuplicates) changesDetected = true;
// 2. Remove invalid task dependencies (non-existent tasks)
const validationChanges = validateTaskDependencies(tasksData.tasks);
if (validationChanges) changesDetected = true;
// 3. Clean up subtask dependencies
const subtaskChanges = cleanupSubtaskDependencies(tasksData);
if (subtaskChanges) changesDetected = true;
// 4. Ensure at least one subtask has no dependencies in each task
const noDepChanges = ensureAtLeastOneIndependentSubtask(tasksData);
if (noDepChanges) changesDetected = true;
// Save changes if needed
if (tasksPath && changesDetected) {
try {
writeJSON(tasksPath, tasksData);
log('debug', 'Saved dependency fixes to tasks.json');
} catch (error) {
log('error', 'Failed to save dependency fixes to tasks.json', error);
}
}
return changesDetected;
}
export {
addDependency,
removeDependency,
validateTaskDependencies,
validateDependenciesCommand,
fixDependenciesCommand,
cleanupSubtaskDependencies,
ensureAtLeastOneIndependentSubtask,
validateAndFixDependencies
}