@task-mcp/cli 1.0.9 → 1.0.11
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 +3 -3
- package/src/commands/dashboard.ts +183 -310
package/package.json
CHANGED
package/src/ansi.ts
CHANGED
|
@@ -136,7 +136,7 @@ export function box(content: string, options: {
|
|
|
136
136
|
const { padding = 1, borderColor = "cyan", title, rounded = true } = options;
|
|
137
137
|
|
|
138
138
|
const lines = content.split("\n");
|
|
139
|
-
const maxLen = Math.max(...lines.map(l => stripAnsi(l)
|
|
139
|
+
const maxLen = Math.max(...lines.map(l => displayWidth(stripAnsi(l))), title ? title.length + 2 : 0);
|
|
140
140
|
const innerWidth = maxLen + padding * 2;
|
|
141
141
|
|
|
142
142
|
const tl = rounded ? BOX.rTopLeft : BOX.topLeft;
|
|
@@ -166,8 +166,8 @@ export function box(content: string, options: {
|
|
|
166
166
|
|
|
167
167
|
// Content lines
|
|
168
168
|
const contentLines = lines.map(line => {
|
|
169
|
-
const
|
|
170
|
-
const padRight = innerWidth -
|
|
169
|
+
const lineWidth = displayWidth(stripAnsi(line));
|
|
170
|
+
const padRight = innerWidth - lineWidth - padding;
|
|
171
171
|
return applyBorder(v) + " ".repeat(padding) + line + " ".repeat(Math.max(0, padRight)) + applyBorder(v);
|
|
172
172
|
});
|
|
173
173
|
|
|
@@ -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,148 +46,141 @@ 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);
|
|
55
|
+
const depMetrics = calculateDependencyMetrics(tasks);
|
|
56
|
+
const today = getTodayTasks(tasks);
|
|
57
|
+
const overdue = getOverdueTasks(tasks);
|
|
83
58
|
const activeTasks = stats.total - stats.cancelled;
|
|
59
|
+
const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
84
60
|
|
|
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
|
-
}
|
|
61
|
+
const lines: string[] = [];
|
|
100
62
|
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
function renderTimeWidget(tasks: Task[]): string {
|
|
106
|
-
const today = getTodayTasks(tasks);
|
|
107
|
-
const overdue = getOverdueTasks(tasks);
|
|
108
|
-
const thisWeek = getThisWeekTasks(tasks);
|
|
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
|
-
];
|
|
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("");
|
|
117
67
|
|
|
118
|
-
//
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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(" "));
|
|
126
82
|
}
|
|
127
83
|
|
|
128
|
-
|
|
84
|
+
lines.push("");
|
|
85
|
+
|
|
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
|
+
});
|
|
129
108
|
}
|
|
130
109
|
|
|
131
110
|
// =============================================================================
|
|
132
|
-
// Widget:
|
|
111
|
+
// Widget: Next Actions (combines Next Task + Inbox)
|
|
133
112
|
// =============================================================================
|
|
134
113
|
|
|
135
|
-
function
|
|
136
|
-
const depMetrics = calculateDependencyMetrics(tasks);
|
|
114
|
+
async function renderActionsWidget(tasks: Task[]): Promise<string> {
|
|
137
115
|
const nextTask = suggestNextTask(tasks);
|
|
138
116
|
|
|
139
|
-
const lines = [
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
);
|
|
117
|
+
const lines: string[] = [];
|
|
118
|
+
|
|
119
|
+
// Next tasks (top 3 suggestions)
|
|
120
|
+
const readyTasks = tasks
|
|
121
|
+
.filter(t => t.status === "pending" && (!t.dependencies || t.dependencies.length === 0))
|
|
122
|
+
.sort((a, b) => {
|
|
123
|
+
const priorityOrder = { critical: 0, high: 1, medium: 2, low: 3 };
|
|
124
|
+
return (priorityOrder[a.priority] ?? 2) - (priorityOrder[b.priority] ?? 2);
|
|
125
|
+
})
|
|
126
|
+
.slice(0, 4);
|
|
127
|
+
|
|
128
|
+
if (readyTasks.length > 0) {
|
|
129
|
+
for (const task of readyTasks) {
|
|
130
|
+
const deps = task.dependencies?.length ?? 0;
|
|
131
|
+
const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
|
|
132
|
+
lines.push(`${c.cyan("→")} ${truncate(task.title, 24)}`);
|
|
133
|
+
lines.push(` ${formatPriority(task.priority)}, ${depsInfo}`);
|
|
134
|
+
}
|
|
135
|
+
} else if (nextTask) {
|
|
136
|
+
const deps = nextTask.dependencies?.length ?? 0;
|
|
137
|
+
const depsInfo = deps > 0 ? `${deps} deps` : c.green("ready");
|
|
138
|
+
lines.push(`${c.cyan("→")} ${truncate(nextTask.title, 24)}`);
|
|
139
|
+
lines.push(` ${formatPriority(nextTask.priority)}, ${depsInfo}`);
|
|
160
140
|
} else {
|
|
161
|
-
lines.push(c.
|
|
141
|
+
lines.push(c.dim("No tasks ready"));
|
|
162
142
|
}
|
|
163
143
|
|
|
164
|
-
return box(lines.join("\n"), {
|
|
144
|
+
return box(lines.join("\n"), {
|
|
145
|
+
title: "Next Actions",
|
|
146
|
+
borderColor: "green",
|
|
147
|
+
padding: 1,
|
|
148
|
+
});
|
|
165
149
|
}
|
|
166
150
|
|
|
167
151
|
// =============================================================================
|
|
168
|
-
// Widget: Inbox
|
|
152
|
+
// Widget: Inbox
|
|
169
153
|
// =============================================================================
|
|
170
154
|
|
|
171
|
-
async function renderInboxWidget(): Promise<string> {
|
|
155
|
+
async function renderInboxWidget(): Promise<string | null> {
|
|
172
156
|
const pendingItems = await listInboxItems("pending");
|
|
173
157
|
|
|
174
|
-
const lines = [
|
|
175
|
-
c.bold("Quick Capture Inbox"),
|
|
176
|
-
"",
|
|
177
|
-
];
|
|
178
|
-
|
|
179
158
|
if (pendingItems.length === 0) {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
} else {
|
|
183
|
-
lines.push(`${c.yellow("Pending")}: ${pendingItems.length} items`);
|
|
184
|
-
lines.push("");
|
|
185
|
-
|
|
186
|
-
// Show recent items
|
|
187
|
-
const recent = pendingItems.slice(0, 4);
|
|
188
|
-
for (const item of recent) {
|
|
189
|
-
const date = new Date(item.capturedAt);
|
|
190
|
-
const ago = getTimeAgo(date);
|
|
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
|
-
}
|
|
159
|
+
return null; // Don't show if empty
|
|
160
|
+
}
|
|
195
161
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
162
|
+
const lines: string[] = [];
|
|
163
|
+
lines.push(`${c.yellow("Pending")}: ${pendingItems.length} items`);
|
|
164
|
+
lines.push("");
|
|
165
|
+
|
|
166
|
+
// Show up to 3 items
|
|
167
|
+
for (const item of pendingItems.slice(0, 3)) {
|
|
168
|
+
const date = new Date(item.capturedAt);
|
|
169
|
+
const ago = getTimeAgo(date);
|
|
170
|
+
const tags = item.tags?.length ? c.dim(` #${item.tags[0]}`) : "";
|
|
171
|
+
lines.push(`${c.yellow("○")} ${truncate(item.content, 40)}${tags}`);
|
|
172
|
+
lines.push(` ${c.dim(ago)}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (pendingItems.length > 3) {
|
|
176
|
+
lines.push(c.gray(`+${pendingItems.length - 3} more`));
|
|
199
177
|
}
|
|
200
178
|
|
|
201
|
-
return box(lines.join("\n"), {
|
|
179
|
+
return box(lines.join("\n"), {
|
|
180
|
+
title: "Inbox",
|
|
181
|
+
borderColor: "yellow",
|
|
182
|
+
padding: 1,
|
|
183
|
+
});
|
|
202
184
|
}
|
|
203
185
|
|
|
204
186
|
function getTimeAgo(date: Date): string {
|
|
@@ -215,164 +197,53 @@ function getTimeAgo(date: Date): string {
|
|
|
215
197
|
}
|
|
216
198
|
|
|
217
199
|
// =============================================================================
|
|
218
|
-
//
|
|
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)"));
|
|
232
|
-
} else {
|
|
233
|
-
lines.push(`${c.red("Path length")}: ${cpm.criticalPath.length} tasks`);
|
|
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
|
-
}
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
// Bottlenecks
|
|
249
|
-
if (cpm.bottlenecks.length > 0) {
|
|
250
|
-
lines.push("", c.dim("Bottlenecks:"));
|
|
251
|
-
for (const b of cpm.bottlenecks.slice(0, 2)) {
|
|
252
|
-
lines.push(` ${truncate(b.task.title, 16)} ${c.red(`(${b.blocksCount})`)}`);
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
return box(lines.join("\n"), { title: "Critical Path", borderColor: "red" });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
// =============================================================================
|
|
260
|
-
// Widget: Hierarchy Tree
|
|
200
|
+
// Projects Table
|
|
261
201
|
// =============================================================================
|
|
262
202
|
|
|
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
|
-
}
|
|
203
|
+
async function renderProjectsTable(projects: Project[]): Promise<string> {
|
|
204
|
+
if (projects.length === 0) {
|
|
205
|
+
return c.gray("No projects found.");
|
|
294
206
|
}
|
|
295
207
|
|
|
296
|
-
|
|
297
|
-
|
|
208
|
+
const rows: {
|
|
209
|
+
name: string;
|
|
210
|
+
progress: string;
|
|
211
|
+
ready: number;
|
|
212
|
+
blocked: number;
|
|
213
|
+
total: number;
|
|
214
|
+
}[] = [];
|
|
215
|
+
|
|
216
|
+
for (const project of projects.slice(0, 10)) {
|
|
217
|
+
const tasks = await listTasks(project.id);
|
|
218
|
+
const stats = calculateStats(tasks);
|
|
219
|
+
const depMetrics = calculateDependencyMetrics(tasks);
|
|
220
|
+
const activeTasks = stats.total - stats.cancelled;
|
|
221
|
+
const percent = activeTasks > 0 ? Math.round((stats.completed / activeTasks) * 100) : 0;
|
|
222
|
+
|
|
223
|
+
// Create mini progress bar
|
|
224
|
+
const barWidth = 8;
|
|
225
|
+
const filled = Math.round((percent / 100) * barWidth);
|
|
226
|
+
const empty = barWidth - filled;
|
|
227
|
+
const miniBar = c.green("█".repeat(filled)) + c.gray("░".repeat(empty));
|
|
228
|
+
|
|
229
|
+
rows.push({
|
|
230
|
+
name: truncate(project.name, 20),
|
|
231
|
+
progress: `${miniBar} ${pad(String(percent) + "%", 4, "right")}`,
|
|
232
|
+
ready: depMetrics.readyToWork,
|
|
233
|
+
blocked: depMetrics.blockedByDependencies,
|
|
234
|
+
total: activeTasks,
|
|
235
|
+
});
|
|
298
236
|
}
|
|
299
237
|
|
|
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
|
-
"",
|
|
238
|
+
const columns: TableColumn[] = [
|
|
239
|
+
{ header: "Project", key: "name", width: 22 },
|
|
240
|
+
{ header: "Progress", key: "progress", width: 16 },
|
|
241
|
+
{ header: "Tasks", key: "total", width: 6, align: "right" },
|
|
242
|
+
{ header: "Ready", key: "ready", width: 6, align: "right", format: (v) => c.green(String(v)) },
|
|
243
|
+
{ header: "Blocked", key: "blocked", width: 8, align: "right", format: (v) => Number(v) > 0 ? c.red(String(v)) : c.gray(String(v)) },
|
|
313
244
|
];
|
|
314
245
|
|
|
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" });
|
|
246
|
+
return table(rows as unknown as Record<string, unknown>[], columns);
|
|
376
247
|
}
|
|
377
248
|
|
|
378
249
|
// =============================================================================
|
|
@@ -417,54 +288,56 @@ export async function dashboard(projectId?: string): Promise<void> {
|
|
|
417
288
|
? `${c.bold("Project:")} ${project.name}`
|
|
418
289
|
: `${c.bold("All Projects")} (${projects.length} projects)`;
|
|
419
290
|
|
|
420
|
-
console.log(c.dim(`
|
|
291
|
+
console.log(c.dim(`v${VERSION} ${projectInfo}`));
|
|
421
292
|
console.log();
|
|
422
293
|
|
|
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();
|
|
294
|
+
// Render widgets
|
|
295
|
+
const statusWidget = renderStatusWidget(tasks, projects);
|
|
296
|
+
const actionsWidget = await renderActionsWidget(tasks);
|
|
431
297
|
|
|
432
|
-
// Print widgets
|
|
433
|
-
|
|
434
|
-
console.log(sideBySide([overview, timeWidget, inboxWidget]));
|
|
298
|
+
// Print widgets side by side
|
|
299
|
+
console.log(sideBySide([statusWidget, actionsWidget], 2));
|
|
435
300
|
console.log();
|
|
436
301
|
|
|
437
|
-
//
|
|
438
|
-
|
|
439
|
-
|
|
302
|
+
// Inbox widget (if there are pending items)
|
|
303
|
+
const inboxWidget = await renderInboxWidget();
|
|
304
|
+
if (inboxWidget) {
|
|
305
|
+
console.log(inboxWidget);
|
|
306
|
+
console.log();
|
|
307
|
+
}
|
|
440
308
|
|
|
441
|
-
//
|
|
442
|
-
|
|
443
|
-
|
|
309
|
+
// Projects table (only if showing all projects)
|
|
310
|
+
if (!project && projects.length > 1) {
|
|
311
|
+
console.log(c.bold("Projects"));
|
|
312
|
+
console.log();
|
|
313
|
+
console.log(await renderProjectsTable(projects));
|
|
314
|
+
console.log();
|
|
315
|
+
}
|
|
444
316
|
|
|
445
|
-
// Task
|
|
446
|
-
if (
|
|
317
|
+
// Task list for single project view
|
|
318
|
+
if (project || projects.length === 1) {
|
|
447
319
|
const activeTasks = tasks.filter(t => t.status !== "completed" && t.status !== "cancelled");
|
|
448
|
-
|
|
320
|
+
if (activeTasks.length > 0) {
|
|
321
|
+
const displayTasks = activeTasks.slice(0, 10);
|
|
449
322
|
|
|
450
|
-
|
|
451
|
-
|
|
323
|
+
console.log(c.bold(`Tasks (${activeTasks.length})`));
|
|
324
|
+
console.log();
|
|
452
325
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
326
|
+
const columns: TableColumn[] = [
|
|
327
|
+
{ header: "Title", key: "title", width: 40 },
|
|
328
|
+
{ header: "Status", key: "status", width: 12, format: (v) => {
|
|
329
|
+
const icon = icons[v as Task["status"]] ?? icons.pending;
|
|
330
|
+
return `${icon} ${v}`;
|
|
331
|
+
}},
|
|
332
|
+
{ header: "Priority", key: "priority", width: 10, format: (v) => formatPriority(v as Task["priority"]) },
|
|
333
|
+
];
|
|
460
334
|
|
|
461
|
-
|
|
335
|
+
console.log(table(displayTasks as unknown as Record<string, unknown>[], columns));
|
|
462
336
|
|
|
463
|
-
|
|
464
|
-
|
|
337
|
+
if (activeTasks.length > 10) {
|
|
338
|
+
console.log(c.gray(`\n(+${activeTasks.length - 10} more tasks)`));
|
|
339
|
+
}
|
|
465
340
|
}
|
|
466
|
-
} else {
|
|
467
|
-
console.log(c.gray("No tasks found."));
|
|
468
341
|
}
|
|
469
342
|
|
|
470
343
|
console.log();
|