feat: add --compact flag for minimal task list output (#1054)

* feat: add --compact flag for minimal task list output

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Ralph Khreish <35776126+Crunchyman-ralph@users.noreply.github.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Ladi
2025-08-11 18:35:23 +02:00
committed by GitHub
parent 30ca144231
commit 782728ff95
7 changed files with 343 additions and 13 deletions

View File

@@ -1753,6 +1753,7 @@ function registerCommands(programInstance) {
)
.option('-s, --status <status>', 'Filter by status')
.option('--with-subtasks', 'Show subtasks for each task')
.option('-c, --compact', 'Display tasks in compact one-line format')
.option('--tag <tag>', 'Specify tag context for task operations')
.action(async (options) => {
// Initialize TaskMaster
@@ -1770,18 +1771,21 @@ function registerCommands(programInstance) {
const statusFilter = options.status;
const withSubtasks = options.withSubtasks || false;
const compact = options.compact || false;
const tag = taskMaster.getCurrentTag();
// Show current tag context
displayCurrentTagIndicator(tag);
console.log(
chalk.blue(`Listing tasks from: ${taskMaster.getTasksPath()}`)
);
if (statusFilter) {
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
}
if (withSubtasks) {
console.log(chalk.blue('Including subtasks in listing'));
if (!compact) {
console.log(
chalk.blue(`Listing tasks from: ${taskMaster.getTasksPath()}`)
);
if (statusFilter) {
console.log(chalk.blue(`Filtering by status: ${statusFilter}`));
}
if (withSubtasks) {
console.log(chalk.blue('Including subtasks in listing'));
}
}
await listTasks(
@@ -1789,7 +1793,7 @@ function registerCommands(programInstance) {
statusFilter,
taskMaster.getComplexityReportPath(),
withSubtasks,
'text',
compact ? 'compact' : 'text',
{ projectRoot: taskMaster.getProjectRoot(), tag }
);
});

View File

@@ -294,6 +294,11 @@ function listTasks(
});
}
// For compact output, return minimal one-line format
if (outputFormat === 'compact') {
return renderCompactOutput(filteredTasks, withSubtasks);
}
// ... existing code for text output ...
// Calculate status breakdowns as percentages of total
@@ -962,4 +967,98 @@ function generateMarkdownOutput(data, filteredTasks, stats) {
return markdown;
}
/**
* Format dependencies for compact output with truncation and coloring
* @param {Array} dependencies - Array of dependency IDs
* @returns {string} - Formatted dependency string with arrow prefix
*/
function formatCompactDependencies(dependencies) {
if (!dependencies || dependencies.length === 0) {
return '';
}
if (dependencies.length > 5) {
const visible = dependencies.slice(0, 5).join(',');
const remaining = dependencies.length - 5;
return `${chalk.cyan(visible)}${chalk.gray('... (+' + remaining + ' more)')}`;
} else {
return `${chalk.cyan(dependencies.join(','))}`;
}
}
/**
* Format a single task in compact one-line format
* @param {Object} task - Task object
* @param {number} maxTitleLength - Maximum title length before truncation
* @returns {string} - Formatted task line
*/
function formatCompactTask(task, maxTitleLength = 50) {
const status = task.status || 'pending';
const priority = task.priority || 'medium';
const title = truncate(task.title || 'Untitled', maxTitleLength);
// Use colored status from existing function
const coloredStatus = getStatusWithColor(status, true);
// Color priority based on level
const priorityColors = {
high: chalk.red,
medium: chalk.yellow,
low: chalk.gray
};
const priorityColor = priorityColors[priority] || chalk.white;
// Format dependencies using shared helper
const depsText = formatCompactDependencies(task.dependencies);
return `${chalk.cyan(task.id)} ${coloredStatus} ${chalk.white(title)} ${priorityColor('(' + priority + ')')}${depsText}`;
}
/**
* Format a subtask in compact format with indentation
* @param {Object} subtask - Subtask object
* @param {string|number} parentId - Parent task ID
* @param {number} maxTitleLength - Maximum title length before truncation
* @returns {string} - Formatted subtask line
*/
function formatCompactSubtask(subtask, parentId, maxTitleLength = 47) {
const status = subtask.status || 'pending';
const title = truncate(subtask.title || 'Untitled', maxTitleLength);
// Use colored status from existing function
const coloredStatus = getStatusWithColor(status, true);
// Format dependencies using shared helper
const depsText = formatCompactDependencies(subtask.dependencies);
return ` ${chalk.cyan(parentId + '.' + subtask.id)} ${coloredStatus} ${chalk.dim(title)}${depsText}`;
}
/**
* Render complete compact output
* @param {Array} filteredTasks - Tasks to display
* @param {boolean} withSubtasks - Whether to include subtasks
* @returns {void} - Outputs directly to console
*/
function renderCompactOutput(filteredTasks, withSubtasks) {
if (filteredTasks.length === 0) {
console.log('No tasks found');
return;
}
const output = [];
filteredTasks.forEach((task) => {
output.push(formatCompactTask(task));
if (withSubtasks && task.subtasks && task.subtasks.length > 0) {
task.subtasks.forEach((subtask) => {
output.push(formatCompactSubtask(subtask, task.id));
});
}
});
console.log(output.join('\n'));
}
export default listTasks;

View File

@@ -1430,6 +1430,20 @@ function ensureTagMetadata(tagObj, opts = {}) {
return tagObj;
}
/**
* Strip ANSI color codes from a string
* Useful for testing, logging to files, or when clean text output is needed
* @param {string} text - The text that may contain ANSI color codes
* @returns {string} - The text with ANSI color codes removed
*/
function stripAnsiCodes(text) {
if (typeof text !== 'string') {
return text;
}
// Remove ANSI escape sequences (color codes, cursor movements, etc.)
return text.replace(/\x1b\[[0-9;]*m/g, '');
}
// Export all utility functions and configuration
export {
LOG_LEVELS,
@@ -1467,5 +1481,6 @@ export {
markMigrationForNotice,
flattenTasksWithSubtasks,
ensureTagMetadata,
stripAnsiCodes,
normalizeTaskIds
};