/**
* 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;
}