* fix: improve projectRoot * fix: improve task-master lang command * feat: add documentation to the readme so more people can access it * fix: expand command subtask dependency validation * fix: update command more reliable with perplexity and other models * chore: fix CI * chore: implement requested changes * chore: fix CI
351 lines
8.9 KiB
JavaScript
351 lines
8.9 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,
|
|
COMPLEXITY_REPORT_FILE
|
|
} from './constants/paths.js';
|
|
import { findProjectRoot } from './utils/path-utils.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;
|
|
#tag;
|
|
|
|
/**
|
|
* The constructor is intended to be used only by the initTaskMaster factory function.
|
|
* @param {object} paths - A pre-resolved object of all application paths.
|
|
* @param {string|undefined} tag - The current tag.
|
|
*/
|
|
constructor(paths, tag) {
|
|
this.#paths = Object.freeze({ ...paths });
|
|
this.#tag = tag;
|
|
}
|
|
|
|
/**
|
|
* @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() {
|
|
if (this.#paths.complexityReportPath) {
|
|
return this.#paths.complexityReportPath;
|
|
}
|
|
|
|
const complexityReportFile =
|
|
this.getCurrentTag() !== 'master'
|
|
? COMPLEXITY_REPORT_FILE.replace(
|
|
'.json',
|
|
`_${this.getCurrentTag()}.json`
|
|
)
|
|
: COMPLEXITY_REPORT_FILE;
|
|
|
|
return path.join(this.#paths.projectRoot, complexityReportFile);
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
}
|
|
|
|
/**
|
|
* Gets the current tag from state.json or falls back to defaultTag from config
|
|
* @returns {string} The current tag name
|
|
*/
|
|
getCurrentTag() {
|
|
if (this.#tag) {
|
|
return this.#tag;
|
|
}
|
|
|
|
try {
|
|
// Try to read current tag from state.json using fs directly
|
|
if (fs.existsSync(this.#paths.statePath)) {
|
|
const rawState = fs.readFileSync(this.#paths.statePath, 'utf8');
|
|
const stateData = JSON.parse(rawState);
|
|
if (stateData && stateData.currentTag) {
|
|
return stateData.currentTag;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignore errors, fall back to default
|
|
}
|
|
|
|
// Fall back to defaultTag from config using fs directly
|
|
try {
|
|
if (fs.existsSync(this.#paths.configPath)) {
|
|
const rawConfig = fs.readFileSync(this.#paths.configPath, 'utf8');
|
|
const configData = JSON.parse(rawConfig);
|
|
if (configData && configData.global && configData.global.defaultTag) {
|
|
return configData.global.defaultTag;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
// Ignore errors, use hardcoded default
|
|
}
|
|
|
|
// Final fallback
|
|
return 'master';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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]
|
|
* @param {string} [overrides.tag]
|
|
* @returns {TaskMaster} An initialized TaskMaster instance.
|
|
*/
|
|
export function initTaskMaster(overrides = {}) {
|
|
const resolvePath = (
|
|
pathType,
|
|
override,
|
|
defaultPaths = [],
|
|
basePath = null,
|
|
createParentDirs = false
|
|
) => {
|
|
if (typeof override === 'string') {
|
|
const resolvedPath = path.isAbsolute(override)
|
|
? override
|
|
: path.resolve(basePath || process.cwd(), override);
|
|
|
|
if (createParentDirs) {
|
|
// For output paths, create parent directory if it doesn't exist
|
|
const parentDir = path.dirname(resolvedPath);
|
|
if (!fs.existsSync(parentDir)) {
|
|
try {
|
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Could not create directory for ${pathType}: ${parentDir}. Error: ${error.message}`
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
// Original validation logic
|
|
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 {
|
|
// findProjectRoot now always returns a value (fallback to cwd)
|
|
paths.projectRoot = findProjectRoot();
|
|
}
|
|
|
|
// 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,
|
|
true // Enable parent directory creation for output paths
|
|
);
|
|
}
|
|
|
|
return new TaskMaster(paths, overrides.tag);
|
|
}
|