@task-mcp/cli 1.0.4 → 1.0.6
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/README.md +127 -35
- package/package.json +5 -1
- package/src/__tests__/ansi.test.ts +221 -0
- package/src/__tests__/index.test.ts +140 -0
- package/src/__tests__/storage.test.ts +271 -0
- package/src/ansi.ts +1 -14
- package/src/commands/dashboard.ts +371 -40
- package/src/commands/inbox.ts +267 -0
- package/src/commands/list.ts +1 -1
- package/src/index.ts +125 -4
- package/src/interactive.ts +400 -0
- package/src/storage.ts +355 -28
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
calculateStats,
|
|
4
|
+
calculateDependencyMetrics,
|
|
5
|
+
suggestNextTask,
|
|
6
|
+
type Task,
|
|
7
|
+
} from "../storage.js";
|
|
8
|
+
|
|
9
|
+
// Helper to create test tasks
|
|
10
|
+
function createTask(overrides: Partial<Task> = {}): Task {
|
|
11
|
+
return {
|
|
12
|
+
id: `task_${Math.random().toString(36).slice(2, 10)}`,
|
|
13
|
+
title: "Test Task",
|
|
14
|
+
status: "pending",
|
|
15
|
+
priority: "medium",
|
|
16
|
+
projectId: "proj_test",
|
|
17
|
+
createdAt: new Date().toISOString(),
|
|
18
|
+
updatedAt: new Date().toISOString(),
|
|
19
|
+
...overrides,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("storage", () => {
|
|
24
|
+
describe("calculateStats", () => {
|
|
25
|
+
test("calculates stats for empty task list", () => {
|
|
26
|
+
const stats = calculateStats([]);
|
|
27
|
+
expect(stats.total).toBe(0);
|
|
28
|
+
expect(stats.completed).toBe(0);
|
|
29
|
+
expect(stats.pending).toBe(0);
|
|
30
|
+
expect(stats.completionPercent).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("counts tasks by status", () => {
|
|
34
|
+
const tasks = [
|
|
35
|
+
createTask({ status: "pending" }),
|
|
36
|
+
createTask({ status: "pending" }),
|
|
37
|
+
createTask({ status: "in_progress" }),
|
|
38
|
+
createTask({ status: "completed" }),
|
|
39
|
+
createTask({ status: "blocked" }),
|
|
40
|
+
createTask({ status: "cancelled" }),
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const stats = calculateStats(tasks);
|
|
44
|
+
expect(stats.total).toBe(6);
|
|
45
|
+
expect(stats.pending).toBe(2);
|
|
46
|
+
expect(stats.inProgress).toBe(1);
|
|
47
|
+
expect(stats.completed).toBe(1);
|
|
48
|
+
expect(stats.blocked).toBe(1);
|
|
49
|
+
expect(stats.cancelled).toBe(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("counts tasks by priority", () => {
|
|
53
|
+
const tasks = [
|
|
54
|
+
createTask({ priority: "critical" }),
|
|
55
|
+
createTask({ priority: "high" }),
|
|
56
|
+
createTask({ priority: "high" }),
|
|
57
|
+
createTask({ priority: "medium" }),
|
|
58
|
+
createTask({ priority: "low" }),
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const stats = calculateStats(tasks);
|
|
62
|
+
expect(stats.byPriority.critical).toBe(1);
|
|
63
|
+
expect(stats.byPriority.high).toBe(2);
|
|
64
|
+
expect(stats.byPriority.medium).toBe(1);
|
|
65
|
+
expect(stats.byPriority.low).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("calculates completion percent excluding cancelled", () => {
|
|
69
|
+
const tasks = [
|
|
70
|
+
createTask({ status: "completed" }),
|
|
71
|
+
createTask({ status: "completed" }),
|
|
72
|
+
createTask({ status: "pending" }),
|
|
73
|
+
createTask({ status: "pending" }),
|
|
74
|
+
createTask({ status: "cancelled" }), // Should be excluded from calculation
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
const stats = calculateStats(tasks);
|
|
78
|
+
// 2 completed out of 4 non-cancelled = 50%
|
|
79
|
+
expect(stats.completionPercent).toBe(50);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("handles all cancelled tasks", () => {
|
|
83
|
+
const tasks = [
|
|
84
|
+
createTask({ status: "cancelled" }),
|
|
85
|
+
createTask({ status: "cancelled" }),
|
|
86
|
+
];
|
|
87
|
+
|
|
88
|
+
const stats = calculateStats(tasks);
|
|
89
|
+
expect(stats.completionPercent).toBe(0);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("calculateDependencyMetrics", () => {
|
|
94
|
+
test("calculates metrics for tasks without dependencies", () => {
|
|
95
|
+
const tasks = [
|
|
96
|
+
createTask({ status: "pending" }),
|
|
97
|
+
createTask({ status: "pending" }),
|
|
98
|
+
createTask({ status: "in_progress" }),
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const metrics = calculateDependencyMetrics(tasks);
|
|
102
|
+
expect(metrics.noDependencies).toBe(3);
|
|
103
|
+
expect(metrics.readyToWork).toBe(3);
|
|
104
|
+
expect(metrics.blockedByDependencies).toBe(0);
|
|
105
|
+
expect(metrics.avgDependencies).toBe(0);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("identifies tasks blocked by dependencies", () => {
|
|
109
|
+
const task1 = createTask({ id: "task_1", status: "pending" });
|
|
110
|
+
const task2 = createTask({
|
|
111
|
+
id: "task_2",
|
|
112
|
+
status: "pending",
|
|
113
|
+
dependencies: [{ taskId: "task_1", type: "blocks" }],
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const metrics = calculateDependencyMetrics([task1, task2]);
|
|
117
|
+
expect(metrics.noDependencies).toBe(1);
|
|
118
|
+
expect(metrics.readyToWork).toBe(1); // Only task1 is ready
|
|
119
|
+
expect(metrics.blockedByDependencies).toBe(1);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("ready to work includes tasks with completed dependencies", () => {
|
|
123
|
+
const task1 = createTask({ id: "task_1", status: "completed" });
|
|
124
|
+
const task2 = createTask({
|
|
125
|
+
id: "task_2",
|
|
126
|
+
status: "pending",
|
|
127
|
+
dependencies: [{ taskId: "task_1", type: "blocks" }],
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const metrics = calculateDependencyMetrics([task1, task2]);
|
|
131
|
+
expect(metrics.readyToWork).toBe(1); // task2 is ready because task1 is completed
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("finds most depended-on task", () => {
|
|
135
|
+
const task1 = createTask({ id: "task_1", title: "Base Task", status: "pending" });
|
|
136
|
+
const task2 = createTask({
|
|
137
|
+
id: "task_2",
|
|
138
|
+
status: "pending",
|
|
139
|
+
dependencies: [{ taskId: "task_1", type: "blocks" }],
|
|
140
|
+
});
|
|
141
|
+
const task3 = createTask({
|
|
142
|
+
id: "task_3",
|
|
143
|
+
status: "pending",
|
|
144
|
+
dependencies: [{ taskId: "task_1", type: "blocks" }],
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
const metrics = calculateDependencyMetrics([task1, task2, task3]);
|
|
148
|
+
expect(metrics.mostDependedOn).not.toBeNull();
|
|
149
|
+
expect(metrics.mostDependedOn?.id).toBe("task_1");
|
|
150
|
+
expect(metrics.mostDependedOn?.count).toBe(2);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("calculates average dependencies", () => {
|
|
154
|
+
const task1 = createTask({ id: "task_1", status: "pending" });
|
|
155
|
+
const task2 = createTask({
|
|
156
|
+
id: "task_2",
|
|
157
|
+
status: "pending",
|
|
158
|
+
dependencies: [
|
|
159
|
+
{ taskId: "task_1", type: "blocks" },
|
|
160
|
+
{ taskId: "task_x", type: "blocks" },
|
|
161
|
+
],
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
const metrics = calculateDependencyMetrics([task1, task2]);
|
|
165
|
+
expect(metrics.avgDependencies).toBe(1); // 2 deps / 2 tasks = 1
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("excludes completed and cancelled tasks from active count", () => {
|
|
169
|
+
const tasks = [
|
|
170
|
+
createTask({ status: "completed" }),
|
|
171
|
+
createTask({ status: "cancelled" }),
|
|
172
|
+
createTask({ status: "pending" }),
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
const metrics = calculateDependencyMetrics(tasks);
|
|
176
|
+
expect(metrics.noDependencies).toBe(1); // Only the pending task
|
|
177
|
+
expect(metrics.readyToWork).toBe(1);
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
describe("suggestNextTask", () => {
|
|
182
|
+
test("returns null for empty task list", () => {
|
|
183
|
+
expect(suggestNextTask([])).toBeNull();
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("returns null when no actionable tasks", () => {
|
|
187
|
+
const tasks = [
|
|
188
|
+
createTask({ status: "completed" }),
|
|
189
|
+
createTask({ status: "cancelled" }),
|
|
190
|
+
createTask({ status: "blocked" }),
|
|
191
|
+
];
|
|
192
|
+
expect(suggestNextTask(tasks)).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test("suggests task with highest priority", () => {
|
|
196
|
+
const lowTask = createTask({ id: "low", priority: "low", status: "pending" });
|
|
197
|
+
const highTask = createTask({ id: "high", priority: "high", status: "pending" });
|
|
198
|
+
const mediumTask = createTask({ id: "medium", priority: "medium", status: "pending" });
|
|
199
|
+
|
|
200
|
+
const suggestion = suggestNextTask([lowTask, highTask, mediumTask]);
|
|
201
|
+
expect(suggestion?.id).toBe("high");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("critical priority comes first", () => {
|
|
205
|
+
const tasks = [
|
|
206
|
+
createTask({ id: "high", priority: "high", status: "pending" }),
|
|
207
|
+
createTask({ id: "critical", priority: "critical", status: "pending" }),
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
const suggestion = suggestNextTask(tasks);
|
|
211
|
+
expect(suggestion?.id).toBe("critical");
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test("suggests in_progress tasks", () => {
|
|
215
|
+
const tasks = [
|
|
216
|
+
createTask({ id: "pending", priority: "high", status: "pending" }),
|
|
217
|
+
createTask({ id: "in_progress", priority: "medium", status: "in_progress" }),
|
|
218
|
+
];
|
|
219
|
+
|
|
220
|
+
// Both should be considered, high priority wins
|
|
221
|
+
const suggestion = suggestNextTask(tasks);
|
|
222
|
+
expect(suggestion?.id).toBe("pending");
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("excludes tasks with uncompleted dependencies", () => {
|
|
226
|
+
const task1 = createTask({ id: "task_1", priority: "low", status: "pending" });
|
|
227
|
+
const task2 = createTask({
|
|
228
|
+
id: "task_2",
|
|
229
|
+
priority: "critical",
|
|
230
|
+
status: "pending",
|
|
231
|
+
dependencies: [{ taskId: "task_1", type: "blocks" }],
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
const suggestion = suggestNextTask([task1, task2]);
|
|
235
|
+
// task2 has higher priority but is blocked, so task1 should be suggested
|
|
236
|
+
expect(suggestion?.id).toBe("task_1");
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
test("includes tasks when dependencies are completed", () => {
|
|
240
|
+
const task1 = createTask({ id: "task_1", priority: "low", status: "completed" });
|
|
241
|
+
const task2 = createTask({
|
|
242
|
+
id: "task_2",
|
|
243
|
+
priority: "critical",
|
|
244
|
+
status: "pending",
|
|
245
|
+
dependencies: [{ taskId: "task_1", type: "blocks" }],
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
const suggestion = suggestNextTask([task1, task2]);
|
|
249
|
+
// task1 is completed, so task2 is now actionable
|
|
250
|
+
expect(suggestion?.id).toBe("task_2");
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("same priority uses creation date (older first)", () => {
|
|
254
|
+
const older = createTask({
|
|
255
|
+
id: "older",
|
|
256
|
+
priority: "high",
|
|
257
|
+
status: "pending",
|
|
258
|
+
createdAt: "2024-01-01T00:00:00Z",
|
|
259
|
+
});
|
|
260
|
+
const newer = createTask({
|
|
261
|
+
id: "newer",
|
|
262
|
+
priority: "high",
|
|
263
|
+
status: "pending",
|
|
264
|
+
createdAt: "2024-12-01T00:00:00Z",
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
const suggestion = suggestNextTask([newer, older]);
|
|
268
|
+
expect(suggestion?.id).toBe("older");
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
});
|
package/src/ansi.ts
CHANGED
|
@@ -120,7 +120,7 @@ export const BOX = {
|
|
|
120
120
|
/**
|
|
121
121
|
* Create a horizontal line
|
|
122
122
|
*/
|
|
123
|
-
export function hline(width: number, char = BOX.horizontal): string {
|
|
123
|
+
export function hline(width: number, char: string = BOX.horizontal): string {
|
|
124
124
|
return char.repeat(width);
|
|
125
125
|
}
|
|
126
126
|
|
|
@@ -407,16 +407,3 @@ export function banner(text: string): string {
|
|
|
407
407
|
return lines.map(l => c.cyan(l)).join("\n");
|
|
408
408
|
}
|
|
409
409
|
|
|
410
|
-
/**
|
|
411
|
-
* Clear screen
|
|
412
|
-
*/
|
|
413
|
-
export function clearScreen(): void {
|
|
414
|
-
process.stdout.write("\x1b[2J\x1b[H");
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/**
|
|
418
|
-
* Move cursor
|
|
419
|
-
*/
|
|
420
|
-
export function moveCursor(row: number, col: number): void {
|
|
421
|
-
process.stdout.write(`\x1b[${row};${col}H`);
|
|
422
|
-
}
|