2025-06-07 20:30:51 -04:00
import chalk from 'chalk' ;
import boxen from 'boxen' ;
import Table from 'cli-table3' ;
2025-04-21 17:48:30 -04:00
2025-05-16 23:24:25 +02:00
import {
2025-06-07 20:30:51 -04:00
log ,
readJSON ,
truncate ,
readComplexityReport ,
addComplexityToTask
} from '../utils.js' ;
import findNextTask from './find-next-task.js' ;
2025-04-21 17:48:30 -04:00
import {
2025-06-07 20:30:51 -04:00
displayBanner ,
getStatusWithColor ,
formatDependenciesWithStatus ,
getComplexityWithColor ,
createProgressBar
} from '../ui.js' ;
2025-04-21 17:48:30 -04:00
/ * *
* List all tasks
* @ param { string } tasksPath - Path to the tasks . json file
* @ param { string } statusFilter - Filter by status
2025-05-16 23:24:25 +02:00
* @ param { string } reportPath - Path to the complexity report
2025-04-21 17:48:30 -04:00
* @ param { boolean } withSubtasks - Whether to show subtasks
* @ param { string } outputFormat - Output format ( text or json )
* @ returns { Object } - Task list result for json format
* /
function listTasks (
2025-06-07 20:30:51 -04:00
tasksPath ,
statusFilter ,
reportPath = null ,
withSubtasks = false ,
outputFormat = 'text'
2025-04-21 17:48:30 -04:00
) {
2025-06-07 20:30:51 -04:00
try {
const data = readJSON ( tasksPath ) ; // Reads the whole tasks.json
if ( ! data || ! data . tasks ) {
throw new Error ( ` No valid tasks found in ${ tasksPath } ` ) ;
}
// Add complexity scores to tasks if report exists
const complexityReport = readComplexityReport ( reportPath ) ;
// Apply complexity scores to tasks
if ( complexityReport && complexityReport . complexityAnalysis ) {
data . tasks . forEach ( ( task ) => addComplexityToTask ( task , complexityReport ) ) ;
}
// Filter tasks by status if specified
const filteredTasks =
statusFilter && statusFilter . toLowerCase ( ) !== 'all' // <-- Added check for 'all'
? data . tasks . filter (
( task ) =>
task . status &&
task . status . toLowerCase ( ) === statusFilter . toLowerCase ( )
)
: data . tasks ; // Default to all tasks if no filter or filter is 'all'
// Calculate completion statistics
const totalTasks = data . tasks . length ;
const completedTasks = data . tasks . filter (
( task ) => task . status === 'done' || task . status === 'completed'
) . length ;
const completionPercentage =
totalTasks > 0 ? ( completedTasks / totalTasks ) * 100 : 0 ;
// Count statuses for tasks
const doneCount = completedTasks ;
const inProgressCount = data . tasks . filter (
( task ) => task . status === 'in-progress'
) . length ;
const pendingCount = data . tasks . filter (
( task ) => task . status === 'pending'
) . length ;
const blockedCount = data . tasks . filter (
( task ) => task . status === 'blocked'
) . length ;
const deferredCount = data . tasks . filter (
( task ) => task . status === 'deferred'
) . length ;
const cancelledCount = data . tasks . filter (
( task ) => task . status === 'cancelled'
) . length ;
// Count subtasks and their statuses
let totalSubtasks = 0 ;
let completedSubtasks = 0 ;
let inProgressSubtasks = 0 ;
let pendingSubtasks = 0 ;
let blockedSubtasks = 0 ;
let deferredSubtasks = 0 ;
let cancelledSubtasks = 0 ;
data . tasks . forEach ( ( task ) => {
if ( task . subtasks && task . subtasks . length > 0 ) {
totalSubtasks += task . subtasks . length ;
completedSubtasks += task . subtasks . filter (
( st ) => st . status === 'done' || st . status === 'completed'
) . length ;
inProgressSubtasks += task . subtasks . filter (
( st ) => st . status === 'in-progress'
) . length ;
pendingSubtasks += task . subtasks . filter (
( st ) => st . status === 'pending'
) . length ;
blockedSubtasks += task . subtasks . filter (
( st ) => st . status === 'blocked'
) . length ;
deferredSubtasks += task . subtasks . filter (
( st ) => st . status === 'deferred'
) . length ;
cancelledSubtasks += task . subtasks . filter (
( st ) => st . status === 'cancelled'
) . length ;
}
} ) ;
const subtaskCompletionPercentage =
totalSubtasks > 0 ? ( completedSubtasks / totalSubtasks ) * 100 : 0 ;
2025-06-07 22:07:35 -04:00
// Calculate dependency statistics (moved up to be available for all output formats)
const completedTaskIds = new Set (
data . tasks
. filter ( ( t ) => t . status === 'done' || t . status === 'completed' )
. map ( ( t ) => t . id )
) ;
const tasksWithNoDeps = data . tasks . filter (
( t ) =>
t . status !== 'done' &&
t . status !== 'completed' &&
( ! t . dependencies || t . dependencies . length === 0 )
) . length ;
const tasksWithAllDepsSatisfied = data . tasks . filter (
( t ) =>
t . status !== 'done' &&
t . status !== 'completed' &&
t . dependencies &&
t . dependencies . length > 0 &&
t . dependencies . every ( ( depId ) => completedTaskIds . has ( depId ) )
) . length ;
const tasksWithUnsatisfiedDeps = data . tasks . filter (
( t ) =>
t . status !== 'done' &&
t . status !== 'completed' &&
t . dependencies &&
t . dependencies . length > 0 &&
! t . dependencies . every ( ( depId ) => completedTaskIds . has ( depId ) )
) . length ;
// Calculate total tasks ready to work on (no deps + satisfied deps)
const tasksReadyToWork = tasksWithNoDeps + tasksWithAllDepsSatisfied ;
// Calculate most depended-on tasks
const dependencyCount = { } ;
data . tasks . forEach ( ( task ) => {
if ( task . dependencies && task . dependencies . length > 0 ) {
task . dependencies . forEach ( ( depId ) => {
dependencyCount [ depId ] = ( dependencyCount [ depId ] || 0 ) + 1 ;
} ) ;
}
} ) ;
// Find the most depended-on task
let mostDependedOnTaskId = null ;
let maxDependents = 0 ;
for ( const [ taskId , count ] of Object . entries ( dependencyCount ) ) {
if ( count > maxDependents ) {
maxDependents = count ;
mostDependedOnTaskId = parseInt ( taskId ) ;
}
}
// Get the most depended-on task
const mostDependedOnTask =
mostDependedOnTaskId !== null
? data . tasks . find ( ( t ) => t . id === mostDependedOnTaskId )
: null ;
// Calculate average dependencies per task
const totalDependencies = data . tasks . reduce (
( sum , task ) => sum + ( task . dependencies ? task . dependencies . length : 0 ) ,
0
) ;
const avgDependenciesPerTask = totalDependencies / data . tasks . length ;
// Find next task to work on, passing the complexity report
const nextItem = findNextTask ( data . tasks , complexityReport ) ;
2025-06-07 20:30:51 -04:00
// For JSON output, return structured data
if ( outputFormat === 'json' ) {
// *** Modification: Remove 'details' field for JSON output ***
const tasksWithoutDetails = filteredTasks . map ( ( task ) => {
// <-- USES filteredTasks!
// Omit 'details' from the parent task
const { details , ... taskRest } = task ;
// If subtasks exist, omit 'details' from them too
if ( taskRest . subtasks && Array . isArray ( taskRest . subtasks ) ) {
taskRest . subtasks = taskRest . subtasks . map ( ( subtask ) => {
const { details : subtaskDetails , ... subtaskRest } = subtask ;
return subtaskRest ;
} ) ;
}
return taskRest ;
} ) ;
// *** End of Modification ***
return {
tasks : tasksWithoutDetails , // <--- THIS IS THE ARRAY BEING RETURNED
filter : statusFilter || 'all' , // Return the actual filter used
stats : {
total : totalTasks ,
completed : doneCount ,
inProgress : inProgressCount ,
pending : pendingCount ,
blocked : blockedCount ,
deferred : deferredCount ,
cancelled : cancelledCount ,
completionPercentage ,
subtasks : {
total : totalSubtasks ,
completed : completedSubtasks ,
inProgress : inProgressSubtasks ,
pending : pendingSubtasks ,
blocked : blockedSubtasks ,
deferred : deferredSubtasks ,
cancelled : cancelledSubtasks ,
completionPercentage : subtaskCompletionPercentage
}
}
} ;
}
2025-06-07 22:07:35 -04:00
// For markdown-readme output, return formatted markdown
if ( outputFormat === 'markdown-readme' ) {
return generateMarkdownOutput ( data , filteredTasks , {
totalTasks ,
completedTasks ,
completionPercentage ,
doneCount ,
inProgressCount ,
pendingCount ,
blockedCount ,
deferredCount ,
cancelledCount ,
totalSubtasks ,
completedSubtasks ,
subtaskCompletionPercentage ,
inProgressSubtasks ,
pendingSubtasks ,
blockedSubtasks ,
deferredSubtasks ,
cancelledSubtasks ,
tasksWithNoDeps ,
tasksReadyToWork ,
tasksWithUnsatisfiedDeps ,
mostDependedOnTask ,
mostDependedOnTaskId ,
maxDependents ,
avgDependenciesPerTask ,
complexityReport ,
withSubtasks ,
nextItem
} ) ;
}
2025-06-07 20:30:51 -04:00
// ... existing code for text output ...
// Calculate status breakdowns as percentages of total
const taskStatusBreakdown = {
'in-progress' : totalTasks > 0 ? ( inProgressCount / totalTasks ) * 100 : 0 ,
pending : totalTasks > 0 ? ( pendingCount / totalTasks ) * 100 : 0 ,
blocked : totalTasks > 0 ? ( blockedCount / totalTasks ) * 100 : 0 ,
deferred : totalTasks > 0 ? ( deferredCount / totalTasks ) * 100 : 0 ,
cancelled : totalTasks > 0 ? ( cancelledCount / totalTasks ) * 100 : 0
} ;
const subtaskStatusBreakdown = {
'in-progress' :
totalSubtasks > 0 ? ( inProgressSubtasks / totalSubtasks ) * 100 : 0 ,
pending : totalSubtasks > 0 ? ( pendingSubtasks / totalSubtasks ) * 100 : 0 ,
blocked : totalSubtasks > 0 ? ( blockedSubtasks / totalSubtasks ) * 100 : 0 ,
deferred :
totalSubtasks > 0 ? ( deferredSubtasks / totalSubtasks ) * 100 : 0 ,
cancelled :
totalSubtasks > 0 ? ( cancelledSubtasks / totalSubtasks ) * 100 : 0
} ;
// Create progress bars with status breakdowns
const taskProgressBar = createProgressBar (
completionPercentage ,
30 ,
taskStatusBreakdown
) ;
const subtaskProgressBar = createProgressBar (
subtaskCompletionPercentage ,
30 ,
subtaskStatusBreakdown
) ;
// Get terminal width - more reliable method
let terminalWidth ;
try {
// Try to get the actual terminal columns
terminalWidth = process . stdout . columns ;
} catch ( e ) {
// Fallback if columns cannot be determined
log ( 'debug' , 'Could not determine terminal width, using default' ) ;
}
// Ensure we have a reasonable default if detection fails
terminalWidth = terminalWidth || 80 ;
// Ensure terminal width is at least a minimum value to prevent layout issues
terminalWidth = Math . max ( terminalWidth , 80 ) ;
// Create dashboard content
const projectDashboardContent =
chalk . white . bold ( 'Project Dashboard' ) +
'\n' +
` Tasks Progress: ${ chalk . greenBright ( taskProgressBar ) } ${ completionPercentage . toFixed ( 0 ) } % \n ` +
` Done: ${ chalk . green ( doneCount ) } In Progress: ${ chalk . blue ( inProgressCount ) } Pending: ${ chalk . yellow ( pendingCount ) } Blocked: ${ chalk . red ( blockedCount ) } Deferred: ${ chalk . gray ( deferredCount ) } Cancelled: ${ chalk . gray ( cancelledCount ) } \n \n ` +
` Subtasks Progress: ${ chalk . cyan ( subtaskProgressBar ) } ${ subtaskCompletionPercentage . toFixed ( 0 ) } % \n ` +
` Completed: ${ chalk . green ( completedSubtasks ) } / ${ totalSubtasks } In Progress: ${ chalk . blue ( inProgressSubtasks ) } Pending: ${ chalk . yellow ( pendingSubtasks ) } Blocked: ${ chalk . red ( blockedSubtasks ) } Deferred: ${ chalk . gray ( deferredSubtasks ) } Cancelled: ${ chalk . gray ( cancelledSubtasks ) } \n \n ` +
chalk . cyan . bold ( 'Priority Breakdown:' ) +
'\n' +
` ${ chalk . red ( '•' ) } ${ chalk . white ( 'High priority:' ) } ${ data . tasks . filter ( ( t ) => t . priority === 'high' ) . length } \n ` +
` ${ chalk . yellow ( '•' ) } ${ chalk . white ( 'Medium priority:' ) } ${ data . tasks . filter ( ( t ) => t . priority === 'medium' ) . length } \n ` +
` ${ chalk . green ( '•' ) } ${ chalk . white ( 'Low priority:' ) } ${ data . tasks . filter ( ( t ) => t . priority === 'low' ) . length } ` ;
const dependencyDashboardContent =
chalk . white . bold ( 'Dependency Status & Next Task' ) +
'\n' +
chalk . cyan . bold ( 'Dependency Metrics:' ) +
'\n' +
` ${ chalk . green ( '•' ) } ${ chalk . white ( 'Tasks with no dependencies:' ) } ${ tasksWithNoDeps } \n ` +
` ${ chalk . green ( '•' ) } ${ chalk . white ( 'Tasks ready to work on:' ) } ${ tasksReadyToWork } \n ` +
` ${ chalk . yellow ( '•' ) } ${ chalk . white ( 'Tasks blocked by dependencies:' ) } ${ tasksWithUnsatisfiedDeps } \n ` +
` ${ chalk . magenta ( '•' ) } ${ chalk . white ( 'Most depended-on task:' ) } ${ mostDependedOnTask ? chalk . cyan ( ` # ${ mostDependedOnTaskId } ( ${ maxDependents } dependents) ` ) : chalk . gray ( 'None' ) } \n ` +
` ${ chalk . blue ( '•' ) } ${ chalk . white ( 'Avg dependencies per task:' ) } ${ avgDependenciesPerTask . toFixed ( 1 ) } \n \n ` +
chalk . cyan . bold ( 'Next Task to Work On:' ) +
'\n' +
` ID: ${ chalk . cyan ( nextItem ? nextItem . id : 'N/A' ) } - ${ nextItem ? chalk . white . bold ( truncate ( nextItem . title , 40 ) ) : chalk . yellow ( 'No task available' ) }
2025-05-16 23:24:25 +02:00
` +
2025-06-07 20:30:51 -04:00
` Priority: ${ nextItem ? chalk . white ( nextItem . priority || 'medium' ) : '' } Dependencies: ${ nextItem ? formatDependenciesWithStatus ( nextItem . dependencies , data . tasks , true , complexityReport ) : '' }
2025-05-16 23:24:25 +02:00
` +
2025-06-07 20:30:51 -04:00
` Complexity: ${ nextItem && nextItem . complexityScore ? getComplexityWithColor ( nextItem . complexityScore ) : chalk . gray ( 'N/A' ) } ` ;
// Calculate width for side-by-side display
// Box borders, padding take approximately 4 chars on each side
const minDashboardWidth = 50 ; // Minimum width for dashboard
const minDependencyWidth = 50 ; // Minimum width for dependency dashboard
const totalMinWidth = minDashboardWidth + minDependencyWidth + 4 ; // Extra 4 chars for spacing
// If terminal is wide enough, show boxes side by side with responsive widths
if ( terminalWidth >= totalMinWidth ) {
// Calculate widths proportionally for each box - use exact 50% width each
const availableWidth = terminalWidth ;
const halfWidth = Math . floor ( availableWidth / 2 ) ;
// Account for border characters (2 chars on each side)
const boxContentWidth = halfWidth - 4 ;
// Create boxen options with precise widths
const dashboardBox = boxen ( projectDashboardContent , {
padding : 1 ,
borderColor : 'blue' ,
borderStyle : 'round' ,
width : boxContentWidth ,
dimBorder : false
} ) ;
const dependencyBox = boxen ( dependencyDashboardContent , {
padding : 1 ,
borderColor : 'magenta' ,
borderStyle : 'round' ,
width : boxContentWidth ,
dimBorder : false
} ) ;
// Create a better side-by-side layout with exact spacing
const dashboardLines = dashboardBox . split ( '\n' ) ;
const dependencyLines = dependencyBox . split ( '\n' ) ;
// Make sure both boxes have the same height
const maxHeight = Math . max ( dashboardLines . length , dependencyLines . length ) ;
// For each line of output, pad the dashboard line to exactly halfWidth chars
// This ensures the dependency box starts at exactly the right position
const combinedLines = [ ] ;
for ( let i = 0 ; i < maxHeight ; i ++ ) {
// Get the dashboard line (or empty string if we've run out of lines)
const dashLine = i < dashboardLines . length ? dashboardLines [ i ] : '' ;
// Get the dependency line (or empty string if we've run out of lines)
const depLine = i < dependencyLines . length ? dependencyLines [ i ] : '' ;
// Remove any trailing spaces from dashLine before padding to exact width
const trimmedDashLine = dashLine . trimEnd ( ) ;
// Pad the dashboard line to exactly halfWidth chars with no extra spaces
const paddedDashLine = trimmedDashLine . padEnd ( halfWidth , ' ' ) ;
// Join the lines with no space in between
combinedLines . push ( paddedDashLine + depLine ) ;
}
// Join all lines and output
console . log ( combinedLines . join ( '\n' ) ) ;
} else {
// Terminal too narrow, show boxes stacked vertically
const dashboardBox = boxen ( projectDashboardContent , {
padding : 1 ,
borderColor : 'blue' ,
borderStyle : 'round' ,
margin : { top : 0 , bottom : 1 }
} ) ;
const dependencyBox = boxen ( dependencyDashboardContent , {
padding : 1 ,
borderColor : 'magenta' ,
borderStyle : 'round' ,
margin : { top : 0 , bottom : 1 }
} ) ;
// Display stacked vertically
console . log ( dashboardBox ) ;
console . log ( dependencyBox ) ;
}
if ( filteredTasks . length === 0 ) {
console . log (
boxen (
statusFilter
? chalk . yellow ( ` No tasks with status ' ${ statusFilter } ' found ` )
: chalk . yellow ( 'No tasks found' ) ,
{ padding : 1 , borderColor : 'yellow' , borderStyle : 'round' }
)
) ;
return ;
}
// COMPLETELY REVISED TABLE APPROACH
// Define percentage-based column widths and calculate actual widths
// Adjust percentages based on content type and user requirements
// Adjust ID width if showing subtasks (subtask IDs are longer: e.g., "1.2")
const idWidthPct = withSubtasks ? 10 : 7 ;
// Calculate max status length to accommodate "in-progress"
const statusWidthPct = 15 ;
// Increase priority column width as requested
const priorityWidthPct = 12 ;
// Make dependencies column smaller as requested (-20%)
const depsWidthPct = 20 ;
const complexityWidthPct = 10 ;
// Calculate title/description width as remaining space (+20% from dependencies reduction)
const titleWidthPct =
100 -
idWidthPct -
statusWidthPct -
priorityWidthPct -
depsWidthPct -
complexityWidthPct ;
// Allow 10 characters for borders and padding
const availableWidth = terminalWidth - 10 ;
// Calculate actual column widths based on percentages
const idWidth = Math . floor ( availableWidth * ( idWidthPct / 100 ) ) ;
const statusWidth = Math . floor ( availableWidth * ( statusWidthPct / 100 ) ) ;
const priorityWidth = Math . floor ( availableWidth * ( priorityWidthPct / 100 ) ) ;
const depsWidth = Math . floor ( availableWidth * ( depsWidthPct / 100 ) ) ;
const complexityWidth = Math . floor (
availableWidth * ( complexityWidthPct / 100 )
) ;
const titleWidth = Math . floor ( availableWidth * ( titleWidthPct / 100 ) ) ;
// Create a table with correct borders and spacing
const table = new Table ( {
head : [
chalk . cyan . bold ( 'ID' ) ,
chalk . cyan . bold ( 'Title' ) ,
chalk . cyan . bold ( 'Status' ) ,
chalk . cyan . bold ( 'Priority' ) ,
chalk . cyan . bold ( 'Dependencies' ) ,
chalk . cyan . bold ( 'Complexity' )
] ,
colWidths : [
idWidth ,
titleWidth ,
statusWidth ,
priorityWidth ,
depsWidth ,
complexityWidth // Added complexity column width
] ,
style : {
head : [ ] , // No special styling for header
border : [ ] , // No special styling for border
compact : false // Use default spacing
} ,
wordWrap : true ,
wrapOnWordBoundary : true
} ) ;
// Process tasks for the table
filteredTasks . forEach ( ( task ) => {
// Format dependencies with status indicators (colored)
let depText = 'None' ;
if ( task . dependencies && task . dependencies . length > 0 ) {
// Use the proper formatDependenciesWithStatus function for colored status
depText = formatDependenciesWithStatus (
task . dependencies ,
data . tasks ,
true ,
complexityReport
) ;
} else {
depText = chalk . gray ( 'None' ) ;
}
// Clean up any ANSI codes or confusing characters
const cleanTitle = task . title . replace ( /\n/g , ' ' ) ;
// Get priority color
const priorityColor =
{
high : chalk . red ,
medium : chalk . yellow ,
low : chalk . gray
} [ task . priority || 'medium' ] || chalk . white ;
// Format status
const status = getStatusWithColor ( task . status , true ) ;
// Add the row without truncating dependencies
table . push ( [
task . id . toString ( ) ,
truncate ( cleanTitle , titleWidth - 3 ) ,
status ,
priorityColor ( truncate ( task . priority || 'medium' , priorityWidth - 2 ) ) ,
depText ,
task . complexityScore
? getComplexityWithColor ( task . complexityScore )
: chalk . gray ( 'N/A' )
] ) ;
// Add subtasks if requested
if ( withSubtasks && task . subtasks && task . subtasks . length > 0 ) {
task . subtasks . forEach ( ( subtask ) => {
// Format subtask dependencies with status indicators
let subtaskDepText = 'None' ;
if ( subtask . dependencies && subtask . dependencies . length > 0 ) {
// Handle both subtask-to-subtask and subtask-to-task dependencies
const formattedDeps = subtask . dependencies
. map ( ( depId ) => {
// Check if it's a dependency on another subtask
if ( typeof depId === 'number' && depId < 100 ) {
const foundSubtask = task . 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 ( ` ${ task . id } . ${ depId } ` ) ;
} else if ( isInProgress ) {
return chalk . hex ( '#FFA500' ) . bold ( ` ${ task . id } . ${ depId } ` ) ;
} else {
return chalk . red . bold ( ` ${ task . id } . ${ depId } ` ) ;
}
}
}
// Default to regular task dependency
const depTask = data . tasks . find ( ( t ) => t . id === depId ) ;
if ( depTask ) {
// Add complexity to depTask before checking status
addComplexityToTask ( depTask , complexityReport ) ;
const isDone =
depTask . status === 'done' || depTask . status === 'completed' ;
const isInProgress = depTask . status === 'in-progress' ;
// Use the same color scheme as in formatDependenciesWithStatus
if ( isDone ) {
return chalk . green . bold ( ` ${ depId } ` ) ;
} else if ( isInProgress ) {
return chalk . hex ( '#FFA500' ) . bold ( ` ${ depId } ` ) ;
} else {
return chalk . red . bold ( ` ${ depId } ` ) ;
}
}
return chalk . cyan ( depId . toString ( ) ) ;
} )
. join ( ', ' ) ;
subtaskDepText = formattedDeps || chalk . gray ( 'None' ) ;
}
// Add the subtask row without truncating dependencies
table . push ( [
` ${ task . id } . ${ subtask . id } ` ,
chalk . dim ( ` └─ ${ truncate ( subtask . title , titleWidth - 5 ) } ` ) ,
getStatusWithColor ( subtask . status , true ) ,
chalk . dim ( '-' ) ,
subtaskDepText ,
subtask . complexityScore
? chalk . gray ( ` ${ subtask . complexityScore } ` )
: chalk . gray ( 'N/A' )
] ) ;
} ) ;
}
} ) ;
// Ensure we output the table even if it had to wrap
try {
console . log ( table . toString ( ) ) ;
} catch ( err ) {
log ( 'error' , ` Error rendering table: ${ err . message } ` ) ;
// Fall back to simpler output
console . log (
chalk . yellow (
'\nFalling back to simple task list due to terminal width constraints:'
)
) ;
filteredTasks . forEach ( ( task ) => {
console . log (
` ${ chalk . cyan ( task . id ) } : ${ chalk . white ( task . title ) } - ${ getStatusWithColor ( task . status ) } `
) ;
} ) ;
}
// Show filter info if applied
if ( statusFilter ) {
console . log ( chalk . yellow ( ` \n Filtered by status: ${ statusFilter } ` ) ) ;
console . log (
chalk . yellow ( ` Showing ${ filteredTasks . length } of ${ totalTasks } tasks ` )
) ;
}
// Define priority colors
const priorityColors = {
high : chalk . red . bold ,
medium : chalk . yellow ,
low : chalk . gray
} ;
// Show next task box in a prominent color
if ( nextItem ) {
// Prepare subtasks section if they exist (Only tasks have .subtasks property)
let subtasksSection = '' ;
// Check if the nextItem is a top-level task before looking for subtasks
const parentTaskForSubtasks = data . tasks . find (
( t ) => String ( t . id ) === String ( nextItem . id )
) ; // Find the original task object
if (
parentTaskForSubtasks &&
parentTaskForSubtasks . subtasks &&
parentTaskForSubtasks . subtasks . length > 0
) {
subtasksSection = ` \n \n ${ chalk . white . bold ( 'Subtasks:' ) } \n ` ;
subtasksSection += parentTaskForSubtasks . subtasks
. map ( ( subtask ) => {
// Add complexity to subtask before display
addComplexityToTask ( subtask , complexityReport ) ;
// Using a more simplified format for subtask status display
const status = subtask . status || 'pending' ;
const statusColors = {
done : chalk . green ,
completed : chalk . green ,
pending : chalk . yellow ,
'in-progress' : chalk . blue ,
deferred : chalk . gray ,
blocked : chalk . red ,
cancelled : chalk . gray
} ;
const statusColor =
statusColors [ status . toLowerCase ( ) ] || chalk . white ;
// Ensure subtask ID is displayed correctly using parent ID from the original task object
return ` ${ chalk . cyan ( ` ${ parentTaskForSubtasks . id } . ${ subtask . id } ` ) } [ ${ statusColor ( status ) } ] ${ subtask . title } ` ;
} )
. join ( '\n' ) ;
}
console . log (
boxen (
chalk . hex ( '#FF8800' ) . bold (
// Use nextItem.id and nextItem.title
` 🔥 Next Task to Work On: # ${ nextItem . id } - ${ nextItem . title } `
) +
'\n\n' +
// Use nextItem.priority, nextItem.status, nextItem.dependencies
` ${ chalk . white ( 'Priority:' ) } ${ priorityColors [ nextItem . priority || 'medium' ] ( nextItem . priority || 'medium' ) } ${ chalk . white ( 'Status:' ) } ${ getStatusWithColor ( nextItem . status , true ) } \n ` +
` ${ chalk . white ( 'Dependencies:' ) } ${ nextItem . dependencies && nextItem . dependencies . length > 0 ? formatDependenciesWithStatus ( nextItem . dependencies , data . tasks , true , complexityReport ) : chalk . gray ( 'None' ) } \n \n ` +
// Use nextTask.description (Note: findNextTask doesn't return description, need to fetch original task/subtask for this)
// *** Fetching original item for description and details ***
` ${ chalk . white ( 'Description:' ) } ${ getWorkItemDescription ( nextItem , data . tasks ) } ` +
subtasksSection + // <-- Subtasks are handled above now
'\n\n' +
// Use nextItem.id
` ${ chalk . cyan ( 'Start working:' ) } ${ chalk . yellow ( ` task-master set-status --id= ${ nextItem . id } --status=in-progress ` ) } \n ` +
// Use nextItem.id
` ${ chalk . cyan ( 'View details:' ) } ${ chalk . yellow ( ` task-master show ${ nextItem . id } ` ) } ` ,
{
padding : { left : 2 , right : 2 , top : 1 , bottom : 1 } ,
borderColor : '#FF8800' ,
borderStyle : 'round' ,
margin : { top : 1 , bottom : 1 } ,
title : '⚡ RECOMMENDED NEXT TASK ⚡' ,
titleAlignment : 'center' ,
width : terminalWidth - 4 ,
fullscreen : false
}
)
) ;
} else {
console . log (
boxen (
chalk . hex ( '#FF8800' ) . bold ( 'No eligible next task found' ) +
'\n\n' +
'All pending tasks have dependencies that are not yet completed, or all tasks are done.' ,
{
padding : 1 ,
borderColor : '#FF8800' ,
borderStyle : 'round' ,
margin : { top : 1 , bottom : 1 } ,
title : '⚡ NEXT TASK ⚡' ,
titleAlignment : 'center' ,
width : terminalWidth - 4 // Use full terminal width minus a small margin
}
)
) ;
}
// Show next steps
console . log (
boxen (
chalk . white . bold ( 'Suggested Next Steps:' ) +
'\n\n' +
` ${ chalk . cyan ( '1.' ) } Run ${ chalk . yellow ( 'task-master next' ) } to see what to work on next \n ` +
` ${ chalk . cyan ( '2.' ) } Run ${ chalk . yellow ( 'task-master expand --id=<id>' ) } to break down a task into subtasks \n ` +
` ${ chalk . cyan ( '3.' ) } Run ${ chalk . yellow ( 'task-master set-status --id=<id> --status=done' ) } to mark a task as complete ` ,
{
padding : 1 ,
borderColor : 'gray' ,
borderStyle : 'round' ,
margin : { top : 1 }
}
)
) ;
} catch ( error ) {
log ( 'error' , ` Error listing tasks: ${ error . message } ` ) ;
if ( outputFormat === 'json' ) {
// Return structured error for JSON output
throw {
code : 'TASK_LIST_ERROR' ,
message : error . message ,
details : error . stack
} ;
}
console . error ( chalk . red ( ` Error: ${ error . message } ` ) ) ;
process . exit ( 1 ) ;
}
2025-04-21 17:48:30 -04:00
}
2025-04-28 00:27:19 -04:00
// *** Helper function to get description for task or subtask ***
function getWorkItemDescription ( item , allTasks ) {
2025-06-07 20:30:51 -04:00
if ( ! item ) return 'N/A' ;
if ( item . parentId ) {
// It's a subtask
const parent = allTasks . find ( ( t ) => t . id === item . parentId ) ;
const subtask = parent ? . subtasks ? . find (
( st ) => ` ${ parent . id } . ${ st . id } ` === item . id
) ;
return subtask ? . description || 'No description available.' ;
} else {
// It's a top-level task
const task = allTasks . find ( ( t ) => String ( t . id ) === String ( item . id ) ) ;
return task ? . description || 'No description available.' ;
}
2025-04-28 00:27:19 -04:00
}
2025-06-07 22:07:35 -04:00
/ * *
* Generate markdown - formatted output for README files
* @ param { Object } data - Full tasks data
* @ param { Array } filteredTasks - Filtered tasks array
* @ param { Object } stats - Statistics object
* @ returns { string } - Formatted markdown string
* /
function generateMarkdownOutput ( data , filteredTasks , stats ) {
const {
totalTasks ,
completedTasks ,
completionPercentage ,
doneCount ,
inProgressCount ,
pendingCount ,
blockedCount ,
deferredCount ,
cancelledCount ,
totalSubtasks ,
completedSubtasks ,
subtaskCompletionPercentage ,
inProgressSubtasks ,
pendingSubtasks ,
blockedSubtasks ,
deferredSubtasks ,
cancelledSubtasks ,
tasksWithNoDeps ,
tasksReadyToWork ,
tasksWithUnsatisfiedDeps ,
mostDependedOnTask ,
mostDependedOnTaskId ,
maxDependents ,
avgDependenciesPerTask ,
complexityReport ,
withSubtasks ,
nextItem
} = stats ;
let markdown = '' ;
// Create progress bars for markdown (using Unicode block characters)
const createMarkdownProgressBar = ( percentage , width = 20 ) => {
const filled = Math . round ( ( percentage / 100 ) * width ) ;
const empty = width - filled ;
return '█' . repeat ( filled ) + '░' . repeat ( empty ) ;
} ;
// Dashboard section
markdown += '```\n' ;
markdown +=
'╭─────────────────────────────────────────────────────────╮╭─────────────────────────────────────────────────────────╮\n' ;
markdown +=
'│ ││ │\n' ;
markdown +=
'│ Project Dashboard ││ Dependency Status & Next Task │\n' ;
markdown += ` │ Tasks Progress: ${ createMarkdownProgressBar ( completionPercentage , 20 ) } ${ Math . round ( completionPercentage ) } % ││ Dependency Metrics: │ \n ` ;
markdown += ` │ ${ Math . round ( completionPercentage ) } % ││ • Tasks with no dependencies: ${ tasksWithNoDeps } │ \n ` ;
markdown += ` │ Done: ${ doneCount } In Progress: ${ inProgressCount } Pending: ${ pendingCount } Blocked: ${ blockedCount } ││ • Tasks ready to work on: ${ tasksReadyToWork } │ \n ` ;
markdown += ` │ Deferred: ${ deferredCount } Cancelled: ${ cancelledCount } ││ • Tasks blocked by dependencies: ${ tasksWithUnsatisfiedDeps } │ \n ` ;
markdown += ` │ ││ • Most depended-on task: # ${ mostDependedOnTaskId } ( ${ maxDependents } dependents) │ \n ` ;
markdown += ` │ Subtasks Progress: ${ createMarkdownProgressBar ( subtaskCompletionPercentage , 20 ) } ││ • Avg dependencies per task: ${ avgDependenciesPerTask . toFixed ( 1 ) } │ \n ` ;
markdown += ` │ ${ Math . round ( subtaskCompletionPercentage ) } % ${ Math . round ( subtaskCompletionPercentage ) } % ││ │ \n ` ;
markdown += ` │ Completed: ${ completedSubtasks } / ${ totalSubtasks } In Progress: ${ inProgressSubtasks } Pending: ${ pendingSubtasks } ││ Next Task to Work On: │ \n ` ;
const nextTaskTitle = nextItem
? nextItem . title . length > 40
? nextItem . title . substring ( 0 , 37 ) + '...'
: nextItem . title
: 'No task available' ;
markdown += ` │ Blocked: ${ blockedSubtasks } Deferred: ${ deferredSubtasks } Cancelled: ${ cancelledSubtasks } ││ ID: ${ nextItem ? nextItem . id : 'N/A' } - ${ nextTaskTitle } │ \n ` ;
markdown += ` │ ││ Priority: ${ nextItem ? nextItem . priority || 'medium' : '' } Dependencies: ${ nextItem && nextItem . dependencies && nextItem . dependencies . length > 0 ? 'Some' : 'None' } │ \n ` ;
markdown += ` │ Priority Breakdown: ││ Complexity: ${ nextItem && nextItem . complexityScore ? '● ' + nextItem . complexityScore : 'N/A' } │ \n ` ;
markdown += ` │ • High priority: ${ data . tasks . filter ( ( t ) => t . priority === 'high' ) . length } │╰─────────────────────────────────────────────────────────╯ \n ` ;
markdown += ` │ • Medium priority: ${ data . tasks . filter ( ( t ) => t . priority === 'medium' ) . length } │ \n ` ;
markdown += ` │ • Low priority: ${ data . tasks . filter ( ( t ) => t . priority === 'low' ) . length } │ \n ` ;
markdown += '│ │\n' ;
markdown += '╰─────────────────────────────────────────────────────────╯\n' ;
// Tasks table
markdown +=
'┌───────────┬──────────────────────────────────────┬─────────────────┬──────────────┬───────────────────────┬───────────┐\n' ;
markdown +=
'│ ID │ Title │ Status │ Priority │ Dependencies │ Complexi… │\n' ;
markdown +=
'├───────────┼──────────────────────────────────────┼─────────────────┼──────────────┼───────────────────────┼───────────┤\n' ;
// Helper function to format status with symbols
const getStatusSymbol = ( status ) => {
switch ( status ) {
case 'done' :
case 'completed' :
return '✓ done' ;
case 'in-progress' :
return '► in-progress' ;
case 'pending' :
return '○ pending' ;
case 'blocked' :
return '⭕ blocked' ;
case 'deferred' :
return 'x deferred' ;
case 'cancelled' :
return 'x cancelled' ;
case 'review' :
return '? review' ;
default :
return status || 'pending' ;
}
} ;
// Helper function to format dependencies without color codes
const formatDependenciesForMarkdown = ( deps , allTasks ) => {
if ( ! deps || deps . length === 0 ) return 'None' ;
return deps
. map ( ( depId ) => {
const depTask = allTasks . find ( ( t ) => t . id === depId ) ;
return depTask ? depId . toString ( ) : depId . toString ( ) ;
} )
. join ( ', ' ) ;
} ;
// Process all tasks
filteredTasks . forEach ( ( task ) => {
const taskTitle = task . title ; // No truncation for README
const statusSymbol = getStatusSymbol ( task . status ) ;
const priority = task . priority || 'medium' ;
const deps = formatDependenciesForMarkdown ( task . dependencies , data . tasks ) ;
const complexity = task . complexityScore
? ` ● ${ task . complexityScore } `
: 'N/A' ;
markdown += ` │ ${ task . id . toString ( ) . padEnd ( 9 ) } │ ${ taskTitle . substring ( 0 , 36 ) . padEnd ( 36 ) } │ ${ statusSymbol . padEnd ( 15 ) } │ ${ priority . padEnd ( 12 ) } │ ${ deps . substring ( 0 , 21 ) . padEnd ( 21 ) } │ ${ complexity . padEnd ( 9 ) } │ \n ` ;
// Add subtasks if requested
if ( withSubtasks && task . subtasks && task . subtasks . length > 0 ) {
task . subtasks . forEach ( ( subtask ) => {
const subtaskTitle = ` └─ ${ subtask . title } ` ; // No truncation
const subtaskStatus = getStatusSymbol ( subtask . status ) ;
const subtaskDeps = formatDependenciesForMarkdown (
subtask . dependencies ,
data . tasks
) ;
const subtaskComplexity = subtask . complexityScore
? subtask . complexityScore . toString ( )
: 'N/A' ;
markdown +=
'├───────────┼──────────────────────────────────────┼─────────────────┼──────────────┼───────────────────────┼───────────┤\n' ;
markdown += ` │ ${ task . id } . ${ subtask . id } ${ ' ' . padEnd ( 6 ) } │ ${ subtaskTitle . substring ( 0 , 36 ) . padEnd ( 36 ) } │ ${ subtaskStatus . padEnd ( 15 ) } │ - │ ${ subtaskDeps . substring ( 0 , 21 ) . padEnd ( 21 ) } │ ${ subtaskComplexity . padEnd ( 9 ) } │ \n ` ;
} ) ;
}
markdown +=
'├───────────┼──────────────────────────────────────┼─────────────────┼──────────────┼───────────────────────┼───────────┤\n' ;
} ) ;
// Close the table
markdown = markdown . slice (
0 ,
- 1 *
'├───────────┼──────────────────────────────────────┼─────────────────┼──────────────┼───────────────────────┼───────────┤\n'
. length
) ;
markdown +=
'└───────────┴──────────────────────────────────────┴─────────────────┴──────────────┴───────────────────────┴───────────┘\n' ;
markdown += '```\n\n' ;
// Next task recommendation
if ( nextItem ) {
markdown +=
'╭────────────────────────────────────────────── ⚡ RECOMMENDED NEXT TASK ⚡ ──────────────────────────────────────────────╮\n' ;
markdown +=
'│ │\n' ;
markdown += ` │ 🔥 Next Task to Work On: # ${ nextItem . id } - ${ nextItem . title } │ \n ` ;
markdown +=
'│ │\n' ;
markdown += ` │ Priority: ${ nextItem . priority || 'medium' } Status: ${ getStatusSymbol ( nextItem . status ) } │ \n ` ;
markdown += ` │ Dependencies: ${ nextItem . dependencies && nextItem . dependencies . length > 0 ? formatDependenciesForMarkdown ( nextItem . dependencies , data . tasks ) : 'None' } │ \n ` ;
markdown +=
'│ │\n' ;
markdown += ` │ Description: ${ getWorkItemDescription ( nextItem , data . tasks ) } │ \n ` ;
markdown +=
'│ │\n' ;
// Add subtasks if they exist
const parentTask = data . tasks . find ( ( t ) => t . id === nextItem . id ) ;
if ( parentTask && parentTask . subtasks && parentTask . subtasks . length > 0 ) {
markdown +=
'│ Subtasks: │\n' ;
parentTask . subtasks . forEach ( ( subtask ) => {
markdown += ` │ ${ nextItem . id } . ${ subtask . id } [ ${ subtask . status || 'pending' } ] ${ subtask . title } │ \n ` ;
} ) ;
markdown +=
'│ │\n' ;
}
markdown += ` │ Start working: task-master set-status --id= ${ nextItem . id } --status=in-progress │ \n ` ;
markdown += ` │ View details: task-master show ${ nextItem . id } │ \n ` ;
markdown +=
'│ │\n' ;
markdown +=
'╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯\n\n' ;
}
// Suggested next steps
markdown += '\n' ;
markdown +=
'╭──────────────────────────────────────────────────────────────────────────────────────╮\n' ;
markdown +=
'│ │\n' ;
markdown +=
'│ Suggested Next Steps: │\n' ;
markdown +=
'│ │\n' ;
markdown +=
'│ 1. Run task-master next to see what to work on next │\n' ;
markdown +=
'│ 2. Run task-master expand --id=<id> to break down a task into subtasks │\n' ;
markdown +=
'│ 3. Run task-master set-status --id=<id> --status=done to mark a task as complete │\n' ;
markdown +=
'│ │\n' ;
markdown +=
'╰──────────────────────────────────────────────────────────────────────────────────────╯\n' ;
return markdown ;
}
2025-04-21 17:48:30 -04:00
export default listTasks ;