feat: Add .taskmaster directory (#619)

This commit is contained in:
Ralph Khreish
2025-05-31 16:21:03 +02:00
committed by Eyal Toledano
parent 78397fe0be
commit 518f73eefa
148 changed files with 3523 additions and 7642 deletions

View File

@@ -25,6 +25,17 @@ import gradient from 'gradient-string';
import { isSilentMode } from './modules/utils.js';
import { convertAllCursorRulesToRooRules } from './modules/rule-transformer.js';
import { execSync } from 'child_process';
import {
EXAMPLE_PRD_FILE,
TASKMASTER_CONFIG_FILE,
TASKMASTER_TEMPLATES_DIR,
TASKMASTER_DIR,
TASKMASTER_TASKS_DIR,
TASKMASTER_DOCS_DIR,
TASKMASTER_REPORTS_DIR,
ENV_EXAMPLE_FILE,
GITIGNORE_FILE
} from '../src/constants/paths.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
@@ -162,8 +173,7 @@ alias taskmaster='task-master'
log('success', `Added Task Master aliases to ${shellConfigFile}`);
log(
'info',
'To use the aliases in your current terminal, run: source ' +
shellConfigFile
`To use the aliases in your current terminal, run: source ${shellConfigFile}`
);
return true;
@@ -233,7 +243,7 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
case 'boomerang-rules':
case 'code-rules':
case 'debug-rules':
case 'test-rules':
case 'test-rules': {
// Extract the mode name from the template name (e.g., 'architect' from 'architect-rules')
const mode = templateName.split('-')[0];
sourcePath = path.join(
@@ -246,6 +256,7 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
templateName
);
break;
}
default:
// For other files like env.example, gitignore, etc. that don't have direct equivalents
sourcePath = path.join(__dirname, '..', 'assets', templateName);
@@ -286,10 +297,7 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
if (newLines.length > 0) {
// Add a comment to separate the original content from our additions
const updatedContent =
existingContent.trim() +
'\n\n# Added by Claude Task Master\n' +
newLines.join('\n');
const updatedContent = `${existingContent.trim()}\n\n# Added by Claude Task Master\n${newLines.join('\n')}`;
fs.writeFileSync(targetPath, updatedContent);
log('success', `Updated ${targetPath} with additional entries`);
} else {
@@ -307,10 +315,7 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
const existingContent = fs.readFileSync(targetPath, 'utf8');
// Add a separator comment before appending our content
const updatedContent =
existingContent.trim() +
'\n\n# Added by Task Master - Development Workflow Rules\n\n' +
content;
const updatedContent = `${existingContent.trim()}\n\n# Added by Task Master - Development Workflow Rules\n\n${content}`;
fs.writeFileSync(targetPath, updatedContent);
log('success', `Updated ${targetPath} with additional rules`);
return;
@@ -390,7 +395,7 @@ async function initializeProject(options = {}) {
};
}
createProjectStructure(addAliases, dryRun);
createProjectStructure(addAliases, dryRun, options);
} else {
// Interactive logic
log('info', 'Required options not provided, proceeding with prompts.');
@@ -446,7 +451,7 @@ async function initializeProject(options = {}) {
}
// Create structure using only necessary values
createProjectStructure(addAliasesPrompted, dryRun);
createProjectStructure(addAliasesPrompted, dryRun, options);
} catch (error) {
rl.close();
log('error', `Error during initialization process: ${error.message}`);
@@ -465,29 +470,29 @@ function promptQuestion(rl, question) {
}
// Function to create the project structure
function createProjectStructure(addAliases, dryRun) {
function createProjectStructure(addAliases, dryRun, options) {
const targetDir = process.cwd();
log('info', `Initializing project in ${targetDir}`);
// Define Roo modes locally (external integration, not part of core Task Master)
const ROO_MODES = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];
// Create directories
ensureDirectoryExists(path.join(targetDir, '.cursor', 'rules'));
ensureDirectoryExists(path.join(targetDir, '.cursor/rules'));
// Create Roo directories
ensureDirectoryExists(path.join(targetDir, '.roo'));
ensureDirectoryExists(path.join(targetDir, '.roo', 'rules'));
for (const mode of [
'architect',
'ask',
'boomerang',
'code',
'debug',
'test'
]) {
ensureDirectoryExists(path.join(targetDir, '.roo/rules'));
for (const mode of ROO_MODES) {
ensureDirectoryExists(path.join(targetDir, '.roo', `rules-${mode}`));
}
ensureDirectoryExists(path.join(targetDir, 'scripts'));
ensureDirectoryExists(path.join(targetDir, 'tasks'));
// Create NEW .taskmaster directory structure (using constants)
ensureDirectoryExists(path.join(targetDir, TASKMASTER_DIR));
ensureDirectoryExists(path.join(targetDir, TASKMASTER_TASKS_DIR));
ensureDirectoryExists(path.join(targetDir, TASKMASTER_DOCS_DIR));
ensureDirectoryExists(path.join(targetDir, TASKMASTER_REPORTS_DIR));
ensureDirectoryExists(path.join(targetDir, TASKMASTER_TEMPLATES_DIR));
// Setup MCP configuration for integration with Cursor
setupMCPConfiguration(targetDir);
@@ -500,44 +505,44 @@ function createProjectStructure(addAliases, dryRun) {
// Copy .env.example
copyTemplateFile(
'env.example',
path.join(targetDir, '.env.example'),
path.join(targetDir, ENV_EXAMPLE_FILE),
replacements
);
// Copy .taskmasterconfig with project name
// Copy .taskmasterconfig with project name to NEW location
copyTemplateFile(
'.taskmasterconfig',
path.join(targetDir, '.taskmasterconfig'),
path.join(targetDir, TASKMASTER_CONFIG_FILE),
{
...replacements
}
);
// Copy .gitignore
copyTemplateFile('gitignore', path.join(targetDir, '.gitignore'));
copyTemplateFile('gitignore', path.join(targetDir, GITIGNORE_FILE));
// Copy dev_workflow.mdc
copyTemplateFile(
'dev_workflow.mdc',
path.join(targetDir, '.cursor', 'rules', 'dev_workflow.mdc')
path.join(targetDir, '.cursor/rules/dev_workflow.mdc')
);
// Copy taskmaster.mdc
copyTemplateFile(
'taskmaster.mdc',
path.join(targetDir, '.cursor', 'rules', 'taskmaster.mdc')
path.join(targetDir, '.cursor/rules/taskmaster.mdc')
);
// Copy cursor_rules.mdc
copyTemplateFile(
'cursor_rules.mdc',
path.join(targetDir, '.cursor', 'rules', 'cursor_rules.mdc')
path.join(targetDir, '.cursor/rules/cursor_rules.mdc')
);
// Copy self_improve.mdc
copyTemplateFile(
'self_improve.mdc',
path.join(targetDir, '.cursor', 'rules', 'self_improve.mdc')
path.join(targetDir, '.cursor/rules/self_improve.mdc')
);
// Generate Roo rules from Cursor rules
@@ -551,26 +556,15 @@ function createProjectStructure(addAliases, dryRun) {
copyTemplateFile('.roomodes', path.join(targetDir, '.roomodes'));
// Copy Roo rule files for each mode
const rooModes = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];
for (const mode of rooModes) {
for (const mode of ROO_MODES) {
copyTemplateFile(
`${mode}-rules`,
path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`)
);
}
// Copy example_prd.txt
copyTemplateFile(
'example_prd.txt',
path.join(targetDir, 'scripts', 'example_prd.txt')
);
// // Create main README.md
// copyTemplateFile(
// 'README-task-master.md',
// path.join(targetDir, 'README-task-master.md'),
// replacements
// );
// Copy example_prd.txt to NEW location
copyTemplateFile('example_prd.txt', path.join(targetDir, EXAMPLE_PRD_FILE));
// Initialize git repository if git is available
try {
@@ -607,7 +601,7 @@ function createProjectStructure(addAliases, dryRun) {
}
// === Add Model Configuration Step ===
if (!isSilentMode() && !dryRun) {
if (!isSilentMode() && !dryRun && !options?.yes) {
console.log(
boxen(chalk.cyan('Configuring AI Models...'), {
padding: 0.5,
@@ -638,6 +632,12 @@ function createProjectStructure(addAliases, dryRun) {
);
} else if (dryRun) {
log('info', 'DRY RUN: Skipping interactive model setup.');
} else if (options?.yes) {
log('info', 'Skipping interactive model setup due to --yes flag.');
log(
'info',
'You can configure AI models later using "task-master models --setup" or "task-master models --set-..." commands.'
);
}
// ====================================
@@ -645,11 +645,9 @@ function createProjectStructure(addAliases, dryRun) {
if (!isSilentMode()) {
console.log(
boxen(
warmGradient.multiline(
`${warmGradient.multiline(
figlet.textSync('Success!', { font: 'Standard' })
) +
'\n' +
chalk.green('Project initialized successfully!'),
)}\n${chalk.green('Project initialized successfully!')}`,
{
padding: 1,
margin: 1,
@@ -664,76 +662,27 @@ function createProjectStructure(addAliases, dryRun) {
if (!isSilentMode()) {
console.log(
boxen(
chalk.cyan.bold('Things you should do next:') +
'\n\n' +
chalk.white('1. ') +
chalk.yellow(
'Configure AI models (if needed) and add API keys to `.env`'
) +
'\n' +
chalk.white(' ├─ ') +
chalk.dim('Models: Use `task-master models` commands') +
'\n' +
chalk.white(' └─ ') +
chalk.dim(
'Keys: Add provider API keys to .env (or inside the MCP config file i.e. .cursor/mcp.json)'
) +
'\n' +
chalk.white('2. ') +
chalk.yellow(
'Discuss your idea with AI and ask for a PRD using example_prd.txt, and save it to scripts/PRD.txt'
) +
'\n' +
chalk.white('3. ') +
chalk.yellow(
'Ask Cursor Agent (or run CLI) to parse your PRD and generate initial tasks:'
) +
'\n' +
chalk.white(' └─ ') +
chalk.dim('MCP Tool: ') +
chalk.cyan('parse_prd') +
chalk.dim(' | CLI: ') +
chalk.cyan('task-master parse-prd scripts/prd.txt') +
'\n' +
chalk.white('4. ') +
chalk.yellow(
'Ask Cursor to analyze the complexity of the tasks in your PRD using research'
) +
'\n' +
chalk.white(' └─ ') +
chalk.dim('MCP Tool: ') +
chalk.cyan('analyze_project_complexity') +
chalk.dim(' | CLI: ') +
chalk.cyan('task-master analyze-complexity') +
'\n' +
chalk.white('5. ') +
chalk.yellow(
'Ask Cursor to expand all of your tasks using the complexity analysis'
) +
'\n' +
chalk.white('6. ') +
chalk.yellow('Ask Cursor to begin working on the next task') +
'\n' +
chalk.white('7. ') +
chalk.yellow(
'Ask Cursor to set the status of one or many tasks/subtasks at a time. Use the task id from the task lists.'
) +
'\n' +
chalk.white('8. ') +
chalk.yellow(
'Ask Cursor to update all tasks from a specific task id based on new learnings or pivots in your project.'
) +
'\n' +
chalk.white('9. ') +
chalk.green.bold('Ship it!') +
'\n\n' +
chalk.dim(
'* Review the README.md file to learn how to use other commands via Cursor Agent.'
) +
'\n' +
chalk.dim(
'* Use the task-master command without arguments to see all available commands.'
),
`${chalk.cyan.bold('Things you should do next:')}\n\n${chalk.white('1. ')}${chalk.yellow(
'Configure AI models (if needed) and add API keys to `.env`'
)}\n${chalk.white(' ├─ ')}${chalk.dim('Models: Use `task-master models` commands')}\n${chalk.white(' └─ ')}${chalk.dim(
'Keys: Add provider API keys to .env (or inside the MCP config file i.e. .cursor/mcp.json)'
)}\n${chalk.white('2. ')}${chalk.yellow(
'Discuss your idea with AI and ask for a PRD using example_prd.txt, and save it to scripts/PRD.txt'
)}\n${chalk.white('3. ')}${chalk.yellow(
'Ask Cursor Agent (or run CLI) to parse your PRD and generate initial tasks:'
)}\n${chalk.white(' └─ ')}${chalk.dim('MCP Tool: ')}${chalk.cyan('parse_prd')}${chalk.dim(' | CLI: ')}${chalk.cyan('task-master parse-prd scripts/prd.txt')}\n${chalk.white('4. ')}${chalk.yellow(
'Ask Cursor to analyze the complexity of the tasks in your PRD using research'
)}\n${chalk.white(' └─ ')}${chalk.dim('MCP Tool: ')}${chalk.cyan('analyze_project_complexity')}${chalk.dim(' | CLI: ')}${chalk.cyan('task-master analyze-complexity')}\n${chalk.white('5. ')}${chalk.yellow(
'Ask Cursor to expand all of your tasks using the complexity analysis'
)}\n${chalk.white('6. ')}${chalk.yellow('Ask Cursor to begin working on the next task')}\n${chalk.white('7. ')}${chalk.yellow(
'Ask Cursor to set the status of one or many tasks/subtasks at a time. Use the task id from the task lists.'
)}\n${chalk.white('8. ')}${chalk.yellow(
'Ask Cursor to update all tasks from a specific task id based on new learnings or pivots in your project.'
)}\n${chalk.white('9. ')}${chalk.green.bold('Ship it!')}\n\n${chalk.dim(
'* Review the README.md file to learn how to use other commands via Cursor Agent.'
)}\n${chalk.dim(
'* Use the task-master command without arguments to see all available commands.'
)}`,
{
padding: 1,
margin: 1,

View File

@@ -32,7 +32,8 @@ import {
removeTask,
findTaskById,
taskExists,
moveTask
moveTask,
migrateProject
} from './task-manager.js';
import {
@@ -53,6 +54,12 @@ import {
getBaseUrlForRole
} from './config-manager.js';
import {
COMPLEXITY_REPORT_FILE,
PRD_FILE,
TASKMASTER_TASKS_FILE
} from '../../src/constants/paths.js';
import {
displayBanner,
displayHelp,
@@ -66,7 +73,6 @@ import {
displayModelConfiguration,
displayAvailableModels,
displayApiKeyStatus,
displayAiUsageSummary,
displayMultipleTasksSummary
} from './ui.js';
@@ -82,6 +88,7 @@ import {
TASK_STATUS_OPTIONS
} from '../../src/constants/task-status.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
/**
* Runs the interactive setup process for model configuration.
* @param {string|null} projectRoot - The resolved project root directory.
@@ -298,7 +305,7 @@ async function runInteractiveSetup(projectRoot) {
commonPrefix.push(customOllamaOption);
commonPrefix.push(customBedrockOption);
let prefixLength = commonPrefix.length; // Initial prefix length
const prefixLength = commonPrefix.length; // Initial prefix length
if (allowNone) {
choices = [
@@ -491,7 +498,7 @@ async function runInteractiveSetup(projectRoot) {
) {
console.error(
chalk.red(
`Error: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Please set them before using custom Bedrock models.`
'Error: AWS_ACCESS_KEY_ID and/or AWS_SECRET_ACCESS_KEY environment variables are missing. Please set them before using custom Bedrock models.'
)
);
setupSuccess = false;
@@ -649,7 +656,7 @@ function registerCommands(programInstance) {
'-i, --input <file>',
'Path to the PRD file (alternative to positional argument)'
)
.option('-o, --output <file>', 'Output file path', 'tasks/tasks.json')
.option('-o, --output <file>', 'Output file path', TASKMASTER_TASKS_FILE)
.option('-n, --num-tasks <number>', 'Number of tasks to generate', '10')
.option('-f, --force', 'Skip confirmation when overwriting existing tasks')
.option(
@@ -663,14 +670,14 @@ function registerCommands(programInstance) {
.action(async (file, options) => {
// Use input option if file argument not provided
const inputFile = file || options.input;
const defaultPrdPath = 'scripts/prd.txt';
const defaultPrdPath = PRD_FILE;
const numTasks = parseInt(options.numTasks, 10);
const outputPath = options.output;
const force = options.force || false;
const append = options.append || false;
const research = options.research || false;
let useForce = force;
let useAppend = append;
const useAppend = append;
// Helper function to check if tasks.json exists and confirm overwrite
async function confirmOverwriteIfNeeded() {
@@ -710,38 +717,12 @@ function registerCommands(programInstance) {
console.log(
chalk.yellow(
'No PRD file specified and default PRD file not found at scripts/prd.txt.'
`No PRD file specified and default PRD file not found at ${PRD_FILE}.`
)
);
console.log(
boxen(
chalk.white.bold('Parse PRD Help') +
'\n\n' +
chalk.cyan('Usage:') +
'\n' +
` task-master parse-prd <prd-file.txt> [options]\n\n` +
chalk.cyan('Options:') +
'\n' +
' -i, --input <file> Path to the PRD file (alternative to positional argument)\n' +
' -o, --output <file> Output file path (default: "tasks/tasks.json")\n' +
' -n, --num-tasks <number> Number of tasks to generate (default: 10)\n' +
' -f, --force Skip confirmation when overwriting existing tasks\n' +
' --append Append new tasks to existing tasks.json instead of overwriting\n' +
' -r, --research Use Perplexity AI for research-backed task generation\n\n' +
chalk.cyan('Example:') +
'\n' +
' task-master parse-prd requirements.txt --num-tasks 15\n' +
' task-master parse-prd --input=requirements.txt\n' +
' task-master parse-prd --force\n' +
' task-master parse-prd requirements_v2.txt --append\n' +
' task-master parse-prd requirements.txt --research\n\n' +
chalk.yellow('Note: This command will:') +
'\n' +
' 1. Look for a PRD file at scripts/prd.txt by default\n' +
' 2. Use the file specified by --input or positional argument if provided\n' +
' 3. Generate tasks from the PRD and either:\n' +
' - Overwrite any existing tasks.json file (default)\n' +
' - Append to existing tasks.json if --append is used',
`${chalk.white.bold('Parse PRD Help')}\n\n${chalk.cyan('Usage:')}\n task-master parse-prd <prd-file.txt> [options]\n\n${chalk.cyan('Options:')}\n -i, --input <file> Path to the PRD file (alternative to positional argument)\n -o, --output <file> Output file path (default: "${TASKMASTER_TASKS_FILE}")\n -n, --num-tasks <number> Number of tasks to generate (default: 10)\n -f, --force Skip confirmation when overwriting existing tasks\n --append Append new tasks to existing tasks.json instead of overwriting\n -r, --research Use Perplexity AI for research-backed task generation\n\n${chalk.cyan('Example:')}\n task-master parse-prd requirements.txt --num-tasks 15\n task-master parse-prd --input=requirements.txt\n task-master parse-prd --force\n task-master parse-prd requirements_v2.txt --append\n task-master parse-prd requirements.txt --research\n\n${chalk.yellow('Note: This command will:')}\n 1. Look for a PRD file at ${PRD_FILE} by default\n 2. Use the file specified by --input or positional argument if provided\n 3. Generate tasks from the PRD and either:\n - Overwrite any existing tasks.json file (default)\n - Append to existing tasks.json if --append is used`,
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
)
);
@@ -793,7 +774,11 @@ function registerCommands(programInstance) {
.description(
'Update multiple tasks with ID >= "from" based on new information or implementation changes'
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'--from <id>',
'Task ID to start updating from (tasks with ID >= this value will be updated)',
@@ -808,7 +793,7 @@ function registerCommands(programInstance) {
'Use Perplexity AI for research-backed task updates'
)
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const fromId = parseInt(options.from, 10); // Validation happens here
const prompt = options.prompt;
const useResearch = options.research || false;
@@ -874,7 +859,11 @@ function registerCommands(programInstance) {
.description(
'Update a single specific task by ID with new information (use --id parameter)'
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option('-i, --id <id>', 'Task ID to update (required)')
.option(
'-p, --prompt <text>',
@@ -886,7 +875,7 @@ function registerCommands(programInstance) {
)
.action(async (options) => {
try {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
// Validate required parameters
if (!options.id) {
@@ -901,7 +890,7 @@ function registerCommands(programInstance) {
// Parse the task ID and validate it's a number
const taskId = parseInt(options.id, 10);
if (isNaN(taskId) || taskId <= 0) {
if (Number.isNaN(taskId) || taskId <= 0) {
console.error(
chalk.red(
`Error: Invalid task ID: ${options.id}. Task ID must be a positive integer.`
@@ -937,7 +926,7 @@ function registerCommands(programInstance) {
console.error(
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
);
if (tasksPath === 'tasks/tasks.json') {
if (tasksPath === TASKMASTER_TASKS_FILE) {
console.log(
chalk.yellow(
'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
@@ -1027,7 +1016,11 @@ function registerCommands(programInstance) {
.description(
'Update a subtask by appending additional timestamped information'
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-i, --id <id>',
'Subtask ID to update in format "parentId.subtaskId" (required)'
@@ -1039,7 +1032,7 @@ function registerCommands(programInstance) {
.option('-r, --research', 'Use Perplexity AI for research-backed updates')
.action(async (options) => {
try {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
// Validate required parameters
if (!options.id) {
@@ -1090,7 +1083,7 @@ function registerCommands(programInstance) {
console.error(
chalk.red(`Error: Tasks file not found at path: ${tasksPath}`)
);
if (tasksPath === 'tasks/tasks.json') {
if (tasksPath === TASKMASTER_TASKS_FILE) {
console.log(
chalk.yellow(
'Hint: Run task-master init or task-master parse-prd to create tasks.json first'
@@ -1181,10 +1174,14 @@ function registerCommands(programInstance) {
programInstance
.command('generate')
.description('Generate task files from tasks.json')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option('-o, --output <dir>', 'Output directory', 'tasks')
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const outputDir = options.output;
console.log(chalk.blue(`Generating task files from: ${tasksPath}`));
@@ -1207,9 +1204,13 @@ function registerCommands(programInstance) {
'-s, --status <status>',
`New status (one of: ${TASK_STATUS_OPTIONS.join(', ')})`
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const taskId = options.id;
const status = options.status;
@@ -1239,16 +1240,20 @@ function registerCommands(programInstance) {
programInstance
.command('list')
.description('List all tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-r, --report <report>',
'Path to the complexity report file',
'scripts/task-complexity-report.json'
COMPLEXITY_REPORT_FILE
)
.option('-s, --status <status>', 'Filter by status')
.option('--with-subtasks', 'Show subtasks for each task')
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const reportPath = options.report;
const statusFilter = options.status;
const withSubtasks = options.withSubtasks || false;
@@ -1287,7 +1292,7 @@ function registerCommands(programInstance) {
.option(
'--file <file>',
'Path to the tasks file (relative to project root)',
'tasks/tasks.json'
TASKMASTER_TASKS_FILE // Allow file override
) // Allow file override
.action(async (options) => {
const projectRoot = findProjectRoot();
@@ -1362,7 +1367,7 @@ function registerCommands(programInstance) {
.option(
'-o, --output <file>',
'Output file path for the report',
'scripts/task-complexity-report.json'
COMPLEXITY_REPORT_FILE
)
.option(
'-m, --model <model>',
@@ -1373,7 +1378,11 @@ function registerCommands(programInstance) {
'Minimum complexity score to recommend expansion (1-10)',
'5'
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-r, --research',
'Use Perplexity AI for research-backed complexity analysis'
@@ -1385,7 +1394,7 @@ function registerCommands(programInstance) {
.option('--from <id>', 'Starting task ID in a range to analyze')
.option('--to <id>', 'Ending task ID in a range to analyze')
.action(async (options) => {
const tasksPath = options.file || 'tasks/tasks.json';
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const outputPath = options.output;
const modelOverride = options.model;
const thresholdScore = parseFloat(options.threshold);
@@ -1657,14 +1666,18 @@ ${result.result}
programInstance
.command('clear-subtasks')
.description('Clear subtasks from specified tasks')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-i, --id <ids>',
'Task IDs (comma-separated) to clear subtasks from'
)
.option('--all', 'Clear subtasks from all tasks')
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const taskIds = options.id;
const all = options.all;
@@ -1695,7 +1708,11 @@ ${result.result}
programInstance
.command('add-task')
.description('Add a new task using AI, optionally providing manual details')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-p, --prompt <prompt>',
'Description of the task to add (required if not using manual fields)'
@@ -1735,10 +1752,14 @@ ${result.result}
process.exit(1);
}
const tasksPath =
options.file ||
path.join(findProjectRoot() || '.', 'tasks', 'tasks.json') || // Ensure tasksPath is also relative to a found root or current dir
'tasks/tasks.json';
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
if (!fs.existsSync(tasksPath)) {
console.error(
`❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file at ${TASKMASTER_TASKS_FILE}`
);
process.exit(1);
}
// Correctly determine projectRoot
const projectRoot = findProjectRoot();
@@ -1810,15 +1831,20 @@ ${result.result}
.description(
`Show the next task to work on based on dependencies and status${chalk.reset('')}`
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-r, --report <report>',
'Path to the complexity report file',
'scripts/task-complexity-report.json'
COMPLEXITY_REPORT_FILE
)
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const reportPath = options.report;
await displayNextTask(tasksPath, reportPath);
});
@@ -1834,11 +1860,15 @@ ${result.result}
'Task ID(s) to show (comma-separated for multiple)'
)
.option('-s, --status <status>', 'Filter subtasks by status')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-r, --report <report>',
'Path to the complexity report file',
'scripts/task-complexity-report.json'
COMPLEXITY_REPORT_FILE
)
.action(async (taskId, options) => {
const idArg = taskId || options.id;
@@ -1849,7 +1879,7 @@ ${result.result}
process.exit(1);
}
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const reportPath = options.report;
// Check if multiple IDs are provided (comma-separated)
@@ -1878,9 +1908,13 @@ ${result.result}
.description('Add a dependency to a task')
.option('-i, --id <id>', 'Task ID to add dependency to')
.option('-d, --depends-on <id>', 'Task ID that will become a dependency')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const taskId = options.id;
const dependencyId = options.dependsOn;
@@ -1909,9 +1943,13 @@ ${result.result}
.description('Remove a dependency from a task')
.option('-i, --id <id>', 'Task ID to remove dependency from')
.option('-d, --depends-on <id>', 'Task ID to remove as a dependency')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const taskId = options.id;
const dependencyId = options.dependsOn;
@@ -1940,18 +1978,26 @@ ${result.result}
.description(
`Identify invalid dependencies without fixing them${chalk.reset('')}`
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.action(async (options) => {
await validateDependenciesCommand(options.file);
await validateDependenciesCommand(options.file || TASKMASTER_TASKS_FILE);
});
// fix-dependencies command
programInstance
.command('fix-dependencies')
.description(`Fix invalid dependencies automatically${chalk.reset('')}`)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.action(async (options) => {
await fixDependenciesCommand(options.file);
await fixDependenciesCommand(options.file || TASKMASTER_TASKS_FILE);
});
// complexity-report command
@@ -1961,17 +2007,21 @@ ${result.result}
.option(
'-f, --file <file>',
'Path to the report file',
'scripts/task-complexity-report.json'
COMPLEXITY_REPORT_FILE
)
.action(async (options) => {
await displayComplexityReport(options.file);
await displayComplexityReport(options.file || COMPLEXITY_REPORT_FILE);
});
// add-subtask command
programInstance
.command('add-subtask')
.description('Add a subtask to an existing task')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option('-p, --parent <id>', 'Parent task ID (required)')
.option('-i, --task-id <id>', 'Existing task ID to convert to subtask')
.option(
@@ -1987,7 +2037,7 @@ ${result.result}
.option('-s, --status <status>', 'Status for the new subtask', 'pending')
.option('--skip-generate', 'Skip regenerating task files')
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const parentId = options.parent;
const existingTaskId = options.taskId;
const generateFiles = !options.skipGenerate;
@@ -2132,26 +2182,7 @@ ${result.result}
function showAddSubtaskHelp() {
console.log(
boxen(
chalk.white.bold('Add Subtask Command Help') +
'\n\n' +
chalk.cyan('Usage:') +
'\n' +
` task-master add-subtask --parent=<id> [options]\n\n` +
chalk.cyan('Options:') +
'\n' +
' -p, --parent <id> Parent task ID (required)\n' +
' -i, --task-id <id> Existing task ID to convert to subtask\n' +
' -t, --title <title> Title for the new subtask\n' +
' -d, --description <text> Description for the new subtask\n' +
' --details <text> Implementation details for the new subtask\n' +
' --dependencies <ids> Comma-separated list of dependency IDs\n' +
' -s, --status <status> Status for the new subtask (default: "pending")\n' +
' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' +
' --skip-generate Skip regenerating task files\n\n' +
chalk.cyan('Examples:') +
'\n' +
' task-master add-subtask --parent=5 --task-id=8\n' +
' task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form"',
`${chalk.white.bold('Add Subtask Command Help')}\n\n${chalk.cyan('Usage:')}\n task-master add-subtask --parent=<id> [options]\n\n${chalk.cyan('Options:')}\n -p, --parent <id> Parent task ID (required)\n -i, --task-id <id> Existing task ID to convert to subtask\n -t, --title <title> Title for the new subtask\n -d, --description <text> Description for the new subtask\n --details <text> Implementation details for the new subtask\n --dependencies <ids> Comma-separated list of dependency IDs\n -s, --status <status> Status for the new subtask (default: "pending")\n -f, --file <file> Path to the tasks file (default: "${TASKMASTER_TASKS_FILE}")\n --skip-generate Skip regenerating task files\n\n${chalk.cyan('Examples:')}\n task-master add-subtask --parent=5 --task-id=8\n task-master add-subtask -p 5 -t "Implement login UI" -d "Create the login form"`,
{ padding: 1, borderColor: 'blue', borderStyle: 'round' }
)
);
@@ -2161,7 +2192,11 @@ ${result.result}
programInstance
.command('remove-subtask')
.description('Remove a subtask from its parent task')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'-i, --id <id>',
'Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated for multiple subtasks)'
@@ -2172,7 +2207,7 @@ ${result.result}
)
.option('--skip-generate', 'Skip regenerating task files')
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const subtaskIds = options.id;
const convertToTask = options.convert || false;
const generateFiles = !options.skipGenerate;
@@ -2292,7 +2327,9 @@ ${result.result}
'\n' +
' -i, --id <id> Subtask ID(s) to remove in format "parentId.subtaskId" (can be comma-separated, required)\n' +
' -c, --convert Convert the subtask to a standalone task instead of deleting it\n' +
' -f, --file <file> Path to the tasks file (default: "tasks/tasks.json")\n' +
' -f, --file <file> Path to the tasks file (default: "' +
TASKMASTER_TASKS_FILE +
'")\n' +
' --skip-generate Skip regenerating task files\n\n' +
chalk.cyan('Examples:') +
'\n' +
@@ -2313,10 +2350,14 @@ ${result.result}
'-i, --id <ids>',
'ID(s) of the task(s) or subtask(s) to remove (e.g., "5", "5.2", or "5,6.1,7")'
)
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option('-y, --yes', 'Skip confirmation prompt', false)
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const taskIdsString = options.id;
if (!taskIdsString) {
@@ -2813,7 +2854,11 @@ Examples:
programInstance
.command('move')
.description('Move a task or subtask to a new position')
.option('-f, --file <file>', 'Path to the tasks file', 'tasks/tasks.json')
.option(
'-f, --file <file>',
'Path to the tasks file',
TASKMASTER_TASKS_FILE
)
.option(
'--from <id>',
'ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated to move multiple tasks (e.g., "5,6,7")'
@@ -2823,7 +2868,7 @@ Examples:
'ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated'
)
.action(async (options) => {
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const sourceId = options.from;
const destinationId = options.to;
@@ -2938,6 +2983,39 @@ Examples:
}
});
programInstance
.command('migrate')
.description(
'Migrate existing project to use the new .taskmaster directory structure'
)
.option(
'-f, --force',
'Force migration even if .taskmaster directory already exists'
)
.option(
'--backup',
'Create backup of old files before migration (default: false)',
false
)
.option(
'--cleanup',
'Remove old files after successful migration (default: true)',
true
)
.option('-y, --yes', 'Skip confirmation prompts')
.option(
'--dry-run',
'Show what would be migrated without actually moving files'
)
.action(async (options) => {
try {
await migrateProject(options);
} catch (error) {
console.error(chalk.red('Error during migration:'), error.message);
process.exit(1);
}
});
return programInstance;
}
@@ -3122,7 +3200,7 @@ async function runCLI(argv = process.argv) {
// Setup and parse
// NOTE: getConfig() might be called during setupCLI->registerCommands if commands need config
// This means the ConfigurationError might be thrown here if .taskmasterconfig is missing.
// This means the ConfigurationError might be thrown here if configuration file is missing.
const programInstance = setupCLI();
await programInstance.parseAsync(argv);
@@ -3141,10 +3219,10 @@ async function runCLI(argv = process.argv) {
boxen(
chalk.red.bold('Configuration Update Required!') +
'\n\n' +
chalk.white('Taskmaster now uses the ') +
chalk.yellow.bold('.taskmasterconfig') +
chalk.white('Taskmaster now uses a ') +
chalk.yellow.bold('configuration file') +
chalk.white(
' file in your project root for AI model choices and settings.\n\n' +
' in your project for AI model choices and settings.\n\n' +
'This file appears to be '
) +
chalk.red.bold('missing') +
@@ -3156,7 +3234,7 @@ async function runCLI(argv = process.argv) {
chalk.white.bold('Key Points:') +
'\n' +
chalk.white('* ') +
chalk.yellow.bold('.taskmasterconfig') +
chalk.yellow.bold('Configuration file') +
chalk.white(
': Stores your AI model settings (do not manually edit)\n'
) +

View File

@@ -3,6 +3,8 @@ import path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { log, findProjectRoot, resolveEnvVariable } from './utils.js';
import { LEGACY_CONFIG_FILE } from '../../src/constants/paths.js';
import { findConfigPath } from '../../src/utils/path-utils.js';
// Calculate __dirname in ESM
const __filename = fileURLToPath(import.meta.url);
@@ -27,12 +29,10 @@ try {
process.exit(1); // Exit if models can't be loaded
}
const CONFIG_FILE_NAME = '.taskmasterconfig';
// Define valid providers dynamically from the loaded MODEL_MAP
const VALID_PROVIDERS = Object.keys(MODEL_MAP || {});
// Default configuration values (used if .taskmasterconfig is missing or incomplete)
// Default configuration values (used if config file is missing or incomplete)
const DEFAULTS = {
models: {
main: {
@@ -96,13 +96,15 @@ function _loadAndValidateConfig(explicitRoot = null) {
}
// ---> End find project root logic <---
// --- Proceed with loading from the determined rootToUse ---
const configPath = path.join(rootToUse, CONFIG_FILE_NAME);
// --- Find configuration file using centralized path utility ---
const configPath = findConfigPath(null, { projectRoot: rootToUse });
let config = { ...defaults }; // Start with a deep copy of defaults
let configExists = false;
if (fs.existsSync(configPath)) {
if (configPath) {
configExists = true;
const isLegacy = configPath.endsWith(LEGACY_CONFIG_FILE);
try {
const rawData = fs.readFileSync(configPath, 'utf-8');
const parsedConfig = JSON.parse(rawData);
@@ -125,6 +127,15 @@ function _loadAndValidateConfig(explicitRoot = null) {
};
configSource = `file (${configPath})`; // Update source info
// Issue deprecation warning if using legacy config file
if (isLegacy) {
console.warn(
chalk.yellow(
`⚠️ DEPRECATION WARNING: Found configuration in legacy location '${configPath}'. Please migrate to .taskmaster/config.json. Run 'task-master migrate' to automatically migrate your project.`
)
);
}
// --- Validation (Warn if file content is invalid) ---
// Use log.warn for consistency
if (!validateProvider(config.models.main.provider)) {
@@ -171,19 +182,19 @@ function _loadAndValidateConfig(explicitRoot = null) {
// Only warn if an explicit root was *expected*.
console.warn(
chalk.yellow(
`Warning: ${CONFIG_FILE_NAME} not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
`Warning: Configuration file not found at provided project root (${explicitRoot}). Using default configuration. Run 'task-master models --setup' to configure.`
)
);
} else {
console.warn(
chalk.yellow(
`Warning: ${CONFIG_FILE_NAME} not found at derived root (${rootToUse}). Using defaults.`
`Warning: Configuration file not found at derived root (${rootToUse}). Using defaults.`
)
);
}
// Keep config as defaults
config = { ...defaults };
configSource = `defaults (file not found at ${configPath})`;
configSource = `defaults (no config file found at ${rootToUse})`;
}
return config;
@@ -342,13 +353,13 @@ function getDefaultSubtasks(explicitRoot = null) {
// Directly return value from config, ensure integer
const val = getGlobalConfig(explicitRoot).defaultSubtasks;
const parsedVal = parseInt(val, 10);
return isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal;
return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultSubtasks : parsedVal;
}
function getDefaultNumTasks(explicitRoot = null) {
const val = getGlobalConfig(explicitRoot).defaultNumTasks;
const parsedVal = parseInt(val, 10);
return isNaN(parsedVal) ? DEFAULTS.global.defaultNumTasks : parsedVal;
return Number.isNaN(parsedVal) ? DEFAULTS.global.defaultNumTasks : parsedVal;
}
function getDefaultPriority(explicitRoot = null) {
@@ -658,12 +669,16 @@ function writeConfig(config, explicitRoot = null) {
}
// ---> End determine root path logic <---
const configPath =
path.basename(rootPath) === CONFIG_FILE_NAME
? rootPath
: path.join(rootPath, CONFIG_FILE_NAME);
// Use new config location: .taskmaster/config.json
const taskmasterDir = path.join(rootPath, '.taskmaster');
const configPath = path.join(taskmasterDir, 'config.json');
try {
// Ensure .taskmaster directory exists
if (!fs.existsSync(taskmasterDir)) {
fs.mkdirSync(taskmasterDir, { recursive: true });
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2));
loadedConfig = config; // Update the cache after successful write
return true;
@@ -678,25 +693,12 @@ function writeConfig(config, explicitRoot = null) {
}
/**
* Checks if the .taskmasterconfig file exists at the project root
* Checks if a configuration file exists at the project root (new or legacy location)
* @param {string|null} explicitRoot - Optional explicit path to the project root
* @returns {boolean} True if the file exists, false otherwise
*/
function isConfigFilePresent(explicitRoot = null) {
// ---> Determine root path reliably <---
let rootPath = explicitRoot;
if (explicitRoot === null || explicitRoot === undefined) {
// Logic matching _loadAndValidateConfig
const foundRoot = findProjectRoot(); // *** Explicitly call findProjectRoot ***
if (!foundRoot) {
return false; // Cannot check if root doesn't exist
}
rootPath = foundRoot;
}
// ---> End determine root path logic <---
const configPath = path.join(rootPath, CONFIG_FILE_NAME);
return fs.existsSync(configPath);
return findConfigPath(null, { projectRoot: explicitRoot }) !== null;
}
/**

View File

@@ -24,6 +24,7 @@ import removeTask from './task-manager/remove-task.js';
import taskExists from './task-manager/task-exists.js';
import isTaskDependentOn from './task-manager/is-task-dependent.js';
import moveTask from './task-manager/move-task.js';
import { migrateProject } from './task-manager/migrate.js';
import { performResearch } from './task-manager/research.js';
import { readComplexityReport } from './utils.js';
// Export task manager functions
@@ -50,5 +51,6 @@ export {
isTaskDependentOn,
moveTask,
performResearch,
readComplexityReport
readComplexityReport,
migrateProject
};

View File

@@ -14,6 +14,10 @@ import {
import { generateTextService } from '../ai-services-unified.js';
import { getDebugFlag, getProjectName } from '../config-manager.js';
import {
COMPLEXITY_REPORT_FILE,
LEGACY_TASKS_FILE
} from '../../../src/constants/paths.js';
/**
* Generates the prompt for complexity analysis.
@@ -64,8 +68,8 @@ Do not include any explanatory text, markdown formatting, or code block markers
*/
async function analyzeTaskComplexity(options, context = {}) {
const { session, mcpLog } = context;
const tasksPath = options.file || 'tasks/tasks.json';
const outputPath = options.output || 'scripts/task-complexity-report.json';
const tasksPath = options.file || LEGACY_TASKS_FILE;
const outputPath = options.output || COMPLEXITY_REPORT_FILE;
const thresholdScore = parseFloat(options.threshold || '5');
const useResearch = options.research || false;
const projectRoot = options.projectRoot;
@@ -74,7 +78,7 @@ async function analyzeTaskComplexity(options, context = {}) {
? options.id
.split(',')
.map((id) => parseInt(id.trim(), 10))
.filter((id) => !isNaN(id))
.filter((id) => !Number.isNaN(id))
: null;
const fromId = options.from !== undefined ? parseInt(options.from, 10) : null;
const toId = options.to !== undefined ? parseInt(options.to, 10) : null;
@@ -92,7 +96,7 @@ async function analyzeTaskComplexity(options, context = {}) {
if (outputFormat === 'text') {
console.log(
chalk.blue(
`Analyzing task complexity and generating expansion recommendations...`
'Analyzing task complexity and generating expansion recommendations...'
)
);
}
@@ -256,13 +260,13 @@ async function analyzeTaskComplexity(options, context = {}) {
// If using ID filtering but no matching tasks, return existing report or empty
if (existingReport && (specificIds || fromId !== null || toId !== null)) {
reportLog(
`No matching tasks found for analysis. Keeping existing report.`,
'No matching tasks found for analysis. Keeping existing report.',
'info'
);
if (outputFormat === 'text') {
console.log(
chalk.yellow(
`No matching tasks found for analysis. Keeping existing report.`
'No matching tasks found for analysis. Keeping existing report.'
)
);
}
@@ -377,7 +381,7 @@ async function analyzeTaskComplexity(options, context = {}) {
);
}
reportLog(`Parsing complexity analysis from text response...`, 'info');
reportLog('Parsing complexity analysis from text response...', 'info');
try {
let cleanedResponse = aiServiceResponse.mainResult;
cleanedResponse = cleanedResponse.trim();

View File

@@ -14,6 +14,7 @@ import { generateTextService } from '../ai-services-unified.js';
import { getDefaultSubtasks, getDebugFlag } from '../config-manager.js';
import generateTaskFiles from './generate-task-files.js';
import { COMPLEXITY_REPORT_FILE } from '../../../src/constants/paths.js';
// --- Zod Schemas (Keep from previous step) ---
const subtaskSchema = z
@@ -463,10 +464,7 @@ async function expandTask(
let complexityReasoningContext = '';
let systemPrompt; // Declare systemPrompt here
const complexityReportPath = path.join(
projectRoot,
'scripts/task-complexity-report.json'
);
const complexityReportPath = path.join(projectRoot, COMPLEXITY_REPORT_FILE);
let taskAnalysis = null;
try {

View File

@@ -0,0 +1,283 @@
import fs from 'fs';
import path from 'path';
import chalk from 'chalk';
import { fileURLToPath } from 'url';
import { createLogWrapper } from '../../../mcp-server/src/tools/utils.js';
import { findProjectRoot } from '../utils.js';
import {
LEGACY_CONFIG_FILE,
TASKMASTER_CONFIG_FILE
} from '../../../src/constants/paths.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Create a simple log wrapper for CLI use
const log = createLogWrapper({
info: (msg) => console.log(chalk.blue(''), msg),
warn: (msg) => console.log(chalk.yellow('⚠'), msg),
error: (msg) => console.error(chalk.red('✗'), msg),
success: (msg) => console.log(chalk.green('✓'), msg)
});
/**
* Main migration function
* @param {Object} options - Migration options
*/
export async function migrateProject(options = {}) {
const projectRoot = findProjectRoot() || process.cwd();
log.info(`Starting migration in: ${projectRoot}`);
// Check if .taskmaster directory already exists
const taskmasterDir = path.join(projectRoot, '.taskmaster');
if (fs.existsSync(taskmasterDir) && !options.force) {
log.warn(
'.taskmaster directory already exists. Use --force to overwrite or skip migration.'
);
return;
}
// Analyze what needs to be migrated
const migrationPlan = analyzeMigrationNeeds(projectRoot);
if (migrationPlan.length === 0) {
log.info(
'No files to migrate. Project may already be using the new structure.'
);
return;
}
// Show migration plan
log.info('Migration plan:');
for (const item of migrationPlan) {
const action = options.dryRun ? 'Would move' : 'Will move';
log.info(` ${action}: ${item.from}${item.to}`);
}
if (options.dryRun) {
log.info(
'Dry run complete. Use --dry-run=false to perform actual migration.'
);
return;
}
// Confirm migration
if (!options.yes) {
const readline = await import('readline');
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise((resolve) => {
rl.question('Proceed with migration? (y/N): ', resolve);
});
rl.close();
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
log.info('Migration cancelled.');
return;
}
}
// Perform migration
try {
await performMigration(projectRoot, migrationPlan, options);
log.success('Migration completed successfully!');
log.info('You can now use the new .taskmaster directory structure.');
if (!options.cleanup) {
log.info(
'Old files were preserved. Use --cleanup to remove them after verification.'
);
}
} catch (error) {
log.error(`Migration failed: ${error.message}`);
throw error;
}
}
/**
* Analyze what files need to be migrated
* @param {string} projectRoot - Project root directory
* @returns {Array} Migration plan items
*/
function analyzeMigrationNeeds(projectRoot) {
const migrationPlan = [];
// Check for tasks directory
const tasksDir = path.join(projectRoot, 'tasks');
if (fs.existsSync(tasksDir)) {
const tasksFiles = fs.readdirSync(tasksDir);
for (const file of tasksFiles) {
migrationPlan.push({
from: path.join('tasks', file),
to: path.join('.taskmaster', 'tasks', file),
type: 'task'
});
}
}
// Check for scripts directory files
const scriptsDir = path.join(projectRoot, 'scripts');
if (fs.existsSync(scriptsDir)) {
const scriptsFiles = fs.readdirSync(scriptsDir);
for (const file of scriptsFiles) {
const filePath = path.join(scriptsDir, file);
if (fs.statSync(filePath).isFile()) {
// Categorize files more intelligently
let destination;
const lowerFile = file.toLowerCase();
if (
lowerFile.includes('example') ||
lowerFile.includes('template') ||
lowerFile.includes('boilerplate') ||
lowerFile.includes('sample')
) {
// Template/example files go to templates (including example_prd.txt)
destination = path.join('.taskmaster', 'templates', file);
} else if (
lowerFile.includes('complexity') &&
lowerFile.includes('report') &&
lowerFile.endsWith('.json')
) {
// Only actual complexity reports go to reports
destination = path.join('.taskmaster', 'reports', file);
} else if (
lowerFile.includes('prd') ||
lowerFile.endsWith('.md') ||
lowerFile.endsWith('.txt')
) {
// Documentation files go to docs (but not examples or reports)
destination = path.join('.taskmaster', 'docs', file);
} else {
// Other files stay in scripts or get skipped - don't force everything into templates
log.warn(
`Skipping migration of '${file}' - uncertain categorization. You may need to move this manually.`
);
continue;
}
migrationPlan.push({
from: path.join('scripts', file),
to: destination,
type: 'script'
});
}
}
}
// Check for .taskmasterconfig
const oldConfig = path.join(projectRoot, LEGACY_CONFIG_FILE);
if (fs.existsSync(oldConfig)) {
migrationPlan.push({
from: LEGACY_CONFIG_FILE,
to: TASKMASTER_CONFIG_FILE,
type: 'config'
});
}
return migrationPlan;
}
/**
* Perform the actual migration
* @param {string} projectRoot - Project root directory
* @param {Array} migrationPlan - List of files to migrate
* @param {Object} options - Migration options
*/
async function performMigration(projectRoot, migrationPlan, options) {
// Create .taskmaster directory
const taskmasterDir = path.join(projectRoot, '.taskmaster');
if (!fs.existsSync(taskmasterDir)) {
fs.mkdirSync(taskmasterDir, { recursive: true });
}
// Group migration items by destination directory to create only needed subdirs
const neededDirs = new Set();
for (const item of migrationPlan) {
const destDir = path.dirname(item.to);
neededDirs.add(destDir);
}
// Create only the directories we actually need
for (const dir of neededDirs) {
const fullDirPath = path.join(projectRoot, dir);
if (!fs.existsSync(fullDirPath)) {
fs.mkdirSync(fullDirPath, { recursive: true });
log.info(`Created directory: ${dir}`);
}
}
// Create backup if requested
if (options.backup) {
const backupDir = path.join(projectRoot, '.taskmaster-migration-backup');
log.info(`Creating backup in: ${backupDir}`);
if (fs.existsSync(backupDir)) {
fs.rmSync(backupDir, { recursive: true, force: true });
}
fs.mkdirSync(backupDir, { recursive: true });
}
// Migrate files
for (const item of migrationPlan) {
const fromPath = path.join(projectRoot, item.from);
const toPath = path.join(projectRoot, item.to);
if (!fs.existsSync(fromPath)) {
log.warn(`Source file not found: ${item.from}`);
continue;
}
// Create backup if requested
if (options.backup) {
const backupPath = path.join(
projectRoot,
'.taskmaster-migration-backup',
item.from
);
const backupDir = path.dirname(backupPath);
if (!fs.existsSync(backupDir)) {
fs.mkdirSync(backupDir, { recursive: true });
}
fs.copyFileSync(fromPath, backupPath);
}
// Ensure destination directory exists
const toDir = path.dirname(toPath);
if (!fs.existsSync(toDir)) {
fs.mkdirSync(toDir, { recursive: true });
}
// Copy file
fs.copyFileSync(fromPath, toPath);
log.info(`Migrated: ${item.from}${item.to}`);
// Remove original if cleanup is requested
if (options.cleanup) {
fs.unlinkSync(fromPath);
}
}
// Clean up empty directories if cleanup is requested
if (options.cleanup) {
const dirsToCheck = ['tasks', 'scripts'];
for (const dir of dirsToCheck) {
const dirPath = path.join(projectRoot, dir);
if (fs.existsSync(dirPath)) {
try {
const files = fs.readdirSync(dirPath);
if (files.length === 0) {
fs.rmdirSync(dirPath);
log.info(`Removed empty directory: ${dir}`);
}
} catch (error) {
// Directory not empty or other error, skip
}
}
}
}
}
export default { migrateProject };

View File

@@ -3,8 +3,6 @@
* Core functionality for managing AI model configurations
*/
import path from 'path';
import fs from 'fs';
import https from 'https';
import http from 'http';
import {
@@ -23,6 +21,8 @@ import {
getAllProviders,
getBaseUrlForRole
} from '../config-manager.js';
import { findConfigPath } from '../../../src/utils/path-utils.js';
import { log } from '../utils.js';
/**
* Fetches the list of models from OpenRouter API.
@@ -149,34 +149,27 @@ async function getModelConfiguration(options = {}) {
}
};
// Check if configuration file exists using provided project root
let configPath;
let configExists = false;
if (projectRoot) {
configPath = path.join(projectRoot, '.taskmasterconfig');
configExists = fs.existsSync(configPath);
report(
'info',
`Checking for .taskmasterconfig at: ${configPath}, exists: ${configExists}`
);
} else {
configExists = isConfigFilePresent();
report(
'info',
`Checking for .taskmasterconfig using isConfigFilePresent(), exists: ${configExists}`
);
if (!projectRoot) {
throw new Error('Project root is required but not found.');
}
// Use centralized config path finding instead of hardcoded path
const configPath = findConfigPath(null, { projectRoot });
const configExists = isConfigFilePresent(projectRoot);
log(
'debug',
`Checking for config file using findConfigPath, found: ${configPath}`
);
log(
'debug',
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
);
if (!configExists) {
return {
success: false,
error: {
code: 'CONFIG_MISSING',
message:
'The .taskmasterconfig file is missing. Run "task-master models --setup" to create it.'
}
};
throw new Error(
'The configuration file is missing. Run "task-master models --setup" to create it.'
);
}
try {
@@ -286,34 +279,27 @@ async function getAvailableModelsList(options = {}) {
}
};
// Check if configuration file exists using provided project root
let configPath;
let configExists = false;
if (projectRoot) {
configPath = path.join(projectRoot, '.taskmasterconfig');
configExists = fs.existsSync(configPath);
report(
'info',
`Checking for .taskmasterconfig at: ${configPath}, exists: ${configExists}`
);
} else {
configExists = isConfigFilePresent();
report(
'info',
`Checking for .taskmasterconfig using isConfigFilePresent(), exists: ${configExists}`
);
if (!projectRoot) {
throw new Error('Project root is required but not found.');
}
// Use centralized config path finding instead of hardcoded path
const configPath = findConfigPath(null, { projectRoot });
const configExists = isConfigFilePresent(projectRoot);
log(
'debug',
`Checking for config file using findConfigPath, found: ${configPath}`
);
log(
'debug',
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
);
if (!configExists) {
return {
success: false,
error: {
code: 'CONFIG_MISSING',
message:
'The .taskmasterconfig file is missing. Run "task-master models --setup" to create it.'
}
};
throw new Error(
'The configuration file is missing. Run "task-master models --setup" to create it.'
);
}
try {
@@ -386,34 +372,27 @@ async function setModel(role, modelId, options = {}) {
}
};
// Check if configuration file exists using provided project root
let configPath;
let configExists = false;
if (projectRoot) {
configPath = path.join(projectRoot, '.taskmasterconfig');
configExists = fs.existsSync(configPath);
report(
'info',
`Checking for .taskmasterconfig at: ${configPath}, exists: ${configExists}`
);
} else {
configExists = isConfigFilePresent();
report(
'info',
`Checking for .taskmasterconfig using isConfigFilePresent(), exists: ${configExists}`
);
if (!projectRoot) {
throw new Error('Project root is required but not found.');
}
// Use centralized config path finding instead of hardcoded path
const configPath = findConfigPath(null, { projectRoot });
const configExists = isConfigFilePresent(projectRoot);
log(
'debug',
`Checking for config file using findConfigPath, found: ${configPath}`
);
log(
'debug',
`Checking config file using isConfigFilePresent(), exists: ${configExists}`
);
if (!configExists) {
return {
success: false,
error: {
code: 'CONFIG_MISSING',
message:
'The .taskmasterconfig file is missing. Run "task-master models --setup" to create it.'
}
};
throw new Error(
'The configuration file is missing. Run "task-master models --setup" to create it.'
);
}
// Validate role
@@ -445,7 +424,7 @@ async function setModel(role, modelId, options = {}) {
let warningMessage = null;
// Find the model data in internal list initially to see if it exists at all
let modelData = availableModels.find((m) => m.id === modelId);
const modelData = availableModels.find((m) => m.id === modelId);
// --- Revised Logic: Prioritize providerHint --- //
@@ -556,8 +535,8 @@ async function setModel(role, modelId, options = {}) {
return {
success: false,
error: {
code: 'WRITE_ERROR',
message: 'Error writing updated configuration to .taskmasterconfig'
code: 'CONFIG_WRITE_ERROR',
message: 'Error writing updated configuration to configuration file'
}
};
}

View File

@@ -24,6 +24,7 @@ import {
} from './task-manager.js';
import { getProjectName, getDefaultSubtasks } from './config-manager.js';
import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js';
import { TASKMASTER_TASKS_FILE } from '../../src/constants/paths.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
// Create a color gradient for the banner
@@ -1529,10 +1530,18 @@ async function displayComplexityReport(reportPath) {
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
// Call the analyze-complexity command
console.log(chalk.blue('Generating complexity report...'));
const tasksPath = TASKMASTER_TASKS_FILE;
if (!fs.existsSync(tasksPath)) {
console.error(
'❌ No tasks.json file found. Please run "task-master init" or create a tasks.json file.'
);
return null;
}
await analyzeTaskComplexity({
output: reportPath,
research: false, // Default to no research for speed
file: 'tasks/tasks.json'
file: tasksPath
});
// Read the newly generated report
return displayComplexityReport(reportPath);

View File

@@ -9,6 +9,11 @@ import chalk from 'chalk';
import dotenv from 'dotenv';
// Import specific config getters needed here
import { getLogLevel, getDebugFlag } from './config-manager.js';
import {
COMPLEXITY_REPORT_FILE,
LEGACY_COMPLEXITY_REPORT_FILE,
LEGACY_CONFIG_FILE
} from '../../src/constants/paths.js';
// Global silent mode flag
let silentMode = false;
@@ -60,16 +65,16 @@ function resolveEnvVariable(key, session = null, projectRoot = null) {
// --- Project Root Finding Utility ---
/**
* Finds the project root directory by searching for marker files/directories.
* @param {string} [startPath=process.cwd()] - The directory to start searching from.
* @param {string[]} [markers=['package.json', '.git', '.taskmasterconfig']] - Marker files/dirs to look for.
* @returns {string|null} The path to the project root directory, or null if not found.
* Recursively searches upwards for project root starting from a given directory.
* @param {string} [startDir=process.cwd()] - The directory to start searching from.
* @param {string[]} [markers=['package.json', '.git', LEGACY_CONFIG_FILE]] - Marker files/dirs to look for.
* @returns {string|null} The path to the project root, or null if not found.
*/
function findProjectRoot(
startPath = process.cwd(),
markers = ['package.json', '.git', '.taskmasterconfig']
startDir = process.cwd(),
markers = ['package.json', '.git', LEGACY_CONFIG_FILE]
) {
let currentPath = path.resolve(startPath);
let currentPath = path.resolve(startDir);
const rootPath = path.parse(currentPath).root;
while (currentPath !== rootPath) {
@@ -236,7 +241,7 @@ function sanitizePrompt(prompt) {
}
/**
* Reads and parses the complexity report if it exists
* Reads the complexity report from file
* @param {string} customPath - Optional custom path to the report
* @returns {Object|null} The parsed complexity report or null if not found
*/
@@ -244,21 +249,35 @@ function readComplexityReport(customPath = null) {
// Get debug flag dynamically from config-manager
const isDebug = getDebugFlag();
try {
const reportPath =
customPath ||
path.join(process.cwd(), 'scripts', 'task-complexity-report.json');
let reportPath;
if (customPath) {
reportPath = customPath;
} else {
// Try new location first, then fall back to legacy
const newPath = path.join(process.cwd(), COMPLEXITY_REPORT_FILE);
const legacyPath = path.join(
process.cwd(),
LEGACY_COMPLEXITY_REPORT_FILE
);
reportPath = fs.existsSync(newPath) ? newPath : legacyPath;
}
if (!fs.existsSync(reportPath)) {
if (isDebug) {
log('debug', `Complexity report not found at ${reportPath}`);
}
return null;
}
const reportData = fs.readFileSync(reportPath, 'utf8');
return JSON.parse(reportData);
} catch (error) {
log('warn', `Could not read complexity report: ${error.message}`);
// Optionally log full error in debug mode
const reportData = readJSON(reportPath);
if (isDebug) {
// Use dynamic debug flag
log('error', 'Full error details:', error);
log('debug', `Successfully read complexity report from ${reportPath}`);
}
return reportData;
} catch (error) {
if (isDebug) {
log('error', `Error reading complexity report: ${error.message}`);
}
return null;
}
@@ -395,7 +414,7 @@ function findTaskById(
}
let taskResult = null;
let originalSubtaskCount = null;
const originalSubtaskCount = null;
// Find the main task
const id = parseInt(taskId, 10);
@@ -410,7 +429,6 @@ function findTaskById(
// If task found and statusFilter provided, filter its subtasks
if (statusFilter && task.subtasks && Array.isArray(task.subtasks)) {
const originalSubtaskCount = task.subtasks.length;
// Clone the task to avoid modifying the original array
const filteredTask = { ...task };
filteredTask.subtasks = task.subtasks.filter(
@@ -420,7 +438,6 @@ function findTaskById(
);
taskResult = filteredTask;
originalSubtaskCount = originalSubtaskCount;
}
// If task found and complexityReport provided, add complexity data
@@ -443,7 +460,7 @@ function truncate(text, maxLength) {
return text;
}
return text.slice(0, maxLength - 3) + '...';
return `${text.slice(0, maxLength - 3)}...`;
}
/**