763 lines
28 KiB
JavaScript
Raw Normal View History

import chalk from "chalk";
import boxen from "boxen";
import Table from "cli-table3";
import {
log,
readJSON,
truncate,
readComplexityReport,
addComplexityToTask,
} from "../utils.js";
import findNextTask from "./find-next-task.js";
import {
displayBanner,
getStatusWithColor,
formatDependenciesWithStatus,
getComplexityWithColor,
createProgressBar,
} from "../ui.js";
/**
* List all tasks
* @param {string} tasksPath - Path to the tasks.json file
* @param {string} statusFilter - Filter by status
* @param {string} reportPath - Path to the complexity report
* @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,
reportPath = null,
withSubtasks = false,
outputFormat = "text"
) {
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;
// 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, passing the complexity report
const nextItem = findNextTask(data.tasks, complexityReport);
// 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")}
` +
`Priority: ${nextItem ? chalk.white(nextItem.priority || "medium") : ""} Dependencies: ${nextItem ? formatDependenciesWithStatus(nextItem.dependencies, data.tasks, true, complexityReport) : ""}
` +
`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(`\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 (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);
}
}
// *** Helper function to get description for task or subtask ***
function getWorkItemDescription(item, allTasks) {
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.";
}
}
export default listTasks;