* feat: Add --append flag to parsePRD command - Fixes #207 * chore: format * chore: implement tests to core logic and commands * feat: implement MCP for append flag of parse_prd tool * fix: append not considering existing tasks * chore: fix tests --------- Co-authored-by: Kresna Sucandra <kresnasucandra@gmail.com>
This commit is contained in:
@@ -88,6 +88,10 @@ function registerCommands(programInstance) {
|
||||
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
|
||||
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
|
||||
.option('-f, --force', 'Skip confirmation when overwriting existing tasks')
|
||||
.option(
|
||||
'--append',
|
||||
'Append new tasks to existing tasks.json instead of overwriting'
|
||||
)
|
||||
.action(async (file, options) => {
|
||||
// Use input option if file argument not provided
|
||||
const inputFile = file || options.input;
|
||||
@@ -95,10 +99,11 @@ function registerCommands(programInstance) {
|
||||
const numTasks = parseInt(options.numTasks, 10);
|
||||
const outputPath = options.output;
|
||||
const force = options.force || false;
|
||||
const append = options.append || false;
|
||||
|
||||
// Helper function to check if tasks.json exists and confirm overwrite
|
||||
async function confirmOverwriteIfNeeded() {
|
||||
if (fs.existsSync(outputPath) && !force) {
|
||||
if (fs.existsSync(outputPath) && !force && !append) {
|
||||
const shouldContinue = await confirmTaskOverwrite(outputPath);
|
||||
if (!shouldContinue) {
|
||||
console.log(chalk.yellow('Operation cancelled by user.'));
|
||||
@@ -117,7 +122,7 @@ function registerCommands(programInstance) {
|
||||
if (!(await confirmOverwriteIfNeeded())) return;
|
||||
|
||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||
await parsePRD(defaultPrdPath, outputPath, numTasks);
|
||||
await parsePRD(defaultPrdPath, outputPath, numTasks, { append });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -138,17 +143,21 @@ function registerCommands(programInstance) {
|
||||
' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' +
|
||||
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
|
||||
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' +
|
||||
' -f, --force Skip confirmation when overwriting existing tasks\n\n' +
|
||||
' -f, --force Skip confirmation when overwriting existing tasks\n' +
|
||||
' --append Append new tasks to existing tasks.json instead of overwriting\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' +
|
||||
' task-master parse-prd --force\n' +
|
||||
' task-master parse-prd requirements_v2.txt --append\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',
|
||||
' 3. Generate tasks from the PRD and either:\n' +
|
||||
' - Overwrite any existing tasks.json file (default)\n' +
|
||||
' - Append to existing tasks.json if --append is used',
|
||||
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
|
||||
)
|
||||
);
|
||||
@@ -160,8 +169,11 @@ function registerCommands(programInstance) {
|
||||
|
||||
console.log(chalk.blue(`Parsing PRD file: ${inputFile}`));
|
||||
console.log(chalk.blue(`Generating ${numTasks} tasks...`));
|
||||
if (append) {
|
||||
console.log(chalk.blue('Appending to existing tasks...'));
|
||||
}
|
||||
|
||||
await parsePRD(inputFile, outputPath, numTasks);
|
||||
await parsePRD(inputFile, outputPath, numTasks, { append });
|
||||
});
|
||||
|
||||
// update command
|
||||
|
||||
@@ -106,7 +106,7 @@ async function parsePRD(
|
||||
aiClient = null,
|
||||
modelConfig = null
|
||||
) {
|
||||
const { reportProgress, mcpLog, session } = options;
|
||||
const { reportProgress, mcpLog, session, append } = options;
|
||||
|
||||
// Determine output format based on mcpLog presence (simplification)
|
||||
const outputFormat = mcpLog ? 'json' : 'text';
|
||||
@@ -127,8 +127,30 @@ async function parsePRD(
|
||||
// Read the PRD content
|
||||
const prdContent = fs.readFileSync(prdPath, 'utf8');
|
||||
|
||||
// If appending and tasks.json exists, read existing tasks first
|
||||
let existingTasks = { tasks: [] };
|
||||
let lastTaskId = 0;
|
||||
if (append && fs.existsSync(tasksPath)) {
|
||||
try {
|
||||
existingTasks = readJSON(tasksPath);
|
||||
if (existingTasks.tasks?.length) {
|
||||
// Find the highest task ID
|
||||
lastTaskId = existingTasks.tasks.reduce((maxId, task) => {
|
||||
const mainId = parseInt(task.id.toString().split('.')[0], 10) || 0;
|
||||
return Math.max(maxId, mainId);
|
||||
}, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
report(
|
||||
`Warning: Could not read existing tasks file: ${error.message}`,
|
||||
'warn'
|
||||
);
|
||||
existingTasks = { tasks: [] };
|
||||
}
|
||||
}
|
||||
|
||||
// Call Claude to generate tasks, passing the provided AI client if available
|
||||
const tasksData = await callClaude(
|
||||
const newTasksData = await callClaude(
|
||||
prdContent,
|
||||
prdPath,
|
||||
numTasks,
|
||||
@@ -138,15 +160,33 @@ async function parsePRD(
|
||||
modelConfig
|
||||
);
|
||||
|
||||
// Update task IDs if appending
|
||||
if (append && lastTaskId > 0) {
|
||||
report(`Updating task IDs to continue from ID ${lastTaskId}`, 'info');
|
||||
newTasksData.tasks.forEach((task, index) => {
|
||||
task.id = lastTaskId + index + 1;
|
||||
});
|
||||
}
|
||||
|
||||
// Merge tasks if appending
|
||||
const tasksData = append
|
||||
? {
|
||||
...existingTasks,
|
||||
tasks: [...existingTasks.tasks, ...newTasksData.tasks]
|
||||
}
|
||||
: newTasksData;
|
||||
|
||||
// Create the directory if it doesn't exist
|
||||
const tasksDir = path.dirname(tasksPath);
|
||||
if (!fs.existsSync(tasksDir)) {
|
||||
fs.mkdirSync(tasksDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Write the tasks to the file
|
||||
writeJSON(tasksPath, tasksData);
|
||||
const actionVerb = append ? 'appended' : 'generated';
|
||||
report(
|
||||
`Successfully generated ${tasksData.tasks.length} tasks from PRD`,
|
||||
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`,
|
||||
'success'
|
||||
);
|
||||
report(`Tasks saved to: ${tasksPath}`, 'info');
|
||||
@@ -166,7 +206,7 @@ async function parsePRD(
|
||||
console.log(
|
||||
boxen(
|
||||
chalk.green(
|
||||
`Successfully generated ${tasksData.tasks.length} tasks from PRD`
|
||||
`Successfully ${actionVerb} ${newTasksData.tasks.length} tasks from PRD`
|
||||
),
|
||||
{ padding: 1, borderColor: 'green', borderStyle: 'round' }
|
||||
)
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
async function updateSubtaskById(tasksPath, subtaskId, prompt, useResearch = false) {
|
||||
let loadingIndicator = null;
|
||||
try {
|
||||
log('info', `Updating subtask ${subtaskId} with prompt: "${prompt}"`);
|
||||
|
||||
// Validate subtask ID format
|
||||
if (!subtaskId || typeof subtaskId !== 'string' || !subtaskId.includes('.')) {
|
||||
throw new Error(`Invalid subtask ID format: ${subtaskId}. Subtask ID must be in format "parentId.subtaskId"`);
|
||||
}
|
||||
|
||||
// Validate prompt
|
||||
if (!prompt || typeof prompt !== 'string' || prompt.trim() === '') {
|
||||
throw new Error('Prompt cannot be empty. Please provide context for the subtask update.');
|
||||
}
|
||||
|
||||
// Prepare for fallback handling
|
||||
let claudeOverloaded = false;
|
||||
|
||||
// Validate tasks file exists
|
||||
if (!fs.existsSync(tasksPath)) {
|
||||
throw new Error(`Tasks file not found at path: ${tasksPath}`);
|
||||
}
|
||||
|
||||
// Read the tasks file
|
||||
const data = readJSON(tasksPath);
|
||||
// ... rest of the function
|
||||
} catch (error) {
|
||||
// Handle errors
|
||||
console.error(`Error updating subtask: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user