task-master-marcus-ver
Version:
A task management system for ambitious AI-driven development that doesn't overwhelm and confuse Cursor.
345 lines (307 loc) • 11.5 kB
JavaScript
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import boxen from 'boxen';
import { z } from 'zod';
import {
log,
// writeJSON,
enableSilentMode,
disableSilentMode,
isSilentMode,
// readJSON,
// need a writeText and readText
readText,
writeText,
// findTaskById
} from '../utils.js';
import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag } from '../config-manager.js';
// import generateTaskFiles from './generate-task-files.js';
import { displayAiUsageSummary } from '../ui.js';
// Define the Zod schema for a SINGLE task object
// const prdSingleTaskSchema = z.object({
// id: z.number().int().positive(),
// title: z.string().min(1),
// description: z.string().min(1),
// details: z.string().optional().default(''),
// testStrategy: z.string().optional().default(''),
// priority: z.enum(['high', 'medium', 'low']).default('medium'),
// dependencies: z.array(z.number().int().positive()).optional().default([]),
// status: z.string().optional().default('pending')
// });
// Define the Zod schema for the ENTIRE expected AI response object
// const prdResponseSchema = z.object({
// prdRequirements: z.string(),
// metadata: z.object({
// projectName: z.string(),
// // totalTasks: z.number(),
// sourceFile: z.string(),
// generatedAt: z.string()
// })
// });
/**
* Parse a PRD file and generate tasks
* @param {string} prdPath - Path to the example PRD file
* @param {string} tasksPath - Path to the tasks.json file | [NEW] Output path to the scripts/prd.txt
* @param {number} numTasks - [OBSOLETE] Number of tasks to generate
* @param {Object} options - Additional options
* @param {boolean} [options.force=false] - Whether to overwrite existing tasks.json.
* @param {boolean} [options.append=false] - Append to existing tasks file.
* @param {Object} [options.reportProgress] - Function to report progress (optional, likely unused).
* @param {Object} [options.mcpLog] - MCP logger object (optional).
* @param {Object} [options.session] - Session object from MCP server (optional).
* @param {string} [options.projectRoot] - Project root path (for MCP/env fallback).
* @param {string} [outputFormat='text'] - Output format ('text' or 'json').
*/
async function addPRD(userRequirements,
prdPath,
tasksPath,
// numTasks,
options = {}) {
const {
reportProgress,
mcpLog,
session,
projectRoot,
force = true, // formerly false
append = true // formerly false
} = options;
const isMCP = !!mcpLog;
const outputFormat = isMCP ? 'json' : 'text';
const logFn = mcpLog
? mcpLog
: {
// Wrapper for CLI
info: (...args) => log('info', ...args),
warn: (...args) => log('warn', ...args),
error: (...args) => log('error', ...args),
debug: (...args) => log('debug', ...args),
success: (...args) => log('success', ...args)
};
// Create custom reporter using logFn
const report = (message, level = 'info') => {
// Check logFn directly
if (logFn && typeof logFn[level] === 'function') {
logFn[level](message);
} else if (!isSilentMode() && outputFormat === 'text') {
// Fallback to original log only if necessary and in CLI text mode
log(level, message);
}
};
report(`Generating PRD file based on: ${prdPath}, Force: ${force}, Append: ${append}`);
let existingTasks = [];
let nextId = 1;
let aiServiceResponse = null;
try {
// Handle file existence and overwrite/append logic
if (fs.existsSync(tasksPath)) {
if (append) {
report(
`Append mode enabled. Reading old PRD file from ${tasksPath}`,
'info'
);
// const existingData = readJSON(tasksPath); // Use readJSON utility
// TODO: refer old prd file as context
const existingTasks = readText(tasksPath);
// if (existingData && Array.isArray(existingData.tasks)) {
// existingTasks = existingData.tasks;
// if (existingTasks.length > 0) {
// nextId = Math.max(...existingTasks.map((t) => t.id || 0)) + 1;
// report(
// `Found ${existingTasks.length} existing tasks. Next ID will be ${nextId}.`,
// 'info'
// );
// }
// } else {
// report(
// `Could not read existing tasks from ${tasksPath} or format is invalid. Proceeding without appending.`,
// 'warn'
// );
// existingTasks = []; // Reset if read fails
// }
} else if (!force) {
// Not appending and not forcing overwrite
const overwriteError = new Error(
`Output file ${tasksPath} already exists. Use --force to overwrite or --append.`
);
report(overwriteError.message, 'error');
if (outputFormat === 'text') {
console.error(chalk.red(overwriteError.message));
process.exit(1);
} else {
throw overwriteError;
}
} else {
// Force overwrite is true
report(
`Force flag enabled. Overwriting existing file: ${tasksPath}`,
'info'
);
}
}
report(`Reading PRD content from ${prdPath}`, 'info');
const prdContent = fs.readFileSync(prdPath, 'utf8');
if (!prdContent) {
throw new Error(`Input file ${prdPath} is empty or could not be read.`);
}
const userRequirementsPrompt = userRequirements
// Build system prompt for PRD parsing
const systemPrompt = `You are an AI assistant specialized in generating Product Requirements Documents (PRDs) in text format.
Analyze the user's requirements.`;
// Build user prompt with PRD content
const userPrompt = `Here's my requirements to analyse and transform into a PRD: \n${userRequirementsPrompt} \n\n
Follow the sample format of PRD here:\n${prdContent}\n\n
Return your response in text.
}`;
// Call the unified AI service
report('Calling AI service to generate PRD file...', 'info');
// Call generateObjectService with the CORRECT schema and additional telemetry params
// aiServiceResponse = await generateObjectService({
// role: 'main',
// session: session,
// projectRoot: projectRoot,
// schema: prdResponseSchema,
// objectName: 'tasks_data',
// systemPrompt: systemPrompt,
// prompt: userPrompt,
// commandName: 'add-prd-file',
// outputType: isMCP ? 'mcp' : 'cli'
// });
aiServiceResponse = await generateTextService({
prompt: userPrompt,
systemPrompt: systemPrompt,
role: 'main',
session: session,
projectRoot: projectRoot,
commandName: 'add-prd-file',
outputType: mcpLog ? 'mcp' : 'cli'
});
// Create the directory if it doesn't exist
const tasksDir = path.dirname(tasksPath);
if (!fs.existsSync(tasksDir)) {
fs.mkdirSync(tasksDir, { recursive: true });
}
logFn.success('Successfully added PRD via AI service.\n');
// Validate and Process Tasks
// const generatedData = aiServiceResponse?.mainResult?.object;
// Robustly get the actual AI-generated object
// let generatedData = null;
// if (aiServiceResponse?.mainResult) {
// if (
// typeof aiServiceResponse.mainResult === 'object' &&
// aiServiceResponse.mainResult !== null &&
// 'prdRequirements' in aiServiceResponse.mainResult
// ) {
// // If mainResult itself is the object with a 'tasks' property
// generatedData = aiServiceResponse.mainResult;
// } else if (
// typeof aiServiceResponse.mainResult.object === 'object' &&
// aiServiceResponse.mainResult.object !== null &&
// 'prdRequirements' in aiServiceResponse.mainResult.object
// ) {
// // If mainResult.object is the object with a 'tasks' property
// generatedData = aiServiceResponse.mainResult.object;
// }
// }
let cleanedResponse = aiServiceResponse.mainResult;
cleanedResponse = cleanedResponse.trim();
cleanedResponse = cleanedResponse.replace(/```text\s*\n?/g, '').replace(/```/g, '');
if (!cleanedResponse) {
logFn.error(
`Internal Error: generateTextService returned unexpected data structure: ${JSON.stringify(generatedData)}`
);
throw new Error(
'AI service returned unexpected data structure after validation.'
);
}
// let currentId = nextId;
// const taskMap = new Map();
// const processedNewTasks = generatedData.tasks.map((task) => {
// const newId = currentId++;
// taskMap.set(task.id, newId);
// return {
// ...task,
// id: newId,
// status: 'pending',
// priority: task.priority || 'medium',
// dependencies: Array.isArray(task.dependencies) ? task.dependencies : [],
// subtasks: []
// };
// });
// // Remap dependencies for the NEWLY processed tasks
// processedNewTasks.forEach((task) => {
// task.dependencies = task.dependencies
// .map((depId) => taskMap.get(depId)) // Map old AI ID to new sequential ID
// .filter(
// (newDepId) =>
// newDepId != null && // Must exist
// newDepId < task.id && // Must be a lower ID (could be existing or newly generated)
// (findTaskById(existingTasks, newDepId) || // Check if it exists in old tasks OR
// processedNewTasks.some((t) => t.id === newDepId)) // check if it exists in new tasks
// );
// });
// const finalTasks = append
// ? [...existingTasks, ...processedNewTasks]
// : processedNewTasks;
// const outputData = { tasks: finalTasks };
const outputData = cleanedResponse
// Write the final tasks to the file
// writeJSON(tasksPath, outputData);
writeText(tasksPath, outputData);
report(
`Successfully ${append ? 'appended' : 'generated'} tasks in ${tasksPath}`,
'success'
);
// Generate markdown task files after writing tasks.json
// await generateTaskFiles(tasksPath, path.dirname(tasksPath), { mcpLog });
// // Handle CLI output (e.g., success message)
// if (outputFormat === 'text') {
// console.log(
// boxen(
// chalk.green(
// `Successfully generated ${processedNewTasks.length} new tasks. Total tasks in ${tasksPath}: ${finalTasks.length}`
// ),
// { padding: 1, borderColor: 'green', borderStyle: 'round' }
// )
// );
// console.log(
// boxen(
// chalk.white.bold('Next Steps:') +
// '\n\n' +
// `${chalk.cyan('1.')} Run ${chalk.yellow('task-master list')} to view all tasks\n` +
// `${chalk.cyan('2.')} Run ${chalk.yellow('task-master expand --id=<id>')} to break down a task into subtasks`,
// {
// padding: 1,
// borderColor: 'cyan',
// borderStyle: 'round',
// margin: { top: 1 }
// }
// )
// );
// if (aiServiceResponse && aiServiceResponse.telemetryData) {
// displayAiUsageSummary(aiServiceResponse.telemetryData, 'cli');
// }
// }
// Return telemetry data
return {
success: true,
tasksPath,
telemetryData: aiServiceResponse?.telemetryData
};
} catch (error) {
report(`Error parsing PRD: ${error.message}`, 'error');
// Only show error UI for text output (CLI)
if (outputFormat === 'text') {
console.error(chalk.red(`Error: ${error.message}`));
if (getDebugFlag(projectRoot)) {
// Use projectRoot for debug flag check
console.error(error);
}
process.exit(1);
} else {
throw error; // Re-throw for JSON output
}
}
}
export default addPRD;