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