Skip to main content
Glama
path-utils.js16 kB
/** * Path utility functions for Task Master * Provides centralized path resolution logic for both CLI and MCP use cases * * NOTE: This file is a legacy wrapper around @tm/core utilities. * New code should import directly from @tm/core instead. * This file exists for backward compatibility during the migration period. */ import fs from 'fs'; import path from 'path'; import { findProjectRoot as findProjectRootCore, normalizeProjectRoot as normalizeProjectRootCore } from '@tm/core'; import { COMPLEXITY_REPORT_FILE, LEGACY_CONFIG_FILE, LEGACY_TASKS_FILE, TASKMASTER_CONFIG_FILE, TASKMASTER_DOCS_DIR, TASKMASTER_REPORTS_DIR, TASKMASTER_TASKS_FILE } from '../constants/paths.js'; import { getLoggerOrDefault } from './logger-utils.js'; import { isConfigWarningSuppressed } from '../../scripts/modules/config-manager.js'; /** * Normalize project root to ensure it doesn't end with .taskmaster * This prevents double .taskmaster paths when using constants that include .taskmaster * * @deprecated Use the TypeScript implementation from @tm/core instead * @param {string} projectRoot - The project root path to normalize * @returns {string} - Normalized project root path */ export function normalizeProjectRoot(projectRoot) { return normalizeProjectRootCore(projectRoot); } /** * Find the project root directory by looking for project markers * Traverses upwards from startDir until a project marker is found or filesystem root is reached * Limited to 50 parent directory levels to prevent excessive traversal * * Strategy: First searches ALL parent directories for .taskmaster (highest priority). * If not found, then searches for other project markers starting from current directory. * This ensures .taskmaster in parent directories takes precedence over other markers in subdirectories. * * @deprecated Use the TypeScript implementation from @tm/core instead * @param {string} startDir - Directory to start searching from (defaults to process.cwd()) * @returns {string} - Project root path (falls back to current directory if no markers found) */ export function findProjectRoot(startDir = process.cwd()) { return findProjectRootCore(startDir); } /** * Find the tasks.json file path with fallback logic * @param {string|null} explicitPath - Explicit path provided by user (highest priority) * @param {Object|null} args - Args object from MCP args (optional) * @param {Object|null} log - Logger object (optional) * @returns {string|null} - Resolved tasks.json path or null if not found */ export function findTasksPath(explicitPath = null, args = null, log = null) { // Use the passed logger if available, otherwise use the default logger const logger = getLoggerOrDefault(log); // 1. First determine project root to use as base for all path resolution const rawProjectRoot = args?.projectRoot || findProjectRoot(); if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } // 2. Normalize project root to prevent double .taskmaster paths const projectRoot = normalizeProjectRoot(rawProjectRoot); // 3. If explicit path is provided, resolve it relative to project root (highest priority) if (explicitPath) { const resolvedPath = path.isAbsolute(explicitPath) ? explicitPath : path.resolve(projectRoot, explicitPath); if (fs.existsSync(resolvedPath)) { logger.info?.(`Using explicit tasks path: ${resolvedPath}`); return resolvedPath; } else { logger.warn?.( `Explicit tasks path not found: ${resolvedPath}, trying fallbacks` ); } } // 4. Check possible locations in order of preference const possiblePaths = [ path.join(projectRoot, TASKMASTER_TASKS_FILE), // .taskmaster/tasks/tasks.json (NEW) path.join(projectRoot, LEGACY_TASKS_FILE) // tasks/tasks.json (LEGACY) ]; for (const tasksPath of possiblePaths) { if (fs.existsSync(tasksPath)) { logger.info?.(`Found tasks file at: ${tasksPath}`); // Issue deprecation warning for legacy paths if ( tasksPath.includes('tasks/tasks.json') && !tasksPath.includes('.taskmaster') ) { logger.warn?.( `⚠️ DEPRECATION WARNING: Found tasks.json in legacy location '${tasksPath}'. Please migrate to the new .taskmaster directory structure. Run 'task-master migrate' to automatically migrate your project.` ); } else if ( tasksPath.endsWith('tasks.json') && !tasksPath.includes('.taskmaster') && !tasksPath.includes('tasks/') ) { logger.warn?.( `⚠️ DEPRECATION WARNING: Found tasks.json in legacy root location '${tasksPath}'. Please migrate to the new .taskmaster directory structure. Run 'task-master migrate' to automatically migrate your project.` ); } return tasksPath; } } logger.warn?.(`No tasks.json found in project: ${projectRoot}`); return null; } /** * Find the PRD document file path with fallback logic * @param {string|null} explicitPath - Explicit path provided by user (highest priority) * @param {Object|null} args - Args object for MCP context (optional) * @param {Object|null} log - Logger object (optional) * @returns {string|null} - Resolved PRD document path or null if not found */ export function findPRDPath(explicitPath = null, args = null, log = null) { const logger = getLoggerOrDefault(log); // 1. If explicit path is provided, use it (highest priority) if (explicitPath) { // Use original cwd if available (set by dev.js), otherwise current cwd // This ensures relative paths are resolved from where the user invoked the command const cwdForResolution = process.env.TASKMASTER_ORIGINAL_CWD || process.cwd(); const resolvedPath = path.isAbsolute(explicitPath) ? explicitPath : path.resolve(cwdForResolution, explicitPath); if (fs.existsSync(resolvedPath)) { logger.info?.(`Using explicit PRD path: ${resolvedPath}`); return resolvedPath; } else { logger.warn?.( `Explicit PRD path not found: ${resolvedPath}, trying fallbacks` ); } } // 2. Try to get project root from args (MCP) or find it const rawProjectRoot = args?.projectRoot || findProjectRoot(); if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } // 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 '' // Project root ]; const fileNames = ['PRD.md', 'prd.md', 'PRD.txt', 'prd.txt']; for (const location of locations) { for (const fileName of fileNames) { const prdPath = path.join(projectRoot, location, fileName); if (fs.existsSync(prdPath)) { logger.info?.(`Found PRD document at: ${prdPath}`); // Issue deprecation warning for legacy paths if (location === 'scripts/' || location === '') { logger.warn?.( `⚠️ DEPRECATION WARNING: Found PRD file in legacy location '${prdPath}'. Please migrate to .taskmaster/docs/ directory. Run 'task-master migrate' to automatically migrate your project.` ); } return prdPath; } } } logger.warn?.(`No PRD document found in project: ${projectRoot}`); return null; } /** * Find the complexity report file path with fallback logic * @param {string|null} explicitPath - Explicit path provided by user (highest priority) * @param {Object|null} args - Args object for MCP context (optional) * @param {Object|null} log - Logger object (optional) * @returns {string|null} - Resolved complexity report path or null if not found */ export function findComplexityReportPath( explicitPath = null, args = null, log = null ) { const logger = getLoggerOrDefault(log); // 1. If explicit path is provided, use it (highest priority) if (explicitPath) { // Use original cwd if available (set by dev.js), otherwise current cwd const cwdForResolution = process.env.TASKMASTER_ORIGINAL_CWD || process.cwd(); const resolvedPath = path.isAbsolute(explicitPath) ? explicitPath : path.resolve(cwdForResolution, explicitPath); if (fs.existsSync(resolvedPath)) { logger.info?.(`Using explicit complexity report path: ${resolvedPath}`); return resolvedPath; } else { logger.warn?.( `Explicit complexity report path not found: ${resolvedPath}, trying fallbacks` ); } } // 2. Try to get project root from args (MCP) or find it const rawProjectRoot = args?.projectRoot || findProjectRoot(); if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } // 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 '' // Project root ]; const fileNames = [ 'task-complexity-report', 'task-complexity', 'complexity-report' ].map((fileName) => { if (args?.tag && args?.tag !== 'master') { return `${fileName}_${args.tag}.json`; } return `${fileName}.json`; }); for (const location of locations) { for (const fileName of fileNames) { const reportPath = path.join(projectRoot, location, fileName); if (fs.existsSync(reportPath)) { logger.info?.(`Found complexity report at: ${reportPath}`); // Issue deprecation warning for legacy paths if (location === 'scripts/' || location === '') { logger.warn?.( `⚠️ DEPRECATION WARNING: Found complexity report in legacy location '${reportPath}'. Please migrate to .taskmaster/reports/ directory. Run 'task-master migrate' to automatically migrate your project.` ); } return reportPath; } } } logger.warn?.(`No complexity report found in project: ${projectRoot}`); return null; } /** * Resolve output path for tasks.json (create if needed) * @param {string|null} explicitPath - Explicit output path provided by user * @param {Object|null} args - Args object for MCP context (optional) * @param {Object|null} log - Logger object (optional) * @returns {string} - Resolved output path for tasks.json */ export function resolveTasksOutputPath( explicitPath = null, args = null, log = null ) { const logger = getLoggerOrDefault(log); // 1. If explicit path is provided, use it if (explicitPath) { // Use original cwd if available (set by dev.js), otherwise current cwd const cwdForResolution = process.env.TASKMASTER_ORIGINAL_CWD || process.cwd(); const resolvedPath = path.isAbsolute(explicitPath) ? explicitPath : path.resolve(cwdForResolution, explicitPath); logger.info?.(`Using explicit output path: ${resolvedPath}`); return resolvedPath; } // 2. Try to get project root from args (MCP) or find it const rawProjectRoot = args?.projectRoot || findProjectRoot() || process.cwd(); // 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}`); // Ensure the directory exists const outputDir = path.dirname(defaultPath); if (!fs.existsSync(outputDir)) { logger.info?.(`Creating tasks directory: ${outputDir}`); fs.mkdirSync(outputDir, { recursive: true }); } return defaultPath; } /** * Resolve output path for complexity report (create if needed) * @param {string|null} explicitPath - Explicit output path provided by user * @param {Object|null} args - Args object for MCP context (optional) * @param {Object|null} log - Logger object (optional) * @returns {string} - Resolved output path for complexity report */ export function resolveComplexityReportOutputPath( explicitPath = null, args = null, log = null ) { const logger = getLoggerOrDefault(log); const tag = args?.tag; // 1. If explicit path is provided, use it if (explicitPath) { // Use original cwd if available (set by dev.js), otherwise current cwd const cwdForResolution = process.env.TASKMASTER_ORIGINAL_CWD || process.cwd(); const resolvedPath = path.isAbsolute(explicitPath) ? explicitPath : path.resolve(cwdForResolution, explicitPath); logger.info?.( `Using explicit complexity report output path: ${resolvedPath}` ); return resolvedPath; } // 2. Try to get project root from args (MCP) or find it const rawProjectRoot = args?.projectRoot || findProjectRoot() || process.cwd(); const projectRoot = normalizeProjectRoot(rawProjectRoot); // 3. Use tag-aware filename let filename = 'task-complexity-report.json'; if (tag && tag !== 'master') { filename = `task-complexity-report_${tag}.json`; } // 4. Use new .taskmaster structure by default const defaultPath = path.join(projectRoot, '.taskmaster/reports', filename); logger.info?.( `Using tag-aware complexity report output path: ${defaultPath}` ); // Ensure the directory exists const outputDir = path.dirname(defaultPath); if (!fs.existsSync(outputDir)) { logger.info?.(`Creating reports directory: ${outputDir}`); fs.mkdirSync(outputDir, { recursive: true }); } return defaultPath; } /** * Find the configuration file path with fallback logic * @param {string|null} explicitPath - Explicit path provided by user (highest priority) * @param {Object|null} args - Args object for MCP context (optional) * @param {Object|null} log - Logger object (optional) * @returns {string|null} - Resolved config file path or null if not found */ export function findConfigPath(explicitPath = null, args = null, log = null) { const logger = getLoggerOrDefault(log); // 1. If explicit path is provided, use it (highest priority) if (explicitPath) { // Use original cwd if available (set by dev.js), otherwise current cwd const cwdForResolution = process.env.TASKMASTER_ORIGINAL_CWD || process.cwd(); const resolvedPath = path.isAbsolute(explicitPath) ? explicitPath : path.resolve(cwdForResolution, explicitPath); if (fs.existsSync(resolvedPath)) { logger.info?.(`Using explicit config path: ${resolvedPath}`); return resolvedPath; } else { logger.warn?.( `Explicit config path not found: ${resolvedPath}, trying fallbacks` ); } } // 2. Try to get project root from args (MCP) or find it const rawProjectRoot = args?.projectRoot || findProjectRoot(); if (!rawProjectRoot) { logger.warn?.('Could not determine project root directory'); return null; } // 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 ]; for (const configPath of possiblePaths) { if (fs.existsSync(configPath)) { // Issue deprecation warning for legacy paths if (configPath?.endsWith(LEGACY_CONFIG_FILE)) { logger.warn?.( `⚠️ DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.` ); } return configPath; } } // Only warn once per command execution to prevent spam during init // Skip warning if: // Global suppress flag is set (during API mode detection) const shouldSkipWarning = isConfigWarningSuppressed() || args?.storageType === 'api'; if (!shouldSkipWarning) { const warningKey = `config_warning_${projectRoot}`; if (!global._tmConfigWarningsThisRun) { global._tmConfigWarningsThisRun = new Set(); } if (!global._tmConfigWarningsThisRun.has(warningKey)) { global._tmConfigWarningsThisRun.add(warningKey); logger.warn?.(`No configuration file found in project: ${projectRoot}`); } } return null; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/eyaltoledano/claude-task-master'

If you have feedback or need assistance with the MCP directory API, please join our Discord server