- parse-prd now supports creating tasks in specific contexts - Fixed tag preservation logic to prevent data loss - analyze-complexity generates tag-specific reports - Non-existent tags created automatically - Enables rapid prototyping and parallel development workflows
202 lines
6.3 KiB
JavaScript
202 lines
6.3 KiB
JavaScript
import fs from 'fs';
|
|
import path from 'path';
|
|
import chalk from 'chalk';
|
|
|
|
import { log, readJSON } from '../utils.js';
|
|
import { formatDependenciesWithStatus } from '../ui.js';
|
|
import { validateAndFixDependencies } from '../dependency-manager.js';
|
|
import { getDebugFlag } from '../config-manager.js';
|
|
|
|
/**
|
|
* Generate individual task files from tasks.json
|
|
* @param {string} tasksPath - Path to the tasks.json file
|
|
* @param {string} outputDir - Output directory for task files
|
|
* @param {Object} options - Additional options (mcpLog for MCP mode, projectRoot, tag)
|
|
* @returns {Object|undefined} Result object in MCP mode, undefined in CLI mode
|
|
*/
|
|
function generateTaskFiles(tasksPath, outputDir, options = {}) {
|
|
try {
|
|
const isMcpMode = !!options?.mcpLog;
|
|
|
|
// 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 ${tasksForGeneration.length} task files for tag '${targetTag}'`
|
|
);
|
|
|
|
// 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 {
|
|
const files = fs.readdirSync(outputDir);
|
|
// Tag-aware file patterns: master -> task_001.txt, other tags -> task_001_tagname.txt
|
|
const masterFilePattern = /^task_(\d+)\.txt$/;
|
|
const taggedFilePattern = new RegExp(`^task_(\\d+)_${targetTag}\\.txt$`);
|
|
|
|
const orphanedFiles = files.filter((file) => {
|
|
let match = null;
|
|
let fileTaskId = null;
|
|
|
|
// Check if file belongs to current tag
|
|
if (targetTag === 'master') {
|
|
match = file.match(masterFilePattern);
|
|
if (match) {
|
|
fileTaskId = parseInt(match[1], 10);
|
|
// Only clean up master files when processing master tag
|
|
return !validTaskIds.includes(fileTaskId);
|
|
}
|
|
} else {
|
|
match = file.match(taggedFilePattern);
|
|
if (match) {
|
|
fileTaskId = parseInt(match[1], 10);
|
|
// Only clean up files for the current tag
|
|
return !validTaskIds.includes(fileTaskId);
|
|
}
|
|
}
|
|
return false;
|
|
});
|
|
|
|
if (orphanedFiles.length > 0) {
|
|
log(
|
|
'info',
|
|
`Found ${orphanedFiles.length} orphaned task files to remove for tag '${targetTag}'`
|
|
);
|
|
orphanedFiles.forEach((file) => {
|
|
const filePath = path.join(outputDir, file);
|
|
fs.unlinkSync(filePath);
|
|
});
|
|
} else {
|
|
log('info', 'No orphaned task files found.');
|
|
}
|
|
} catch (err) {
|
|
log('warn', `Error cleaning up orphaned task files: ${err.message}`);
|
|
}
|
|
|
|
// Generate task files for the target tag
|
|
log('info', `Generating individual task files for tag '${targetTag}'...`);
|
|
tasksForGeneration.forEach((task) => {
|
|
// Tag-aware file naming: master -> task_001.txt, other tags -> task_001_tagname.txt
|
|
const taskFileName =
|
|
targetTag === 'master'
|
|
? `task_${task.id.toString().padStart(3, '0')}.txt`
|
|
: `task_${task.id.toString().padStart(3, '0')}_${targetTag}.txt`;
|
|
|
|
const taskPath = path.join(outputDir, taskFileName);
|
|
|
|
let content = `# Task ID: ${task.id}\n`;
|
|
content += `# Title: ${task.title}\n`;
|
|
content += `# Status: ${task.status || 'pending'}\n`;
|
|
|
|
if (task.dependencies && task.dependencies.length > 0) {
|
|
content += `# Dependencies: ${formatDependenciesWithStatus(task.dependencies, allTasksInTag, false)}\n`;
|
|
} else {
|
|
content += '# Dependencies: None\n';
|
|
}
|
|
|
|
content += `# Priority: ${task.priority || 'medium'}\n`;
|
|
content += `# Description: ${task.description || ''}\n`;
|
|
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')
|
|
.map((line) => line)
|
|
.join('\n');
|
|
content += '\n';
|
|
|
|
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) {
|
|
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 || '')
|
|
.split('\n')
|
|
.map((line) => line)
|
|
.join('\n');
|
|
content += '\n\n';
|
|
});
|
|
}
|
|
|
|
fs.writeFileSync(taskPath, content);
|
|
});
|
|
|
|
log(
|
|
'success',
|
|
`All ${tasksForGeneration.length} tasks for tag '${targetTag}' have been generated into '${outputDir}'.`
|
|
);
|
|
|
|
if (isMcpMode) {
|
|
return {
|
|
success: true,
|
|
count: tasksForGeneration.length,
|
|
directory: outputDir
|
|
};
|
|
}
|
|
} catch (error) {
|
|
log('error', `Error generating task files: ${error.message}`);
|
|
if (!options?.mcpLog) {
|
|
console.error(chalk.red(`Error generating task files: ${error.message}`));
|
|
if (getDebugFlag()) {
|
|
console.error(error);
|
|
}
|
|
process.exit(1);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
export default generateTaskFiles;
|