@nijaru/tk 0.0.5 → 0.1.3
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/bin/tk.js +30 -0
- package/package.json +22 -45
- package/LICENSE +0 -21
- package/README.md +0 -242
- package/src/cli.test.ts +0 -1172
- package/src/cli.ts +0 -671
- package/src/db/storage.ts +0 -1050
- package/src/lib/completions.ts +0 -440
- package/src/lib/format.test.ts +0 -433
- package/src/lib/format.ts +0 -189
- package/src/lib/help.ts +0 -266
- package/src/lib/priority.test.ts +0 -105
- package/src/lib/priority.ts +0 -40
- package/src/lib/root.ts +0 -79
- package/src/lib/time.test.ts +0 -222
- package/src/lib/time.ts +0 -138
- package/src/types.ts +0 -130
package/src/lib/format.test.ts
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import { test, expect, describe, afterEach } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
shouldUseColor,
|
|
4
|
-
formatTaskRow,
|
|
5
|
-
formatTaskList,
|
|
6
|
-
formatTaskDetail,
|
|
7
|
-
formatJson,
|
|
8
|
-
} from "./format";
|
|
9
|
-
import type { TaskWithMeta, LogEntry } from "../types";
|
|
10
|
-
|
|
11
|
-
describe("shouldUseColor", () => {
|
|
12
|
-
const originalEnv = process.env.NO_COLOR;
|
|
13
|
-
|
|
14
|
-
afterEach(() => {
|
|
15
|
-
if (originalEnv === undefined) {
|
|
16
|
-
delete process.env.NO_COLOR;
|
|
17
|
-
} else {
|
|
18
|
-
process.env.NO_COLOR = originalEnv;
|
|
19
|
-
}
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
test("returns false when NO_COLOR is set (non-empty)", () => {
|
|
23
|
-
process.env.NO_COLOR = "1";
|
|
24
|
-
expect(shouldUseColor()).toBe(false);
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("returns false when NO_COLOR is set to any value", () => {
|
|
28
|
-
process.env.NO_COLOR = "true";
|
|
29
|
-
expect(shouldUseColor()).toBe(false);
|
|
30
|
-
process.env.NO_COLOR = "yes";
|
|
31
|
-
expect(shouldUseColor()).toBe(false);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
test("respects NO_COLOR empty string as color enabled", () => {
|
|
35
|
-
process.env.NO_COLOR = "";
|
|
36
|
-
// NO_COLOR="" is treated as unset — color depends on TTY (always false in tests)
|
|
37
|
-
expect(shouldUseColor()).toBe(false);
|
|
38
|
-
});
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
describe("formatTaskRow", () => {
|
|
42
|
-
const task: TaskWithMeta = {
|
|
43
|
-
id: "tk-a1b2",
|
|
44
|
-
project: "tk",
|
|
45
|
-
ref: "a1b2",
|
|
46
|
-
title: "Test task",
|
|
47
|
-
description: null,
|
|
48
|
-
priority: 1,
|
|
49
|
-
status: "open",
|
|
50
|
-
labels: [],
|
|
51
|
-
assignees: [],
|
|
52
|
-
parent: null,
|
|
53
|
-
blocked_by: [],
|
|
54
|
-
estimate: null,
|
|
55
|
-
due_date: null,
|
|
56
|
-
logs: [],
|
|
57
|
-
created_at: new Date().toISOString(),
|
|
58
|
-
updated_at: new Date().toISOString(),
|
|
59
|
-
completed_at: null,
|
|
60
|
-
external: {},
|
|
61
|
-
blocked_by_incomplete: false,
|
|
62
|
-
is_overdue: false,
|
|
63
|
-
days_until_due: null,
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
test("formats task without color", () => {
|
|
67
|
-
const result = formatTaskRow(task, false);
|
|
68
|
-
expect(result).toContain("tk-a1b2");
|
|
69
|
-
expect(result).toContain("p1");
|
|
70
|
-
expect(result).toContain("open");
|
|
71
|
-
expect(result).toContain("Test task");
|
|
72
|
-
expect(result).not.toContain("\x1b[");
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
test("formats task with color", () => {
|
|
76
|
-
const result = formatTaskRow(task, true);
|
|
77
|
-
expect(result).toContain("tk-a1b2");
|
|
78
|
-
expect(result).toContain("\x1b[");
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
test("truncates long titles to 50 chars", () => {
|
|
82
|
-
const longTask: TaskWithMeta = {
|
|
83
|
-
...task,
|
|
84
|
-
title: "A".repeat(100),
|
|
85
|
-
};
|
|
86
|
-
const result = formatTaskRow(longTask, false);
|
|
87
|
-
expect(result.split("A").length - 1).toBeLessThanOrEqual(50);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test("includes column dividers", () => {
|
|
91
|
-
const result = formatTaskRow(task, false);
|
|
92
|
-
expect(result).toContain(" | ");
|
|
93
|
-
const parts = result.split(" | ");
|
|
94
|
-
expect(parts.length).toBe(4); // ID | PRIO | STATUS | TITLE
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test("truncates long project names with ellipsis", () => {
|
|
98
|
-
const longProjectTask: TaskWithMeta = {
|
|
99
|
-
...task,
|
|
100
|
-
id: "mylongproject-a1b2",
|
|
101
|
-
};
|
|
102
|
-
const result = formatTaskRow(longProjectTask, false);
|
|
103
|
-
expect(result).toContain("mylon…-a1b2");
|
|
104
|
-
expect(result).not.toContain("mylongproject");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("keeps short project names intact", () => {
|
|
108
|
-
const result = formatTaskRow(task, false);
|
|
109
|
-
expect(result).toContain("tk-a1b2");
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test("shows overdue marker for overdue tasks", () => {
|
|
113
|
-
const overdueTask: TaskWithMeta = {
|
|
114
|
-
...task,
|
|
115
|
-
due_date: "2020-01-01",
|
|
116
|
-
is_overdue: true,
|
|
117
|
-
};
|
|
118
|
-
const result = formatTaskRow(overdueTask, false);
|
|
119
|
-
expect(result).toContain("[OVERDUE]");
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test("does NOT show overdue marker for tasks due today", () => {
|
|
123
|
-
const today = new Date().toISOString().split("T")[0] ?? null;
|
|
124
|
-
const todayTask: TaskWithMeta = {
|
|
125
|
-
...task,
|
|
126
|
-
due_date: today,
|
|
127
|
-
is_overdue: false,
|
|
128
|
-
};
|
|
129
|
-
const result = formatTaskRow(todayTask, false);
|
|
130
|
-
expect(result).not.toContain("[OVERDUE]");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test("shows [due today] for tasks due today", () => {
|
|
134
|
-
const today = new Date().toISOString().split("T")[0] ?? null;
|
|
135
|
-
const todayTask: TaskWithMeta = {
|
|
136
|
-
...task,
|
|
137
|
-
due_date: today,
|
|
138
|
-
is_overdue: false,
|
|
139
|
-
days_until_due: 0,
|
|
140
|
-
};
|
|
141
|
-
const result = formatTaskRow(todayTask, false);
|
|
142
|
-
expect(result).toContain("[due today]");
|
|
143
|
-
expect(result).not.toContain("[OVERDUE]");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("shows [due Nd] for tasks due in N days within threshold", () => {
|
|
147
|
-
const future = new Date();
|
|
148
|
-
future.setDate(future.getDate() + 3);
|
|
149
|
-
const dateStr = future.toISOString().split("T")[0] ?? null;
|
|
150
|
-
const soonTask: TaskWithMeta = {
|
|
151
|
-
...task,
|
|
152
|
-
due_date: dateStr,
|
|
153
|
-
is_overdue: false,
|
|
154
|
-
days_until_due: 3,
|
|
155
|
-
};
|
|
156
|
-
const result = formatTaskRow(soonTask, false);
|
|
157
|
-
expect(result).toContain("[due 3d]");
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
test("shows no due-soon marker at 8 days", () => {
|
|
161
|
-
const future = new Date();
|
|
162
|
-
future.setDate(future.getDate() + 8);
|
|
163
|
-
const dateStr = future.toISOString().split("T")[0] ?? null;
|
|
164
|
-
const notSoonTask: TaskWithMeta = {
|
|
165
|
-
...task,
|
|
166
|
-
due_date: dateStr,
|
|
167
|
-
is_overdue: false,
|
|
168
|
-
days_until_due: 8,
|
|
169
|
-
};
|
|
170
|
-
const result = formatTaskRow(notSoonTask, false);
|
|
171
|
-
expect(result).not.toContain("[due");
|
|
172
|
-
expect(result).not.toContain("[OVERDUE]");
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
test("shows no due-soon marker when overdue", () => {
|
|
176
|
-
const overdueWithSoon: TaskWithMeta = {
|
|
177
|
-
...task,
|
|
178
|
-
due_date: "2020-01-01",
|
|
179
|
-
is_overdue: true,
|
|
180
|
-
days_until_due: null,
|
|
181
|
-
};
|
|
182
|
-
const result = formatTaskRow(overdueWithSoon, false);
|
|
183
|
-
expect(result).toContain("[OVERDUE]");
|
|
184
|
-
expect(result).not.toContain("[due today]");
|
|
185
|
-
expect(result).not.toContain("[due ");
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
test("shows no due-soon marker for done tasks", () => {
|
|
189
|
-
const doneTask: TaskWithMeta = {
|
|
190
|
-
...task,
|
|
191
|
-
status: "done",
|
|
192
|
-
days_until_due: null,
|
|
193
|
-
completed_at: new Date().toISOString(),
|
|
194
|
-
};
|
|
195
|
-
const result = formatTaskRow(doneTask, false);
|
|
196
|
-
expect(result).not.toContain("[due");
|
|
197
|
-
});
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
describe("formatTaskList", () => {
|
|
201
|
-
test("returns helpful message for empty list", () => {
|
|
202
|
-
expect(formatTaskList([])).toBe("No tasks found. Run 'tk add \"title\"' to create one.");
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
test("allows custom empty hint", () => {
|
|
206
|
-
expect(formatTaskList([], undefined, "Custom hint")).toBe("Custom hint");
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
test("includes header and divider", () => {
|
|
210
|
-
const tasks: TaskWithMeta[] = [
|
|
211
|
-
{
|
|
212
|
-
id: "tk-a1b2",
|
|
213
|
-
project: "tk",
|
|
214
|
-
ref: "a1b2",
|
|
215
|
-
title: "Test",
|
|
216
|
-
description: null,
|
|
217
|
-
priority: 3,
|
|
218
|
-
status: "open",
|
|
219
|
-
labels: [],
|
|
220
|
-
assignees: [],
|
|
221
|
-
parent: null,
|
|
222
|
-
blocked_by: [],
|
|
223
|
-
estimate: null,
|
|
224
|
-
due_date: null,
|
|
225
|
-
logs: [],
|
|
226
|
-
created_at: new Date().toISOString(),
|
|
227
|
-
updated_at: new Date().toISOString(),
|
|
228
|
-
completed_at: null,
|
|
229
|
-
external: {},
|
|
230
|
-
blocked_by_incomplete: false,
|
|
231
|
-
is_overdue: false,
|
|
232
|
-
days_until_due: null,
|
|
233
|
-
},
|
|
234
|
-
];
|
|
235
|
-
const result = formatTaskList(tasks, false);
|
|
236
|
-
expect(result).toContain("ID");
|
|
237
|
-
expect(result).toContain("PRI");
|
|
238
|
-
expect(result).toContain("STATUS");
|
|
239
|
-
expect(result).toContain("TITLE");
|
|
240
|
-
expect(result).toContain("----");
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
test("formats multiple tasks", () => {
|
|
244
|
-
const tasks: TaskWithMeta[] = [
|
|
245
|
-
{
|
|
246
|
-
id: "tk-a1b2",
|
|
247
|
-
project: "tk",
|
|
248
|
-
ref: "a1b2",
|
|
249
|
-
title: "First",
|
|
250
|
-
description: null,
|
|
251
|
-
priority: 1,
|
|
252
|
-
status: "open",
|
|
253
|
-
labels: [],
|
|
254
|
-
assignees: [],
|
|
255
|
-
parent: null,
|
|
256
|
-
blocked_by: [],
|
|
257
|
-
estimate: null,
|
|
258
|
-
due_date: null,
|
|
259
|
-
logs: [],
|
|
260
|
-
created_at: new Date().toISOString(),
|
|
261
|
-
updated_at: new Date().toISOString(),
|
|
262
|
-
completed_at: null,
|
|
263
|
-
external: {},
|
|
264
|
-
blocked_by_incomplete: false,
|
|
265
|
-
is_overdue: false,
|
|
266
|
-
days_until_due: null,
|
|
267
|
-
},
|
|
268
|
-
{
|
|
269
|
-
id: "tk-c3d4",
|
|
270
|
-
project: "tk",
|
|
271
|
-
ref: "c3d4",
|
|
272
|
-
title: "Second",
|
|
273
|
-
description: null,
|
|
274
|
-
priority: 3,
|
|
275
|
-
status: "active",
|
|
276
|
-
labels: [],
|
|
277
|
-
assignees: [],
|
|
278
|
-
parent: null,
|
|
279
|
-
blocked_by: [],
|
|
280
|
-
estimate: null,
|
|
281
|
-
due_date: null,
|
|
282
|
-
logs: [],
|
|
283
|
-
created_at: new Date().toISOString(),
|
|
284
|
-
updated_at: new Date().toISOString(),
|
|
285
|
-
completed_at: null,
|
|
286
|
-
external: {},
|
|
287
|
-
blocked_by_incomplete: false,
|
|
288
|
-
is_overdue: false,
|
|
289
|
-
days_until_due: null,
|
|
290
|
-
},
|
|
291
|
-
];
|
|
292
|
-
const result = formatTaskList(tasks, false);
|
|
293
|
-
expect(result).toContain("tk-a1b2");
|
|
294
|
-
expect(result).toContain("tk-c3d4");
|
|
295
|
-
expect(result).toContain("First");
|
|
296
|
-
expect(result).toContain("Second");
|
|
297
|
-
});
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
describe("formatTaskDetail", () => {
|
|
301
|
-
const task: TaskWithMeta = {
|
|
302
|
-
id: "tk-a1b2",
|
|
303
|
-
project: "tk",
|
|
304
|
-
ref: "a1b2",
|
|
305
|
-
title: "Test task",
|
|
306
|
-
description: "A description",
|
|
307
|
-
priority: 1,
|
|
308
|
-
status: "open",
|
|
309
|
-
labels: ["bug", "urgent"],
|
|
310
|
-
assignees: ["nick"],
|
|
311
|
-
parent: null,
|
|
312
|
-
blocked_by: ["tk-c3d4"],
|
|
313
|
-
estimate: 3,
|
|
314
|
-
due_date: "2026-01-15",
|
|
315
|
-
logs: [],
|
|
316
|
-
created_at: "2024-01-01T00:00:00.000Z",
|
|
317
|
-
updated_at: "2024-01-01T00:00:00.000Z",
|
|
318
|
-
completed_at: null,
|
|
319
|
-
external: {},
|
|
320
|
-
blocked_by_incomplete: true,
|
|
321
|
-
is_overdue: false,
|
|
322
|
-
days_until_due: null,
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
test("includes all task fields", () => {
|
|
326
|
-
const result = formatTaskDetail(task, [], false);
|
|
327
|
-
expect(result).toContain("ID:");
|
|
328
|
-
expect(result).toContain("tk-a1b2");
|
|
329
|
-
expect(result).toContain("Title:");
|
|
330
|
-
expect(result).toContain("Test task");
|
|
331
|
-
expect(result).toContain("Status:");
|
|
332
|
-
expect(result).toContain("open");
|
|
333
|
-
expect(result).toContain("Priority:");
|
|
334
|
-
expect(result).toContain("p1");
|
|
335
|
-
expect(result).toContain("Description:");
|
|
336
|
-
expect(result).toContain("A description");
|
|
337
|
-
});
|
|
338
|
-
|
|
339
|
-
test("shows labels", () => {
|
|
340
|
-
const result = formatTaskDetail(task, [], false);
|
|
341
|
-
expect(result).toContain("Labels:");
|
|
342
|
-
expect(result).toContain("bug, urgent");
|
|
343
|
-
});
|
|
344
|
-
|
|
345
|
-
test("shows assignees", () => {
|
|
346
|
-
const result = formatTaskDetail(task, [], false);
|
|
347
|
-
expect(result).toContain("Assignees:");
|
|
348
|
-
expect(result).toContain("nick");
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
test("shows estimate", () => {
|
|
352
|
-
const result = formatTaskDetail(task, [], false);
|
|
353
|
-
expect(result).toContain("Estimate:");
|
|
354
|
-
expect(result).toContain("3");
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("shows due date", () => {
|
|
358
|
-
const result = formatTaskDetail(task, [], false);
|
|
359
|
-
expect(result).toContain("Due:");
|
|
360
|
-
expect(result).toContain("2026-01-15");
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
test("shows blockers with status", () => {
|
|
364
|
-
const result = formatTaskDetail(task, [], false);
|
|
365
|
-
expect(result).toContain("Blockers:");
|
|
366
|
-
expect(result).toContain("tk-c3d4");
|
|
367
|
-
expect(result).toContain("(blocked)");
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
test("shows resolved blockers", () => {
|
|
371
|
-
const resolvedTask: TaskWithMeta = {
|
|
372
|
-
...task,
|
|
373
|
-
blocked_by_incomplete: false,
|
|
374
|
-
};
|
|
375
|
-
const result = formatTaskDetail(resolvedTask, [], false);
|
|
376
|
-
expect(result).toContain("(resolved)");
|
|
377
|
-
});
|
|
378
|
-
|
|
379
|
-
test("shows overdue indicator", () => {
|
|
380
|
-
const overdueTask: TaskWithMeta = {
|
|
381
|
-
...task,
|
|
382
|
-
is_overdue: true,
|
|
383
|
-
};
|
|
384
|
-
const result = formatTaskDetail(overdueTask, [], false);
|
|
385
|
-
expect(result).toContain("[OVERDUE]");
|
|
386
|
-
});
|
|
387
|
-
|
|
388
|
-
test("includes log entries", () => {
|
|
389
|
-
const logs: LogEntry[] = [
|
|
390
|
-
{ ts: "2024-01-01T00:00:00.000Z", msg: "Started work" },
|
|
391
|
-
{ ts: "2024-01-02T00:00:00.000Z", msg: "Made progress" },
|
|
392
|
-
];
|
|
393
|
-
const result = formatTaskDetail(task, logs, false);
|
|
394
|
-
expect(result).toContain("Log:");
|
|
395
|
-
expect(result).toContain("Started work");
|
|
396
|
-
expect(result).toContain("Made progress");
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
test("shows completed_at for done tasks", () => {
|
|
400
|
-
const doneTask: TaskWithMeta = {
|
|
401
|
-
...task,
|
|
402
|
-
status: "done",
|
|
403
|
-
completed_at: "2024-01-02T00:00:00.000Z",
|
|
404
|
-
};
|
|
405
|
-
const result = formatTaskDetail(doneTask, [], false);
|
|
406
|
-
expect(result).toContain("Completed:");
|
|
407
|
-
});
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
describe("formatJson", () => {
|
|
411
|
-
test("formats objects as pretty JSON", () => {
|
|
412
|
-
const data = { foo: "bar", num: 42 };
|
|
413
|
-
const result = formatJson(data);
|
|
414
|
-
expect(result).toBe('{\n "foo": "bar",\n "num": 42\n}');
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
test("formats arrays", () => {
|
|
418
|
-
const data = [1, 2, 3];
|
|
419
|
-
const result = formatJson(data);
|
|
420
|
-
expect(result).toBe("[\n 1,\n 2,\n 3\n]");
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
test("handles null", () => {
|
|
424
|
-
expect(formatJson(null)).toBe("null");
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
test("handles nested objects", () => {
|
|
428
|
-
const data = { nested: { value: true } };
|
|
429
|
-
const result = formatJson(data);
|
|
430
|
-
expect(result).toContain('"nested"');
|
|
431
|
-
expect(result).toContain('"value"');
|
|
432
|
-
});
|
|
433
|
-
});
|
package/src/lib/format.ts
DELETED
|
@@ -1,189 +0,0 @@
|
|
|
1
|
-
import type { TaskWithMeta, LogEntry } from "../types";
|
|
2
|
-
import { PRIORITY_COLORS, STATUS_COLORS, OVERDUE_COLOR, RESET } from "../types";
|
|
3
|
-
import { formatPriority } from "./priority";
|
|
4
|
-
import { formatDate, formatRelativeTime, DUE_SOON_THRESHOLD } from "./time";
|
|
5
|
-
|
|
6
|
-
const DUE_SOON_COLOR = "\x1b[33m"; // yellow
|
|
7
|
-
|
|
8
|
-
// Message colors
|
|
9
|
-
const GREEN = "\x1b[32m";
|
|
10
|
-
const RED = "\x1b[31m";
|
|
11
|
-
const YELLOW = "\x1b[33m";
|
|
12
|
-
const DIM = "\x1b[2m";
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Determines if color output should be used for the given stream.
|
|
16
|
-
* Respects NO_COLOR env var (https://no-color.org/) and TTY detection.
|
|
17
|
-
*/
|
|
18
|
-
export function shouldUseColor(stream: NodeJS.WriteStream = process.stdout): boolean {
|
|
19
|
-
if (process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== "") {
|
|
20
|
-
return false;
|
|
21
|
-
}
|
|
22
|
-
return stream.isTTY ?? false;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Format text green (success) */
|
|
26
|
-
export function green(msg: string): string {
|
|
27
|
-
return shouldUseColor() ? `${GREEN}${msg}${RESET}` : msg;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Format text red (error) — checks stderr since errors are written there */
|
|
31
|
-
export function red(msg: string): string {
|
|
32
|
-
return shouldUseColor(process.stderr) ? `${RED}${msg}${RESET}` : msg;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/** Format text yellow (warning) */
|
|
36
|
-
export function yellow(msg: string): string {
|
|
37
|
-
return shouldUseColor() ? `${YELLOW}${msg}${RESET}` : msg;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** Format text dim/muted */
|
|
41
|
-
export function dim(msg: string): string {
|
|
42
|
-
return shouldUseColor() ? `${DIM}${msg}${RESET}` : msg;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function truncate(text: string, maxLen: number): string {
|
|
46
|
-
return text.length <= maxLen ? text : text.slice(0, maxLen - 1) + "…";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function formatId(id: string, maxLen = 11): string {
|
|
50
|
-
// Truncate project prefix to fit maxLen, keep full 4-char ref
|
|
51
|
-
// "myproject-a1b2" -> "mypr…-a1b2"
|
|
52
|
-
const dash = id.lastIndexOf("-");
|
|
53
|
-
if (dash === -1) return truncate(id, maxLen);
|
|
54
|
-
const project = id.slice(0, dash);
|
|
55
|
-
const ref = id.slice(dash + 1);
|
|
56
|
-
const maxProjectLen = maxLen - ref.length - 1; // -1 for dash
|
|
57
|
-
return project.length > maxProjectLen
|
|
58
|
-
? `${truncate(project, maxProjectLen)}-${ref}`
|
|
59
|
-
: `${project}-${ref}`;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export function formatTaskRow(task: TaskWithMeta, useColor?: boolean): string {
|
|
63
|
-
const color = useColor ?? shouldUseColor();
|
|
64
|
-
const id = formatId(task.id).padEnd(11);
|
|
65
|
-
const priority = formatPriority(task.priority).padEnd(4);
|
|
66
|
-
const isOverdue = task.is_overdue;
|
|
67
|
-
const dueSoon =
|
|
68
|
-
!isOverdue && task.days_until_due !== null && task.days_until_due <= DUE_SOON_THRESHOLD;
|
|
69
|
-
|
|
70
|
-
let statusText: string = task.status;
|
|
71
|
-
if (task.status === "done" && task.completed_at) {
|
|
72
|
-
statusText = `done ${formatRelativeTime(task.completed_at)}`;
|
|
73
|
-
}
|
|
74
|
-
const status = statusText.padEnd(12);
|
|
75
|
-
|
|
76
|
-
if (color) {
|
|
77
|
-
const pc = PRIORITY_COLORS[task.priority];
|
|
78
|
-
const sc = isOverdue ? OVERDUE_COLOR : dueSoon ? DUE_SOON_COLOR : STATUS_COLORS[task.status];
|
|
79
|
-
const tc = task.status === "done" ? DIM : "";
|
|
80
|
-
const title = truncate(task.title, 50);
|
|
81
|
-
return `${id} | ${pc}${priority}${RESET} | ${sc}${status}${RESET} | ${tc}${title}${RESET}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
let dueSoonMarker = "";
|
|
85
|
-
if (dueSoon) {
|
|
86
|
-
dueSoonMarker = task.days_until_due === 0 ? " [due today]" : ` [due ${task.days_until_due}d]`;
|
|
87
|
-
}
|
|
88
|
-
const overdueMarker = isOverdue ? " [OVERDUE]" : "";
|
|
89
|
-
const title = truncate(task.title, 50);
|
|
90
|
-
return `${id} | ${priority} | ${status} | ${title}${overdueMarker}${dueSoonMarker}`;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export function formatTaskList(
|
|
94
|
-
tasks: TaskWithMeta[],
|
|
95
|
-
useColor?: boolean,
|
|
96
|
-
emptyHint?: string,
|
|
97
|
-
): string {
|
|
98
|
-
if (tasks.length === 0) {
|
|
99
|
-
return emptyHint ?? "No tasks found. Run 'tk add \"title\"' to create one.";
|
|
100
|
-
}
|
|
101
|
-
const color = useColor ?? shouldUseColor();
|
|
102
|
-
|
|
103
|
-
const header = "ID | PRIO | STATUS | TITLE";
|
|
104
|
-
const divider = "-".repeat(65);
|
|
105
|
-
const rows = tasks.map((t) => formatTaskRow(t, color));
|
|
106
|
-
|
|
107
|
-
return [header, divider, ...rows].join("\n");
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export function formatTaskDetail(task: TaskWithMeta, logs: LogEntry[], useColor?: boolean): string {
|
|
111
|
-
const color = useColor ?? shouldUseColor();
|
|
112
|
-
const lines: string[] = [];
|
|
113
|
-
|
|
114
|
-
const sc = color ? STATUS_COLORS[task.status] : "";
|
|
115
|
-
const pc = color ? PRIORITY_COLORS[task.priority] : "";
|
|
116
|
-
const oc = color ? OVERDUE_COLOR : "";
|
|
117
|
-
const r = color ? RESET : "";
|
|
118
|
-
|
|
119
|
-
lines.push(`ID: ${task.id}`);
|
|
120
|
-
lines.push(`Title: ${task.title}`);
|
|
121
|
-
lines.push(`Status: ${sc}${task.status}${r}`);
|
|
122
|
-
lines.push(`Priority: ${pc}${formatPriority(task.priority)}${r}`);
|
|
123
|
-
|
|
124
|
-
if (task.description) {
|
|
125
|
-
lines.push(`Description: ${task.description}`);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
if (task.labels.length > 0) {
|
|
129
|
-
lines.push(`Labels: ${task.labels.join(", ")}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if (task.assignees.length > 0) {
|
|
133
|
-
lines.push(`Assignees: ${task.assignees.join(", ")}`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (task.parent) {
|
|
137
|
-
lines.push(`Parent: ${task.parent}`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (task.estimate !== null) {
|
|
141
|
-
lines.push(`Estimate: ${task.estimate}`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (task.due_date) {
|
|
145
|
-
const yc = color ? DUE_SOON_COLOR : "";
|
|
146
|
-
const dueSoon =
|
|
147
|
-
!task.is_overdue && task.days_until_due !== null && task.days_until_due <= DUE_SOON_THRESHOLD;
|
|
148
|
-
let dueSoonStr = "";
|
|
149
|
-
if (dueSoon) {
|
|
150
|
-
const marker = task.days_until_due === 0 ? "[due today]" : `[due ${task.days_until_due}d]`;
|
|
151
|
-
dueSoonStr = ` ${yc}${marker}${r}`;
|
|
152
|
-
}
|
|
153
|
-
const overdueStr = task.is_overdue ? ` ${oc}[OVERDUE]${r}` : "";
|
|
154
|
-
lines.push(`Due: ${task.due_date}${overdueStr}${dueSoonStr}`);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
lines.push(`Created: ${formatDate(task.created_at)}`);
|
|
158
|
-
lines.push(`Updated: ${formatDate(task.updated_at)}`);
|
|
159
|
-
|
|
160
|
-
if (task.completed_at) {
|
|
161
|
-
lines.push(`Completed: ${formatDate(task.completed_at)}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
if (task.blocked_by.length > 0) {
|
|
165
|
-
const blockStatus = task.blocked_by_incomplete ? " (blocked)" : " (resolved)";
|
|
166
|
-
lines.push(`Blockers: ${task.blocked_by.join(", ")}${blockStatus}`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (logs.length > 0) {
|
|
170
|
-
lines.push("");
|
|
171
|
-
lines.push("Log:");
|
|
172
|
-
for (const entry of logs) {
|
|
173
|
-
lines.push(` [${formatDate(entry.ts)}] ${entry.msg}`);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return lines.join("\n");
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
export function formatJson(data: unknown): string {
|
|
181
|
-
return JSON.stringify(data, null, 2);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
export function formatConfig(config: { version: number; project: string }): string {
|
|
185
|
-
const lines: string[] = [];
|
|
186
|
-
lines.push(`Version: ${config.version}`);
|
|
187
|
-
lines.push(`Project: ${config.project}`);
|
|
188
|
-
return lines.join("\n");
|
|
189
|
-
}
|