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