Files
claude-task-master/mcp-server/src/core/direct-functions/parse-prd.js
Joe Danziger e3ed4d7c14 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>
2025-08-12 22:37:07 +02:00

217 lines
6.0 KiB
JavaScript

/**
* parse-prd.js
* Direct function implementation for parsing PRD documents
*/
import path from 'path';
import fs from 'fs';
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
import {
enableSilentMode,
disableSilentMode,
isSilentMode
} from '../../../../scripts/modules/utils.js';
import { createLogWrapper } from '../../tools/utils.js';
import { getDefaultNumTasks } from '../../../../scripts/modules/config-manager.js';
import { resolvePrdPath, resolveProjectPath } from '../utils/path-utils.js';
import { TASKMASTER_TASKS_FILE } from '../../../../src/constants/paths.js';
/**
* Direct function wrapper for parsing PRD documents and generating tasks.
*
* @param {Object} args - Command arguments containing projectRoot, input, output, numTasks options.
* @param {string} args.input - Path to the input PRD file.
* @param {string} args.output - Path to the output directory.
* @param {string} args.numTasks - Number of tasks to generate.
* @param {boolean} args.force - Whether to force parsing.
* @param {boolean} args.append - Whether to append to the output file.
* @param {boolean} args.research - Whether to use research mode.
* @param {string} args.tag - Tag context for organizing tasks into separate task lists.
* @param {Object} log - Logger object.
* @param {Object} context - Context object containing session data.
* @returns {Promise<Object>} - Result object with success status and data/error information.
*/
export async function parsePRDDirect(args, log, context = {}) {
const { session, reportProgress } = context;
// Extract projectRoot from args
const {
input: inputArg,
output: outputArg,
numTasks: numTasksArg,
force,
append,
research,
projectRoot,
tag
} = args;
// Create the standard logger wrapper
const logWrapper = createLogWrapper(log);
// --- Input Validation and Path Resolution ---
if (!projectRoot) {
logWrapper.error('parsePRDDirect requires a projectRoot argument.');
return {
success: false,
error: {
code: 'MISSING_ARGUMENT',
message: 'projectRoot is required.'
}
};
}
// Resolve input path using path utilities
let inputPath;
if (inputArg) {
try {
inputPath = resolvePrdPath({ input: inputArg, projectRoot }, session);
} catch (error) {
logWrapper.error(`Error resolving PRD path: ${error.message}`);
return {
success: false,
error: { code: 'FILE_NOT_FOUND', message: error.message }
};
}
} else {
logWrapper.error('parsePRDDirect called without input path');
return {
success: false,
error: { code: 'MISSING_ARGUMENT', message: 'Input path is required' }
};
}
// Resolve output path - use new path utilities for default
const outputPath = outputArg
? path.isAbsolute(outputArg)
? outputArg
: path.resolve(projectRoot, outputArg)
: resolveProjectPath(TASKMASTER_TASKS_FILE, args) ||
path.resolve(projectRoot, TASKMASTER_TASKS_FILE);
// Check if input file exists
if (!fs.existsSync(inputPath)) {
const errorMsg = `Input PRD file not found at resolved path: ${inputPath}`;
logWrapper.error(errorMsg);
return {
success: false,
error: { code: 'FILE_NOT_FOUND', message: errorMsg }
};
}
const outputDir = path.dirname(outputPath);
try {
if (!fs.existsSync(outputDir)) {
logWrapper.info(`Creating output directory: ${outputDir}`);
fs.mkdirSync(outputDir, { recursive: true });
}
} catch (error) {
const errorMsg = `Failed to create output directory ${outputDir}: ${error.message}`;
logWrapper.error(errorMsg);
return {
success: false,
error: { code: 'DIRECTORY_CREATE_FAILED', message: errorMsg }
};
}
let numTasks = getDefaultNumTasks(projectRoot);
if (numTasksArg) {
numTasks =
typeof numTasksArg === 'string' ? parseInt(numTasksArg, 10) : numTasksArg;
if (Number.isNaN(numTasks) || numTasks < 0) {
// Ensure positive number
numTasks = getDefaultNumTasks(projectRoot); // Fallback to default if parsing fails or invalid
logWrapper.warn(
`Invalid numTasks value: ${numTasksArg}. Using default: ${numTasks}`
);
}
}
if (append) {
logWrapper.info('Append mode enabled.');
if (force) {
logWrapper.warn(
'Both --force and --append flags were provided. --force takes precedence; append mode will be ignored.'
);
}
}
if (research) {
logWrapper.info(
'Research mode enabled. Using Perplexity AI for enhanced PRD analysis.'
);
}
logWrapper.info(
`Parsing PRD via direct function. Input: ${inputPath}, Output: ${outputPath}, NumTasks: ${numTasks}, Force: ${force}, Append: ${append}, Research: ${research}, ProjectRoot: ${projectRoot}`
);
const wasSilent = isSilentMode();
if (!wasSilent) {
enableSilentMode();
}
try {
// Call the core parsePRD function
const result = await parsePRD(
inputPath,
outputPath,
numTasks,
{
session,
mcpLog: logWrapper,
projectRoot,
tag,
force,
append,
research,
reportProgress,
commandName: 'parse-prd',
outputType: 'mcp'
},
'json'
);
// Adjust check for the new return structure
if (result && result.success) {
const successMsg = `Successfully parsed PRD and generated tasks in ${result.tasksPath}`;
logWrapper.success(successMsg);
return {
success: true,
data: {
message: successMsg,
outputPath: result.tasksPath,
telemetryData: result.telemetryData,
tagInfo: result.tagInfo
}
};
} else {
// Handle case where core function didn't return expected success structure
logWrapper.error(
'Core parsePRD function did not return a successful structure.'
);
return {
success: false,
error: {
code: 'CORE_FUNCTION_ERROR',
message:
result?.message ||
'Core function failed to parse PRD or returned unexpected result.'
}
};
}
} catch (error) {
logWrapper.error(`Error executing core parsePRD: ${error.message}`);
return {
success: false,
error: {
code: 'PARSE_PRD_CORE_ERROR',
message: error.message || 'Unknown error parsing PRD'
}
};
} finally {
if (!wasSilent && isSilentMode()) {
disableSilentMode();
}
}
}