Eyal Toledano 7df58df199 feat: Enhance Task Master CLI with Testing Framework, Perplexity AI Integration, and Refactored Core Logic
This commit introduces significant enhancements and refactoring to the Task Master CLI, focusing on improved testing, integration with Perplexity AI for research-backed task updates, and core logic refactoring for better maintainability and functionality.

**Testing Infrastructure Setup:**
- Implemented Jest as the primary testing framework, setting up a comprehensive testing environment.
- Added new test scripts to  including , , and  for streamlined testing workflows.
- Integrated necessary devDependencies for testing, such as , , , , and , to support unit, integration, and end-to-end testing.

**Dependency Updates:**
- Updated  and  to reflect the latest dependency versions, ensuring project stability and access to the newest features and security patches.
- Upgraded  to version 0.9.16 and usage: openai [-h] [-v] [-b API_BASE] [-k API_KEY] [-p PROXY [PROXY ...]]
              [-o ORGANIZATION] [-t {openai,azure}]
              [--api-version API_VERSION] [--azure-endpoint AZURE_ENDPOINT]
              [--azure-ad-token AZURE_AD_TOKEN] [-V]
              {api,tools,migrate,grit} ...

positional arguments:
  {api,tools,migrate,grit}
    api                 Direct API calls
    tools               Client side tools for convenience

options:
  -h, --help            show this help message and exit
  -v, --verbose         Set verbosity.
  -b, --api-base API_BASE
                        What API base url to use.
  -k, --api-key API_KEY
                        What API key to use.
  -p, --proxy PROXY [PROXY ...]
                        What proxy to use.
  -o, --organization ORGANIZATION
                        Which organization to run as (will use your default
                        organization if not specified)
  -t, --api-type {openai,azure}
                        The backend API to call, must be `openai` or `azure`
  --api-version API_VERSION
                        The Azure API version, e.g.
                        'https://learn.microsoft.com/en-us/azure/ai-
                        services/openai/reference#rest-api-versioning'
  --azure-endpoint AZURE_ENDPOINT
                        The Azure endpoint, e.g.
                        'https://endpoint.openai.azure.com'
  --azure-ad-token AZURE_AD_TOKEN
                        A token from Azure Active Directory,
                        https://www.microsoft.com/en-
                        us/security/business/identity-access/microsoft-entra-
                        id
  -V, --version         show program's version number and exit to 4.89.0.
- Added  dependency (version 2.3.0) and updated  related dependencies to their latest versions.

**Perplexity AI Integration for Research-Backed Updates:**
- Introduced an option to leverage Perplexity AI for task updates, enabling research-backed enhancements to task details.
- Implemented logic to initialize a Perplexity AI client if the  environment variable is available.
- Modified the  function to accept a  parameter, allowing dynamic selection between Perplexity AI and Claude AI for task updates based on API key availability and user preference.
- Enhanced  to handle responses from Perplexity AI and update tasks accordingly, including improved error handling and logging for robust operation.

**Core Logic Refactoring and Improvements:**
- Refactored the  function to utilize task IDs instead of dependency IDs, ensuring consistency and clarity in dependency management.
- Implemented a new  function to rigorously check for both circular dependencies and self-dependencies within tasks, improving task relationship integrity.
- Enhanced UI elements in :
    - Refactored  to incorporate icons for different task statuses and utilize a  object for color mapping, improving visual representation of task status.
    - Updated  to display colored complexity scores with emojis, providing a more intuitive and visually appealing representation of task complexity.
- Refactored the task data structure creation and validation process:
    - Updated the JSON Schema for  to reflect a more streamlined and efficient task structure.
    - Implemented Task Model Classes for better data modeling and type safety.
    - Improved File System Operations for task data management.
    - Developed robust Validation Functions and an Error Handling System to ensure data integrity and application stability.

**Testing Guidelines Implementation:**
- Implemented guidelines for writing testable code when developing new features, promoting a test-driven development approach.
- Added testing requirements and best practices for unit, integration, and edge case testing to ensure comprehensive test coverage.
- Updated the development workflow to mandate writing tests before proceeding with configuration and documentation updates, reinforcing the importance of testing throughout the development lifecycle.

