diff --git a/.changeset/chilly-chicken-leave.md b/.changeset/chilly-chicken-leave.md new file mode 100644 index 00000000..38f3b460 --- /dev/null +++ b/.changeset/chilly-chicken-leave.md @@ -0,0 +1,5 @@ +--- +'task-master-ai': patch +--- + +Fix remove-task command to handle multiple comma-separated task IDs diff --git a/mcp-server/src/core/direct-functions/remove-task.js b/mcp-server/src/core/direct-functions/remove-task.js index e6d429b9..2fb17099 100644 --- a/mcp-server/src/core/direct-functions/remove-task.js +++ b/mcp-server/src/core/direct-functions/remove-task.js @@ -3,18 +3,23 @@ * Direct function implementation for removing a task */ -import { removeTask } from '../../../../scripts/modules/task-manager.js'; +import { + removeTask, + taskExists +} from '../../../../scripts/modules/task-manager.js'; import { enableSilentMode, - disableSilentMode + disableSilentMode, + readJSON } from '../../../../scripts/modules/utils.js'; /** * Direct function wrapper for removeTask with error handling. + * Supports removing multiple tasks at once with comma-separated IDs. * * @param {Object} args - Command arguments * @param {string} args.tasksJsonPath - Explicit path to the tasks.json file. - * @param {string} args.id - The ID of the task or subtask to remove. + * @param {string} args.id - The ID(s) of the task(s) or subtask(s) to remove (comma-separated for multiple). * @param {Object} log - Logger object * @returns {Promise} - Remove task result { success: boolean, data?: any, error?: { code: string, message: string }, fromCache: false } */ @@ -36,8 +41,7 @@ export async function removeTaskDirect(args, log) { } // Validate task ID parameter - const taskId = id; - if (!taskId) { + if (!id) { log.error('Task ID is required'); return { success: false, @@ -49,46 +53,103 @@ export async function removeTaskDirect(args, log) { }; } - // Skip confirmation in the direct function since it's handled by the client - log.info(`Removing task with ID: ${taskId} from ${tasksJsonPath}`); + // Split task IDs if comma-separated + const taskIdArray = id.split(',').map((taskId) => taskId.trim()); - try { - // Enable silent mode to prevent console logs from interfering with JSON response - enableSilentMode(); + log.info( + `Removing ${taskIdArray.length} task(s) with ID(s): ${taskIdArray.join(', ')} from ${tasksJsonPath}` + ); - // Call the core removeTask function using the provided path - const result = await removeTask(tasksJsonPath, taskId); - - // Restore normal logging - disableSilentMode(); - - log.info(`Successfully removed task: ${taskId}`); - - // Return the result - return { - success: true, - data: { - message: result.message, - taskId: taskId, - tasksPath: tasksJsonPath, - removedTask: result.removedTask - }, - fromCache: false - }; - } catch (error) { - // Make sure to restore normal logging even if there's an error - disableSilentMode(); - - log.error(`Error removing task: ${error.message}`); + // Validate all task IDs exist before proceeding + const data = readJSON(tasksJsonPath); + if (!data || !data.tasks) { return { success: false, error: { - code: error.code || 'REMOVE_TASK_ERROR', - message: error.message || 'Failed to remove task' + code: 'INVALID_TASKS_FILE', + message: `No valid tasks found in ${tasksJsonPath}` }, fromCache: false }; } + + const invalidTasks = taskIdArray.filter( + (taskId) => !taskExists(data.tasks, taskId) + ); + + if (invalidTasks.length > 0) { + return { + success: false, + error: { + code: 'INVALID_TASK_ID', + message: `The following tasks were not found: ${invalidTasks.join(', ')}` + }, + fromCache: false + }; + } + + // Remove tasks one by one + const results = []; + + // Enable silent mode to prevent console logs from interfering with JSON response + enableSilentMode(); + + try { + for (const taskId of taskIdArray) { + try { + const result = await removeTask(tasksJsonPath, taskId); + results.push({ + taskId, + success: true, + message: result.message, + removedTask: result.removedTask + }); + log.info(`Successfully removed task: ${taskId}`); + } catch (error) { + results.push({ + taskId, + success: false, + error: error.message + }); + log.error(`Error removing task ${taskId}: ${error.message}`); + } + } + } finally { + // Restore normal logging + disableSilentMode(); + } + + // Check if all tasks were successfully removed + const successfulRemovals = results.filter((r) => r.success); + const failedRemovals = results.filter((r) => !r.success); + + if (successfulRemovals.length === 0) { + // All removals failed + return { + success: false, + error: { + code: 'REMOVE_TASK_ERROR', + message: 'Failed to remove any tasks', + details: failedRemovals + .map((r) => `${r.taskId}: ${r.error}`) + .join('; ') + }, + fromCache: false + }; + } + + // At least some tasks were removed successfully + return { + success: true, + data: { + totalTasks: taskIdArray.length, + successful: successfulRemovals.length, + failed: failedRemovals.length, + results: results, + tasksPath: tasksJsonPath + }, + fromCache: false + }; } catch (error) { // Ensure silent mode is disabled even if an outer error occurs disableSilentMode(); diff --git a/mcp-server/src/tools/remove-task.js b/mcp-server/src/tools/remove-task.js index c0f9d6f7..8898c041 100644 --- a/mcp-server/src/tools/remove-task.js +++ b/mcp-server/src/tools/remove-task.js @@ -23,7 +23,9 @@ export function registerRemoveTaskTool(server) { parameters: z.object({ id: z .string() - .describe("ID of the task or subtask to remove (e.g., '5' or '5.2')"), + .describe( + "ID(s) of the task(s) or subtask(s) to remove (e.g., '5' or '5.2' or '5,6,7')" + ), file: z.string().optional().describe('Absolute path to the tasks file'), projectRoot: z .string() @@ -35,7 +37,7 @@ export function registerRemoveTaskTool(server) { }), execute: async (args, { log, session }) => { try { - log.info(`Removing task with ID: ${args.id}`); + log.info(`Removing task(s) with ID(s): ${args.id}`); // Get project root from args or session const rootFolder = diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 9e42e42f..7a5494f5 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -1374,18 +1374,18 @@ function registerCommands(programInstance) { // remove-task command programInstance .command('remove-task') - .description('Remove a task or subtask permanently') + .description('Remove one or more tasks or subtasks permanently') .option( '-i, --id ', - 'ID of the task or subtask to remove (e.g., "5" or "5.2")' + 'ID(s) of the task(s) or subtask(s) to remove (e.g., "5" or "5.2" or "5,6,7")' ) .option('-f, --file ', 'Path to the tasks file', 'tasks/tasks.json') .option('-y, --yes', 'Skip confirmation prompt', false) .action(async (options) => { const tasksPath = options.file; - const taskId = options.id; + const taskIds = options.id; - if (!taskId) { + if (!taskIds) { console.error(chalk.red('Error: Task ID is required')); console.error( chalk.yellow('Usage: task-master remove-task --id=') @@ -1394,7 +1394,7 @@ function registerCommands(programInstance) { } try { - // Check if the task exists + // Check if the tasks file exists and is valid const data = readJSON(tasksPath); if (!data || !data.tasks) { console.error( @@ -1403,75 +1403,89 @@ function registerCommands(programInstance) { process.exit(1); } - if (!taskExists(data.tasks, taskId)) { - console.error(chalk.red(`Error: Task with ID ${taskId} not found`)); + // Split task IDs if comma-separated + const taskIdArray = taskIds.split(',').map((id) => id.trim()); + + // Validate all task IDs exist before proceeding + const invalidTasks = taskIdArray.filter( + (id) => !taskExists(data.tasks, id) + ); + if (invalidTasks.length > 0) { + console.error( + chalk.red( + `Error: The following tasks were not found: ${invalidTasks.join(', ')}` + ) + ); process.exit(1); } - // Load task for display - const task = findTaskById(data.tasks, taskId); - // Skip confirmation if --yes flag is provided if (!options.yes) { - // Display task information + // Display tasks to be removed console.log(); console.log( chalk.red.bold( - '⚠️ WARNING: This will permanently delete the following task:' + '⚠️ WARNING: This will permanently delete the following tasks:' ) ); console.log(); - if (typeof taskId === 'string' && taskId.includes('.')) { - // It's a subtask - const [parentId, subtaskId] = taskId.split('.'); - console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`)); - console.log( - chalk.gray( - `Parent Task: ${task.parentTask.id} - ${task.parentTask.title}` - ) - ); - } else { - // It's a main task - console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`)); + for (const taskId of taskIdArray) { + const task = findTaskById(data.tasks, taskId); - // Show if it has subtasks - if (task.subtasks && task.subtasks.length > 0) { + if (typeof taskId === 'string' && taskId.includes('.')) { + // It's a subtask + const [parentId, subtaskId] = taskId.split('.'); + console.log(chalk.white.bold(`Subtask ${taskId}: ${task.title}`)); console.log( - chalk.yellow( - `⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!` + chalk.gray( + `Parent Task: ${task.parentTask.id} - ${task.parentTask.title}` ) ); - } + } else { + // It's a main task + console.log(chalk.white.bold(`Task ${taskId}: ${task.title}`)); - // Show if other tasks depend on it - const dependentTasks = data.tasks.filter( - (t) => - t.dependencies && t.dependencies.includes(parseInt(taskId, 10)) - ); + // Show if it has subtasks + if (task.subtasks && task.subtasks.length > 0) { + console.log( + chalk.yellow( + `⚠️ This task has ${task.subtasks.length} subtasks that will also be deleted!` + ) + ); + } - if (dependentTasks.length > 0) { - console.log( - chalk.yellow( - `⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!` - ) + // Show if other tasks depend on it + const dependentTasks = data.tasks.filter( + (t) => + t.dependencies && + t.dependencies.includes(parseInt(taskId, 10)) ); - console.log(chalk.yellow('These dependencies will be removed:')); - dependentTasks.forEach((t) => { - console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`)); - }); + + if (dependentTasks.length > 0) { + console.log( + chalk.yellow( + `⚠️ Warning: ${dependentTasks.length} other tasks depend on this task!` + ) + ); + console.log( + chalk.yellow('These dependencies will be removed:') + ); + dependentTasks.forEach((t) => { + console.log(chalk.yellow(` - Task ${t.id}: ${t.title}`)); + }); + } } + console.log(); } - console.log(); - // Prompt for confirmation const { confirm } = await inquirer.prompt([ { type: 'confirm', name: 'confirm', message: chalk.red.bold( - 'Are you sure you want to permanently delete this task?' + `Are you sure you want to permanently delete ${taskIdArray.length > 1 ? 'these tasks' : 'this task'}?` ), default: false } @@ -1483,31 +1497,72 @@ function registerCommands(programInstance) { } } - const indicator = startLoadingIndicator('Removing task...'); + const indicator = startLoadingIndicator('Removing tasks...'); - // Remove the task - const result = await removeTask(tasksPath, taskId); + // Remove each task + const results = []; + for (const taskId of taskIdArray) { + try { + const result = await removeTask(tasksPath, taskId); + results.push({ taskId, success: true, ...result }); + } catch (error) { + results.push({ taskId, success: false, error: error.message }); + } + } stopLoadingIndicator(indicator); - // Display success message with appropriate color based on task or subtask - if (typeof taskId === 'string' && taskId.includes('.')) { - // It was a subtask + // Display results + const successfulRemovals = results.filter((r) => r.success); + const failedRemovals = results.filter((r) => !r.success); + + if (successfulRemovals.length > 0) { console.log( boxen( - chalk.green(`Subtask ${taskId} has been successfully removed`), - { padding: 1, borderColor: 'green', borderStyle: 'round' } + chalk.green( + `Successfully removed ${successfulRemovals.length} task${successfulRemovals.length > 1 ? 's' : ''}` + ) + + '\n\n' + + successfulRemovals + .map((r) => + chalk.white( + `✓ ${r.taskId.includes('.') ? 'Subtask' : 'Task'} ${r.taskId}` + ) + ) + .join('\n'), + { + padding: 1, + borderColor: 'green', + borderStyle: 'round', + margin: { top: 1 } + } ) ); - } else { - // It was a main task + } + + if (failedRemovals.length > 0) { console.log( - boxen(chalk.green(`Task ${taskId} has been successfully removed`), { - padding: 1, - borderColor: 'green', - borderStyle: 'round' - }) + boxen( + chalk.red( + `Failed to remove ${failedRemovals.length} task${failedRemovals.length > 1 ? 's' : ''}` + ) + + '\n\n' + + failedRemovals + .map((r) => chalk.white(`✗ ${r.taskId}: ${r.error}`)) + .join('\n'), + { + padding: 1, + borderColor: 'red', + borderStyle: 'round', + margin: { top: 1 } + } + ) ); + + // Exit with error if any removals failed + if (successfulRemovals.length === 0) { + process.exit(1); + } } } catch (error) { console.error(