taskwerk
Version:
A task management CLI for developers and AI agents working together
259 lines (224 loc) ⢠8.17 kB
JavaScript
import { Command } from 'commander';
import { TaskwerkAPI } from '../../api/taskwerk-api.js';
import { Logger } from '../../logging/logger.js';
function formatTableRow(task, widths, indent = 0) {
const statusEmoji = {
'todo': 'ā³ Todo',
'in-progress': 'š Prog',
'in_progress': 'š Prog',
'blocked': 'š« Block',
'done': 'ā
Done',
'completed': 'ā
Done',
'cancelled': 'ā Canc',
};
const priorityEmoji = {
low: 'šµ Low',
medium: 'š” Med',
high: 'š“ High',
critical: 'šØ Crit',
};
// Add indentation for subtasks
const indentStr = ' '.repeat(indent);
const taskName = indentStr + (indent > 0 ? 'āā ' : '') + task.name;
const cols = [
task.id.padEnd(widths.id),
(statusEmoji[task.status] || 'ā³ Todo').padEnd(widths.status),
(priorityEmoji[task.priority] || 'š” Med').padEnd(widths.priority),
taskName.slice(0, widths.name).padEnd(widths.name),
new Date(task.created_at).toLocaleDateString().padEnd(widths.created),
(task.assignee || '').padEnd(widths.assignee),
];
return cols.join(' ');
}
function calculateColumnWidths(_tasks) {
const widths = {
id: 12, // TASK-001.1
status: 8, // š Prog
priority: 8, // šØ Crit
name: 35, // Task name
created: 10, // MM/DD/YYYY
assignee: 10, // @username
};
// Adjust name width based on terminal width
const terminalWidth = process.stdout.columns || 80;
const fixedWidth =
widths.id + widths.status + widths.priority + widths.created + widths.assignee + 5; // spaces
const availableWidth = terminalWidth - fixedWidth;
widths.name = Math.max(20, Math.min(50, availableWidth));
return widths;
}
function buildTaskTree(tasks, api) {
// Group tasks by parent_id
const taskMap = new Map();
const rootTasks = [];
// First pass: create map and identify root tasks
tasks.forEach(task => {
taskMap.set(task.id, task);
if (!task.parent_id) {
rootTasks.push(task);
}
});
// Second pass: get subtasks for each task
const processedTasks = [];
function addTaskWithSubtasks(task, indent = 0) {
processedTasks.push({ ...task, _indent: indent });
// Get subtasks
const subtasks = api.getSubtasks(task.id);
subtasks.forEach(subtask => {
if (taskMap.has(subtask.id)) {
addTaskWithSubtasks(subtask, indent + 1);
}
});
}
// Process root tasks
rootTasks.forEach(task => {
addTaskWithSubtasks(task);
});
return processedTasks;
}
export function taskListCommand() {
const list = new Command('list');
list
.description('List tasks')
.option('-s, --status <status>', 'Filter by status')
.option('-a, --assignee <name>', 'Filter by assignee')
.option('-p, --priority <level>', 'Filter by priority')
.option('-t, --tags <tags...>', 'Filter by tags')
.option('--search <term>', 'Search in task name, description, and content')
.option('--sort <field>', 'Sort by field (created, updated, priority)', 'created')
.option('--format <format>', 'Output format (table, json, csv)', 'table')
.option('--limit <number>', 'Limit number of results', '50')
.option('--all', 'Show all tasks including completed/archived')
.option('--tree', 'Show tasks in hierarchical tree view')
.addHelpText(
'after',
`
Examples:
$ twrk listtask # List active tasks
$ twrk listtask --all # List all tasks including done
$ twrk listtask -s todo # List only todo tasks
$ twrk listtask -a @john # List tasks assigned to john
$ twrk listtask -p high # List high priority tasks
$ twrk listtask -t bug # List tasks tagged with 'bug'
$ twrk listtask --search "login" # Search for tasks mentioning login
$ twrk listtask --sort priority --limit 10 # Top 10 tasks by priority`
)
.action(async options => {
const logger = new Logger('task-list');
try {
const api = new TaskwerkAPI();
// Build query options
const queryOptions = {};
if (options.status) {
queryOptions.status = options.status;
}
if (options.priority) {
queryOptions.priority = options.priority;
}
if (options.assignee) {
queryOptions.assignee = options.assignee;
}
if (options.tags) {
queryOptions.tags = options.tags;
}
if (!options.all) {
queryOptions.limit = parseInt(options.limit) || 50;
}
// Map sort options
const sortField =
options.sort === 'created'
? 'created_at'
: options.sort === 'updated'
? 'updated_at'
: options.sort === 'priority'
? 'priority'
: 'created_at';
queryOptions.order_by = sortField;
queryOptions.order_dir = 'DESC';
// Get tasks - use search if search term provided
const tasks = options.search
? api.searchTasks(options.search, queryOptions)
: api.listTasks(queryOptions);
if (tasks.length === 0) {
console.log('š No tasks found');
console.log('\nTry creating a task first:');
console.log(' taskwerk task add "My task"');
return;
}
// Handle different output formats
if (options.format === 'json') {
console.log(JSON.stringify(tasks, null, 2));
return;
}
if (options.format === 'csv') {
console.log('ID,Name,Status,Priority,Assignee,Created');
tasks.forEach(task => {
console.log(
`${task.id},"${task.name}",${task.status},${task.priority},${task.assignee || ''},${task.created_at}`
);
});
return;
}
// Default table format
let header = `š Tasks`;
if (options.search) {
header = `š Search results for "${options.search}"`;
}
const filters = [];
if (options.status) {
filters.push(`status: ${options.status}`);
}
if (options.priority) {
filters.push(`priority: ${options.priority}`);
}
if (options.assignee) {
filters.push(`assignee: ${options.assignee}`);
}
if (options.tags) {
filters.push(`tags: ${options.tags.join(', ')}`);
}
if (filters.length > 0) {
header += ` (${filters.join(', ')})`;
}
console.log(header);
// Calculate column widths
const widths = calculateColumnWidths(tasks);
const totalWidth = Object.values(widths).reduce((sum, w) => sum + w, 0) + 5;
// Header row
console.log('ā'.repeat(totalWidth));
const headerCols = [
'ID'.padEnd(widths.id),
'Status'.padEnd(widths.status),
'Priority'.padEnd(widths.priority),
'Task'.padEnd(widths.name),
'Created'.padEnd(widths.created),
'Assignee'.padEnd(widths.assignee),
];
console.log(headerCols.join(' '));
console.log('ā'.repeat(totalWidth));
// Display tasks
if (options.tree) {
// Build hierarchical view
const treeTasks = buildTaskTree(tasks, api);
treeTasks.forEach(task => {
console.log(formatTableRow(task, widths, task._indent || 0));
});
} else {
// Regular flat view
tasks.forEach(task => {
console.log(formatTableRow(task, widths));
});
}
console.log('ā'.repeat(totalWidth));
console.log(`š Showing ${tasks.length} task${tasks.length !== 1 ? 's' : ''}`);
if (!options.all && tasks.length >= parseInt(options.limit)) {
console.log(`\nš” Use --all to see all tasks or --limit <number> to see more`);
}
} catch (error) {
logger.error('Failed to list tasks', error);
console.error('ā Failed to list tasks:', error.message);
process.exit(1);
}
});
return list;
}