@task-mcp/cli 1.0.21 → 1.0.22

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,347 +0,0 @@
1
- import { describe, expect, test, mock, beforeEach, afterEach, spyOn } from "bun:test";
2
- import type { Task, WorkspaceInfo } from "../storage.js";
3
-
4
- // Helper to create test tasks
5
- function createTask(overrides: Partial<Task> = {}): Task {
6
- return {
7
- id: `task_${Math.random().toString(36).slice(2, 10)}`,
8
- title: "Test Task",
9
- status: "pending",
10
- priority: "medium",
11
- workspace: "test-workspace",
12
- createdAt: new Date().toISOString(),
13
- updatedAt: new Date().toISOString(),
14
- ...overrides,
15
- };
16
- }
17
-
18
- // Helper to create test workspaces
19
- function createWorkspace(overrides: Partial<WorkspaceInfo> = {}): WorkspaceInfo {
20
- return {
21
- name: "Test Workspace",
22
- taskCount: 0,
23
- completedCount: 0,
24
- ...overrides,
25
- };
26
- }
27
-
28
- describe("list commands - unit tests", () => {
29
- describe("task filtering logic", () => {
30
- test("filters tasks by single status", () => {
31
- const tasks = [
32
- createTask({ id: "task_1", status: "pending" }),
33
- createTask({ id: "task_2", status: "completed" }),
34
- createTask({ id: "task_3", status: "in_progress" }),
35
- ];
36
-
37
- const status = "pending";
38
- const statuses = status.split(",");
39
- const filtered = tasks.filter((t) => statuses.includes(t.status));
40
-
41
- expect(filtered.length).toBe(1);
42
- expect(filtered[0]?.id).toBe("task_1");
43
- });
44
-
45
- test("filters tasks by multiple statuses", () => {
46
- const tasks = [
47
- createTask({ id: "task_1", status: "pending" }),
48
- createTask({ id: "task_2", status: "completed" }),
49
- createTask({ id: "task_3", status: "in_progress" }),
50
- createTask({ id: "task_4", status: "blocked" }),
51
- ];
52
-
53
- const status = "pending,in_progress";
54
- const statuses = status.split(",");
55
- const filtered = tasks.filter((t) => statuses.includes(t.status));
56
-
57
- expect(filtered.length).toBe(2);
58
- expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_3"]);
59
- });
60
-
61
- test("filters tasks by priority", () => {
62
- const tasks = [
63
- createTask({ id: "task_1", priority: "high" }),
64
- createTask({ id: "task_2", priority: "low" }),
65
- createTask({ id: "task_3", priority: "high" }),
66
- createTask({ id: "task_4", priority: "medium" }),
67
- ];
68
-
69
- const priority = "high";
70
- const priorities = priority.split(",");
71
- const filtered = tasks.filter((t) => priorities.includes(t.priority));
72
-
73
- expect(filtered.length).toBe(2);
74
- expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_3"]);
75
- });
76
-
77
- test("filters by multiple priorities", () => {
78
- const tasks = [
79
- createTask({ id: "task_1", priority: "high" }),
80
- createTask({ id: "task_2", priority: "low" }),
81
- createTask({ id: "task_3", priority: "critical" }),
82
- createTask({ id: "task_4", priority: "medium" }),
83
- ];
84
-
85
- const priority = "high,critical";
86
- const priorities = priority.split(",");
87
- const filtered = tasks.filter((t) => priorities.includes(t.priority));
88
-
89
- expect(filtered.length).toBe(2);
90
- expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_3"]);
91
- });
92
-
93
- test("default filter hides completed and cancelled tasks", () => {
94
- const tasks = [
95
- createTask({ id: "task_1", status: "pending" }),
96
- createTask({ id: "task_2", status: "completed" }),
97
- createTask({ id: "task_3", status: "cancelled" }),
98
- createTask({ id: "task_4", status: "in_progress" }),
99
- ];
100
-
101
- // Default behavior: hide completed/cancelled
102
- const filtered = tasks.filter(
103
- (t) => t.status !== "completed" && t.status !== "cancelled"
104
- );
105
-
106
- expect(filtered.length).toBe(2);
107
- expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_4"]);
108
- });
109
-
110
- test("all flag includes completed and cancelled tasks", () => {
111
- const tasks = [
112
- createTask({ id: "task_1", status: "pending" }),
113
- createTask({ id: "task_2", status: "completed" }),
114
- createTask({ id: "task_3", status: "cancelled" }),
115
- ];
116
-
117
- const all = true;
118
- // With --all, no filtering
119
- const filtered = all ? tasks : tasks.filter(
120
- (t) => t.status !== "completed" && t.status !== "cancelled"
121
- );
122
-
123
- expect(filtered.length).toBe(3);
124
- });
125
-
126
- test("combined status and priority filtering", () => {
127
- const tasks = [
128
- createTask({ id: "task_1", status: "pending", priority: "high" }),
129
- createTask({ id: "task_2", status: "pending", priority: "low" }),
130
- createTask({ id: "task_3", status: "completed", priority: "high" }),
131
- createTask({ id: "task_4", status: "in_progress", priority: "high" }),
132
- ];
133
-
134
- const status = "pending,in_progress";
135
- const priority = "high";
136
- const statuses = status.split(",");
137
- const priorities = priority.split(",");
138
-
139
- let filtered = tasks.filter((t) => statuses.includes(t.status));
140
- filtered = filtered.filter((t) => priorities.includes(t.priority));
141
-
142
- expect(filtered.length).toBe(2);
143
- expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_4"]);
144
- });
145
- });
146
-
147
- describe("project progress calculation", () => {
148
- test("calculates progress for empty project", () => {
149
- const tasks: Task[] = [];
150
- const completed = tasks.filter((t) => t.status === "completed").length;
151
- const total = tasks.filter((t) => t.status !== "cancelled").length;
152
- const progress = total > 0 ? `${completed}/${total}` : "0/0";
153
-
154
- expect(progress).toBe("0/0");
155
- });
156
-
157
- test("calculates progress excluding cancelled tasks", () => {
158
- const tasks = [
159
- createTask({ status: "completed" }),
160
- createTask({ status: "completed" }),
161
- createTask({ status: "pending" }),
162
- createTask({ status: "cancelled" }),
163
- createTask({ status: "cancelled" }),
164
- ];
165
-
166
- const completed = tasks.filter((t) => t.status === "completed").length;
167
- const total = tasks.filter((t) => t.status !== "cancelled").length;
168
- const progress = `${completed}/${total}`;
169
-
170
- expect(progress).toBe("2/3"); // 2 completed out of 3 non-cancelled
171
- });
172
-
173
- test("calculates 100% completion", () => {
174
- const tasks = [
175
- createTask({ status: "completed" }),
176
- createTask({ status: "completed" }),
177
- createTask({ status: "completed" }),
178
- ];
179
-
180
- const completed = tasks.filter((t) => t.status === "completed").length;
181
- const total = tasks.filter((t) => t.status !== "cancelled").length;
182
- const progress = `${completed}/${total}`;
183
-
184
- expect(progress).toBe("3/3");
185
- });
186
-
187
- test("handles all cancelled tasks", () => {
188
- const tasks = [
189
- createTask({ status: "cancelled" }),
190
- createTask({ status: "cancelled" }),
191
- ];
192
-
193
- const completed = tasks.filter((t) => t.status === "completed").length;
194
- const total = tasks.filter((t) => t.status !== "cancelled").length;
195
- const progress = total > 0 ? `${completed}/${total}` : "0/0";
196
-
197
- expect(progress).toBe("0/0");
198
- });
199
- });
200
-
201
- describe("formatProjectStatus", () => {
202
- test("maps project statuses correctly", () => {
203
- type ProjectStatus = "active" | "on_hold" | "completed" | "archived";
204
-
205
- const formatProjectStatus = (status: ProjectStatus): string => {
206
- const statusMap: Record<ProjectStatus, string> = {
207
- active: "active",
208
- on_hold: "on_hold",
209
- completed: "completed",
210
- archived: "archived",
211
- };
212
- return statusMap[status] ?? status;
213
- };
214
-
215
- expect(formatProjectStatus("active")).toBe("active");
216
- expect(formatProjectStatus("on_hold")).toBe("on_hold");
217
- expect(formatProjectStatus("completed")).toBe("completed");
218
- expect(formatProjectStatus("archived")).toBe("archived");
219
- });
220
- });
221
-
222
- describe("formatDate", () => {
223
- test("formats valid date string", () => {
224
- const formatDate = (dateStr?: string): string => {
225
- if (!dateStr) return "-";
226
- const date = new Date(dateStr);
227
- return date.toLocaleDateString();
228
- };
229
-
230
- const result = formatDate("2024-12-25");
231
- // Result depends on locale, but should be non-empty
232
- expect(result).not.toBe("-");
233
- expect(result.length).toBeGreaterThan(0);
234
- });
235
-
236
- test("returns dash for undefined date", () => {
237
- const formatDate = (dateStr?: string): string => {
238
- if (!dateStr) return "-";
239
- const date = new Date(dateStr);
240
- return date.toLocaleDateString();
241
- };
242
-
243
- expect(formatDate(undefined)).toBe("-");
244
- });
245
-
246
- test("returns dash for empty date string", () => {
247
- const formatDate = (dateStr?: string): string => {
248
- if (!dateStr) return "-";
249
- const date = new Date(dateStr);
250
- return date.toLocaleDateString();
251
- };
252
-
253
- expect(formatDate("")).toBe("-");
254
- });
255
- });
256
-
257
- describe("table column definitions", () => {
258
- test("task table has required columns", () => {
259
- const columns = [
260
- { header: "ID", key: "id", width: 6 },
261
- { header: "Title", key: "title", width: 45 },
262
- { header: "Status", key: "status", width: 14 },
263
- { header: "Priority", key: "priority", width: 10 },
264
- { header: "Due", key: "dueDate", width: 12 },
265
- ];
266
-
267
- expect(columns.find((c) => c.key === "id")).toBeDefined();
268
- expect(columns.find((c) => c.key === "title")).toBeDefined();
269
- expect(columns.find((c) => c.key === "status")).toBeDefined();
270
- expect(columns.find((c) => c.key === "priority")).toBeDefined();
271
- expect(columns.find((c) => c.key === "dueDate")).toBeDefined();
272
- });
273
-
274
- test("project table has required columns", () => {
275
- const columns = [
276
- { header: "ID", key: "id", width: 6 },
277
- { header: "Name", key: "name", width: 30 },
278
- { header: "Status", key: "status", width: 14 },
279
- { header: "Tasks", key: "progress", width: 10 },
280
- { header: "Updated", key: "updatedAt", width: 12 },
281
- ];
282
-
283
- expect(columns.find((c) => c.key === "id")).toBeDefined();
284
- expect(columns.find((c) => c.key === "name")).toBeDefined();
285
- expect(columns.find((c) => c.key === "status")).toBeDefined();
286
- expect(columns.find((c) => c.key === "progress")).toBeDefined();
287
- expect(columns.find((c) => c.key === "updatedAt")).toBeDefined();
288
- });
289
- });
290
-
291
- describe("ID truncation", () => {
292
- test("truncates task ID to 4 characters", () => {
293
- const id = "task_abc12345";
294
- const truncated = id.slice(0, 4);
295
- expect(truncated).toBe("task");
296
- });
297
-
298
- test("truncates project ID to 4 characters", () => {
299
- const id = "proj_xyz98765";
300
- const truncated = id.slice(0, 4);
301
- expect(truncated).toBe("proj");
302
- });
303
-
304
- test("handles short IDs", () => {
305
- const id = "ab";
306
- const truncated = id.slice(0, 4);
307
- expect(truncated).toBe("ab");
308
- });
309
- });
310
- });
311
-
312
- describe("list commands - exports", () => {
313
- test("listTasksCmd is exported", async () => {
314
- const { listTasksCmd } = await import("../commands/list.js");
315
- expect(typeof listTasksCmd).toBe("function");
316
- });
317
-
318
- test("listWorkspacesCmd is exported", async () => {
319
- const { listWorkspacesCmd } = await import("../commands/list.js");
320
- expect(typeof listWorkspacesCmd).toBe("function");
321
- });
322
-
323
- test("listTasksCmd accepts options object", async () => {
324
- const { listTasksCmd } = await import("../commands/list.js");
325
-
326
- // Verify the function signature accepts the expected options
327
- const options = {
328
- workspace: "test-workspace",
329
- status: "pending",
330
- priority: "high",
331
- all: true,
332
- };
333
-
334
- // Type check passes if this compiles
335
- expect(options.workspace).toBeDefined();
336
- expect(options.status).toBeDefined();
337
- expect(options.priority).toBeDefined();
338
- expect(options.all).toBeDefined();
339
- });
340
-
341
- test("listWorkspacesCmd accepts no options", async () => {
342
- const { listWorkspacesCmd } = await import("../commands/list.js");
343
-
344
- // listWorkspacesCmd takes no options
345
- expect(typeof listWorkspacesCmd).toBe("function");
346
- });
347
- });
@@ -1,271 +0,0 @@
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
- workspace: "test-workspace",
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 DELETED
@@ -1,50 +0,0 @@
1
- /**
2
- * Re-export terminal UI utilities from shared package
3
- * This file exists for backward compatibility
4
- */
5
-
6
- export {
7
- // Colors and styles
8
- color,
9
- style,
10
- styled,
11
- c,
12
- // Box drawing
13
- BOX,
14
- box,
15
- drawBox,
16
- hline,
17
- // String utilities
18
- stripAnsi,
19
- displayWidth,
20
- visibleLength,
21
- pad,
22
- padEnd,
23
- padStart,
24
- center,
25
- truncateStr,
26
- // Progress bar
27
- progressBar,
28
- type ProgressBarOptions,
29
- // Layout
30
- sideBySide,
31
- sideBySideArrays,
32
- type BoxOptions,
33
- // Tables
34
- table,
35
- renderTable,
36
- type TableColumn,
37
- // Formatters
38
- statusColors,
39
- statusIcons,
40
- icons,
41
- formatStatus,
42
- priorityColors,
43
- formatPriority,
44
- formatDependencies,
45
- // Banner
46
- banner,
47
- } from "@task-mcp/shared";
48
-
49
- // Alias for CLI compatibility
50
- export { truncateStr as truncate } from "@task-mcp/shared";