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/scripts/modules/commands.js b/scripts/modules/commands.js index 9e42e42f..d5b46bb4 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -129,26 +129,26 @@ function registerCommands(programInstance) { console.log( boxen( chalk.white.bold('Parse PRD Help') + - '\n\n' + - chalk.cyan('Usage:') + - '\n' + - ` task-master parse-prd [options]\n\n` + - chalk.cyan('Options:') + - '\n' + - ' -i, --input Path to the PRD file (alternative to positional argument)\n' + - ' -o, --output Output file path (default: "tasks/tasks.json")\n' + - ' -n, --num-tasks Number of tasks to generate (default: 10)\n' + - ' -f, --force Skip confirmation when overwriting existing tasks\n\n' + - chalk.cyan('Example:') + - '\n' + - ' task-master parse-prd requirements.txt --num-tasks 15\n' + - ' task-master parse-prd --input=requirements.txt\n' + - ' task-master parse-prd --force\n\n' + - chalk.yellow('Note: This command will:') + - '\n' + - ' 1. Look for a PRD file at scripts/prd.txt by default\n' + - ' 2. Use the file specified by --input or positional argument if provided\n' + - ' 3. Generate tasks from the PRD and overwrite any existing tasks.json file', + '\n\n' + + chalk.cyan('Usage:') + + '\n' + + ` task-master parse-prd [options]\n\n` + + chalk.cyan('Options:') + + '\n' + + ' -i, --input Path to the PRD file (alternative to positional argument)\n' + + ' -o, --output Output file path (default: "tasks/tasks.json")\n' + + ' -n, --num-tasks Number of tasks to generate (default: 10)\n' + + ' -f, --force Skip confirmation when overwriting existing tasks\n\n' + + chalk.cyan('Example:') + + '\n' + + ' task-master parse-prd requirements.txt --num-tasks 15\n' + + ' task-master parse-prd --input=requirements.txt\n' + + ' task-master parse-prd --force\n\n' + + chalk.yellow('Note: This command will:') + + '\n' + + ' 1. Look for a PRD file at scripts/prd.txt by default\n' + + ' 2. Use the file specified by --input or positional argument if provided\n' + + ' 3. Generate tasks from the PRD and overwrite any existing tasks.json file', { padding: 1, borderColor: 'blue', borderStyle: 'round' } ) ); @@ -1132,25 +1132,25 @@ function registerCommands(programInstance) { chalk.white.bold( `Subtask ${parentId}.${subtask.id} Added Successfully` ) + - '\n\n' + - chalk.white(`Title: ${subtask.title}`) + - '\n' + - chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) + - '\n' + - (dependencies.length > 0 - ? chalk.white(`Dependencies: ${dependencies.join(', ')}`) + - '\n' - : '') + - '\n' + - chalk.white.bold('Next Steps:') + - '\n' + - chalk.cyan( - `1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks` - ) + - '\n' + - chalk.cyan( - `2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it` - ), + '\n\n' + + chalk.white(`Title: ${subtask.title}`) + + '\n' + + chalk.white(`Status: ${getStatusWithColor(subtask.status)}`) + + '\n' + + (dependencies.length > 0 + ? chalk.white(`Dependencies: ${dependencies.join(', ')}`) + + '\n' + : '') + + '\n' + + chalk.white.bold('Next Steps:') + + '\n' + + chalk.cyan( + `1. Run ${chalk.yellow(`task-master show ${parentId}`)} to see the parent task with all subtasks` + ) + + '\n' + + chalk.cyan( + `2. Run ${chalk.yellow(`task-master set-status --id=${parentId}.${subtask.id} --status=in-progress`)} to start working on it` + ), { padding: 1, borderColor: 'green', @@ -1166,19 +1166,19 @@ function registerCommands(programInstance) { console.log( boxen( chalk.white.bold('Usage Examples:') + - '\n\n' + - chalk.white('Convert existing task to subtask:') + - '\n' + - chalk.yellow( - ` task-master add-subtask --parent=5 --task-id=8` - ) + - '\n\n' + - chalk.white('Create new subtask:') + - '\n' + - chalk.yellow( - ` task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"` - ) + - '\n\n', + '\n\n' + + chalk.white('Convert existing task to subtask:') + + '\n' + + chalk.yellow( + ` task-master add-subtask --parent=5 --task-id=8` + ) + + '\n\n' + + chalk.white('Create new subtask:') + + '\n' + + chalk.yellow( + ` task-master add-subtask --parent=5 --title="Implement login UI" --description="Create the login form"` + ) + + '\n\n', { padding: 1, borderColor: 'blue', borderStyle: 'round' } ) ); @@ -1200,25 +1200,25 @@ function registerCommands(programInstance) { console.log( boxen( chalk.white.bold('Add Subtask Command Help') + - '\n\n' + - chalk.cyan('Usage:') + - '\n' + - ` task-master add-subtask --parent= [options]\n\n` + - chalk.cyan('Options:') + - '\n' + - ' -p, --parent Parent task ID (required)\n' + - ' -i, --task-id Existing task ID to convert to subtask\n' + - ' -t, --title Title for the new subtask\n' + - ' -d, --description <text> Description for the new subtask\n' + - ' --details <text> Implementation details for the new subtask\n' + - ' --dependencies <ids> Comma-separated list of dependency IDs\n' + - ' -s, --status <status> Status for the new subtask (default: "pending")\n' + - ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + - ' --skip-generate Skip regenerating task files\n\n' + - chalk.cyan('Examples:') + - '\n' + - ' task-master add-subtask --parent=5 --task-id=8\n' + - ' task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form"', + '\n\n' + + chalk.cyan('Usage:') + + '\n' + + ` task-master add-subtask --parent=<id> [options]\n\n` + + chalk.cyan('Options:') + + '\n' + + ' -p, --parent <id> Parent task ID (required)\n' + + ' -i, --task-id <id> Existing task ID to convert to subtask\n' + + ' -t, --title <title> Title for the new subtask\n' + + ' -d, --description <text> Description for the new subtask\n' + + ' --details <text> Implementation details for the new subtask\n' + + ' --dependencies <ids> Comma-separated list of dependency IDs\n' + + ' -s, --status <status> Status for the new subtask (default: "pending")\n' + + ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + + ' --skip-generate Skip regenerating task files\n\n' + + chalk.cyan('Examples:') + + '\n' + + ' task-master add-subtask --parent=5 --task-id=8\n' + + ' task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form"', { padding: 1, borderColor: 'blue', borderStyle: 'round' } ) ); @@ -1291,24 +1291,24 @@ function registerCommands(programInstance) { chalk.white.bold( `Subtask ${subtaskId} Converted to Task #${result.id}` ) + - '\n\n' + - chalk.white(`Title: ${result.title}`) + - '\n' + - chalk.white(`Status: ${getStatusWithColor(result.status)}`) + - '\n' + - chalk.white( - `Dependencies: ${result.dependencies.join(', ')}` - ) + - '\n\n' + - chalk.white.bold('Next Steps:') + - '\n' + - chalk.cyan( - `1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task` - ) + - '\n' + - chalk.cyan( - `2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it` - ), + '\n\n' + + chalk.white(`Title: ${result.title}`) + + '\n' + + chalk.white(`Status: ${getStatusWithColor(result.status)}`) + + '\n' + + chalk.white( + `Dependencies: ${result.dependencies.join(', ')}` + ) + + '\n\n' + + chalk.white.bold('Next Steps:') + + '\n' + + chalk.cyan( + `1. Run ${chalk.yellow(`task-master show ${result.id}`)} to see details of the new task` + ) + + '\n' + + chalk.cyan( + `2. Run ${chalk.yellow(`task-master set-status --id=${result.id} --status=in-progress`)} to start working on it` + ), { padding: 1, borderColor: 'green', @@ -1322,8 +1322,8 @@ function registerCommands(programInstance) { console.log( boxen( chalk.white.bold(`Subtask ${subtaskId} Removed`) + - '\n\n' + - chalk.white('The subtask has been successfully deleted.'), + '\n\n' + + chalk.white('The subtask has been successfully deleted.'), { padding: 1, borderColor: 'green', @@ -1351,21 +1351,21 @@ function registerCommands(programInstance) { console.log( boxen( chalk.white.bold('Remove Subtask Command Help') + - '\n\n' + - chalk.cyan('Usage:') + - '\n' + - ` task-master remove-subtask --id=<parentId.subtaskId> [options]\n\n` + - chalk.cyan('Options:') + - '\n' + - ' -i, --id <id> Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' + - ' -c, --convert Convert the subtask to a standalone task instead of deleting it\n' + - ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + - ' --skip-generate Skip regenerating task files\n\n' + - chalk.cyan('Examples:') + - '\n' + - ' task-master remove-subtask --id=5.2\n' + - ' task-master remove-subtask --id=5.2,6.3,7.1\n' + - ' task-master remove-subtask --id=5.2 --convert', + '\n\n' + + chalk.cyan('Usage:') + + '\n' + + ` task-master remove-subtask --id=<parentId.subtaskId> [options]\n\n` + + chalk.cyan('Options:') + + '\n' + + ' -i, --id <id> Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' + + ' -c, --convert Convert the subtask to a standalone task instead of deleting it\n' + + ' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' + + ' --skip-generate Skip regenerating task files\n\n' + + chalk.cyan('Examples:') + + '\n' + + ' task-master remove-subtask --id=5.2\n' + + ' task-master remove-subtask --id=5.2,6.3,7.1\n' + + ' task-master remove-subtask --id=5.2 --convert', { padding: 1, borderColor: 'blue', borderStyle: 'round' } ) ); @@ -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>', - '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 <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=<taskId>') @@ -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,80 @@ 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 +1488,68 @@ 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( @@ -1719,7 +1761,7 @@ function compareVersions(v1, v2) { function displayUpgradeNotification(currentVersion, latestVersion) { const message = boxen( `${chalk.blue.bold('Update Available!')} ${chalk.dim(currentVersion)} → ${chalk.green(latestVersion)}\n\n` + - `Run ${chalk.cyan('npm i task-master-ai@latest -g')} to update to the latest version with new features and bug fixes.`, + `Run ${chalk.cyan('npm i task-master-ai@latest -g')} to update to the latest version with new features and bug fixes.`, { padding: 1, margin: { top: 1, bottom: 1 },