@dvc2/tasktracker-cli
Version:
Developer context journal for AI-assisted coding - maintain project context across sessions
305 lines (252 loc) • 8.79 kB
JavaScript
/**
* TaskTracker - Task Context Export
*
* Generates a markdown summary of task details suitable for PR descriptions,
* sharing with team members, or AI context windows.
*/
const fs = require('fs');
const path = require('path');
// Constants
const DATA_DIR = process.env.TASKTRACKER_DATA_DIR || path.join(process.cwd(), '.tasktracker');
const TASKS_PATH = path.join(DATA_DIR, 'tasks.json');
/**
* Generate a markdown context summary for a task
*
* @param {string} taskId - The ID of the task
* @param {Object} options - Options for format and included fields
*/
function generateTaskContext(taskId, options = {}) {
// Check if TaskTracker is initialized
if (!fs.existsSync(TASKS_PATH)) {
throw new Error('TaskTracker not initialized! Please run: tasktracker init');
}
// Load tasks
const tasksData = JSON.parse(fs.readFileSync(TASKS_PATH, 'utf8'));
let task;
// If taskId is "current", try to find an in-progress task
if (taskId === 'current') {
task = tasksData.tasks.find(t => t.status.toLowerCase() === 'in-progress');
if (!task) {
throw new Error('No task currently in progress. Specify a task ID or start working on a task.');
}
} else {
// Find the task by ID
task = tasksData.tasks.find(t => t.id.toString() === taskId.toString());
if (!task) {
throw new Error(`Task #${taskId} not found`);
}
}
// Generate context
const format = options.format || 'markdown';
if (format === 'markdown') {
generateMarkdownContext(task, options, tasksData);
} else if (format === 'json') {
console.log(JSON.stringify(task, null, 2));
} else if (format === 'plain') {
generatePlainTextContext(task, options, tasksData);
} else {
throw new Error(`Unknown format: ${format}. Supported formats: markdown, json, plain`);
}
}
/**
* Generate markdown context for a task
*/
function generateMarkdownContext(task, options = {}, tasksData) {
const includeFiles = options.includeFiles !== false;
const includeComments = options.includeComments !== false;
const includeCommits = options.includeCommits !== false;
const includeDependencies = options.includeDependencies !== false;
const includeCodeSnippets = options.includeCodeSnippets === true;
const forPr = options.forPr === true;
let markdown = '';
// PR title format if requested
if (forPr) {
markdown += `# ${task.title}\n\n`;
markdown += `Resolves #${task.id}\n\n`;
} else {
markdown += `## Task #${task.id}: ${task.title}\n\n`;
}
// Task metadata
markdown += `**Status:** ${task.status}\n`;
markdown += `**Category:** ${task.category}\n`;
if (task.priority) {
markdown += `**Priority:** ${task.priority}\n`;
}
if (task.effort) {
markdown += `**Effort:** ${task.effort}\n`;
}
markdown += `**Created:** ${new Date(task.created).toLocaleString()}\n`;
if (task.lastUpdated) {
markdown += `**Last Updated:** ${new Date(task.lastUpdated).toLocaleString()}\n`;
}
markdown += '\n';
// Task description
if (task.description) {
markdown += `### Description\n\n${task.description}\n\n`;
}
// Related files
if (includeFiles && task.relatedFiles && task.relatedFiles.length > 0) {
markdown += '### Related Files\n\n';
task.relatedFiles.forEach(file => {
markdown += `- \`${file}\`\n`;
// Add code snippets if requested
if (includeCodeSnippets && fs.existsSync(file)) {
try {
const stats = fs.statSync(file);
if (stats.isFile() && stats.size < 100 * 1024) { // Skip files larger than 100KB
const extension = path.extname(file).substring(1);
const content = fs.readFileSync(file, 'utf8');
const snippet = content.length > 1000 ? content.substring(0, 997) + '...' : content;
markdown += '\n```' + extension + '\n' + snippet + '\n```\n\n';
}
} catch (error) {
// Silently skip files with issues
}
}
});
markdown += '\n';
}
// Comments
if (includeComments && task.comments && task.comments.length > 0) {
markdown += '### Comments\n\n';
task.comments.forEach(comment => {
const date = new Date(comment.date).toLocaleString();
markdown += `**${comment.author}** (${date}):\n`;
markdown += `${comment.text}\n\n`;
});
}
// Commits
if (includeCommits && task.commits && task.commits.length > 0) {
markdown += '### Commits\n\n';
task.commits.forEach(commit => {
const shortHash = commit.hash.substring(0, 8);
const date = new Date(commit.date).toLocaleString();
markdown += `- **${shortHash}** (${date}): ${commit.message}\n`;
if (commit.files && commit.files.length > 0) {
markdown += ' - Files:\n';
commit.files.forEach(file => {
if (file.trim()) {
markdown += ` - \`${file}\`\n`;
}
});
}
markdown += '\n';
});
}
// Dependencies
if (includeDependencies) {
if (task.dependencies && task.dependencies.length > 0) {
markdown += '### Dependencies\n\n';
markdown += 'This task depends on:\n\n';
task.dependencies.forEach(depId => {
const dep = tasksData.tasks.find(t => t.id.toString() === depId.toString());
if (dep) {
markdown += `- #${dep.id}: ${dep.title} (${dep.status})\n`;
} else {
markdown += `- #${depId} (not found)\n`;
}
});
markdown += '\n';
}
if (task.blockedBy && task.blockedBy.length > 0) {
markdown += '### Blocked By\n\n';
task.blockedBy.forEach(blockerId => {
const blocker = tasksData.tasks.find(t => t.id.toString() === blockerId.toString());
if (blocker) {
markdown += `- #${blocker.id}: ${blocker.title} (${blocker.status})\n`;
} else {
markdown += `- #${blockerId} (not found)\n`;
}
});
markdown += '\n';
}
}
// Additional PR information
if (forPr) {
markdown += '### Testing Performed\n\n';
markdown += '- [ ] Unit tests\n';
markdown += '- [ ] Integration tests\n';
markdown += '- [ ] Manual testing\n\n';
markdown += '### Screenshots/Recordings\n\n';
markdown += '_Add screenshots or recordings if applicable._\n\n';
}
console.log(markdown);
}
/**
* Generate plain text context for a task
*/
function generatePlainTextContext(task, options = {}, _tasksData) {
const includeFiles = options.includeFiles !== false;
const includeComments = options.includeComments !== false;
let text = '';
// Basic info
text += `Task #${task.id}: ${task.title}\n`;
text += `Status: ${task.status} Category: ${task.category}\n`;
if (task.priority) {
text += `Priority: ${task.priority}`;
}
if (task.effort) {
text += ` Effort: ${task.effort}`;
}
text += '\n\n';
// Description
if (task.description) {
text += `Description:\n${task.description}\n\n`;
}
// Related files
if (includeFiles && task.relatedFiles && task.relatedFiles.length > 0) {
text += 'Related Files:\n';
task.relatedFiles.forEach(file => {
text += `- ${file}\n`;
});
text += '\n';
}
// Comments
if (includeComments && task.comments && task.comments.length > 0) {
text += 'Comments:\n';
task.comments.forEach(comment => {
const date = new Date(comment.date).toLocaleString();
text += `${comment.author} (${date}):\n${comment.text}\n\n`;
});
}
console.log(text);
}
// Allow use as a command-line script or imported module
if (require.main === module) {
const args = process.argv.slice(2);
const taskId = args[0] || 'current';
const options = {
format: 'markdown',
includeFiles: true,
includeComments: true,
includeCommits: true,
includeCodeSnippets: false,
forPr: false
};
// Parse additional options
for (let i = 1; i < args.length; i++) {
if (args[i] === '--json') {
options.format = 'json';
} else if (args[i] === '--plain') {
options.format = 'plain';
} else if (args[i] === '--pr') {
options.forPr = true;
} else if (args[i] === '--no-files') {
options.includeFiles = false;
} else if (args[i] === '--no-comments') {
options.includeComments = false;
} else if (args[i] === '--no-commits') {
options.includeCommits = false;
} else if (args[i] === '--snippets') {
options.includeCodeSnippets = true;
}
}
try {
generateTaskContext(taskId, options);
} catch (error) {
console.error(`❌ ${error.message}`);
throw error;
}
} else {
module.exports = { generateTaskContext };
}