diff --git a/scripts/modules/commands.js b/scripts/modules/commands.js index 8b653463..65970c8a 100644 --- a/scripts/modules/commands.js +++ b/scripts/modules/commands.js @@ -18,7 +18,6 @@ import { log, readJSON, writeJSON, - findProjectRoot, getCurrentTag, detectCamelCaseFlags, toKebabCase @@ -79,11 +78,12 @@ import { CUSTOM_PROVIDERS } from '../../src/constants/providers.js'; import { COMPLEXITY_REPORT_FILE, - PRD_FILE, TASKMASTER_TASKS_FILE, - TASKMASTER_CONFIG_FILE + TASKMASTER_DOCS_DIR } from '../../src/constants/paths.js'; +import { initTaskMaster } from '../../src/task-master.js'; + import { displayBanner, displayHelp, @@ -822,25 +822,34 @@ function registerCommands(programInstance) { ) .option('--tag ', 'Specify tag context for task operations') .action(async (file, options) => { - // Use input option if file argument not provided - const inputFile = file || options.input; - const defaultPrdPath = PRD_FILE; + // Initialize TaskMaster + let taskMaster; + try { + taskMaster = initTaskMaster({ + prdPath: file || options.input || true, + tasksPath: options.output || true + }); + } catch (error) { + console.log( + boxen( + `${chalk.white.bold('Parse PRD Help')}\n\n${chalk.cyan('Usage:')}\n task-master parse-prd [options]\n\n${chalk.cyan('Options:')}\n -i, --input Path to the PRD file (alternative to positional argument)\n -o, --output Output file path (default: "${TASKMASTER_TASKS_FILE}")\n -n, --num-tasks Number of tasks to generate (default: 10)\n -f, --force Skip confirmation when overwriting existing tasks\n --append Append new tasks to existing tasks.json instead of overwriting\n -r, --research Use Perplexity AI for research-backed task generation\n\n${chalk.cyan('Example:')}\n task-master parse-prd requirements.txt --num-tasks 15\n task-master parse-prd --input=requirements.txt\n task-master parse-prd --force\n task-master parse-prd requirements_v2.txt --append\n task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n 1. Look for a PRD file at ${TASKMASTER_DOCS_DIR}/PRD.md by default\n 2. Use the file specified by --input or positional argument if provided\n 3. Generate tasks from the PRD and either:\n - Overwrite any existing tasks.json file (default)\n - Append to existing tasks.json if --append is used`, + { padding: 1, borderColor: 'blue', borderStyle: 'round' } + ) + ); + console.error(chalk.red(`\nError: ${error.message}`)); + process.exit(1); + } + const numTasks = parseInt(options.numTasks, 10); - const outputPath = options.output; const force = options.force || false; const append = options.append || false; const research = options.research || false; let useForce = force; const useAppend = append; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -849,10 +858,11 @@ function registerCommands(programInstance) { async function confirmOverwriteIfNeeded() { // Check if there are existing tasks in the target tag let hasExistingTasksInTag = false; - if (fs.existsSync(outputPath)) { + const tasksPath = taskMaster.getTasksPath(); + if (fs.existsSync(tasksPath)) { try { // Read the entire file to check if the tag exists - const existingFileContent = fs.readFileSync(outputPath, 'utf8'); + const existingFileContent = fs.readFileSync(tasksPath, 'utf8'); const allData = JSON.parse(existingFileContent); // Check if the target tag exists and has tasks @@ -871,7 +881,7 @@ function registerCommands(programInstance) { // Only show confirmation if there are existing tasks in the target tag if (hasExistingTasksInTag && !useForce && !useAppend) { - const overwrite = await confirmTaskOverwrite(outputPath); + const overwrite = await confirmTaskOverwrite(tasksPath); if (!overwrite) { log('info', 'Operation cancelled.'); return false; @@ -886,50 +896,9 @@ function registerCommands(programInstance) { let spinner; try { - if (!inputFile) { - if (fs.existsSync(defaultPrdPath)) { - console.log( - chalk.blue(`Using default PRD file path: ${defaultPrdPath}`) - ); - if (!(await confirmOverwriteIfNeeded())) return; - - console.log(chalk.blue(`Generating ${numTasks} tasks...`)); - spinner = ora('Parsing PRD and generating tasks...\n').start(); - await parsePRD(defaultPrdPath, outputPath, numTasks, { - append: useAppend, // Changed key from useAppend to append - force: useForce, // Changed key from useForce to force - research: research, - projectRoot: projectRoot, - tag: tag - }); - spinner.succeed('Tasks generated successfully!'); - return; - } - - console.log( - chalk.yellow( - `No PRD file specified and default PRD file not found at ${PRD_FILE}.` - ) - ); - console.log( - boxen( - `${chalk.white.bold('Parse PRD Help')}\n\n${chalk.cyan('Usage:')}\n task-master parse-prd [options]\n\n${chalk.cyan('Options:')}\n -i, --input Path to the PRD file (alternative to positional argument)\n -o, --output Output file path (default: "${TASKMASTER_TASKS_FILE}")\n -n, --num-tasks Number of tasks to generate (default: 10)\n -f, --force Skip confirmation when overwriting existing tasks\n --append Append new tasks to existing tasks.json instead of overwriting\n -r, --research Use Perplexity AI for research-backed task generation\n\n${chalk.cyan('Example:')}\n task-master parse-prd requirements.txt --num-tasks 15\n task-master parse-prd --input=requirements.txt\n task-master parse-prd --force\n task-master parse-prd requirements_v2.txt --append\n task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n 1. Look for a PRD file at ${PRD_FILE} by default\n 2. Use the file specified by --input or positional argument if provided\n 3. Generate tasks from the PRD and either:\n - Overwrite any existing tasks.json file (default)\n - Append to existing tasks.json if --append is used`, - { padding: 1, borderColor: 'blue', borderStyle: 'round' } - ) - ); - return; - } - - if (!fs.existsSync(inputFile)) { - console.error( - chalk.red(`Error: Input PRD file not found: ${inputFile}`) - ); - process.exit(1); - } - if (!(await confirmOverwriteIfNeeded())) return; - console.log(chalk.blue(`Parsing PRD file: ${inputFile}`)); + console.log(chalk.blue(`Parsing PRD file: ${taskMaster.getPrdPath()}`)); console.log(chalk.blue(`Generating ${numTasks} tasks...`)); if (append) { console.log(chalk.blue('Appending to existing tasks...')); @@ -943,13 +912,18 @@ function registerCommands(programInstance) { } spinner = ora('Parsing PRD and generating tasks...\n').start(); - await parsePRD(inputFile, outputPath, numTasks, { - append: useAppend, - force: useForce, - research: research, - projectRoot: projectRoot, - tag: tag - }); + await parsePRD( + taskMaster.getPrdPath(), + taskMaster.getTasksPath(), + numTasks, + { + append: useAppend, + force: useForce, + research: research, + projectRoot: taskMaster.getProjectRoot(), + tag: tag + } + ); spinner.succeed('Tasks generated successfully!'); } catch (error) { if (spinner) { @@ -987,19 +961,18 @@ function registerCommands(programInstance) { ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const fromId = parseInt(options.from, 10); // Validation happens here const prompt = options.prompt; const useResearch = options.research || false; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -1051,11 +1024,11 @@ function registerCommands(programInstance) { // Call core updateTasks, passing context for CLI await updateTasks( - tasksPath, + taskMaster.getTasksPath(), fromId, prompt, useResearch, - { projectRoot, tag } // Pass context with projectRoot and tag + { projectRoot: taskMaster.getProjectRoot(), tag } // Pass context with projectRoot and tag ); }); @@ -1086,16 +1059,14 @@ function registerCommands(programInstance) { .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { try { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; - - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -1193,7 +1164,7 @@ function registerCommands(programInstance) { taskId, prompt, useResearch, - { projectRoot, tag }, + { projectRoot: taskMaster.getProjectRoot(), tag }, 'text', options.append || false ); @@ -1259,16 +1230,14 @@ function registerCommands(programInstance) { .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { try { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; - - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -1368,7 +1337,7 @@ function registerCommands(programInstance) { subtaskId, prompt, useResearch, - { projectRoot, tag } + { projectRoot: taskMaster.getProjectRoot(), tag } ); if (!result) { @@ -1426,20 +1395,23 @@ function registerCommands(programInstance) { ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const outputDir = options.output; const tag = options.tag; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - - console.log(chalk.blue(`Generating task files from: ${tasksPath}`)); + console.log( + chalk.blue(`Generating task files from: ${taskMaster.getTasksPath()}`) + ); console.log(chalk.blue(`Output directory: ${outputDir}`)); - await generateTaskFiles(tasksPath, outputDir, { projectRoot, tag }); + await generateTaskFiles(taskMaster.getTasksPath(), outputDir, { + projectRoot: taskMaster.getProjectRoot(), + tag + }); }); // set-status command @@ -1463,7 +1435,11 @@ function registerCommands(programInstance) { ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const taskId = options.id; const status = options.status; const tag = options.tag; @@ -1483,22 +1459,19 @@ function registerCommands(programInstance) { process.exit(1); } - // Find project root for tag resolution - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Resolve tag using standard pattern and show current tag context - const resolvedTag = tag || getCurrentTag(projectRoot) || 'master'; + const resolvedTag = + tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; displayCurrentTagIndicator(resolvedTag); console.log( chalk.blue(`Setting status of task(s) ${taskId} to: ${status}`) ); - await setTaskStatus(tasksPath, taskId, status, { projectRoot, tag }); + await setTaskStatus(taskMaster.getTasksPath(), taskId, status, { + projectRoot: taskMaster.getProjectRoot(), + tag + }); }); // list command @@ -1519,22 +1492,23 @@ function registerCommands(programInstance) { .option('--with-subtasks', 'Show subtasks for each task') .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true, + complexityReportPath: options.report || false + }); - const tasksPath = options.file || TASKMASTER_TASKS_FILE; - const reportPath = options.report; const statusFilter = options.status; const withSubtasks = options.withSubtasks || false; - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); - console.log(chalk.blue(`Listing tasks from: ${tasksPath}`)); + console.log( + chalk.blue(`Listing tasks from: ${taskMaster.getTasksPath()}`) + ); if (statusFilter) { console.log(chalk.blue(`Filtering by status: ${statusFilter}`)); } @@ -1543,13 +1517,13 @@ function registerCommands(programInstance) { } await listTasks( - tasksPath, + taskMaster.getTasksPath(), statusFilter, - reportPath, + taskMaster.getComplexityReportPath(), withSubtasks, 'text', tag, - { projectRoot } + { projectRoot: taskMaster.getProjectRoot() } ); }); @@ -1580,16 +1554,16 @@ function registerCommands(programInstance) { ) // Allow file override .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - const tasksPath = path.resolve(projectRoot, options.file); // Resolve tasks path + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); const tag = options.tag; // Show current tag context - displayCurrentTagIndicator(tag || getCurrentTag(projectRoot) || 'master'); + displayCurrentTagIndicator( + tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master' + ); if (options.all) { // --- Handle expand --all --- @@ -1602,7 +1576,7 @@ function registerCommands(programInstance) { options.research, // Pass research flag options.prompt, // Pass additional context options.force, // Pass force flag - { projectRoot, tag } // Pass context with projectRoot and tag + { projectRoot: taskMaster.getProjectRoot(), tag } // Pass context with projectRoot and tag // outputFormat defaults to 'text' in expandAllTasks for CLI ); } catch (error) { @@ -1629,7 +1603,7 @@ function registerCommands(programInstance) { options.num, options.research, options.prompt, - { projectRoot, tag }, // Pass context with projectRoot and tag + { projectRoot: taskMaster.getProjectRoot(), tag }, // Pass context with projectRoot and tag options.force // Pass the force flag down ); // expandTask logs its own success/failure for single task @@ -1684,31 +1658,36 @@ function registerCommands(programInstance) { .option('--to ', 'Ending task ID in a range to analyze') .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true, + complexityReportPath: options.output || true + }); + const tag = options.tag; const modelOverride = options.model; const thresholdScore = parseFloat(options.threshold); const useResearch = options.research || false; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Use the provided tag, or the current active tag, or default to 'master' - const targetTag = tag || getCurrentTag(projectRoot) || 'master'; + const targetTag = + tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(targetTag); // Tag-aware output file naming: master -> task-complexity-report.json, other tags -> task-complexity-report_tagname.json + const baseOutputPath = taskMaster.getComplexityReportPath(); const outputPath = options.output === COMPLEXITY_REPORT_FILE && targetTag !== 'master' - ? options.output.replace('.json', `_${targetTag}.json`) - : options.output; + ? baseOutputPath.replace('.json', `_${targetTag}.json`) + : baseOutputPath; - console.log(chalk.blue(`Analyzing task complexity from: ${tasksPath}`)); + console.log( + chalk.blue( + `Analyzing task complexity from: ${taskMaster.getTasksPath()}` + ) + ); console.log(chalk.blue(`Output report will be saved to: ${outputPath}`)); if (options.id) { @@ -1734,7 +1713,8 @@ function registerCommands(programInstance) { ...options, output: outputPath, tag: targetTag, - projectRoot: projectRoot + projectRoot: taskMaster.getProjectRoot(), + file: taskMaster.getTasksPath() }; await analyzeTaskComplexity(updatedOptions); @@ -1879,12 +1859,8 @@ function registerCommands(programInstance) { } } - // Determine project root and tasks file path - const projectRoot = findProjectRoot() || '.'; - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; - const tasksPath = - options.file || - path.join(projectRoot, '.taskmaster', 'tasks', 'tasks.json'); + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -1892,11 +1868,15 @@ function registerCommands(programInstance) { // Validate tasks file exists if task IDs are specified if (taskIds.length > 0) { try { - const tasksData = readJSON(tasksPath, projectRoot, tag); + const tasksData = readJSON( + taskMaster.getTasksPath(), + taskMaster.getProjectRoot(), + tag + ); if (!tasksData || !tasksData.tasks) { console.error( chalk.red( - `Error: No valid tasks found in ${tasksPath} for tag '${tag}'` + `Error: No valid tasks found in ${taskMaster.getTasksPath()} for tag '${tag}'` ) ); process.exit(1); @@ -1914,7 +1894,7 @@ function registerCommands(programInstance) { for (const filePath of filePaths) { const fullPath = path.isAbsolute(filePath) ? filePath - : path.join(projectRoot, filePath); + : path.join(taskMaster.getProjectRoot(), filePath); if (!fs.existsSync(fullPath)) { console.error(chalk.red(`Error: File not found: ${filePath}`)); process.exit(1); @@ -1933,8 +1913,8 @@ function registerCommands(programInstance) { saveToId: options.saveTo ? options.saveTo.trim() : null, allowFollowUp: true, // Always allow follow-up in CLI detailLevel: options.detail ? options.detail.toLowerCase() : 'medium', - tasksPath: tasksPath, - projectRoot: projectRoot + tasksPath: taskMaster.getTasksPath(), + projectRoot: taskMaster.getProjectRoot() }; // Display what we're about to do @@ -2116,14 +2096,15 @@ ${result.result} const all = options.all; const tag = options.tag; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Show current tag context - displayCurrentTagIndicator(tag || getCurrentTag(projectRoot) || 'master'); + displayCurrentTagIndicator( + tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master' + ); if (!taskIds && !all) { console.error( @@ -2136,15 +2117,25 @@ ${result.result} if (all) { // If --all is specified, get all task IDs - const data = readJSON(tasksPath, projectRoot, tag); + const data = readJSON( + taskMaster.getTasksPath(), + taskMaster.getProjectRoot(), + tag + ); if (!data || !data.tasks) { console.error(chalk.red('Error: No valid tasks found')); process.exit(1); } const allIds = data.tasks.map((t) => t.id).join(','); - clearSubtasks(tasksPath, allIds, { projectRoot, tag }); + clearSubtasks(taskMaster.getTasksPath(), allIds, { + projectRoot: taskMaster.getProjectRoot(), + tag + }); } else { - clearSubtasks(tasksPath, taskIds, { projectRoot, tag }); + clearSubtasks(taskMaster.getTasksPath(), taskIds, { + projectRoot: taskMaster.getProjectRoot(), + tag + }); } }); @@ -2207,15 +2198,14 @@ ${result.result} } // Correctly determine projectRoot - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Show current tag context displayCurrentTagIndicator( - options.tag || getCurrentTag(projectRoot) || 'master' + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master' ); let manualTaskData = null; @@ -2302,16 +2292,21 @@ ${result.result} const reportPath = options.report; const tag = options.tag; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Show current tag context - displayCurrentTagIndicator(tag || getCurrentTag(projectRoot) || 'master'); + displayCurrentTagIndicator( + tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master' + ); - await displayNextTask(tasksPath, reportPath, { projectRoot, tag }); + await displayNextTask( + taskMaster.getTasksPath(), + taskMaster.getComplexityReportPath(), + { projectRoot: taskMaster.getProjectRoot(), tag } + ); }); // show command @@ -2338,27 +2333,26 @@ ${result.result} ) .option('--tag ', 'Specify tag context for task operations') .action(async (taskId, options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true, + complexityReportPath: options.report || false + }); const idArg = taskId || options.id; const statusFilter = options.status; const tag = options.tag; // Show current tag context - displayCurrentTagIndicator(tag || getCurrentTag(projectRoot) || 'master'); + displayCurrentTagIndicator( + tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master' + ); if (!idArg) { console.error(chalk.red('Error: Please provide a task ID')); process.exit(1); } - const tasksPath = options.file || TASKMASTER_TASKS_FILE; - const reportPath = options.report; - // Check if multiple IDs are provided (comma-separated) const taskIds = idArg .split(',') @@ -2368,21 +2362,21 @@ ${result.result} if (taskIds.length > 1) { // Multiple tasks - use compact summary view with interactive drill-down await displayMultipleTasksSummary( - tasksPath, + taskMaster.getTasksPath(), taskIds, - reportPath, + taskMaster.getComplexityReportPath(), statusFilter, - { projectRoot, tag } + { projectRoot: taskMaster.getProjectRoot(), tag } ); } else { // Single task - use detailed view await displayTaskById( - tasksPath, + taskMaster.getTasksPath(), taskIds[0], - reportPath, + taskMaster.getComplexityReportPath(), statusFilter, tag, - { projectRoot } + { projectRoot: taskMaster.getProjectRoot() } ); } }); @@ -2400,18 +2394,17 @@ ${result.result} ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const taskId = options.id; const dependencyId = options.dependsOn; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -2432,10 +2425,15 @@ ${result.result} ? dependencyId : parseInt(dependencyId, 10); - await addDependency(tasksPath, formattedTaskId, formattedDependencyId, { - projectRoot, - tag - }); + await addDependency( + taskMaster.getTasksPath(), + formattedTaskId, + formattedDependencyId, + { + projectRoot: taskMaster.getProjectRoot(), + tag + } + ); }); // remove-dependency command @@ -2451,18 +2449,17 @@ ${result.result} ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const taskId = options.id; const dependencyId = options.dependsOn; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -2484,11 +2481,11 @@ ${result.result} : parseInt(dependencyId, 10); await removeDependency( - tasksPath, + taskMaster.getTasksPath(), formattedTaskId, formattedDependencyId, { - projectRoot, + projectRoot: taskMaster.getProjectRoot(), tag } ); @@ -2507,20 +2504,20 @@ ${result.result} ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); - await validateDependenciesCommand(options.file || TASKMASTER_TASKS_FILE, { - context: { projectRoot, tag } + await validateDependenciesCommand(taskMaster.getTasksPath(), { + context: { projectRoot: taskMaster.getProjectRoot(), tag } }); }); @@ -2535,20 +2532,20 @@ ${result.result} ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); - await fixDependenciesCommand(options.file || TASKMASTER_TASKS_FILE, { - context: { projectRoot, tag } + await fixDependenciesCommand(taskMaster.getTasksPath(), { + context: { projectRoot: taskMaster.getProjectRoot(), tag } }); }); @@ -2563,23 +2560,24 @@ ${result.result} ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + complexityReportPath: options.file || true + }); // Use the provided tag, or the current active tag, or default to 'master' - const targetTag = options.tag || getCurrentTag(projectRoot) || 'master'; + const targetTag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(targetTag); // Tag-aware report file naming: master -> task-complexity-report.json, other tags -> task-complexity-report_tagname.json + const baseReportPath = taskMaster.getComplexityReportPath(); const reportPath = options.file === COMPLEXITY_REPORT_FILE && targetTag !== 'master' - ? options.file.replace('.json', `_${targetTag}.json`) - : options.file || COMPLEXITY_REPORT_FILE; + ? baseReportPath.replace('.json', `_${targetTag}.json`) + : baseReportPath; await displayComplexityReport(reportPath); }); @@ -2609,19 +2607,18 @@ ${result.result} .option('--skip-generate', 'Skip regenerating task files') .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); - const tasksPath = options.file || TASKMASTER_TASKS_FILE; const parentId = options.parent; const existingTaskId = options.taskId; const generateFiles = !options.skipGenerate; // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -2654,12 +2651,12 @@ ${result.result} ) ); await addSubtask( - tasksPath, + taskMaster.getTasksPath(), parentId, existingTaskId, null, generateFiles, - { projectRoot, tag } + { projectRoot: taskMaster.getProjectRoot(), tag } ); console.log( chalk.green( @@ -2681,12 +2678,12 @@ ${result.result} }; const subtask = await addSubtask( - tasksPath, + taskMaster.getTasksPath(), parentId, null, newSubtaskData, generateFiles, - { projectRoot, tag } + { projectRoot: taskMaster.getProjectRoot(), tag } ); console.log( chalk.green( @@ -2794,18 +2791,16 @@ ${result.result} .option('--skip-generate', 'Skip regenerating task files') .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const subtaskIds = options.id; const convertToTask = options.convert || false; const generateFiles = !options.skipGenerate; const tag = options.tag; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - if (!subtaskIds) { console.error( chalk.red( @@ -2840,11 +2835,11 @@ ${result.result} } const result = await removeSubtask( - tasksPath, + taskMaster.getTasksPath(), subtaskId, convertToTask, generateFiles, - { projectRoot, tag } + { projectRoot: taskMaster.getProjectRoot(), tag } ); if (convertToTask && result) { @@ -3097,17 +3092,16 @@ ${result.result} .option('-y, --yes', 'Skip confirmation prompt', false) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const taskIdsString = options.id; - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Resolve tag using standard pattern - const tag = options.tag || getCurrentTag(projectRoot) || 'master'; + const tag = + options.tag || getCurrentTag(taskMaster.getProjectRoot()) || 'master'; // Show current tag context displayCurrentTagIndicator(tag); @@ -3134,7 +3128,11 @@ ${result.result} try { // Read data once for checks and confirmation - const data = readJSON(tasksPath, projectRoot, tag); + const data = readJSON( + taskMaster.getTasksPath(), + taskMaster.getProjectRoot(), + tag + ); if (!data || !data.tasks) { console.error( chalk.red(`Error: No valid tasks found in ${tasksPath}`) @@ -3274,10 +3272,14 @@ ${result.result} const existingIdsString = existingTasksToRemove .map(({ id }) => id) .join(','); - const result = await removeTask(tasksPath, existingIdsString, { - projectRoot, - tag - }); + const result = await removeTask( + taskMaster.getTasksPath(), + existingIdsString, + { + projectRoot: taskMaster.getProjectRoot(), + tag + } + ); stopLoadingIndicator(indicator); @@ -3450,11 +3452,10 @@ Examples: $ task-master models --setup # Run interactive setup` ) .action(async (options) => { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Validate flags: cannot use multiple provider flags simultaneously const providerFlags = [ options.openrouter, @@ -3669,7 +3670,8 @@ Examples: .option('--response ', 'Set the response language') .option('--setup', 'Run interactive setup to configure response language') .action(async (options) => { - const projectRoot = findProjectRoot(); // Find project root for context + const taskMaster = initTaskMaster({}); + const projectRoot = taskMaster.getProjectRoot(); // Find project root for context const { response, setup } = options; console.log( chalk.blue('Response language set to:', JSON.stringify(options)) @@ -3738,7 +3740,11 @@ Examples: ) .option('--tag ', 'Specify tag context for task operations') .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const sourceId = options.from; const destinationId = options.to; const tag = options.tag; @@ -3755,13 +3761,6 @@ Examples: process.exit(1); } - // Find project root for tag resolution - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - // Check if we're moving multiple tasks (comma-separated IDs) const sourceIds = sourceId.split(',').map((id) => id.trim()); const destinationIds = destinationId.split(',').map((id) => id.trim()); @@ -3789,7 +3788,11 @@ Examples: try { // Read tasks data once to validate destination IDs - const tasksData = readJSON(tasksPath, projectRoot, tag); + const tasksData = readJSON( + taskMaster.getTasksPath(), + taskMaster.getProjectRoot(), + tag + ); if (!tasksData || !tasksData.tasks) { console.error( chalk.red(`Error: Invalid or missing tasks file at ${tasksPath}`) @@ -3815,11 +3818,11 @@ Examples: ); try { await moveTask( - tasksPath, + taskMaster.getTasksPath(), fromId, toId, i === sourceIds.length - 1, - { projectRoot, tag } + { projectRoot: taskMaster.getProjectRoot(), tag } ); console.log( chalk.green( @@ -3845,11 +3848,11 @@ Examples: try { const result = await moveTask( - tasksPath, + taskMaster.getTasksPath(), sourceId, destinationId, true, - { projectRoot, tag } + { projectRoot: taskMaster.getProjectRoot(), tag } ); console.log( chalk.green( @@ -3886,7 +3889,8 @@ Examples: $ task-master rules --${RULES_SETUP_ACTION} # Interactive setup to select rule profiles` ) .action(async (action, profiles, options) => { - const projectRoot = findProjectRoot(); + const taskMaster = initTaskMaster({}); + const projectRoot = taskMaster.getProjectRoot(); if (!projectRoot) { console.error(chalk.red('Error: Could not find project root.')); process.exit(1); @@ -4186,31 +4190,24 @@ Examples: 'Show only tasks matching this status (e.g., pending, done)' ) .action(async (options) => { - const tasksPath = options.file || TASKMASTER_TASKS_FILE; + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); + const withSubtasks = options.withSubtasks || false; const status = options.status || null; - // Find project root - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error( - chalk.red( - 'Error: Could not find project root. Make sure you are in a Task Master project directory.' - ) - ); - process.exit(1); - } - console.log( chalk.blue( `📝 Syncing tasks to README.md${withSubtasks ? ' (with subtasks)' : ''}${status ? ` (status: ${status})` : ''}...` ) ); - const success = await syncTasksToReadme(projectRoot, { + const success = await syncTasksToReadme(taskMaster.getProjectRoot(), { withSubtasks, status, - tasksPath + tasksPath: taskMaster.getTasksPath() }); if (!success) { @@ -4249,13 +4246,10 @@ Examples: .option('-d, --description ', 'Optional description for the tag') .action(async (tagName, options) => { try { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - - const tasksPath = path.resolve(projectRoot, options.file); + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Validate tasks file exists if (!fs.existsSync(tasksPath)) { @@ -4284,7 +4278,7 @@ Examples: } const context = { - projectRoot, + projectRoot: taskMaster.getProjectRoot(), commandName: 'add-tag', outputType: 'cli' }; @@ -4339,7 +4333,13 @@ Examples: description: options.description }; - await createTag(tasksPath, tagName, createOptions, context, 'text'); + await createTag( + taskMaster.getTasksPath(), + tagName, + createOptions, + context, + 'text' + ); } // Handle auto-switch if requested @@ -4352,7 +4352,13 @@ Examples: ) ) : tagName; - await useTag(tasksPath, finalTagName, {}, context, 'text'); + await useTag( + taskMaster.getTasksPath(), + finalTagName, + {}, + context, + 'text' + ); } } catch (error) { console.error(chalk.red(`Error creating tag: ${error.message}`)); @@ -4379,13 +4385,10 @@ Examples: .option('-y, --yes', 'Skip confirmation prompts') .action(async (tagName, options) => { try { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - - const tasksPath = path.resolve(projectRoot, options.file); + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Validate tasks file exists if (!fs.existsSync(tasksPath)) { @@ -4400,12 +4403,18 @@ Examples: }; const context = { - projectRoot, + projectRoot: taskMaster.getProjectRoot(), commandName: 'delete-tag', outputType: 'cli' }; - await deleteTag(tasksPath, tagName, deleteOptions, context, 'text'); + await deleteTag( + taskMaster.getTasksPath(), + tagName, + deleteOptions, + context, + 'text' + ); } catch (error) { console.error(chalk.red(`Error deleting tag: ${error.message}`)); showDeleteTagHelp(); @@ -4430,13 +4439,10 @@ Examples: .option('--show-metadata', 'Show detailed metadata for each tag') .action(async (options) => { try { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - - const tasksPath = path.resolve(projectRoot, options.file); + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Validate tasks file exists if (!fs.existsSync(tasksPath)) { @@ -4452,12 +4458,12 @@ Examples: }; const context = { - projectRoot, + projectRoot: taskMaster.getProjectRoot(), commandName: 'tags', outputType: 'cli' }; - await tags(tasksPath, listOptions, context, 'text'); + await tags(taskMaster.getTasksPath(), listOptions, context, 'text'); } catch (error) { console.error(chalk.red(`Error listing tags: ${error.message}`)); showTagsHelp(); @@ -4482,13 +4488,10 @@ Examples: ) .action(async (tagName, options) => { try { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - - const tasksPath = path.resolve(projectRoot, options.file); + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Validate tasks file exists if (!fs.existsSync(tasksPath)) { @@ -4499,12 +4502,12 @@ Examples: } const context = { - projectRoot, + projectRoot: taskMaster.getProjectRoot(), commandName: 'use-tag', outputType: 'cli' }; - await useTag(tasksPath, tagName, {}, context, 'text'); + await useTag(taskMaster.getTasksPath(), tagName, {}, context, 'text'); } catch (error) { console.error(chalk.red(`Error switching tag: ${error.message}`)); showUseTagHelp(); @@ -4530,13 +4533,10 @@ Examples: ) .action(async (oldName, newName, options) => { try { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - - const tasksPath = path.resolve(projectRoot, options.file); + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Validate tasks file exists if (!fs.existsSync(tasksPath)) { @@ -4547,12 +4547,19 @@ Examples: } const context = { - projectRoot, + projectRoot: taskMaster.getProjectRoot(), commandName: 'rename-tag', outputType: 'cli' }; - await renameTag(tasksPath, oldName, newName, {}, context, 'text'); + await renameTag( + taskMaster.getTasksPath(), + oldName, + newName, + {}, + context, + 'text' + ); } catch (error) { console.error(chalk.red(`Error renaming tag: ${error.message}`)); process.exit(1); @@ -4577,13 +4584,10 @@ Examples: .option('-d, --description ', 'Optional description for the new tag') .action(async (sourceName, targetName, options) => { try { - const projectRoot = findProjectRoot(); - if (!projectRoot) { - console.error(chalk.red('Error: Could not find project root.')); - process.exit(1); - } - - const tasksPath = path.resolve(projectRoot, options.file); + // Initialize TaskMaster + const taskMaster = initTaskMaster({ + tasksPath: options.file || true + }); // Validate tasks file exists if (!fs.existsSync(tasksPath)) { @@ -4598,13 +4602,13 @@ Examples: }; const context = { - projectRoot, + projectRoot: taskMaster.getProjectRoot(), commandName: 'copy-tag', outputType: 'cli' }; await copyTag( - tasksPath, + taskMaster.getTasksPath(), sourceName, targetName, copyOptions, @@ -4827,16 +4831,13 @@ async function runCLI(argv = process.argv) { // Check if migration has occurred and show FYI notice once try { - const projectRoot = findProjectRoot() || '.'; - const tasksPath = path.join( - projectRoot, - '.taskmaster', - 'tasks', - 'tasks.json' - ); - const statePath = path.join(projectRoot, '.taskmaster', 'state.json'); + // Use initTaskMaster with no required fields - will only fail if no project root + const taskMaster = initTaskMaster({}); - if (fs.existsSync(tasksPath)) { + const tasksPath = taskMaster.getTasksPath(); + const statePath = taskMaster.getStatePath(); + + if (tasksPath && fs.existsSync(tasksPath)) { // Read raw file to check if it has master key (bypassing tag resolution) const rawData = fs.readFileSync(tasksPath, 'utf8'); const parsedData = JSON.parse(rawData); @@ -4844,7 +4845,7 @@ async function runCLI(argv = process.argv) { if (parsedData && parsedData.master) { // Migration has occurred, check if we've shown the notice let stateData = { migrationNoticeShown: false }; - if (fs.existsSync(statePath)) { + if (statePath && fs.existsSync(statePath)) { // Read state.json directly without tag resolution since it's not a tagged file const rawStateData = fs.readFileSync(statePath, 'utf8'); stateData = JSON.parse(rawStateData) || stateData; @@ -4856,7 +4857,9 @@ async function runCLI(argv = process.argv) { // Mark as shown stateData.migrationNoticeShown = true; // Write state.json directly without tag resolution since it's not a tagged file - fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2)); + if (statePath) { + fs.writeFileSync(statePath, JSON.stringify(stateData, null, 2)); + } } } } diff --git a/src/task-master.js b/src/task-master.js new file mode 100644 index 00000000..71eefb0e --- /dev/null +++ b/src/task-master.js @@ -0,0 +1,288 @@ +/** + * task-master.js + * This module provides a centralized path management system for the Task Master application. + * It exports the TaskMaster class and the initTaskMaster factory function to create a single, + * authoritative source for all critical file and directory paths, resolving circular dependencies. + */ + +import path from 'path'; +import fs from 'fs'; +import { + TASKMASTER_DIR, + TASKMASTER_TASKS_FILE, + LEGACY_TASKS_FILE, + TASKMASTER_DOCS_DIR, + TASKMASTER_REPORTS_DIR, + TASKMASTER_CONFIG_FILE, + LEGACY_CONFIG_FILE +} from './constants/paths.js'; + +/** + * TaskMaster class manages all the paths for the application. + * An instance of this class is created by the initTaskMaster function. + */ +export class TaskMaster { + #paths; + + /** + * The constructor is intended to be used only by the initTaskMaster factory function. + * @param {object} paths - A pre-resolved object of all application paths. + */ + constructor(paths) { + this.#paths = Object.freeze({ ...paths }); + } + + /** + * @returns {string|null} The absolute path to the project root. + */ + getProjectRoot() { + return this.#paths.projectRoot; + } + + /** + * @returns {string|null} The absolute path to the .taskmaster directory. + */ + getTaskMasterDir() { + return this.#paths.taskMasterDir; + } + + /** + * @returns {string|null} The absolute path to the tasks.json file. + */ + getTasksPath() { + return this.#paths.tasksPath; + } + + /** + * @returns {string|null} The absolute path to the PRD file. + */ + getPrdPath() { + return this.#paths.prdPath; + } + + /** + * @returns {string|null} The absolute path to the complexity report. + */ + getComplexityReportPath() { + return this.#paths.complexityReportPath; + } + + /** + * @returns {string|null} The absolute path to the config.json file. + */ + getConfigPath() { + return this.#paths.configPath; + } + + /** + * @returns {string|null} The absolute path to the state.json file. + */ + getStatePath() { + return this.#paths.statePath; + } + + /** + * @returns {object} A frozen object containing all resolved paths. + */ + getAllPaths() { + return this.#paths; + } +} + +/** + * Initializes a TaskMaster instance with resolved paths. + * This function centralizes path resolution logic. + * + * @param {object} [overrides={}] - An object with possible path overrides. + * @param {string} [overrides.projectRoot] + * @param {string} [overrides.tasksPath] + * @param {string} [overrides.prdPath] + * @param {string} [overrides.complexityReportPath] + * @param {string} [overrides.configPath] + * @param {string} [overrides.statePath] + * @returns {TaskMaster} An initialized TaskMaster instance. + */ +export function initTaskMaster(overrides = {}) { + const findProjectRoot = (startDir = process.cwd()) => { + const projectMarkers = [TASKMASTER_DIR, LEGACY_CONFIG_FILE]; + let currentDir = path.resolve(startDir); + const rootDir = path.parse(currentDir).root; + while (currentDir !== rootDir) { + for (const marker of projectMarkers) { + const markerPath = path.join(currentDir, marker); + if (fs.existsSync(markerPath)) { + return currentDir; + } + } + currentDir = path.dirname(currentDir); + } + return null; + }; + + const resolvePath = ( + pathType, + override, + defaultPaths = [], + basePath = null + ) => { + if (typeof override === 'string') { + const resolvedPath = path.isAbsolute(override) + ? override + : path.resolve(basePath || process.cwd(), override); + + if (!fs.existsSync(resolvedPath)) { + throw new Error( + `${pathType} override path does not exist: ${resolvedPath}` + ); + } + return resolvedPath; + } + + if (override === true) { + // Required path - search defaults and fail if not found + for (const defaultPath of defaultPaths) { + const fullPath = path.isAbsolute(defaultPath) + ? defaultPath + : path.join(basePath || process.cwd(), defaultPath); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + throw new Error( + `Required ${pathType} not found. Searched: ${defaultPaths.join(', ')}` + ); + } + + // Optional path (override === false/undefined) - search defaults, return null if not found + for (const defaultPath of defaultPaths) { + const fullPath = path.isAbsolute(defaultPath) + ? defaultPath + : path.join(basePath || process.cwd(), defaultPath); + if (fs.existsSync(fullPath)) { + return fullPath; + } + } + + return null; + }; + + const paths = {}; + + // Project Root + if (overrides.projectRoot) { + const resolvedOverride = path.resolve(overrides.projectRoot); + if (!fs.existsSync(resolvedOverride)) { + throw new Error( + `Project root override path does not exist: ${resolvedOverride}` + ); + } + + const hasTaskmasterDir = fs.existsSync( + path.join(resolvedOverride, TASKMASTER_DIR) + ); + const hasLegacyConfig = fs.existsSync( + path.join(resolvedOverride, LEGACY_CONFIG_FILE) + ); + + if (!hasTaskmasterDir && !hasLegacyConfig) { + throw new Error( + `Project root override is not a valid taskmaster project: ${resolvedOverride}` + ); + } + + paths.projectRoot = resolvedOverride; + } else { + const foundRoot = findProjectRoot(); + if (!foundRoot) { + throw new Error( + 'Unable to find project root. No project markers found. Run "init" command first.' + ); + } + paths.projectRoot = foundRoot; + } + + // TaskMaster Directory + if ('taskMasterDir' in overrides) { + paths.taskMasterDir = resolvePath( + 'taskmaster directory', + overrides.taskMasterDir, + [TASKMASTER_DIR], + paths.projectRoot + ); + } else { + paths.taskMasterDir = resolvePath( + 'taskmaster directory', + false, + [TASKMASTER_DIR], + paths.projectRoot + ); + } + + // Remaining paths - only resolve if key exists in overrides + if ('configPath' in overrides) { + paths.configPath = resolvePath( + 'config file', + overrides.configPath, + [TASKMASTER_CONFIG_FILE, LEGACY_CONFIG_FILE], + paths.projectRoot + ); + } + + if ('statePath' in overrides) { + paths.statePath = resolvePath( + 'state file', + overrides.statePath, + ['state.json'], + paths.taskMasterDir + ); + } + + if ('tasksPath' in overrides) { + paths.tasksPath = resolvePath( + 'tasks file', + overrides.tasksPath, + [TASKMASTER_TASKS_FILE, LEGACY_TASKS_FILE], + paths.projectRoot + ); + } + + if ('prdPath' in overrides) { + paths.prdPath = resolvePath( + 'PRD file', + overrides.prdPath, + [ + path.join(TASKMASTER_DOCS_DIR, 'PRD.md'), + path.join(TASKMASTER_DOCS_DIR, 'prd.md'), + path.join(TASKMASTER_DOCS_DIR, 'PRD.txt'), + path.join(TASKMASTER_DOCS_DIR, 'prd.txt'), + path.join('scripts', 'PRD.md'), + path.join('scripts', 'prd.md'), + path.join('scripts', 'PRD.txt'), + path.join('scripts', 'prd.txt'), + 'PRD.md', + 'prd.md', + 'PRD.txt', + 'prd.txt' + ], + paths.projectRoot + ); + } + + if ('complexityReportPath' in overrides) { + paths.complexityReportPath = resolvePath( + 'complexity report', + overrides.complexityReportPath, + [ + path.join(TASKMASTER_REPORTS_DIR, 'task-complexity-report.json'), + path.join(TASKMASTER_REPORTS_DIR, 'complexity-report.json'), + path.join('scripts', 'task-complexity-report.json'), + path.join('scripts', 'complexity-report.json'), + 'task-complexity-report.json', + 'complexity-report.json' + ], + paths.projectRoot + ); + } + + return new TaskMaster(paths); +} diff --git a/tests/unit/task-master.test.js b/tests/unit/task-master.test.js new file mode 100644 index 00000000..26067d12 --- /dev/null +++ b/tests/unit/task-master.test.js @@ -0,0 +1,425 @@ +/** + * Tests for task-master.js initTaskMaster function + */ + +import { jest } from '@jest/globals'; +import path from 'path'; +import fs from 'fs'; +import os from 'os'; +import { initTaskMaster, TaskMaster } from '../../src/task-master.js'; +import { + TASKMASTER_DIR, + TASKMASTER_TASKS_FILE, + LEGACY_CONFIG_FILE, + TASKMASTER_CONFIG_FILE, + LEGACY_TASKS_FILE +} from '../../src/constants/paths.js'; + +// Mock the console to prevent noise during tests +jest.spyOn(console, 'error').mockImplementation(() => {}); + +describe('initTaskMaster', () => { + let tempDir; + let originalCwd; + + beforeEach(() => { + // Create a temporary directory for testing + tempDir = fs.realpathSync( + fs.mkdtempSync(path.join(os.tmpdir(), 'taskmaster-test-')) + ); + originalCwd = process.cwd(); + + // Clear all mocks + jest.clearAllMocks(); + }); + + afterEach(() => { + // Restore original working directory + process.chdir(originalCwd); + + // Clean up temporary directory + if (fs.existsSync(tempDir)) { + fs.rmSync(tempDir, { recursive: true, force: true }); + } + }); + + describe('Project root detection', () => { + test('should find project root when .taskmaster directory exists', () => { + // Arrange - Create .taskmaster directory in temp dir + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + + // Change to temp directory + process.chdir(tempDir); + + // Act + const taskMaster = initTaskMaster({}); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + expect(taskMaster).toBeInstanceOf(TaskMaster); + }); + + test('should find project root when legacy config file exists', () => { + // Arrange - Create legacy config file in temp dir + const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE); + fs.writeFileSync(legacyConfigPath, '{}'); + + // Change to temp directory + process.chdir(tempDir); + + // Act + const taskMaster = initTaskMaster({}); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + }); + + test('should find project root from subdirectory', () => { + // Arrange - Create .taskmaster directory in temp dir + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + + // Create a subdirectory and change to it + const srcDir = path.join(tempDir, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + process.chdir(srcDir); + + // Act + const taskMaster = initTaskMaster({}); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + }); + + test('should find project root from deeply nested subdirectory', () => { + // Arrange - Create .taskmaster directory in temp dir + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + + // Create deeply nested subdirectory and change to it + const deepDir = path.join(tempDir, 'src', 'components', 'ui'); + fs.mkdirSync(deepDir, { recursive: true }); + process.chdir(deepDir); + + // Act + const taskMaster = initTaskMaster({}); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + }); + + test('should throw error when no project markers found', () => { + // Arrange - Empty temp directory, no project markers + process.chdir(tempDir); + + // Act & Assert + expect(() => { + initTaskMaster({}); + }).toThrow( + 'Unable to find project root. No project markers found. Run "init" command first.' + ); + }); + }); + + describe('Project root override validation', () => { + test('should accept valid project root override with .taskmaster directory', () => { + // Arrange - Create .taskmaster directory in temp dir + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + + // Act + const taskMaster = initTaskMaster({ projectRoot: tempDir }); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + }); + + test('should accept valid project root override with legacy config', () => { + // Arrange - Create legacy config file in temp dir + const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE); + fs.writeFileSync(legacyConfigPath, '{}'); + + // Act + const taskMaster = initTaskMaster({ projectRoot: tempDir }); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + }); + + test('should throw error when project root override does not exist', () => { + // Arrange - Non-existent path + const nonExistentPath = path.join(tempDir, 'does-not-exist'); + + // Act & Assert + expect(() => { + initTaskMaster({ projectRoot: nonExistentPath }); + }).toThrow( + `Project root override path does not exist: ${nonExistentPath}` + ); + }); + + test('should throw error when project root override has no project markers', () => { + // Arrange - Empty temp directory (no project markers) + + // Act & Assert + expect(() => { + initTaskMaster({ projectRoot: tempDir }); + }).toThrow( + `Project root override is not a valid taskmaster project: ${tempDir}` + ); + }); + + test('should resolve relative project root override', () => { + // Arrange - Create .taskmaster directory in temp dir + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + + // Create subdirectory and change to it + const srcDir = path.join(tempDir, 'src'); + fs.mkdirSync(srcDir, { recursive: true }); + process.chdir(srcDir); + + // Act - Use relative path '../' to go back to project root + const taskMaster = initTaskMaster({ projectRoot: '../' }); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + }); + }); + + describe('Path resolution with boolean logic', () => { + let taskMasterDir, tasksPath, configPath, statePath; + + beforeEach(() => { + // Setup a valid project structure + taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + + tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE); + fs.mkdirSync(path.dirname(tasksPath), { recursive: true }); + fs.writeFileSync(tasksPath, '[]'); + + configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE); + fs.writeFileSync(configPath, '{}'); + + statePath = path.join(taskMasterDir, 'state.json'); + fs.writeFileSync(statePath, '{}'); + + process.chdir(tempDir); + }); + + test('should return paths when required (true) and files exist', () => { + // Act + const taskMaster = initTaskMaster({ + tasksPath: true, + configPath: true, + statePath: true + }); + + // Assert + expect(taskMaster.getTasksPath()).toBe(tasksPath); + expect(taskMaster.getConfigPath()).toBe(configPath); + expect(taskMaster.getStatePath()).toBe(statePath); + }); + + test('should throw error when required (true) files do not exist', () => { + // Arrange - Remove tasks file + fs.unlinkSync(tasksPath); + + // Act & Assert + expect(() => { + initTaskMaster({ tasksPath: true }); + }).toThrow( + 'Required tasks file not found. Searched: .taskmaster/tasks/tasks.json, tasks/tasks.json' + ); + }); + + test('should return null when optional (false/undefined) files do not exist', () => { + // Arrange - Remove tasks file + fs.unlinkSync(tasksPath); + + // Act + const taskMaster = initTaskMaster({ + tasksPath: false + }); + + // Assert + expect(taskMaster.getTasksPath()).toBeNull(); + }); + + test('should return null when optional files not specified in overrides', () => { + // Arrange - Remove all optional files + fs.unlinkSync(tasksPath); + fs.unlinkSync(configPath); + fs.unlinkSync(statePath); + + // Act - Don't specify any optional paths + const taskMaster = initTaskMaster({}); + + // Assert + expect(taskMaster.getTasksPath()).toBeUndefined(); + expect(taskMaster.getConfigPath()).toBeUndefined(); + expect(taskMaster.getStatePath()).toBeUndefined(); + }); + }); + + describe('String path overrides', () => { + let taskMasterDir; + + beforeEach(() => { + taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + process.chdir(tempDir); + }); + + test('should accept valid absolute path override', () => { + // Arrange - Create custom tasks file + const customTasksPath = path.join(tempDir, 'custom-tasks.json'); + fs.writeFileSync(customTasksPath, '[]'); + + // Act + const taskMaster = initTaskMaster({ + tasksPath: customTasksPath + }); + + // Assert + expect(taskMaster.getTasksPath()).toBe(customTasksPath); + }); + + test('should accept valid relative path override', () => { + // Arrange - Create custom tasks file + const customTasksPath = path.join(tempDir, 'custom-tasks.json'); + fs.writeFileSync(customTasksPath, '[]'); + + // Act + const taskMaster = initTaskMaster({ + tasksPath: './custom-tasks.json' + }); + + // Assert + expect(taskMaster.getTasksPath()).toBe(customTasksPath); + }); + + test('should throw error when string path override does not exist', () => { + // Arrange - Non-existent file path + const nonExistentPath = path.join(tempDir, 'does-not-exist.json'); + + // Act & Assert + expect(() => { + initTaskMaster({ tasksPath: nonExistentPath }); + }).toThrow(`tasks file override path does not exist: ${nonExistentPath}`); + }); + }); + + describe('Legacy file support', () => { + beforeEach(() => { + // Setup basic project structure + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + process.chdir(tempDir); + }); + + test('should find legacy tasks file when new format does not exist', () => { + // Arrange - Create legacy tasks file + const legacyTasksDir = path.join(tempDir, 'tasks'); + fs.mkdirSync(legacyTasksDir, { recursive: true }); + const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE); + fs.writeFileSync(legacyTasksPath, '[]'); + + // Act + const taskMaster = initTaskMaster({ tasksPath: true }); + + // Assert + expect(taskMaster.getTasksPath()).toBe(legacyTasksPath); + }); + + test('should prefer new format over legacy when both exist', () => { + // Arrange - Create both new and legacy files + const newTasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE); + fs.mkdirSync(path.dirname(newTasksPath), { recursive: true }); + fs.writeFileSync(newTasksPath, '[]'); + + const legacyTasksDir = path.join(tempDir, 'tasks'); + fs.mkdirSync(legacyTasksDir, { recursive: true }); + const legacyTasksPath = path.join(tempDir, LEGACY_TASKS_FILE); + fs.writeFileSync(legacyTasksPath, '[]'); + + // Act + const taskMaster = initTaskMaster({ tasksPath: true }); + + // Assert + expect(taskMaster.getTasksPath()).toBe(newTasksPath); + }); + + test('should find legacy config file when new format does not exist', () => { + // Arrange - Create legacy config file + const legacyConfigPath = path.join(tempDir, LEGACY_CONFIG_FILE); + fs.writeFileSync(legacyConfigPath, '{}'); + + // Act + const taskMaster = initTaskMaster({ configPath: true }); + + // Assert + expect(taskMaster.getConfigPath()).toBe(legacyConfigPath); + }); + }); + + describe('TaskMaster class methods', () => { + test('should return all paths via getAllPaths method', () => { + // Arrange - Setup project with all files + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + + const tasksPath = path.join(tempDir, TASKMASTER_TASKS_FILE); + fs.mkdirSync(path.dirname(tasksPath), { recursive: true }); + fs.writeFileSync(tasksPath, '[]'); + + const configPath = path.join(tempDir, TASKMASTER_CONFIG_FILE); + fs.writeFileSync(configPath, '{}'); + + process.chdir(tempDir); + + // Act + const taskMaster = initTaskMaster({ + tasksPath: true, + configPath: true + }); + + // Assert + const allPaths = taskMaster.getAllPaths(); + expect(allPaths).toEqual( + expect.objectContaining({ + projectRoot: tempDir, + taskMasterDir: taskMasterDir, + tasksPath: tasksPath, + configPath: configPath + }) + ); + + // Verify paths object is frozen + expect(() => { + allPaths.projectRoot = '/different/path'; + }).toThrow(); + }); + + test('should return correct individual paths', () => { + // Arrange + const taskMasterDir = path.join(tempDir, TASKMASTER_DIR); + fs.mkdirSync(taskMasterDir, { recursive: true }); + process.chdir(tempDir); + + // Act + const taskMaster = initTaskMaster({}); + + // Assert + expect(taskMaster.getProjectRoot()).toBe(tempDir); + expect(taskMaster.getTaskMasterDir()).toBe(taskMasterDir); + expect(taskMaster.getTasksPath()).toBeUndefined(); + expect(taskMaster.getPrdPath()).toBeUndefined(); + expect(taskMaster.getComplexityReportPath()).toBeUndefined(); + expect(taskMaster.getConfigPath()).toBeUndefined(); + expect(taskMaster.getStatePath()).toBeUndefined(); + }); + }); +});