Merge branch 'next' of https://github.com/eyaltoledano/claude-task-master into joedanz/flexible-brand-rules

# Conflicts:
#	scripts/init.js
#	scripts/modules/commands.js
#	tests/integration/roo-files-inclusion.test.js
#	tests/integration/roo-init-functionality.test.js
This commit is contained in:
Joe Danziger
2025-06-04 13:39:50 -04:00
191 changed files with 15679 additions and 19668 deletions

View File

@@ -31,6 +31,17 @@ import {
} from '../src/utils/rule-transformer.js';
import { runInteractiveProfilesSetup } from '../src/utils/profiles.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);
@@ -168,8 +179,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;
@@ -186,43 +196,9 @@ function copyTemplateFile(templateName, targetPath, replacements = {}) {
// Map template names to their actual source paths
switch (templateName) {
case 'dev_workflow.mdc':
sourcePath = path.join(
__dirname,
'..',
'.cursor',
'rules',
'dev_workflow.mdc'
);
break;
case 'taskmaster.mdc':
sourcePath = path.join(
__dirname,
'..',
'.cursor',
'rules',
'taskmaster.mdc'
);
break;
case 'cursor_rules.mdc':
sourcePath = path.join(
__dirname,
'..',
'.cursor',
'rules',
'cursor_rules.mdc'
);
break;
case 'self_improve.mdc':
sourcePath = path.join(
__dirname,
'..',
'.cursor',
'rules',
'self_improve.mdc'
);
break;
// case 'README-task-master.md':
// sourcePath = path.join(__dirname, '..', 'README-task-master.md');
// break;
default:
// For other files like env.example, gitignore, etc. that don't have direct equivalents
sourcePath = path.join(__dirname, '..', 'assets', templateName);
@@ -263,10 +239,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 Task Master AI\n${newLines.join('\n')}`;
fs.writeFileSync(targetPath, updatedContent);
log('success', `Updated ${targetPath} with additional entries`);
} else {
@@ -334,12 +307,7 @@ async function initializeProject(options = {}) {
};
}
try {
createProjectStructure(addAliases, dryRun, selectedRuleProfiles);
} catch (error) {
log('error', `Error during initialization process: ${error.message}`);
process.exit(1);
}
createProjectStructure(addAliases, dryRun, options, selectedRuleProfiles);
} else {
// Interactive logic
log('info', 'Required options not provided, proceeding with prompts.');
@@ -405,7 +373,12 @@ async function initializeProject(options = {}) {
}
// Create structure using only necessary values
createProjectStructure(addAliasesPrompted, dryRun, selectedRuleProfiles);
createProjectStructure(
addAliasesPrompted,
dryRun,
options,
selectedRuleProfiles
);
} catch (error) {
rl.close();
log('error', `Error during initialization process: ${error.message}`);
@@ -427,14 +400,18 @@ function promptQuestion(rl, question) {
function createProjectStructure(
addAliases,
dryRun,
options,
selectedRuleProfiles = RULE_PROFILES // Default to all rule profiles
) {
const targetDir = process.cwd();
log('info', `Initializing project in ${targetDir}`);
// Create directories
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));
// Copy template files with replacements
const replacements = {
@@ -455,27 +432,24 @@ function createProjectStructure(
// Copy .env.example
copyTemplateFile(
'env.example',
path.join(targetDir, '.env.example'),
path.join(targetDir, ENV_EXAMPLE_FILE),
replacements
);
// Copy .taskmasterconfig with project name
// Copy config.json with project name to NEW location
copyTemplateFile(
'.taskmasterconfig',
path.join(targetDir, '.taskmasterconfig'),
'config.json',
path.join(targetDir, TASKMASTER_CONFIG_FILE),
{
...replacements
}
);
// Copy .gitignore
copyTemplateFile('gitignore', path.join(targetDir, '.gitignore'));
copyTemplateFile('gitignore', path.join(targetDir, GITIGNORE_FILE));
// Copy example_prd.txt
copyTemplateFile(
'example_prd.txt',
path.join(targetDir, 'scripts', 'example_prd.txt')
);
// 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 {
@@ -523,7 +497,7 @@ function createProjectStructure(
}
// === Add Model Configuration Step ===
if (!isSilentMode() && !dryRun) {
if (!isSilentMode() && !dryRun && !options?.yes) {
console.log(
boxen(chalk.cyan('Configuring AI Models...'), {
padding: 0.5,
@@ -554,6 +528,12 @@ function createProjectStructure(
);
} 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',
'Default AI models will be used. You can configure different models later using "task-master models --setup" or "task-master models --set-..." commands.'
);
}
// ====================================
@@ -561,11 +541,9 @@ function createProjectStructure(
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,
@@ -580,76 +558,29 @@ function createProjectStructure(
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(
'Add new tasks anytime using the add-task command or MCP tool'
)}\n${chalk.white('8. ')}${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('9. ')}${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('10. ')}${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

@@ -22,6 +22,7 @@ import {
isApiKeySet,
getOllamaBaseURL,
getAzureBaseURL,
getBedrockBaseURL,
getVertexProjectId,
getVertexLocation
} from './config-manager.js';
@@ -410,6 +411,10 @@ async function _unifiedServiceRunner(serviceType, params) {
// For Ollama, use the global Ollama base URL if role-specific URL is not configured
baseURL = getOllamaBaseURL(effectiveProjectRoot);
log('debug', `Using global Ollama base URL: ${baseURL}`);
} else if (providerName?.toLowerCase() === 'bedrock' && !baseURL) {
// For Bedrock, use the global Bedrock base URL if role-specific URL is not configured
baseURL = getBedrockBaseURL(effectiveProjectRoot);
log('debug', `Using global Bedrock base URL: ${baseURL}`);
}
// Get AI parameters for the current role

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,
@@ -65,8 +72,7 @@ import {
stopLoadingIndicator,
displayModelConfiguration,
displayAvailableModels,
displayApiKeyStatus,
displayAiUsageSummary
displayApiKeyStatus
} from './ui.js';
import {
confirmProfilesRemove,
@@ -325,7 +331,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 = [
@@ -518,7 +524,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;
@@ -676,7 +682,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(
@@ -690,14 +696,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() {
@@ -737,38 +743,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' }
)
);
@@ -820,7 +800,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)',
@@ -835,7 +819,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;
@@ -901,7 +885,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>',
@@ -913,7 +901,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) {
@@ -928,7 +916,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.`
@@ -964,7 +952,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'
@@ -1054,7 +1042,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)'
@@ -1066,7 +1058,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) {
@@ -1117,7 +1109,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'
@@ -1208,10 +1200,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}`));
@@ -1234,9 +1230,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;
@@ -1266,16 +1266,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;
@@ -1314,7 +1318,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();
@@ -1389,7 +1393,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>',
@@ -1400,7 +1404,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'
@@ -1412,7 +1420,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);
@@ -1446,14 +1454,18 @@ function registerCommands(programInstance) {
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;
@@ -1484,7 +1496,11 @@ function registerCommands(programInstance) {
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)'
@@ -1524,10 +1540,14 @@ function registerCommands(programInstance) {
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();
@@ -1599,15 +1619,20 @@ function registerCommands(programInstance) {
.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);
});
@@ -1620,11 +1645,15 @@ function registerCommands(programInstance) {
.argument('[id]', 'Task ID to show')
.option('-i, --id <id>', 'Task ID to show')
.option('-s, --status <status>', 'Filter subtasks by status') // ADDED status option
.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;
@@ -1635,7 +1664,7 @@ function registerCommands(programInstance) {
process.exit(1);
}
const tasksPath = options.file;
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
const reportPath = options.report;
// PASS statusFilter to the display function
await displayTaskById(tasksPath, idArg, reportPath, statusFilter);
@@ -1647,9 +1676,13 @@ function registerCommands(programInstance) {
.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;
@@ -1678,9 +1711,13 @@ function registerCommands(programInstance) {
.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;
@@ -1709,18 +1746,26 @@ function registerCommands(programInstance) {
.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
@@ -1730,17 +1775,21 @@ function registerCommands(programInstance) {
.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(
@@ -1756,7 +1805,7 @@ function registerCommands(programInstance) {
.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;
@@ -1901,26 +1950,7 @@ function registerCommands(programInstance) {
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' }
)
);
@@ -1930,7 +1960,11 @@ function registerCommands(programInstance) {
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)'
@@ -1941,7 +1975,7 @@ function registerCommands(programInstance) {
)
.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;
@@ -2061,7 +2095,9 @@ function registerCommands(programInstance) {
'\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' +
@@ -2082,10 +2118,14 @@ function registerCommands(programInstance) {
'-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) {
@@ -2600,7 +2640,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")'
@@ -2610,7 +2654,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;
@@ -2954,6 +2998,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;
}
@@ -3138,7 +3215,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);
@@ -3157,10 +3234,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') +
@@ -3172,7 +3249,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: {
@@ -61,7 +61,8 @@ const DEFAULTS = {
defaultSubtasks: 5,
defaultPriority: 'medium',
projectName: 'Task Master',
ollamaBaseURL: 'http://localhost:11434/api'
ollamaBaseURL: 'http://localhost:11434/api',
bedrockBaseURL: 'https://bedrock.us-east-1.amazonaws.com'
}
};
@@ -96,13 +97,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 +128,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 +183,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 +354,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) {
@@ -371,6 +383,11 @@ function getAzureBaseURL(explicitRoot = null) {
return getGlobalConfig(explicitRoot).azureBaseURL;
}
function getBedrockBaseURL(explicitRoot = null) {
// Directly return value from config
return getGlobalConfig(explicitRoot).bedrockBaseURL;
}
/**
* Gets the Google Cloud project ID for Vertex AI from configuration
* @param {string|null} explicitRoot - Optional explicit path to the project root.
@@ -658,12 +675,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 +699,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;
}
/**
@@ -777,6 +785,7 @@ export {
getProjectName,
getOllamaBaseURL,
getAzureBaseURL,
getBedrockBaseURL,
getParametersForRole,
getUserId,
// API Key Checkers (still relevant)

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 { readComplexityReport } from './utils.js';
// Export task manager functions
export {
@@ -48,5 +49,6 @@ export {
taskExists,
isTaskDependentOn,
moveTask,
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...'
)
);
}

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 --- //
@@ -503,6 +482,11 @@ async function setModel(role, modelId, options = {}) {
`Model ID "${modelId}" not found in the Ollama instance. Please verify the model is pulled and available. You can check available models with: curl ${tagsUrl}`
);
}
} else if (providerHint === 'bedrock') {
// Set provider without model validation since Bedrock models are managed by AWS
determinedProvider = 'bedrock';
warningMessage = `Warning: Custom Bedrock model '${modelId}' set. Please ensure the model ID is valid and accessible in your AWS account.`;
report('warn', warningMessage);
} else {
// Invalid provider hint - should not happen
throw new Error(`Invalid provider hint received: ${providerHint}`);
@@ -556,8 +540,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,10 @@ import {
} from './task-manager.js';
import { getProjectName, getDefaultSubtasks } from './config-manager.js';
import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js';
import {
TASKMASTER_CONFIG_FILE,
TASKMASTER_TASKS_FILE
} from '../../src/constants/paths.js';
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
// Create a color gradient for the banner
@@ -686,7 +690,7 @@ function displayHelp() {
configTable.push(
[
`${chalk.yellow('.taskmasterconfig')}${chalk.reset('')}`,
`${chalk.yellow(TASKMASTER_CONFIG_FILE)}${chalk.reset('')}`,
`${chalk.white('AI model configuration file (project root)')}${chalk.reset('')}`,
`${chalk.dim('Managed by models cmd')}${chalk.reset('')}`
],
@@ -1524,10 +1528,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);
@@ -1842,7 +1854,7 @@ function displayApiKeyStatus(statusReport) {
console.log(table.toString());
console.log(
chalk.gray(
' Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in .taskmasterconfig.'
` Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in ${TASKMASTER_CONFIG_FILE}.`
)
);
}

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) {
@@ -150,8 +155,17 @@ function log(level, ...args) {
return;
}
// Get log level dynamically from config-manager
const configLevel = getLogLevel() || 'info'; // Use getter
// GUARD: Prevent circular dependency during config loading
// Use a simple fallback log level instead of calling getLogLevel()
let configLevel = 'info'; // Default fallback
try {
// Only try to get config level if we're not in the middle of config loading
configLevel = getLogLevel() || 'info';
} catch (error) {
// If getLogLevel() fails (likely due to circular dependency),
// use default 'info' level and continue
configLevel = 'info';
}
// Use text prefixes instead of emojis
const prefixes = {
@@ -185,8 +199,17 @@ function log(level, ...args) {
* @returns {Object|null} Parsed JSON data or null if error occurs
*/
function readJSON(filepath) {
// Get debug flag dynamically from config-manager
const isDebug = getDebugFlag();
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
// Only try to get debug flag if we're not in the middle of config loading
isDebug = getDebugFlag();
} catch (error) {
// If getDebugFlag() fails (likely due to circular dependency),
// use default false and continue
isDebug = false;
}
try {
const rawData = fs.readFileSync(filepath, 'utf8');
return JSON.parse(rawData);
@@ -207,8 +230,17 @@ function readJSON(filepath) {
* @param {Object} data - Data to write
*/
function writeJSON(filepath, data) {
// Get debug flag dynamically from config-manager
const isDebug = getDebugFlag();
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
// Only try to get debug flag if we're not in the middle of config loading
isDebug = getDebugFlag();
} catch (error) {
// If getDebugFlag() fails (likely due to circular dependency),
// use default false and continue
isDebug = false;
}
try {
const dir = path.dirname(filepath);
if (!fs.existsSync(dir)) {
@@ -236,29 +268,52 @@ 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
*/
function readComplexityReport(customPath = null) {
// Get debug flag dynamically from config-manager
const isDebug = getDebugFlag();
// GUARD: Prevent circular dependency during config loading
let isDebug = false; // Default fallback
try {
const reportPath =
customPath ||
path.join(process.cwd(), 'scripts', 'task-complexity-report.json');
// Only try to get debug flag if we're not in the middle of config loading
isDebug = getDebugFlag();
} catch (error) {
// If getDebugFlag() fails (likely due to circular dependency),
// use default false and continue
isDebug = false;
}
try {
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 +450,7 @@ function findTaskById(
}
let taskResult = null;
let originalSubtaskCount = null;
const originalSubtaskCount = null;
// Find the main task
const id = parseInt(taskId, 10);
@@ -410,7 +465,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 +474,6 @@ function findTaskById(
);
taskResult = filteredTask;
originalSubtaskCount = originalSubtaskCount;
}
// If task found and complexityReport provided, add complexity data
@@ -443,7 +496,7 @@ function truncate(text, maxLength) {
return text;
}
return text.slice(0, maxLength - 3) + '...';
return `${text.slice(0, maxLength - 3)}...`;
}
/**

View File

@@ -3,6 +3,7 @@ import path from 'path';
import fs from 'fs';
import { isSilentMode, log } from '../modules/utils.js';
import { createProfile, COMMON_TOOL_MAPPINGS } from './base-profile.js';
import { ROO_MODES } from '../../src/constants/profiles.js';
// Lifecycle functions for Roo profile
function onAddRulesProfile(targetDir) {
@@ -10,7 +11,6 @@ function onAddRulesProfile(targetDir) {
copyRecursiveSync(sourceDir, targetDir);
const rooModesDir = path.join(sourceDir, '.roo');
const rooModes = ['architect', 'ask', 'boomerang', 'code', 'debug', 'test'];
// Copy .roomodes to project root
const roomodesSrc = path.join(sourceDir, '.roomodes');
@@ -26,7 +26,7 @@ function onAddRulesProfile(targetDir) {
log('debug', `[Roo] .roomodes not found at ${roomodesSrc}`);
}
for (const mode of rooModes) {
for (const mode of ROO_MODES) {
const src = path.join(rooModesDir, `rules-${mode}`, `${mode}-rules`);
const dest = path.join(targetDir, '.roo', `rules-${mode}`, `${mode}-rules`);
if (fs.existsSync(src)) {
@@ -89,7 +89,7 @@ function onRemoveRulesProfile(targetDir) {
if (fs.readdirSync(rooDir).length === 0) {
try {
fs.rmSync(rooDir, { recursive: true, force: true });
log('debug', `[Roo] Removed empty .roo directory`);
log('debug', '[Roo] Removed empty .roo directory');
} catch (err) {
log('debug', `[Roo] Failed to remove .roo directory: ${err.message}`);
}