fix(tags): Resolve tag deletion bug in remove-task command
Refactored the core 'removeTask' function to be fully tag-aware, preventing data corruption. - The function now correctly reads the full tagged data structure by prioritizing '_rawTaggedData' instead of operating on a resolved single-tag view. - All subsequent operations (task removal, dependency cleanup, file writing) now correctly reference the full multi-tag data object, preserving the integrity of 'tasks.json'. - This resolves the critical bug where removing a task would delete all other tags.
This commit is contained in:
@@ -16,108 +16,111 @@ import { getDebugFlag } from '../config-manager.js';
|
||||
*/
|
||||
function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
try {
|
||||
// Determine if we're in MCP mode by checking for mcpLog
|
||||
const isMcpMode = !!options?.mcpLog;
|
||||
|
||||
const data = readJSON(tasksPath, options.projectRoot, options.tag);
|
||||
if (!data || !data.tasks) {
|
||||
throw new Error(`No valid tasks found in ${tasksPath}`);
|
||||
// 1. Read the raw data structure, ensuring we have all tags.
|
||||
// We call readJSON without a specific tag to get the resolved default view,
|
||||
// which correctly contains the full structure in `_rawTaggedData`.
|
||||
const resolvedData = readJSON(tasksPath, options.projectRoot);
|
||||
if (!resolvedData) {
|
||||
throw new Error(`Could not read or parse tasks file: ${tasksPath}`);
|
||||
}
|
||||
// Prioritize the _rawTaggedData if it exists, otherwise use the data as is.
|
||||
const rawData = resolvedData._rawTaggedData || resolvedData;
|
||||
|
||||
// 2. Determine the target tag we need to generate files for.
|
||||
const targetTag = options.tag || resolvedData.tag || 'master';
|
||||
const tagData = rawData[targetTag];
|
||||
|
||||
if (!tagData || !tagData.tasks) {
|
||||
throw new Error(
|
||||
`Tag '${targetTag}' not found or has no tasks in the data.`
|
||||
);
|
||||
}
|
||||
const tasksForGeneration = tagData.tasks;
|
||||
|
||||
// Create the output directory if it doesn't exist
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
log('info', `Preparing to regenerate ${data.tasks.length} task files`);
|
||||
|
||||
// Validate and fix dependencies before generating files
|
||||
log('info', `Validating and fixing dependencies`);
|
||||
validateAndFixDependencies(
|
||||
data,
|
||||
tasksPath,
|
||||
options.projectRoot,
|
||||
options.tag
|
||||
log(
|
||||
'info',
|
||||
`Preparing to regenerate ${tasksForGeneration.length} task files for tag '${targetTag}'`
|
||||
);
|
||||
|
||||
// Get valid task IDs from tasks.json
|
||||
const validTaskIds = data.tasks.map((task) => task.id);
|
||||
// 3. Validate dependencies using the FULL, raw data structure to prevent data loss.
|
||||
validateAndFixDependencies(
|
||||
rawData, // Pass the entire object with all tags
|
||||
tasksPath,
|
||||
options.projectRoot,
|
||||
targetTag // Provide the current tag context for the operation
|
||||
);
|
||||
|
||||
const allTasksInTag = tagData.tasks;
|
||||
const validTaskIds = allTasksInTag.map((task) => task.id);
|
||||
|
||||
// Cleanup orphaned task files
|
||||
log('info', 'Checking for orphaned task files to clean up...');
|
||||
try {
|
||||
// Get all task files in the output directory
|
||||
const files = fs.readdirSync(outputDir);
|
||||
const taskFilePattern = /^task_(\d+)\.txt$/;
|
||||
|
||||
// Filter for task files and check if they match a valid task ID
|
||||
const orphanedFiles = files.filter((file) => {
|
||||
const match = file.match(taskFilePattern);
|
||||
if (match) {
|
||||
const fileTaskId = parseInt(match[1], 10);
|
||||
// Important: Only clean up files for tasks that *should* be in the current tag.
|
||||
// This prevents deleting files from other tags.
|
||||
// A more robust cleanup might need to check across all tags.
|
||||
// For now, this is safer than the previous implementation.
|
||||
return !validTaskIds.includes(fileTaskId);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Delete orphaned files
|
||||
if (orphanedFiles.length > 0) {
|
||||
log(
|
||||
'info',
|
||||
`Found ${orphanedFiles.length} orphaned task files to remove`
|
||||
`Found ${orphanedFiles.length} orphaned task files to remove for tag '${targetTag}'`
|
||||
);
|
||||
|
||||
orphanedFiles.forEach((file) => {
|
||||
const filePath = path.join(outputDir, file);
|
||||
try {
|
||||
fs.unlinkSync(filePath);
|
||||
log('info', `Removed orphaned task file: ${file}`);
|
||||
} catch (err) {
|
||||
log(
|
||||
'warn',
|
||||
`Failed to remove orphaned task file ${file}: ${err.message}`
|
||||
);
|
||||
}
|
||||
fs.unlinkSync(filePath);
|
||||
});
|
||||
} else {
|
||||
log('info', 'No orphaned task files found');
|
||||
log('info', 'No orphaned task files found.');
|
||||
}
|
||||
} catch (err) {
|
||||
log('warn', `Error cleaning up orphaned task files: ${err.message}`);
|
||||
// Continue with file generation even if cleanup fails
|
||||
}
|
||||
|
||||
// Generate task files
|
||||
log('info', 'Generating individual task files...');
|
||||
data.tasks.forEach((task) => {
|
||||
// Generate task files for the target tag
|
||||
log('info', `Generating individual task files for tag '${targetTag}'...`);
|
||||
tasksForGeneration.forEach((task) => {
|
||||
const taskPath = path.join(
|
||||
outputDir,
|
||||
`task_${task.id.toString().padStart(3, '0')}.txt`
|
||||
);
|
||||
|
||||
// Format the content
|
||||
let content = `# Task ID: ${task.id}\n`;
|
||||
content += `# Title: ${task.title}\n`;
|
||||
content += `# Status: ${task.status || 'pending'}\n`;
|
||||
|
||||
// Format dependencies with their status
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, data.tasks, false)}\n`;
|
||||
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, allTasksInTag, false)}\n`;
|
||||
} else {
|
||||
content += '# Dependencies: None\n';
|
||||
}
|
||||
|
||||
content += `# Priority: ${task.priority || 'medium'}\n`;
|
||||
content += `# Description: ${task.description || ''}\n`;
|
||||
|
||||
// Add more detailed sections
|
||||
content += '# Details:\n';
|
||||
content += (task.details || '')
|
||||
.split('\n')
|
||||
.map((line) => line)
|
||||
.join('\n');
|
||||
content += '\n\n';
|
||||
|
||||
content += '# Test Strategy:\n';
|
||||
content += (task.testStrategy || '')
|
||||
.split('\n')
|
||||
@@ -125,36 +128,22 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
.join('\n');
|
||||
content += '\n';
|
||||
|
||||
// Add subtasks if they exist
|
||||
if (task.subtasks && task.subtasks.length > 0) {
|
||||
content += '\n# Subtasks:\n';
|
||||
|
||||
task.subtasks.forEach((subtask) => {
|
||||
content += `## ${subtask.id}. ${subtask.title} [${subtask.status || 'pending'}]\n`;
|
||||
|
||||
if (subtask.dependencies && subtask.dependencies.length > 0) {
|
||||
// Format subtask dependencies
|
||||
let subtaskDeps = subtask.dependencies
|
||||
.map((depId) => {
|
||||
if (typeof depId === 'number') {
|
||||
// Handle numeric dependencies to other subtasks
|
||||
const foundSubtask = task.subtasks.find(
|
||||
(st) => st.id === depId
|
||||
);
|
||||
if (foundSubtask) {
|
||||
// Just return the plain ID format without any color formatting
|
||||
return `${task.id}.${depId}`;
|
||||
}
|
||||
}
|
||||
return depId.toString();
|
||||
})
|
||||
const subtaskDeps = subtask.dependencies
|
||||
.map((depId) =>
|
||||
typeof depId === 'number'
|
||||
? `${task.id}.${depId}`
|
||||
: depId.toString()
|
||||
)
|
||||
.join(', ');
|
||||
|
||||
content += `### Dependencies: ${subtaskDeps}\n`;
|
||||
} else {
|
||||
content += '### Dependencies: None\n';
|
||||
}
|
||||
|
||||
content += `### Description: ${subtask.description || ''}\n`;
|
||||
content += '### Details:\n';
|
||||
content += (subtask.details || '')
|
||||
@@ -165,39 +154,30 @@ function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
||||
});
|
||||
}
|
||||
|
||||
// Write the file
|
||||
fs.writeFileSync(taskPath, content);
|
||||
// log('info', `Generated: task_${task.id.toString().padStart(3, '0')}.txt`); // Pollutes the CLI output
|
||||
});
|
||||
|
||||
log(
|
||||
'success',
|
||||
`All ${data.tasks.length} tasks have been generated into '${outputDir}'.`
|
||||
`All ${tasksForGeneration.length} tasks for tag '${targetTag}' have been generated into '${outputDir}'.`
|
||||
);
|
||||
|
||||
// Return success data in MCP mode
|
||||
if (isMcpMode) {
|
||||
return {
|
||||
success: true,
|
||||
count: data.tasks.length,
|
||||
count: tasksForGeneration.length,
|
||||
directory: outputDir
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
log('error', `Error generating task files: ${error.message}`);
|
||||
|
||||
// Only show error UI in CLI mode
|
||||
if (!options?.mcpLog) {
|
||||
console.error(chalk.red(`Error generating task files: ${error.message}`));
|
||||
|
||||
if (getDebugFlag()) {
|
||||
// Use getter
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
process.exit(1);
|
||||
} else {
|
||||
// In MCP mode, throw the error for the caller to handle
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user