@task-mcp/cli 1.0.8 → 1.0.10
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/package.json +1 -1
- package/src/ansi.ts +44 -0
- package/src/commands/dashboard.ts +164 -334
package/package.json
CHANGED
package/src/ansi.ts
CHANGED
|
@@ -378,6 +378,50 @@ export const icons = {
|
|
|
378
378
|
info: c.cyan("ℹ"),
|
|
379
379
|
};
|
|
380
380
|
|
|
381
|
+
/**
|
|
382
|
+
* Place multiple boxes side by side
|
|
383
|
+
* Each box is a multi-line string
|
|
384
|
+
*/
|
|
385
|
+
export function sideBySide(boxes: string[], gap: number = 2): string {
|
|
386
|
+
// Split each box into lines
|
|
387
|
+
const boxLines = boxes.map(b => b.split("\n"));
|
|
388
|
+
|
|
389
|
+
// Find max height
|
|
390
|
+
const maxHeight = Math.max(...boxLines.map(lines => lines.length));
|
|
391
|
+
|
|
392
|
+
// Find width of each box (using first line as reference)
|
|
393
|
+
const boxWidths = boxLines.map(lines => {
|
|
394
|
+
return Math.max(...lines.map(l => displayWidth(l)));
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Pad each box to max height
|
|
398
|
+
const paddedBoxLines = boxLines.map((lines, i) => {
|
|
399
|
+
const width = boxWidths[i];
|
|
400
|
+
while (lines.length < maxHeight) {
|
|
401
|
+
lines.push(" ".repeat(width));
|
|
402
|
+
}
|
|
403
|
+
// Ensure each line is padded to box width
|
|
404
|
+
return lines.map(line => {
|
|
405
|
+
const lineWidth = displayWidth(line);
|
|
406
|
+
if (lineWidth < width) {
|
|
407
|
+
return line + " ".repeat(width - lineWidth);
|
|
408
|
+
}
|
|
409
|
+
return line;
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// Combine lines horizontally
|
|
414
|
+
const result: string[] = [];
|
|
415
|
+
const gapStr = " ".repeat(gap);
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < maxHeight; i++) {
|
|
418
|
+
const lineParts = paddedBoxLines.map(lines => lines[i] ?? "");
|
|
419
|
+
result.push(lineParts.join(gapStr));
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
return result.join("\n");
|
|
423
|
+
}
|
|
424
|
+
|
|
381
425
|
/**
|
|
382
426
|
* ASCII art text (simple implementation)
|
|
383
427
|
*/
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dashboard command - displays project overview with
|
|
2
|
+
* Dashboard command - displays project overview with unified widgets
|
|
3
|
+
* Inspired by TaskMaster CLI design
|
|
3
4
|
*/
|
|
4
5
|
|
|
5
6
|
import { VERSION } from "../index.js";
|
|
@@ -10,10 +11,11 @@ import {
|
|
|
10
11
|
table,
|
|
11
12
|
icons,
|
|
12
13
|
banner,
|
|
13
|
-
hline,
|
|
14
14
|
pad,
|
|
15
15
|
truncate,
|
|
16
16
|
stripAnsi,
|
|
17
|
+
sideBySide,
|
|
18
|
+
displayWidth,
|
|
17
19
|
type TableColumn,
|
|
18
20
|
} from "../ansi.js";
|
|
19
21
|
import {
|
|
@@ -24,28 +26,16 @@ import {
|
|
|
24
26
|
calculateDependencyMetrics,
|
|
25
27
|
suggestNextTask,
|
|
26
28
|
getTodayTasks,
|
|
27
|
-
getThisWeekTasks,
|
|
28
29
|
getOverdueTasks,
|
|
29
|
-
buildTaskTree,
|
|
30
|
-
countTreeNodes,
|
|
31
|
-
calculateCriticalPath,
|
|
32
|
-
calculateAnalysisStats,
|
|
33
30
|
listInboxItems,
|
|
34
31
|
type Task,
|
|
35
32
|
type Project,
|
|
36
|
-
type TaskTreeNode,
|
|
37
|
-
type InboxItem,
|
|
38
33
|
} from "../storage.js";
|
|
39
34
|
|
|
40
35
|
// =============================================================================
|
|
41
36
|
// Formatters
|
|
42
37
|
// =============================================================================
|
|
43
38
|
|
|
44
|
-
function formatStatus(status: Task["status"]): string {
|
|
45
|
-
const icon = icons[status] ?? icons.pending;
|
|
46
|
-
return `${icon} ${status}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
39
|
function formatPriority(priority: Task["priority"]): string {
|
|
50
40
|
const colors: Record<string, (s: string) => string> = {
|
|
51
41
|
critical: c.red,
|
|
@@ -56,322 +46,170 @@ function formatPriority(priority: Task["priority"]): string {
|
|
|
56
46
|
return (colors[priority] ?? c.gray)(priority);
|
|
57
47
|
}
|
|
58
48
|
|
|
59
|
-
function formatDependencies(deps: Task["dependencies"]): string {
|
|
60
|
-
if (!deps || deps.length === 0) return c.gray("None");
|
|
61
|
-
return c.cyan(deps.map(d => d.taskId.slice(0, 4)).join(", "));
|
|
62
|
-
}
|
|
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
49
|
// =============================================================================
|
|
77
|
-
// Widget:
|
|
50
|
+
// Widget: Unified Status (combines Overview + Schedule + Dependencies)
|
|
78
51
|
// =============================================================================
|
|
79
52
|
|
|
80
|
-
function
|
|
53
|
+
function renderStatusWidget(tasks: Task[], projects: Project[]): string {
|
|
81
54
|
const stats = calculateStats(tasks);
|
|
82
|
-
const
|
|
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 {
|
|
55
|
+
const depMetrics = calculateDependencyMetrics(tasks);
|
|
105
56
|
const today = getTodayTasks(tasks);
|
|
106
57
|
const overdue = getOverdueTasks(tasks);
|
|
107
|
-
const
|
|
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
|
-
// =============================================================================
|
|
58
|
+
const activeTasks = stats.total - stats.cancelled;
|
|
59
|
+
const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
133
60
|
|
|
134
|
-
|
|
135
|
-
const depMetrics = calculateDependencyMetrics(tasks);
|
|
136
|
-
const nextTask = suggestNextTask(tasks);
|
|
61
|
+
const lines: string[] = [];
|
|
137
62
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`${c.gray("No deps")}: ${depMetrics.noDependencies}`,
|
|
143
|
-
];
|
|
63
|
+
// Progress bar with fraction
|
|
64
|
+
const bar = progressBar(stats.completed, activeTasks, { width: 24 });
|
|
65
|
+
lines.push(`${bar} ${c.bold(`${percent}%`)} ${stats.completed}/${activeTasks} tasks`);
|
|
66
|
+
lines.push("");
|
|
144
67
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
)
|
|
68
|
+
// Status counts (2 rows)
|
|
69
|
+
lines.push(
|
|
70
|
+
`${c.green("Done")}: ${stats.completed} ` +
|
|
71
|
+
`${c.blue("Progress")}: ${stats.inProgress} ` +
|
|
72
|
+
`${c.yellow("Pending")}: ${stats.pending} ` +
|
|
73
|
+
`${c.red("Blocked")}: ${stats.blocked}`
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
// Schedule info
|
|
77
|
+
const scheduleInfo = [];
|
|
78
|
+
if (overdue.length > 0) scheduleInfo.push(c.red(`Overdue: ${overdue.length}`));
|
|
79
|
+
if (today.length > 0) scheduleInfo.push(c.yellow(`Today: ${today.length}`));
|
|
80
|
+
if (scheduleInfo.length > 0) {
|
|
81
|
+
lines.push(scheduleInfo.join(" "));
|
|
151
82
|
}
|
|
152
83
|
|
|
153
|
-
lines.push(""
|
|
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
|
-
}
|
|
84
|
+
lines.push("");
|
|
162
85
|
|
|
163
|
-
|
|
86
|
+
// Priority breakdown
|
|
87
|
+
lines.push(
|
|
88
|
+
`${c.red("Critical")}: ${stats.byPriority.critical} ` +
|
|
89
|
+
`${c.yellow("High")}: ${stats.byPriority.high} ` +
|
|
90
|
+
`${c.blue("Medium")}: ${stats.byPriority.medium} ` +
|
|
91
|
+
`${c.gray("Low")}: ${stats.byPriority.low}`
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
// Dependencies summary
|
|
95
|
+
lines.push(
|
|
96
|
+
`${c.green("Ready")}: ${depMetrics.readyToWork} ` +
|
|
97
|
+
`${c.red("Blocked")}: ${depMetrics.blockedByDependencies}` +
|
|
98
|
+
(depMetrics.mostDependedOn
|
|
99
|
+
? ` ${c.dim("Bottleneck:")} ${truncate(depMetrics.mostDependedOn.title, 15)}`
|
|
100
|
+
: "")
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return box(lines.join("\n"), {
|
|
104
|
+
title: "Status",
|
|
105
|
+
borderColor: "cyan",
|
|
106
|
+
padding: 1,
|
|
107
|
+
});
|
|
164
108
|
}
|
|
165
109
|
|
|
166
110
|
// =============================================================================
|
|
167
|
-
// Widget: Inbox
|
|
111
|
+
// Widget: Next Actions (combines Next Task + Inbox)
|
|
168
112
|
// =============================================================================
|
|
169
113
|
|
|
170
|
-
async function
|
|
114
|
+
async function renderActionsWidget(tasks: Task[]): Promise<string> {
|
|
115
|
+
const nextTask = suggestNextTask(tasks);
|
|
171
116
|
const pendingItems = await listInboxItems("pending");
|
|
172
117
|
|
|
173
|
-
const lines = [
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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`));
|
|
118
|
+
const lines: string[] = [];
|
|
119
|
+
|
|
120
|
+
// Next tasks (top 2 suggestions)
|
|
121
|
+
const readyTasks = tasks
|
|
122
|
+
.filter(t => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
|
|
123
|
+
.sort((a, b) => {
|
|
124
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
125
|
+
return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
|
|
126
|
+
})
|
|
127
|
+
.slice(0, 3);
|
|
128
|
+
|
|
129
|
+
if (readyTasks.length > 0) {
|
|
130
|
+
for (const task of readyTasks) {
|
|
131
|
+
const deps = task.dependencies?.length ?? 0;
|
|
132
|
+
const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
|
|
133
|
+
lines.push(`${c.cyan("→")} ${truncate(task.title, 22)}`);
|
|
134
|
+
lines.push(` ${formatPriority(task.priority)}, ${depsInfo}`);
|
|
197
135
|
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
|
|
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)"));
|
|
136
|
+
} else if (nextTask) {
|
|
137
|
+
const deps = nextTask.dependencies?.length ?? 0;
|
|
138
|
+
const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
|
|
139
|
+
lines.push(`${c.cyan("→")} ${truncate(nextTask.title, 22)}`);
|
|
140
|
+
lines.push(` ${formatPriority(nextTask.priority)}, ${depsInfo}`);
|
|
231
141
|
} else {
|
|
232
|
-
lines.push(
|
|
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
|
-
}
|
|
142
|
+
lines.push(c.dim("No tasks ready"));
|
|
245
143
|
}
|
|
246
144
|
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
145
|
+
// Inbox summary
|
|
146
|
+
lines.push("");
|
|
147
|
+
if (pendingItems.length > 0) {
|
|
148
|
+
lines.push(`${c.yellow("Inbox")}: ${pendingItems.length} items`);
|
|
149
|
+
// Show first item preview
|
|
150
|
+
const first = pendingItems[0];
|
|
151
|
+
if (first) {
|
|
152
|
+
lines.push(` ${c.dim(truncate(first.content, 20))}`);
|
|
252
153
|
}
|
|
154
|
+
} else {
|
|
155
|
+
lines.push(c.dim("Inbox empty"));
|
|
253
156
|
}
|
|
254
157
|
|
|
255
|
-
return box(lines.join("\n"), {
|
|
158
|
+
return box(lines.join("\n"), {
|
|
159
|
+
title: "Next Actions",
|
|
160
|
+
borderColor: "green",
|
|
161
|
+
padding: 1,
|
|
162
|
+
});
|
|
256
163
|
}
|
|
257
164
|
|
|
258
165
|
// =============================================================================
|
|
259
|
-
//
|
|
166
|
+
// Projects Table
|
|
260
167
|
// =============================================================================
|
|
261
168
|
|
|
262
|
-
function
|
|
263
|
-
|
|
264
|
-
|
|
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
|
-
}
|
|
169
|
+
async function renderProjectsTable(projects: Project[]): Promise<string> {
|
|
170
|
+
if (projects.length === 0) {
|
|
171
|
+
return c.gray("No projects found.");
|
|
293
172
|
}
|
|
294
173
|
|
|
295
|
-
|
|
296
|
-
|
|
174
|
+
const rows: {
|
|
175
|
+
name: string;
|
|
176
|
+
progress: string;
|
|
177
|
+
ready: number;
|
|
178
|
+
blocked: number;
|
|
179
|
+
total: number;
|
|
180
|
+
}[] = [];
|
|
181
|
+
|
|
182
|
+
for (const project of projects.slice(0, 10)) {
|
|
183
|
+
const tasks = await listTasks(project.id);
|
|
184
|
+
const stats = calculateStats(tasks);
|
|
185
|
+
const depMetrics = calculateDependencyMetrics(tasks);
|
|
186
|
+
const activeTasks = stats.total - stats.cancelled;
|
|
187
|
+
const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
188
|
+
|
|
189
|
+
// Create mini progress bar
|
|
190
|
+
const barWidth = 8;
|
|
191
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
192
|
+
const empty = barWidth - filled;
|
|
193
|
+
const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
|
|
194
|
+
|
|
195
|
+
rows.push({
|
|
196
|
+
name: truncate(project.name, 20),
|
|
197
|
+
progress: `${miniBar} ${pad(String(percent) + "%", 4, "right")}`,
|
|
198
|
+
ready: depMetrics.readyToWork,
|
|
199
|
+
blocked: depMetrics.blockedByDependencies,
|
|
200
|
+
total: activeTasks,
|
|
201
|
+
});
|
|
297
202
|
}
|
|
298
203
|
|
|
299
|
-
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
function renderAnalysisWidget(tasks: Task[]): string {
|
|
307
|
-
const stats = calculateAnalysisStats(tasks);
|
|
308
|
-
|
|
309
|
-
const lines = [
|
|
310
|
-
c.bold("Analysis Overview"),
|
|
311
|
-
"",
|
|
204
|
+
const columns: TableColumn[] = [
|
|
205
|
+
{ header: "Project", key: "name", width: 22 },
|
|
206
|
+
{ header: "Progress", key: "progress", width: 16 },
|
|
207
|
+
{ header: "Tasks", key: "total", width: 6, align: "right" },
|
|
208
|
+
{ header: "Ready", key: "ready", width: 6, align: "right", format: (v) => c.green(String(v)) },
|
|
209
|
+
{ header: "Blocked", key: "blocked", width: 8, align: "right", format: (v) => Number(v) > 0 ? c.red(String(v)) : c.gray(String(v)) },
|
|
312
210
|
];
|
|
313
211
|
|
|
314
|
-
|
|
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" });
|
|
212
|
+
return table(rows as unknown as Record<string, unknown>[], columns);
|
|
375
213
|
}
|
|
376
214
|
|
|
377
215
|
// =============================================================================
|
|
@@ -416,57 +254,49 @@ export async function dashboard(projectId?: string): Promise<void> {
|
|
|
416
254
|
? `${c.bold("Project:")} ${project.name}`
|
|
417
255
|
: `${c.bold("All Projects")} (${projects.length} projects)`;
|
|
418
256
|
|
|
419
|
-
console.log(c.dim(`
|
|
257
|
+
console.log(c.dim(`v${VERSION} ${projectInfo}`));
|
|
420
258
|
console.log();
|
|
421
259
|
|
|
422
|
-
// Render widgets
|
|
423
|
-
const
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const analysisWidget = renderAnalysisWidget(tasks);
|
|
429
|
-
const inboxWidget = await renderInboxWidget();
|
|
430
|
-
|
|
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);
|
|
439
|
-
console.log();
|
|
440
|
-
console.log(criticalPathWidget);
|
|
441
|
-
console.log();
|
|
442
|
-
console.log(analysisWidget);
|
|
443
|
-
console.log();
|
|
444
|
-
console.log(hierarchyWidget);
|
|
260
|
+
// Render widgets
|
|
261
|
+
const statusWidget = renderStatusWidget(tasks, projects);
|
|
262
|
+
const actionsWidget = await renderActionsWidget(tasks);
|
|
263
|
+
|
|
264
|
+
// Print widgets side by side
|
|
265
|
+
console.log(sideBySide([statusWidget, actionsWidget], 2));
|
|
445
266
|
console.log();
|
|
446
267
|
|
|
447
|
-
//
|
|
448
|
-
if (
|
|
268
|
+
// Projects table (only if showing all projects)
|
|
269
|
+
if (!project && projects.length > 1) {
|
|
270
|
+
console.log(c.bold("Projects"));
|
|
271
|
+
console.log();
|
|
272
|
+
console.log(await renderProjectsTable(projects));
|
|
273
|
+
console.log();
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Task list for single project view
|
|
277
|
+
if (project || projects.length === 1) {
|
|
449
278
|
const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
|
|
450
|
-
|
|
279
|
+
if (activeTasks.length > 0) {
|
|
280
|
+
const displayTasks = activeTasks.slice(0, 10);
|
|
451
281
|
|
|
452
|
-
|
|
453
|
-
|
|
282
|
+
console.log(c.bold(`Tasks (${activeTasks.length})`));
|
|
283
|
+
console.log();
|
|
454
284
|
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
285
|
+
const columns: TableColumn[] = [
|
|
286
|
+
{ header: "Title", key: "title", width: 40 },
|
|
287
|
+
{ header: "Status", key: "status", width: 12, format: (v) => {
|
|
288
|
+
const icon = icons[v as Task["status"]] ?? icons.pending;
|
|
289
|
+
return `${icon} ${v}`;
|
|
290
|
+
}},
|
|
291
|
+
{ header: "Priority", key: "priority", width: 10, format: (v) => formatPriority(v as Task["priority"]) },
|
|
292
|
+
];
|
|
462
293
|
|
|
463
|
-
|
|
294
|
+
console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
|
|
464
295
|
|
|
465
|
-
|
|
466
|
-
|
|
296
|
+
if (activeTasks.length > 10) {
|
|
297
|
+
console.log(c.gray(`\n(+${activeTasks.length - 10} more tasks)`));
|
|
298
|
+
}
|
|
467
299
|
}
|
|
468
|
-
} else {
|
|
469
|
-
console.log(c.gray("No tasks found."));
|
|
470
300
|
}
|
|
471
301
|
|
|
472
302
|
console.log();
|