fix(parse-prd): pass projectRoot and fix schema/logging
Modified parse-prd core, direct function, and tool to pass projectRoot for .env API key fallback. Corrected Zod schema used in generateObjectService call. Fixed logFn reference error in core parsePRD. Updated unit test mock for utils.js.
This commit is contained in:
@@ -8,9 +8,11 @@ import fs from 'fs';
|
||||
import { parsePRD } from '../../../../scripts/modules/task-manager.js';
|
||||
import {
|
||||
enableSilentMode,
|
||||
disableSilentMode
|
||||
disableSilentMode,
|
||||
isSilentMode
|
||||
} from '../../../../scripts/modules/utils.js';
|
||||
import { createLogWrapper } from '../../tools/utils.js';
|
||||
import { getDefaultNumTasks } from '../../../../scripts/modules/config-manager.js';
|
||||
|
||||
/**
|
||||
* Direct function wrapper for parsing PRD documents and generating tasks.
|
||||
@@ -21,177 +23,160 @@ import { createLogWrapper } from '../../tools/utils.js';
|
||||
* @returns {Promise<Object>} - Result object with success status and data/error information.
|
||||
*/
|
||||
export async function parsePRDDirect(args, log, context = {}) {
|
||||
const { session } = context; // Only extract session
|
||||
const { session } = context;
|
||||
// Extract projectRoot from args
|
||||
const {
|
||||
input: inputArg,
|
||||
output: outputArg,
|
||||
numTasks: numTasksArg,
|
||||
force,
|
||||
append,
|
||||
projectRoot
|
||||
} = args;
|
||||
|
||||
try {
|
||||
log.info(`Parsing PRD document with args: ${JSON.stringify(args)}`);
|
||||
const logWrapper = createLogWrapper(log);
|
||||
|
||||
// Validate required parameters
|
||||
if (!args.projectRoot) {
|
||||
const errorMessage = 'Project root is required for parsePRDDirect';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_PROJECT_ROOT', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
if (!args.input) {
|
||||
const errorMessage = 'Input file path is required for parsePRDDirect';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_INPUT_PATH', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
if (!args.output) {
|
||||
const errorMessage = 'Output file path is required for parsePRDDirect';
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_OUTPUT_PATH', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve input path (expecting absolute path or path relative to project root)
|
||||
const projectRoot = args.projectRoot;
|
||||
const inputPath = path.isAbsolute(args.input)
|
||||
? args.input
|
||||
: path.resolve(projectRoot, args.input);
|
||||
|
||||
// Verify input file exists
|
||||
if (!fs.existsSync(inputPath)) {
|
||||
const errorMessage = `Input file not found: ${inputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'INPUT_FILE_NOT_FOUND',
|
||||
message: errorMessage,
|
||||
details: `Checked path: ${inputPath}\nProject root: ${projectRoot}\nInput argument: ${args.input}`
|
||||
},
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve output path (expecting absolute path or path relative to project root)
|
||||
const outputPath = path.isAbsolute(args.output)
|
||||
? args.output
|
||||
: path.resolve(projectRoot, args.output);
|
||||
|
||||
// Ensure output directory exists
|
||||
const outputDir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
log.info(`Creating output directory: ${outputDir}`);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Parse number of tasks - handle both string and number values
|
||||
let numTasks = 10; // Default
|
||||
if (args.numTasks) {
|
||||
numTasks =
|
||||
typeof args.numTasks === 'string'
|
||||
? parseInt(args.numTasks, 10)
|
||||
: args.numTasks;
|
||||
if (isNaN(numTasks)) {
|
||||
numTasks = 10; // Fallback to default if parsing fails
|
||||
log.warn(`Invalid numTasks value: ${args.numTasks}. Using default: 10`);
|
||||
}
|
||||
}
|
||||
|
||||
// Extract the append flag from args
|
||||
const append = Boolean(args.append) === true;
|
||||
|
||||
// Log key parameters including append flag
|
||||
log.info(
|
||||
`Preparing to parse PRD from ${inputPath} and output to ${outputPath} with ${numTasks} tasks, append mode: ${append}`
|
||||
// --- Input Validation and Path Resolution ---
|
||||
if (!projectRoot || !path.isAbsolute(projectRoot)) {
|
||||
logWrapper.error(
|
||||
'parsePRDDirect requires an absolute projectRoot argument.'
|
||||
);
|
||||
|
||||
// --- Logger Wrapper ---
|
||||
const mcpLog = createLogWrapper(log);
|
||||
|
||||
// Prepare options for the core function
|
||||
const options = {
|
||||
mcpLog,
|
||||
session
|
||||
};
|
||||
|
||||
// Enable silent mode to prevent console logs from interfering with JSON response
|
||||
enableSilentMode();
|
||||
try {
|
||||
// Make sure the output directory exists
|
||||
const outputDir = path.dirname(outputPath);
|
||||
if (!fs.existsSync(outputDir)) {
|
||||
log.info(`Creating output directory: ${outputDir}`);
|
||||
fs.mkdirSync(outputDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Execute core parsePRD function with AI client
|
||||
const tasksDataResult = await parsePRD(
|
||||
inputPath,
|
||||
outputPath,
|
||||
numTasks,
|
||||
{
|
||||
mcpLog: logWrapper,
|
||||
session,
|
||||
append
|
||||
},
|
||||
aiClient,
|
||||
modelConfig
|
||||
);
|
||||
|
||||
// Since parsePRD doesn't return a value but writes to a file, we'll read the result
|
||||
// to return it to the caller
|
||||
if (fs.existsSync(outputPath)) {
|
||||
const tasksData = JSON.parse(fs.readFileSync(outputPath, 'utf8'));
|
||||
const actionVerb = append ? 'appended' : 'generated';
|
||||
const message = `Successfully ${actionVerb} ${tasksData.tasks?.length || 0} tasks from PRD`;
|
||||
|
||||
if (!tasksDataResult || !tasksDataResult.tasks || !tasksData) {
|
||||
throw new Error(
|
||||
'Core parsePRD function did not return valid task data.'
|
||||
);
|
||||
}
|
||||
|
||||
log.info(message);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message,
|
||||
taskCount: tasksDataResult.tasks?.length || 0,
|
||||
outputPath,
|
||||
appended: append
|
||||
},
|
||||
fromCache: false // This operation always modifies state and should never be cached
|
||||
};
|
||||
} else {
|
||||
const errorMessage = `Tasks file was not created at ${outputPath}`;
|
||||
log.error(errorMessage);
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'OUTPUT_FILE_NOT_CREATED', message: errorMessage },
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
} finally {
|
||||
// Always restore normal logging
|
||||
disableSilentMode();
|
||||
}
|
||||
} catch (error) {
|
||||
// Make sure to restore normal logging even if there's an error
|
||||
disableSilentMode();
|
||||
|
||||
log.error(`Error parsing PRD: ${error.message}`);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: error.code || 'PARSE_PRD_ERROR', // Use error code if available
|
||||
message: error.message || 'Unknown error parsing PRD'
|
||||
},
|
||||
fromCache: false
|
||||
code: 'MISSING_ARGUMENT',
|
||||
message: 'projectRoot is required and must be absolute.'
|
||||
}
|
||||
};
|
||||
}
|
||||
if (!inputArg) {
|
||||
logWrapper.error('parsePRDDirect called without input path');
|
||||
return {
|
||||
success: false,
|
||||
error: { code: 'MISSING_ARGUMENT', message: 'Input path is required' }
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve input and output paths relative to projectRoot if they aren't absolute
|
||||
const inputPath = path.resolve(projectRoot, inputArg);
|
||||
const outputPath = outputArg
|
||||
? path.resolve(projectRoot, outputArg)
|
||||
: path.resolve(projectRoot, 'tasks', 'tasks.json'); // Default output path
|
||||
|
||||
// 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 (dirError) {
|
||||
logWrapper.error(
|
||||
`Failed to create output directory ${outputDir}: ${dirError.message}`
|
||||
);
|
||||
// Return an error response immediately if dir creation fails
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
code: 'DIRECTORY_CREATION_ERROR',
|
||||
message: `Failed to create output directory: ${dirError.message}`
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let numTasks = getDefaultNumTasks(projectRoot);
|
||||
if (numTasksArg) {
|
||||
numTasks =
|
||||
typeof numTasksArg === 'string' ? parseInt(numTasksArg, 10) : numTasksArg;
|
||||
if (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: 10`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const useForce = force === true;
|
||||
const useAppend = append === true;
|
||||
if (useAppend) {
|
||||
logWrapper.info('Append mode enabled.');
|
||||
if (useForce) {
|
||||
logWrapper.warn(
|
||||
'Both --force and --append flags were provided. --force takes precedence; append mode will be ignored.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
logWrapper.info(
|
||||
`Parsing PRD via direct function. Input: ${inputPath}, Output: ${outputPath}, NumTasks: ${numTasks}, Force: ${useForce}, Append: ${useAppend}, 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, useForce, useAppend },
|
||||
'json'
|
||||
);
|
||||
|
||||
// parsePRD returns { success: true, tasks: processedTasks } on success
|
||||
if (result && result.success && Array.isArray(result.tasks)) {
|
||||
logWrapper.success(
|
||||
`Successfully parsed PRD. Generated ${result.tasks.length} tasks.`
|
||||
);
|
||||
return {
|
||||
success: true,
|
||||
data: {
|
||||
message: `Successfully parsed PRD and generated ${result.tasks.length} tasks.`,
|
||||
outputPath: outputPath,
|
||||
taskCount: result.tasks.length
|
||||
// Optionally include tasks if needed by client: tasks: result.tasks
|
||||
}
|
||||
};
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user