diff --git a/.taskmaster/tasks/task_095.txt b/.taskmaster/tasks/task_095.txt index 39038951..fec48871 100644 --- a/.taskmaster/tasks/task_095.txt +++ b/.taskmaster/tasks/task_095.txt @@ -1,6 +1,6 @@ # Task ID: 95 # Title: Implement .taskmaster Directory Structure -# Status: in-progress +# Status: done # Dependencies: 1, 3, 4, 17 # Priority: high # Description: Consolidate all Task Master-managed files in user projects into a clean, centralized .taskmaster/ directory structure to improve organization and keep user project directories clean, based on GitHub issue #275. @@ -105,7 +105,7 @@ Update Task Master's file handling code to use the new paths: tasks in .taskmast ### Details: Update the task file generation system to create and read task files from .taskmaster/tasks/ instead of tasks/. Ensure all template paths are updated. Modify any path resolution logic specific to task file handling. -## 4. Implement backward compatibility logic [in-progress] +## 4. Implement backward compatibility logic [done] ### Dependencies: 95.2, 95.3 ### Description: Add fallback mechanisms to support both old and new file locations during transition ### Details: @@ -141,7 +141,7 @@ Update README.md and other documentation to reflect the new .taskmaster structur ### Details: Create functionality to support user-defined templates in .taskmaster/templates/. Allow users to store custom task templates, PRD templates, or other reusable files. Update Task Master commands to recognize and use templates from this directory when available. -## 10. Verify clean user project directories [in-progress] +## 10. Verify clean user project directories [done] ### Dependencies: 95.8, 95.9 ### Description: Ensure the new structure keeps user project root directories clean and organized ### Details: diff --git a/.taskmaster/tasks/tasks.json b/.taskmaster/tasks/tasks.json index 4553f8cc..797456da 100644 --- a/.taskmaster/tasks/tasks.json +++ b/.taskmaster/tasks/tasks.json @@ -5681,7 +5681,7 @@ "id": 95, "title": "Implement .taskmaster Directory Structure", "description": "Consolidate all Task Master-managed files in user projects into a clean, centralized .taskmaster/ directory structure to improve organization and keep user project directories clean, based on GitHub issue #275.", - "status": "in-progress", + "status": "done", "dependencies": [ 1, 3, @@ -5732,7 +5732,7 @@ 3 ], "details": "Implement path fallback logic that checks both old and new locations when files aren't found. Add deprecation warnings when old paths are used, informing users about the new structure. Ensure error messages are clear about the transition.", - "status": "in-progress", + "status": "done", "testStrategy": "Test with both old and new directory structures to verify fallback works correctly. Verify deprecation warnings appear when using old paths." }, { @@ -5804,7 +5804,7 @@ 9 ], "details": "Validate that after implementing the new structure, user project root directories only contain their actual project files plus the single .taskmaster/ directory. Verify that no Task Master files are created outside of .taskmaster/. Test that users can easily add .taskmaster/ to .gitignore if they choose to exclude Task Master files from version control.", - "status": "in-progress", + "status": "done", "testStrategy": "Test complete workflows and verify only .taskmaster/ directory is created in project root. Check that all Task Master operations respect the new file organization. Verify .gitignore compatibility." } ] diff --git a/scripts/modules/ui.js b/scripts/modules/ui.js index c17d6b80..1118f8c7 100644 --- a/scripts/modules/ui.js +++ b/scripts/modules/ui.js @@ -3,73 +3,73 @@ * 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 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 { - log, - findTaskById, - readJSON, - truncate, - isSilentMode -} from './utils.js'; -import fs from 'fs'; + log, + findTaskById, + readJSON, + truncate, + isSilentMode, +} from "./utils.js"; +import fs from "fs"; import { - findNextTask, - analyzeTaskComplexity, - readComplexityReport -} from './task-manager.js'; -import { getProjectName, getDefaultSubtasks } from './config-manager.js'; -import { TASK_STATUS_OPTIONS } from '../../src/constants/task-status.js'; + findNextTask, + analyzeTaskComplexity, + readComplexityReport, +} 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'; + 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 -const coolGradient = gradient(['#00b4d8', '#0077b6', '#03045e']); -const warmGradient = gradient(['#fb8b24', '#e36414', '#9a031e']); +const coolGradient = gradient(["#00b4d8", "#0077b6", "#03045e"]); +const warmGradient = gradient(["#fb8b24", "#e36414", "#9a031e"]); /** * Display a fancy banner for the CLI */ function displayBanner() { - if (isSilentMode()) return; + if (isSilentMode()) return; - console.clear(); - const bannerText = figlet.textSync('Task Master', { - font: 'Standard', - horizontalLayout: 'default', - verticalLayout: 'default' - }); + console.clear(); + const bannerText = figlet.textSync("Task Master", { + font: "Standard", + horizontalLayout: "default", + verticalLayout: "default", + }); - console.log(coolGradient(bannerText)); + console.log(coolGradient(bannerText)); - // Add creator credit line below the banner - console.log( - chalk.dim('by ') + chalk.cyan.underline('https://x.com/eyaltoledano') - ); + // 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 - const version = getTaskMasterVersion(); + // Read version directly from package.json + const version = getTaskMasterVersion(); - console.log( - boxen( - chalk.white( - `${chalk.bold('Version:')} ${version} ${chalk.bold('Project:')} ${getProjectName(null)}` - ), - { - padding: 1, - margin: { top: 0, bottom: 1 }, - borderStyle: 'round', - borderColor: 'cyan' - } - ) - ); + console.log( + boxen( + chalk.white( + `${chalk.bold("Version:")} ${version} ${chalk.bold("Project:")} ${getProjectName(null)}` + ), + { + padding: 1, + margin: { top: 0, bottom: 1 }, + borderStyle: "round", + borderColor: "cyan", + } + ) + ); } /** @@ -78,12 +78,12 @@ function displayBanner() { * @returns {Object} Spinner object */ function startLoadingIndicator(message) { - const spinner = ora({ - text: message, - color: 'cyan' - }).start(); + const spinner = ora({ + text: message, + color: "cyan", + }).start(); - return spinner; + return spinner; } /** @@ -91,9 +91,9 @@ function startLoadingIndicator(message) { * @param {Object} spinner - Spinner object to stop */ function stopLoadingIndicator(spinner) { - if (spinner && spinner.stop) { - spinner.stop(); - } + if (spinner && spinner.stop) { + spinner.stop(); + } } /** @@ -104,120 +104,120 @@ function stopLoadingIndicator(spinner) { * @returns {string} The formatted progress bar */ function createProgressBar(percent, length = 30, statusBreakdown = null) { - // Adjust the percent to treat deferred and cancelled as complete - const effectivePercent = statusBreakdown - ? Math.min( - 100, - percent + - (statusBreakdown.deferred || 0) + - (statusBreakdown.cancelled || 0) - ) - : percent; + // Adjust the percent to treat deferred and cancelled as complete + const effectivePercent = statusBreakdown + ? Math.min( + 100, + percent + + (statusBreakdown.deferred || 0) + + (statusBreakdown.cancelled || 0) + ) + : percent; - // Calculate how many characters to fill for "true completion" - const trueCompletedFilled = Math.round((percent * length) / 100); + // Calculate how many characters to fill for "true completion" + const trueCompletedFilled = Math.round((percent * length) / 100); - // Calculate how many characters to fill for "effective completion" (including deferred/cancelled) - const effectiveCompletedFilled = Math.round( - (effectivePercent * length) / 100 - ); + // Calculate how many characters to fill for "effective completion" (including deferred/cancelled) + const effectiveCompletedFilled = Math.round( + (effectivePercent * length) / 100 + ); - // The "deferred/cancelled" section (difference between true and effective) - const deferredCancelledFilled = - effectiveCompletedFilled - trueCompletedFilled; + // The "deferred/cancelled" section (difference between true and effective) + const deferredCancelledFilled = + effectiveCompletedFilled - trueCompletedFilled; - // Set the empty section (remaining after effective completion) - const empty = length - effectiveCompletedFilled; + // Set the empty section (remaining after effective completion) + const empty = length - effectiveCompletedFilled; - // Determine color based on percentage for the completed section - let completedColor; - if (percent < 25) { - completedColor = chalk.red; - } else if (percent < 50) { - completedColor = chalk.hex('#FFA500'); // Orange - } else if (percent < 75) { - completedColor = chalk.yellow; - } else if (percent < 100) { - completedColor = chalk.green; - } else { - completedColor = chalk.hex('#006400'); // Dark green - } + // Determine color based on percentage for the completed section + let completedColor; + if (percent < 25) { + completedColor = chalk.red; + } else if (percent < 50) { + completedColor = chalk.hex("#FFA500"); // Orange + } else if (percent < 75) { + completedColor = chalk.yellow; + } else if (percent < 100) { + completedColor = chalk.green; + } else { + completedColor = chalk.hex("#006400"); // Dark green + } - // Create colored sections - const completedSection = completedColor('█'.repeat(trueCompletedFilled)); + // Create colored sections + const completedSection = completedColor("█".repeat(trueCompletedFilled)); - // Gray section for deferred/cancelled items - const deferredCancelledSection = chalk.gray( - '█'.repeat(deferredCancelledFilled) - ); + // Gray section for deferred/cancelled items + const deferredCancelledSection = chalk.gray( + "█".repeat(deferredCancelledFilled) + ); - // If we have a status breakdown, create a multi-colored remaining section - let remainingSection = ''; + // If we have a status breakdown, create a multi-colored remaining section + let remainingSection = ""; - if (statusBreakdown && empty > 0) { - // Status colors (matching the statusConfig colors in getStatusWithColor) - const statusColors = { - pending: chalk.yellow, - 'in-progress': chalk.hex('#FFA500'), // Orange - blocked: chalk.red, - review: chalk.magenta - // Deferred and cancelled are treated as part of the completed section - }; + if (statusBreakdown && empty > 0) { + // Status colors (matching the statusConfig colors in getStatusWithColor) + const statusColors = { + pending: chalk.yellow, + "in-progress": chalk.hex("#FFA500"), // Orange + blocked: chalk.red, + review: chalk.magenta, + // Deferred and cancelled are treated as part of the completed section + }; - // Calculate proportions for each status - const totalRemaining = Object.entries(statusBreakdown) - .filter( - ([status]) => - !['deferred', 'cancelled', 'done', 'completed'].includes(status) - ) - .reduce((sum, [_, val]) => sum + val, 0); + // Calculate proportions for each status + const totalRemaining = Object.entries(statusBreakdown) + .filter( + ([status]) => + !["deferred", "cancelled", "done", "completed"].includes(status) + ) + .reduce((sum, [_, val]) => sum + val, 0); - // If no remaining tasks with tracked statuses, just use gray - if (totalRemaining <= 0) { - remainingSection = chalk.gray('░'.repeat(empty)); - } else { - // Track how many characters we've added - let addedChars = 0; + // If no remaining tasks with tracked statuses, just use gray + if (totalRemaining <= 0) { + remainingSection = chalk.gray("░".repeat(empty)); + } else { + // Track how many characters we've added + let addedChars = 0; - // Add each status section proportionally - for (const [status, percentage] of Object.entries(statusBreakdown)) { - // Skip statuses that are considered complete - if (['deferred', 'cancelled', 'done', 'completed'].includes(status)) - continue; + // Add each status section proportionally + for (const [status, percentage] of Object.entries(statusBreakdown)) { + // Skip statuses that are considered complete + if (["deferred", "cancelled", "done", "completed"].includes(status)) + continue; - // Calculate how many characters this status should fill - const statusChars = Math.round((percentage / totalRemaining) * empty); + // Calculate how many characters this status should fill + const statusChars = Math.round((percentage / totalRemaining) * empty); - // Make sure we don't exceed the total length due to rounding - const actualChars = Math.min(statusChars, empty - addedChars); + // Make sure we don't exceed the total length due to rounding + const actualChars = Math.min(statusChars, empty - addedChars); - // Add colored section for this status - const colorFn = statusColors[status] || chalk.gray; - remainingSection += colorFn('░'.repeat(actualChars)); + // Add colored section for this status + const colorFn = statusColors[status] || chalk.gray; + remainingSection += colorFn("░".repeat(actualChars)); - addedChars += actualChars; - } + addedChars += actualChars; + } - // If we have any remaining space due to rounding, fill with gray - if (addedChars < empty) { - remainingSection += chalk.gray('░'.repeat(empty - addedChars)); - } - } - } else { - // Default to gray for the empty section if no breakdown provided - remainingSection = chalk.gray('░'.repeat(empty)); - } + // If we have any remaining space due to rounding, fill with gray + if (addedChars < empty) { + remainingSection += chalk.gray("░".repeat(empty - addedChars)); + } + } + } else { + // Default to gray for the empty section if no breakdown provided + remainingSection = chalk.gray("░".repeat(empty)); + } - // Effective percentage text color should reflect the highest category - const percentTextColor = - percent === 100 - ? chalk.hex('#006400') // Dark green for 100% - : effectivePercent === 100 - ? chalk.gray // Gray for 100% with deferred/cancelled - : completedColor; // Otherwise match the completed color + // Effective percentage text color should reflect the highest category + const percentTextColor = + percent === 100 + ? chalk.hex("#006400") // Dark green for 100% + : effectivePercent === 100 + ? chalk.gray // Gray for 100% with deferred/cancelled + : completedColor; // Otherwise match the completed color - // Build the complete progress bar - return `${completedSection}${deferredCancelledSection}${remainingSection} ${percentTextColor(`${effectivePercent.toFixed(0)}%`)}`; + // Build the complete progress bar + return `${completedSection}${deferredCancelledSection}${remainingSection} ${percentTextColor(`${effectivePercent.toFixed(0)}%`)}`; } /** @@ -227,44 +227,44 @@ function createProgressBar(percent, length = 30, statusBreakdown = null) { * @returns {string} Colored status string */ function getStatusWithColor(status, forTable = false) { - if (!status) { - return chalk.gray('❓ unknown'); - } + if (!status) { + return chalk.gray("❓ unknown"); + } - const statusConfig = { - done: { color: chalk.green, icon: '✅', tableIcon: '✓' }, - completed: { color: chalk.green, icon: '✅', tableIcon: '✓' }, - pending: { color: chalk.yellow, icon: '⏱️', tableIcon: '⏱' }, - 'in-progress': { color: chalk.hex('#FFA500'), icon: '🔄', tableIcon: '►' }, - deferred: { color: chalk.gray, icon: '⏱️', tableIcon: '⏱' }, - blocked: { color: chalk.red, icon: '❌', tableIcon: '✗' }, - review: { color: chalk.magenta, icon: '👀', tableIcon: '👁' }, - cancelled: { color: chalk.gray, icon: '❌', tableIcon: '✗' } - }; + const statusConfig = { + done: { color: chalk.green, icon: "✅", tableIcon: "✓" }, + completed: { color: chalk.green, icon: "✅", tableIcon: "✓" }, + pending: { color: chalk.yellow, icon: "⏱️", tableIcon: "⏱" }, + "in-progress": { color: chalk.hex("#FFA500"), icon: "🔄", tableIcon: "►" }, + deferred: { color: chalk.gray, icon: "⏱️", tableIcon: "⏱" }, + blocked: { color: chalk.red, icon: "❌", tableIcon: "✗" }, + review: { color: chalk.magenta, icon: "👀", tableIcon: "👁" }, + cancelled: { color: chalk.gray, icon: "❌", tableIcon: "✗" }, + }; - const config = statusConfig[status.toLowerCase()] || { - color: chalk.red, - icon: '❌', - tableIcon: '✗' - }; + const config = statusConfig[status.toLowerCase()] || { + color: chalk.red, + icon: "❌", + tableIcon: "✗", + }; - // Use simpler icons for table display to prevent border issues - if (forTable) { - // Use ASCII characters instead of Unicode for completely stable display - const simpleIcons = { - done: '✓', - completed: '✓', - pending: '○', - 'in-progress': '►', - deferred: 'x', - blocked: '!', // Using plain x character for better compatibility - review: '?' // Using circled dot symbol - }; - const simpleIcon = simpleIcons[status.toLowerCase()] || 'x'; - return config.color(`${simpleIcon} ${status}`); - } + // Use simpler icons for table display to prevent border issues + if (forTable) { + // Use ASCII characters instead of Unicode for completely stable display + const simpleIcons = { + done: "✓", + completed: "✓", + pending: "○", + "in-progress": "►", + deferred: "x", + blocked: "!", // Using plain x character for better compatibility + review: "?", // Using circled dot symbol + }; + const simpleIcon = simpleIcons[status.toLowerCase()] || "x"; + return config.color(`${simpleIcon} ${status}`); + } - return config.color(`${config.icon} ${status}`); + return config.color(`${config.icon} ${status}`); } /** @@ -276,467 +276,467 @@ function getStatusWithColor(status, forTable = false) { * @returns {string} Formatted dependencies string */ function formatDependenciesWithStatus( - dependencies, - allTasks, - forConsole = false, - complexityReport = null // Add complexityReport parameter + dependencies, + allTasks, + forConsole = false, + complexityReport = null // Add complexityReport parameter ) { - if ( - !dependencies || - !Array.isArray(dependencies) || - dependencies.length === 0 - ) { - return forConsole ? chalk.gray('None') : 'None'; - } + if ( + !dependencies || + !Array.isArray(dependencies) || + dependencies.length === 0 + ) { + return forConsole ? chalk.gray("None") : "None"; + } - const formattedDeps = dependencies.map((depId) => { - const depIdStr = depId.toString(); // Ensure string format for display + const formattedDeps = dependencies.map((depId) => { + const depIdStr = depId.toString(); // Ensure string format for display - // Check if it's already a fully qualified subtask ID (like "22.1") - if (depIdStr.includes('.')) { - const [parentId, subtaskId] = depIdStr - .split('.') - .map((id) => parseInt(id, 10)); + // Check if it's already a fully qualified subtask ID (like "22.1") + if (depIdStr.includes(".")) { + const [parentId, subtaskId] = depIdStr + .split(".") + .map((id) => parseInt(id, 10)); - // Find the parent task - const parentTask = allTasks.find((t) => t.id === parentId); - if (!parentTask || !parentTask.subtasks) { - return forConsole - ? chalk.red(`${depIdStr} (Not found)`) - : `${depIdStr} (Not found)`; - } + // Find the parent task + const parentTask = allTasks.find((t) => t.id === parentId); + if (!parentTask || !parentTask.subtasks) { + return forConsole + ? chalk.red(`${depIdStr} (Not found)`) + : `${depIdStr} (Not found)`; + } - // Find the subtask - const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); - if (!subtask) { - return forConsole - ? chalk.red(`${depIdStr} (Not found)`) - : `${depIdStr} (Not found)`; - } + // Find the subtask + const subtask = parentTask.subtasks.find((st) => st.id === subtaskId); + if (!subtask) { + return forConsole + ? chalk.red(`${depIdStr} (Not found)`) + : `${depIdStr} (Not found)`; + } - // Format with status - const status = subtask.status || 'pending'; - const isDone = - status.toLowerCase() === 'done' || status.toLowerCase() === 'completed'; - const isInProgress = status.toLowerCase() === 'in-progress'; + // Format with status + const status = subtask.status || "pending"; + const isDone = + status.toLowerCase() === "done" || status.toLowerCase() === "completed"; + const isInProgress = status.toLowerCase() === "in-progress"; - if (forConsole) { - if (isDone) { - return chalk.green.bold(depIdStr); - } else if (isInProgress) { - return chalk.hex('#FFA500').bold(depIdStr); - } else { - return chalk.red.bold(depIdStr); - } - } + if (forConsole) { + if (isDone) { + return chalk.green.bold(depIdStr); + } else if (isInProgress) { + return chalk.hex("#FFA500").bold(depIdStr); + } else { + return chalk.red.bold(depIdStr); + } + } - // For plain text output (task files), return just the ID without any formatting or emoji - return depIdStr; - } + // For plain text output (task files), return just the ID without any formatting or emoji + return depIdStr; + } - // If depId is a number less than 100, it's likely a reference to a subtask ID in the current task - // This case is typically handled elsewhere (in task-specific code) before calling this function + // If depId is a number less than 100, it's likely a reference to a subtask ID in the current task + // This case is typically handled elsewhere (in task-specific code) before calling this function - // For regular task dependencies (not subtasks) - // Convert string depId to number if needed - const numericDepId = - typeof depId === 'string' ? parseInt(depId, 10) : depId; + // For regular task dependencies (not subtasks) + // Convert string depId to number if needed + const numericDepId = + typeof depId === "string" ? parseInt(depId, 10) : depId; - // Look up the task using the numeric ID - const depTaskResult = findTaskById( - allTasks, - numericDepId, - complexityReport - ); - const depTask = depTaskResult.task; // Access the task object from the result + // Look up the task using the numeric ID + const depTaskResult = findTaskById( + allTasks, + numericDepId, + complexityReport + ); + const depTask = depTaskResult.task; // Access the task object from the result - if (!depTask) { - return forConsole - ? chalk.red(`${depIdStr} (Not found)`) - : `${depIdStr} (Not found)`; - } + if (!depTask) { + return forConsole + ? chalk.red(`${depIdStr} (Not found)`) + : `${depIdStr} (Not found)`; + } - // Format with status - const status = depTask.status || 'pending'; - const isDone = - status.toLowerCase() === 'done' || status.toLowerCase() === 'completed'; - const isInProgress = status.toLowerCase() === 'in-progress'; + // Format with status + const status = depTask.status || "pending"; + const isDone = + status.toLowerCase() === "done" || status.toLowerCase() === "completed"; + const isInProgress = status.toLowerCase() === "in-progress"; - if (forConsole) { - if (isDone) { - return chalk.green.bold(depIdStr); - } else if (isInProgress) { - return chalk.yellow.bold(depIdStr); - } else { - return chalk.red.bold(depIdStr); - } - } + if (forConsole) { + if (isDone) { + return chalk.green.bold(depIdStr); + } else if (isInProgress) { + return chalk.yellow.bold(depIdStr); + } else { + return chalk.red.bold(depIdStr); + } + } - // For plain text output (task files), return just the ID without any formatting or emoji - return depIdStr; - }); + // For plain text output (task files), return just the ID without any formatting or emoji + return depIdStr; + }); - return formattedDeps.join(', '); + return formattedDeps.join(", "); } /** * Display a comprehensive help guide */ function displayHelp() { - displayBanner(); + displayBanner(); - // Get terminal width - moved to top of function to make it available throughout - const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect + // Get terminal width - moved to top of function to make it available throughout + const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect - console.log( - boxen(chalk.white.bold('Task Master CLI'), { - padding: 1, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 1 } - }) - ); + 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: 'Project Setup & Configuration', - color: 'blue', - commands: [ - { - name: 'init', - args: '[--name=] [--description=] [-y]', - desc: 'Initialize a new project with Task Master structure' - }, - { - name: 'models', - args: '', - desc: 'View current AI model configuration and available models' - }, - { - name: 'models --setup', - args: '', - desc: 'Run interactive setup to configure AI models' - }, - { - name: 'models --set-main', - args: '', - desc: 'Set the primary model for task generation' - }, - { - name: 'models --set-research', - args: '', - desc: 'Set the model for research operations' - }, - { - name: 'models --set-fallback', - args: '', - desc: 'Set the fallback model (optional)' - } - ] - }, - { - title: 'Task Generation', - color: 'cyan', - commands: [ - { - name: 'parse-prd', - args: '--input= [--num-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=] [--with-subtasks]', - desc: 'List all tasks with their status' - }, - { - name: 'set-status', - args: '--id= --status=', - desc: `Update task status (${TASK_STATUS_OPTIONS.join(', ')})` - }, - { - name: 'update', - args: '--from= --prompt=""', - desc: 'Update multiple tasks based on new requirements' - }, - { - name: 'update-task', - args: '--id= --prompt=""', - desc: 'Update a single specific task with new information' - }, - { - name: 'update-subtask', - args: '--id= --prompt=""', - desc: 'Append additional information to a subtask' - }, - { - name: 'add-task', - args: '--prompt="" [--dependencies=] [--priority=]', - desc: 'Add a new task using AI' - }, - { - name: 'remove-task', - args: '--id= [-y]', - desc: 'Permanently remove a task or subtask' - } - ] - }, - { - title: 'Subtask Management', - color: 'yellow', - commands: [ - { - name: 'add-subtask', - args: '--parent= --title="" [--description="<desc>"]', - desc: 'Add a new subtask to a parent task' - }, - { - name: 'add-subtask', - args: '--parent=<id> --task-id=<id>', - desc: 'Convert an existing task into a subtask' - }, - { - name: 'remove-subtask', - args: '--id=<parentId.subtaskId> [--convert]', - desc: 'Remove a subtask (optionally convert to standalone task)' - }, - { - name: 'clear-subtasks', - args: '--id=<id>', - desc: 'Remove all subtasks from specified tasks' - }, - { - name: 'clear-subtasks --all', - args: '', - desc: 'Remove subtasks from all tasks' - } - ] - }, - { - title: 'Task Analysis & Breakdown', - color: 'magenta', - 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' - } - ] - }, - { - title: 'Task Navigation & Viewing', - color: 'cyan', - 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: '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' - }, - { - name: 'validate-dependencies', - args: '', - desc: 'Identify invalid dependencies without fixing them' - }, - { - name: 'fix-dependencies', - args: '', - desc: 'Fix invalid dependencies automatically' - } - ] - } - ]; + // Command categories + const commandCategories = [ + { + title: "Project Setup & Configuration", + color: "blue", + commands: [ + { + name: "init", + args: "[--name=<name>] [--description=<desc>] [-y]", + desc: "Initialize a new project with Task Master structure", + }, + { + name: "models", + args: "", + desc: "View current AI model configuration and available models", + }, + { + name: "models --setup", + args: "", + desc: "Run interactive setup to configure AI models", + }, + { + name: "models --set-main", + args: "<model_id>", + desc: "Set the primary model for task generation", + }, + { + name: "models --set-research", + args: "<model_id>", + desc: "Set the model for research operations", + }, + { + name: "models --set-fallback", + args: "<model_id>", + desc: "Set the fallback model (optional)", + }, + ], + }, + { + title: "Task Generation", + color: "cyan", + commands: [ + { + name: "parse-prd", + args: "--input=<file.txt> [--num-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 (${TASK_STATUS_OPTIONS.join(", ")})`, + }, + { + name: "update", + args: '--from=<id> --prompt="<context>"', + desc: "Update multiple tasks based on new requirements", + }, + { + name: "update-task", + args: '--id=<id> --prompt="<context>"', + desc: "Update a single specific task with new information", + }, + { + name: "update-subtask", + args: '--id=<parentId.subtaskId> --prompt="<context>"', + desc: "Append additional information to a subtask", + }, + { + name: "add-task", + args: '--prompt="<text>" [--dependencies=<ids>] [--priority=<priority>]', + desc: "Add a new task using AI", + }, + { + name: "remove-task", + args: "--id=<id> [-y]", + desc: "Permanently remove a task or subtask", + }, + ], + }, + { + title: "Subtask Management", + color: "yellow", + commands: [ + { + name: "add-subtask", + args: '--parent=<id> --title="<title>" [--description="<desc>"]', + desc: "Add a new subtask to a parent task", + }, + { + name: "add-subtask", + args: "--parent=<id> --task-id=<id>", + desc: "Convert an existing task into a subtask", + }, + { + name: "remove-subtask", + args: "--id=<parentId.subtaskId> [--convert]", + desc: "Remove a subtask (optionally convert to standalone task)", + }, + { + name: "clear-subtasks", + args: "--id=<id>", + desc: "Remove all subtasks from specified tasks", + }, + { + name: "clear-subtasks --all", + args: "", + desc: "Remove subtasks from all tasks", + }, + ], + }, + { + title: "Task Analysis & Breakdown", + color: "magenta", + 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", + }, + ], + }, + { + title: "Task Navigation & Viewing", + color: "cyan", + 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: "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", + }, + { + 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' - }) - ); + // 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", + }) + ); - // Calculate dynamic column widths - adjust ratios as needed - const nameWidth = Math.max(25, Math.floor(terminalWidth * 0.2)); // 20% of width but min 25 - const argsWidth = Math.max(40, Math.floor(terminalWidth * 0.35)); // 35% of width but min 40 - const descWidth = Math.max(45, Math.floor(terminalWidth * 0.45) - 10); // 45% of width but min 45, minus some buffer + // Calculate dynamic column widths - adjust ratios as needed + const nameWidth = Math.max(25, Math.floor(terminalWidth * 0.2)); // 20% of width but min 25 + const argsWidth = Math.max(40, Math.floor(terminalWidth * 0.35)); // 35% of width but min 40 + const descWidth = Math.max(45, Math.floor(terminalWidth * 0.45) - 10); // 45% of width but min 45, minus some buffer - const commandTable = new Table({ - colWidths: [nameWidth, argsWidth, descWidth], - 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 }, - wordWrap: true - }); + const commandTable = new Table({ + colWidths: [nameWidth, argsWidth, descWidth], + 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 }, + wordWrap: true, + }); - 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('')}` - ]); - }); + 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(''); - }); + console.log(commandTable.toString()); + console.log(""); + }); - // Display configuration section - console.log( - boxen(chalk.cyan.bold('Configuration'), { - padding: { left: 2, right: 2, top: 0, bottom: 0 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'cyan', - borderStyle: 'round' - }) - ); + // Display configuration section + console.log( + boxen(chalk.cyan.bold("Configuration"), { + padding: { left: 2, right: 2, top: 0, bottom: 0 }, + margin: { top: 1, bottom: 0 }, + borderColor: "cyan", + borderStyle: "round", + }) + ); - // Get terminal width if not already defined - const configTerminalWidth = terminalWidth || process.stdout.columns || 100; + // Get terminal width if not already defined + const configTerminalWidth = terminalWidth || process.stdout.columns || 100; - // Calculate dynamic column widths for config table - const configKeyWidth = Math.max(30, Math.floor(configTerminalWidth * 0.25)); - const configDescWidth = Math.max(50, Math.floor(configTerminalWidth * 0.45)); - const configValueWidth = Math.max( - 30, - Math.floor(configTerminalWidth * 0.3) - 10 - ); + // Calculate dynamic column widths for config table + const configKeyWidth = Math.max(30, Math.floor(configTerminalWidth * 0.25)); + const configDescWidth = Math.max(50, Math.floor(configTerminalWidth * 0.45)); + const configValueWidth = Math.max( + 30, + Math.floor(configTerminalWidth * 0.3) - 10 + ); - const configTable = new Table({ - colWidths: [configKeyWidth, configDescWidth, configValueWidth], - 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 }, - wordWrap: true - }); + const configTable = new Table({ + colWidths: [configKeyWidth, configDescWidth, configValueWidth], + 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 }, + wordWrap: true, + }); - configTable.push( - [ - `${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('')}` - ], - [ - `${chalk.yellow('API Keys (.env)')}${chalk.reset('')}`, - `${chalk.white('API keys for AI providers (ANTHROPIC_API_KEY, etc.)')}${chalk.reset('')}`, - `${chalk.dim('Required in .env file')}${chalk.reset('')}` - ], - [ - `${chalk.yellow('MCP Keys (mcp.json)')}${chalk.reset('')}`, - `${chalk.white('API keys for Cursor integration')}${chalk.reset('')}`, - `${chalk.dim('Required in .cursor/')}${chalk.reset('')}` - ] - ); + configTable.push( + [ + `${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("")}`, + ], + [ + `${chalk.yellow("API Keys (.env)")}${chalk.reset("")}`, + `${chalk.white("API keys for AI providers (ANTHROPIC_API_KEY, etc.)")}${chalk.reset("")}`, + `${chalk.dim("Required in .env file")}${chalk.reset("")}`, + ], + [ + `${chalk.yellow("MCP Keys (mcp.json)")}${chalk.reset("")}`, + `${chalk.white("API keys for Cursor integration")}${chalk.reset("")}`, + `${chalk.dim("Required in .cursor/")}${chalk.reset("")}`, + ] + ); - console.log(configTable.toString()); - console.log(''); + console.log(configTable.toString()); + console.log(""); - // Show helpful hints - console.log( - boxen( - chalk.white.bold('Quick Start:') + - '\n\n' + - chalk.cyan('1. Create Project: ') + - chalk.white('task-master init') + - '\n' + - chalk.cyan('2. Setup Models: ') + - chalk.white('task-master models --setup') + - '\n' + - chalk.cyan('3. Parse PRD: ') + - chalk.white('task-master parse-prd --input=<prd-file>') + - '\n' + - chalk.cyan('4. List Tasks: ') + - chalk.white('task-master list') + - '\n' + - chalk.cyan('5. Find Next Task: ') + - chalk.white('task-master next'), - { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 }, - width: Math.min(configTerminalWidth - 10, 100) // Limit width to terminal width minus padding, max 100 - } - ) - ); + // Show helpful hints + console.log( + boxen( + chalk.white.bold("Quick Start:") + + "\n\n" + + chalk.cyan("1. Create Project: ") + + chalk.white("task-master init") + + "\n" + + chalk.cyan("2. Setup Models: ") + + chalk.white("task-master models --setup") + + "\n" + + chalk.cyan("3. Parse PRD: ") + + chalk.white("task-master parse-prd --input=<prd-file>") + + "\n" + + chalk.cyan("4. List Tasks: ") + + chalk.white("task-master list") + + "\n" + + chalk.cyan("5. Find Next Task: ") + + chalk.white("task-master next"), + { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + width: Math.min(configTerminalWidth - 10, 100), // Limit width to terminal width minus padding, max 100 + } + ) + ); } /** @@ -745,9 +745,9 @@ function displayHelp() { * @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}`); + if (score <= 3) return chalk.green(`● ${score}`); + if (score <= 6) return chalk.yellow(`● ${score}`); + return chalk.red(`● ${score}`); } /** @@ -757,9 +757,9 @@ function getComplexityWithColor(score) { * @returns {string} Truncated string */ function truncateString(str, maxLength) { - if (!str) return ''; - if (str.length <= maxLength) return str; - return str.substring(0, maxLength - 3) + '...'; + if (!str) return ""; + if (str.length <= maxLength) return str; + return str.substring(0, maxLength - 3) + "..."; } /** @@ -767,264 +767,264 @@ function truncateString(str, maxLength) { * @param {string} tasksPath - Path to the tasks.json file */ async function displayNextTask(tasksPath, complexityReportPath = null) { - displayBanner(); + displayBanner(); - // Read the tasks file - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found.'); - process.exit(1); - } + // Read the tasks file + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log("error", "No valid tasks found."); + process.exit(1); + } - // Read complexity report once - const complexityReport = readComplexityReport(complexityReportPath); + // Read complexity report once + const complexityReport = readComplexityReport(complexityReportPath); - // Find the next task - const nextTask = findNextTask(data.tasks, complexityReport); + // Find the next task + const nextTask = findNextTask(data.tasks, complexityReport); - 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; - } + 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 } - }) - ); + // 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, Math.min(75, process.stdout.columns - 20 || 60)], - wordWrap: true - }); + // 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, Math.min(75, process.stdout.columns - 20 || 60)], + wordWrap: true, + }); - // Priority with color - const priorityColors = { - high: chalk.red.bold, - medium: chalk.yellow, - low: chalk.gray - }; - const priorityColor = - priorityColors[nextTask.priority || 'medium'] || chalk.white; + // 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, - complexityReport - ) - ], - [ - chalk.cyan.bold('Complexity:'), - nextTask.complexityScore - ? getComplexityWithColor(nextTask.complexityScore) - : chalk.gray('N/A') - ], - [chalk.cyan.bold('Description:'), nextTask.description] - ); + // 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, + complexityReport + ), + ], + [ + chalk.cyan.bold("Complexity:"), + nextTask.complexityScore + ? getComplexityWithColor(nextTask.complexityScore) + : chalk.gray("N/A"), + ], + [chalk.cyan.bold("Description:"), nextTask.description] + ); - console.log(taskTable.toString()); + 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 } - } - ) - ); - } + // 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 }, + } + ) + ); + } - // Determine if the nextTask is a subtask - const isSubtask = !!nextTask.parentId; + // Determine if the nextTask is a subtask + const isSubtask = !!nextTask.parentId; - // Show subtasks if they exist (only for parent tasks) - if (!isSubtask && 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' - }) - ); + // Show subtasks if they exist (only for parent tasks) + if (!isSubtask && 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", + }) + ); - // Calculate available width for the subtask table - const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect + // Calculate available width for the subtask table + const availableWidth = process.stdout.columns - 10 || 100; // Default to 100 if can't detect - // Define percentage-based column widths - const idWidthPct = 8; - const statusWidthPct = 15; - const depsWidthPct = 25; - const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; + // Define percentage-based column widths + const idWidthPct = 8; + const statusWidthPct = 15; + const depsWidthPct = 25; + const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; - // Calculate actual column widths - const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); - const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); - const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); - const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); + // Calculate actual column widths + const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); + const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); + const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); + const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); - // Create a table for subtasks with improved handling - const subtaskTable = new Table({ - head: [ - chalk.magenta.bold('ID'), - chalk.magenta.bold('Status'), - chalk.magenta.bold('Title'), - chalk.magenta.bold('Deps') - ], - colWidths: [idWidth, statusWidth, titleWidth, depsWidth], - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - wordWrap: true - }); + // Create a table for subtasks with improved handling + const subtaskTable = new Table({ + head: [ + chalk.magenta.bold("ID"), + chalk.magenta.bold("Status"), + chalk.magenta.bold("Title"), + chalk.magenta.bold("Deps"), + ], + colWidths: [idWidth, statusWidth, titleWidth, depsWidth], + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + wordWrap: true, + }); - // 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; + // 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) { - const foundSubtask = nextTask.subtasks.find( - (st) => st.id === depId - ); - if (foundSubtask) { - const isDone = - foundSubtask.status === 'done' || - foundSubtask.status === 'completed'; - const isInProgress = foundSubtask.status === 'in-progress'; + // 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) { + const foundSubtask = nextTask.subtasks.find( + (st) => st.id === depId + ); + if (foundSubtask) { + const isDone = + foundSubtask.status === "done" || + foundSubtask.status === "completed"; + const isInProgress = foundSubtask.status === "in-progress"; - // Use consistent color formatting instead of emojis - if (isDone) { - return chalk.green.bold(`${nextTask.id}.${depId}`); - } else if (isInProgress) { - return chalk.hex('#FFA500').bold(`${nextTask.id}.${depId}`); - } else { - return chalk.red.bold(`${nextTask.id}.${depId}`); - } - } - return chalk.red(`${nextTask.id}.${depId} (Not found)`); - } - return depId; - }); + // Use consistent color formatting instead of emojis + if (isDone) { + return chalk.green.bold(`${nextTask.id}.${depId}`); + } else if (isInProgress) { + return chalk.hex("#FFA500").bold(`${nextTask.id}.${depId}`); + } else { + return chalk.red.bold(`${nextTask.id}.${depId}`); + } + } + return chalk.red(`${nextTask.id}.${depId} (Not found)`); + } + return depId; + }); - // Join the formatted dependencies directly instead of passing to formatDependenciesWithStatus again - subtaskDeps = - formattedDeps.length === 1 - ? formattedDeps[0] - : formattedDeps.join(chalk.white(', ')); - } + // Join the formatted dependencies directly instead of passing to formatDependenciesWithStatus again + subtaskDeps = + formattedDeps.length === 1 + ? formattedDeps[0] + : formattedDeps.join(chalk.white(", ")); + } - subtaskTable.push([ - `${nextTask.id}.${st.id}`, - statusColor(st.status || 'pending'), - st.title, - subtaskDeps - ]); - }); + subtaskTable.push([ + `${nextTask.id}.${st.id}`, + statusColor(st.status || "pending"), + st.title, + subtaskDeps, + ]); + }); - console.log(subtaskTable.toString()); - } + console.log(subtaskTable.toString()); + } - // Suggest expanding if no subtasks (only for parent tasks without subtasks) - if (!isSubtask && (!nextTask.subtasks || nextTask.subtasks.length === 0)) { - 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 } - } - ) - ); - } + // Suggest expanding if no subtasks (only for parent tasks without subtasks) + if (!isSubtask && (!nextTask.subtasks || nextTask.subtasks.length === 0)) { + 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 - let suggestedActionsContent = chalk.white.bold('Suggested Actions:') + '\n'; - if (isSubtask) { - // Suggested actions for a subtask - suggestedActionsContent += - `${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` + - `${chalk.cyan('3.')} View parent task: ${chalk.yellow(`task-master show --id=${nextTask.parentId}`)}`; - } else { - // Suggested actions for a parent task - suggestedActionsContent += - `${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`)}` // Example: first subtask - : `${chalk.cyan('3.')} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`); - } + // Show action suggestions + let suggestedActionsContent = chalk.white.bold("Suggested Actions:") + "\n"; + if (isSubtask) { + // Suggested actions for a subtask + suggestedActionsContent += + `${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` + + `${chalk.cyan("3.")} View parent task: ${chalk.yellow(`task-master show --id=${nextTask.parentId}`)}`; + } else { + // Suggested actions for a parent task + suggestedActionsContent += + `${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`)}` // Example: first subtask + : `${chalk.cyan("3.")} Break down into subtasks: ${chalk.yellow(`task-master expand --id=${nextTask.id}`)}`); + } - console.log( - boxen(suggestedActionsContent, { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'green', - borderStyle: 'round', - margin: { top: 1 } - }) - ); + console.log( + boxen(suggestedActionsContent, { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "green", + borderStyle: "round", + margin: { top: 1 }, + }) + ); } /** @@ -1034,460 +1034,460 @@ async function displayNextTask(tasksPath, complexityReportPath = null) { * @param {string} [statusFilter] - Optional status to filter subtasks by */ async function displayTaskById( - tasksPath, - taskId, - complexityReportPath = null, - statusFilter = null + tasksPath, + taskId, + complexityReportPath = null, + statusFilter = null ) { - displayBanner(); + displayBanner(); - // Read the tasks file - const data = readJSON(tasksPath); - if (!data || !data.tasks) { - log('error', 'No valid tasks found.'); - process.exit(1); - } + // Read the tasks file + const data = readJSON(tasksPath); + if (!data || !data.tasks) { + log("error", "No valid tasks found."); + process.exit(1); + } - // Read complexity report once - const complexityReport = readComplexityReport(complexityReportPath); + // Read complexity report once + const complexityReport = readComplexityReport(complexityReportPath); - // Find the task by ID, applying the status filter if provided - // Returns { task, originalSubtaskCount, originalSubtasks } - const { task, originalSubtaskCount, originalSubtasks } = findTaskById( - data.tasks, - taskId, - complexityReport, - statusFilter - ); + // Find the task by ID, applying the status filter if provided + // Returns { task, originalSubtaskCount, originalSubtasks } + const { task, originalSubtaskCount, originalSubtasks } = findTaskById( + data.tasks, + taskId, + complexityReport, + statusFilter + ); - 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; - } + 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 (This logic remains the same) - 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 } - } - ) - ); + // Handle subtask display specially (This logic remains the same) + 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 }, + } + ) + ); - const subtaskTable = new Table({ - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], - wordWrap: true - }); - subtaskTable.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', true) - ], - [ - chalk.cyan.bold('Complexity:'), - task.complexityScore - ? getComplexityWithColor(task.complexityScore) - : chalk.gray('N/A') - ], - [ - chalk.cyan.bold('Description:'), - task.description || 'No description provided.' - ] - ); - console.log(subtaskTable.toString()); + const subtaskTable = new Table({ + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + colWidths: [15, Math.min(75, process.stdout.columns - 20 || 60)], + wordWrap: true, + }); + subtaskTable.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", true), + ], + [ + chalk.cyan.bold("Complexity:"), + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray("N/A"), + ], + [ + chalk.cyan.bold("Description:"), + task.description || "No description provided.", + ] + ); + console.log(subtaskTable.toString()); - 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 } - } - ) - ); - } + 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 }, + } + ) + ); + } - 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; // Exit after displaying subtask details - } + 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; // Exit after displaying subtask details + } - // --- Display Regular Task Details --- - 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 } - }) - ); + // --- Display Regular Task Details --- + 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 }, + }) + ); - 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, Math.min(75, process.stdout.columns - 20 || 60)], - wordWrap: true - }); - const priorityColors = { - high: chalk.red.bold, - medium: chalk.yellow, - low: chalk.gray - }; - const priorityColor = - priorityColors[task.priority || 'medium'] || chalk.white; - taskTable.push( - [chalk.cyan.bold('ID:'), task.id.toString()], - [chalk.cyan.bold('Title:'), task.title], - [ - chalk.cyan.bold('Status:'), - getStatusWithColor(task.status || 'pending', true) - ], - [chalk.cyan.bold('Priority:'), priorityColor(task.priority || 'medium')], - [ - chalk.cyan.bold('Dependencies:'), - formatDependenciesWithStatus( - task.dependencies, - data.tasks, - true, - complexityReport - ) - ], - [ - chalk.cyan.bold('Complexity:'), - task.complexityScore - ? getComplexityWithColor(task.complexityScore) - : chalk.gray('N/A') - ], - [chalk.cyan.bold('Description:'), task.description] - ); - console.log(taskTable.toString()); + 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, Math.min(75, process.stdout.columns - 20 || 60)], + wordWrap: true, + }); + const priorityColors = { + high: chalk.red.bold, + medium: chalk.yellow, + low: chalk.gray, + }; + const priorityColor = + priorityColors[task.priority || "medium"] || chalk.white; + taskTable.push( + [chalk.cyan.bold("ID:"), task.id.toString()], + [chalk.cyan.bold("Title:"), task.title], + [ + chalk.cyan.bold("Status:"), + getStatusWithColor(task.status || "pending", true), + ], + [chalk.cyan.bold("Priority:"), priorityColor(task.priority || "medium")], + [ + chalk.cyan.bold("Dependencies:"), + formatDependenciesWithStatus( + task.dependencies, + data.tasks, + true, + complexityReport + ), + ], + [ + chalk.cyan.bold("Complexity:"), + task.complexityScore + ? getComplexityWithColor(task.complexityScore) + : chalk.gray("N/A"), + ], + [chalk.cyan.bold("Description:"), task.description] + ); + console.log(taskTable.toString()); - 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 } - } - ) - ); - } - 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 } - }) - ); - } + 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 }, + } + ) + ); + } + 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 }, + }) + ); + } - // --- Subtask Table Display (uses filtered list: task.subtasks) --- - 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' - }) - ); + // --- Subtask Table Display (uses filtered list: task.subtasks) --- + 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", + }) + ); - const availableWidth = process.stdout.columns - 10 || 100; - const idWidthPct = 10; - const statusWidthPct = 15; - const depsWidthPct = 25; - const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; - const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); - const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); - const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); - const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); + const availableWidth = process.stdout.columns - 10 || 100; + const idWidthPct = 10; + const statusWidthPct = 15; + const depsWidthPct = 25; + const titleWidthPct = 100 - idWidthPct - statusWidthPct - depsWidthPct; + const idWidth = Math.floor(availableWidth * (idWidthPct / 100)); + const statusWidth = Math.floor(availableWidth * (statusWidthPct / 100)); + const depsWidth = Math.floor(availableWidth * (depsWidthPct / 100)); + const titleWidth = Math.floor(availableWidth * (titleWidthPct / 100)); - const subtaskTable = new Table({ - head: [ - chalk.magenta.bold('ID'), - chalk.magenta.bold('Status'), - chalk.magenta.bold('Title'), - chalk.magenta.bold('Deps') - ], - colWidths: [idWidth, statusWidth, titleWidth, depsWidth], - style: { - head: [], - border: [], - 'padding-top': 0, - 'padding-bottom': 0, - compact: true - }, - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' }, - wordWrap: true - }); + const subtaskTable = new Table({ + head: [ + chalk.magenta.bold("ID"), + chalk.magenta.bold("Status"), + chalk.magenta.bold("Title"), + chalk.magenta.bold("Deps"), + ], + colWidths: [idWidth, statusWidth, titleWidth, depsWidth], + style: { + head: [], + border: [], + "padding-top": 0, + "padding-bottom": 0, + compact: true, + }, + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + wordWrap: true, + }); - // Populate table with the potentially filtered subtasks - task.subtasks.forEach((st) => { - const statusColorMap = { - done: chalk.green, - completed: chalk.green, - pending: chalk.yellow, - 'in-progress': chalk.blue - }; - const statusColor = statusColorMap[st.status || 'pending'] || chalk.white; - let subtaskDeps = 'None'; - if (st.dependencies && st.dependencies.length > 0) { - const formattedDeps = st.dependencies.map((depId) => { - // Use the original, unfiltered list for dependency status lookup - const sourceListForDeps = originalSubtasks || task.subtasks; - const foundDepSubtask = - typeof depId === 'number' && depId < 100 - ? sourceListForDeps.find((sub) => sub.id === depId) - : null; + // Populate table with the potentially filtered subtasks + task.subtasks.forEach((st) => { + const statusColorMap = { + done: chalk.green, + completed: chalk.green, + pending: chalk.yellow, + "in-progress": chalk.blue, + }; + const statusColor = statusColorMap[st.status || "pending"] || chalk.white; + let subtaskDeps = "None"; + if (st.dependencies && st.dependencies.length > 0) { + const formattedDeps = st.dependencies.map((depId) => { + // Use the original, unfiltered list for dependency status lookup + const sourceListForDeps = originalSubtasks || task.subtasks; + const foundDepSubtask = + typeof depId === "number" && depId < 100 + ? sourceListForDeps.find((sub) => sub.id === depId) + : null; - if (foundDepSubtask) { - const isDone = - foundDepSubtask.status === 'done' || - foundDepSubtask.status === 'completed'; - const isInProgress = foundDepSubtask.status === 'in-progress'; - const color = isDone - ? chalk.green.bold - : isInProgress - ? chalk.hex('#FFA500').bold - : chalk.red.bold; - return color(`${task.id}.${depId}`); - } else if (typeof depId === 'number' && depId < 100) { - return chalk.red(`${task.id}.${depId} (Not found)`); - } - return depId; // Assume it's a top-level task ID if not a number < 100 - }); - subtaskDeps = - formattedDeps.length === 1 - ? formattedDeps[0] - : formattedDeps.join(chalk.white(', ')); - } - subtaskTable.push([ - `${task.id}.${st.id}`, - statusColor(st.status || 'pending'), - st.title, - subtaskDeps - ]); - }); - console.log(subtaskTable.toString()); + if (foundDepSubtask) { + const isDone = + foundDepSubtask.status === "done" || + foundDepSubtask.status === "completed"; + const isInProgress = foundDepSubtask.status === "in-progress"; + const color = isDone + ? chalk.green.bold + : isInProgress + ? chalk.hex("#FFA500").bold + : chalk.red.bold; + return color(`${task.id}.${depId}`); + } else if (typeof depId === "number" && depId < 100) { + return chalk.red(`${task.id}.${depId} (Not found)`); + } + return depId; // Assume it's a top-level task ID if not a number < 100 + }); + subtaskDeps = + formattedDeps.length === 1 + ? formattedDeps[0] + : formattedDeps.join(chalk.white(", ")); + } + subtaskTable.push([ + `${task.id}.${st.id}`, + statusColor(st.status || "pending"), + st.title, + subtaskDeps, + ]); + }); + console.log(subtaskTable.toString()); - // Display filter summary line *immediately after the table* if a filter was applied - if (statusFilter && originalSubtaskCount !== null) { - console.log( - chalk.cyan( - ` Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.` - ) - ); - // Add a newline for spacing before the progress bar if the filter line was shown - console.log(); - } - // --- Conditional Messages for No Subtasks Shown --- - } else if (statusFilter && originalSubtaskCount === 0) { - // Case where filter applied, but the parent task had 0 subtasks originally - console.log( - boxen( - chalk.yellow( - `No subtasks found matching status: ${statusFilter} (Task has no subtasks)` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'yellow', - borderStyle: 'round' - } - ) - ); - } else if ( - statusFilter && - originalSubtaskCount > 0 && - task.subtasks.length === 0 - ) { - // Case where filter applied, original subtasks existed, but none matched - console.log( - boxen( - chalk.yellow( - `No subtasks found matching status: ${statusFilter} (out of ${originalSubtaskCount} total)` - ), - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - margin: { top: 1, bottom: 0 }, - borderColor: 'yellow', - borderStyle: 'round' - } - ) - ); - } else if ( - !statusFilter && - (!originalSubtasks || originalSubtasks.length === 0) - ) { - // Case where NO filter applied AND the task genuinely has no subtasks - // Use the authoritative originalSubtasks if it exists (from filtering), else check task.subtasks - const actualSubtasks = originalSubtasks || task.subtasks; - if (!actualSubtasks || actualSubtasks.length === 0) { - 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 } - } - ) - ); - } - } + // Display filter summary line *immediately after the table* if a filter was applied + if (statusFilter && originalSubtaskCount !== null) { + console.log( + chalk.cyan( + ` Filtered by status: ${chalk.bold(statusFilter)}. Showing ${chalk.bold(task.subtasks.length)} of ${chalk.bold(originalSubtaskCount)} subtasks.` + ) + ); + // Add a newline for spacing before the progress bar if the filter line was shown + console.log(); + } + // --- Conditional Messages for No Subtasks Shown --- + } else if (statusFilter && originalSubtaskCount === 0) { + // Case where filter applied, but the parent task had 0 subtasks originally + console.log( + boxen( + chalk.yellow( + `No subtasks found matching status: ${statusFilter} (Task has no subtasks)` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: "yellow", + borderStyle: "round", + } + ) + ); + } else if ( + statusFilter && + originalSubtaskCount > 0 && + task.subtasks.length === 0 + ) { + // Case where filter applied, original subtasks existed, but none matched + console.log( + boxen( + chalk.yellow( + `No subtasks found matching status: ${statusFilter} (out of ${originalSubtaskCount} total)` + ), + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + margin: { top: 1, bottom: 0 }, + borderColor: "yellow", + borderStyle: "round", + } + ) + ); + } else if ( + !statusFilter && + (!originalSubtasks || originalSubtasks.length === 0) + ) { + // Case where NO filter applied AND the task genuinely has no subtasks + // Use the authoritative originalSubtasks if it exists (from filtering), else check task.subtasks + const actualSubtasks = originalSubtasks || task.subtasks; + if (!actualSubtasks || actualSubtasks.length === 0) { + 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 }, + } + ) + ); + } + } - // --- Subtask Progress Bar Display (uses originalSubtasks or task.subtasks) --- - // Determine the list to use for progress calculation (always the original if available and filtering happened) - const subtasksForProgress = originalSubtasks || task.subtasks; // Use original if filtering occurred, else the potentially empty task.subtasks + // --- Subtask Progress Bar Display (uses originalSubtasks or task.subtasks) --- + // Determine the list to use for progress calculation (always the original if available and filtering happened) + const subtasksForProgress = originalSubtasks || task.subtasks; // Use original if filtering occurred, else the potentially empty task.subtasks - // Only show progress if there are actually subtasks - if (subtasksForProgress && subtasksForProgress.length > 0) { - const totalSubtasks = subtasksForProgress.length; - const completedSubtasks = subtasksForProgress.filter( - (st) => st.status === 'done' || st.status === 'completed' - ).length; + // Only show progress if there are actually subtasks + if (subtasksForProgress && subtasksForProgress.length > 0) { + const totalSubtasks = subtasksForProgress.length; + const completedSubtasks = subtasksForProgress.filter( + (st) => st.status === "done" || st.status === "completed" + ).length; - // Count other statuses from the original/complete list - const inProgressSubtasks = subtasksForProgress.filter( - (st) => st.status === 'in-progress' - ).length; - const pendingSubtasks = subtasksForProgress.filter( - (st) => st.status === 'pending' - ).length; - const blockedSubtasks = subtasksForProgress.filter( - (st) => st.status === 'blocked' - ).length; - const deferredSubtasks = subtasksForProgress.filter( - (st) => st.status === 'deferred' - ).length; - const cancelledSubtasks = subtasksForProgress.filter( - (st) => st.status === 'cancelled' - ).length; + // Count other statuses from the original/complete list + const inProgressSubtasks = subtasksForProgress.filter( + (st) => st.status === "in-progress" + ).length; + const pendingSubtasks = subtasksForProgress.filter( + (st) => st.status === "pending" + ).length; + const blockedSubtasks = subtasksForProgress.filter( + (st) => st.status === "blocked" + ).length; + const deferredSubtasks = subtasksForProgress.filter( + (st) => st.status === "deferred" + ).length; + const cancelledSubtasks = subtasksForProgress.filter( + (st) => st.status === "cancelled" + ).length; - const statusBreakdown = { - // Calculate breakdown based on the complete list - 'in-progress': (inProgressSubtasks / totalSubtasks) * 100, - pending: (pendingSubtasks / totalSubtasks) * 100, - blocked: (blockedSubtasks / totalSubtasks) * 100, - deferred: (deferredSubtasks / totalSubtasks) * 100, - cancelled: (cancelledSubtasks / totalSubtasks) * 100 - }; - const completionPercentage = (completedSubtasks / totalSubtasks) * 100; + const statusBreakdown = { + // Calculate breakdown based on the complete list + "in-progress": (inProgressSubtasks / totalSubtasks) * 100, + pending: (pendingSubtasks / totalSubtasks) * 100, + blocked: (blockedSubtasks / totalSubtasks) * 100, + deferred: (deferredSubtasks / totalSubtasks) * 100, + cancelled: (cancelledSubtasks / totalSubtasks) * 100, + }; + const completionPercentage = (completedSubtasks / totalSubtasks) * 100; - const availableWidth = process.stdout.columns || 80; - const boxPadding = 2; - const boxBorders = 2; - const percentTextLength = 5; - const progressBarLength = Math.max( - 20, - Math.min( - 60, - availableWidth - boxPadding - boxBorders - percentTextLength - 35 - ) - ); + const availableWidth = process.stdout.columns || 80; + const boxPadding = 2; + const boxBorders = 2; + const percentTextLength = 5; + const progressBarLength = Math.max( + 20, + Math.min( + 60, + availableWidth - boxPadding - boxBorders - percentTextLength - 35 + ) + ); - const statusCounts = - `${chalk.green('✓ Done:')} ${completedSubtasks} ${chalk.hex('#FFA500')('► In Progress:')} ${inProgressSubtasks} ${chalk.yellow('○ Pending:')} ${pendingSubtasks}\n` + - `${chalk.red('! Blocked:')} ${blockedSubtasks} ${chalk.gray('⏱ Deferred:')} ${deferredSubtasks} ${chalk.gray('✗ Cancelled:')} ${cancelledSubtasks}`; + const statusCounts = + `${chalk.green("✓ Done:")} ${completedSubtasks} ${chalk.hex("#FFA500")("► In Progress:")} ${inProgressSubtasks} ${chalk.yellow("○ Pending:")} ${pendingSubtasks}\n` + + `${chalk.red("! Blocked:")} ${blockedSubtasks} ${chalk.gray("⏱ Deferred:")} ${deferredSubtasks} ${chalk.gray("✗ Cancelled:")} ${cancelledSubtasks}`; - console.log( - boxen( - chalk.white.bold('Subtask Progress:') + - '\n\n' + - `${chalk.cyan('Completed:')} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` + - `${statusCounts}\n` + - `${chalk.cyan('Progress:')} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`, - { - padding: { top: 0, bottom: 0, left: 1, right: 1 }, - borderColor: 'blue', - borderStyle: 'round', - margin: { top: 1, bottom: 0 }, - width: Math.min(availableWidth - 10, 100), - textAlignment: 'left' - } - ) - ); - } + console.log( + boxen( + chalk.white.bold("Subtask Progress:") + + "\n\n" + + `${chalk.cyan("Completed:")} ${completedSubtasks}/${totalSubtasks} (${completionPercentage.toFixed(1)}%)\n` + + `${statusCounts}\n` + + `${chalk.cyan("Progress:")} ${createProgressBar(completionPercentage, progressBarLength, statusBreakdown)}`, + { + padding: { top: 0, bottom: 0, left: 1, right: 1 }, + borderColor: "blue", + borderStyle: "round", + margin: { top: 1, bottom: 0 }, + width: Math.min(availableWidth - 10, 100), + textAlignment: "left", + } + ) + ); + } - // --- Suggested Actions --- - 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` + - // Determine action 3 based on whether subtasks *exist* (use the source list for progress) - (subtasksForProgress && subtasksForProgress.length > 0 - ? `${chalk.cyan('3.')} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` // Example uses .1 - : `${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 } - } - ) - ); + // --- Suggested Actions --- + 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` + + // Determine action 3 based on whether subtasks *exist* (use the source list for progress) + (subtasksForProgress && subtasksForProgress.length > 0 + ? `${chalk.cyan("3.")} Update subtask status: ${chalk.yellow(`task-master set-status --id=${task.id}.1 --status=done`)}` // Example uses .1 + : `${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 }, + } + ) + ); } /** @@ -1495,251 +1495,251 @@ async function displayTaskById( * @param {string} reportPath - Path to the complexity report file */ async function displayComplexityReport(reportPath) { - displayBanner(); + 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 } - } - ) - ); + // 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 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(); + 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...')); - 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; - } + 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: tasksPath - }); - // Read the newly generated report - return displayComplexityReport(reportPath); - } else { - console.log(chalk.yellow('Report generation cancelled.')); - return; - } - } + await analyzeTaskComplexity({ + output: reportPath, + research: false, // Default to no research for speed + file: tasksPath, + }); + // 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; - } + // 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 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] - }); + // 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' - ] - ); + 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()); + console.log(metaTable.toString()); - // Sort tasks by complexity score (highest first) - const sortedTasks = [...report.complexityAnalysis].sort( - (a, b) => b.complexityScore - a.complexityScore - ); + // 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 - ); + // 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]++; - }); + // 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 - ); + 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 } - } - ) - ); + 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 }, + } + ) + ); - // Get terminal width - const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect + // Get terminal width + const terminalWidth = process.stdout.columns || 100; // Default to 100 if can't detect - // Calculate dynamic column widths - const idWidth = 12; - const titleWidth = Math.floor(terminalWidth * 0.25); // 25% of width - const scoreWidth = 8; - const subtasksWidth = 8; - // Command column gets the remaining space (minus some buffer for borders) - const commandWidth = - terminalWidth - idWidth - titleWidth - scoreWidth - subtasksWidth - 10; + // Calculate dynamic column widths + const idWidth = 12; + const titleWidth = Math.floor(terminalWidth * 0.25); // 25% of width + const scoreWidth = 8; + const subtasksWidth = 8; + // Command column gets the remaining space (minus some buffer for borders) + const commandWidth = + terminalWidth - idWidth - titleWidth - scoreWidth - subtasksWidth - 10; - // Create table with new column widths and word wrapping - 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: [idWidth, titleWidth, scoreWidth, subtasksWidth, commandWidth], - style: { head: [], border: [] }, - wordWrap: true, - wrapOnWordBoundary: true - }); + // Create table with new column widths and word wrapping + 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: [idWidth, titleWidth, scoreWidth, subtasksWidth, commandWidth], + style: { head: [], border: [] }, + wordWrap: true, + wrapOnWordBoundary: true, + }); - // When adding rows, don't truncate the expansion command - tasksNeedingExpansion.forEach((task) => { - const expansionCommand = `task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}${task.expansionPrompt ? ` --prompt="${task.expansionPrompt}"` : ''}`; + // When adding rows, don't truncate the expansion command + tasksNeedingExpansion.forEach((task) => { + const expansionCommand = `task-master expand --id=${task.taskId} --num=${task.recommendedSubtasks}${task.expansionPrompt ? ` --prompt="${task.expansionPrompt}"` : ""}`; - complexTable.push([ - task.taskId, - truncate(task.taskTitle, titleWidth - 3), // Still truncate title for readability - getComplexityWithColor(task.complexityScore), - task.recommendedSubtasks, - chalk.cyan(expansionCommand) // Don't truncate - allow wrapping - ]); - }); + complexTable.push([ + task.taskId, + truncate(task.taskTitle, titleWidth - 3), // Still truncate title for readability + getComplexityWithColor(task.complexityScore), + task.recommendedSubtasks, + chalk.cyan(expansionCommand), // Don't truncate - allow wrapping + ]); + }); - console.log(complexTable.toString()); + 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' - }) - ); + // 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: [] } - }); + 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) - ]); - }); + simpleTasks.forEach((task) => { + simpleTable.push([ + task.taskId, + truncate(task.taskTitle, 37), + getComplexityWithColor(task.complexityScore), + truncate(task.reasoning, 47), + ]); + }); - console.log(simpleTable.toString()); - } + 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 } - } - ) - ); + // 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 }, + } + ) + ); } /** @@ -1748,21 +1748,21 @@ async function displayComplexityReport(reportPath) { * @returns {string} Generated prompt */ function generateComplexityAnalysisPrompt(tasksData) { - const defaultSubtasks = getDefaultSubtasks(null); // Use the getter - return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown: + const defaultSubtasks = getDefaultSubtasks(null); // Use the getter + return `Analyze the complexity of the following tasks and provide recommendations for subtask breakdown: ${tasksData.tasks - .map( - (task) => ` + .map( + (task) => ` Task ID: ${task.id} Title: ${task.title} Description: ${task.description} Details: ${task.details} Dependencies: ${JSON.stringify(task.dependencies || [])} -Priority: ${task.priority || 'medium'} +Priority: ${task.priority || "medium"} ` - ) - .join('\n---\n')} + ) + .join("\n---\n")} Analyze each task and return a JSON array with the following structure for each task: [ @@ -1787,39 +1787,39 @@ IMPORTANT: Make sure to include an analysis for EVERY task listed above, with th * @returns {Promise<boolean>} - Promise resolving to true if user confirms, false otherwise */ async function confirmTaskOverwrite(tasksPath) { - console.log( - boxen( - chalk.yellow( - "It looks like you've already generated tasks for this project.\n" - ) + - chalk.yellow( - 'Executing this command will overwrite any existing tasks.' - ), - { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + console.log( + boxen( + chalk.yellow( + "It looks like you've already generated tasks for this project.\n" + ) + + chalk.yellow( + "Executing this command will overwrite any existing tasks." + ), + { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); - // Use dynamic import to get the readline module - const readline = await import('readline'); - const rl = readline.createInterface({ - input: process.stdin, - output: process.stdout - }); + // Use dynamic import to get the readline module + const readline = await import("readline"); + const rl = readline.createInterface({ + input: process.stdin, + output: process.stdout, + }); - const answer = await new Promise((resolve) => { - rl.question( - chalk.cyan('Are you sure you wish to continue? (y/N): '), - resolve - ); - }); - rl.close(); + const answer = await new Promise((resolve) => { + rl.question( + chalk.cyan("Are you sure you wish to continue? (y/N): "), + resolve + ); + }); + rl.close(); - return answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes'; + return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes"; } /** @@ -1827,75 +1827,75 @@ async function confirmTaskOverwrite(tasksPath) { * @param {Array<{provider: string, cli: boolean, mcp: boolean}>} statusReport - The report generated by getApiKeyStatusReport. */ function displayApiKeyStatus(statusReport) { - if (!statusReport || statusReport.length === 0) { - console.log(chalk.yellow('No API key status information available.')); - return; - } + if (!statusReport || statusReport.length === 0) { + console.log(chalk.yellow("No API key status information available.")); + return; + } - const table = new Table({ - head: [ - chalk.cyan('Provider'), - chalk.cyan('CLI Key (.env)'), - chalk.cyan('MCP Key (mcp.json)') - ], - colWidths: [15, 20, 25], - chars: { mid: '', 'left-mid': '', 'mid-mid': '', 'right-mid': '' } - }); + const table = new Table({ + head: [ + chalk.cyan("Provider"), + chalk.cyan("CLI Key (.env)"), + chalk.cyan("MCP Key (mcp.json)"), + ], + colWidths: [15, 20, 25], + chars: { mid: "", "left-mid": "", "mid-mid": "", "right-mid": "" }, + }); - statusReport.forEach(({ provider, cli, mcp }) => { - const cliStatus = cli ? chalk.green('✅ Found') : chalk.red('❌ Missing'); - const mcpStatus = mcp ? chalk.green('✅ Found') : chalk.red('❌ Missing'); - // Capitalize provider name for display - const providerName = provider.charAt(0).toUpperCase() + provider.slice(1); - table.push([providerName, cliStatus, mcpStatus]); - }); + statusReport.forEach(({ provider, cli, mcp }) => { + const cliStatus = cli ? chalk.green("✅ Found") : chalk.red("❌ Missing"); + const mcpStatus = mcp ? chalk.green("✅ Found") : chalk.red("❌ Missing"); + // Capitalize provider name for display + const providerName = provider.charAt(0).toUpperCase() + provider.slice(1); + table.push([providerName, cliStatus, mcpStatus]); + }); - console.log(chalk.bold('\n🔑 API Key Status:')); - console.log(table.toString()); - console.log( - chalk.gray( - ` Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in ${TASKMASTER_CONFIG_FILE}.` - ) - ); + console.log(chalk.bold("\n🔑 API Key Status:")); + console.log(table.toString()); + console.log( + chalk.gray( + ` Note: Some providers (e.g., Azure, Ollama) may require additional endpoint configuration in ${TASKMASTER_CONFIG_FILE}.` + ) + ); } // --- Formatting Helpers (Potentially move some to utils.js if reusable) --- const formatSweScoreWithTertileStars = (score, allModels) => { - // ... (Implementation from previous version or refine) ... - if (score === null || score === undefined || score <= 0) return 'N/A'; - const formattedPercentage = `${(score * 100).toFixed(1)}%`; + // ... (Implementation from previous version or refine) ... + if (score === null || score === undefined || score <= 0) return "N/A"; + const formattedPercentage = `${(score * 100).toFixed(1)}%`; - const validScores = allModels - .map((m) => m.sweScore) - .filter((s) => s !== null && s !== undefined && s > 0); - const sortedScores = [...validScores].sort((a, b) => b - a); - const n = sortedScores.length; - let stars = chalk.gray('☆☆☆'); + const validScores = allModels + .map((m) => m.sweScore) + .filter((s) => s !== null && s !== undefined && s > 0); + const sortedScores = [...validScores].sort((a, b) => b - a); + const n = sortedScores.length; + let stars = chalk.gray("☆☆☆"); - if (n > 0) { - const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1); - const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1); - if (score >= sortedScores[topThirdIndex]) stars = chalk.yellow('★★★'); - else if (score >= sortedScores[midThirdIndex]) - stars = chalk.yellow('★★') + chalk.gray('☆'); - else stars = chalk.yellow('★') + chalk.gray('☆☆'); - } - return `${formattedPercentage} ${stars}`; + if (n > 0) { + const topThirdIndex = Math.max(0, Math.floor(n / 3) - 1); + const midThirdIndex = Math.max(0, Math.floor((2 * n) / 3) - 1); + if (score >= sortedScores[topThirdIndex]) stars = chalk.yellow("★★★"); + else if (score >= sortedScores[midThirdIndex]) + stars = chalk.yellow("★★") + chalk.gray("☆"); + else stars = chalk.yellow("★") + chalk.gray("☆☆"); + } + return `${formattedPercentage} ${stars}`; }; const formatCost = (costObj) => { - // ... (Implementation from previous version or refine) ... - if (!costObj) return 'N/A'; - if (costObj.input === 0 && costObj.output === 0) { - return chalk.green('Free'); - } - const formatSingleCost = (costValue) => { - if (costValue === null || costValue === undefined) return 'N/A'; - const isInteger = Number.isInteger(costValue); - return `$${costValue.toFixed(isInteger ? 0 : 2)}`; - }; - return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(costObj.output)} out`; + // ... (Implementation from previous version or refine) ... + if (!costObj) return "N/A"; + if (costObj.input === 0 && costObj.output === 0) { + return chalk.green("Free"); + } + const formatSingleCost = (costValue) => { + if (costValue === null || costValue === undefined) return "N/A"; + const isInteger = Number.isInteger(costValue); + return `$${costValue.toFixed(isInteger ? 0 : 2)}`; + }; + return `${formatSingleCost(costObj.input)} in, ${formatSingleCost(costObj.output)} out`; }; // --- Display Functions --- @@ -1906,63 +1906,63 @@ const formatCost = (costObj) => { * @param {AvailableModel[]} allAvailableModels - Needed for SWE score tertiles. */ function displayModelConfiguration(configData, allAvailableModels = []) { - console.log(chalk.cyan.bold('\nActive Model Configuration:')); - const active = configData.activeModels; - const activeTable = new Table({ - head: [ - 'Role', - 'Provider', - 'Model ID', - 'SWE Score', - 'Cost ($/1M tkns)' - // 'API Key Status' // Removed, handled by separate displayApiKeyStatus - ].map((h) => chalk.cyan.bold(h)), - colWidths: [10, 14, 30, 18, 20 /*, 28 */], // Adjusted widths - style: { head: ['cyan', 'bold'] } - }); + console.log(chalk.cyan.bold("\nActive Model Configuration:")); + const active = configData.activeModels; + const activeTable = new Table({ + head: [ + "Role", + "Provider", + "Model ID", + "SWE Score", + "Cost ($/1M tkns)", + // 'API Key Status' // Removed, handled by separate displayApiKeyStatus + ].map((h) => chalk.cyan.bold(h)), + colWidths: [10, 14, 30, 18, 20 /*, 28 */], // Adjusted widths + style: { head: ["cyan", "bold"] }, + }); - activeTable.push([ - chalk.white('Main'), - active.main.provider, - active.main.modelId, - formatSweScoreWithTertileStars(active.main.sweScore, allAvailableModels), - formatCost(active.main.cost) - // getCombinedStatus(active.main.keyStatus) // Removed - ]); - activeTable.push([ - chalk.white('Research'), - active.research.provider, - active.research.modelId, - formatSweScoreWithTertileStars( - active.research.sweScore, - allAvailableModels - ), - formatCost(active.research.cost) - // getCombinedStatus(active.research.keyStatus) // Removed - ]); - if (active.fallback && active.fallback.provider && active.fallback.modelId) { - activeTable.push([ - chalk.white('Fallback'), - active.fallback.provider, - active.fallback.modelId, - formatSweScoreWithTertileStars( - active.fallback.sweScore, - allAvailableModels - ), - formatCost(active.fallback.cost) - // getCombinedStatus(active.fallback.keyStatus) // Removed - ]); - } else { - activeTable.push([ - chalk.white('Fallback'), - chalk.gray('-'), - chalk.gray('(Not Set)'), - chalk.gray('-'), - chalk.gray('-') - // chalk.gray('-') // Removed - ]); - } - console.log(activeTable.toString()); + activeTable.push([ + chalk.white("Main"), + active.main.provider, + active.main.modelId, + formatSweScoreWithTertileStars(active.main.sweScore, allAvailableModels), + formatCost(active.main.cost), + // getCombinedStatus(active.main.keyStatus) // Removed + ]); + activeTable.push([ + chalk.white("Research"), + active.research.provider, + active.research.modelId, + formatSweScoreWithTertileStars( + active.research.sweScore, + allAvailableModels + ), + formatCost(active.research.cost), + // getCombinedStatus(active.research.keyStatus) // Removed + ]); + if (active.fallback && active.fallback.provider && active.fallback.modelId) { + activeTable.push([ + chalk.white("Fallback"), + active.fallback.provider, + active.fallback.modelId, + formatSweScoreWithTertileStars( + active.fallback.sweScore, + allAvailableModels + ), + formatCost(active.fallback.cost), + // getCombinedStatus(active.fallback.keyStatus) // Removed + ]); + } else { + activeTable.push([ + chalk.white("Fallback"), + chalk.gray("-"), + chalk.gray("(Not Set)"), + chalk.gray("-"), + chalk.gray("-"), + // chalk.gray('-') // Removed + ]); + } + console.log(activeTable.toString()); } /** @@ -1970,64 +1970,64 @@ function displayModelConfiguration(configData, allAvailableModels = []) { * @param {AvailableModel[]} availableModels - List of available models. */ function displayAvailableModels(availableModels) { - if (!availableModels || availableModels.length === 0) { - console.log( - chalk.gray('\n(No other models available or all are configured)') - ); - return; - } + if (!availableModels || availableModels.length === 0) { + console.log( + chalk.gray("\n(No other models available or all are configured)") + ); + return; + } - console.log(chalk.cyan.bold('\nOther Available Models:')); - const availableTable = new Table({ - head: ['Provider', 'Model ID', 'SWE Score', 'Cost ($/1M tkns)'].map((h) => - chalk.cyan.bold(h) - ), - colWidths: [15, 40, 18, 25], - style: { head: ['cyan', 'bold'] } - }); + console.log(chalk.cyan.bold("\nOther Available Models:")); + const availableTable = new Table({ + head: ["Provider", "Model ID", "SWE Score", "Cost ($/1M tkns)"].map((h) => + chalk.cyan.bold(h) + ), + colWidths: [15, 40, 18, 25], + style: { head: ["cyan", "bold"] }, + }); - availableModels.forEach((model) => { - availableTable.push([ - model.provider, - model.modelId, - formatSweScoreWithTertileStars(model.sweScore, availableModels), // Pass itself for comparison - formatCost(model.cost) - ]); - }); - console.log(availableTable.toString()); + availableModels.forEach((model) => { + availableTable.push([ + model.provider, + model.modelId, + formatSweScoreWithTertileStars(model.sweScore, availableModels), // Pass itself for comparison + formatCost(model.cost), + ]); + }); + console.log(availableTable.toString()); - // --- Suggested Actions Section (moved here from models command) --- - console.log( - boxen( - chalk.white.bold('Next Steps:') + - '\n' + - chalk.cyan( - `1. Set main model: ${chalk.yellow('task-master models --set-main <model_id>')}` - ) + - '\n' + - chalk.cyan( - `2. Set research model: ${chalk.yellow('task-master models --set-research <model_id>')}` - ) + - '\n' + - chalk.cyan( - `3. Set fallback model: ${chalk.yellow('task-master models --set-fallback <model_id>')}` - ) + - '\n' + - chalk.cyan( - `4. Run interactive setup: ${chalk.yellow('task-master models --setup')}` - ) + - '\n' + - chalk.cyan( - `5. Use custom ollama/openrouter models: ${chalk.yellow('task-master models --openrouter|ollama --set-main|research|fallback <model_id>')}` - ), - { - padding: 1, - borderColor: 'yellow', - borderStyle: 'round', - margin: { top: 1 } - } - ) - ); + // --- Suggested Actions Section (moved here from models command) --- + console.log( + boxen( + chalk.white.bold("Next Steps:") + + "\n" + + chalk.cyan( + `1. Set main model: ${chalk.yellow("task-master models --set-main <model_id>")}` + ) + + "\n" + + chalk.cyan( + `2. Set research model: ${chalk.yellow("task-master models --set-research <model_id>")}` + ) + + "\n" + + chalk.cyan( + `3. Set fallback model: ${chalk.yellow("task-master models --set-fallback <model_id>")}` + ) + + "\n" + + chalk.cyan( + `4. Run interactive setup: ${chalk.yellow("task-master models --setup")}` + ) + + "\n" + + chalk.cyan( + `5. Use custom ollama/openrouter models: ${chalk.yellow("task-master models --openrouter|ollama --set-main|research|fallback <model_id>")}` + ), + { + padding: 1, + borderColor: "yellow", + borderStyle: "round", + margin: { top: 1 }, + } + ) + ); } /** @@ -2035,63 +2035,63 @@ function displayAvailableModels(availableModels) { * @param {object} telemetryData - The telemetry data object. * @param {string} outputType - 'cli' or 'mcp' (though typically only called for 'cli'). */ -function displayAiUsageSummary(telemetryData, outputType = 'cli') { - if ( - (outputType !== 'cli' && outputType !== 'text') || - !telemetryData || - isSilentMode() - ) { - return; // Only display for CLI and if data exists and not in silent mode - } +function displayAiUsageSummary(telemetryData, outputType = "cli") { + if ( + (outputType !== "cli" && outputType !== "text") || + !telemetryData || + isSilentMode() + ) { + return; // Only display for CLI and if data exists and not in silent mode + } - const { - modelUsed, - providerName, - inputTokens, - outputTokens, - totalTokens, - totalCost, - commandName - } = telemetryData; + const { + modelUsed, + providerName, + inputTokens, + outputTokens, + totalTokens, + totalCost, + commandName, + } = telemetryData; - let summary = chalk.bold.blue('AI Usage Summary:') + '\n'; - summary += chalk.gray(` Command: ${commandName}\n`); - summary += chalk.gray(` Provider: ${providerName}\n`); - summary += chalk.gray(` Model: ${modelUsed}\n`); - summary += chalk.gray( - ` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n` - ); - summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`); + let summary = chalk.bold.blue("AI Usage Summary:") + "\n"; + summary += chalk.gray(` Command: ${commandName}\n`); + summary += chalk.gray(` Provider: ${providerName}\n`); + summary += chalk.gray(` Model: ${modelUsed}\n`); + summary += chalk.gray( + ` Tokens: ${totalTokens} (Input: ${inputTokens}, Output: ${outputTokens})\n` + ); + summary += chalk.gray(` Est. Cost: $${totalCost.toFixed(6)}`); - console.log( - boxen(summary, { - padding: 1, - margin: { top: 1 }, - borderColor: 'blue', - borderStyle: 'round', - title: '💡 Telemetry', - titleAlignment: 'center' - }) - ); + console.log( + boxen(summary, { + padding: 1, + margin: { top: 1 }, + borderColor: "blue", + borderStyle: "round", + title: "💡 Telemetry", + titleAlignment: "center", + }) + ); } // Export UI functions export { - displayBanner, - startLoadingIndicator, - stopLoadingIndicator, - createProgressBar, - getStatusWithColor, - formatDependenciesWithStatus, - displayHelp, - getComplexityWithColor, - displayNextTask, - displayTaskById, - displayComplexityReport, - generateComplexityAnalysisPrompt, - confirmTaskOverwrite, - displayApiKeyStatus, - displayModelConfiguration, - displayAvailableModels, - displayAiUsageSummary + displayBanner, + startLoadingIndicator, + stopLoadingIndicator, + createProgressBar, + getStatusWithColor, + formatDependenciesWithStatus, + displayHelp, + getComplexityWithColor, + displayNextTask, + displayTaskById, + displayComplexityReport, + generateComplexityAnalysisPrompt, + confirmTaskOverwrite, + displayApiKeyStatus, + displayModelConfiguration, + displayAvailableModels, + displayAiUsageSummary, }; diff --git a/tests/unit/ui.test.js b/tests/unit/ui.test.js index 8be90e1d..dbab8ea8 100644 --- a/tests/unit/ui.test.js +++ b/tests/unit/ui.test.js @@ -2,245 +2,245 @@ * UI module tests */ -import { jest } from '@jest/globals'; +import { jest } from "@jest/globals"; import { - getStatusWithColor, - formatDependenciesWithStatus, - createProgressBar, - getComplexityWithColor -} from '../../scripts/modules/ui.js'; -import { sampleTasks } from '../fixtures/sample-tasks.js'; + getStatusWithColor, + formatDependenciesWithStatus, + createProgressBar, + getComplexityWithColor, +} from "../../scripts/modules/ui.js"; +import { sampleTasks } from "../fixtures/sample-tasks.js"; // Mock dependencies -jest.mock('chalk', () => { - const origChalkFn = (text) => text; - const chalk = origChalkFn; - chalk.green = (text) => text; // Return text as-is for status functions - chalk.yellow = (text) => text; - chalk.red = (text) => text; - chalk.cyan = (text) => text; - chalk.blue = (text) => text; - chalk.gray = (text) => text; - chalk.white = (text) => text; - chalk.bold = (text) => text; - chalk.dim = (text) => text; +jest.mock("chalk", () => { + const origChalkFn = (text) => text; + const chalk = origChalkFn; + chalk.green = (text) => text; // Return text as-is for status functions + chalk.yellow = (text) => text; + chalk.red = (text) => text; + chalk.cyan = (text) => text; + chalk.blue = (text) => text; + chalk.gray = (text) => text; + chalk.white = (text) => text; + chalk.bold = (text) => text; + chalk.dim = (text) => text; - // Add hex and other methods - chalk.hex = () => origChalkFn; - chalk.rgb = () => origChalkFn; + // Add hex and other methods + chalk.hex = () => origChalkFn; + chalk.rgb = () => origChalkFn; - return chalk; + return chalk; }); -jest.mock('figlet', () => ({ - textSync: jest.fn(() => 'Task Master Banner') +jest.mock("figlet", () => ({ + textSync: jest.fn(() => "Task Master Banner"), })); -jest.mock('boxen', () => jest.fn((text) => `[boxed: ${text}]`)); +jest.mock("boxen", () => jest.fn((text) => `[boxed: ${text}]`)); -jest.mock('ora', () => - jest.fn(() => ({ - start: jest.fn(), - succeed: jest.fn(), - fail: jest.fn(), - stop: jest.fn() - })) +jest.mock("ora", () => + jest.fn(() => ({ + start: jest.fn(), + succeed: jest.fn(), + fail: jest.fn(), + stop: jest.fn(), + })) ); -jest.mock('cli-table3', () => - jest.fn().mockImplementation(() => ({ - push: jest.fn(), - toString: jest.fn(() => 'Table Content') - })) +jest.mock("cli-table3", () => + jest.fn().mockImplementation(() => ({ + push: jest.fn(), + toString: jest.fn(() => "Table Content"), + })) ); -jest.mock('gradient-string', () => jest.fn(() => jest.fn((text) => text))); +jest.mock("gradient-string", () => jest.fn(() => jest.fn((text) => text))); -jest.mock('../../scripts/modules/utils.js', () => ({ - CONFIG: { - projectName: 'Test Project', - projectVersion: '1.0.0' - }, - log: jest.fn(), - findTaskById: jest.fn(), - readJSON: jest.fn(), - readComplexityReport: jest.fn(), - truncate: jest.fn((text) => text) +jest.mock("../../scripts/modules/utils.js", () => ({ + CONFIG: { + projectName: "Test Project", + projectVersion: "1.0.0", + }, + log: jest.fn(), + findTaskById: jest.fn(), + readJSON: jest.fn(), + readComplexityReport: jest.fn(), + truncate: jest.fn((text) => text), })); -jest.mock('../../scripts/modules/task-manager.js', () => ({ - findNextTask: jest.fn(), - analyzeTaskComplexity: jest.fn() +jest.mock("../../scripts/modules/task-manager.js", () => ({ + findNextTask: jest.fn(), + analyzeTaskComplexity: jest.fn(), })); -describe('UI Module', () => { - beforeEach(() => { - jest.clearAllMocks(); - }); +describe("UI Module", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); - describe('getStatusWithColor function', () => { - test('should return done status with emoji for console output', () => { - const result = getStatusWithColor('done'); - expect(result).toMatch(/done/); - expect(result).toContain('✅'); - }); + describe("getStatusWithColor function", () => { + test("should return done status with emoji for console output", () => { + const result = getStatusWithColor("done"); + expect(result).toMatch(/done/); + expect(result).toContain("✅"); + }); - test('should return pending status with emoji for console output', () => { - const result = getStatusWithColor('pending'); - expect(result).toMatch(/pending/); - expect(result).toContain('⏱️'); - }); + test("should return pending status with emoji for console output", () => { + const result = getStatusWithColor("pending"); + expect(result).toMatch(/pending/); + expect(result).toContain("⏱️"); + }); - test('should return deferred status with emoji for console output', () => { - const result = getStatusWithColor('deferred'); - expect(result).toMatch(/deferred/); - expect(result).toContain('⏱️'); - }); + test("should return deferred status with emoji for console output", () => { + const result = getStatusWithColor("deferred"); + expect(result).toMatch(/deferred/); + expect(result).toContain("⏱️"); + }); - test('should return in-progress status with emoji for console output', () => { - const result = getStatusWithColor('in-progress'); - expect(result).toMatch(/in-progress/); - expect(result).toContain('🔄'); - }); + test("should return in-progress status with emoji for console output", () => { + const result = getStatusWithColor("in-progress"); + expect(result).toMatch(/in-progress/); + expect(result).toContain("🔄"); + }); - test('should return unknown status with emoji for console output', () => { - const result = getStatusWithColor('unknown'); - expect(result).toMatch(/unknown/); - expect(result).toContain('❌'); - }); + test("should return unknown status with emoji for console output", () => { + const result = getStatusWithColor("unknown"); + expect(result).toMatch(/unknown/); + expect(result).toContain("❌"); + }); - test('should use simple icons when forTable is true', () => { - const doneResult = getStatusWithColor('done', true); - expect(doneResult).toMatch(/done/); - expect(doneResult).toContain('✓'); + test("should use simple icons when forTable is true", () => { + const doneResult = getStatusWithColor("done", true); + expect(doneResult).toMatch(/done/); + expect(doneResult).toContain("✓"); - const pendingResult = getStatusWithColor('pending', true); - expect(pendingResult).toMatch(/pending/); - expect(pendingResult).toContain('○'); + const pendingResult = getStatusWithColor("pending", true); + expect(pendingResult).toMatch(/pending/); + expect(pendingResult).toContain("○"); - const inProgressResult = getStatusWithColor('in-progress', true); - expect(inProgressResult).toMatch(/in-progress/); - expect(inProgressResult).toContain('►'); + const inProgressResult = getStatusWithColor("in-progress", true); + expect(inProgressResult).toMatch(/in-progress/); + expect(inProgressResult).toContain("►"); - const deferredResult = getStatusWithColor('deferred', true); - expect(deferredResult).toMatch(/deferred/); - expect(deferredResult).toContain('x'); - }); - }); + const deferredResult = getStatusWithColor("deferred", true); + expect(deferredResult).toMatch(/deferred/); + expect(deferredResult).toContain("x"); + }); + }); - describe('formatDependenciesWithStatus function', () => { - test('should format dependencies as plain IDs when forConsole is false (default)', () => { - const dependencies = [1, 2, 3]; - const allTasks = [ - { id: 1, status: 'done' }, - { id: 2, status: 'pending' }, - { id: 3, status: 'deferred' } - ]; + describe("formatDependenciesWithStatus function", () => { + test("should format dependencies as plain IDs when forConsole is false (default)", () => { + const dependencies = [1, 2, 3]; + const allTasks = [ + { id: 1, status: "done" }, + { id: 2, status: "pending" }, + { id: 3, status: "deferred" }, + ]; - const result = formatDependenciesWithStatus(dependencies, allTasks); + const result = formatDependenciesWithStatus(dependencies, allTasks); - // With recent changes, we expect just plain IDs when forConsole is false - expect(result).toBe('1, 2, 3'); - }); + // With recent changes, we expect just plain IDs when forConsole is false + expect(result).toBe("1, 2, 3"); + }); - test('should format dependencies with status indicators when forConsole is true', () => { - const dependencies = [1, 2, 3]; - const allTasks = [ - { id: 1, status: 'done' }, - { id: 2, status: 'pending' }, - { id: 3, status: 'deferred' } - ]; + test("should format dependencies with status indicators when forConsole is true", () => { + const dependencies = [1, 2, 3]; + const allTasks = [ + { id: 1, status: "done" }, + { id: 2, status: "pending" }, + { id: 3, status: "deferred" }, + ]; - const result = formatDependenciesWithStatus(dependencies, allTasks, true); + const result = formatDependenciesWithStatus(dependencies, allTasks, true); - // We can't test for exact color formatting due to our chalk mocks - // Instead, test that the result contains all the expected IDs - expect(result).toContain('1'); - expect(result).toContain('2'); - expect(result).toContain('3'); + // We can't test for exact color formatting due to our chalk mocks + // Instead, test that the result contains all the expected IDs + expect(result).toContain("1"); + expect(result).toContain("2"); + expect(result).toContain("3"); - // Test that it's a comma-separated list - expect(result.split(', ').length).toBe(3); - }); + // Test that it's a comma-separated list + expect(result.split(", ").length).toBe(3); + }); - test('should return "None" for empty dependencies', () => { - const result = formatDependenciesWithStatus([], []); - expect(result).toBe('None'); - }); + test('should return "None" for empty dependencies', () => { + const result = formatDependenciesWithStatus([], []); + expect(result).toBe("None"); + }); - test('should handle missing tasks in the task list', () => { - const dependencies = [1, 999]; - const allTasks = [{ id: 1, status: 'done' }]; + test("should handle missing tasks in the task list", () => { + const dependencies = [1, 999]; + const allTasks = [{ id: 1, status: "done" }]; - const result = formatDependenciesWithStatus(dependencies, allTasks); - expect(result).toBe('1, 999 (Not found)'); - }); - }); + const result = formatDependenciesWithStatus(dependencies, allTasks); + expect(result).toBe("1, 999 (Not found)"); + }); + }); - describe('createProgressBar function', () => { - test('should create a progress bar with the correct percentage', () => { - const result = createProgressBar(50, 10, { - pending: 20, - 'in-progress': 15, - blocked: 5 - }); - expect(result).toContain('50%'); - }); + describe("createProgressBar function", () => { + test("should create a progress bar with the correct percentage", () => { + const result = createProgressBar(50, 10, { + pending: 20, + "in-progress": 15, + blocked: 5, + }); + expect(result).toContain("50%"); + }); - test('should handle 0% progress', () => { - const result = createProgressBar(0, 10); - expect(result).toContain('0%'); - }); + test("should handle 0% progress", () => { + const result = createProgressBar(0, 10); + expect(result).toContain("0%"); + }); - test('should handle 100% progress', () => { - const result = createProgressBar(100, 10); - expect(result).toContain('100%'); - }); + test("should handle 100% progress", () => { + const result = createProgressBar(100, 10); + expect(result).toContain("100%"); + }); - test('should handle invalid percentages by clamping', () => { - const result1 = createProgressBar(0, 10); - expect(result1).toContain('0%'); + test("should handle invalid percentages by clamping", () => { + const result1 = createProgressBar(0, 10); + expect(result1).toContain("0%"); - const result2 = createProgressBar(100, 10); - expect(result2).toContain('100%'); - }); + const result2 = createProgressBar(100, 10); + expect(result2).toContain("100%"); + }); - test('should support status breakdown in the progress bar', () => { - const result = createProgressBar(30, 10, { - pending: 30, - 'in-progress': 20, - blocked: 10, - deferred: 5, - cancelled: 5 - }); + test("should support status breakdown in the progress bar", () => { + const result = createProgressBar(30, 10, { + pending: 30, + "in-progress": 20, + blocked: 10, + deferred: 5, + cancelled: 5, + }); - expect(result).toContain('40%'); - }); - }); + expect(result).toContain("40%"); + }); + }); - describe('getComplexityWithColor function', () => { - test('should return high complexity in red', () => { - const result = getComplexityWithColor(8); - expect(result).toMatch(/8/); - expect(result).toContain('🔴'); - }); + describe("getComplexityWithColor function", () => { + test("should return high complexity in red", () => { + const result = getComplexityWithColor(8); + expect(result).toMatch(/8/); + expect(result).toContain("●"); + }); - test('should return medium complexity in yellow', () => { - const result = getComplexityWithColor(5); - expect(result).toMatch(/5/); - expect(result).toContain('🟡'); - }); + test("should return medium complexity in yellow", () => { + const result = getComplexityWithColor(5); + expect(result).toMatch(/5/); + expect(result).toContain("●"); + }); - test('should return low complexity in green', () => { - const result = getComplexityWithColor(3); - expect(result).toMatch(/3/); - expect(result).toContain('🟢'); - }); + test("should return low complexity in green", () => { + const result = getComplexityWithColor(3); + expect(result).toMatch(/3/); + expect(result).toContain("●"); + }); - test('should handle non-numeric inputs', () => { - const result = getComplexityWithColor('high'); - expect(result).toMatch(/high/); - expect(result).toContain('🔴'); - }); - }); + test("should handle non-numeric inputs", () => { + const result = getComplexityWithColor("high"); + expect(result).toMatch(/high/); + expect(result).toContain("●"); + }); + }); });