ninja(sync): add sync-readme command for GitHub README export with UTM tracking and professional markdown formatting. Experimental
This commit is contained in:
22
.changeset/vast-shrimps-happen.md
Normal file
22
.changeset/vast-shrimps-happen.md
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
"task-master-ai": minor
|
||||
---
|
||||
|
||||
Add sync-readme command for a task export to GitHub README
|
||||
|
||||
Introduces a new `sync-readme` command that exports your task list to your project's README.md file.
|
||||
|
||||
**Features:**
|
||||
|
||||
- **Flexible filtering**: Supports `--status` filtering (e.g., pending, done) and `--with-subtasks` flag
|
||||
- **Smart content management**: Automatically replaces existing exports or appends to new READMEs
|
||||
- **Metadata display**: Shows export timestamp, subtask inclusion status, and filter settings
|
||||
|
||||
**Usage:**
|
||||
|
||||
- `task-master sync-readme` - Export tasks without subtasks
|
||||
- `task-master sync-readme --with-subtasks` - Include subtasks in export
|
||||
- `task-master sync-readme --status=pending` - Only export pending tasks
|
||||
- `task-master sync-readme --status=done --with-subtasks` - Export completed tasks with subtasks
|
||||
|
||||
Perfect for showcasing project progress on GitHub with professional presentation and traffic analytics.
|
||||
@@ -20,6 +20,7 @@
|
||||
}
|
||||
},
|
||||
"global": {
|
||||
"userId": "1234567890",
|
||||
"logLevel": "info",
|
||||
"debug": false,
|
||||
"defaultSubtasks": 5,
|
||||
@@ -27,7 +28,6 @@
|
||||
"projectName": "Taskmaster",
|
||||
"ollamaBaseURL": "http://localhost:11434/api",
|
||||
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
||||
"userId": "1234567890",
|
||||
"azureBaseURL": "https://your-endpoint.azure.com/"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,6 +88,7 @@ import {
|
||||
TASK_STATUS_OPTIONS
|
||||
} from '../../src/constants/task-status.js';
|
||||
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
|
||||
import { syncTasksToReadme } from './sync-readme.js';
|
||||
|
||||
/**
|
||||
* Runs the interactive setup process for model configuration.
|
||||
@@ -3016,6 +3017,54 @@ Examples:
|
||||
}
|
||||
});
|
||||
|
||||
// sync-readme command
|
||||
programInstance
|
||||
.command('sync-readme')
|
||||
.description('Sync the current task list to README.md in the project root')
|
||||
.option(
|
||||
'-f, --file <file>',
|
||||
'Path to the tasks file',
|
||||
TASKMASTER_TASKS_FILE
|
||||
)
|
||||
.option('--with-subtasks', 'Include subtasks in the README output')
|
||||
.option(
|
||||
'-s, --status <status>',
|
||||
'Show only tasks matching this status (e.g., pending, done)'
|
||||
)
|
||||
.action(async (options) => {
|
||||
const tasksPath = options.file || TASKMASTER_TASKS_FILE;
|
||||
const withSubtasks = options.withSubtasks || false;
|
||||
const status = options.status || null;
|
||||
|
||||
// Find project root
|
||||
const projectRoot = findProjectRoot();
|
||||
if (!projectRoot) {
|
||||
console.error(
|
||||
chalk.red(
|
||||
'Error: Could not find project root. Make sure you are in a Task Master project directory.'
|
||||
)
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
chalk.blue(
|
||||
`📝 Syncing tasks to README.md${withSubtasks ? ' (with subtasks)' : ''}${status ? ` (status: ${status})` : ''}...`
|
||||
)
|
||||
);
|
||||
|
||||
const success = await syncTasksToReadme(projectRoot, {
|
||||
withSubtasks,
|
||||
status,
|
||||
tasksPath
|
||||
});
|
||||
|
||||
if (!success) {
|
||||
console.error(chalk.red('❌ Failed to sync tasks to README.md'));
|
||||
process.exit(1);
|
||||
}
|
||||
});
|
||||
|
||||
return programInstance;
|
||||
}
|
||||
|
||||
|
||||
184
scripts/modules/sync-readme.js
Normal file
184
scripts/modules/sync-readme.js
Normal file
@@ -0,0 +1,184 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import chalk from 'chalk';
|
||||
import { log, findProjectRoot } from './utils.js';
|
||||
import { getProjectName } from './config-manager.js';
|
||||
import listTasks from './task-manager/list-tasks.js';
|
||||
|
||||
/**
|
||||
* Creates a basic README structure if one doesn't exist
|
||||
* @param {string} projectName - Name of the project
|
||||
* @returns {string} - Basic README content
|
||||
*/
|
||||
function createBasicReadme(projectName) {
|
||||
return `# ${projectName}
|
||||
|
||||
This project is managed using Task Master.
|
||||
|
||||
`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create UTM tracking URL for task-master.dev
|
||||
* @param {string} projectRoot - The project root path
|
||||
* @returns {string} - UTM tracked URL
|
||||
*/
|
||||
function createTaskMasterUrl(projectRoot) {
|
||||
// Get the actual folder name from the project root path
|
||||
const folderName = path.basename(projectRoot);
|
||||
|
||||
// Clean folder name for UTM (replace spaces/special chars with hyphens)
|
||||
const cleanFolderName = folderName
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
|
||||
const utmParams = new URLSearchParams({
|
||||
utm_source: 'github-readme',
|
||||
utm_medium: 'readme-export',
|
||||
utm_campaign: cleanFolderName || 'task-sync',
|
||||
utm_content: 'task-export-link'
|
||||
});
|
||||
|
||||
return `https://task-master.dev?${utmParams.toString()}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the start marker with metadata
|
||||
* @param {Object} options - Export options
|
||||
* @returns {string} - Formatted start marker
|
||||
*/
|
||||
function createStartMarker(options) {
|
||||
const { timestamp, withSubtasks, status, projectRoot } = options;
|
||||
|
||||
// Format status filter text
|
||||
const statusText = status
|
||||
? `Status filter: ${status}`
|
||||
: 'Status filter: none';
|
||||
const subtasksText = withSubtasks ? 'with subtasks' : 'without subtasks';
|
||||
|
||||
// Create the export info content
|
||||
const exportInfo =
|
||||
`🎯 **Taskmaster Export** - ${timestamp}\n` +
|
||||
`📋 Export: ${subtasksText} • ${statusText}\n` +
|
||||
`🔗 Powered by [Task Master](${createTaskMasterUrl(projectRoot)})`;
|
||||
|
||||
// Create a markdown box using code blocks and emojis to mimic our UI style
|
||||
const boxContent =
|
||||
`<!-- TASKMASTER_EXPORT_START -->\n` +
|
||||
`> ${exportInfo.split('\n').join('\n> ')}\n\n`;
|
||||
|
||||
return boxContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the end marker
|
||||
* @returns {string} - Formatted end marker
|
||||
*/
|
||||
function createEndMarker() {
|
||||
return (
|
||||
`\n> 📋 **End of Taskmaster Export** - Tasks are synced from your project using the \`sync-readme\` command.\n` +
|
||||
`<!-- TASKMASTER_EXPORT_END -->\n`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Syncs the current task list to README.md at the project root
|
||||
* @param {string} projectRoot - Path to the project root directory
|
||||
* @param {Object} options - Options for syncing
|
||||
* @param {boolean} options.withSubtasks - Include subtasks in the output (default: false)
|
||||
* @param {string} options.status - Filter by status (e.g., 'pending', 'done')
|
||||
* @param {string} options.tasksPath - Custom path to tasks.json
|
||||
* @returns {boolean} - True if sync was successful, false otherwise
|
||||
*/
|
||||
export async function syncTasksToReadme(projectRoot = null, options = {}) {
|
||||
try {
|
||||
const actualProjectRoot = projectRoot || findProjectRoot() || '.';
|
||||
const { withSubtasks = false, status, tasksPath } = options;
|
||||
|
||||
// Get current tasks using the list-tasks functionality with markdown-readme format
|
||||
const tasksOutput = await listTasks(
|
||||
tasksPath ||
|
||||
path.join(actualProjectRoot, '.taskmaster', 'tasks', 'tasks.json'),
|
||||
status,
|
||||
null,
|
||||
withSubtasks,
|
||||
'markdown-readme'
|
||||
);
|
||||
|
||||
if (!tasksOutput) {
|
||||
console.log(chalk.red('❌ Failed to generate task output'));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Generate timestamp and metadata
|
||||
const timestamp =
|
||||
new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||
const projectName = getProjectName(actualProjectRoot);
|
||||
|
||||
// Create the export markers with metadata
|
||||
const startMarker = createStartMarker({
|
||||
timestamp,
|
||||
withSubtasks,
|
||||
status,
|
||||
projectRoot: actualProjectRoot
|
||||
});
|
||||
|
||||
const endMarker = createEndMarker();
|
||||
|
||||
// Create the complete task section
|
||||
const taskSection = startMarker + tasksOutput + endMarker;
|
||||
|
||||
// Read current README content
|
||||
const readmePath = path.join(actualProjectRoot, 'README.md');
|
||||
let readmeContent = '';
|
||||
try {
|
||||
readmeContent = fs.readFileSync(readmePath, 'utf8');
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
// Create basic README if it doesn't exist
|
||||
readmeContent = createBasicReadme(projectName);
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if export markers exist and replace content between them
|
||||
const startComment = '<!-- TASKMASTER_EXPORT_START -->';
|
||||
const endComment = '<!-- TASKMASTER_EXPORT_END -->';
|
||||
|
||||
let updatedContent;
|
||||
const startIndex = readmeContent.indexOf(startComment);
|
||||
const endIndex = readmeContent.indexOf(endComment);
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1) {
|
||||
// Replace existing task section
|
||||
const beforeTasks = readmeContent.substring(0, startIndex);
|
||||
const afterTasks = readmeContent.substring(endIndex + endComment.length);
|
||||
updatedContent = beforeTasks + taskSection + afterTasks;
|
||||
} else {
|
||||
// Append to end of README
|
||||
updatedContent = readmeContent + '\n' + taskSection;
|
||||
}
|
||||
|
||||
// Write updated content to README
|
||||
fs.writeFileSync(readmePath, updatedContent, 'utf8');
|
||||
|
||||
console.log(chalk.green('✅ Successfully synced tasks to README.md'));
|
||||
console.log(
|
||||
chalk.cyan(
|
||||
`📋 Export details: ${withSubtasks ? 'with' : 'without'} subtasks${status ? `, status: ${status}` : ''}`
|
||||
)
|
||||
);
|
||||
console.log(chalk.gray(`📍 Location: ${readmePath}`));
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.log(chalk.red('❌ Failed to sync tasks to README:'), error.message);
|
||||
log('error', `README sync error: ${error.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export default syncTasksToReadme;
|
||||
@@ -120,86 +120,7 @@ function listTasks(
|
||||
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
|
||||
// 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')
|
||||
@@ -271,6 +192,118 @@ function listTasks(
|
||||
// Find next task to work on, passing the complexity report
|
||||
const nextItem = findNextTask(data.tasks, complexityReport);
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// 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
|
||||
});
|
||||
}
|
||||
|
||||
// ... 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 {
|
||||
@@ -759,4 +792,232 @@ function getWorkItemDescription(item, allTasks) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
export default listTasks;
|
||||
|
||||
@@ -838,6 +838,11 @@ function displayHelp() {
|
||||
args: '--id=<id> --status=<status>',
|
||||
desc: `Update task status (${TASK_STATUS_OPTIONS.join(', ')})`
|
||||
},
|
||||
{
|
||||
name: 'sync-readme',
|
||||
args: '[--with-subtasks] [--status=<status>]',
|
||||
desc: 'Export tasks to README.md with professional formatting'
|
||||
},
|
||||
{
|
||||
name: 'update',
|
||||
args: '--from=<id> --prompt="<context>"',
|
||||
|
||||
Reference in New Issue
Block a user