298 lines
7.4 KiB
JavaScript
298 lines
7.4 KiB
JavaScript
/**
|
|
* task-master.js
|
|
* This module provides a centralized path management system for the Task Master application.
|
|
* It exports the TaskMaster class and the initTaskMaster factory function to create a single,
|
|
* authoritative source for all critical file and directory paths, resolving circular dependencies.
|
|
*/
|
|
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import {
|
|
TASKMASTER_DIR,
|
|
TASKMASTER_TASKS_FILE,
|
|
LEGACY_TASKS_FILE,
|
|
TASKMASTER_DOCS_DIR,
|
|
TASKMASTER_REPORTS_DIR,
|
|
TASKMASTER_CONFIG_FILE,
|
|
LEGACY_CONFIG_FILE
|
|
} from './constants/paths.js';
|
|
|
|
/**
|
|
* TaskMaster class manages all the paths for the application.
|
|
* An instance of this class is created by the initTaskMaster function.
|
|
*/
|
|
export class TaskMaster {
|
|
#paths;
|
|
|
|
/**
|
|
* The constructor is intended to be used only by the initTaskMaster factory function.
|
|
* @param {object} paths - A pre-resolved object of all application paths.
|
|
*/
|
|
constructor(paths) {
|
|
this.#paths = Object.freeze({ ...paths });
|
|
}
|
|
|
|
/**
|
|
* @returns {string|null} The absolute path to the project root.
|
|
*/
|
|
getProjectRoot() {
|
|
return this.#paths.projectRoot;
|
|
}
|
|
|
|
/**
|
|
* @returns {string|null} The absolute path to the .taskmaster directory.
|
|
*/
|
|
getTaskMasterDir() {
|
|
return this.#paths.taskMasterDir;
|
|
}
|
|
|
|
/**
|
|
* @returns {string|null} The absolute path to the tasks.json file.
|
|
*/
|
|
getTasksPath() {
|
|
return this.#paths.tasksPath;
|
|
}
|
|
|
|
/**
|
|
* @returns {string|null} The absolute path to the PRD file.
|
|
*/
|
|
getPrdPath() {
|
|
return this.#paths.prdPath;
|
|
}
|
|
|
|
/**
|
|
* @returns {string|null} The absolute path to the complexity report.
|
|
*/
|
|
getComplexityReportPath() {
|
|
return this.#paths.complexityReportPath;
|
|
}
|
|
|
|
/**
|
|
* @returns {string|null} The absolute path to the config.json file.
|
|
*/
|
|
getConfigPath() {
|
|
return this.#paths.configPath;
|
|
}
|
|
|
|
/**
|
|
* @returns {string|null} The absolute path to the state.json file.
|
|
*/
|
|
getStatePath() {
|
|
return this.#paths.statePath;
|
|
}
|
|
|
|
/**
|
|
* @returns {object} A frozen object containing all resolved paths.
|
|
*/
|
|
getAllPaths() {
|
|
return this.#paths;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Initializes a TaskMaster instance with resolved paths.
|
|
* This function centralizes path resolution logic.
|
|
*
|
|
* @param {object} [overrides={}] - An object with possible path overrides.
|
|
* @param {string} [overrides.projectRoot]
|
|
* @param {string} [overrides.tasksPath]
|
|
* @param {string} [overrides.prdPath]
|
|
* @param {string} [overrides.complexityReportPath]
|
|
* @param {string} [overrides.configPath]
|
|
* @param {string} [overrides.statePath]
|
|
* @returns {TaskMaster} An initialized TaskMaster instance.
|
|
*/
|
|
export function initTaskMaster(overrides = {}) {
|
|
const findProjectRoot = (startDir = process.cwd()) => {
|
|
const projectMarkers = [TASKMASTER_DIR, LEGACY_CONFIG_FILE];
|
|
let currentDir = path.resolve(startDir);
|
|
const rootDir = path.parse(currentDir).root;
|
|
while (currentDir !== rootDir) {
|
|
for (const marker of projectMarkers) {
|
|
const markerPath = path.join(currentDir, marker);
|
|
if (fs.existsSync(markerPath)) {
|
|
return currentDir;
|
|
}
|
|
}
|
|
currentDir = path.dirname(currentDir);
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const resolvePath = (
|
|
pathType,
|
|
override,
|
|
defaultPaths = [],
|
|
basePath = null
|
|
) => {
|
|
if (typeof override === 'string') {
|
|
const resolvedPath = path.isAbsolute(override)
|
|
? override
|
|
: path.resolve(basePath || process.cwd(), override);
|
|
|
|
if (!fs.existsSync(resolvedPath)) {
|
|
throw new Error(
|
|
`${pathType} override path does not exist: ${resolvedPath}`
|
|
);
|
|
}
|
|
return resolvedPath;
|
|
}
|
|
|
|
if (override === true) {
|
|
// Required path - search defaults and fail if not found
|
|
for (const defaultPath of defaultPaths) {
|
|
const fullPath = path.isAbsolute(defaultPath)
|
|
? defaultPath
|
|
: path.join(basePath || process.cwd(), defaultPath);
|
|
if (fs.existsSync(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
}
|
|
throw new Error(
|
|
`Required ${pathType} not found. Searched: ${defaultPaths.join(', ')}`
|
|
);
|
|
}
|
|
|
|
// Optional path (override === false/undefined) - search defaults, return null if not found
|
|
for (const defaultPath of defaultPaths) {
|
|
const fullPath = path.isAbsolute(defaultPath)
|
|
? defaultPath
|
|
: path.join(basePath || process.cwd(), defaultPath);
|
|
if (fs.existsSync(fullPath)) {
|
|
return fullPath;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const paths = {};
|
|
|
|
// Project Root
|
|
if (overrides.projectRoot) {
|
|
const resolvedOverride = path.resolve(overrides.projectRoot);
|
|
if (!fs.existsSync(resolvedOverride)) {
|
|
throw new Error(
|
|
`Project root override path does not exist: ${resolvedOverride}`
|
|
);
|
|
}
|
|
|
|
const hasTaskmasterDir = fs.existsSync(
|
|
path.join(resolvedOverride, TASKMASTER_DIR)
|
|
);
|
|
const hasLegacyConfig = fs.existsSync(
|
|
path.join(resolvedOverride, LEGACY_CONFIG_FILE)
|
|
);
|
|
|
|
if (!hasTaskmasterDir && !hasLegacyConfig) {
|
|
throw new Error(
|
|
`Project root override is not a valid taskmaster project: ${resolvedOverride}`
|
|
);
|
|
}
|
|
|
|
paths.projectRoot = resolvedOverride;
|
|
} else {
|
|
const foundRoot = findProjectRoot();
|
|
if (!foundRoot) {
|
|
throw new Error(
|
|
'Unable to find project root. No project markers found. Run "init" command first.'
|
|
);
|
|
}
|
|
paths.projectRoot = foundRoot;
|
|
}
|
|
|
|
// TaskMaster Directory
|
|
if ('taskMasterDir' in overrides) {
|
|
paths.taskMasterDir = resolvePath(
|
|
'taskmaster directory',
|
|
overrides.taskMasterDir,
|
|
[TASKMASTER_DIR],
|
|
paths.projectRoot
|
|
);
|
|
} else {
|
|
paths.taskMasterDir = resolvePath(
|
|
'taskmaster directory',
|
|
false,
|
|
[TASKMASTER_DIR],
|
|
paths.projectRoot
|
|
);
|
|
}
|
|
|
|
// Always set default paths first
|
|
// These can be overridden below if needed
|
|
paths.configPath = path.join(paths.projectRoot, TASKMASTER_CONFIG_FILE);
|
|
paths.statePath = path.join(
|
|
paths.taskMasterDir || path.join(paths.projectRoot, TASKMASTER_DIR),
|
|
'state.json'
|
|
);
|
|
paths.tasksPath = path.join(paths.projectRoot, TASKMASTER_TASKS_FILE);
|
|
|
|
// Handle overrides - only validate/resolve if explicitly provided
|
|
if ('configPath' in overrides) {
|
|
paths.configPath = resolvePath(
|
|
'config file',
|
|
overrides.configPath,
|
|
[TASKMASTER_CONFIG_FILE, LEGACY_CONFIG_FILE],
|
|
paths.projectRoot
|
|
);
|
|
}
|
|
|
|
if ('statePath' in overrides) {
|
|
paths.statePath = resolvePath(
|
|
'state file',
|
|
overrides.statePath,
|
|
['state.json'],
|
|
paths.taskMasterDir
|
|
);
|
|
}
|
|
|
|
if ('tasksPath' in overrides) {
|
|
paths.tasksPath = resolvePath(
|
|
'tasks file',
|
|
overrides.tasksPath,
|
|
[TASKMASTER_TASKS_FILE, LEGACY_TASKS_FILE],
|
|
paths.projectRoot
|
|
);
|
|
}
|
|
|
|
if ('prdPath' in overrides) {
|
|
paths.prdPath = resolvePath(
|
|
'PRD file',
|
|
overrides.prdPath,
|
|
[
|
|
path.join(TASKMASTER_DOCS_DIR, 'PRD.md'),
|
|
path.join(TASKMASTER_DOCS_DIR, 'prd.md'),
|
|
path.join(TASKMASTER_DOCS_DIR, 'PRD.txt'),
|
|
path.join(TASKMASTER_DOCS_DIR, 'prd.txt'),
|
|
path.join('scripts', 'PRD.md'),
|
|
path.join('scripts', 'prd.md'),
|
|
path.join('scripts', 'PRD.txt'),
|
|
path.join('scripts', 'prd.txt'),
|
|
'PRD.md',
|
|
'prd.md',
|
|
'PRD.txt',
|
|
'prd.txt'
|
|
],
|
|
paths.projectRoot
|
|
);
|
|
}
|
|
|
|
if ('complexityReportPath' in overrides) {
|
|
paths.complexityReportPath = resolvePath(
|
|
'complexity report',
|
|
overrides.complexityReportPath,
|
|
[
|
|
path.join(TASKMASTER_REPORTS_DIR, 'task-complexity-report.json'),
|
|
path.join(TASKMASTER_REPORTS_DIR, 'complexity-report.json'),
|
|
path.join('scripts', 'task-complexity-report.json'),
|
|
path.join('scripts', 'complexity-report.json'),
|
|
'task-complexity-report.json',
|
|
'complexity-report.json'
|
|
],
|
|
paths.projectRoot
|
|
);
|
|
}
|
|
|
|
return new TaskMaster(paths);
|
|
}
|