- Added new CLI commands 'scope-up' and 'scope-down' with comma-separated ID support - Implemented strength levels (light/regular/heavy) and custom prompt functionality - Created core complexity adjustment logic with AI integration - Added MCP tool equivalents for integrated environments - Comprehensive error handling and task validation - Full test coverage with TDD approach - Updated task manager core and UI components Task 104: Implement 'scope-up' and 'scope-down' CLI Commands for Dynamic Task Complexity Adjustment - Complete implementation with CLI, MCP integration, and testing
469 lines
14 KiB
JavaScript
469 lines
14 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;
|
|
}
|
|
}
|
|
|
|
logger.warn?.(`No configuration file found in project: ${projectRoot}`);
|
|
return null;
|
|
}
|