From edaa5fe0d56e0e4e7c4370670a7a388eebd922ac Mon Sep 17 00:00:00 2001 From: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com> Date: Tue, 3 Jun 2025 15:12:50 +0200 Subject: [PATCH] fix: projectRoot duplicate .taskmaster directory (#655) --- .changeset/pink-houses-lay.md | 7 ++ mcp-server/src/core/utils/path-utils.js | 43 +++++++++---- mcp-server/src/tools/complexity-report.js | 18 ++++-- src/utils/path-utils.js | 78 ++++++++++++++++++----- 4 files changed, 113 insertions(+), 33 deletions(-) create mode 100644 .changeset/pink-houses-lay.md diff --git a/.changeset/pink-houses-lay.md b/.changeset/pink-houses-lay.md new file mode 100644 index 00000000..e571e3ee --- /dev/null +++ b/.changeset/pink-houses-lay.md @@ -0,0 +1,7 @@ +--- +"task-master-ai": patch +--- + +Fix double .taskmaster directory paths in file resolution utilities + +- Closes #636 diff --git a/mcp-server/src/core/utils/path-utils.js b/mcp-server/src/core/utils/path-utils.js index 5971a5df..be4c8462 100644 --- a/mcp-server/src/core/utils/path-utils.js +++ b/mcp-server/src/core/utils/path-utils.js @@ -3,7 +3,8 @@ import { findTasksPath as coreFindTasksPath, findPRDPath as coreFindPrdPath, findComplexityReportPath as coreFindComplexityReportPath, - findProjectRoot as coreFindProjectRoot + findProjectRoot as coreFindProjectRoot, + normalizeProjectRoot } from '../../../../src/utils/path-utils.js'; import { PROJECT_MARKERS } from '../../../../src/constants/paths.js'; @@ -49,19 +50,24 @@ export function findPrdPath(explicitPath, args = null, log = silentLogger) { export function resolveTasksPath(args, log = silentLogger) { // Get explicit path from args.file if provided const explicitPath = args?.file; - const projectRoot = args?.projectRoot; + const rawProjectRoot = args?.projectRoot; // If explicit path is provided and absolute, use it directly if (explicitPath && path.isAbsolute(explicitPath)) { return explicitPath; } - // If explicit path is relative, resolve it relative to projectRoot + // Normalize project root if provided + const projectRoot = rawProjectRoot + ? normalizeProjectRoot(rawProjectRoot) + : null; + + // If explicit path is relative, resolve it relative to normalized projectRoot if (explicitPath && projectRoot) { return path.resolve(projectRoot, explicitPath); } - // Use core findTasksPath with explicit path and projectRoot context + // Use core findTasksPath with explicit path and normalized projectRoot context if (projectRoot) { return coreFindTasksPath(explicitPath, { projectRoot }, log); } @@ -79,19 +85,24 @@ export function resolveTasksPath(args, log = silentLogger) { export function resolvePrdPath(args, log = silentLogger) { // Get explicit path from args.input if provided const explicitPath = args?.input; - const projectRoot = args?.projectRoot; + const rawProjectRoot = args?.projectRoot; // If explicit path is provided and absolute, use it directly if (explicitPath && path.isAbsolute(explicitPath)) { return explicitPath; } - // If explicit path is relative, resolve it relative to projectRoot + // Normalize project root if provided + const projectRoot = rawProjectRoot + ? normalizeProjectRoot(rawProjectRoot) + : null; + + // If explicit path is relative, resolve it relative to normalized projectRoot if (explicitPath && projectRoot) { return path.resolve(projectRoot, explicitPath); } - // Use core findPRDPath with explicit path and projectRoot context + // Use core findPRDPath with explicit path and normalized projectRoot context if (projectRoot) { return coreFindPrdPath(explicitPath, { projectRoot }, log); } @@ -109,19 +120,24 @@ export function resolvePrdPath(args, log = silentLogger) { export function resolveComplexityReportPath(args, log = silentLogger) { // Get explicit path from args.complexityReport if provided const explicitPath = args?.complexityReport; - const projectRoot = args?.projectRoot; + const rawProjectRoot = args?.projectRoot; // If explicit path is provided and absolute, use it directly if (explicitPath && path.isAbsolute(explicitPath)) { return explicitPath; } - // If explicit path is relative, resolve it relative to projectRoot + // Normalize project root if provided + const projectRoot = rawProjectRoot + ? normalizeProjectRoot(rawProjectRoot) + : null; + + // If explicit path is relative, resolve it relative to normalized projectRoot if (explicitPath && projectRoot) { return path.resolve(projectRoot, explicitPath); } - // Use core findComplexityReportPath with explicit path and projectRoot context + // Use core findComplexityReportPath with explicit path and normalized projectRoot context if (projectRoot) { return coreFindComplexityReportPath(explicitPath, { projectRoot }, log); } @@ -142,13 +158,16 @@ export function resolveProjectPath(relativePath, args) { throw new Error('projectRoot is required in args to resolve project paths'); } + // Normalize the project root to prevent double .taskmaster paths + const projectRoot = normalizeProjectRoot(args.projectRoot); + // If already absolute, return as-is if (path.isAbsolute(relativePath)) { return relativePath; } - // Resolve relative to projectRoot - return path.resolve(args.projectRoot, relativePath); + // Resolve relative to normalized projectRoot + return path.resolve(projectRoot, relativePath); } /** diff --git a/mcp-server/src/tools/complexity-report.js b/mcp-server/src/tools/complexity-report.js index dbc03a86..3ae43127 100644 --- a/mcp-server/src/tools/complexity-report.js +++ b/mcp-server/src/tools/complexity-report.js @@ -11,7 +11,7 @@ import { } from './utils.js'; import { complexityReportDirect } from '../core/task-master-core.js'; import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js'; -import path from 'path'; +import { findComplexityReportPath } from '../core/utils/path-utils.js'; /** * Register the complexityReport tool with the MCP server @@ -38,10 +38,18 @@ export function registerComplexityReportTool(server) { `Getting complexity report with args: ${JSON.stringify(args)}` ); - // Use args.projectRoot directly (guaranteed by withNormalizedProjectRoot) - const reportPath = args.file - ? path.resolve(args.projectRoot, args.file) - : path.resolve(args.projectRoot, COMPLEXITY_REPORT_FILE); + const pathArgs = { + projectRoot: args.projectRoot, + complexityReport: args.file + }; + + const reportPath = findComplexityReportPath(pathArgs, log); + + if (!reportPath) { + return createErrorResponse( + 'No complexity report found. Run task-master analyze-complexity first.' + ); + } const result = await complexityReportDirect( { diff --git a/src/utils/path-utils.js b/src/utils/path-utils.js index c4f22cb0..406159a9 100644 --- a/src/utils/path-utils.js +++ b/src/utils/path-utils.js @@ -16,6 +16,32 @@ import { } from '../constants/paths.js'; import { getLoggerOrDefault } from './logger-utils.js'; +/** + * Normalize project root to ensure it doesn't end with .taskmaster + * This prevents double .taskmaster paths when using constants that include .taskmaster + * @param {string} projectRoot - The project root path to normalize + * @returns {string} - Normalized project root path + */ +export function normalizeProjectRoot(projectRoot) { + if (!projectRoot) return projectRoot; + + // Split the path into segments + const segments = projectRoot.split(path.sep); + + // Find the index of .taskmaster segment + const taskmasterIndex = segments.findIndex( + (segment) => segment === '.taskmaster' + ); + + if (taskmasterIndex !== -1) { + // If .taskmaster is found, return everything up to but not including .taskmaster + const normalizedSegments = segments.slice(0, taskmasterIndex); + return normalizedSegments.join(path.sep) || path.sep; + } + + return projectRoot; +} + /** * Find the project root directory by looking for project markers * @param {string} startDir - Directory to start searching from @@ -80,14 +106,17 @@ export function findTasksPath(explicitPath = null, args = null, log = null) { } // 2. Try to get project root from args (MCP) or find it - const projectRoot = args?.projectRoot || findProjectRoot(); + const rawProjectRoot = args?.projectRoot || findProjectRoot(); - if (!projectRoot) { + if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } - // 3. Check possible locations in order of preference + // 3. Normalize project root to prevent double .taskmaster paths + const projectRoot = normalizeProjectRoot(rawProjectRoot); + + // 4. Check possible locations in order of preference const possiblePaths = [ path.join(projectRoot, TASKMASTER_TASKS_FILE), // .taskmaster/tasks/tasks.json (NEW) path.join(projectRoot, 'tasks.json'), // tasks.json in root (LEGACY) @@ -151,14 +180,17 @@ export function findPRDPath(explicitPath = null, args = null, log = null) { } // 2. Try to get project root from args (MCP) or find it - const projectRoot = args?.projectRoot || findProjectRoot(); + const rawProjectRoot = args?.projectRoot || findProjectRoot(); - if (!projectRoot) { + if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } - // 3. Check possible locations in order of preference + // 3. Normalize project root to prevent double .taskmaster paths + const projectRoot = normalizeProjectRoot(rawProjectRoot); + + // 4. Check possible locations in order of preference const locations = [ TASKMASTER_DOCS_DIR, // .taskmaster/docs/ (NEW) 'scripts/', // Legacy location @@ -220,14 +252,17 @@ export function findComplexityReportPath( } // 2. Try to get project root from args (MCP) or find it - const projectRoot = args?.projectRoot || findProjectRoot(); + const rawProjectRoot = args?.projectRoot || findProjectRoot(); - if (!projectRoot) { + if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } - // 3. Check possible locations in order of preference + // 3. Normalize project root to prevent double .taskmaster paths + const projectRoot = normalizeProjectRoot(rawProjectRoot); + + // 4. Check possible locations in order of preference const locations = [ TASKMASTER_REPORTS_DIR, // .taskmaster/reports/ (NEW) 'scripts/', // Legacy location @@ -283,9 +318,13 @@ export function resolveTasksOutputPath( } // 2. Try to get project root from args (MCP) or find it - const projectRoot = args?.projectRoot || findProjectRoot() || process.cwd(); + const rawProjectRoot = + args?.projectRoot || findProjectRoot() || process.cwd(); - // 3. Use new .taskmaster structure by default + // 3. Normalize project root to prevent double .taskmaster paths + const projectRoot = normalizeProjectRoot(rawProjectRoot); + + // 4. Use new .taskmaster structure by default const defaultPath = path.join(projectRoot, TASKMASTER_TASKS_FILE); logger.info?.(`Using default output path: ${defaultPath}`); @@ -326,9 +365,13 @@ export function resolveComplexityReportOutputPath( } // 2. Try to get project root from args (MCP) or find it - const projectRoot = args?.projectRoot || findProjectRoot() || process.cwd(); + const rawProjectRoot = + args?.projectRoot || findProjectRoot() || process.cwd(); - // 3. Use new .taskmaster structure by default + // 3. Normalize project root to prevent double .taskmaster paths + const projectRoot = normalizeProjectRoot(rawProjectRoot); + + // 4. Use new .taskmaster structure by default const defaultPath = path.join(projectRoot, COMPLEXITY_REPORT_FILE); logger.info?.(`Using default complexity report output path: ${defaultPath}`); @@ -369,14 +412,17 @@ export function findConfigPath(explicitPath = null, args = null, log = null) { } // 2. Try to get project root from args (MCP) or find it - const projectRoot = args?.projectRoot || findProjectRoot(); + const rawProjectRoot = args?.projectRoot || findProjectRoot(); - if (!projectRoot) { + if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } - // 3. Check possible locations in order of preference + // 3. Normalize project root to prevent double .taskmaster paths + const projectRoot = normalizeProjectRoot(rawProjectRoot); + + // 4. Check possible locations in order of preference const possiblePaths = [ path.join(projectRoot, TASKMASTER_CONFIG_FILE), // NEW location path.join(projectRoot, LEGACY_CONFIG_FILE) // LEGACY location