feat: adds remove-task command + MCP implementation.
This commit is contained in:
@@ -9,6 +9,7 @@ import chalk from 'chalk';
|
||||
import boxen from 'boxen';
|
||||
import fs from 'fs';
|
||||
import https from 'https';
|
||||
import inquirer from 'inquirer';
|
||||
|
||||
import { CONFIG, log, readJSON } from './utils.js';
|
||||
import {
|
||||
@@ -25,7 +26,10 @@ import {
|
||||
removeSubtask,
|
||||
analyzeTaskComplexity,
|
||||
updateTaskById,
|
||||
updateSubtaskById
|
||||
updateSubtaskById,
|
||||
removeTask,
|
||||
findTaskById,
|
||||
taskExists
|
||||
} from './task-manager.js';
|
||||
|
||||
import {
|
||||
@@ -42,7 +46,9 @@ import {
|
||||
displayTaskById,
|
||||
displayComplexityReport,
|
||||
getStatusWithColor,
|
||||
confirmTaskOverwrite
|
||||
confirmTaskOverwrite,
|
||||
startLoadingIndicator,
|
||||
stopLoadingIndicator
|
||||
} from './ui.js';
|
||||
|
||||
/**
|
||||
@@ -863,7 +869,120 @@ function registerCommands(programInstance) {
|
||||
console.log(chalk.white(' task-master init -y'));
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
|
||||
// remove-task command
|
||||
programInstance
|
||||
.command('remove-task')
|
||||
.description('Remove a task or subtask permanently')
|
||||
.option('-i, --id <id>', 'ID of the task or subtask to remove (e.g., "5" or "5.2")')
|
||||
.option('-f, --file <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;
|
||||
|
||||
if (!taskId) {
|
||||
console.error(chalk.red('Error: Task ID is required'));
|
||||
console.error(chalk.yellow('Usage: task-master remove-task --id=<taskId>'));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
try {
|
||||
// Check if the task exists
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
console.error(chalk.red(`Error: No valid tasks found in ${tasksPath}`));
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
console.error(chalk.red(`Error: Task with ID ${taskId} not found`));
|
||||
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
|
||||
console.log();
|
||||
console.log(chalk.red.bold('⚠️ WARNING: This will permanently delete the following task:'));
|
||||
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}`));
|
||||
|
||||
// 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!`));
|
||||
}
|
||||
|
||||
// Show if other tasks depend on it
|
||||
const dependentTasks = data.tasks.filter(t =>
|
||||
t.dependencies && t.dependencies.includes(parseInt(taskId, 10)));
|
||||
|
||||
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();
|
||||
|
||||
// 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?'),
|
||||
default: false
|
||||
}
|
||||
]);
|
||||
|
||||
if (!confirm) {
|
||||
console.log(chalk.blue('Task deletion cancelled.'));
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
const indicator = startLoadingIndicator('Removing task...');
|
||||
|
||||
// Remove the task
|
||||
const result = await removeTask(tasksPath, taskId);
|
||||
|
||||
stopLoadingIndicator(indicator);
|
||||
|
||||
// Display success message with appropriate color based on task or subtask
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
// It was a subtask
|
||||
console.log(boxen(
|
||||
chalk.green(`Subtask ${taskId} has been successfully removed`),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
));
|
||||
} else {
|
||||
// It was a main task
|
||||
console.log(boxen(
|
||||
chalk.green(`Task ${taskId} has been successfully removed`),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
));
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(chalk.red(`Error: ${error.message || 'An unknown error occurred'}`));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Add more commands as needed...
|
||||
|
||||
return programInstance;
|
||||
|
||||
@@ -3464,6 +3464,156 @@ Provide concrete examples, code snippets, or implementation details when relevan
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a task or subtask from the tasks file
|
||||
* @param {string} tasksPath - Path to the tasks file
|
||||
* @param {string|number} taskId - ID of task or subtask to remove (e.g., '5' or '5.2')
|
||||
* @returns {Object} Result object with success message and removed task info
|
||||
*/
|
||||
async function removeTask(tasksPath, taskId) {
|
||||
try {
|
||||
// Read the tasks file
|
||||
const data = readJSON(tasksPath);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Check if the task ID exists
|
||||
if (!taskExists(data.tasks, taskId)) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
// Handle subtask removal (e.g., '5.2')
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
const [parentTaskId, subtaskId] = taskId.split('.').map(id => parseInt(id, 10));
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = data.tasks.find(t => t.id === parentTaskId);
|
||||
if (!parentTask || !parentTask.subtasks) {
|
||||
throw new Error(`Parent task with ID ${parentTaskId} or its subtasks not found`);
|
||||
}
|
||||
|
||||
// Find the subtask to remove
|
||||
const subtaskIndex = parentTask.subtasks.findIndex(st => st.id === subtaskId);
|
||||
if (subtaskIndex === -1) {
|
||||
throw new Error(`Subtask with ID ${subtaskId} not found in parent task ${parentTaskId}`);
|
||||
}
|
||||
|
||||
// Store the subtask info before removal for the result
|
||||
const removedSubtask = parentTask.subtasks[subtaskIndex];
|
||||
|
||||
// Remove the subtask
|
||||
parentTask.subtasks.splice(subtaskIndex, 1);
|
||||
|
||||
// Remove references to this subtask in other subtasks' dependencies
|
||||
if (parentTask.subtasks && parentTask.subtasks.length > 0) {
|
||||
parentTask.subtasks.forEach(subtask => {
|
||||
if (subtask.dependencies && subtask.dependencies.includes(subtaskId)) {
|
||||
subtask.dependencies = subtask.dependencies.filter(depId => depId !== subtaskId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Save the updated tasks
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Generate updated task files
|
||||
try {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
} catch (genError) {
|
||||
log('warn', `Successfully removed subtask but failed to regenerate task files: ${genError.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully removed subtask ${subtaskId} from task ${parentTaskId}`,
|
||||
removedTask: removedSubtask,
|
||||
parentTaskId: parentTaskId
|
||||
};
|
||||
}
|
||||
|
||||
// Handle main task removal
|
||||
const taskIdNum = parseInt(taskId, 10);
|
||||
const taskIndex = data.tasks.findIndex(t => t.id === taskIdNum);
|
||||
if (taskIndex === -1) {
|
||||
throw new Error(`Task with ID ${taskId} not found`);
|
||||
}
|
||||
|
||||
// Store the task info before removal for the result
|
||||
const removedTask = data.tasks[taskIndex];
|
||||
|
||||
// Remove the task
|
||||
data.tasks.splice(taskIndex, 1);
|
||||
|
||||
// Remove references to this task in other tasks' dependencies
|
||||
data.tasks.forEach(task => {
|
||||
if (task.dependencies && task.dependencies.includes(taskIdNum)) {
|
||||
task.dependencies = task.dependencies.filter(depId => depId !== taskIdNum);
|
||||
}
|
||||
});
|
||||
|
||||
// Save the updated tasks
|
||||
writeJSON(tasksPath, data);
|
||||
|
||||
// Delete the task file if it exists
|
||||
const taskFileName = path.join(path.dirname(tasksPath), `task_${taskIdNum.toString().padStart(3, '0')}.txt`);
|
||||
if (fs.existsSync(taskFileName)) {
|
||||
try {
|
||||
fs.unlinkSync(taskFileName);
|
||||
} catch (unlinkError) {
|
||||
log('warn', `Successfully removed task from tasks.json but failed to delete task file: ${unlinkError.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate updated task files
|
||||
try {
|
||||
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
||||
} catch (genError) {
|
||||
log('warn', `Successfully removed task but failed to regenerate task files: ${genError.message}`);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: `Successfully removed task ${taskId}`,
|
||||
removedTask: removedTask
|
||||
};
|
||||
} catch (error) {
|
||||
log('error', `Error removing task: ${error.message}`);
|
||||
throw {
|
||||
code: 'REMOVE_TASK_ERROR',
|
||||
message: error.message,
|
||||
details: error.stack
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a task with the given ID exists
|
||||
* @param {Array} tasks - Array of tasks to search
|
||||
* @param {string|number} taskId - ID of task or subtask to check
|
||||
* @returns {boolean} Whether the task exists
|
||||
*/
|
||||
function taskExists(tasks, taskId) {
|
||||
// Handle subtask IDs (e.g., "1.2")
|
||||
if (typeof taskId === 'string' && taskId.includes('.')) {
|
||||
const [parentIdStr, subtaskIdStr] = taskId.split('.');
|
||||
const parentId = parseInt(parentIdStr, 10);
|
||||
const subtaskId = parseInt(subtaskIdStr, 10);
|
||||
|
||||
// Find the parent task
|
||||
const parentTask = tasks.find(t => t.id === parentId);
|
||||
|
||||
// If parent exists, check if subtask exists
|
||||
return parentTask &&
|
||||
parentTask.subtasks &&
|
||||
parentTask.subtasks.some(st => st.id === subtaskId);
|
||||
}
|
||||
|
||||
// Handle regular task IDs
|
||||
const id = parseInt(taskId, 10);
|
||||
return tasks.some(t => t.id === id);
|
||||
}
|
||||
|
||||
// Export task manager functions
|
||||
export {
|
||||
parsePRD,
|
||||
@@ -3482,4 +3632,7 @@ export {
|
||||
removeSubtask,
|
||||
findNextTask,
|
||||
analyzeTaskComplexity,
|
||||
removeTask,
|
||||
findTaskById,
|
||||
taskExists,
|
||||
};
|
||||
Reference in New Issue
Block a user