Files
claude-task-master/src/utils/path-utils.js
2025-09-24 10:57:17 +02:00

480 lines
15 KiB
JavaScript

/**
* Path utility functions for Task Master
* Provides centralized path resolution logic for both CLI and MCP use cases
*/
import path from 'path';
import fs from 'fs';
import {
TASKMASTER_TASKS_FILE,
LEGACY_TASKS_FILE,
TASKMASTER_DOCS_DIR,
TASKMASTER_REPORTS_DIR,
COMPLEXITY_REPORT_FILE,
TASKMASTER_CONFIG_FILE,
LEGACY_CONFIG_FILE
} 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;
// Ensure it's a string
projectRoot = String(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
* @returns {string|null} - Project root path or null if not found
*/
export function findProjectRoot(startDir = process.cwd()) {
const projectMarkers = [
'.taskmaster',
TASKMASTER_TASKS_FILE,
'tasks.json',
LEGACY_TASKS_FILE,
'.git',
'.svn',
'package.json',
'yarn.lock',
'package-lock.json',
'pnpm-lock.yaml'
];
let currentDir = path.resolve(startDir);
const rootDir = path.parse(currentDir).root;
const maxDepth = 50; // Reasonable limit to prevent infinite loops
let depth = 0;
while (currentDir !== rootDir && depth < maxDepth) {
// Check if current directory contains any project markers
for (const marker of projectMarkers) {
const markerPath = path.join(currentDir, marker);
if (fs.existsSync(markerPath)) {
return currentDir;
}
}
currentDir = path.dirname(currentDir);
depth++;
}
// Fallback to current working directory if no project root found
return process.cwd();
}
/**
* 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) {
const resolvedPath = path.isAbsolute(explicitPath)
? explicitPath
: path.resolve(process.cwd(), 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) {
const resolvedPath = path.isAbsolute(explicitPath)
? explicitPath
: path.resolve(process.cwd(), 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) {
const resolvedPath = path.isAbsolute(explicitPath)
? explicitPath
: path.resolve(process.cwd(), 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) {
const resolvedPath = path.isAbsolute(explicitPath)
? explicitPath
: path.resolve(process.cwd(), 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) {
const resolvedPath = path.isAbsolute(explicitPath)
? explicitPath
: path.resolve(process.cwd(), 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
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;
}