2025-04-21 17:48:30 -04:00
|
|
|
import chalk from 'chalk';
|
|
|
|
import boxen from 'boxen';
|
|
|
|
import Table from 'cli-table3';
|
|
|
|
|
|
|
|
import { log, readJSON, truncate } from '../utils.js';
|
2025-04-22 16:09:33 -04:00
|
|
|
import findNextTask from './find-next-task.js';
|
2025-04-21 17:48:30 -04:00
|
|
|
|
|
|
|
import {
|
|
|
|
displayBanner,
|
|
|
|
getStatusWithColor,
|
|
|
|
formatDependenciesWithStatus,
|
|
|
|
createProgressBar
|
|
|
|
} from '../ui.js';
|
|
|
|
|
|
|
|
/**
|
|
|
|
* List all tasks
|
|
|
|
* @param {string} tasksPath - Path to the tasks.json file
|
|
|
|
* @param {string} statusFilter - Filter by status
|
|
|
|
* @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(
|
|
|
|
tasksPath,
|
|
|
|
statusFilter,
|
|
|
|
withSubtasks = false,
|
|
|
|
outputFormat = 'text'
|
|
|
|
) {
|
|
|
|
try {
|
|
|
|
// Only display banner for text output
|
|
|
|
if (outputFormat === 'text') {
|
|
|
|
displayBanner();
|
|
|
|
}
|
|
|
|
|
|
|
|
const data = readJSON(tasksPath); // Reads the whole tasks.json
|
|
|
|
if (!data || !data.tasks) {
|
|
|
|
throw new Error(`No valid tasks found in ${tasksPath}`);
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
// ... 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
|
|
|
|
);
|
|
|
|
|
|
|
|
// Calculate dependency statistics
|
|
|
|
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
|
|
|
|
const nextTask = findNextTask(data.tasks);
|
|
|
|
const nextTaskInfo = nextTask
|
|
|
|
? `ID: ${chalk.cyan(nextTask.id)} - ${chalk.white.bold(truncate(nextTask.title, 40))}\n` +
|
|
|
|
`Priority: ${chalk.white(nextTask.priority || 'medium')} Dependencies: ${formatDependenciesWithStatus(nextTask.dependencies, data.tasks, true)}`
|
|
|
|
: chalk.yellow(
|
|
|
|
'No eligible tasks found. All tasks are either completed or have unsatisfied dependencies.'
|
|
|
|
);
|
|
|
|
|
|
|
|
// 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(nextTask ? nextTask.id : 'N/A')} - ${nextTask ? chalk.white.bold(truncate(nextTask.title, 40)) : chalk.yellow('No task available')}\n` +
|
|
|
|
`Priority: ${nextTask ? chalk.white(nextTask.priority || 'medium') : ''} Dependencies: ${nextTask ? formatDependenciesWithStatus(nextTask.dependencies, data.tasks, true) : ''}`;
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
|
|
|
|
// Calculate title/description width as remaining space (+20% from dependencies reduction)
|
|
|
|
const titleWidthPct =
|
|
|
|
100 - idWidthPct - statusWidthPct - priorityWidthPct - depsWidthPct;
|
|
|
|
|
|
|
|
// 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 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')
|
|
|
|
],
|
|
|
|
colWidths: [idWidth, titleWidth, statusWidth, priorityWidth, depsWidth],
|
|
|
|
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
|
|
|
|
);
|
|
|
|
} 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 // No truncation for dependencies
|
|
|
|
]);
|
|
|
|
|
|
|
|
// 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) {
|
|
|
|
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 // No truncation for dependencies
|
|
|
|
]);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// 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(`\nFiltered 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 (nextTask) {
|
|
|
|
// Prepare subtasks section if they exist
|
|
|
|
let subtasksSection = '';
|
|
|
|
if (nextTask.subtasks && nextTask.subtasks.length > 0) {
|
|
|
|
subtasksSection = `\n\n${chalk.white.bold('Subtasks:')}\n`;
|
|
|
|
subtasksSection += nextTask.subtasks
|
|
|
|
.map((subtask) => {
|
|
|
|
// 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;
|
|
|
|
return `${chalk.cyan(`${nextTask.id}.${subtask.id}`)} [${statusColor(status)}] ${subtask.title}`;
|
|
|
|
})
|
|
|
|
.join('\n');
|
|
|
|
}
|
|
|
|
|
|
|
|
console.log(
|
|
|
|
boxen(
|
|
|
|
chalk
|
|
|
|
.hex('#FF8800')
|
|
|
|
.bold(
|
|
|
|
`🔥 Next Task to Work On: #${nextTask.id} - ${nextTask.title}`
|
|
|
|
) +
|
|
|
|
'\n\n' +
|
|
|
|
`${chalk.white('Priority:')} ${priorityColors[nextTask.priority || 'medium'](nextTask.priority || 'medium')} ${chalk.white('Status:')} ${getStatusWithColor(nextTask.status, true)}\n` +
|
|
|
|
`${chalk.white('Dependencies:')} ${nextTask.dependencies && nextTask.dependencies.length > 0 ? formatDependenciesWithStatus(nextTask.dependencies, data.tasks, true) : chalk.gray('None')}\n\n` +
|
|
|
|
`${chalk.white('Description:')} ${nextTask.description}` +
|
|
|
|
subtasksSection +
|
|
|
|
'\n\n' +
|
|
|
|
`${chalk.cyan('Start working:')} ${chalk.yellow(`task-master set-status --id=${nextTask.id} --status=in-progress`)}\n` +
|
|
|
|
`${chalk.cyan('View details:')} ${chalk.yellow(`task-master show ${nextTask.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, // Use full terminal width minus a small margin
|
|
|
|
fullscreen: false // Keep it expandable but not literally fullscreen
|
|
|
|
}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
} 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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default listTasks;
|