This commit collectively enhances the Task Master CLI's reliability, functionality, and developer experience through improved testing practices, AI-powered research capabilities, and a more robust and maintainable codebase.
2025-03-24 13:28:08 -04:00

903 lines
31 KiB
JavaScript

/**
* ui.js
* User interface functions for the Task Master CLI
*/
import chalk from 'chalk';
import figlet from 'figlet';
import boxen from 'boxen';
import ora from 'ora';
import Table from 'cli-table3';
import gradient from 'gradient-string';
import { CONFIG, log, findTaskById, readJSON, readComplexityReport, truncate } from './utils.js';
import path from 'path';
import fs from 'fs';
import { findNextTask, analyzeTaskComplexity } from './task-manager.js';
// Create a color gradient for the banner
const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']);
const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']);
/**
* Display a fancy banner for the CLI
*/
function displayBanner() {
console.clear();
const bannerText = figlet.textSync('Task Master', {
font: 'Standard',
horizontalLayout: 'default',
verticalLayout: 'default'
});
console.log(coolGradient(bannerText));
// Add creator credit line below the banner
console.log(chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano'));
// Read version directly from package.json
let version = CONFIG.projectVersion; // Default fallback
try {
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJsonPath)) {
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
version = packageJson.version;
}
} catch (error) {
// Silently fall back to default version
}
console.log(boxen(chalk.white(`${chalk.bold('Version:')} ${version} ${chalk.bold('Project:')} ${CONFIG.projectName}`), {
padding: 1,
margin: { top: 0, bottom: 1 },
borderStyle: 'round',
borderColor: 'cyan'
}));
}
/**
* Start a loading indicator with an animated spinner
* @param {string} message - Message to display next to the spinner
* @returns {Object} Spinner object
*/
function startLoadingIndicator(message) {
const spinner = ora({
text: message,
color: 'cyan'
}).start();
return spinner;
}
/**
* Stop a loading indicator
* @param {Object} spinner - Spinner object to stop
*/
function stopLoadingIndicator(spinner) {
if (spinner && spinner.stop) {
spinner.stop();
}
}
/**
* Create a progress bar using ASCII characters
* @param {number} percent - Progress percentage (0-100)
* @param {number} length - Length of the progress bar in characters
* @returns {string} Formatted progress bar
*/
function createProgressBar(percent, length = 30) {
const filled = Math.round(percent * length / 100);
const empty = length - filled;
const filledBar = '█'.repeat(filled);
const emptyBar = '░'.repeat(empty);
return `${filledBar}${emptyBar} ${percent.toFixed(0)}%`;
}
/**
* Get a colored status string based on the status value
* @param {string} status - Task status (e.g., "done", "pending", "in-progress")
* @returns {string} Colored status string
*/
function getStatusWithColor(status) {
if (!status) {
return chalk.gray('❓ unknown');
}
const statusConfig = {
'done': { color: chalk.green, icon: '✅' },
'completed': { color: chalk.green, icon: '✅' },
'pending': { color: chalk.yellow, icon: '⏱️' },
'in-progress': { color: chalk.blue, icon: '🔄' },
'deferred': { color: chalk.gray, icon: '⏱️' },
'blocked': { color: chalk.red, icon: '❌' },
'review': { color: chalk.magenta, icon: '👀' }
};
const config = statusConfig[status.toLowerCase()] || { color: chalk.red, icon: '❌' };
return config.color(`${config.icon} ${status}`);
}
/**
* Format dependencies list with status indicators
* @param {Array} dependencies - Array of dependency IDs
* @param {Array} allTasks - Array of all tasks
* @param {boolean} forConsole - Whether the output is for console display
* @returns {string} Formatted dependencies string
*/
function formatDependenciesWithStatus(dependencies, allTasks, forConsole = false) {
if (!dependencies || !Array.isArray(dependencies) || dependencies.length === 0) {
return forConsole ? chalk.gray('None') : 'None';
}
const formattedDeps = dependencies.map(depId => {
const depTask = findTaskById(allTasks, depId);
if (!depTask) {
return forConsole ?
chalk.red(`${depId} (Not found)`) :
`${depId} (Not found)`;
}
const status = depTask.status || 'pending';
const isDone = status.toLowerCase() === 'done' || status.toLowerCase() === 'completed';
if (forConsole) {
return isDone ?
chalk.green(`${depId}`) :
chalk.red(`${depId}`);
}
const statusIcon = isDone ? '✅' : '⏱️';
return `${statusIcon} ${depId} (${status})`;
});
return formattedDeps.join(', ');
}
/**
* Display a comprehensive help guide
*/
function displayHelp() {
displayBanner();
console.log(boxen(
chalk.white.bold('Task Master CLI'),
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
// Command categories
const commandCategories = [
{
title: 'Task Generation',
color: 'cyan',
commands: [
{ name: 'parse-prd', args: '--input=<file.txt> [--tasks=10]',
desc: 'Generate tasks from a PRD document' },
{ name: 'generate', args: '',
desc: 'Create individual task files from tasks.json' }
]
},
{
title: 'Task Management',
color: 'green',
commands: [
{ name: 'list', args: '[--status=<status>] [--with-subtasks]',
desc: 'List all tasks with their status' },
{ name: 'set-status', args: '--id=<id> --status=<status>',
desc: 'Update task status (done, pending, etc.)' },
{ name: 'update', args: '--from=<id> --prompt="<context>"',
desc: 'Update tasks based on new requirements' },
{ name: 'add-task', args: '--prompt="<text>" [--dependencies=<ids>] [--priority=<priority>]',
desc: 'Add a new task using AI' },
{ name: 'add-dependency', args: '--id=<id> --depends-on=<id>',
desc: 'Add a dependency to a task' },
{ name: 'remove-dependency', args: '--id=<id> --depends-on=<id>',
desc: 'Remove a dependency from a task' }
]
},
{
title: 'Task Analysis & Detail',
color: 'yellow',
commands: [
{ name: 'analyze-complexity', args: '[--research] [--threshold=5]',
desc: 'Analyze tasks and generate expansion recommendations' },
{ name: 'complexity-report', args: '[--file=<path>]',
desc: 'Display the complexity analysis report' },
{ name: 'expand', args: '--id=<id> [--num=5] [--research] [--prompt="<context>"]',
desc: 'Break down tasks into detailed subtasks' },
{ name: 'expand --all', args: '[--force] [--research]',
desc: 'Expand all pending tasks with subtasks' },
{ name: 'clear-subtasks', args: '--id=<id>',
desc: 'Remove subtasks from specified tasks' }
]
},
{
title: 'Task Navigation & Viewing',
color: 'magenta',
commands: [
{ name: 'next', args: '',
desc: 'Show the next task to work on based on dependencies' },
{ name: 'show', args: '<id>',
desc: 'Display detailed information about a specific task' }
]
},
{
title: 'Dependency Management',
color: 'blue',
commands: [
{ name: 'validate-dependencies', args: '',
desc: 'Identify invalid dependencies without fixing them' },
{ name: 'fix-dependencies', args: '',
desc: 'Fix invalid dependencies automatically' }
]
}
];
// Display each category
commandCategories.forEach(category => {
console.log(boxen(
chalk[category.color].bold(category.title),
{
padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 },
borderColor: category.color,
borderStyle: 'round'
}
));
const commandTable = new Table({
colWidths: [25, 40, 45],
chars: {
'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '',
'right': '', 'right-mid': '', 'middle': ' '
},
style: { border: [], 'padding-left': 4 }
});
category.commands.forEach((cmd, index) => {
commandTable.push([
`${chalk.yellow.bold(cmd.name)}${chalk.reset('')}`,
`${chalk.white(cmd.args)}${chalk.reset('')}`,
`${chalk.dim(cmd.desc)}${chalk.reset('')}`
]);
});
console.log(commandTable.toString());
console.log('');
});
// Display environment variables section
console.log(boxen(
chalk.cyan.bold('Environment Variables'),
{
padding: { left: 2, right: 2, top: 0, bottom: 0 },
margin: { top: 1, bottom: 0 },
borderColor: 'cyan',
borderStyle: 'round'
}
));
const envTable = new Table({
colWidths: [30, 50, 30],
chars: {
'top': '', 'top-mid': '', 'top-left': '', 'top-right': '',
'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '',
'left': '', 'left-mid': '', 'mid': '', 'mid-mid': '',
'right': '', 'right-mid': '', 'middle': ' '
},
style: { border: [], 'padding-left': 4 }
});
envTable.push(
[`${chalk.yellow('ANTHROPIC_API_KEY')}${chalk.reset('')}`,
`${chalk.white('Your Anthropic API key')}${chalk.reset('')}`,
`${chalk.dim('Required')}${chalk.reset('')}`],
[`${chalk.yellow('MODEL')}${chalk.reset('')}`,
`${chalk.white('Claude model to use')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.model}`)}${chalk.reset('')}`],
[`${chalk.yellow('MAX_TOKENS')}${chalk.reset('')}`,
`${chalk.white('Maximum tokens for responses')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.maxTokens}`)}${chalk.reset('')}`],
[`${chalk.yellow('TEMPERATURE')}${chalk.reset('')}`,
`${chalk.white('Temperature for model responses')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.temperature}`)}${chalk.reset('')}`],
[`${chalk.yellow('PERPLEXITY_API_KEY')}${chalk.reset('')}`,
`${chalk.white('Perplexity API key for research')}${chalk.reset('')}`,
`${chalk.dim('Optional')}${chalk.reset('')}`],
[`${chalk.yellow('PERPLEXITY_MODEL')}${chalk.reset('')}`,
`${chalk.white('Perplexity model to use')}${chalk.reset('')}`,
`${chalk.dim('Default: sonar-small-online')}${chalk.reset('')}`],
[`${chalk.yellow('DEBUG')}${chalk.reset('')}`,
`${chalk.white('Enable debug logging')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.debug}`)}${chalk.reset('')}`],
[`${chalk.yellow('LOG_LEVEL')}${chalk.reset('')}`,
`${chalk.white('Console output level (debug,info,warn,error)')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.logLevel}`)}${chalk.reset('')}`],
[`${chalk.yellow('DEFAULT_SUBTASKS')}${chalk.reset('')}`,
`${chalk.white('Default number of subtasks to generate')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.defaultSubtasks}`)}${chalk.reset('')}`],
[`${chalk.yellow('DEFAULT_PRIORITY')}${chalk.reset('')}`,
`${chalk.white('Default task priority')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.defaultPriority}`)}${chalk.reset('')}`],
[`${chalk.yellow('PROJECT_NAME')}${chalk.reset('')}`,
`${chalk.white('Project name displayed in UI')}${chalk.reset('')}`,
`${chalk.dim(`Default: ${CONFIG.projectName}`)}${chalk.reset('')}`]
);
console.log(envTable.toString());
console.log('');
}
/**
* Get colored complexity score
* @param {number} score - Complexity score (1-10)
* @returns {string} Colored complexity score
*/
function getComplexityWithColor(score) {
if (score <= 3) return chalk.green(`🟢 ${score}`);
if (score <= 6) return chalk.yellow(`🟡 ${score}`);
return chalk.red(`🔴 ${score}`);
}
/**
* Display the next task to work on
* @param {string} tasksPath - Path to the tasks.json file
*/
async function displayNextTask(tasksPath) {
displayBanner();
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', "No valid tasks found.");
process.exit(1);
}
// Find the next task
const nextTask = findNextTask(data.tasks);
if (!nextTask) {
console.log(boxen(
chalk.yellow('No eligible tasks found!\n\n') +
'All pending tasks have unsatisfied dependencies, or all tasks are completed.',
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1 } }
));
return;
}
// Display the task in a nice format
console.log(boxen(
chalk.white.bold(`Next Task: #${nextTask.id} - ${nextTask.title}`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
// Create a table with task details
const taskTable = new Table({
style: {
head: [],
border: [],
'padding-top': 0,
'padding-bottom': 0,
compact: true
},
chars: {
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
},
colWidths: [15, 75]
});
// Priority with color
const priorityColors = {
'high': chalk.red.bold,
'medium': chalk.yellow,
'low': chalk.gray
};
const priorityColor = priorityColors[nextTask.priority || 'medium'] || chalk.white;
// Add task details to table
taskTable.push(
[chalk.cyan.bold('ID:'), nextTask.id.toString()],
[chalk.cyan.bold('Title:'), nextTask.title],
[chalk.cyan.bold('Priority:'), priorityColor(nextTask.priority || 'medium')],
[chalk.cyan.bold('Dependencies:'), formatDependenciesWithStatus(nextTask.dependencies, data.tasks, true)],
[chalk.cyan.bold('Description:'), nextTask.description]
);
console.log(taskTable.toString());
// If task has details, show them in a separate box
if (nextTask.details && nextTask.details.trim().length > 0) {
console.log(boxen(
chalk.white.bold('Implementation Details:') + '\n\n' +
nextTask.details,
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
}
// Show subtasks if they exist
if (nextTask.subtasks && nextTask.subtasks.length > 0) {
console.log(boxen(
chalk.white.bold('Subtasks'),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 1, bottom: 0 }, borderColor: 'magenta', borderStyle: 'round' }
));
// Create a table for subtasks
const subtaskTable = new Table({
head: [
chalk.magenta.bold('ID'),
chalk.magenta.bold('Status'),
chalk.magenta.bold('Title'),
chalk.magenta.bold('Dependencies')
],
colWidths: [6, 12, 50, 20],
style: {
head: [],
border: [],
'padding-top': 0,
'padding-bottom': 0,
compact: true
},
chars: {
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
}
});
// Add subtasks to table
nextTask.subtasks.forEach(st => {
const statusColor = {
'done': chalk.green,
'completed': chalk.green,
'pending': chalk.yellow,
'in-progress': chalk.blue
}[st.status || 'pending'] || chalk.white;
// Format subtask dependencies
let subtaskDeps = 'None';
if (st.dependencies && st.dependencies.length > 0) {
// Format dependencies with correct notation
const formattedDeps = st.dependencies.map(depId => {
if (typeof depId === 'number' && depId < 100) {
return `${nextTask.id}.${depId}`;
}
return depId;
});
subtaskDeps = formatDependenciesWithStatus(formattedDeps, data.tasks, true);
}
subtaskTable.push([
`${nextTask.id}.${st.id}`,
statusColor(st.status || 'pending'),
st.title,
subtaskDeps
]);
});
console.log(subtaskTable.toString());
} else {
// Suggest expanding if no subtasks
console.log(boxen(
chalk.yellow('No subtasks found. Consider breaking down this task:') + '\n' +
chalk.white(`Run: ${chalk.cyan(`task-master expand --id=${nextTask.id}`)}`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
}
// Show action suggestions
console.log(boxen(
chalk.white.bold('Suggested Actions:') + '\n' +
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` +
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=done`)}\n` +
(nextTask.subtasks && nextTask.subtasks.length > 0
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${nextTask.id}.1 --status=done`)}`
: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
));
}
/**
* Display a specific task by ID
* @param {string} tasksPath - Path to the tasks.json file
* @param {string|number} taskId - The ID of the task to display
*/
async function displayTaskById(tasksPath, taskId) {
displayBanner();
// Read the tasks file
const data = readJSON(tasksPath);
if (!data || !data.tasks) {
log('error', "No valid tasks found.");
process.exit(1);
}
// Find the task by ID
const task = findTaskById(data.tasks, taskId);
if (!task) {
console.log(boxen(
chalk.yellow(`Task with ID ${taskId} not found!`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1 } }
));
return;
}
// Handle subtask display specially
if (task.isSubtask || task.parentTask) {
console.log(boxen(
chalk.white.bold(`Subtask: #${task.parentTask.id}.${task.id} - ${task.title}`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'magenta', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
// Create a table with subtask details
const taskTable = new Table({
style: {
head: [],
border: [],
'padding-top': 0,
'padding-bottom': 0,
compact: true
},
chars: {
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
},
colWidths: [15, 75]
});
// Add subtask details to table
taskTable.push(
[chalk.cyan.bold('ID:'), `${task.parentTask.id}.${task.id}`],
[chalk.cyan.bold('Parent Task:'), `#${task.parentTask.id} - ${task.parentTask.title}`],
[chalk.cyan.bold('Title:'), task.title],
[chalk.cyan.bold('Status:'), getStatusWithColor(task.status || 'pending')],
[chalk.cyan.bold('Description:'), task.description || 'No description provided.']
);
console.log(taskTable.toString());
// Show action suggestions for subtask
console.log(boxen(
chalk.white.bold('Suggested Actions:') + '\n' +
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=in-progress`)}\n` +
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.parentTask.id}.${task.id} --status=done`)}\n` +
`${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${task.parentTask.id}`)}`,
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
));
return;
}
// Display a regular task
console.log(boxen(
chalk.white.bold(`Task: #${task.id} - ${task.title}`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
// Create a table with task details
const taskTable = new Table({
style: {
head: [],
border: [],
'padding-top': 0,
'padding-bottom': 0,
compact: true
},
chars: {
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
},
colWidths: [15, 75]
});
// Priority with color
const priorityColors = {
'high': chalk.red.bold,
'medium': chalk.yellow,
'low': chalk.gray
};
const priorityColor = priorityColors[task.priority || 'medium'] || chalk.white;
// Add task details to table
taskTable.push(
[chalk.cyan.bold('ID:'), task.id.toString()],
[chalk.cyan.bold('Title:'), task.title],
[chalk.cyan.bold('Status:'), getStatusWithColor(task.status || 'pending')],
[chalk.cyan.bold('Priority:'), priorityColor(task.priority || 'medium')],
[chalk.cyan.bold('Dependencies:'), formatDependenciesWithStatus(task.dependencies, data.tasks, true)],
[chalk.cyan.bold('Description:'), task.description]
);
console.log(taskTable.toString());
// If task has details, show them in a separate box
if (task.details && task.details.trim().length > 0) {
console.log(boxen(
chalk.white.bold('Implementation Details:') + '\n\n' +
task.details,
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
}
// Show test strategy if available
if (task.testStrategy && task.testStrategy.trim().length > 0) {
console.log(boxen(
chalk.white.bold('Test Strategy:') + '\n\n' +
task.testStrategy,
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
}
// Show subtasks if they exist
if (task.subtasks && task.subtasks.length > 0) {
console.log(boxen(
chalk.white.bold('Subtasks'),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, margin: { top: 1, bottom: 0 }, borderColor: 'magenta', borderStyle: 'round' }
));
// Create a table for subtasks
const subtaskTable = new Table({
head: [
chalk.magenta.bold('ID'),
chalk.magenta.bold('Status'),
chalk.magenta.bold('Title'),
chalk.magenta.bold('Dependencies')
],
colWidths: [6, 12, 50, 20],
style: {
head: [],
border: [],
'padding-top': 0,
'padding-bottom': 0,
compact: true
},
chars: {
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
}
});
// Add subtasks to table
task.subtasks.forEach(st => {
const statusColor = {
'done': chalk.green,
'completed': chalk.green,
'pending': chalk.yellow,
'in-progress': chalk.blue
}[st.status || 'pending'] || chalk.white;
// Format subtask dependencies
let subtaskDeps = 'None';
if (st.dependencies && st.dependencies.length > 0) {
// Format dependencies with correct notation
const formattedDeps = st.dependencies.map(depId => {
if (typeof depId === 'number' && depId < 100) {
return `${task.id}.${depId}`;
}
return depId;
});
subtaskDeps = formatDependenciesWithStatus(formattedDeps, data.tasks, true);
}
subtaskTable.push([
`${task.id}.${st.id}`,
statusColor(st.status || 'pending'),
st.title,
subtaskDeps
]);
});
console.log(subtaskTable.toString());
} else {
// Suggest expanding if no subtasks
console.log(boxen(
chalk.yellow('No subtasks found. Consider breaking down this task:') + '\n' +
chalk.white(`Run: ${chalk.cyan(`task-master expand --id=${task.id}`)}`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1, bottom: 0 } }
));
}
// Show action suggestions
console.log(boxen(
chalk.white.bold('Suggested Actions:') + '\n' +
`${chalk.cyan('1.')} Mark as in-progress: ${chalk.yellow(`task-master set-status --id=${task.id} --status=in-progress`)}\n` +
`${chalk.cyan('2.')} Mark as done when completed: ${chalk.yellow(`task-master set-status --id=${task.id} --status=done`)}\n` +
(task.subtasks && task.subtasks.length > 0
? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}`
: `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${task.id}`)}`),
{ padding: { top: 0, bottom: 0, left: 1, right: 1 }, borderColor: 'green', borderStyle: 'round', margin: { top: 1 } }
));
}
/**
* Display the complexity analysis report in a nice format
* @param {string} reportPath - Path to the complexity report file
*/
async function displayComplexityReport(reportPath) {
displayBanner();
// Check if the report exists
if (!fs.existsSync(reportPath)) {
console.log(boxen(
chalk.yellow(`No complexity report found at ${reportPath}\n\n`) +
'Would you like to generate one now?',
{ padding: 1, borderColor: 'yellow', borderStyle: 'round', margin: { top: 1 } }
));
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
const answer = await new Promise(resolve => {
readline.question(chalk.cyan('Generate complexity report? (y/n): '), resolve);
});
readline.close();
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
// Call the analyze-complexity command
console.log(chalk.blue('Generating complexity report...'));
await analyzeTaskComplexity({
output: reportPath,
research: false, // Default to no research for speed
file: 'tasks/tasks.json'
});
// Read the newly generated report
return displayComplexityReport(reportPath);
} else {
console.log(chalk.yellow('Report generation cancelled.'));
return;
}
}
// Read the report
let report;
try {
report = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
} catch (error) {
log('error', `Error reading complexity report: ${error.message}`);
return;
}
// Display report header
console.log(boxen(
chalk.white.bold('Task Complexity Analysis Report'),
{ padding: 1, borderColor: 'blue', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
// Display metadata
const metaTable = new Table({
style: {
head: [],
border: [],
'padding-top': 0,
'padding-bottom': 0,
compact: true
},
chars: {
'mid': '', 'left-mid': '', 'mid-mid': '', 'right-mid': ''
},
colWidths: [20, 50]
});
metaTable.push(
[chalk.cyan.bold('Generated:'), new Date(report.meta.generatedAt).toLocaleString()],
[chalk.cyan.bold('Tasks Analyzed:'), report.meta.tasksAnalyzed],
[chalk.cyan.bold('Threshold Score:'), report.meta.thresholdScore],
[chalk.cyan.bold('Project:'), report.meta.projectName],
[chalk.cyan.bold('Research-backed:'), report.meta.usedResearch ? 'Yes' : 'No']
);
console.log(metaTable.toString());
// Sort tasks by complexity score (highest first)
const sortedTasks = [...report.complexityAnalysis].sort((a, b) => b.complexityScore - a.complexityScore);
// Determine which tasks need expansion based on threshold
const tasksNeedingExpansion = sortedTasks.filter(task => task.complexityScore >= report.meta.thresholdScore);
const simpleTasks = sortedTasks.filter(task => task.complexityScore < report.meta.thresholdScore);
// Create progress bar to show complexity distribution
const complexityDistribution = [0, 0, 0]; // Low (0-4), Medium (5-7), High (8-10)
sortedTasks.forEach(task => {
if (task.complexityScore < 5) complexityDistribution[0]++;
else if (task.complexityScore < 8) complexityDistribution[1]++;
else complexityDistribution[2]++;
});
const percentLow = Math.round((complexityDistribution[0] / sortedTasks.length) * 100);
const percentMedium = Math.round((complexityDistribution[1] / sortedTasks.length) * 100);
const percentHigh = Math.round((complexityDistribution[2] / sortedTasks.length) * 100);
console.log(boxen(
chalk.white.bold('Complexity Distribution\n\n') +
`${chalk.green.bold('Low (1-4):')} ${complexityDistribution[0]} tasks (${percentLow}%)\n` +
`${chalk.yellow.bold('Medium (5-7):')} ${complexityDistribution[1]} tasks (${percentMedium}%)\n` +
`${chalk.red.bold('High (8-10):')} ${complexityDistribution[2]} tasks (${percentHigh}%)`,
{ padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1, bottom: 1 } }
));
// Create table for tasks that need expansion
if (tasksNeedingExpansion.length > 0) {
console.log(boxen(
chalk.yellow.bold(`Tasks Recommended for Expansion (${tasksNeedingExpansion.length})`),
{ padding: { left: 2, right: 2, top: 0, bottom: 0 }, margin: { top: 1, bottom: 0 }, borderColor: 'yellow', borderStyle: 'round' }
));
const complexTable = new Table({
head: [
chalk.yellow.bold('ID'),
chalk.yellow.bold('Title'),
chalk.yellow.bold('Score'),
chalk.yellow.bold('Subtasks'),
chalk.yellow.bold('Expansion Command')
],
colWidths: [5, 40, 8, 10, 45],
style: { head: [], border: [] }
});
tasksNeedingExpansion.forEach(task => {
complexTable.push([
task.taskId,
truncate(task.taskTitle, 37),
getComplexityWithColor(task.complexityScore),
task.recommendedSubtasks,
chalk.cyan(`task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}`)
]);
});
console.log(complexTable.toString());
}
// Create table for simple tasks
if (simpleTasks.length > 0) {
console.log(boxen(
chalk.green.bold(`Simple Tasks (${simpleTasks.length})`),
{ padding: { left: 2, right: 2, top: 0, bottom: 0 }, margin: { top: 1, bottom: 0 }, borderColor: 'green', borderStyle: 'round' }
));
const simpleTable = new Table({
head: [
chalk.green.bold('ID'),
chalk.green.bold('Title'),
chalk.green.bold('Score'),
chalk.green.bold('Reasoning')
],
colWidths: [5, 40, 8, 50],
style: { head: [], border: [] }
});
simpleTasks.forEach(task => {
simpleTable.push([
task.taskId,
truncate(task.taskTitle, 37),
getComplexityWithColor(task.complexityScore),
truncate(task.reasoning, 47)
]);
});
console.log(simpleTable.toString());
}
// Show action suggestions
console.log(boxen(
chalk.white.bold('Suggested Actions:') + '\n\n' +
`${chalk.cyan('1.')} Expand all complex tasks: ${chalk.yellow(`task-master expand --all`)}\n` +
`${chalk.cyan('2.')} Expand a specific task: ${chalk.yellow(`task-master expand --id=<id>`)}\n` +
`${chalk.cyan('3.')} Regenerate with research: ${chalk.yellow(`task-master analyze-complexity --research`)}`,
{ padding: 1, borderColor: 'cyan', borderStyle: 'round', margin: { top: 1 } }
));
}
// Export UI functions
export {
displayBanner,
startLoadingIndicator,
stopLoadingIndicator,
createProgressBar,
getStatusWithColor,
formatDependenciesWithStatus,
displayHelp,
getComplexityWithColor,
displayNextTask,
displayTaskById,
displayComplexityReport,
};