Files
claude-task-master/src/task-master.js
Ralph Khreish 444aa5ae19 Batch fixes before release (#1011)
* 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
2025-07-20 00:51:41 +03:00

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);
}