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": {
|
"global": {
|
||||||
|
"userId": "1234567890",
|
||||||
"logLevel": "info",
|
"logLevel": "info",
|
||||||
"debug": false,
|
"debug": false,
|
||||||
"defaultSubtasks": 5,
|
"defaultSubtasks": 5,
|
||||||
@@ -27,7 +28,6 @@
|
|||||||
"projectName": "Taskmaster",
|
"projectName": "Taskmaster",
|
||||||
"ollamaBaseURL": "http://localhost:11434/api",
|
"ollamaBaseURL": "http://localhost:11434/api",
|
||||||
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
"bedrockBaseURL": "https://bedrock.us-east-1.amazonaws.com",
|
||||||
"userId": "1234567890",
|
|
||||||
"azureBaseURL": "https://your-endpoint.azure.com/"
|
"azureBaseURL": "https://your-endpoint.azure.com/"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import {
|
|||||||
TASK_STATUS_OPTIONS
|
TASK_STATUS_OPTIONS
|
||||||
} from '../../src/constants/task-status.js';
|
} from '../../src/constants/task-status.js';
|
||||||
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
|
import { getTaskMasterVersion } from '../../src/utils/getVersion.js';
|
||||||
|
import { syncTasksToReadme } from './sync-readme.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Runs the interactive setup process for model configuration.
|
* Runs the interactive setup process for model configuration.
|
||||||
@@ -2757,6 +2758,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;
|
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 =
|
const subtaskCompletionPercentage =
|
||||||
totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
|
totalSubtasks > 0 ? (completedSubtasks / totalSubtasks) * 100 : 0;
|
||||||
|
|
||||||
// For JSON output, return structured data
|
// Calculate dependency statistics (moved up to be available for all output formats)
|
||||||
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(
|
const completedTaskIds = new Set(
|
||||||
data.tasks
|
data.tasks
|
||||||
.filter((t) => t.status === 'done' || t.status === 'completed')
|
.filter((t) => t.status === 'done' || t.status === 'completed')
|
||||||
@@ -271,6 +192,118 @@ function listTasks(
|
|||||||
// Find next task to work on, passing the complexity report
|
// Find next task to work on, passing the complexity report
|
||||||
const nextItem = findNextTask(data.tasks, complexityReport);
|
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
|
// Get terminal width - more reliable method
|
||||||
let terminalWidth;
|
let terminalWidth;
|
||||||
try {
|
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;
|
export default listTasks;
|
||||||
|
|||||||
@@ -525,6 +525,11 @@ function displayHelp() {
|
|||||||
args: '--id=<id> --status=<status>',
|
args: '--id=<id> --status=<status>',
|
||||||
desc: `Update task status (${TASK_STATUS_OPTIONS.join(', ')})`
|
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',
|
name: 'update',
|
||||||
args: '--from=<id> --prompt="<context>"',
|
args: '--from=<id> --prompt="<context>"',
|
||||||
|
|||||||
Reference in New Issue
Block a user