feat: CLI & MCP progress tracking for parse-prd command (#1048)
* 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>
This commit is contained in:
272
scripts/modules/task-manager/parse-prd/parse-prd.js
Normal file
272
scripts/modules/task-manager/parse-prd/parse-prd.js
Normal file
@@ -0,0 +1,272 @@
|
||||
import chalk from 'chalk';
|
||||
import {
|
||||
StreamingError,
|
||||
STREAMING_ERROR_CODES
|
||||
} from '../../../../src/utils/stream-parser.js';
|
||||
import { TimeoutManager } from '../../../../src/utils/timeout-manager.js';
|
||||
import { getDebugFlag, getDefaultPriority } from '../../config-manager.js';
|
||||
|
||||
// Import configuration classes
|
||||
import { PrdParseConfig, LoggingConfig } from './parse-prd-config.js';
|
||||
|
||||
// Import helper functions
|
||||
import {
|
||||
readPrdContent,
|
||||
loadExistingTasks,
|
||||
validateFileOperations,
|
||||
processTasks,
|
||||
saveTasksToFile,
|
||||
buildPrompts,
|
||||
displayCliSummary,
|
||||
displayNonStreamingCliOutput
|
||||
} from './parse-prd-helpers.js';
|
||||
|
||||
// Import handlers
|
||||
import { handleStreamingService } from './parse-prd-streaming.js';
|
||||
import { handleNonStreamingService } from './parse-prd-non-streaming.js';
|
||||
|
||||
// ============================================================================
|
||||
// MAIN PARSING FUNCTIONS (Simplified after refactoring)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Shared parsing logic for both streaming and non-streaming
|
||||
* @param {PrdParseConfig} config - Configuration object
|
||||
* @param {Function} serviceHandler - Handler function for AI service
|
||||
* @param {boolean} isStreaming - Whether this is streaming mode
|
||||
* @returns {Promise<Object>} Result object with success status and telemetry
|
||||
*/
|
||||
async function parsePRDCore(config, serviceHandler, isStreaming) {
|
||||
const logger = new LoggingConfig(config.mcpLog, config.reportProgress);
|
||||
|
||||
logger.report(
|
||||
`Parsing PRD file: ${config.prdPath}, Force: ${config.force}, Append: ${config.append}, Research: ${config.research}`,
|
||||
'debug'
|
||||
);
|
||||
|
||||
try {
|
||||
// Load existing tasks
|
||||
const { existingTasks, nextId } = loadExistingTasks(
|
||||
config.tasksPath,
|
||||
config.targetTag
|
||||
);
|
||||
|
||||
// Validate operations
|
||||
validateFileOperations({
|
||||
existingTasks,
|
||||
targetTag: config.targetTag,
|
||||
append: config.append,
|
||||
force: config.force,
|
||||
isMCP: config.isMCP,
|
||||
logger
|
||||
});
|
||||
|
||||
// Read PRD content and build prompts
|
||||
const prdContent = readPrdContent(config.prdPath);
|
||||
const prompts = await buildPrompts(config, prdContent, nextId);
|
||||
|
||||
// Call the appropriate service handler
|
||||
const serviceResult = await serviceHandler(
|
||||
config,
|
||||
prompts,
|
||||
config.numTasks
|
||||
);
|
||||
|
||||
// Process tasks
|
||||
const defaultPriority = getDefaultPriority(config.projectRoot) || 'medium';
|
||||
const processedNewTasks = processTasks(
|
||||
serviceResult.parsedTasks,
|
||||
nextId,
|
||||
existingTasks,
|
||||
defaultPriority
|
||||
);
|
||||
|
||||
// Combine with existing if appending
|
||||
const finalTasks = config.append
|
||||
? [...existingTasks, ...processedNewTasks]
|
||||
: processedNewTasks;
|
||||
|
||||
// Save to file
|
||||
saveTasksToFile(config.tasksPath, finalTasks, config.targetTag, logger);
|
||||
|
||||
// Handle completion reporting
|
||||
await handleCompletionReporting(
|
||||
config,
|
||||
serviceResult,
|
||||
processedNewTasks,
|
||||
finalTasks,
|
||||
nextId,
|
||||
isStreaming
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
tasksPath: config.tasksPath,
|
||||
telemetryData: serviceResult.aiServiceResponse?.telemetryData,
|
||||
tagInfo: serviceResult.aiServiceResponse?.tagInfo
|
||||
};
|
||||
} catch (error) {
|
||||
logger.report(`Error parsing PRD: ${error.message}`, 'error');
|
||||
|
||||
if (!config.isMCP) {
|
||||
console.error(chalk.red(`Error: ${error.message}`));
|
||||
if (getDebugFlag(config.projectRoot)) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle completion reporting for both CLI and MCP
|
||||
* @param {PrdParseConfig} config - Configuration object
|
||||
* @param {Object} serviceResult - Result from service handler
|
||||
* @param {Array} processedNewTasks - New tasks that were processed
|
||||
* @param {Array} finalTasks - All tasks after processing
|
||||
* @param {number} nextId - Next available task ID
|
||||
* @param {boolean} isStreaming - Whether this was streaming mode
|
||||
*/
|
||||
async function handleCompletionReporting(
|
||||
config,
|
||||
serviceResult,
|
||||
processedNewTasks,
|
||||
finalTasks,
|
||||
nextId,
|
||||
isStreaming
|
||||
) {
|
||||
const { aiServiceResponse, estimatedInputTokens, estimatedOutputTokens } =
|
||||
serviceResult;
|
||||
|
||||
// MCP progress reporting
|
||||
if (config.reportProgress) {
|
||||
const hasValidTelemetry =
|
||||
aiServiceResponse?.telemetryData &&
|
||||
(aiServiceResponse.telemetryData.inputTokens > 0 ||
|
||||
aiServiceResponse.telemetryData.outputTokens > 0);
|
||||
|
||||
let completionMessage;
|
||||
if (hasValidTelemetry) {
|
||||
const cost = aiServiceResponse.telemetryData.totalCost || 0;
|
||||
const currency = aiServiceResponse.telemetryData.currency || 'USD';
|
||||
completionMessage = `✅ Task Generation Completed | Tokens (I/O): ${aiServiceResponse.telemetryData.inputTokens}/${aiServiceResponse.telemetryData.outputTokens} | Cost: ${currency === 'USD' ? '$' : currency}${cost.toFixed(4)}`;
|
||||
} else {
|
||||
const outputTokens = isStreaming ? estimatedOutputTokens : 'unknown';
|
||||
completionMessage = `✅ Task Generation Completed | ~Tokens (I/O): ${estimatedInputTokens}/${outputTokens} | Cost: ~$0.00`;
|
||||
}
|
||||
|
||||
await config.reportProgress({
|
||||
progress: config.numTasks,
|
||||
total: config.numTasks,
|
||||
message: completionMessage
|
||||
});
|
||||
}
|
||||
|
||||
// CLI output
|
||||
if (config.outputFormat === 'text' && !config.isMCP) {
|
||||
if (isStreaming && serviceResult.summary) {
|
||||
await displayCliSummary({
|
||||
processedTasks: processedNewTasks,
|
||||
nextId,
|
||||
summary: serviceResult.summary,
|
||||
prdPath: config.prdPath,
|
||||
tasksPath: config.tasksPath,
|
||||
usedFallback: serviceResult.usedFallback,
|
||||
aiServiceResponse
|
||||
});
|
||||
} else if (!isStreaming) {
|
||||
displayNonStreamingCliOutput({
|
||||
processedTasks: processedNewTasks,
|
||||
research: config.research,
|
||||
finalTasks,
|
||||
tasksPath: config.tasksPath,
|
||||
aiServiceResponse
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PRD with streaming progress reporting
|
||||
*/
|
||||
async function parsePRDWithStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options = {}
|
||||
) {
|
||||
const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
|
||||
return parsePRDCore(config, handleStreamingService, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PRD without streaming (fallback)
|
||||
*/
|
||||
async function parsePRDWithoutStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options = {}
|
||||
) {
|
||||
const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
|
||||
return parsePRDCore(config, handleNonStreamingService, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Main entry point - decides between streaming and non-streaming
|
||||
*/
|
||||
async function parsePRD(prdPath, tasksPath, numTasks, options = {}) {
|
||||
const config = new PrdParseConfig(prdPath, tasksPath, numTasks, options);
|
||||
|
||||
if (config.useStreaming) {
|
||||
try {
|
||||
return await parsePRDWithStreaming(prdPath, tasksPath, numTasks, options);
|
||||
} catch (streamingError) {
|
||||
// Check if this is a streaming-specific error (including timeout)
|
||||
const isStreamingError =
|
||||
streamingError instanceof StreamingError ||
|
||||
streamingError.code === STREAMING_ERROR_CODES.NOT_ASYNC_ITERABLE ||
|
||||
streamingError.code ===
|
||||
STREAMING_ERROR_CODES.STREAM_PROCESSING_FAILED ||
|
||||
streamingError.code === STREAMING_ERROR_CODES.STREAM_NOT_ITERABLE ||
|
||||
TimeoutManager.isTimeoutError(streamingError);
|
||||
|
||||
if (isStreamingError) {
|
||||
const logger = new LoggingConfig(config.mcpLog, config.reportProgress);
|
||||
|
||||
// Show fallback message
|
||||
if (config.outputFormat === 'text' && !config.isMCP) {
|
||||
console.log(
|
||||
chalk.yellow(
|
||||
`⚠️ Streaming operation ${streamingError.message.includes('timed out') ? 'timed out' : 'failed'}. Falling back to non-streaming mode...`
|
||||
)
|
||||
);
|
||||
} else {
|
||||
logger.report(
|
||||
`Streaming failed (${streamingError.message}), falling back to non-streaming mode...`,
|
||||
'warn'
|
||||
);
|
||||
}
|
||||
|
||||
// Fallback to non-streaming
|
||||
return await parsePRDWithoutStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options
|
||||
);
|
||||
} else {
|
||||
throw streamingError;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return await parsePRDWithoutStreaming(
|
||||
prdPath,
|
||||
tasksPath,
|
||||
numTasks,
|
||||
options
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default parsePRD;
|
||||
Reference in New Issue
Block a user