@task-mcp/cli 1.0.29 → 1.0.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1168 -7
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -31,7 +31,17 @@ import {
|
|
|
31
31
|
priorityColors,
|
|
32
32
|
formatPriority,
|
|
33
33
|
formatDependencies,
|
|
34
|
-
banner
|
|
34
|
+
banner,
|
|
35
|
+
STATUS_SYMBOLS,
|
|
36
|
+
formatStatusSymbol,
|
|
37
|
+
PRIORITY_LEVELS,
|
|
38
|
+
formatPriorityBadge,
|
|
39
|
+
renderSection,
|
|
40
|
+
renderSections,
|
|
41
|
+
renderAlert,
|
|
42
|
+
renderStats,
|
|
43
|
+
formatDueDate,
|
|
44
|
+
formatTaskLine
|
|
35
45
|
} from "@task-mcp/shared";
|
|
36
46
|
import { truncateStr as truncateStr2 } from "@task-mcp/shared";
|
|
37
47
|
|
|
@@ -42,10 +52,17 @@ import {
|
|
|
42
52
|
} from "@task-mcp/shared";
|
|
43
53
|
|
|
44
54
|
// src/storage.ts
|
|
45
|
-
import {
|
|
55
|
+
import {
|
|
56
|
+
TaskStore,
|
|
57
|
+
InboxStore,
|
|
58
|
+
StateStore,
|
|
59
|
+
SessionStore,
|
|
60
|
+
getDefaultTasksDir
|
|
61
|
+
} from "@task-mcp/mcp-server/storage";
|
|
46
62
|
var _taskStore = null;
|
|
47
63
|
var _inboxStore = null;
|
|
48
64
|
var _stateStore = null;
|
|
65
|
+
var _sessionStore = null;
|
|
49
66
|
function getTaskStore() {
|
|
50
67
|
if (!_taskStore) {
|
|
51
68
|
_taskStore = new TaskStore;
|
|
@@ -64,6 +81,13 @@ function getStateStore() {
|
|
|
64
81
|
}
|
|
65
82
|
return _stateStore;
|
|
66
83
|
}
|
|
84
|
+
function getSessionStore() {
|
|
85
|
+
if (!_sessionStore) {
|
|
86
|
+
const taskStore = getTaskStore();
|
|
87
|
+
_sessionStore = new SessionStore(getDefaultTasksDir(), taskStore.currentWorkspace);
|
|
88
|
+
}
|
|
89
|
+
return _sessionStore;
|
|
90
|
+
}
|
|
67
91
|
function getCurrentWorkspace() {
|
|
68
92
|
return getTaskStore().currentWorkspace;
|
|
69
93
|
}
|
|
@@ -93,6 +117,149 @@ async function listInboxItems(status) {
|
|
|
93
117
|
async function getActiveTag() {
|
|
94
118
|
return getStateStore().getActiveTag();
|
|
95
119
|
}
|
|
120
|
+
function calculateStats(tasks) {
|
|
121
|
+
const stats = {
|
|
122
|
+
total: tasks.length,
|
|
123
|
+
completed: 0,
|
|
124
|
+
inProgress: 0,
|
|
125
|
+
pending: 0,
|
|
126
|
+
blocked: 0,
|
|
127
|
+
cancelled: 0,
|
|
128
|
+
byPriority: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
129
|
+
completionPercent: 0
|
|
130
|
+
};
|
|
131
|
+
for (const task of tasks) {
|
|
132
|
+
switch (task.status) {
|
|
133
|
+
case "completed":
|
|
134
|
+
stats.completed++;
|
|
135
|
+
break;
|
|
136
|
+
case "in_progress":
|
|
137
|
+
stats.inProgress++;
|
|
138
|
+
break;
|
|
139
|
+
case "pending":
|
|
140
|
+
stats.pending++;
|
|
141
|
+
break;
|
|
142
|
+
case "blocked":
|
|
143
|
+
stats.blocked++;
|
|
144
|
+
break;
|
|
145
|
+
case "cancelled":
|
|
146
|
+
stats.cancelled++;
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
stats.byPriority[task.priority]++;
|
|
150
|
+
}
|
|
151
|
+
const nonCancelled = stats.total - stats.cancelled;
|
|
152
|
+
stats.completionPercent = nonCancelled > 0 ? Math.round(stats.completed / nonCancelled * 100) : 0;
|
|
153
|
+
return stats;
|
|
154
|
+
}
|
|
155
|
+
function calculateDependencyMetrics(tasks) {
|
|
156
|
+
const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
|
|
157
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
158
|
+
const dependentCounts = {};
|
|
159
|
+
for (const task of activeTasks) {
|
|
160
|
+
for (const dep of task.dependencies ?? []) {
|
|
161
|
+
dependentCounts[dep.taskId] = (dependentCounts[dep.taskId] ?? 0) + 1;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
let mostDependedOn = null;
|
|
165
|
+
let maxCount = 0;
|
|
166
|
+
for (const [id, count] of Object.entries(dependentCounts)) {
|
|
167
|
+
if (count > maxCount) {
|
|
168
|
+
maxCount = count;
|
|
169
|
+
const task = tasks.find((t) => t.id === id);
|
|
170
|
+
if (task) {
|
|
171
|
+
mostDependedOn = { id, title: task.title, count };
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
const noDependencies = activeTasks.filter((t) => (t.dependencies?.length ?? 0) === 0).length;
|
|
176
|
+
const readyToWork = activeTasks.filter((t) => {
|
|
177
|
+
const deps = t.dependencies ?? [];
|
|
178
|
+
if (deps.length === 0)
|
|
179
|
+
return true;
|
|
180
|
+
return deps.every((d) => completedIds.has(d.taskId));
|
|
181
|
+
}).length;
|
|
182
|
+
const blockedByDependencies = activeTasks.length - readyToWork;
|
|
183
|
+
const totalDeps = activeTasks.reduce((sum, t) => sum + (t.dependencies?.length ?? 0), 0);
|
|
184
|
+
const avgDependencies = activeTasks.length > 0 ? totalDeps / activeTasks.length : 0;
|
|
185
|
+
return {
|
|
186
|
+
noDependencies,
|
|
187
|
+
readyToWork,
|
|
188
|
+
blockedByDependencies,
|
|
189
|
+
mostDependedOn,
|
|
190
|
+
avgDependencies: Math.round(avgDependencies * 10) / 10
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function suggestNextTask(tasks) {
|
|
194
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
195
|
+
const actionable = tasks.filter((t) => {
|
|
196
|
+
if (t.status !== "pending" && t.status !== "in_progress")
|
|
197
|
+
return false;
|
|
198
|
+
const deps = t.dependencies ?? [];
|
|
199
|
+
return deps.every((d) => completedIds.has(d.taskId));
|
|
200
|
+
});
|
|
201
|
+
if (actionable.length === 0)
|
|
202
|
+
return null;
|
|
203
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
204
|
+
actionable.sort((a, b) => {
|
|
205
|
+
const pDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
206
|
+
if (pDiff !== 0)
|
|
207
|
+
return pDiff;
|
|
208
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
209
|
+
});
|
|
210
|
+
return actionable[0] ?? null;
|
|
211
|
+
}
|
|
212
|
+
function formatTimeSince(isoTimestamp) {
|
|
213
|
+
const then = new Date(isoTimestamp).getTime();
|
|
214
|
+
const nowMs = Date.now();
|
|
215
|
+
const diffMs = nowMs - then;
|
|
216
|
+
const minutes = Math.floor(diffMs / (1000 * 60));
|
|
217
|
+
const hours = Math.floor(minutes / 60);
|
|
218
|
+
const days = Math.floor(hours / 24);
|
|
219
|
+
if (days > 0) {
|
|
220
|
+
return `${days}d ago`;
|
|
221
|
+
}
|
|
222
|
+
if (hours > 0) {
|
|
223
|
+
return `${hours}h ago`;
|
|
224
|
+
}
|
|
225
|
+
if (minutes > 0) {
|
|
226
|
+
return `${minutes}m ago`;
|
|
227
|
+
}
|
|
228
|
+
return "just now";
|
|
229
|
+
}
|
|
230
|
+
async function getLastSessionInfo() {
|
|
231
|
+
try {
|
|
232
|
+
const sessionStore = getSessionStore();
|
|
233
|
+
const lastSession = await sessionStore.getLastSession();
|
|
234
|
+
if (!lastSession || !lastSession.endedAt) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
const tasks = await listTasks();
|
|
238
|
+
const inProgressTasks = tasks.filter((t) => t.status === "in_progress");
|
|
239
|
+
const summary = lastSession.tasksSummary;
|
|
240
|
+
let currentInProgressTask;
|
|
241
|
+
if (inProgressTasks.length > 0) {
|
|
242
|
+
const sessionTaskIds = new Set(lastSession.tasksWorked);
|
|
243
|
+
const sessionInProgress = inProgressTasks.find((t) => sessionTaskIds.has(t.id));
|
|
244
|
+
const taskToShow = sessionInProgress ?? inProgressTasks[0];
|
|
245
|
+
if (taskToShow) {
|
|
246
|
+
currentInProgressTask = {
|
|
247
|
+
id: taskToShow.id,
|
|
248
|
+
title: taskToShow.title
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
session: lastSession,
|
|
254
|
+
timeSince: formatTimeSince(lastSession.endedAt),
|
|
255
|
+
completed: summary?.completed ?? 0,
|
|
256
|
+
inProgress: inProgressTasks.length,
|
|
257
|
+
currentInProgressTask
|
|
258
|
+
};
|
|
259
|
+
} catch {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
96
263
|
|
|
97
264
|
// src/commands/dashboard.ts
|
|
98
265
|
async function dashboard(workspaceName) {
|
|
@@ -141,6 +308,552 @@ async function dashboard(workspaceName) {
|
|
|
141
308
|
console.log(output);
|
|
142
309
|
}
|
|
143
310
|
|
|
311
|
+
// src/commands/smart-dashboard.ts
|
|
312
|
+
function getOverdueTasks(tasks) {
|
|
313
|
+
const today = new Date;
|
|
314
|
+
today.setHours(0, 0, 0, 0);
|
|
315
|
+
return tasks.filter((t) => {
|
|
316
|
+
if (t.status === "completed" || t.status === "cancelled")
|
|
317
|
+
return false;
|
|
318
|
+
if (!t.dueDate)
|
|
319
|
+
return false;
|
|
320
|
+
const due = new Date(t.dueDate);
|
|
321
|
+
due.setHours(0, 0, 0, 0);
|
|
322
|
+
return due < today;
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
function getStaleBlockedTasks(tasks, daysThreshold = 3) {
|
|
326
|
+
const now = Date.now();
|
|
327
|
+
const thresholdMs = daysThreshold * 24 * 60 * 60 * 1000;
|
|
328
|
+
return tasks.filter((t) => {
|
|
329
|
+
if (t.status !== "blocked")
|
|
330
|
+
return false;
|
|
331
|
+
const createdAt = new Date(t.createdAt).getTime();
|
|
332
|
+
return now - createdAt > thresholdMs;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
function getDueSoonTasks(tasks) {
|
|
336
|
+
const today = new Date;
|
|
337
|
+
today.setHours(0, 0, 0, 0);
|
|
338
|
+
const tomorrow = new Date(today);
|
|
339
|
+
tomorrow.setDate(tomorrow.getDate() + 1);
|
|
340
|
+
const dayAfter = new Date(today);
|
|
341
|
+
dayAfter.setDate(dayAfter.getDate() + 2);
|
|
342
|
+
return tasks.filter((t) => {
|
|
343
|
+
if (t.status === "completed" || t.status === "cancelled")
|
|
344
|
+
return false;
|
|
345
|
+
if (!t.dueDate)
|
|
346
|
+
return false;
|
|
347
|
+
const due = new Date(t.dueDate);
|
|
348
|
+
due.setHours(0, 0, 0, 0);
|
|
349
|
+
return due >= today && due < dayAfter;
|
|
350
|
+
}).sort((a, b) => {
|
|
351
|
+
const dateA = new Date(a.dueDate).getTime();
|
|
352
|
+
const dateB = new Date(b.dueDate).getTime();
|
|
353
|
+
return dateA - dateB;
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function getSuggestionReason(task, tasks) {
|
|
357
|
+
const reasons = [];
|
|
358
|
+
const blockingCount = tasks.filter((t) => t.dependencies?.some((d) => d.taskId === task.id)).length;
|
|
359
|
+
if (blockingCount > 0) {
|
|
360
|
+
reasons.push(`Unblocks ${blockingCount} task${blockingCount > 1 ? "s" : ""}`);
|
|
361
|
+
}
|
|
362
|
+
if (task.priority === "critical") {
|
|
363
|
+
reasons.push("Critical priority");
|
|
364
|
+
} else if (task.priority === "high") {
|
|
365
|
+
reasons.push("High priority");
|
|
366
|
+
}
|
|
367
|
+
if (task.dueDate) {
|
|
368
|
+
const today = new Date;
|
|
369
|
+
today.setHours(0, 0, 0, 0);
|
|
370
|
+
const due = new Date(task.dueDate);
|
|
371
|
+
due.setHours(0, 0, 0, 0);
|
|
372
|
+
const diffDays = Math.floor((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
373
|
+
if (diffDays < 0) {
|
|
374
|
+
reasons.push("Overdue");
|
|
375
|
+
} else if (diffDays === 0) {
|
|
376
|
+
reasons.push("Due today");
|
|
377
|
+
} else if (diffDays === 1) {
|
|
378
|
+
reasons.push("Due tomorrow");
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
if (task.status === "in_progress") {
|
|
382
|
+
reasons.push("Already started");
|
|
383
|
+
}
|
|
384
|
+
return reasons.length > 0 ? reasons.join(", ") : "Ready to start";
|
|
385
|
+
}
|
|
386
|
+
function taskToSectionItem(task, options = {}) {
|
|
387
|
+
const prefix = formatStatusSymbol(task.status);
|
|
388
|
+
const idStr = c.gray(task.id.slice(-4));
|
|
389
|
+
const title = truncateStr(task.title, 35);
|
|
390
|
+
const primary = `${idStr} ${title}`;
|
|
391
|
+
const suffixParts = [];
|
|
392
|
+
if (task.priority) {
|
|
393
|
+
suffixParts.push(formatPriorityBadge(task.priority));
|
|
394
|
+
}
|
|
395
|
+
if (task.dueDate) {
|
|
396
|
+
suffixParts.push(formatDueDate(task.dueDate));
|
|
397
|
+
}
|
|
398
|
+
const details = [];
|
|
399
|
+
if (options.showReason) {
|
|
400
|
+
details.push(options.showReason);
|
|
401
|
+
}
|
|
402
|
+
return {
|
|
403
|
+
primary,
|
|
404
|
+
prefix,
|
|
405
|
+
suffix: suffixParts.join(" "),
|
|
406
|
+
details
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
function renderHeader(workspace, stats) {
|
|
410
|
+
const lines = [];
|
|
411
|
+
const title = `task-mcp`;
|
|
412
|
+
const wsName = c.cyan(workspace);
|
|
413
|
+
const bar = progressBar(stats.completed, stats.total - stats.cancelled, {
|
|
414
|
+
width: 10,
|
|
415
|
+
showPercent: false
|
|
416
|
+
});
|
|
417
|
+
lines.push(c.gray(BOX.rTopLeft + hline(60, BOX.horizontal) + BOX.rTopRight));
|
|
418
|
+
lines.push(c.gray(BOX.vertical) + ` ${c.bold(title)} ${c.gray("@")} ${wsName}` + " ".repeat(20) + `Health: ${bar} ${stats.completionPercent}%` + " " + c.gray(BOX.vertical));
|
|
419
|
+
lines.push(c.gray(BOX.rBottomLeft + hline(60, BOX.horizontal) + BOX.rBottomRight));
|
|
420
|
+
return lines;
|
|
421
|
+
}
|
|
422
|
+
function renderNextAction(tasks) {
|
|
423
|
+
const nextTask = suggestNextTask(tasks);
|
|
424
|
+
if (!nextTask) {
|
|
425
|
+
return renderSection("Next Action", [], {
|
|
426
|
+
icon: "\uD83C\uDFAF",
|
|
427
|
+
emptyMessage: "No actionable tasks"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
const reason = getSuggestionReason(nextTask, tasks);
|
|
431
|
+
const item = taskToSectionItem(nextTask, { showReason: reason });
|
|
432
|
+
return renderSection("Next Action", [item], { icon: "\uD83C\uDFAF" });
|
|
433
|
+
}
|
|
434
|
+
function renderAttention(tasks) {
|
|
435
|
+
const alerts = [];
|
|
436
|
+
const overdue = getOverdueTasks(tasks);
|
|
437
|
+
if (overdue.length > 0) {
|
|
438
|
+
alerts.push(c.red(`${overdue.length} task${overdue.length > 1 ? "s" : ""} overdue`));
|
|
439
|
+
}
|
|
440
|
+
const staleBlocked = getStaleBlockedTasks(tasks, 3);
|
|
441
|
+
if (staleBlocked.length > 0) {
|
|
442
|
+
alerts.push(c.yellow(`${staleBlocked.length} task${staleBlocked.length > 1 ? "s" : ""} blocked > 3 days`));
|
|
443
|
+
}
|
|
444
|
+
const blockedCount = tasks.filter((t) => t.status === "blocked").length;
|
|
445
|
+
if (blockedCount > 0 && staleBlocked.length === 0) {
|
|
446
|
+
alerts.push(c.yellow(`${blockedCount} task${blockedCount > 1 ? "s" : ""} blocked`));
|
|
447
|
+
}
|
|
448
|
+
const inProgressCount = tasks.filter((t) => t.status === "in_progress").length;
|
|
449
|
+
if (inProgressCount > 0) {
|
|
450
|
+
alerts.push(c.cyan(`${inProgressCount} task${inProgressCount > 1 ? "s" : ""} in progress`));
|
|
451
|
+
}
|
|
452
|
+
if (alerts.length === 0) {
|
|
453
|
+
return renderSection("Attention", [], {
|
|
454
|
+
icon: "\u26A0",
|
|
455
|
+
emptyMessage: c.green("All clear!"),
|
|
456
|
+
headerColor: "yellow"
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
const items = alerts.map((alert) => ({
|
|
460
|
+
primary: alert,
|
|
461
|
+
prefix: c.gray("\u2022")
|
|
462
|
+
}));
|
|
463
|
+
return renderSection("Attention", items, {
|
|
464
|
+
icon: "\u26A0",
|
|
465
|
+
headerColor: "yellow"
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function renderDueSoon(tasks) {
|
|
469
|
+
const dueSoon = getDueSoonTasks(tasks);
|
|
470
|
+
if (dueSoon.length === 0) {
|
|
471
|
+
return renderSection("Due Soon", [], {
|
|
472
|
+
icon: "\uD83D\uDCC5",
|
|
473
|
+
emptyMessage: "No upcoming deadlines"
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
const items = dueSoon.slice(0, 5).map((task) => taskToSectionItem(task));
|
|
477
|
+
return renderSection("Due Soon", items, {
|
|
478
|
+
icon: "\uD83D\uDCC5",
|
|
479
|
+
showCount: dueSoon.length > 5
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function renderQuickStats(stats) {
|
|
483
|
+
const statsRow = renderStats([
|
|
484
|
+
{ label: "Total", value: stats.total },
|
|
485
|
+
{ label: "Done", value: `${stats.completed} (${stats.completionPercent}%)`, color: "green" },
|
|
486
|
+
{ label: "In Progress", value: stats.inProgress, color: "cyan" },
|
|
487
|
+
{ label: "Blocked", value: stats.blocked, color: stats.blocked > 0 ? "red" : "gray" }
|
|
488
|
+
]);
|
|
489
|
+
return ["", c.gray(statsRow)];
|
|
490
|
+
}
|
|
491
|
+
function renderLastSession(sessionInfo) {
|
|
492
|
+
if (!sessionInfo) {
|
|
493
|
+
return [];
|
|
494
|
+
}
|
|
495
|
+
const lines = [];
|
|
496
|
+
const header = `${c.gray("Last Session:")} ${c.cyan(sessionInfo.timeSince)}`;
|
|
497
|
+
lines.push(` ${header}`);
|
|
498
|
+
const summaryParts = [];
|
|
499
|
+
if (sessionInfo.completed > 0) {
|
|
500
|
+
summaryParts.push(`${sessionInfo.completed} completed`);
|
|
501
|
+
}
|
|
502
|
+
if (sessionInfo.inProgress > 0) {
|
|
503
|
+
summaryParts.push(`${sessionInfo.inProgress} in progress`);
|
|
504
|
+
}
|
|
505
|
+
if (summaryParts.length > 0) {
|
|
506
|
+
lines.push(` ${c.gray("->")} ${summaryParts.join(", ")}`);
|
|
507
|
+
}
|
|
508
|
+
if (sessionInfo.currentInProgressTask) {
|
|
509
|
+
const taskId = c.gray(sessionInfo.currentInProgressTask.id.slice(-8));
|
|
510
|
+
const title = truncateStr(sessionInfo.currentInProgressTask.title, 40);
|
|
511
|
+
lines.push(` ${c.gray("->")} ${taskId}: ${title} ${c.cyan("(in_progress)")}`);
|
|
512
|
+
}
|
|
513
|
+
return lines;
|
|
514
|
+
}
|
|
515
|
+
async function smartDashboard() {
|
|
516
|
+
const workspaces = await listWorkspaces();
|
|
517
|
+
if (workspaces.length === 0) {
|
|
518
|
+
console.log(c.yellow("No tasks found. Create a task first using the MCP server."));
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const workspace = getCurrentWorkspace();
|
|
522
|
+
const tasks = await listTasks();
|
|
523
|
+
const stats = calculateStats(tasks);
|
|
524
|
+
const lastSessionInfo = await getLastSessionInfo();
|
|
525
|
+
const lines = [];
|
|
526
|
+
lines.push(...renderHeader(workspace, stats));
|
|
527
|
+
lines.push("");
|
|
528
|
+
const lastSessionLines = renderLastSession(lastSessionInfo);
|
|
529
|
+
if (lastSessionLines.length > 0) {
|
|
530
|
+
lines.push(...lastSessionLines);
|
|
531
|
+
lines.push("");
|
|
532
|
+
}
|
|
533
|
+
lines.push(...renderNextAction(tasks));
|
|
534
|
+
lines.push("");
|
|
535
|
+
const attentionLines = renderAttention(tasks);
|
|
536
|
+
if (attentionLines.length > 2) {
|
|
537
|
+
lines.push(...attentionLines);
|
|
538
|
+
lines.push("");
|
|
539
|
+
}
|
|
540
|
+
lines.push(...renderDueSoon(tasks));
|
|
541
|
+
lines.push(...renderQuickStats(stats));
|
|
542
|
+
lines.push("");
|
|
543
|
+
lines.push(c.gray(" Run: task next | task health | task blocked | task tree"));
|
|
544
|
+
console.log(lines.join(`
|
|
545
|
+
`));
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/commands/next.ts
|
|
549
|
+
var STRATEGY_DESCRIPTIONS = {
|
|
550
|
+
balanced: "Balance priority, dependencies, and due dates",
|
|
551
|
+
quick_wins: "Focus on quick, low-effort tasks",
|
|
552
|
+
unblock: "Prioritize tasks that unblock others",
|
|
553
|
+
critical_path: "Focus on critical path tasks"
|
|
554
|
+
};
|
|
555
|
+
function getDependentTasks(task, tasks) {
|
|
556
|
+
return tasks.filter((t) => t.dependencies?.some((d) => d.taskId === task.id));
|
|
557
|
+
}
|
|
558
|
+
function suggestByStrategy(tasks, strategy) {
|
|
559
|
+
const completedIds = new Set(tasks.filter((t) => t.status === "completed").map((t) => t.id));
|
|
560
|
+
const actionable = tasks.filter((t) => {
|
|
561
|
+
if (t.status !== "pending" && t.status !== "in_progress")
|
|
562
|
+
return false;
|
|
563
|
+
const deps = t.dependencies ?? [];
|
|
564
|
+
return deps.every((d) => completedIds.has(d.taskId));
|
|
565
|
+
});
|
|
566
|
+
if (actionable.length === 0)
|
|
567
|
+
return null;
|
|
568
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
569
|
+
switch (strategy) {
|
|
570
|
+
case "quick_wins":
|
|
571
|
+
actionable.sort((a, b) => {
|
|
572
|
+
const estA = a.estimate?.expected ?? 60;
|
|
573
|
+
const estB = b.estimate?.expected ?? 60;
|
|
574
|
+
if (estA !== estB)
|
|
575
|
+
return estA - estB;
|
|
576
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
577
|
+
});
|
|
578
|
+
break;
|
|
579
|
+
case "unblock":
|
|
580
|
+
actionable.sort((a, b) => {
|
|
581
|
+
const depsA = getDependentTasks(a, tasks).length;
|
|
582
|
+
const depsB = getDependentTasks(b, tasks).length;
|
|
583
|
+
if (depsA !== depsB)
|
|
584
|
+
return depsB - depsA;
|
|
585
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
586
|
+
});
|
|
587
|
+
break;
|
|
588
|
+
case "critical_path":
|
|
589
|
+
actionable.sort((a, b) => {
|
|
590
|
+
const pDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
591
|
+
if (pDiff !== 0)
|
|
592
|
+
return pDiff;
|
|
593
|
+
const depsA = getDependentTasks(a, tasks).length;
|
|
594
|
+
const depsB = getDependentTasks(b, tasks).length;
|
|
595
|
+
return depsB - depsA;
|
|
596
|
+
});
|
|
597
|
+
break;
|
|
598
|
+
case "balanced":
|
|
599
|
+
default:
|
|
600
|
+
actionable.sort((a, b) => {
|
|
601
|
+
const pDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
602
|
+
if (pDiff !== 0)
|
|
603
|
+
return pDiff;
|
|
604
|
+
const dueA = a.dueDate ? new Date(a.dueDate).getTime() : Infinity;
|
|
605
|
+
const dueB = b.dueDate ? new Date(b.dueDate).getTime() : Infinity;
|
|
606
|
+
if (dueA !== dueB)
|
|
607
|
+
return dueA - dueB;
|
|
608
|
+
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime();
|
|
609
|
+
});
|
|
610
|
+
break;
|
|
611
|
+
}
|
|
612
|
+
return actionable[0] ?? null;
|
|
613
|
+
}
|
|
614
|
+
function buildReasons(task, tasks) {
|
|
615
|
+
const reasons = [];
|
|
616
|
+
if (task.priority === "critical") {
|
|
617
|
+
reasons.push("\uD83D\uDD34 Critical priority");
|
|
618
|
+
} else if (task.priority === "high") {
|
|
619
|
+
reasons.push("\uD83D\uDFE0 High priority");
|
|
620
|
+
}
|
|
621
|
+
if (task.dueDate) {
|
|
622
|
+
const today = new Date;
|
|
623
|
+
today.setHours(0, 0, 0, 0);
|
|
624
|
+
const due = new Date(task.dueDate);
|
|
625
|
+
due.setHours(0, 0, 0, 0);
|
|
626
|
+
const diffDays = Math.floor((due.getTime() - today.getTime()) / (1000 * 60 * 60 * 24));
|
|
627
|
+
if (diffDays < 0) {
|
|
628
|
+
reasons.push(`\u26A0\uFE0F Overdue by ${Math.abs(diffDays)} day${Math.abs(diffDays) > 1 ? "s" : ""}`);
|
|
629
|
+
} else if (diffDays === 0) {
|
|
630
|
+
reasons.push("\uD83D\uDCC5 Due today");
|
|
631
|
+
} else if (diffDays === 1) {
|
|
632
|
+
reasons.push("\uD83D\uDCC5 Due tomorrow");
|
|
633
|
+
} else if (diffDays <= 3) {
|
|
634
|
+
reasons.push(`\uD83D\uDCC5 Due in ${diffDays} days`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
const dependents = getDependentTasks(task, tasks);
|
|
638
|
+
if (dependents.length > 0) {
|
|
639
|
+
reasons.push(`\uD83D\uDD13 Unblocks ${dependents.length} task${dependents.length > 1 ? "s" : ""}`);
|
|
640
|
+
}
|
|
641
|
+
if (task.status === "in_progress") {
|
|
642
|
+
reasons.push("\u25B6\uFE0F Already in progress");
|
|
643
|
+
}
|
|
644
|
+
if (!task.dependencies || task.dependencies.length === 0) {
|
|
645
|
+
reasons.push("\u2705 No blockers");
|
|
646
|
+
}
|
|
647
|
+
if (reasons.length === 0) {
|
|
648
|
+
reasons.push("Ready to start");
|
|
649
|
+
}
|
|
650
|
+
return reasons;
|
|
651
|
+
}
|
|
652
|
+
function renderTaskDetail(task, _tasks) {
|
|
653
|
+
const lines = [];
|
|
654
|
+
const status = formatStatusSymbol(task.status);
|
|
655
|
+
const priority = formatPriorityBadge(task.priority);
|
|
656
|
+
lines.push("");
|
|
657
|
+
lines.push(` ${status} ${c.bold(task.title)} ${priority}`);
|
|
658
|
+
lines.push("");
|
|
659
|
+
const details = [
|
|
660
|
+
["ID", c.gray(task.id)],
|
|
661
|
+
["Status", task.status],
|
|
662
|
+
["Priority", task.priority]
|
|
663
|
+
];
|
|
664
|
+
if (task.dueDate) {
|
|
665
|
+
details.push(["Due", formatDueDate(task.dueDate)]);
|
|
666
|
+
}
|
|
667
|
+
if (task.estimate?.expected) {
|
|
668
|
+
const hours = Math.floor(task.estimate.expected / 60);
|
|
669
|
+
const mins = task.estimate.expected % 60;
|
|
670
|
+
const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
671
|
+
details.push(["Estimate", timeStr]);
|
|
672
|
+
}
|
|
673
|
+
if (task.tags && task.tags.length > 0) {
|
|
674
|
+
details.push(["Tags", task.tags.map((t) => c.cyan(`#${t}`)).join(" ")]);
|
|
675
|
+
}
|
|
676
|
+
for (const [label, value] of details) {
|
|
677
|
+
lines.push(` ${c.gray(label + ":")} ${value}`);
|
|
678
|
+
}
|
|
679
|
+
if (task.description) {
|
|
680
|
+
lines.push("");
|
|
681
|
+
lines.push(` ${c.gray("Description:")}`);
|
|
682
|
+
const descLines = task.description.split(`
|
|
683
|
+
`).slice(0, 3);
|
|
684
|
+
for (const line of descLines) {
|
|
685
|
+
lines.push(` ${c.dim(line)}`);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return lines;
|
|
689
|
+
}
|
|
690
|
+
function renderImpact(task, tasks) {
|
|
691
|
+
const dependents = getDependentTasks(task, tasks);
|
|
692
|
+
if (dependents.length === 0) {
|
|
693
|
+
return [];
|
|
694
|
+
}
|
|
695
|
+
const items = dependents.slice(0, 5).map((t) => ({
|
|
696
|
+
primary: `${c.gray(t.id.slice(-4))} ${t.title}`,
|
|
697
|
+
prefix: formatStatusSymbol(t.status)
|
|
698
|
+
}));
|
|
699
|
+
return [
|
|
700
|
+
"",
|
|
701
|
+
...renderSection("Will Unblock", items, {
|
|
702
|
+
icon: "\uD83D\uDD13",
|
|
703
|
+
showCount: dependents.length > 5
|
|
704
|
+
})
|
|
705
|
+
];
|
|
706
|
+
}
|
|
707
|
+
function renderAlternatives(currentStrategy) {
|
|
708
|
+
const others = Object.keys(STRATEGY_DESCRIPTIONS).filter((s) => s !== currentStrategy);
|
|
709
|
+
const lines = ["", c.gray(" Alternative strategies:")];
|
|
710
|
+
for (const strategy of others) {
|
|
711
|
+
lines.push(` ${c.cyan(`task next --strategy ${strategy}`)}`);
|
|
712
|
+
}
|
|
713
|
+
return lines;
|
|
714
|
+
}
|
|
715
|
+
async function nextCmd(options = {}) {
|
|
716
|
+
const strategy = options.strategy ?? "balanced";
|
|
717
|
+
const tasks = await listTasks();
|
|
718
|
+
const nextTask = suggestByStrategy(tasks, strategy);
|
|
719
|
+
if (!nextTask) {
|
|
720
|
+
console.log(c.yellow("No actionable tasks found."));
|
|
721
|
+
console.log(c.gray("All tasks may be completed, blocked, or have unmet dependencies."));
|
|
722
|
+
return;
|
|
723
|
+
}
|
|
724
|
+
const reasons = buildReasons(nextTask, tasks);
|
|
725
|
+
const lines = [];
|
|
726
|
+
lines.push(c.cyan(c.bold("\uD83C\uDFAF Suggested Next Task")));
|
|
727
|
+
lines.push(c.gray(hline(50)));
|
|
728
|
+
lines.push(...renderTaskDetail(nextTask, tasks));
|
|
729
|
+
lines.push("");
|
|
730
|
+
lines.push(c.gray(" Why this task:"));
|
|
731
|
+
for (const reason of reasons) {
|
|
732
|
+
lines.push(` ${reason}`);
|
|
733
|
+
}
|
|
734
|
+
lines.push(...renderImpact(nextTask, tasks));
|
|
735
|
+
lines.push("");
|
|
736
|
+
lines.push(c.gray(` Strategy: ${strategy} - ${STRATEGY_DESCRIPTIONS[strategy]}`));
|
|
737
|
+
lines.push(...renderAlternatives(strategy));
|
|
738
|
+
console.log(lines.join(`
|
|
739
|
+
`));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
// src/commands/tree.ts
|
|
743
|
+
function buildTree(tasks) {
|
|
744
|
+
const taskMap = new Map;
|
|
745
|
+
for (const task of tasks) {
|
|
746
|
+
taskMap.set(task.id, task);
|
|
747
|
+
}
|
|
748
|
+
const nodeMap = new Map;
|
|
749
|
+
for (const task of tasks) {
|
|
750
|
+
nodeMap.set(task.id, { task, children: [] });
|
|
751
|
+
}
|
|
752
|
+
const roots = [];
|
|
753
|
+
for (const task of tasks) {
|
|
754
|
+
const node = nodeMap.get(task.id);
|
|
755
|
+
if (task.parentId && nodeMap.has(task.parentId)) {
|
|
756
|
+
const parent = nodeMap.get(task.parentId);
|
|
757
|
+
parent.children.push(node);
|
|
758
|
+
} else {
|
|
759
|
+
roots.push(node);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const sortNodes = (nodes) => {
|
|
763
|
+
nodes.sort((a, b) => {
|
|
764
|
+
const orderA = a.task.sortOrder ?? Infinity;
|
|
765
|
+
const orderB = b.task.sortOrder ?? Infinity;
|
|
766
|
+
if (orderA !== orderB)
|
|
767
|
+
return orderA - orderB;
|
|
768
|
+
return new Date(a.task.createdAt).getTime() - new Date(b.task.createdAt).getTime();
|
|
769
|
+
});
|
|
770
|
+
for (const node of nodes) {
|
|
771
|
+
sortNodes(node.children);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
sortNodes(roots);
|
|
775
|
+
return roots;
|
|
776
|
+
}
|
|
777
|
+
function countProgress(node) {
|
|
778
|
+
let completed = node.task.status === "completed" ? 1 : 0;
|
|
779
|
+
let total = 1;
|
|
780
|
+
for (const child of node.children) {
|
|
781
|
+
const childProgress = countProgress(child);
|
|
782
|
+
completed += childProgress.completed;
|
|
783
|
+
total += childProgress.total;
|
|
784
|
+
}
|
|
785
|
+
return { completed, total };
|
|
786
|
+
}
|
|
787
|
+
function formatTaskLine2(task, progress) {
|
|
788
|
+
const status = formatStatusSymbol(task.status);
|
|
789
|
+
const shortId = c.gray(task.id.slice(-4));
|
|
790
|
+
const priority = task.priority !== "medium" ? formatPriorityBadge(task.priority) : "";
|
|
791
|
+
let progressStr = "";
|
|
792
|
+
if (progress && progress.total > 1) {
|
|
793
|
+
const childCompleted = progress.completed - (task.status === "completed" ? 1 : 0);
|
|
794
|
+
const childTotal = progress.total - 1;
|
|
795
|
+
if (childTotal > 0) {
|
|
796
|
+
const pct = Math.round(childCompleted / childTotal * 100);
|
|
797
|
+
progressStr = c.gray(` (${childCompleted}/${childTotal})`);
|
|
798
|
+
if (pct === 100) {
|
|
799
|
+
progressStr = c.green(` (${childCompleted}/${childTotal} \u2713)`);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
if (task.status === "completed" || task.status === "cancelled") {
|
|
804
|
+
const title2 = c.gray(task.title);
|
|
805
|
+
return `${status} ${shortId} ${title2}${progressStr}`;
|
|
806
|
+
}
|
|
807
|
+
const title = task.title;
|
|
808
|
+
return `${status} ${shortId} ${title}${progressStr}${priority ? " " + priority : ""}`;
|
|
809
|
+
}
|
|
810
|
+
function renderNode(node, prefix, isLast, isRoot, lines) {
|
|
811
|
+
const branch = isRoot ? "" : isLast ? "\\-- " : "|-- ";
|
|
812
|
+
const continuation = isLast ? " " : "| ";
|
|
813
|
+
const progress = node.children.length > 0 ? countProgress(node) : undefined;
|
|
814
|
+
const taskLine = formatTaskLine2(node.task, progress);
|
|
815
|
+
lines.push(`${prefix}${branch}${taskLine}`);
|
|
816
|
+
const childPrefix = isRoot ? prefix : prefix + continuation;
|
|
817
|
+
const childCount = node.children.length;
|
|
818
|
+
for (let i = 0;i < childCount; i++) {
|
|
819
|
+
const child = node.children[i];
|
|
820
|
+
const childIsLast = i === childCount - 1;
|
|
821
|
+
renderNode(child, childPrefix, childIsLast, false, lines);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
function renderTree(roots) {
|
|
825
|
+
const lines = [];
|
|
826
|
+
for (const root of roots) {
|
|
827
|
+
renderNode(root, "", true, true, lines);
|
|
828
|
+
if (roots.indexOf(root) < roots.length - 1) {
|
|
829
|
+
lines.push("");
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return lines;
|
|
833
|
+
}
|
|
834
|
+
async function treeCmd(options = {}) {
|
|
835
|
+
const allTasks = await listTasks();
|
|
836
|
+
const tasks = options.hideCompleted ? allTasks.filter((t) => t.status !== "completed" && t.status !== "cancelled") : allTasks;
|
|
837
|
+
if (tasks.length === 0) {
|
|
838
|
+
console.log(c.yellow("No tasks found."));
|
|
839
|
+
if (options.hideCompleted) {
|
|
840
|
+
console.log(c.gray("Use without --hide-completed to include completed tasks."));
|
|
841
|
+
}
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
const roots = buildTree(tasks);
|
|
845
|
+
const lines = [];
|
|
846
|
+
lines.push(c.cyan(c.bold("Task Hierarchy")));
|
|
847
|
+
lines.push(c.gray(hline(50)));
|
|
848
|
+
lines.push("");
|
|
849
|
+
lines.push(...renderTree(roots));
|
|
850
|
+
lines.push("");
|
|
851
|
+
lines.push(c.gray(hline(50)));
|
|
852
|
+
lines.push(c.gray("Legend: [ ] pending [~] progress [x] done [!] blocked"));
|
|
853
|
+
console.log(lines.join(`
|
|
854
|
+
`));
|
|
855
|
+
}
|
|
856
|
+
|
|
144
857
|
// src/constants.ts
|
|
145
858
|
var MENU_SEPARATOR_WIDTH = 40;
|
|
146
859
|
var LIST_SEPARATOR_WIDTH = 50;
|
|
@@ -250,14 +963,26 @@ Workspaces (${workspaces.length})
|
|
|
250
963
|
|
|
251
964
|
// src/interactive.ts
|
|
252
965
|
import * as readline from "readline";
|
|
253
|
-
async function prompt(question) {
|
|
966
|
+
async function prompt(question, options) {
|
|
254
967
|
const rl = readline.createInterface({
|
|
255
968
|
input: process.stdin,
|
|
256
969
|
output: process.stdout
|
|
257
970
|
});
|
|
258
971
|
return new Promise((resolve) => {
|
|
259
|
-
|
|
972
|
+
let timeoutId;
|
|
973
|
+
const cleanup = () => {
|
|
974
|
+
if (timeoutId)
|
|
975
|
+
clearTimeout(timeoutId);
|
|
260
976
|
rl.close();
|
|
977
|
+
};
|
|
978
|
+
if (options?.timeoutMs !== undefined) {
|
|
979
|
+
timeoutId = setTimeout(() => {
|
|
980
|
+
cleanup();
|
|
981
|
+
resolve(options.defaultValue ?? "");
|
|
982
|
+
}, options.timeoutMs);
|
|
983
|
+
}
|
|
984
|
+
rl.question(question, (answer) => {
|
|
985
|
+
cleanup();
|
|
261
986
|
resolve(answer.trim());
|
|
262
987
|
});
|
|
263
988
|
});
|
|
@@ -585,10 +1310,407 @@ async function inboxCountCmd() {
|
|
|
585
1310
|
}
|
|
586
1311
|
console.log();
|
|
587
1312
|
}
|
|
1313
|
+
|
|
1314
|
+
// src/commands/blocked.ts
|
|
1315
|
+
function calculateDaysBlocked(task) {
|
|
1316
|
+
const blockedSince = task.updatedAt ?? task.createdAt;
|
|
1317
|
+
const blockedDate = new Date(blockedSince);
|
|
1318
|
+
const now = new Date;
|
|
1319
|
+
const diffMs = now.getTime() - blockedDate.getTime();
|
|
1320
|
+
return Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
1321
|
+
}
|
|
1322
|
+
function getBlockerTasks(task, allTasks) {
|
|
1323
|
+
if (!task.dependencies || task.dependencies.length === 0)
|
|
1324
|
+
return [];
|
|
1325
|
+
const blockers = [];
|
|
1326
|
+
const completedStatuses = new Set(["completed", "cancelled"]);
|
|
1327
|
+
for (const dep of task.dependencies) {
|
|
1328
|
+
const blockerTask = allTasks.find((t) => t.id === dep.taskId);
|
|
1329
|
+
if (blockerTask && !completedStatuses.has(blockerTask.status)) {
|
|
1330
|
+
blockers.push(blockerTask);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
return blockers;
|
|
1334
|
+
}
|
|
1335
|
+
function getBlockReason(blockers) {
|
|
1336
|
+
if (blockers.length === 0) {
|
|
1337
|
+
return "Unknown (no active blockers found)";
|
|
1338
|
+
}
|
|
1339
|
+
const inProgress = blockers.filter((t) => t.status === "in_progress");
|
|
1340
|
+
const pending = blockers.filter((t) => t.status === "pending");
|
|
1341
|
+
const blocked = blockers.filter((t) => t.status === "blocked");
|
|
1342
|
+
const reasons = [];
|
|
1343
|
+
if (inProgress.length > 0) {
|
|
1344
|
+
reasons.push(`${inProgress.length} in progress`);
|
|
1345
|
+
}
|
|
1346
|
+
if (pending.length > 0) {
|
|
1347
|
+
reasons.push(`${pending.length} pending`);
|
|
1348
|
+
}
|
|
1349
|
+
if (blocked.length > 0) {
|
|
1350
|
+
reasons.push(`${blocked.length} blocked (chain)`);
|
|
1351
|
+
}
|
|
1352
|
+
return `Dependency tasks: ${reasons.join(", ")}`;
|
|
1353
|
+
}
|
|
1354
|
+
function analyzeBlockedTasks(tasks) {
|
|
1355
|
+
const blockedTasks = tasks.filter((t) => t.status === "blocked");
|
|
1356
|
+
const result = [];
|
|
1357
|
+
for (const task of blockedTasks) {
|
|
1358
|
+
const blockers = getBlockerTasks(task, tasks);
|
|
1359
|
+
const daysBlocked = calculateDaysBlocked(task);
|
|
1360
|
+
const isStale = daysBlocked >= 3;
|
|
1361
|
+
result.push({
|
|
1362
|
+
task,
|
|
1363
|
+
blockers,
|
|
1364
|
+
reason: getBlockReason(blockers),
|
|
1365
|
+
daysBlocked,
|
|
1366
|
+
isStale
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
1370
|
+
result.sort((a, b) => {
|
|
1371
|
+
const pDiff = priorityOrder[a.task.priority] - priorityOrder[b.task.priority];
|
|
1372
|
+
if (pDiff !== 0)
|
|
1373
|
+
return pDiff;
|
|
1374
|
+
return b.daysBlocked - a.daysBlocked;
|
|
1375
|
+
});
|
|
1376
|
+
return result;
|
|
1377
|
+
}
|
|
1378
|
+
function renderBlockedTask(info) {
|
|
1379
|
+
const lines = [];
|
|
1380
|
+
const { task, blockers, reason, daysBlocked, isStale } = info;
|
|
1381
|
+
const staleIndicator = isStale ? c.red("[!]") : c.yellow("[B]");
|
|
1382
|
+
const taskId = c.gray(task.id.slice(-6));
|
|
1383
|
+
const priority = formatPriorityBadge(task.priority);
|
|
1384
|
+
const title = isStale ? c.red(task.title) : task.title;
|
|
1385
|
+
lines.push(` ${staleIndicator} ${taskId} ${title} ${priority}`);
|
|
1386
|
+
if (blockers.length > 0) {
|
|
1387
|
+
const blockerIds = blockers.map((b) => c.cyan(b.id.slice(-6))).join(", ");
|
|
1388
|
+
lines.push(` ${c.gray("blocked_by:")} ${blockerIds}`);
|
|
1389
|
+
}
|
|
1390
|
+
lines.push(` ${c.dim("->")} ${c.dim("Reason:")} ${c.dim(reason)}`);
|
|
1391
|
+
if (isStale) {
|
|
1392
|
+
lines.push(` ${c.dim("->")} ${c.red(`Blocked for ${daysBlocked} days`)}`);
|
|
1393
|
+
}
|
|
1394
|
+
return lines;
|
|
1395
|
+
}
|
|
1396
|
+
function renderStaleWarnings(staleItems) {
|
|
1397
|
+
if (staleItems.length === 0)
|
|
1398
|
+
return [];
|
|
1399
|
+
const message = `${staleItems.length} task${staleItems.length > 1 ? "s" : ""} blocked for 3+ days`;
|
|
1400
|
+
return ["", ...renderAlert(message, { severity: "warning" })];
|
|
1401
|
+
}
|
|
1402
|
+
function renderSummary(totalBlocked, staleCount, byPriority) {
|
|
1403
|
+
const stats = [
|
|
1404
|
+
{ label: "Total Blocked", value: totalBlocked, color: "yellow" },
|
|
1405
|
+
{ label: "Stale (3+ days)", value: staleCount, color: staleCount > 0 ? "red" : "gray" }
|
|
1406
|
+
];
|
|
1407
|
+
if (totalBlocked > 0) {
|
|
1408
|
+
if (byPriority.critical > 0) {
|
|
1409
|
+
stats.push({ label: "Critical", value: byPriority.critical, color: "red" });
|
|
1410
|
+
}
|
|
1411
|
+
if (byPriority.high > 0) {
|
|
1412
|
+
stats.push({ label: "High", value: byPriority.high, color: "magenta" });
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
return ["", ...renderStats(stats)];
|
|
1416
|
+
}
|
|
1417
|
+
async function blockedCmd(options = {}) {
|
|
1418
|
+
const tasks = await listTasks();
|
|
1419
|
+
const blockedInfo = analyzeBlockedTasks(tasks);
|
|
1420
|
+
const displayItems = options.stale ? blockedInfo.filter((info) => info.isStale) : blockedInfo;
|
|
1421
|
+
const staleItems = blockedInfo.filter((info) => info.isStale);
|
|
1422
|
+
const byPriority = { critical: 0, high: 0, medium: 0, low: 0 };
|
|
1423
|
+
for (const info of blockedInfo) {
|
|
1424
|
+
byPriority[info.task.priority]++;
|
|
1425
|
+
}
|
|
1426
|
+
const lines = [];
|
|
1427
|
+
const title = options.stale ? "Stale Blocked Tasks" : "Blocked Tasks";
|
|
1428
|
+
lines.push(c.yellow(c.bold(`[!] ${title}`)));
|
|
1429
|
+
lines.push(c.gray(hline(50)));
|
|
1430
|
+
if (displayItems.length === 0) {
|
|
1431
|
+
if (options.stale) {
|
|
1432
|
+
lines.push("");
|
|
1433
|
+
lines.push(c.green(" No stale blocked tasks found."));
|
|
1434
|
+
lines.push(c.gray(" All blocked tasks have been blocked for less than 3 days."));
|
|
1435
|
+
} else {
|
|
1436
|
+
lines.push("");
|
|
1437
|
+
lines.push(c.green(" No blocked tasks found."));
|
|
1438
|
+
lines.push(c.gray(" All tasks are ready to work or completed."));
|
|
1439
|
+
}
|
|
1440
|
+
console.log(lines.join(`
|
|
1441
|
+
`));
|
|
1442
|
+
return;
|
|
1443
|
+
}
|
|
1444
|
+
lines.push("");
|
|
1445
|
+
for (const info of displayItems) {
|
|
1446
|
+
lines.push(...renderBlockedTask(info));
|
|
1447
|
+
lines.push("");
|
|
1448
|
+
}
|
|
1449
|
+
if (!options.stale && staleItems.length > 0) {
|
|
1450
|
+
lines.push(...renderStaleWarnings(staleItems));
|
|
1451
|
+
}
|
|
1452
|
+
lines.push(...renderSummary(blockedInfo.length, staleItems.length, byPriority));
|
|
1453
|
+
if (!options.stale && staleItems.length > 0) {
|
|
1454
|
+
lines.push("");
|
|
1455
|
+
lines.push(c.gray(" Tip: Use --stale to show only stale blocked tasks"));
|
|
1456
|
+
}
|
|
1457
|
+
console.log(lines.join(`
|
|
1458
|
+
`));
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// src/commands/health.ts
|
|
1462
|
+
import { detectCircularDependencies, buildDependencyIndices } from "@task-mcp/shared";
|
|
1463
|
+
function getOverdueTasks2(tasks) {
|
|
1464
|
+
const today = new Date;
|
|
1465
|
+
today.setHours(0, 0, 0, 0);
|
|
1466
|
+
return tasks.filter((t) => {
|
|
1467
|
+
if (t.status === "completed" || t.status === "cancelled")
|
|
1468
|
+
return false;
|
|
1469
|
+
if (!t.dueDate)
|
|
1470
|
+
return false;
|
|
1471
|
+
const due = new Date(t.dueDate);
|
|
1472
|
+
due.setHours(0, 0, 0, 0);
|
|
1473
|
+
return due < today;
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
function getStaleBlockedTasks2(tasks, staleDays = 3) {
|
|
1477
|
+
const cutoff = Date.now() - staleDays * 24 * 60 * 60 * 1000;
|
|
1478
|
+
return tasks.filter((t) => {
|
|
1479
|
+
if (t.status !== "blocked")
|
|
1480
|
+
return false;
|
|
1481
|
+
const updated = new Date(t.updatedAt).getTime();
|
|
1482
|
+
return updated < cutoff;
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
function findBottlenecks(tasks, limit = 5) {
|
|
1486
|
+
const activeTasks = tasks.filter((t) => t.status !== "completed" && t.status !== "cancelled");
|
|
1487
|
+
const { dependentsIndex } = buildDependencyIndices(activeTasks);
|
|
1488
|
+
const bottlenecks = [];
|
|
1489
|
+
for (const task of activeTasks) {
|
|
1490
|
+
const dependents = dependentsIndex.get(task.id) ?? [];
|
|
1491
|
+
if (dependents.length > 0) {
|
|
1492
|
+
bottlenecks.push({
|
|
1493
|
+
task,
|
|
1494
|
+
blockingCount: dependents.length
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1498
|
+
bottlenecks.sort((a, b) => b.blockingCount - a.blockingCount);
|
|
1499
|
+
return bottlenecks.slice(0, limit);
|
|
1500
|
+
}
|
|
1501
|
+
function generateRecommendations(tasks, stats, metrics, bottlenecks) {
|
|
1502
|
+
const recommendations = [];
|
|
1503
|
+
const overdueMetric = metrics.find((m) => m.name === "Overdue tasks");
|
|
1504
|
+
if (overdueMetric?.status === "fail") {
|
|
1505
|
+
recommendations.push("Address overdue tasks immediately to prevent further delays");
|
|
1506
|
+
}
|
|
1507
|
+
const staleMetric = metrics.find((m) => m.name === "Stale blocked");
|
|
1508
|
+
if (staleMetric?.status !== "pass") {
|
|
1509
|
+
recommendations.push("Review stale blocked tasks - they may need dependency updates or reassignment");
|
|
1510
|
+
}
|
|
1511
|
+
if (bottlenecks.length > 0 && bottlenecks[0].blockingCount >= 3) {
|
|
1512
|
+
recommendations.push(`Focus on "${bottlenecks[0].task.title}" - it blocks ${bottlenecks[0].blockingCount} other tasks`);
|
|
1513
|
+
}
|
|
1514
|
+
if (stats.completionPercent < 30 && stats.total > 5) {
|
|
1515
|
+
recommendations.push("Completion rate is low. Consider breaking down large tasks or adjusting priorities");
|
|
1516
|
+
}
|
|
1517
|
+
if (stats.blocked > stats.total * 0.3 && stats.total > 3) {
|
|
1518
|
+
recommendations.push("Many tasks are blocked. Review dependency structure for optimization");
|
|
1519
|
+
}
|
|
1520
|
+
if (stats.inProgress === 0 && stats.pending > 0) {
|
|
1521
|
+
recommendations.push("No tasks in progress. Pick up the next available task to maintain momentum");
|
|
1522
|
+
}
|
|
1523
|
+
if (recommendations.length === 0) {
|
|
1524
|
+
recommendations.push("Workspace is in good health. Keep up the good work!");
|
|
1525
|
+
}
|
|
1526
|
+
return recommendations;
|
|
1527
|
+
}
|
|
1528
|
+
function calculateHealthScore(stats, cycles, overdue, staleBlocked) {
|
|
1529
|
+
let score = 100;
|
|
1530
|
+
if (cycles.length > 0) {
|
|
1531
|
+
score -= 30;
|
|
1532
|
+
}
|
|
1533
|
+
const overdueRatio = stats.total > 0 ? overdue.length / stats.total : 0;
|
|
1534
|
+
score -= Math.min(25, overdueRatio * 100);
|
|
1535
|
+
const staleRatio = stats.total > 0 ? staleBlocked.length / stats.total : 0;
|
|
1536
|
+
score -= Math.min(15, staleRatio * 50);
|
|
1537
|
+
const blockedRatio = stats.total > 0 ? stats.blocked / stats.total : 0;
|
|
1538
|
+
score -= Math.min(15, blockedRatio * 30);
|
|
1539
|
+
if (stats.completionPercent >= 50) {
|
|
1540
|
+
score += 5;
|
|
1541
|
+
}
|
|
1542
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
1543
|
+
}
|
|
1544
|
+
function analyzeHealth(tasks) {
|
|
1545
|
+
const stats = calculateStats(tasks);
|
|
1546
|
+
const depMetrics = calculateDependencyMetrics(tasks);
|
|
1547
|
+
const cycles = detectCircularDependencies(tasks);
|
|
1548
|
+
const overdue = getOverdueTasks2(tasks);
|
|
1549
|
+
const staleBlocked = getStaleBlockedTasks2(tasks);
|
|
1550
|
+
const bottlenecks = findBottlenecks(tasks);
|
|
1551
|
+
const metrics = [
|
|
1552
|
+
{
|
|
1553
|
+
name: "Completion rate",
|
|
1554
|
+
status: stats.completionPercent >= 50 ? "pass" : stats.completionPercent >= 20 ? "warn" : "fail",
|
|
1555
|
+
value: `${stats.completionPercent}%`,
|
|
1556
|
+
detail: `${stats.completed}/${stats.total - stats.cancelled} tasks`
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
name: "No cycles",
|
|
1560
|
+
status: cycles.length === 0 ? "pass" : "fail",
|
|
1561
|
+
value: cycles.length === 0 ? "Clean" : `${cycles.length} found`,
|
|
1562
|
+
detail: cycles.length > 0 ? `Involves: ${cycles[0].join(" -> ")}` : undefined
|
|
1563
|
+
},
|
|
1564
|
+
{
|
|
1565
|
+
name: "Blocked tasks",
|
|
1566
|
+
status: stats.blocked === 0 ? "pass" : stats.blocked <= 2 ? "warn" : "fail",
|
|
1567
|
+
value: `${stats.blocked} tasks`,
|
|
1568
|
+
detail: depMetrics.blockedByDependencies > 0 ? `${depMetrics.blockedByDependencies} waiting on dependencies` : undefined
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
name: "Overdue tasks",
|
|
1572
|
+
status: overdue.length === 0 ? "pass" : "fail",
|
|
1573
|
+
value: overdue.length === 0 ? "None" : `${overdue.length} tasks`,
|
|
1574
|
+
detail: overdue.length > 0 ? overdue.map((t) => t.title).slice(0, 2).join(", ") : undefined
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
name: "Stale blocked",
|
|
1578
|
+
status: staleBlocked.length === 0 ? "pass" : "warn",
|
|
1579
|
+
value: staleBlocked.length === 0 ? "None" : `${staleBlocked.length} tasks (>3 days)`,
|
|
1580
|
+
detail: staleBlocked.length > 0 ? staleBlocked.map((t) => t.title).slice(0, 2).join(", ") : undefined
|
|
1581
|
+
}
|
|
1582
|
+
];
|
|
1583
|
+
const score = calculateHealthScore(stats, cycles, overdue, staleBlocked);
|
|
1584
|
+
const recommendations = generateRecommendations(tasks, stats, metrics, bottlenecks);
|
|
1585
|
+
return {
|
|
1586
|
+
score,
|
|
1587
|
+
metrics,
|
|
1588
|
+
bottlenecks,
|
|
1589
|
+
recommendations
|
|
1590
|
+
};
|
|
1591
|
+
}
|
|
1592
|
+
function getMetricSymbol(status) {
|
|
1593
|
+
switch (status) {
|
|
1594
|
+
case "pass":
|
|
1595
|
+
return c.green("[/]");
|
|
1596
|
+
case "warn":
|
|
1597
|
+
return c.yellow("[!]");
|
|
1598
|
+
case "fail":
|
|
1599
|
+
return c.red("[x]");
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
function renderHealthScore(score, workspace) {
|
|
1603
|
+
const lines = [];
|
|
1604
|
+
let scoreColor;
|
|
1605
|
+
let label;
|
|
1606
|
+
if (score >= 80) {
|
|
1607
|
+
scoreColor = c.green;
|
|
1608
|
+
label = "Excellent";
|
|
1609
|
+
} else if (score >= 60) {
|
|
1610
|
+
scoreColor = c.cyan;
|
|
1611
|
+
label = "Good";
|
|
1612
|
+
} else if (score >= 40) {
|
|
1613
|
+
scoreColor = c.yellow;
|
|
1614
|
+
label = "Fair";
|
|
1615
|
+
} else {
|
|
1616
|
+
scoreColor = c.red;
|
|
1617
|
+
label = "Needs Attention";
|
|
1618
|
+
}
|
|
1619
|
+
lines.push(c.cyan(c.bold("Workspace Health")));
|
|
1620
|
+
lines.push(c.gray(hline(50)));
|
|
1621
|
+
lines.push("");
|
|
1622
|
+
lines.push(` ${c.gray("Workspace:")} ${c.bold(workspace)}`);
|
|
1623
|
+
lines.push("");
|
|
1624
|
+
lines.push(` ${c.gray("Score:")} ${scoreColor(c.bold(`${score}`))}${c.gray("/100")} ${c.dim(`(${label})`)}`);
|
|
1625
|
+
lines.push("");
|
|
1626
|
+
lines.push(` ${progressBar(score, 100, { width: 40, showPercent: false })}`);
|
|
1627
|
+
return lines;
|
|
1628
|
+
}
|
|
1629
|
+
function renderMetrics(metrics) {
|
|
1630
|
+
const lines = [];
|
|
1631
|
+
lines.push("");
|
|
1632
|
+
lines.push(c.cyan(c.bold(" Health Metrics")));
|
|
1633
|
+
lines.push("");
|
|
1634
|
+
for (const metric of metrics) {
|
|
1635
|
+
const symbol = getMetricSymbol(metric.status);
|
|
1636
|
+
const value = metric.status === "pass" ? c.green(metric.value) : metric.status === "warn" ? c.yellow(metric.value) : c.red(metric.value);
|
|
1637
|
+
lines.push(` ${symbol} ${metric.name}: ${value}`);
|
|
1638
|
+
if (metric.detail) {
|
|
1639
|
+
lines.push(` ${c.dim(metric.detail)}`);
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
return lines;
|
|
1643
|
+
}
|
|
1644
|
+
function renderBottlenecks(bottlenecks) {
|
|
1645
|
+
if (bottlenecks.length === 0) {
|
|
1646
|
+
return [];
|
|
1647
|
+
}
|
|
1648
|
+
const items = bottlenecks.map((b) => ({
|
|
1649
|
+
primary: `${c.gray(b.task.id.slice(-4))} ${b.task.title}`,
|
|
1650
|
+
secondary: c.yellow(`blocks ${b.blockingCount} task${b.blockingCount > 1 ? "s" : ""}`),
|
|
1651
|
+
prefix: formatStatusSymbol(b.task.status)
|
|
1652
|
+
}));
|
|
1653
|
+
return ["", ...renderSection("Bottlenecks", items, { icon: "" })];
|
|
1654
|
+
}
|
|
1655
|
+
function renderRecommendations(recommendations) {
|
|
1656
|
+
const lines = [];
|
|
1657
|
+
lines.push("");
|
|
1658
|
+
lines.push(c.cyan(c.bold(" Recommendations")));
|
|
1659
|
+
lines.push("");
|
|
1660
|
+
for (const rec of recommendations) {
|
|
1661
|
+
lines.push(` ${c.dim("-")} ${rec}`);
|
|
1662
|
+
}
|
|
1663
|
+
return lines;
|
|
1664
|
+
}
|
|
1665
|
+
function renderQuickSummary(score, workspace) {
|
|
1666
|
+
let scoreColor;
|
|
1667
|
+
let label;
|
|
1668
|
+
if (score >= 80) {
|
|
1669
|
+
scoreColor = c.green;
|
|
1670
|
+
label = "Excellent";
|
|
1671
|
+
} else if (score >= 60) {
|
|
1672
|
+
scoreColor = c.cyan;
|
|
1673
|
+
label = "Good";
|
|
1674
|
+
} else if (score >= 40) {
|
|
1675
|
+
scoreColor = c.yellow;
|
|
1676
|
+
label = "Fair";
|
|
1677
|
+
} else {
|
|
1678
|
+
scoreColor = c.red;
|
|
1679
|
+
label = "Needs Attention";
|
|
1680
|
+
}
|
|
1681
|
+
return [
|
|
1682
|
+
`${c.bold(workspace)} health: ${scoreColor(c.bold(`${score}`))}${c.gray("/100")} ${c.dim(`(${label})`)}`,
|
|
1683
|
+
`${progressBar(score, 100, { width: 30, showPercent: false })}`
|
|
1684
|
+
];
|
|
1685
|
+
}
|
|
1686
|
+
async function healthCmd(options = {}) {
|
|
1687
|
+
const tasks = await listTasks();
|
|
1688
|
+
const workspace = getCurrentWorkspace();
|
|
1689
|
+
if (tasks.length === 0) {
|
|
1690
|
+
console.log(c.yellow("No tasks found in workspace."));
|
|
1691
|
+
console.log(c.gray(`Workspace: ${workspace}`));
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
const analysis = analyzeHealth(tasks);
|
|
1695
|
+
if (options.quick) {
|
|
1696
|
+
const lines2 = renderQuickSummary(analysis.score, workspace);
|
|
1697
|
+
console.log(lines2.join(`
|
|
1698
|
+
`));
|
|
1699
|
+
return;
|
|
1700
|
+
}
|
|
1701
|
+
const lines = [];
|
|
1702
|
+
lines.push(...renderHealthScore(analysis.score, workspace));
|
|
1703
|
+
lines.push(...renderMetrics(analysis.metrics));
|
|
1704
|
+
lines.push(...renderBottlenecks(analysis.bottlenecks));
|
|
1705
|
+
lines.push(...renderRecommendations(analysis.recommendations));
|
|
1706
|
+
lines.push("");
|
|
1707
|
+
console.log(lines.join(`
|
|
1708
|
+
`));
|
|
1709
|
+
}
|
|
588
1710
|
// package.json
|
|
589
1711
|
var package_default = {
|
|
590
1712
|
name: "@task-mcp/cli",
|
|
591
|
-
version: "1.0.
|
|
1713
|
+
version: "1.0.30",
|
|
592
1714
|
description: "Zero-dependency CLI for task-mcp with Bun native visualization",
|
|
593
1715
|
type: "module",
|
|
594
1716
|
bin: {
|
|
@@ -658,7 +1780,7 @@ function parseArgs(argv) {
|
|
|
658
1780
|
}
|
|
659
1781
|
}
|
|
660
1782
|
return {
|
|
661
|
-
command: positional[0] ?? "
|
|
1783
|
+
command: positional[0] ?? "",
|
|
662
1784
|
args: positional.slice(1),
|
|
663
1785
|
flags
|
|
664
1786
|
};
|
|
@@ -672,7 +1794,16 @@ ${c.yellow("USAGE")}
|
|
|
672
1794
|
${c.bold("task")} [command] [options]
|
|
673
1795
|
|
|
674
1796
|
${c.yellow("COMMANDS")}
|
|
675
|
-
${c.cyan("
|
|
1797
|
+
${c.cyan("(no command)")} Show smart dashboard (default)
|
|
1798
|
+
${c.cyan("next")} Show suggested next task with details
|
|
1799
|
+
${c.cyan("n")} Alias for next
|
|
1800
|
+
${c.cyan("blocked")} Show blocked tasks and their causes
|
|
1801
|
+
${c.cyan("b")} Alias for blocked
|
|
1802
|
+
${c.cyan("tree")} Show task hierarchy as ASCII tree
|
|
1803
|
+
${c.cyan("t")} Alias for tree
|
|
1804
|
+
${c.cyan("health")} Show workspace health analysis
|
|
1805
|
+
${c.cyan("h")} Alias for health
|
|
1806
|
+
${c.cyan("dashboard")} [workspace] Show detailed workspace dashboard
|
|
676
1807
|
${c.cyan("d")} [workspace] Alias for dashboard
|
|
677
1808
|
${c.cyan("list")} List all tasks
|
|
678
1809
|
${c.cyan("ls")} Alias for list
|
|
@@ -695,8 +1826,11 @@ ${c.yellow("INBOX SUBCOMMANDS")}
|
|
|
695
1826
|
|
|
696
1827
|
${c.yellow("OPTIONS")}
|
|
697
1828
|
${c.cyan("-i, --interactive")} Start interactive mode
|
|
1829
|
+
${c.cyan("--strategy")} <strategy> Next task strategy (balanced,quick_wins,unblock,critical_path)
|
|
698
1830
|
${c.cyan("--status")} <status> Filter tasks by status (pending,in_progress,blocked,completed,cancelled)
|
|
699
1831
|
${c.cyan("--priority")} <priority> Filter tasks by priority (critical,high,medium,low)
|
|
1832
|
+
${c.cyan("--stale")} Show only stale blocked tasks (3+ days)
|
|
1833
|
+
${c.cyan("--quick")} Quick health score only
|
|
700
1834
|
${c.cyan("--all")} Include completed/cancelled tasks
|
|
701
1835
|
|
|
702
1836
|
${c.yellow("EXAMPLES")}
|
|
@@ -786,10 +1920,37 @@ async function main() {
|
|
|
786
1920
|
}
|
|
787
1921
|
try {
|
|
788
1922
|
switch (command) {
|
|
1923
|
+
case "":
|
|
1924
|
+
await smartDashboard();
|
|
1925
|
+
break;
|
|
789
1926
|
case "dashboard":
|
|
790
1927
|
case "d":
|
|
791
1928
|
await dashboard(args[0]);
|
|
792
1929
|
break;
|
|
1930
|
+
case "next":
|
|
1931
|
+
case "n":
|
|
1932
|
+
await nextCmd({
|
|
1933
|
+
strategy: flags.strategy
|
|
1934
|
+
});
|
|
1935
|
+
break;
|
|
1936
|
+
case "blocked":
|
|
1937
|
+
case "b":
|
|
1938
|
+
await blockedCmd({
|
|
1939
|
+
stale: flags.stale === true
|
|
1940
|
+
});
|
|
1941
|
+
break;
|
|
1942
|
+
case "tree":
|
|
1943
|
+
case "t":
|
|
1944
|
+
await treeCmd({
|
|
1945
|
+
hideCompleted: flags["hide-completed"] === true
|
|
1946
|
+
});
|
|
1947
|
+
break;
|
|
1948
|
+
case "health":
|
|
1949
|
+
case "h":
|
|
1950
|
+
await healthCmd({
|
|
1951
|
+
quick: flags.quick === true
|
|
1952
|
+
});
|
|
1953
|
+
break;
|
|
793
1954
|
case "list":
|
|
794
1955
|
case "ls":
|
|
795
1956
|
await listTasksCmd({
|