@task-mcp/cli 1.0.4 → 1.0.5

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.
@@ -1,7 +1,8 @@
1
1
  /**
2
- * Dashboard command - displays project overview like Taskmaster
2
+ * Dashboard command - displays project overview with multiple widgets
3
3
  */
4
4
 
5
+ import { VERSION } from "../index.js";
5
6
  import {
6
7
  c,
7
8
  box,
@@ -11,6 +12,8 @@ import {
11
12
  banner,
12
13
  hline,
13
14
  pad,
15
+ truncate,
16
+ stripAnsi,
14
17
  type TableColumn,
15
18
  } from "../ansi.js";
16
19
  import {
@@ -20,10 +23,24 @@ import {
20
23
  calculateStats,
21
24
  calculateDependencyMetrics,
22
25
  suggestNextTask,
26
+ getTodayTasks,
27
+ getThisWeekTasks,
28
+ getOverdueTasks,
29
+ buildTaskTree,
30
+ countTreeNodes,
31
+ calculateCriticalPath,
32
+ calculateAnalysisStats,
33
+ listInboxItems,
23
34
  type Task,
24
35
  type Project,
36
+ type TaskTreeNode,
37
+ type InboxItem,
25
38
  } from "../storage.js";
26
39
 
40
+ // =============================================================================
41
+ // Formatters
42
+ // =============================================================================
43
+
27
44
  function formatStatus(status: Task["status"]): string {
28
45
  const icon = icons[status] ?? icons.pending;
29
46
  return `${icon} ${status}`;
@@ -44,6 +61,323 @@ function formatDependencies(deps: Task["dependencies"]): string {
44
61
  return c.cyan(deps.map(d => d.taskId.slice(0, 4)).join(", "));
45
62
  }
46
63
 
64
+ function formatDate(dateStr: string): string {
65
+ const date = new Date(dateStr);
66
+ const now = new Date();
67
+ const diffDays = Math.floor((date.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
68
+
69
+ if (diffDays < 0) return c.red(`${Math.abs(diffDays)}d overdue`);
70
+ if (diffDays === 0) return c.yellow("Today");
71
+ if (diffDays === 1) return c.yellow("Tomorrow");
72
+ if (diffDays <= 7) return c.blue(`${diffDays}d`);
73
+ return c.gray(date.toLocaleDateString());
74
+ }
75
+
76
+ // =============================================================================
77
+ // Widget: Project Overview
78
+ // =============================================================================
79
+
80
+ function renderProjectOverview(tasks: Task[], project?: Project): string {
81
+ const stats = calculateStats(tasks);
82
+ const activeTasks = stats.total - stats.cancelled;
83
+
84
+ const lines = [
85
+ c.bold("Progress"),
86
+ progressBar(stats.completed, activeTasks, { width: 28 }),
87
+ "",
88
+ `${c.green("Done")}: ${stats.completed} ${c.blue("In Progress")}: ${stats.inProgress}`,
89
+ `${c.yellow("Pending")}: ${stats.pending} ${c.red("Blocked")}: ${stats.blocked}`,
90
+ stats.cancelled > 0 ? c.gray(`Cancelled: ${stats.cancelled}`) : "",
91
+ "",
92
+ c.bold("Priority"),
93
+ `${c.red("Critical")}: ${stats.byPriority.critical} ${c.yellow("High")}: ${stats.byPriority.high}`,
94
+ `${c.blue("Medium")}: ${stats.byPriority.medium} ${c.gray("Low")}: ${stats.byPriority.low}`,
95
+ ].filter(Boolean);
96
+
97
+ return box(lines.join("\n"), { title: "Overview", borderColor: "cyan" });
98
+ }
99
+
100
+ // =============================================================================
101
+ // Widget: Time-based View
102
+ // =============================================================================
103
+
104
+ function renderTimeWidget(tasks: Task[]): string {
105
+ const today = getTodayTasks(tasks);
106
+ const overdue = getOverdueTasks(tasks);
107
+ const thisWeek = getThisWeekTasks(tasks);
108
+
109
+ const lines = [
110
+ c.bold("Due Dates"),
111
+ "",
112
+ `${c.red(icons.warning)} ${c.red("Overdue")}: ${overdue.length}`,
113
+ `${c.yellow("!")} ${c.yellow("Today")}: ${today.length}`,
114
+ `${c.blue(">")} ${c.blue("This Week")}: ${thisWeek.length}`,
115
+ ];
116
+
117
+ // Show up to 3 urgent tasks
118
+ const urgent = [...overdue, ...today.filter(t => !overdue.includes(t))].slice(0, 3);
119
+ if (urgent.length > 0) {
120
+ lines.push("", c.dim("Urgent:"));
121
+ for (const t of urgent) {
122
+ const due = t.dueDate ? formatDate(t.dueDate) : "";
123
+ lines.push(` ${truncate(t.title, 20)} ${due}`);
124
+ }
125
+ }
126
+
127
+ return box(lines.join("\n"), { title: "Schedule", borderColor: "yellow" });
128
+ }
129
+
130
+ // =============================================================================
131
+ // Widget: Dependencies & Next Task
132
+ // =============================================================================
133
+
134
+ function renderDependencyWidget(tasks: Task[]): string {
135
+ const depMetrics = calculateDependencyMetrics(tasks);
136
+ const nextTask = suggestNextTask(tasks);
137
+
138
+ const lines = [
139
+ c.bold("Dependencies"),
140
+ "",
141
+ `${c.green("Ready")}: ${depMetrics.readyToWork} ${c.red("Blocked")}: ${depMetrics.blockedByDependencies}`,
142
+ `${c.gray("No deps")}: ${depMetrics.noDependencies}`,
143
+ ];
144
+
145
+ if (depMetrics.mostDependedOn) {
146
+ lines.push(
147
+ "",
148
+ c.dim("Bottleneck:"),
149
+ ` ${truncate(depMetrics.mostDependedOn.title, 18)} (${depMetrics.mostDependedOn.count})`
150
+ );
151
+ }
152
+
153
+ lines.push("", c.bold("Next Task"));
154
+ if (nextTask) {
155
+ lines.push(
156
+ ` ${truncate(nextTask.title, 22)}`,
157
+ ` ${formatPriority(nextTask.priority)}`
158
+ );
159
+ } else {
160
+ lines.push(c.gray(" No tasks available"));
161
+ }
162
+
163
+ return box(lines.join("\n"), { title: "Work Queue", borderColor: "green" });
164
+ }
165
+
166
+ // =============================================================================
167
+ // Widget: Inbox Summary
168
+ // =============================================================================
169
+
170
+ async function renderInboxWidget(): Promise<string> {
171
+ const pendingItems = await listInboxItems("pending");
172
+
173
+ const lines = [
174
+ c.bold("Quick Capture Inbox"),
175
+ "",
176
+ ];
177
+
178
+ if (pendingItems.length === 0) {
179
+ lines.push(c.dim("No pending items"));
180
+ lines.push(c.dim("Use 'task inbox add' to capture"));
181
+ } else {
182
+ lines.push(`${c.yellow("Pending")}: ${pendingItems.length} items`);
183
+ lines.push("");
184
+
185
+ // Show recent items
186
+ const recent = pendingItems.slice(0, 4);
187
+ for (const item of recent) {
188
+ const date = new Date(item.capturedAt);
189
+ const ago = getTimeAgo(date);
190
+ const tags = item.tags?.length ? c.dim(` #${item.tags[0]}`) : "";
191
+ lines.push(`${c.yellow("○")} ${truncate(item.content, 22)}${tags}`);
192
+ lines.push(` ${c.dim(ago)}`);
193
+ }
194
+
195
+ if (pendingItems.length > 4) {
196
+ lines.push(c.gray(`+${pendingItems.length - 4} more`));
197
+ }
198
+ }
199
+
200
+ return box(lines.join("\n"), { title: "Inbox", borderColor: "yellow" });
201
+ }
202
+
203
+ function getTimeAgo(date: Date): string {
204
+ const now = new Date();
205
+ const diffMs = now.getTime() - date.getTime();
206
+ const diffMins = Math.floor(diffMs / 60000);
207
+ const diffHours = Math.floor(diffMins / 60);
208
+ const diffDays = Math.floor(diffHours / 24);
209
+
210
+ if (diffMins < 60) return `${diffMins}m ago`;
211
+ if (diffHours < 24) return `${diffHours}h ago`;
212
+ if (diffDays < 7) return `${diffDays}d ago`;
213
+ return date.toLocaleDateString();
214
+ }
215
+
216
+ // =============================================================================
217
+ // Widget: Critical Path
218
+ // =============================================================================
219
+
220
+ function renderCriticalPathWidget(tasks: Task[]): string {
221
+ const cpm = calculateCriticalPath(tasks);
222
+
223
+ const lines = [
224
+ c.bold("Critical Path Analysis"),
225
+ "",
226
+ ];
227
+
228
+ if (cpm.criticalPath.length === 0) {
229
+ lines.push(c.gray("No critical path detected."));
230
+ lines.push(c.dim("(Tasks have no dependencies)"));
231
+ } else {
232
+ lines.push(`${c.red("Path length")}: ${cpm.criticalPath.length} tasks`);
233
+ lines.push(`${c.yellow("Est. duration")}: ~${cpm.totalDuration}m`);
234
+ lines.push("");
235
+
236
+ // Show critical tasks
237
+ lines.push(c.dim("Critical tasks:"));
238
+ for (const task of cpm.criticalPath.slice(0, 3)) {
239
+ const status = icons[task.status] ?? icons.pending;
240
+ lines.push(` ${status} ${truncate(task.title, 20)}`);
241
+ }
242
+ if (cpm.criticalPath.length > 3) {
243
+ lines.push(c.gray(` +${cpm.criticalPath.length - 3} more`));
244
+ }
245
+ }
246
+
247
+ // Bottlenecks
248
+ if (cpm.bottlenecks.length > 0) {
249
+ lines.push("", c.dim("Bottlenecks:"));
250
+ for (const b of cpm.bottlenecks.slice(0, 2)) {
251
+ lines.push(` ${truncate(b.task.title, 16)} ${c.red(`(${b.blocksCount})`)}`);
252
+ }
253
+ }
254
+
255
+ return box(lines.join("\n"), { title: "Critical Path", borderColor: "red" });
256
+ }
257
+
258
+ // =============================================================================
259
+ // Widget: Hierarchy Tree
260
+ // =============================================================================
261
+
262
+ function renderHierarchyWidget(tasks: Task[]): string {
263
+ const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
264
+ const tree = buildTaskTree(activeTasks);
265
+ const totalNodes = countTreeNodes(tree);
266
+ const rootCount = tree.length;
267
+ const subtaskCount = totalNodes - rootCount;
268
+
269
+ const lines = [
270
+ c.bold("Task Hierarchy"),
271
+ "",
272
+ `${c.cyan("Root tasks")}: ${rootCount}`,
273
+ `${c.magenta("Subtasks")}: ${subtaskCount}`,
274
+ "",
275
+ ];
276
+
277
+ // Render first few root tasks with their immediate children
278
+ const maxRoots = 4;
279
+ for (let i = 0; i < Math.min(tree.length, maxRoots); i++) {
280
+ const node = tree[i]!;
281
+ const status = icons[node.task.status] ?? icons.pending;
282
+ lines.push(`${status} ${truncate(node.task.title, 22)}`);
283
+
284
+ // Show up to 2 children
285
+ for (let j = 0; j < Math.min(node.children.length, 2); j++) {
286
+ const child = node.children[j]!;
287
+ const childStatus = icons[child.task.status] ?? icons.pending;
288
+ lines.push(` ${c.gray("└")} ${childStatus} ${truncate(child.task.title, 18)}`);
289
+ }
290
+ if (node.children.length > 2) {
291
+ lines.push(` ${c.gray(` +${node.children.length - 2} more`)}`);
292
+ }
293
+ }
294
+
295
+ if (tree.length > maxRoots) {
296
+ lines.push(c.gray(`+${tree.length - maxRoots} more root tasks`));
297
+ }
298
+
299
+ return box(lines.join("\n"), { title: "Hierarchy", borderColor: "magenta" });
300
+ }
301
+
302
+ // =============================================================================
303
+ // Widget: Complexity & Tech Stack Analysis
304
+ // =============================================================================
305
+
306
+ function renderAnalysisWidget(tasks: Task[]): string {
307
+ const stats = calculateAnalysisStats(tasks);
308
+
309
+ const lines = [
310
+ c.bold("Analysis Overview"),
311
+ "",
312
+ ];
313
+
314
+ // Complexity section
315
+ if (stats.complexity.analyzed > 0) {
316
+ lines.push(c.cyan("Complexity"));
317
+ lines.push(` Analyzed: ${stats.complexity.analyzed} tasks`);
318
+ lines.push(` Avg Score: ${c.yellow(String(stats.complexity.avgScore))}/10`);
319
+
320
+ const dist = stats.complexity.distribution;
321
+ const distBar = [
322
+ c.green(`■`.repeat(Math.min(dist.low, 5))),
323
+ c.yellow(`■`.repeat(Math.min(dist.medium, 5))),
324
+ c.red(`■`.repeat(Math.min(dist.high, 5))),
325
+ ].join("");
326
+ lines.push(` ${distBar} L:${dist.low} M:${dist.medium} H:${dist.high}`);
327
+
328
+ if (stats.complexity.topFactors.length > 0) {
329
+ lines.push("");
330
+ lines.push(c.dim("Top factors:"));
331
+ for (const f of stats.complexity.topFactors.slice(0, 3)) {
332
+ lines.push(` ${c.gray("•")} ${truncate(f.factor, 16)} (${f.count})`);
333
+ }
334
+ }
335
+ } else {
336
+ lines.push(c.gray("No complexity data"));
337
+ lines.push(c.dim("Use save_complexity_analysis"));
338
+ }
339
+
340
+ lines.push("");
341
+
342
+ // Tech Stack section
343
+ if (stats.techStack.analyzed > 0) {
344
+ lines.push(c.magenta("Tech Stack"));
345
+ lines.push(` Analyzed: ${stats.techStack.analyzed} tasks`);
346
+
347
+ // Risk distribution
348
+ const risk = stats.techStack.byRisk;
349
+ const riskItems = [];
350
+ if (risk.critical > 0) riskItems.push(c.red(`Crit:${risk.critical}`));
351
+ if (risk.high > 0) riskItems.push(c.yellow(`High:${risk.high}`));
352
+ if (risk.medium > 0) riskItems.push(c.blue(`Med:${risk.medium}`));
353
+ if (risk.low > 0) riskItems.push(c.green(`Low:${risk.low}`));
354
+ lines.push(` Risk: ${riskItems.join(" ")}`);
355
+
356
+ if (stats.techStack.breakingChanges > 0) {
357
+ lines.push(` ${c.red(icons.warning)} ${stats.techStack.breakingChanges} breaking change(s)`);
358
+ }
359
+
360
+ // Top areas
361
+ const areas = Object.entries(stats.techStack.byArea)
362
+ .sort((a, b) => b[1] - a[1])
363
+ .slice(0, 4);
364
+ if (areas.length > 0) {
365
+ lines.push("");
366
+ lines.push(c.dim("Areas:"));
367
+ lines.push(` ${areas.map(([a, n]) => `${a}(${n})`).join(" ")}`);
368
+ }
369
+ } else {
370
+ lines.push(c.gray("No tech stack data"));
371
+ lines.push(c.dim("Use save_tech_stack_analysis"));
372
+ }
373
+
374
+ return box(lines.join("\n"), { title: "Analysis", borderColor: "blue" });
375
+ }
376
+
377
+ // =============================================================================
378
+ // Main Dashboard
379
+ // =============================================================================
380
+
47
381
  export async function dashboard(projectId?: string): Promise<void> {
48
382
  // Print banner
49
383
  console.log();
@@ -77,63 +411,60 @@ export async function dashboard(projectId?: string): Promise<void> {
77
411
  tasks = await listAllTasks();
78
412
  }
79
413
 
80
- // Project info
414
+ // Project info header
81
415
  const projectInfo = project
82
416
  ? `${c.bold("Project:")} ${project.name}`
83
417
  : `${c.bold("All Projects")} (${projects.length} projects)`;
84
418
 
85
- console.log(c.dim(`Version: 1.0.4 ${projectInfo}`));
419
+ console.log(c.dim(`Version: ${VERSION} ${projectInfo}`));
86
420
  console.log();
87
421
 
88
- // Calculate stats
89
- const stats = calculateStats(tasks);
90
- const depMetrics = calculateDependencyMetrics(tasks);
91
- const nextTask = suggestNextTask(tasks);
422
+ // Render widgets in grid layout
423
+ const overview = renderProjectOverview(tasks, project);
424
+ const timeWidget = renderTimeWidget(tasks);
425
+ const depWidget = renderDependencyWidget(tasks);
426
+ const criticalPathWidget = renderCriticalPathWidget(tasks);
427
+ const hierarchyWidget = renderHierarchyWidget(tasks);
428
+ const analysisWidget = renderAnalysisWidget(tasks);
429
+ const inboxWidget = await renderInboxWidget();
92
430
 
93
- // Left panel: Project Dashboard
94
- const dashboardContent = `${c.bold("Project Dashboard")}
95
- Tasks Progress: ${progressBar(stats.completed, stats.total - stats.cancelled, { width: 30 })}
96
- ${c.green(`Done: ${stats.completed}`)} ${c.blue(`In Progress: ${stats.inProgress}`)} ${c.yellow(`Pending: ${stats.pending}`)} ${c.red(`Blocked: ${stats.blocked}`)}
97
- ${stats.cancelled > 0 ? c.gray(`Cancelled: ${stats.cancelled}`) : ""}
98
-
99
- ${c.bold("Priority Breakdown")}
100
- ${c.red("•")} Critical: ${stats.byPriority.critical}
101
- ${c.yellow("•")} High: ${stats.byPriority.high}
102
- ${c.blue("•")} Medium: ${stats.byPriority.medium}
103
- ${c.gray("•")} Low: ${stats.byPriority.low}`;
104
-
105
- // Right panel: Dependency Status
106
- const depContent = `${c.bold("Dependency Status & Next Task")}
107
- ${c.cyan("Dependency Metrics:")}
108
- • Tasks with no dependencies: ${depMetrics.noDependencies}
109
- • Tasks ready to work on: ${c.green(String(depMetrics.readyToWork))}
110
- • Tasks blocked by dependencies: ${depMetrics.blockedByDependencies}
111
- ${depMetrics.mostDependedOn ? `• Most depended-on task: ${c.cyan(`#${depMetrics.mostDependedOn.id.slice(0, 4)}`)} (${depMetrics.mostDependedOn.count} dependents)` : ""}
112
- • Avg dependencies per task: ${depMetrics.avgDependencies}
113
-
114
- ${c.yellow("Next Task to Work On:")}
115
- ${nextTask
116
- ? `ID: ${c.cyan(nextTask.id.slice(0, 4))} - ${c.bold(nextTask.title)}
117
- Priority: ${formatPriority(nextTask.priority)} Dependencies: ${formatDependencies(nextTask.dependencies)}`
118
- : c.gray("No actionable tasks available")}`;
119
-
120
- // Print boxes side by side (simplified: stacked for now)
121
- console.log(box(dashboardContent, { title: "Dashboard", borderColor: "cyan" }));
431
+ // Print widgets (2 column layout simulation)
432
+ console.log(overview);
433
+ console.log();
434
+ console.log(timeWidget);
435
+ console.log();
436
+ console.log(inboxWidget);
437
+ console.log();
438
+ console.log(depWidget);
122
439
  console.log();
123
- console.log(box(depContent, { title: "Dependencies", borderColor: "yellow" }));
440
+ console.log(criticalPathWidget);
441
+ console.log();
442
+ console.log(analysisWidget);
443
+ console.log();
444
+ console.log(hierarchyWidget);
124
445
  console.log();
125
446
 
126
447
  // Task table
127
448
  if (tasks.length > 0) {
449
+ const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
450
+ const displayTasks = activeTasks.slice(0, 15); // Limit to 15 rows
451
+
452
+ console.log(c.bold(`Active Tasks (${activeTasks.length})`));
453
+ console.log();
454
+
128
455
  const columns: TableColumn[] = [
129
456
  { header: "ID", key: "id", width: 6, format: (v) => c.cyan(String(v).slice(0, 4)) },
130
- { header: "Title", key: "title", width: 50 },
457
+ { header: "Title", key: "title", width: 45 },
131
458
  { header: "Status", key: "status", width: 14, format: (v) => formatStatus(v as Task["status"]) },
132
459
  { header: "Priority", key: "priority", width: 10, format: (v) => formatPriority(v as Task["priority"]) },
133
- { header: "Dependencies", key: "dependencies", width: 16, format: (v) => formatDependencies(v as Task["dependencies"]) },
460
+ { header: "Due", key: "dueDate", width: 12, format: (v) => v ? formatDate(String(v)) : c.gray("-") },
134
461
  ];
135
462
 
136
- console.log(table(tasks, columns));
463
+ console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
464
+
465
+ if (activeTasks.length > 15) {
466
+ console.log(c.gray(`\n(Showing 15 of ${activeTasks.length} active tasks)`));
467
+ }
137
468
  } else {
138
469
  console.log(c.gray("No tasks found."));
139
470
  }