mirror of
https://github.com/eyaltoledano/claude-task-master.git
synced 2026-01-01 01:32:19 +00:00
* initial cutover * update log to debug * update tracker to pass units * update test to match new base tracker format * add streamTextService mocks * remove unused imports * Ensure the CLI waits for async main() completion * refactor to reduce code duplication * update comment * reuse function * ensure targetTag is defined in streaming mode * avoid throwing inside process.exit spy * check for null * remove reference to generate * fix formatting * fix textStream assignment * ensure no division by 0 * fix jest chalk mocks * refactor for maintainability * Improve bar chart calculation logic for consistent visual representation * use custom streaming error types; fix mocks * Update streamText extraction in parse-prd.js to match actual service response * remove check - doesn't belong here * update mocks * remove streaming test that wasn't really doing anything * add comment * make parsing logic more DRY * fix formatting * Fix textStream extraction to match actual service response * fix mock * Add a cleanup method to ensure proper resource disposal and prevent memory leaks * debounce progress updates to reduce UI flicker during rapid updates * Implement timeout protection for streaming operations (60-second timeout) with automatic fallback to non-streaming mode. * clear timeout properly * Add a maximum buffer size limit (1MB) to prevent unbounded memory growth with very large streaming responses. * fix formatting * remove duplicate mock * better docs * fix formatting * sanitize the dynamic property name * Fix incorrect remaining progress calculation * Use onError callback instead of console.warn * Remove unused chalk import * Add missing custom validator in fallback parsing configuration * add custom validator parameter in fallback parsing * chore: fix package-lock.json * chore: large code refactor * chore: increase timeout from 1 minute to 3 minutes * fix: refactor and fix streaming * Merge remote-tracking branch 'origin/next' into joedanz/parse-prd-progress * fix: cleanup and fix unit tests * chore: fix unit tests * chore: fix format * chore: run format * chore: fix weird CI unit test error * chore: fix format --------- Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
478 lines
13 KiB
JavaScript
478 lines
13 KiB
JavaScript
/**
|
|
* parse-prd.js
|
|
* UI functions specifically for PRD parsing operations
|
|
*/
|
|
|
|
import chalk from 'chalk';
|
|
import boxen from 'boxen';
|
|
import Table from 'cli-table3';
|
|
import { formatElapsedTime } from '../utils/format.js';
|
|
|
|
// Constants
|
|
const CONSTANTS = {
|
|
BAR_WIDTH: 40,
|
|
TABLE_COL_WIDTHS: [28, 50],
|
|
DEFAULT_MODEL: 'Default',
|
|
DEFAULT_TEMPERATURE: 0.7
|
|
};
|
|
|
|
const PRIORITIES = {
|
|
HIGH: 'high',
|
|
MEDIUM: 'medium',
|
|
LOW: 'low'
|
|
};
|
|
|
|
const PRIORITY_COLORS = {
|
|
[PRIORITIES.HIGH]: '#CC0000',
|
|
[PRIORITIES.MEDIUM]: '#FF8800',
|
|
[PRIORITIES.LOW]: '#FFCC00'
|
|
};
|
|
|
|
// Reusable box styles
|
|
const BOX_STYLES = {
|
|
main: {
|
|
padding: { top: 1, bottom: 1, left: 2, right: 2 },
|
|
margin: { top: 0, bottom: 0 },
|
|
borderColor: 'blue',
|
|
borderStyle: 'round'
|
|
},
|
|
summary: {
|
|
padding: { top: 1, right: 1, bottom: 1, left: 1 },
|
|
borderColor: 'blue',
|
|
borderStyle: 'round',
|
|
margin: { top: 1, right: 1, bottom: 1, left: 0 }
|
|
},
|
|
warning: {
|
|
padding: 1,
|
|
borderColor: 'yellow',
|
|
borderStyle: 'round',
|
|
margin: { top: 1, bottom: 1 }
|
|
},
|
|
nextSteps: {
|
|
padding: 1,
|
|
borderColor: 'cyan',
|
|
borderStyle: 'round',
|
|
margin: { top: 1, right: 0, bottom: 1, left: 0 }
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Helper function for building main message content
|
|
* @param {Object} params - Message parameters
|
|
* @param {string} params.prdFilePath - Path to the PRD file
|
|
* @param {string} params.outputPath - Path where tasks will be saved
|
|
* @param {number} params.numTasks - Number of tasks to generate
|
|
* @param {string} params.model - AI model name
|
|
* @param {number} params.temperature - AI temperature setting
|
|
* @param {boolean} params.append - Whether appending to existing tasks
|
|
* @param {boolean} params.research - Whether research mode is enabled
|
|
* @returns {string} The formatted message content
|
|
*/
|
|
function buildMainMessage({
|
|
prdFilePath,
|
|
outputPath,
|
|
numTasks,
|
|
model,
|
|
temperature,
|
|
append,
|
|
research
|
|
}) {
|
|
const actionVerb = append ? 'Appending' : 'Generating';
|
|
|
|
let modelLine = `Model: ${model} | Temperature: ${temperature}`;
|
|
if (research) {
|
|
modelLine += ` | ${chalk.cyan.bold('🔬 Research Mode')}`;
|
|
}
|
|
|
|
return (
|
|
chalk.bold(`🤖 Parsing PRD and ${actionVerb} Tasks`) +
|
|
'\n' +
|
|
chalk.dim(modelLine) +
|
|
'\n\n' +
|
|
chalk.blue(`Input: ${prdFilePath}`) +
|
|
'\n' +
|
|
chalk.blue(`Output: ${outputPath}`) +
|
|
'\n' +
|
|
chalk.blue(`Tasks to ${append ? 'Append' : 'Generate'}: ${numTasks}`)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function for displaying the main message box
|
|
* @param {string} message - The message content to display in the box
|
|
*/
|
|
function displayMainMessageBox(message) {
|
|
console.log(boxen(message, BOX_STYLES.main));
|
|
}
|
|
|
|
/**
|
|
* Helper function for displaying append mode notice
|
|
* @param {number} existingTasksCount - Number of existing tasks
|
|
* @param {number} nextId - Next ID to be used
|
|
*/
|
|
function displayAppendModeNotice(existingTasksCount, nextId) {
|
|
console.log(
|
|
chalk.yellow.bold('📝 Append mode') +
|
|
` - Adding to ${existingTasksCount} existing tasks (next ID: ${nextId})`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Helper function for force mode messages
|
|
* @param {boolean} append - Whether in append mode
|
|
* @returns {string} The formatted force mode message
|
|
*/
|
|
function createForceMessage(append) {
|
|
const baseMessage = chalk.red.bold('⚠️ Force flag enabled');
|
|
return append
|
|
? `${baseMessage} - Will overwrite if conflicts occur`
|
|
: `${baseMessage} - Overwriting existing tasks`;
|
|
}
|
|
|
|
/**
|
|
* Display the start of PRD parsing with a boxen announcement
|
|
* @param {Object} options - Options for PRD parsing start
|
|
* @param {string} options.prdFilePath - Path to the PRD file being parsed
|
|
* @param {string} options.outputPath - Path where the tasks will be saved
|
|
* @param {number} options.numTasks - Number of tasks to generate
|
|
* @param {string} [options.model] - AI model name
|
|
* @param {number} [options.temperature] - AI temperature setting
|
|
* @param {boolean} [options.append=false] - Whether to append to existing tasks
|
|
* @param {boolean} [options.research=false] - Whether research mode is enabled
|
|
* @param {boolean} [options.force=false] - Whether force mode is enabled
|
|
* @param {Array} [options.existingTasks=[]] - Existing tasks array
|
|
* @param {number} [options.nextId=1] - Next ID to be used
|
|
*/
|
|
function displayParsePrdStart({
|
|
prdFilePath,
|
|
outputPath,
|
|
numTasks,
|
|
model = CONSTANTS.DEFAULT_MODEL,
|
|
temperature = CONSTANTS.DEFAULT_TEMPERATURE,
|
|
append = false,
|
|
research = false,
|
|
force = false,
|
|
existingTasks = [],
|
|
nextId = 1
|
|
}) {
|
|
// Input validation
|
|
if (
|
|
!prdFilePath ||
|
|
typeof prdFilePath !== 'string' ||
|
|
prdFilePath.trim() === ''
|
|
) {
|
|
throw new Error('prdFilePath is required and must be a non-empty string');
|
|
}
|
|
if (
|
|
!outputPath ||
|
|
typeof outputPath !== 'string' ||
|
|
outputPath.trim() === ''
|
|
) {
|
|
throw new Error('outputPath is required and must be a non-empty string');
|
|
}
|
|
|
|
// Build and display the main message box
|
|
const message = buildMainMessage({
|
|
prdFilePath,
|
|
outputPath,
|
|
numTasks,
|
|
model,
|
|
temperature,
|
|
append,
|
|
research
|
|
});
|
|
displayMainMessageBox(message);
|
|
|
|
// Display append/force notices beneath the boxen if either flag is set
|
|
if (append || force) {
|
|
// Add append mode details if enabled
|
|
if (append) {
|
|
displayAppendModeNotice(existingTasks.length, nextId);
|
|
}
|
|
|
|
// Add force mode details if enabled
|
|
if (force) {
|
|
console.log(createForceMessage(append));
|
|
}
|
|
|
|
// Add a blank line after notices for spacing
|
|
console.log();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Calculate priority statistics
|
|
* @param {Object} taskPriorities - Priority counts object
|
|
* @param {number} totalTasks - Total number of tasks
|
|
* @returns {Object} Priority statistics with counts and percentages
|
|
*/
|
|
function calculatePriorityStats(taskPriorities, totalTasks) {
|
|
const stats = {};
|
|
|
|
Object.values(PRIORITIES).forEach((priority) => {
|
|
const count = taskPriorities[priority] || 0;
|
|
stats[priority] = {
|
|
count,
|
|
percentage: totalTasks > 0 ? Math.round((count / totalTasks) * 100) : 0
|
|
};
|
|
});
|
|
|
|
return stats;
|
|
}
|
|
|
|
/**
|
|
* Calculate bar character distribution for priorities
|
|
* @param {Object} priorityStats - Priority statistics
|
|
* @param {number} totalTasks - Total number of tasks
|
|
* @returns {Object} Character counts for each priority
|
|
*/
|
|
function calculateBarDistribution(priorityStats, totalTasks) {
|
|
const barWidth = CONSTANTS.BAR_WIDTH;
|
|
const distribution = {};
|
|
|
|
if (totalTasks === 0) {
|
|
Object.values(PRIORITIES).forEach((priority) => {
|
|
distribution[priority] = 0;
|
|
});
|
|
return distribution;
|
|
}
|
|
|
|
// Calculate raw proportions
|
|
const rawChars = {};
|
|
Object.values(PRIORITIES).forEach((priority) => {
|
|
rawChars[priority] =
|
|
(priorityStats[priority].count / totalTasks) * barWidth;
|
|
});
|
|
|
|
// Initial distribution - floor values
|
|
Object.values(PRIORITIES).forEach((priority) => {
|
|
distribution[priority] = Math.floor(rawChars[priority]);
|
|
});
|
|
|
|
// Ensure non-zero priorities get at least 1 character
|
|
Object.values(PRIORITIES).forEach((priority) => {
|
|
if (priorityStats[priority].count > 0 && distribution[priority] === 0) {
|
|
distribution[priority] = 1;
|
|
}
|
|
});
|
|
|
|
// Distribute remaining characters based on decimal parts
|
|
const currentTotal = Object.values(distribution).reduce(
|
|
(sum, val) => sum + val,
|
|
0
|
|
);
|
|
const remainingChars = barWidth - currentTotal;
|
|
|
|
if (remainingChars > 0) {
|
|
const decimals = Object.values(PRIORITIES)
|
|
.map((priority) => ({
|
|
priority,
|
|
decimal: rawChars[priority] - Math.floor(rawChars[priority])
|
|
}))
|
|
.sort((a, b) => b.decimal - a.decimal);
|
|
|
|
for (let i = 0; i < remainingChars && i < decimals.length; i++) {
|
|
distribution[decimals[i].priority]++;
|
|
}
|
|
}
|
|
|
|
return distribution;
|
|
}
|
|
|
|
/**
|
|
* Create priority distribution bar visual
|
|
* @param {Object} barDistribution - Character distribution for priorities
|
|
* @returns {string} Visual bar string
|
|
*/
|
|
function createPriorityBar(barDistribution) {
|
|
let bar = '';
|
|
|
|
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])(
|
|
'█'.repeat(barDistribution[PRIORITIES.HIGH])
|
|
);
|
|
bar += chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])(
|
|
'█'.repeat(barDistribution[PRIORITIES.MEDIUM])
|
|
);
|
|
bar += chalk.yellow('█'.repeat(barDistribution[PRIORITIES.LOW]));
|
|
|
|
const totalChars = Object.values(barDistribution).reduce(
|
|
(sum, val) => sum + val,
|
|
0
|
|
);
|
|
if (totalChars < CONSTANTS.BAR_WIDTH) {
|
|
bar += chalk.gray('░'.repeat(CONSTANTS.BAR_WIDTH - totalChars));
|
|
}
|
|
|
|
return bar;
|
|
}
|
|
|
|
/**
|
|
* Build priority distribution row for table
|
|
* @param {Object} priorityStats - Priority statistics
|
|
* @returns {Array} Table row for priority distribution
|
|
*/
|
|
function buildPriorityRow(priorityStats) {
|
|
const parts = [];
|
|
|
|
Object.entries(PRIORITIES).forEach(([key, priority]) => {
|
|
const stats = priorityStats[priority];
|
|
const color =
|
|
priority === PRIORITIES.HIGH
|
|
? chalk.hex(PRIORITY_COLORS[PRIORITIES.HIGH])
|
|
: priority === PRIORITIES.MEDIUM
|
|
? chalk.hex(PRIORITY_COLORS[PRIORITIES.MEDIUM])
|
|
: chalk.yellow;
|
|
|
|
const label = key.charAt(0) + key.slice(1).toLowerCase();
|
|
parts.push(
|
|
`${color.bold(stats.count)} ${color(label)} (${stats.percentage}%)`
|
|
);
|
|
});
|
|
|
|
return [chalk.cyan('Priority distribution:'), parts.join(' · ')];
|
|
}
|
|
|
|
/**
|
|
* Display a summary of the PRD parsing results
|
|
* @param {Object} summary - Summary of the parsing results
|
|
* @param {number} summary.totalTasks - Total number of tasks generated
|
|
* @param {string} summary.prdFilePath - Path to the PRD file
|
|
* @param {string} summary.outputPath - Path where the tasks were saved
|
|
* @param {number} summary.elapsedTime - Total elapsed time in seconds
|
|
* @param {Object} summary.taskPriorities - Breakdown of tasks by category/priority
|
|
* @param {boolean} summary.usedFallback - Whether fallback parsing was used
|
|
* @param {string} summary.actionVerb - Whether tasks were 'generated' or 'appended'
|
|
*/
|
|
function displayParsePrdSummary(summary) {
|
|
const {
|
|
totalTasks,
|
|
taskPriorities = {},
|
|
prdFilePath,
|
|
outputPath,
|
|
elapsedTime,
|
|
usedFallback = false,
|
|
actionVerb = 'generated'
|
|
} = summary;
|
|
|
|
// Format the elapsed time
|
|
const timeDisplay = formatElapsedTime(elapsedTime);
|
|
|
|
// Create a table for better alignment
|
|
const table = new Table({
|
|
chars: {
|
|
top: '',
|
|
'top-mid': '',
|
|
'top-left': '',
|
|
'top-right': '',
|
|
bottom: '',
|
|
'bottom-mid': '',
|
|
'bottom-left': '',
|
|
'bottom-right': '',
|
|
left: '',
|
|
'left-mid': '',
|
|
mid: '',
|
|
'mid-mid': '',
|
|
right: '',
|
|
'right-mid': '',
|
|
middle: ' '
|
|
},
|
|
style: { border: [], 'padding-left': 2 },
|
|
colWidths: CONSTANTS.TABLE_COL_WIDTHS
|
|
});
|
|
|
|
// Basic info
|
|
// Use the action verb to properly display if tasks were generated or appended
|
|
table.push(
|
|
[chalk.cyan(`Total tasks ${actionVerb}:`), chalk.bold(totalTasks)],
|
|
[chalk.cyan('Processing time:'), chalk.bold(timeDisplay)]
|
|
);
|
|
|
|
// Priority distribution if available
|
|
if (taskPriorities && Object.keys(taskPriorities).length > 0) {
|
|
const priorityStats = calculatePriorityStats(taskPriorities, totalTasks);
|
|
const priorityRow = buildPriorityRow(priorityStats);
|
|
table.push(priorityRow);
|
|
|
|
// Visual bar representation
|
|
const barDistribution = calculateBarDistribution(priorityStats, totalTasks);
|
|
const distributionBar = createPriorityBar(barDistribution);
|
|
table.push([chalk.cyan('Distribution:'), distributionBar]);
|
|
}
|
|
|
|
// Add file paths
|
|
table.push(
|
|
[chalk.cyan('PRD source:'), chalk.italic(prdFilePath)],
|
|
[chalk.cyan('Tasks file:'), chalk.italic(outputPath)]
|
|
);
|
|
|
|
// Add fallback parsing indicator if applicable
|
|
if (usedFallback) {
|
|
table.push([
|
|
chalk.yellow('Fallback parsing:'),
|
|
chalk.yellow('✓ Used fallback parsing')
|
|
]);
|
|
}
|
|
|
|
// Final string output with title and footer
|
|
const output = [
|
|
chalk.bold.underline(
|
|
`PRD Parsing Complete - Tasks ${actionVerb.charAt(0).toUpperCase() + actionVerb.slice(1)}`
|
|
),
|
|
'',
|
|
table.toString()
|
|
].join('\n');
|
|
|
|
// Display the summary box
|
|
console.log(boxen(output, BOX_STYLES.summary));
|
|
|
|
// Show fallback parsing warning if needed
|
|
if (usedFallback) {
|
|
displayFallbackWarning();
|
|
}
|
|
|
|
// Show next steps
|
|
displayNextSteps();
|
|
}
|
|
|
|
/**
|
|
* Display fallback parsing warning
|
|
*/
|
|
function displayFallbackWarning() {
|
|
const warningContent =
|
|
chalk.yellow.bold('⚠️ Fallback Parsing Used') +
|
|
'\n\n' +
|
|
chalk.white(
|
|
'The system used fallback parsing to complete task generation.'
|
|
) +
|
|
'\n' +
|
|
chalk.white(
|
|
'This typically happens when streaming JSON parsing is incomplete.'
|
|
) +
|
|
'\n' +
|
|
chalk.white('Your tasks were successfully generated, but consider:') +
|
|
'\n' +
|
|
chalk.white('• Reviewing task completeness') +
|
|
'\n' +
|
|
chalk.white('• Checking for any missing details') +
|
|
'\n\n' +
|
|
chalk.white("This is normal and usually doesn't indicate any issues.");
|
|
|
|
console.log(boxen(warningContent, BOX_STYLES.warning));
|
|
}
|
|
|
|
/**
|
|
* Display next steps after parsing
|
|
*/
|
|
function displayNextSteps() {
|
|
const stepsContent =
|
|
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\n` +
|
|
`${chalk.cyan('3.')} Run ${chalk.yellow('task-master analyze-complexity')} to analyze task complexity`;
|
|
|
|
console.log(boxen(stepsContent, BOX_STYLES.nextSteps));
|
|
}
|
|
|
|
export { displayParsePrdStart, displayParsePrdSummary, formatElapsedTime };
|