2025-04-21 17:48:30 -04:00
|
|
|
import fs from 'fs';
|
|
|
|
import path from 'path';
|
|
|
|
|
|
|
|
import { log, readJSON, writeJSON } from '../utils.js';
|
|
|
|
import generateTaskFiles from './generate-task-files.js';
|
|
|
|
import taskExists from './task-exists.js';
|
|
|
|
|
|
|
|
/**
|
2025-04-28 00:41:32 -04:00
|
|
|
* Removes one or more tasks or subtasks from the tasks file
|
2025-04-21 17:48:30 -04:00
|
|
|
* @param {string} tasksPath - Path to the tasks file
|
2025-04-28 00:41:32 -04:00
|
|
|
* @param {string} taskIds - Comma-separated string of task/subtask IDs to remove (e.g., '5,6.1,7')
|
|
|
|
* @returns {Object} Result object with success status, messages, and removed task info
|
2025-04-21 17:48:30 -04:00
|
|
|
*/
|
2025-04-28 00:41:32 -04:00
|
|
|
async function removeTask(tasksPath, taskIds) {
|
|
|
|
const results = {
|
|
|
|
success: true,
|
|
|
|
messages: [],
|
|
|
|
errors: [],
|
|
|
|
removedTasks: []
|
|
|
|
};
|
|
|
|
const taskIdsToRemove = taskIds
|
|
|
|
.split(',')
|
|
|
|
.map((id) => id.trim())
|
|
|
|
.filter(Boolean); // Remove empty strings if any
|
|
|
|
|
|
|
|
if (taskIdsToRemove.length === 0) {
|
|
|
|
results.success = false;
|
|
|
|
results.errors.push('No valid task IDs provided.');
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
|
2025-04-21 17:48:30 -04:00
|
|
|
try {
|
2025-04-28 00:41:32 -04:00
|
|
|
// Read the tasks file ONCE before the loop
|
2025-04-21 17:48:30 -04:00
|
|
|
const data = readJSON(tasksPath);
|
|
|
|
if (!data || !data.tasks) {
|
|
|
|
throw new Error(`No valid tasks found in ${tasksPath}`);
|
|
|
|
}
|
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
const tasksToDeleteFiles = []; // Collect IDs of main tasks whose files should be deleted
|
2025-04-21 17:48:30 -04:00
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
for (const taskId of taskIdsToRemove) {
|
|
|
|
// Check if the task ID exists *before* attempting removal
|
|
|
|
if (!taskExists(data.tasks, taskId)) {
|
|
|
|
const errorMsg = `Task with ID ${taskId} not found or already removed.`;
|
|
|
|
results.errors.push(errorMsg);
|
|
|
|
results.success = false; // Mark overall success as false if any error occurs
|
|
|
|
continue; // Skip to the next ID
|
2025-04-21 17:48:30 -04:00
|
|
|
}
|
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
try {
|
|
|
|
// 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 ${parentTaskId} or its subtasks not found for subtask ${taskId}`
|
|
|
|
);
|
|
|
|
}
|
2025-04-21 17:48:30 -04:00
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// Find the subtask to remove
|
|
|
|
const subtaskIndex = parentTask.subtasks.findIndex(
|
|
|
|
(st) => st.id === subtaskId
|
|
|
|
);
|
|
|
|
if (subtaskIndex === -1) {
|
|
|
|
throw new Error(
|
|
|
|
`Subtask ${subtaskId} not found in parent task ${parentTaskId}`
|
2025-04-21 17:48:30 -04:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// Store the subtask info before removal
|
|
|
|
const removedSubtask = {
|
|
|
|
...parentTask.subtasks[subtaskIndex],
|
|
|
|
parentTaskId: parentTaskId
|
|
|
|
};
|
|
|
|
results.removedTasks.push(removedSubtask);
|
|
|
|
|
|
|
|
// Remove the subtask from the parent
|
|
|
|
parentTask.subtasks.splice(subtaskIndex, 1);
|
|
|
|
|
|
|
|
results.messages.push(`Successfully removed subtask ${taskId}`);
|
|
|
|
}
|
|
|
|
// Handle main task removal
|
|
|
|
else {
|
|
|
|
const taskIdNum = parseInt(taskId, 10);
|
|
|
|
const taskIndex = data.tasks.findIndex((t) => t.id === taskIdNum);
|
|
|
|
if (taskIndex === -1) {
|
|
|
|
// This case should theoretically be caught by the taskExists check above,
|
|
|
|
// but keep it as a safeguard.
|
|
|
|
throw new Error(`Task with ID ${taskId} not found`);
|
|
|
|
}
|
2025-04-21 17:48:30 -04:00
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// Store the task info before removal
|
|
|
|
const removedTask = data.tasks[taskIndex];
|
|
|
|
results.removedTasks.push(removedTask);
|
|
|
|
tasksToDeleteFiles.push(taskIdNum); // Add to list for file deletion
|
|
|
|
|
|
|
|
// Remove the task from the main array
|
|
|
|
data.tasks.splice(taskIndex, 1);
|
|
|
|
|
|
|
|
results.messages.push(`Successfully removed task ${taskId}`);
|
|
|
|
}
|
|
|
|
} catch (innerError) {
|
|
|
|
// Catch errors specific to processing *this* ID
|
|
|
|
const errorMsg = `Error processing ID ${taskId}: ${innerError.message}`;
|
|
|
|
results.errors.push(errorMsg);
|
|
|
|
results.success = false;
|
|
|
|
log('warn', errorMsg); // Log as warning and continue with next ID
|
2025-04-21 17:48:30 -04:00
|
|
|
}
|
2025-04-28 00:41:32 -04:00
|
|
|
} // End of loop through taskIdsToRemove
|
2025-04-21 17:48:30 -04:00
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// --- Post-Loop Operations ---
|
2025-04-21 17:48:30 -04:00
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// Only proceed with cleanup and saving if at least one task was potentially removed
|
|
|
|
if (results.removedTasks.length > 0) {
|
|
|
|
// Remove all references AFTER all tasks/subtasks are removed
|
|
|
|
const allRemovedIds = new Set(
|
|
|
|
taskIdsToRemove.map((id) =>
|
|
|
|
typeof id === 'string' && id.includes('.') ? id : parseInt(id, 10)
|
|
|
|
)
|
|
|
|
);
|
2025-04-21 17:48:30 -04:00
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
data.tasks.forEach((task) => {
|
|
|
|
// Clean dependencies in main tasks
|
|
|
|
if (task.dependencies) {
|
|
|
|
task.dependencies = task.dependencies.filter(
|
|
|
|
(depId) => !allRemovedIds.has(depId)
|
|
|
|
);
|
|
|
|
}
|
|
|
|
// Clean dependencies in remaining subtasks
|
|
|
|
if (task.subtasks) {
|
|
|
|
task.subtasks.forEach((subtask) => {
|
|
|
|
if (subtask.dependencies) {
|
|
|
|
subtask.dependencies = subtask.dependencies.filter(
|
|
|
|
(depId) =>
|
|
|
|
!allRemovedIds.has(`${task.id}.${depId}`) &&
|
|
|
|
!allRemovedIds.has(depId) // check both subtask and main task refs
|
|
|
|
);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Save the updated tasks file ONCE
|
|
|
|
writeJSON(tasksPath, data);
|
2025-04-21 17:48:30 -04:00
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// Delete task files AFTER saving tasks.json
|
|
|
|
for (const taskIdNum of tasksToDeleteFiles) {
|
|
|
|
const taskFileName = path.join(
|
|
|
|
path.dirname(tasksPath),
|
|
|
|
`task_${taskIdNum.toString().padStart(3, '0')}.txt`
|
2025-04-21 17:48:30 -04:00
|
|
|
);
|
2025-04-28 00:41:32 -04:00
|
|
|
if (fs.existsSync(taskFileName)) {
|
|
|
|
try {
|
|
|
|
fs.unlinkSync(taskFileName);
|
|
|
|
results.messages.push(`Deleted task file: ${taskFileName}`);
|
|
|
|
} catch (unlinkError) {
|
|
|
|
const unlinkMsg = `Failed to delete task file ${taskFileName}: ${unlinkError.message}`;
|
|
|
|
results.errors.push(unlinkMsg);
|
|
|
|
results.success = false;
|
|
|
|
log('warn', unlinkMsg);
|
|
|
|
}
|
|
|
|
}
|
2025-04-21 17:48:30 -04:00
|
|
|
}
|
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// Generate updated task files ONCE
|
2025-04-21 17:48:30 -04:00
|
|
|
try {
|
2025-04-28 00:41:32 -04:00
|
|
|
await generateTaskFiles(tasksPath, path.dirname(tasksPath));
|
|
|
|
results.messages.push('Task files regenerated successfully.');
|
|
|
|
} catch (genError) {
|
|
|
|
const genErrMsg = `Failed to regenerate task files: ${genError.message}`;
|
|
|
|
results.errors.push(genErrMsg);
|
|
|
|
results.success = false;
|
|
|
|
log('warn', genErrMsg);
|
2025-04-21 17:48:30 -04:00
|
|
|
}
|
2025-04-28 00:41:32 -04:00
|
|
|
} else if (results.errors.length === 0) {
|
|
|
|
// Case where valid IDs were provided but none existed
|
|
|
|
results.messages.push('No tasks found matching the provided IDs.');
|
2025-04-21 17:48:30 -04:00
|
|
|
}
|
|
|
|
|
2025-04-28 00:41:32 -04:00
|
|
|
// Consolidate messages for final output
|
|
|
|
const finalMessage = results.messages.join('\n');
|
|
|
|
const finalError = results.errors.join('\n');
|
2025-04-21 17:48:30 -04:00
|
|
|
|
|
|
|
return {
|
2025-04-28 00:41:32 -04:00
|
|
|
success: results.success,
|
|
|
|
message: finalMessage || 'No tasks were removed.',
|
|
|
|
error: finalError || null,
|
|
|
|
removedTasks: results.removedTasks
|
2025-04-21 17:48:30 -04:00
|
|
|
};
|
|
|
|
} catch (error) {
|
2025-04-28 00:41:32 -04:00
|
|
|
// Catch errors from reading file or other initial setup
|
|
|
|
log('error', `Error removing tasks: ${error.message}`);
|
|
|
|
return {
|
|
|
|
success: false,
|
|
|
|
message: '',
|
|
|
|
error: `Operation failed: ${error.message}`,
|
|
|
|
removedTasks: []
|
2025-04-21 17:48:30 -04:00
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default removeTask;
|