@task-mcp/cli 1.0.15 → 1.0.17
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 +0 -2
- package/package.json +1 -1
- package/src/__tests__/dashboard.test.ts +228 -0
- package/src/__tests__/inbox.test.ts +307 -0
- package/src/__tests__/list.test.ts +352 -0
- package/src/commands/dashboard.ts +11 -6
- package/src/commands/inbox.ts +7 -20
- package/src/commands/list.ts +19 -26
- package/src/constants.ts +56 -0
- package/src/interactive.ts +9 -145
- package/src/storage.ts +37 -424
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { describe, expect, test, mock, beforeEach, afterEach, spyOn } from "bun:test";
|
|
2
|
+
import type { Task, Project } 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
|
+
projectId: "proj_test",
|
|
12
|
+
createdAt: new Date().toISOString(),
|
|
13
|
+
updatedAt: new Date().toISOString(),
|
|
14
|
+
...overrides,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Helper to create test projects
|
|
19
|
+
function createProject(overrides: Partial<Project> = {}): Project {
|
|
20
|
+
return {
|
|
21
|
+
id: `proj_${Math.random().toString(36).slice(2, 10)}`,
|
|
22
|
+
name: "Test Project",
|
|
23
|
+
status: "active",
|
|
24
|
+
createdAt: new Date().toISOString(),
|
|
25
|
+
updatedAt: new Date().toISOString(),
|
|
26
|
+
...overrides,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
describe("list commands - unit tests", () => {
|
|
31
|
+
describe("task filtering logic", () => {
|
|
32
|
+
test("filters tasks by single status", () => {
|
|
33
|
+
const tasks = [
|
|
34
|
+
createTask({ id: "task_1", status: "pending" }),
|
|
35
|
+
createTask({ id: "task_2", status: "completed" }),
|
|
36
|
+
createTask({ id: "task_3", status: "in_progress" }),
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const status = "pending";
|
|
40
|
+
const statuses = status.split(",");
|
|
41
|
+
const filtered = tasks.filter((t) => statuses.includes(t.status));
|
|
42
|
+
|
|
43
|
+
expect(filtered.length).toBe(1);
|
|
44
|
+
expect(filtered[0]?.id).toBe("task_1");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("filters tasks by multiple statuses", () => {
|
|
48
|
+
const tasks = [
|
|
49
|
+
createTask({ id: "task_1", status: "pending" }),
|
|
50
|
+
createTask({ id: "task_2", status: "completed" }),
|
|
51
|
+
createTask({ id: "task_3", status: "in_progress" }),
|
|
52
|
+
createTask({ id: "task_4", status: "blocked" }),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const status = "pending,in_progress";
|
|
56
|
+
const statuses = status.split(",");
|
|
57
|
+
const filtered = tasks.filter((t) => statuses.includes(t.status));
|
|
58
|
+
|
|
59
|
+
expect(filtered.length).toBe(2);
|
|
60
|
+
expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_3"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("filters tasks by priority", () => {
|
|
64
|
+
const tasks = [
|
|
65
|
+
createTask({ id: "task_1", priority: "high" }),
|
|
66
|
+
createTask({ id: "task_2", priority: "low" }),
|
|
67
|
+
createTask({ id: "task_3", priority: "high" }),
|
|
68
|
+
createTask({ id: "task_4", priority: "medium" }),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
const priority = "high";
|
|
72
|
+
const priorities = priority.split(",");
|
|
73
|
+
const filtered = tasks.filter((t) => priorities.includes(t.priority));
|
|
74
|
+
|
|
75
|
+
expect(filtered.length).toBe(2);
|
|
76
|
+
expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_3"]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("filters by multiple priorities", () => {
|
|
80
|
+
const tasks = [
|
|
81
|
+
createTask({ id: "task_1", priority: "high" }),
|
|
82
|
+
createTask({ id: "task_2", priority: "low" }),
|
|
83
|
+
createTask({ id: "task_3", priority: "critical" }),
|
|
84
|
+
createTask({ id: "task_4", priority: "medium" }),
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const priority = "high,critical";
|
|
88
|
+
const priorities = priority.split(",");
|
|
89
|
+
const filtered = tasks.filter((t) => priorities.includes(t.priority));
|
|
90
|
+
|
|
91
|
+
expect(filtered.length).toBe(2);
|
|
92
|
+
expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_3"]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("default filter hides completed and cancelled tasks", () => {
|
|
96
|
+
const tasks = [
|
|
97
|
+
createTask({ id: "task_1", status: "pending" }),
|
|
98
|
+
createTask({ id: "task_2", status: "completed" }),
|
|
99
|
+
createTask({ id: "task_3", status: "cancelled" }),
|
|
100
|
+
createTask({ id: "task_4", status: "in_progress" }),
|
|
101
|
+
];
|
|
102
|
+
|
|
103
|
+
// Default behavior: hide completed/cancelled
|
|
104
|
+
const filtered = tasks.filter(
|
|
105
|
+
(t) => t.status !== "completed" && t.status !== "cancelled"
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(filtered.length).toBe(2);
|
|
109
|
+
expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_4"]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("all flag includes completed and cancelled tasks", () => {
|
|
113
|
+
const tasks = [
|
|
114
|
+
createTask({ id: "task_1", status: "pending" }),
|
|
115
|
+
createTask({ id: "task_2", status: "completed" }),
|
|
116
|
+
createTask({ id: "task_3", status: "cancelled" }),
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
const all = true;
|
|
120
|
+
// With --all, no filtering
|
|
121
|
+
const filtered = all ? tasks : tasks.filter(
|
|
122
|
+
(t) => t.status !== "completed" && t.status !== "cancelled"
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
expect(filtered.length).toBe(3);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("combined status and priority filtering", () => {
|
|
129
|
+
const tasks = [
|
|
130
|
+
createTask({ id: "task_1", status: "pending", priority: "high" }),
|
|
131
|
+
createTask({ id: "task_2", status: "pending", priority: "low" }),
|
|
132
|
+
createTask({ id: "task_3", status: "completed", priority: "high" }),
|
|
133
|
+
createTask({ id: "task_4", status: "in_progress", priority: "high" }),
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
const status = "pending,in_progress";
|
|
137
|
+
const priority = "high";
|
|
138
|
+
const statuses = status.split(",");
|
|
139
|
+
const priorities = priority.split(",");
|
|
140
|
+
|
|
141
|
+
let filtered = tasks.filter((t) => statuses.includes(t.status));
|
|
142
|
+
filtered = filtered.filter((t) => priorities.includes(t.priority));
|
|
143
|
+
|
|
144
|
+
expect(filtered.length).toBe(2);
|
|
145
|
+
expect(filtered.map((t) => t.id).sort()).toEqual(["task_1", "task_4"]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("project progress calculation", () => {
|
|
150
|
+
test("calculates progress for empty project", () => {
|
|
151
|
+
const tasks: Task[] = [];
|
|
152
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
153
|
+
const total = tasks.filter((t) => t.status !== "cancelled").length;
|
|
154
|
+
const progress = total > 0 ? `${completed}/${total}` : "0/0";
|
|
155
|
+
|
|
156
|
+
expect(progress).toBe("0/0");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("calculates progress excluding cancelled tasks", () => {
|
|
160
|
+
const tasks = [
|
|
161
|
+
createTask({ status: "completed" }),
|
|
162
|
+
createTask({ status: "completed" }),
|
|
163
|
+
createTask({ status: "pending" }),
|
|
164
|
+
createTask({ status: "cancelled" }),
|
|
165
|
+
createTask({ status: "cancelled" }),
|
|
166
|
+
];
|
|
167
|
+
|
|
168
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
169
|
+
const total = tasks.filter((t) => t.status !== "cancelled").length;
|
|
170
|
+
const progress = `${completed}/${total}`;
|
|
171
|
+
|
|
172
|
+
expect(progress).toBe("2/3"); // 2 completed out of 3 non-cancelled
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("calculates 100% completion", () => {
|
|
176
|
+
const tasks = [
|
|
177
|
+
createTask({ status: "completed" }),
|
|
178
|
+
createTask({ status: "completed" }),
|
|
179
|
+
createTask({ status: "completed" }),
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
183
|
+
const total = tasks.filter((t) => t.status !== "cancelled").length;
|
|
184
|
+
const progress = `${completed}/${total}`;
|
|
185
|
+
|
|
186
|
+
expect(progress).toBe("3/3");
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("handles all cancelled tasks", () => {
|
|
190
|
+
const tasks = [
|
|
191
|
+
createTask({ status: "cancelled" }),
|
|
192
|
+
createTask({ status: "cancelled" }),
|
|
193
|
+
];
|
|
194
|
+
|
|
195
|
+
const completed = tasks.filter((t) => t.status === "completed").length;
|
|
196
|
+
const total = tasks.filter((t) => t.status !== "cancelled").length;
|
|
197
|
+
const progress = total > 0 ? `${completed}/${total}` : "0/0";
|
|
198
|
+
|
|
199
|
+
expect(progress).toBe("0/0");
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe("formatProjectStatus", () => {
|
|
204
|
+
test("maps project statuses correctly", () => {
|
|
205
|
+
type ProjectStatus = "active" | "on_hold" | "completed" | "archived";
|
|
206
|
+
|
|
207
|
+
const formatProjectStatus = (status: ProjectStatus): string => {
|
|
208
|
+
const statusMap: Record<ProjectStatus, string> = {
|
|
209
|
+
active: "active",
|
|
210
|
+
on_hold: "on_hold",
|
|
211
|
+
completed: "completed",
|
|
212
|
+
archived: "archived",
|
|
213
|
+
};
|
|
214
|
+
return statusMap[status] ?? status;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
expect(formatProjectStatus("active")).toBe("active");
|
|
218
|
+
expect(formatProjectStatus("on_hold")).toBe("on_hold");
|
|
219
|
+
expect(formatProjectStatus("completed")).toBe("completed");
|
|
220
|
+
expect(formatProjectStatus("archived")).toBe("archived");
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("formatDate", () => {
|
|
225
|
+
test("formats valid date string", () => {
|
|
226
|
+
const formatDate = (dateStr?: string): string => {
|
|
227
|
+
if (!dateStr) return "-";
|
|
228
|
+
const date = new Date(dateStr);
|
|
229
|
+
return date.toLocaleDateString();
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const result = formatDate("2024-12-25");
|
|
233
|
+
// Result depends on locale, but should be non-empty
|
|
234
|
+
expect(result).not.toBe("-");
|
|
235
|
+
expect(result.length).toBeGreaterThan(0);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("returns dash for undefined date", () => {
|
|
239
|
+
const formatDate = (dateStr?: string): string => {
|
|
240
|
+
if (!dateStr) return "-";
|
|
241
|
+
const date = new Date(dateStr);
|
|
242
|
+
return date.toLocaleDateString();
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
expect(formatDate(undefined)).toBe("-");
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("returns dash for empty date string", () => {
|
|
249
|
+
const formatDate = (dateStr?: string): string => {
|
|
250
|
+
if (!dateStr) return "-";
|
|
251
|
+
const date = new Date(dateStr);
|
|
252
|
+
return date.toLocaleDateString();
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
expect(formatDate("")).toBe("-");
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
describe("table column definitions", () => {
|
|
260
|
+
test("task table has required columns", () => {
|
|
261
|
+
const columns = [
|
|
262
|
+
{ header: "ID", key: "id", width: 6 },
|
|
263
|
+
{ header: "Title", key: "title", width: 45 },
|
|
264
|
+
{ header: "Status", key: "status", width: 14 },
|
|
265
|
+
{ header: "Priority", key: "priority", width: 10 },
|
|
266
|
+
{ header: "Due", key: "dueDate", width: 12 },
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
expect(columns.find((c) => c.key === "id")).toBeDefined();
|
|
270
|
+
expect(columns.find((c) => c.key === "title")).toBeDefined();
|
|
271
|
+
expect(columns.find((c) => c.key === "status")).toBeDefined();
|
|
272
|
+
expect(columns.find((c) => c.key === "priority")).toBeDefined();
|
|
273
|
+
expect(columns.find((c) => c.key === "dueDate")).toBeDefined();
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("project table has required columns", () => {
|
|
277
|
+
const columns = [
|
|
278
|
+
{ header: "ID", key: "id", width: 6 },
|
|
279
|
+
{ header: "Name", key: "name", width: 30 },
|
|
280
|
+
{ header: "Status", key: "status", width: 14 },
|
|
281
|
+
{ header: "Tasks", key: "progress", width: 10 },
|
|
282
|
+
{ header: "Updated", key: "updatedAt", width: 12 },
|
|
283
|
+
];
|
|
284
|
+
|
|
285
|
+
expect(columns.find((c) => c.key === "id")).toBeDefined();
|
|
286
|
+
expect(columns.find((c) => c.key === "name")).toBeDefined();
|
|
287
|
+
expect(columns.find((c) => c.key === "status")).toBeDefined();
|
|
288
|
+
expect(columns.find((c) => c.key === "progress")).toBeDefined();
|
|
289
|
+
expect(columns.find((c) => c.key === "updatedAt")).toBeDefined();
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe("ID truncation", () => {
|
|
294
|
+
test("truncates task ID to 4 characters", () => {
|
|
295
|
+
const id = "task_abc12345";
|
|
296
|
+
const truncated = id.slice(0, 4);
|
|
297
|
+
expect(truncated).toBe("task");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("truncates project ID to 4 characters", () => {
|
|
301
|
+
const id = "proj_xyz98765";
|
|
302
|
+
const truncated = id.slice(0, 4);
|
|
303
|
+
expect(truncated).toBe("proj");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("handles short IDs", () => {
|
|
307
|
+
const id = "ab";
|
|
308
|
+
const truncated = id.slice(0, 4);
|
|
309
|
+
expect(truncated).toBe("ab");
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
describe("list commands - exports", () => {
|
|
315
|
+
test("listTasksCmd is exported", async () => {
|
|
316
|
+
const { listTasksCmd } = await import("../commands/list.js");
|
|
317
|
+
expect(typeof listTasksCmd).toBe("function");
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("listProjectsCmd is exported", async () => {
|
|
321
|
+
const { listProjectsCmd } = await import("../commands/list.js");
|
|
322
|
+
expect(typeof listProjectsCmd).toBe("function");
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test("listTasksCmd accepts options object", async () => {
|
|
326
|
+
const { listTasksCmd } = await import("../commands/list.js");
|
|
327
|
+
|
|
328
|
+
// Verify the function signature accepts the expected options
|
|
329
|
+
const options = {
|
|
330
|
+
projectId: "proj_123",
|
|
331
|
+
status: "pending",
|
|
332
|
+
priority: "high",
|
|
333
|
+
all: true,
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
// Type check passes if this compiles
|
|
337
|
+
expect(options.projectId).toBeDefined();
|
|
338
|
+
expect(options.status).toBeDefined();
|
|
339
|
+
expect(options.priority).toBeDefined();
|
|
340
|
+
expect(options.all).toBeDefined();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test("listProjectsCmd accepts options object", async () => {
|
|
344
|
+
const { listProjectsCmd } = await import("../commands/list.js");
|
|
345
|
+
|
|
346
|
+
const options = {
|
|
347
|
+
all: true,
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
expect(options.all).toBeDefined();
|
|
351
|
+
});
|
|
352
|
+
});
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
listTasks,
|
|
17
17
|
listAllTasks,
|
|
18
18
|
listInboxItems,
|
|
19
|
+
getActiveTag,
|
|
19
20
|
type Task,
|
|
20
21
|
type Project,
|
|
21
22
|
} from "../storage.js";
|
|
@@ -25,8 +26,9 @@ import {
|
|
|
25
26
|
// =============================================================================
|
|
26
27
|
|
|
27
28
|
export async function dashboard(projectId?: string): Promise<void> {
|
|
28
|
-
// Get projects and
|
|
29
|
+
// Get projects, tasks, and active tag
|
|
29
30
|
const projects = await listProjects();
|
|
31
|
+
const activeTag = await getActiveTag();
|
|
30
32
|
|
|
31
33
|
if (projects.length === 0) {
|
|
32
34
|
console.log(c.yellow("No projects found. Create a project first using the MCP server."));
|
|
@@ -55,10 +57,13 @@ export async function dashboard(projectId?: string): Promise<void> {
|
|
|
55
57
|
// Get inbox items
|
|
56
58
|
const inboxItems = await listInboxItems("pending");
|
|
57
59
|
|
|
58
|
-
// Create task lookup for projects table
|
|
60
|
+
// Create task lookup for projects table (batch query to avoid N+1)
|
|
61
|
+
const allTasksForLookup = await listAllTasks();
|
|
59
62
|
const tasksByProject = new Map<string, Task[]>();
|
|
60
|
-
for (const
|
|
61
|
-
tasksByProject.
|
|
63
|
+
for (const task of allTasksForLookup) {
|
|
64
|
+
const projectTasks = tasksByProject.get(task.projectId) ?? [];
|
|
65
|
+
projectTasks.push(task);
|
|
66
|
+
tasksByProject.set(task.projectId, projectTasks);
|
|
62
67
|
}
|
|
63
68
|
const getProjectTasks = (pid: string) => tasksByProject.get(pid) ?? [];
|
|
64
69
|
|
|
@@ -69,7 +74,7 @@ export async function dashboard(projectId?: string): Promise<void> {
|
|
|
69
74
|
output = renderProjectDashboard(
|
|
70
75
|
project as SharedProject,
|
|
71
76
|
tasks as SharedTask[],
|
|
72
|
-
{ version: VERSION }
|
|
77
|
+
{ version: VERSION, activeTag }
|
|
73
78
|
);
|
|
74
79
|
} else {
|
|
75
80
|
output = renderGlobalDashboard(
|
|
@@ -77,7 +82,7 @@ export async function dashboard(projectId?: string): Promise<void> {
|
|
|
77
82
|
tasks as SharedTask[],
|
|
78
83
|
inboxItems,
|
|
79
84
|
(pid) => (getProjectTasks(pid) as SharedTask[]),
|
|
80
|
-
{ version: VERSION }
|
|
85
|
+
{ version: VERSION, activeTag }
|
|
81
86
|
);
|
|
82
87
|
}
|
|
83
88
|
|
package/src/commands/inbox.ts
CHANGED
|
@@ -3,9 +3,15 @@
|
|
|
3
3
|
* Quick capture and management of ideas
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { c } from "../ansi.js";
|
|
6
|
+
import { c, formatStatus, formatPriority } from "../ansi.js";
|
|
7
7
|
import { InboxStore, TaskStore, ProjectStore } from "@task-mcp/mcp-server/storage";
|
|
8
8
|
import { parseInboxInput } from "@task-mcp/shared";
|
|
9
|
+
import {
|
|
10
|
+
LIST_SEPARATOR_WIDTH,
|
|
11
|
+
MENU_SEPARATOR_WIDTH,
|
|
12
|
+
INBOX_PROMOTE_TITLE_MAX_LENGTH,
|
|
13
|
+
INBOX_DISCARD_PREVIEW_LENGTH,
|
|
14
|
+
} from "../constants.js";
|
|
9
15
|
|
|
10
16
|
const inboxStore = new InboxStore();
|
|
11
17
|
const taskStore = new TaskStore();
|
|
@@ -246,22 +252,3 @@ export async function inboxCountCmd(): Promise<void> {
|
|
|
246
252
|
console.log();
|
|
247
253
|
}
|
|
248
254
|
|
|
249
|
-
// Helper functions
|
|
250
|
-
function formatStatus(status: string): string {
|
|
251
|
-
switch (status) {
|
|
252
|
-
case "pending": return c.yellow("pending");
|
|
253
|
-
case "promoted": return c.green("promoted");
|
|
254
|
-
case "discarded": return c.dim("discarded");
|
|
255
|
-
default: return status;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function formatPriority(priority: string): string {
|
|
260
|
-
switch (priority) {
|
|
261
|
-
case "critical": return c.red("critical");
|
|
262
|
-
case "high": return c.yellow("high");
|
|
263
|
-
case "medium": return c.blue("medium");
|
|
264
|
-
case "low": return c.dim("low");
|
|
265
|
-
default: return priority;
|
|
266
|
-
}
|
|
267
|
-
}
|
package/src/commands/list.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* List commands - list tasks and projects
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { c, table,
|
|
5
|
+
import { c, table, formatStatus, formatPriority, type TableColumn } from "../ansi.js";
|
|
6
6
|
import {
|
|
7
7
|
listProjects,
|
|
8
8
|
listTasks,
|
|
@@ -10,21 +10,14 @@ import {
|
|
|
10
10
|
type Task,
|
|
11
11
|
type Project,
|
|
12
12
|
} from "../storage.js";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
critical: c.red,
|
|
22
|
-
high: c.yellow,
|
|
23
|
-
medium: c.blue,
|
|
24
|
-
low: c.gray,
|
|
25
|
-
};
|
|
26
|
-
return (colors[priority] ?? c.gray)(priority);
|
|
27
|
-
}
|
|
13
|
+
import {
|
|
14
|
+
COLUMN_WIDTH_ID,
|
|
15
|
+
COLUMN_WIDTH_TITLE,
|
|
16
|
+
COLUMN_WIDTH_PROJECT_NAME,
|
|
17
|
+
COLUMN_WIDTH_STATUS,
|
|
18
|
+
COLUMN_WIDTH_PRIORITY,
|
|
19
|
+
COLUMN_WIDTH_DATE,
|
|
20
|
+
} from "../constants.js";
|
|
28
21
|
|
|
29
22
|
function formatProjectStatus(status: Project["status"]): string {
|
|
30
23
|
const statusMap: Record<string, string> = {
|
|
@@ -78,11 +71,11 @@ export async function listTasksCmd(options: {
|
|
|
78
71
|
console.log(c.bold(`\nTasks (${tasks.length})\n`));
|
|
79
72
|
|
|
80
73
|
const columns: TableColumn[] = [
|
|
81
|
-
{ header: "ID", key: "id", width:
|
|
82
|
-
{ header: "Title", key: "title", width:
|
|
83
|
-
{ header: "Status", key: "status", width:
|
|
84
|
-
{ header: "Priority", key: "priority", width:
|
|
85
|
-
{ header: "Due", key: "dueDate", width:
|
|
74
|
+
{ header: "ID", key: "id", width: COLUMN_WIDTH_ID, format: (v) => c.cyan(String(v).slice(0, 4)) },
|
|
75
|
+
{ header: "Title", key: "title", width: COLUMN_WIDTH_TITLE },
|
|
76
|
+
{ header: "Status", key: "status", width: COLUMN_WIDTH_STATUS, format: (v) => formatStatus(v as Task["status"]) },
|
|
77
|
+
{ header: "Priority", key: "priority", width: COLUMN_WIDTH_PRIORITY, format: (v) => formatPriority(v as Task["priority"]) },
|
|
78
|
+
{ header: "Due", key: "dueDate", width: COLUMN_WIDTH_DATE, format: (v) => formatDate(v as string) },
|
|
86
79
|
];
|
|
87
80
|
|
|
88
81
|
console.log(table(tasks as unknown as Record<string, unknown>[], columns));
|
|
@@ -116,11 +109,11 @@ export async function listProjectsCmd(options: {
|
|
|
116
109
|
);
|
|
117
110
|
|
|
118
111
|
const columns: TableColumn[] = [
|
|
119
|
-
{ header: "ID", key: "id", width:
|
|
120
|
-
{ header: "Name", key: "name", width:
|
|
121
|
-
{ header: "Status", key: "status", width:
|
|
122
|
-
{ header: "Tasks", key: "progress", width:
|
|
123
|
-
{ header: "Updated", key: "updatedAt", width:
|
|
112
|
+
{ header: "ID", key: "id", width: COLUMN_WIDTH_ID, format: (v) => c.cyan(String(v).slice(0, 4)) },
|
|
113
|
+
{ header: "Name", key: "name", width: COLUMN_WIDTH_PROJECT_NAME },
|
|
114
|
+
{ header: "Status", key: "status", width: COLUMN_WIDTH_STATUS, format: (v) => formatProjectStatus(v as Project["status"]) },
|
|
115
|
+
{ header: "Tasks", key: "progress", width: COLUMN_WIDTH_PRIORITY },
|
|
116
|
+
{ header: "Updated", key: "updatedAt", width: COLUMN_WIDTH_DATE, format: (v) => formatDate(v as string) },
|
|
124
117
|
];
|
|
125
118
|
|
|
126
119
|
console.log(table(projectsWithStats, columns));
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Constants
|
|
3
|
+
* Centralized magic numbers for column widths, limits, and UI settings
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// UI Layout
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
/** Width for horizontal separators in menus */
|
|
11
|
+
export const MENU_SEPARATOR_WIDTH = 40;
|
|
12
|
+
|
|
13
|
+
/** Width for horizontal separators in lists */
|
|
14
|
+
export const LIST_SEPARATOR_WIDTH = 50;
|
|
15
|
+
|
|
16
|
+
// =============================================================================
|
|
17
|
+
// Timing
|
|
18
|
+
// =============================================================================
|
|
19
|
+
|
|
20
|
+
/** Delay in ms after showing error message */
|
|
21
|
+
export const ERROR_MESSAGE_DELAY_MS = 1000;
|
|
22
|
+
|
|
23
|
+
/** Delay in ms for welcome screen display */
|
|
24
|
+
export const WELCOME_SCREEN_DELAY_MS = 1000;
|
|
25
|
+
|
|
26
|
+
// =============================================================================
|
|
27
|
+
// Table Column Widths
|
|
28
|
+
// =============================================================================
|
|
29
|
+
|
|
30
|
+
/** ID column width (shows truncated ID) */
|
|
31
|
+
export const COLUMN_WIDTH_ID = 6;
|
|
32
|
+
|
|
33
|
+
/** Task title column width */
|
|
34
|
+
export const COLUMN_WIDTH_TITLE = 45;
|
|
35
|
+
|
|
36
|
+
/** Project name column width */
|
|
37
|
+
export const COLUMN_WIDTH_PROJECT_NAME = 30;
|
|
38
|
+
|
|
39
|
+
/** Status column width */
|
|
40
|
+
export const COLUMN_WIDTH_STATUS = 14;
|
|
41
|
+
|
|
42
|
+
/** Priority column width */
|
|
43
|
+
export const COLUMN_WIDTH_PRIORITY = 10;
|
|
44
|
+
|
|
45
|
+
/** Date column width */
|
|
46
|
+
export const COLUMN_WIDTH_DATE = 12;
|
|
47
|
+
|
|
48
|
+
// =============================================================================
|
|
49
|
+
// Text Limits
|
|
50
|
+
// =============================================================================
|
|
51
|
+
|
|
52
|
+
/** Maximum length for task title when promoting from inbox */
|
|
53
|
+
export const INBOX_PROMOTE_TITLE_MAX_LENGTH = 100;
|
|
54
|
+
|
|
55
|
+
/** Maximum length for content preview when discarding inbox item */
|
|
56
|
+
export const INBOX_DISCARD_PREVIEW_LENGTH = 50;
|