@nijaru/tk 0.0.2 → 0.0.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/README.md +8 -8
- package/package.json +1 -1
- package/src/cli.test.ts +83 -3
- package/src/cli.ts +71 -136
- package/src/db/storage.ts +135 -113
- package/src/lib/completions.ts +56 -56
- package/src/lib/format.test.ts +18 -8
- package/src/lib/format.ts +13 -25
- package/src/lib/time.ts +115 -0
package/README.md
CHANGED
|
@@ -33,9 +33,9 @@ myapp-x9k2
|
|
|
33
33
|
$ tk block x9k2 a7b3 # tests blocked by auth (just use ref)
|
|
34
34
|
|
|
35
35
|
$ tk ready # what can I work on?
|
|
36
|
-
ID | PRIO | STATUS
|
|
37
|
-
|
|
38
|
-
myapp-a7b3 | p1 | open
|
|
36
|
+
ID | PRIO | STATUS | TITLE
|
|
37
|
+
-----------------------------------------------------------------
|
|
38
|
+
myapp-a7b3 | p1 | open | Implement auth
|
|
39
39
|
|
|
40
40
|
$ tk start a7b3 # just the ref works everywhere
|
|
41
41
|
Started: myapp-a7b3
|
|
@@ -47,9 +47,9 @@ $ tk done a7b3
|
|
|
47
47
|
Completed: myapp-a7b3
|
|
48
48
|
|
|
49
49
|
$ tk ready # tests now unblocked
|
|
50
|
-
ID | PRIO | STATUS
|
|
51
|
-
|
|
52
|
-
myapp-x9k2 | p2 | open
|
|
50
|
+
ID | PRIO | STATUS | TITLE
|
|
51
|
+
-----------------------------------------------------------------
|
|
52
|
+
myapp-x9k2 | p2 | open | Write tests
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
## Commands
|
|
@@ -59,7 +59,7 @@ myapp-x9k2 | p2 | open | Write tests
|
|
|
59
59
|
| `tk init` | Initialize (project name from directory) |
|
|
60
60
|
| `tk add <title>` | Create task |
|
|
61
61
|
| `tk ls` / `tk list` | List tasks |
|
|
62
|
-
| `tk ready` | List open + unblocked tasks
|
|
62
|
+
| `tk ready` | List active/open + unblocked tasks |
|
|
63
63
|
| `tk show <id>` | Show task details |
|
|
64
64
|
| `tk start <id>` | Start working (open → active) |
|
|
65
65
|
| `tk done <id>` | Complete task |
|
|
@@ -130,7 +130,7 @@ tk clean --force # Force clean even if disabled in config
|
|
|
130
130
|
tk config # Show all config
|
|
131
131
|
tk config project # Show default project
|
|
132
132
|
tk config project api # Set default project
|
|
133
|
-
tk config project lsmvec --rename cloudlsmvec # Rename
|
|
133
|
+
tk config project lsmvec --rename cloudlsmvec # Rename cloudlsmvec-* → lsmvec-*
|
|
134
134
|
tk config alias # List aliases
|
|
135
135
|
tk config alias web src/web # Add alias
|
|
136
136
|
tk config alias --rm web # Remove alias
|
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -243,16 +243,96 @@ describe("tk CLI", () => {
|
|
|
243
243
|
const { stdout } = await run(["ls"], testDir);
|
|
244
244
|
expect(stdout).toContain("No tasks found");
|
|
245
245
|
});
|
|
246
|
+
|
|
247
|
+
test("sorts by status (active > open > done)", async () => {
|
|
248
|
+
const { stdout: idDone } = await run(["add", "Done task"], testDir);
|
|
249
|
+
await run(["done", idDone.trim()], testDir);
|
|
250
|
+
const { stdout: idActive } = await run(["add", "Active task"], testDir);
|
|
251
|
+
await run(["start", idActive.trim()], testDir);
|
|
252
|
+
await run(["add", "Open task"], testDir);
|
|
253
|
+
|
|
254
|
+
const { stdout } = await run(["ls"], testDir);
|
|
255
|
+
const lines = stdout
|
|
256
|
+
.trim()
|
|
257
|
+
.split("\n")
|
|
258
|
+
.filter((l) => l.includes("task"));
|
|
259
|
+
expect(lines[0]).toContain("Active task");
|
|
260
|
+
expect(lines[1]).toContain("Open task");
|
|
261
|
+
expect(lines[2]).toContain("Done task");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("sorts by priority (p1-4, then p0/none)", async () => {
|
|
265
|
+
await run(["add", "Medium", "-p", "3"], testDir);
|
|
266
|
+
await run(["add", "Urgent", "-p", "1"], testDir);
|
|
267
|
+
await run(["add", "None", "-p", "0"], testDir);
|
|
268
|
+
await run(["add", "Low", "-p", "4"], testDir);
|
|
269
|
+
|
|
270
|
+
const { stdout } = await run(["ls"], testDir);
|
|
271
|
+
const lines = stdout
|
|
272
|
+
.trim()
|
|
273
|
+
.split("\n")
|
|
274
|
+
.filter((l) => /Medium|Urgent|None|Low/.test(l));
|
|
275
|
+
expect(lines[0]).toContain("Urgent");
|
|
276
|
+
expect(lines[1]).toContain("Medium");
|
|
277
|
+
expect(lines[2]).toContain("Low");
|
|
278
|
+
expect(lines[3]).toContain("None");
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("hoists overdue tasks to the top of their status group", async () => {
|
|
282
|
+
await run(["add", "Urgent Future", "-p", "1", "--due", "2099-01-01"], testDir);
|
|
283
|
+
await run(["add", "Low Overdue", "-p", "4", "--due", "2020-01-01"], testDir);
|
|
284
|
+
|
|
285
|
+
const { stdout } = await run(["ls"], testDir);
|
|
286
|
+
const lines = stdout
|
|
287
|
+
.trim()
|
|
288
|
+
.split("\n")
|
|
289
|
+
.filter((l) => /Future|Overdue/.test(l));
|
|
290
|
+
expect(lines[0]).toContain("Low Overdue");
|
|
291
|
+
expect(lines[1]).toContain("Urgent Future");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("sorts by due date when priority is equal", async () => {
|
|
295
|
+
await run(["add", "Later", "-p", "3", "--due", "2099-01-02"], testDir);
|
|
296
|
+
await run(["add", "Earlier", "-p", "3", "--due", "2099-01-01"], testDir);
|
|
297
|
+
|
|
298
|
+
const { stdout } = await run(["ls"], testDir);
|
|
299
|
+
const lines = stdout
|
|
300
|
+
.trim()
|
|
301
|
+
.split("\n")
|
|
302
|
+
.filter((l) => /Earlier|Later/.test(l));
|
|
303
|
+
expect(lines[0]).toContain("Earlier");
|
|
304
|
+
expect(lines[1]).toContain("Later");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("sorts done tasks by completion time (newest first)", async () => {
|
|
308
|
+
const { stdout: id1 } = await run(["add", "Done First"], testDir);
|
|
309
|
+
const { stdout: id2 } = await run(["add", "Done Second"], testDir);
|
|
310
|
+
await run(["done", id1.trim()], testDir);
|
|
311
|
+
// Ensure time difference
|
|
312
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
313
|
+
await run(["done", id2.trim()], testDir);
|
|
314
|
+
|
|
315
|
+
const { stdout } = await run(["ls", "-s", "done"], testDir);
|
|
316
|
+
const lines = stdout
|
|
317
|
+
.trim()
|
|
318
|
+
.split("\n")
|
|
319
|
+
.filter((l) => l.includes("Done"));
|
|
320
|
+
expect(lines[0]).toContain("Done Second");
|
|
321
|
+
expect(lines[1]).toContain("Done First");
|
|
322
|
+
});
|
|
246
323
|
});
|
|
247
324
|
|
|
248
325
|
describe("ready", () => {
|
|
249
|
-
test("shows
|
|
250
|
-
const { stdout: id1 } = await run(["add", "
|
|
326
|
+
test("shows active and unblocked open tasks", async () => {
|
|
327
|
+
const { stdout: id1 } = await run(["add", "Active task"], testDir);
|
|
328
|
+
await run(["start", id1.trim()], testDir);
|
|
329
|
+
await run(["add", "Open task"], testDir);
|
|
251
330
|
const { stdout: id2 } = await run(["add", "Blocked task"], testDir);
|
|
252
331
|
await run(["block", id2.trim(), id1.trim()], testDir);
|
|
253
332
|
|
|
254
333
|
const { stdout } = await run(["ready"], testDir);
|
|
255
|
-
expect(stdout).toContain("
|
|
334
|
+
expect(stdout).toContain("Active task");
|
|
335
|
+
expect(stdout).toContain("Open task");
|
|
256
336
|
expect(stdout).not.toContain("Blocked task");
|
|
257
337
|
});
|
|
258
338
|
|
package/src/cli.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { version } from "../package.json";
|
|
|
4
4
|
|
|
5
5
|
import * as storage from "./db/storage";
|
|
6
6
|
import { parsePriority } from "./lib/priority";
|
|
7
|
+
import { parseDueDate, parseEstimate } from "./lib/time";
|
|
7
8
|
import {
|
|
8
9
|
formatTaskList,
|
|
9
10
|
formatTaskDetail,
|
|
@@ -15,12 +16,27 @@ import {
|
|
|
15
16
|
} from "./lib/format";
|
|
16
17
|
import { findRoot, setWorkingDir } from "./lib/root";
|
|
17
18
|
import { parseId } from "./types";
|
|
18
|
-
import type { Status } from "./types";
|
|
19
|
+
import type { Status, TaskWithMeta } from "./types";
|
|
19
20
|
import { BASH_COMPLETION, ZSH_COMPLETION, FISH_COMPLETION } from "./lib/completions";
|
|
20
21
|
|
|
21
22
|
const VALID_STATUSES: Status[] = ["open", "active", "done"];
|
|
22
23
|
const PROJECT_PATTERN = /^[a-z][a-z0-9]*$/;
|
|
23
24
|
|
|
25
|
+
const COMMON_OPTIONS = {
|
|
26
|
+
project: { type: "string", short: "P" },
|
|
27
|
+
priority: { type: "string", short: "p" },
|
|
28
|
+
labels: { type: "string", short: "l" },
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
const TASK_MUTATION_OPTIONS = {
|
|
32
|
+
...COMMON_OPTIONS,
|
|
33
|
+
description: { type: "string", short: "d" },
|
|
34
|
+
assignees: { type: "string", short: "A" },
|
|
35
|
+
parent: { type: "string" },
|
|
36
|
+
estimate: { type: "string" },
|
|
37
|
+
due: { type: "string" },
|
|
38
|
+
} as const;
|
|
39
|
+
|
|
24
40
|
function validateProject(name: string): void {
|
|
25
41
|
if (!PROJECT_PATTERN.test(name)) {
|
|
26
42
|
throw new Error(
|
|
@@ -39,74 +55,16 @@ function parseStatus(input: string | undefined): Status | undefined {
|
|
|
39
55
|
|
|
40
56
|
function parseLimit(input: string | undefined): number | undefined {
|
|
41
57
|
if (!input) return undefined;
|
|
42
|
-
|
|
43
|
-
if (isNaN(n) || n < 1) {
|
|
58
|
+
if (!/^\d+$/.test(input)) {
|
|
44
59
|
throw new Error(`Invalid limit: ${input}. Must be a positive number.`);
|
|
45
60
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
function parseEstimate(input: string | undefined): number | undefined {
|
|
50
|
-
if (!input) return undefined;
|
|
51
|
-
const n = parseInt(input, 10);
|
|
52
|
-
if (isNaN(n) || n < 0) {
|
|
53
|
-
throw new Error(`Invalid estimate: ${input}. Must be a non-negative number.`);
|
|
61
|
+
const n = Number(input);
|
|
62
|
+
if (n < 1) {
|
|
63
|
+
throw new Error(`Invalid limit: ${input}. Must be a positive number.`);
|
|
54
64
|
}
|
|
55
65
|
return n;
|
|
56
66
|
}
|
|
57
67
|
|
|
58
|
-
function formatLocalDate(date: Date): string {
|
|
59
|
-
const year = date.getFullYear();
|
|
60
|
-
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
61
|
-
const day = String(date.getDate()).padStart(2, "0");
|
|
62
|
-
return `${year}-${month}-${day}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function parseDueDate(input: string | undefined): string | undefined {
|
|
66
|
-
if (!input) return undefined;
|
|
67
|
-
if (input === "-") return undefined; // clear
|
|
68
|
-
|
|
69
|
-
// Handle relative dates like +7d
|
|
70
|
-
if (input.startsWith("+")) {
|
|
71
|
-
const match = input.match(/^\+(\d+)([dwmh])$/);
|
|
72
|
-
if (match && match[1] && match[2]) {
|
|
73
|
-
const num = match[1];
|
|
74
|
-
const unit = match[2];
|
|
75
|
-
const n = parseInt(num, 10);
|
|
76
|
-
const now = new Date();
|
|
77
|
-
switch (unit) {
|
|
78
|
-
case "h":
|
|
79
|
-
now.setHours(now.getHours() + n);
|
|
80
|
-
break;
|
|
81
|
-
case "d":
|
|
82
|
-
now.setDate(now.getDate() + n);
|
|
83
|
-
break;
|
|
84
|
-
case "w":
|
|
85
|
-
now.setDate(now.getDate() + n * 7);
|
|
86
|
-
break;
|
|
87
|
-
case "m":
|
|
88
|
-
now.setMonth(now.getMonth() + n);
|
|
89
|
-
break;
|
|
90
|
-
}
|
|
91
|
-
return formatLocalDate(now);
|
|
92
|
-
}
|
|
93
|
-
throw new Error(`Invalid relative date: ${input}. Use format like +7d, +2w, +1m`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// Validate YYYY-MM-DD format - return as-is to avoid timezone issues
|
|
97
|
-
const dateMatch = input.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
98
|
-
if (dateMatch) {
|
|
99
|
-
const [, , month, day] = dateMatch;
|
|
100
|
-
const m = parseInt(month!, 10);
|
|
101
|
-
const d = parseInt(day!, 10);
|
|
102
|
-
// Basic validation: month 1-12, day 1-31
|
|
103
|
-
if (m >= 1 && m <= 12 && d >= 1 && d <= 31) {
|
|
104
|
-
return input; // Return as-is, already in correct format
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
throw new Error(`Invalid date: ${input}. Use YYYY-MM-DD or +Nd format.`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
68
|
function parseLabels(input: string | undefined): string[] | undefined {
|
|
111
69
|
if (!input) return undefined;
|
|
112
70
|
return input
|
|
@@ -146,9 +104,6 @@ function resolveId(input: string | undefined, context: string): string {
|
|
|
146
104
|
const resolved = storage.resolveId(input);
|
|
147
105
|
if (resolved) return resolved;
|
|
148
106
|
|
|
149
|
-
// Check if it's a valid full ID format
|
|
150
|
-
if (parseId(input)) return input;
|
|
151
|
-
|
|
152
107
|
// Check for ambiguous matches
|
|
153
108
|
const matches = storage.findMatchingIds(input);
|
|
154
109
|
if (matches.length > 1) {
|
|
@@ -157,9 +112,26 @@ function resolveId(input: string | undefined, context: string): string {
|
|
|
157
112
|
);
|
|
158
113
|
}
|
|
159
114
|
|
|
115
|
+
// If it's a valid full ID format but doesn't exist, we still return it
|
|
116
|
+
// and let getTask handle the "not found" error consistently.
|
|
117
|
+
if (parseId(input)) return input;
|
|
118
|
+
|
|
160
119
|
throw new Error(`Task not found: ${input}`);
|
|
161
120
|
}
|
|
162
121
|
|
|
122
|
+
/**
|
|
123
|
+
* Resolves an ID and fetches the task.
|
|
124
|
+
* Handles "Task not found" and cleanup output automatically.
|
|
125
|
+
*/
|
|
126
|
+
function resolveTask(input: string | undefined, context: string): TaskWithMeta {
|
|
127
|
+
const id = resolveId(input, context);
|
|
128
|
+
const result = storage.getTask(id);
|
|
129
|
+
if (!result) error(`Task not found: ${id}`);
|
|
130
|
+
|
|
131
|
+
outputCleanup(id, result.cleanup);
|
|
132
|
+
return result.task;
|
|
133
|
+
}
|
|
134
|
+
|
|
163
135
|
const rawArgs = process.argv.slice(2);
|
|
164
136
|
|
|
165
137
|
function isFlag(arg: string): boolean {
|
|
@@ -233,7 +205,7 @@ COMMANDS:
|
|
|
233
205
|
init Initialize .tasks/ directory
|
|
234
206
|
add Create task
|
|
235
207
|
ls, list List tasks
|
|
236
|
-
ready List ready tasks (open + unblocked)
|
|
208
|
+
ready List ready tasks (active/open + unblocked)
|
|
237
209
|
show Show task details
|
|
238
210
|
start Start working (open -> active)
|
|
239
211
|
done Complete task
|
|
@@ -314,12 +286,12 @@ EXAMPLES:
|
|
|
314
286
|
|
|
315
287
|
Run 'tk ls --help' for options.
|
|
316
288
|
`,
|
|
317
|
-
ready: `tk ready - List ready tasks (open + unblocked)
|
|
289
|
+
ready: `tk ready - List ready tasks (active/open + unblocked)
|
|
318
290
|
|
|
319
291
|
USAGE:
|
|
320
292
|
tk ready
|
|
321
293
|
|
|
322
|
-
Shows open tasks that are not blocked by any incomplete task.
|
|
294
|
+
Shows active or open tasks that are not blocked by any incomplete task.
|
|
323
295
|
`,
|
|
324
296
|
show: `tk show - Show task details
|
|
325
297
|
|
|
@@ -538,16 +510,7 @@ function main() {
|
|
|
538
510
|
case "add": {
|
|
539
511
|
const { values, positionals } = parseArgs({
|
|
540
512
|
args,
|
|
541
|
-
options:
|
|
542
|
-
description: { type: "string", short: "d" },
|
|
543
|
-
priority: { type: "string", short: "p" },
|
|
544
|
-
project: { type: "string", short: "P" },
|
|
545
|
-
labels: { type: "string", short: "l" },
|
|
546
|
-
assignees: { type: "string", short: "A" },
|
|
547
|
-
parent: { type: "string" },
|
|
548
|
-
estimate: { type: "string" },
|
|
549
|
-
due: { type: "string" },
|
|
550
|
-
},
|
|
513
|
+
options: TASK_MUTATION_OPTIONS,
|
|
551
514
|
allowPositionals: true,
|
|
552
515
|
});
|
|
553
516
|
const title = positionals[0]?.trim();
|
|
@@ -588,10 +551,9 @@ function main() {
|
|
|
588
551
|
const { values } = parseArgs({
|
|
589
552
|
args,
|
|
590
553
|
options: {
|
|
554
|
+
...COMMON_OPTIONS,
|
|
555
|
+
label: COMMON_OPTIONS.labels, // alias for consistency
|
|
591
556
|
status: { type: "string", short: "s" },
|
|
592
|
-
priority: { type: "string", short: "p" },
|
|
593
|
-
project: { type: "string", short: "P" },
|
|
594
|
-
label: { type: "string", short: "l" },
|
|
595
557
|
assignee: { type: "string" },
|
|
596
558
|
parent: { type: "string" },
|
|
597
559
|
roots: { type: "boolean" },
|
|
@@ -617,7 +579,7 @@ function main() {
|
|
|
617
579
|
status,
|
|
618
580
|
priority,
|
|
619
581
|
project: values.project,
|
|
620
|
-
label: values.label,
|
|
582
|
+
label: values.label ?? values.labels,
|
|
621
583
|
assignee: values.assignee,
|
|
622
584
|
parent: parentFilter,
|
|
623
585
|
roots: values.roots,
|
|
@@ -635,44 +597,32 @@ function main() {
|
|
|
635
597
|
}
|
|
636
598
|
|
|
637
599
|
case "show": {
|
|
638
|
-
const
|
|
639
|
-
|
|
640
|
-
if (!result) error(`Task not found: ${id}`);
|
|
641
|
-
outputCleanup(id, result.cleanup);
|
|
642
|
-
output(result.task, formatTaskDetail(result.task, result.task.logs));
|
|
600
|
+
const task = resolveTask(args[0], "show");
|
|
601
|
+
output(task, formatTaskDetail(task, task.logs));
|
|
643
602
|
break;
|
|
644
603
|
}
|
|
645
604
|
|
|
646
605
|
case "start": {
|
|
647
|
-
const
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
if (result.task.status === "done") error(`Task already done. Use 'tk reopen ${id}' first.`);
|
|
654
|
-
const updated = storage.updateTaskStatus(id, "active");
|
|
655
|
-
output(updated, green(`Started: ${id}`));
|
|
606
|
+
const task = resolveTask(args[0], "start");
|
|
607
|
+
if (task.status === "active")
|
|
608
|
+
error(`Task already active. Use 'tk done ${task.id}' to complete it.`);
|
|
609
|
+
if (task.status === "done") error(`Task already done. Use 'tk reopen ${task.id}' first.`);
|
|
610
|
+
const updated = storage.updateTaskStatus(task.id, "active");
|
|
611
|
+
output(updated, green(`Started: ${task.id}`));
|
|
656
612
|
break;
|
|
657
613
|
}
|
|
658
614
|
|
|
659
615
|
case "done": {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
outputCleanup(id, result.cleanup);
|
|
664
|
-
const updated = storage.updateTaskStatus(id, "done");
|
|
665
|
-
output(updated, green(`Completed: ${id}`));
|
|
616
|
+
const task = resolveTask(args[0], "done");
|
|
617
|
+
const updated = storage.updateTaskStatus(task.id, "done");
|
|
618
|
+
output(updated, green(`Completed: ${task.id}`));
|
|
666
619
|
break;
|
|
667
620
|
}
|
|
668
621
|
|
|
669
622
|
case "reopen": {
|
|
670
|
-
const
|
|
671
|
-
const
|
|
672
|
-
|
|
673
|
-
outputCleanup(id, result.cleanup);
|
|
674
|
-
const updated = storage.updateTaskStatus(id, "open");
|
|
675
|
-
output(updated, green(`Reopened: ${id}`));
|
|
623
|
+
const task = resolveTask(args[0], "reopen");
|
|
624
|
+
const updated = storage.updateTaskStatus(task.id, "open");
|
|
625
|
+
output(updated, green(`Reopened: ${task.id}`));
|
|
676
626
|
break;
|
|
677
627
|
}
|
|
678
628
|
|
|
@@ -680,21 +630,12 @@ function main() {
|
|
|
680
630
|
const { values, positionals } = parseArgs({
|
|
681
631
|
args,
|
|
682
632
|
options: {
|
|
633
|
+
...TASK_MUTATION_OPTIONS,
|
|
683
634
|
title: { type: "string", short: "t" },
|
|
684
|
-
description: { type: "string", short: "d" },
|
|
685
|
-
priority: { type: "string", short: "p" },
|
|
686
|
-
labels: { type: "string", short: "l" },
|
|
687
|
-
assignees: { type: "string", short: "A" },
|
|
688
|
-
parent: { type: "string" },
|
|
689
|
-
estimate: { type: "string" },
|
|
690
|
-
due: { type: "string" },
|
|
691
635
|
},
|
|
692
636
|
allowPositionals: true,
|
|
693
637
|
});
|
|
694
|
-
const
|
|
695
|
-
const result = storage.getTask(id);
|
|
696
|
-
if (!result) error(`Task not found: ${id}`);
|
|
697
|
-
outputCleanup(id, result.cleanup);
|
|
638
|
+
const task = resolveTask(positionals[0], "edit");
|
|
698
639
|
|
|
699
640
|
// Handle label modifications (+tag, -tag)
|
|
700
641
|
let labels: string[] | undefined;
|
|
@@ -702,13 +643,11 @@ function main() {
|
|
|
702
643
|
if (values.labels.startsWith("+")) {
|
|
703
644
|
// Add label (avoid duplicates)
|
|
704
645
|
const newLabel = values.labels.slice(1);
|
|
705
|
-
labels =
|
|
706
|
-
? result.task.labels
|
|
707
|
-
: [...result.task.labels, newLabel];
|
|
646
|
+
labels = task.labels.includes(newLabel) ? task.labels : [...task.labels, newLabel];
|
|
708
647
|
} else if (values.labels.startsWith("-")) {
|
|
709
648
|
// Remove label
|
|
710
649
|
const removeLabel = values.labels.slice(1);
|
|
711
|
-
labels =
|
|
650
|
+
labels = task.labels.filter((l: string) => l !== removeLabel);
|
|
712
651
|
} else {
|
|
713
652
|
// Replace labels
|
|
714
653
|
labels = parseLabels(values.labels);
|
|
@@ -723,11 +662,11 @@ function main() {
|
|
|
723
662
|
const resolved = storage.resolveId(values.parent);
|
|
724
663
|
if (!resolved) error(`Parent task not found: ${values.parent}`);
|
|
725
664
|
resolvedParent = resolved;
|
|
726
|
-
const parentResult = storage.validateParent(resolvedParent, id);
|
|
665
|
+
const parentResult = storage.validateParent(resolvedParent, task.id);
|
|
727
666
|
if (!parentResult.ok) error(parentResult.error!);
|
|
728
667
|
}
|
|
729
668
|
|
|
730
|
-
const updated = storage.updateTask(id, {
|
|
669
|
+
const updated = storage.updateTask(task.id, {
|
|
731
670
|
title: values.title?.trim() || undefined,
|
|
732
671
|
description: values.description,
|
|
733
672
|
priority: values.priority ? parsePriority(values.priority) : undefined,
|
|
@@ -737,12 +676,12 @@ function main() {
|
|
|
737
676
|
estimate: values.estimate === "-" ? null : (parseEstimate(values.estimate) ?? undefined),
|
|
738
677
|
due_date: values.due === "-" ? null : (parseDueDate(values.due) ?? undefined),
|
|
739
678
|
});
|
|
740
|
-
output(updated, green(`Updated: ${id}`));
|
|
679
|
+
output(updated, green(`Updated: ${task.id}`));
|
|
741
680
|
break;
|
|
742
681
|
}
|
|
743
682
|
|
|
744
683
|
case "log": {
|
|
745
|
-
const
|
|
684
|
+
const task = resolveTask(args[0], "log");
|
|
746
685
|
const message = args[1]?.trim();
|
|
747
686
|
if (!message) error('Message required: tk log <id> "<message>"');
|
|
748
687
|
if (args.length > 2) {
|
|
@@ -751,11 +690,8 @@ function main() {
|
|
|
751
690
|
` Got ${args.length - 1} arguments instead of 1`,
|
|
752
691
|
);
|
|
753
692
|
}
|
|
754
|
-
const
|
|
755
|
-
|
|
756
|
-
outputCleanup(id, result.cleanup);
|
|
757
|
-
const entry = storage.addLogEntry(id, message);
|
|
758
|
-
output(entry, green(`Logged: ${id}`));
|
|
693
|
+
const entry = storage.addLogEntry(task.id, message);
|
|
694
|
+
output(entry, green(`Logged: ${task.id}`));
|
|
759
695
|
break;
|
|
760
696
|
}
|
|
761
697
|
|
|
@@ -809,11 +745,10 @@ function main() {
|
|
|
809
745
|
// Get days from CLI or config
|
|
810
746
|
let days: number | false;
|
|
811
747
|
if (values["older-than"] !== undefined) {
|
|
812
|
-
|
|
813
|
-
if (isNaN(n) || n < 0) {
|
|
748
|
+
if (!/^\d+$/.test(values["older-than"])) {
|
|
814
749
|
error(`Invalid --older-than: ${values["older-than"]}. Use a number of days.`);
|
|
815
750
|
}
|
|
816
|
-
days =
|
|
751
|
+
days = Number(values["older-than"]);
|
|
817
752
|
} else {
|
|
818
753
|
days = config.clean_after;
|
|
819
754
|
// Validate config value at runtime
|