UNPKG

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
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;