@nijaru/tk 0.0.1 → 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 +34 -21
- package/package.json +4 -4
- package/src/cli.test.ts +348 -8
- package/src/cli.ts +240 -184
- package/src/db/storage.ts +389 -152
- package/src/lib/completions.ts +68 -61
- package/src/lib/format.test.ts +18 -8
- package/src/lib/format.ts +39 -25
- package/src/lib/time.ts +115 -0
- package/src/types.ts +2 -2
package/README.md
CHANGED
|
@@ -1,22 +1,20 @@
|
|
|
1
1
|
# tk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Minimal task tracker. Simple, fast, git-friendly.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
- Plain JSON files in `.tasks/`
|
|
6
|
+
- No daemons, no merge conflicts, no viruses
|
|
6
7
|
|
|
7
8
|
## Install
|
|
8
9
|
|
|
10
|
+
Requires [Bun](https://bun.sh) runtime.
|
|
11
|
+
|
|
9
12
|
```bash
|
|
10
13
|
# Install Bun first (if not installed)
|
|
11
14
|
curl -fsSL https://bun.sh/install | bash
|
|
12
15
|
|
|
13
16
|
# Then install tk globally
|
|
14
17
|
bun add -g @nijaru/tk
|
|
15
|
-
|
|
16
|
-
# Or run from source
|
|
17
|
-
git clone https://github.com/nijaru/tk.git
|
|
18
|
-
cd tk && bun install
|
|
19
|
-
bun run src/cli.ts --help
|
|
20
18
|
```
|
|
21
19
|
|
|
22
20
|
## Quick Start
|
|
@@ -35,9 +33,9 @@ myapp-x9k2
|
|
|
35
33
|
$ tk block x9k2 a7b3 # tests blocked by auth (just use ref)
|
|
36
34
|
|
|
37
35
|
$ tk ready # what can I work on?
|
|
38
|
-
ID
|
|
39
|
-
|
|
40
|
-
myapp-a7b3
|
|
36
|
+
ID | PRIO | STATUS | TITLE
|
|
37
|
+
-----------------------------------------------------------------
|
|
38
|
+
myapp-a7b3 | p1 | open | Implement auth
|
|
41
39
|
|
|
42
40
|
$ tk start a7b3 # just the ref works everywhere
|
|
43
41
|
Started: myapp-a7b3
|
|
@@ -49,9 +47,9 @@ $ tk done a7b3
|
|
|
49
47
|
Completed: myapp-a7b3
|
|
50
48
|
|
|
51
49
|
$ tk ready # tests now unblocked
|
|
52
|
-
ID
|
|
53
|
-
|
|
54
|
-
myapp-x9k2
|
|
50
|
+
ID | PRIO | STATUS | TITLE
|
|
51
|
+
-----------------------------------------------------------------
|
|
52
|
+
myapp-x9k2 | p2 | open | Write tests
|
|
55
53
|
```
|
|
56
54
|
|
|
57
55
|
## Commands
|
|
@@ -61,17 +59,18 @@ myapp-x9k2 p2 open Write tests
|
|
|
61
59
|
| `tk init` | Initialize (project name from directory) |
|
|
62
60
|
| `tk add <title>` | Create task |
|
|
63
61
|
| `tk ls` / `tk list` | List tasks |
|
|
64
|
-
| `tk ready` | List open + unblocked tasks
|
|
62
|
+
| `tk ready` | List active/open + unblocked tasks |
|
|
65
63
|
| `tk show <id>` | Show task details |
|
|
66
64
|
| `tk start <id>` | Start working (open → active) |
|
|
67
65
|
| `tk done <id>` | Complete task |
|
|
68
66
|
| `tk reopen <id>` | Reopen task |
|
|
69
67
|
| `tk edit <id>` | Edit task |
|
|
70
|
-
| `tk log <id> <msg
|
|
68
|
+
| `tk log <id> "<msg>"` | Add log entry |
|
|
71
69
|
| `tk block <id> <blocker>` | Add dependency (id blocked by blocker) |
|
|
72
70
|
| `tk unblock <id> <blocker>` | Remove dependency |
|
|
73
71
|
| `tk rm` / `tk remove <id>` | Delete task |
|
|
74
|
-
| `tk clean` | Remove old done tasks (default:
|
|
72
|
+
| `tk clean` | Remove old done tasks (default: 14 days) |
|
|
73
|
+
| `tk check` | Check task integrity |
|
|
75
74
|
| `tk config` | Show/set configuration |
|
|
76
75
|
| `tk completions <shell>` | Output shell completions (bash, zsh, fish) |
|
|
77
76
|
| `tk help [command]` | Show help (or command-specific help) |
|
|
@@ -120,9 +119,9 @@ tk edit a7b3 --parent - # Clear parent
|
|
|
120
119
|
## Clean Options
|
|
121
120
|
|
|
122
121
|
```bash
|
|
123
|
-
tk clean # Remove done tasks older than
|
|
124
|
-
tk clean --older-than
|
|
125
|
-
tk clean
|
|
122
|
+
tk clean # Remove done tasks older than config (default: 14 days)
|
|
123
|
+
tk clean --older-than 30 # Custom threshold (days)
|
|
124
|
+
tk clean --force # Force clean even if disabled in config
|
|
126
125
|
```
|
|
127
126
|
|
|
128
127
|
## Config
|
|
@@ -131,12 +130,26 @@ tk clean -a # Remove all done tasks (ignore age)
|
|
|
131
130
|
tk config # Show all config
|
|
132
131
|
tk config project # Show default project
|
|
133
132
|
tk config project api # Set default project
|
|
134
|
-
tk config project lsmvec --rename cloudlsmvec # Rename
|
|
133
|
+
tk config project lsmvec --rename cloudlsmvec # Rename cloudlsmvec-* → lsmvec-*
|
|
135
134
|
tk config alias # List aliases
|
|
136
135
|
tk config alias web src/web # Add alias
|
|
137
136
|
tk config alias --rm web # Remove alias
|
|
138
137
|
```
|
|
139
138
|
|
|
139
|
+
Config file (`.tasks/config.json`):
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"version": 1,
|
|
144
|
+
"project": "myapp",
|
|
145
|
+
"clean_after": 14,
|
|
146
|
+
"defaults": { "priority": 3, "labels": [], "assignees": [] },
|
|
147
|
+
"aliases": {}
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
- `clean_after`: Days to keep done tasks (default: 14). Set to `false` to disable cleaning.
|
|
152
|
+
|
|
140
153
|
## Task IDs
|
|
141
154
|
|
|
142
155
|
Format: `project-ref` (e.g., `myapp-a7b3`, `api-x9k2`, `web-3m8p`)
|
|
@@ -226,4 +239,4 @@ tk completions fish | source
|
|
|
226
239
|
|
|
227
240
|
## License
|
|
228
241
|
|
|
229
|
-
MIT
|
|
242
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nijaru/tk",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "Minimal task tracker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"tk": "
|
|
7
|
+
"tk": "src/cli.ts"
|
|
8
8
|
},
|
|
9
9
|
"files": [
|
|
10
10
|
"src",
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"license": "MIT",
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
|
-
"url": "https://github.com/nijaru/tk"
|
|
40
|
+
"url": "git+https://github.com/nijaru/tk.git"
|
|
41
41
|
},
|
|
42
42
|
"author": "nijaru",
|
|
43
43
|
"homepage": "https://github.com/nijaru/tk#readme",
|
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
|
|
|
@@ -298,13 +378,24 @@ describe("tk CLI", () => {
|
|
|
298
378
|
expect(task.completed_at).toBeNull();
|
|
299
379
|
});
|
|
300
380
|
|
|
301
|
-
test("start errors if
|
|
381
|
+
test("start errors if already active", async () => {
|
|
302
382
|
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
303
383
|
await run(["start", id.trim()], testDir);
|
|
304
384
|
|
|
305
385
|
const { stderr, exitCode } = await run(["start", id.trim()], testDir);
|
|
306
386
|
expect(exitCode).toBe(1);
|
|
307
|
-
expect(stderr).toContain("
|
|
387
|
+
expect(stderr).toContain("already active");
|
|
388
|
+
expect(stderr).toContain("tk done"); // suggests next action
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test("start errors if already done", async () => {
|
|
392
|
+
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
393
|
+
await run(["done", id.trim()], testDir);
|
|
394
|
+
|
|
395
|
+
const { stderr, exitCode } = await run(["start", id.trim()], testDir);
|
|
396
|
+
expect(exitCode).toBe(1);
|
|
397
|
+
expect(stderr).toContain("already done");
|
|
398
|
+
expect(stderr).toContain("tk reopen"); // suggests next action
|
|
308
399
|
});
|
|
309
400
|
});
|
|
310
401
|
|
|
@@ -458,13 +549,29 @@ describe("tk CLI", () => {
|
|
|
458
549
|
const task = JSON.parse(stdout);
|
|
459
550
|
expect(task.blocked_by).not.toContain(id1.trim());
|
|
460
551
|
});
|
|
552
|
+
|
|
553
|
+
test("clears parent references on child tasks", async () => {
|
|
554
|
+
const { stdout: parentId } = await run(["add", "Parent"], testDir);
|
|
555
|
+
const { stdout: childId } = await run(["add", "Child", "--parent", parentId.trim()], testDir);
|
|
556
|
+
|
|
557
|
+
// Verify child has parent
|
|
558
|
+
const { stdout: before } = await run(["show", childId.trim(), "--json"], testDir);
|
|
559
|
+
expect(JSON.parse(before).parent).toBe(parentId.trim());
|
|
560
|
+
|
|
561
|
+
// Delete parent
|
|
562
|
+
await run(["rm", parentId.trim()], testDir);
|
|
563
|
+
|
|
564
|
+
// Child's parent should be cleared
|
|
565
|
+
const { stdout: after } = await run(["show", childId.trim(), "--json"], testDir);
|
|
566
|
+
expect(JSON.parse(after).parent).toBeNull();
|
|
567
|
+
});
|
|
461
568
|
});
|
|
462
569
|
|
|
463
570
|
describe("clean", () => {
|
|
464
571
|
test("removes completed tasks", async () => {
|
|
465
572
|
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
466
573
|
await run(["done", id.trim()], testDir);
|
|
467
|
-
await run(["clean", "--
|
|
574
|
+
await run(["clean", "--force"], testDir);
|
|
468
575
|
|
|
469
576
|
const { exitCode } = await run(["show", id.trim()], testDir);
|
|
470
577
|
expect(exitCode).toBe(1);
|
|
@@ -472,11 +579,26 @@ describe("tk CLI", () => {
|
|
|
472
579
|
|
|
473
580
|
test("keeps open tasks", async () => {
|
|
474
581
|
const { stdout: id } = await run(["add", "Open task"], testDir);
|
|
475
|
-
await run(["clean", "--
|
|
582
|
+
await run(["clean", "--force"], testDir);
|
|
476
583
|
|
|
477
584
|
const { exitCode } = await run(["show", id.trim()], testDir);
|
|
478
585
|
expect(exitCode).toBe(0);
|
|
479
586
|
});
|
|
587
|
+
|
|
588
|
+
test("--older-than respects age threshold", async () => {
|
|
589
|
+
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
590
|
+
await run(["done", id.trim()], testDir);
|
|
591
|
+
|
|
592
|
+
// Task was just completed, so --older-than 1 should keep it
|
|
593
|
+
await run(["clean", "--older-than", "1"], testDir);
|
|
594
|
+
const { exitCode: stillExists } = await run(["show", id.trim()], testDir);
|
|
595
|
+
expect(stillExists).toBe(0);
|
|
596
|
+
|
|
597
|
+
// --older-than 0 should remove it (0 days = remove all)
|
|
598
|
+
await run(["clean", "--older-than", "0"], testDir);
|
|
599
|
+
const { exitCode: gone } = await run(["show", id.trim()], testDir);
|
|
600
|
+
expect(gone).toBe(1);
|
|
601
|
+
});
|
|
480
602
|
});
|
|
481
603
|
|
|
482
604
|
describe("init", () => {
|
|
@@ -547,6 +669,38 @@ describe("tk CLI", () => {
|
|
|
547
669
|
expect(stdout).toContain("api");
|
|
548
670
|
expect(stdout).toContain("packages/api");
|
|
549
671
|
});
|
|
672
|
+
|
|
673
|
+
test("renames project and updates references", async () => {
|
|
674
|
+
await run(["init", "-P", "old"], testDir);
|
|
675
|
+
|
|
676
|
+
// Create tasks with references
|
|
677
|
+
const { stdout: id1 } = await run(["add", "Task 1"], testDir);
|
|
678
|
+
const { stdout: id2 } = await run(["add", "Task 2"], testDir);
|
|
679
|
+
await run(["block", id2.trim(), id1.trim()], testDir);
|
|
680
|
+
|
|
681
|
+
// Rename project
|
|
682
|
+
const { stdout, exitCode } = await run(
|
|
683
|
+
["config", "project", "new", "--rename", "old"],
|
|
684
|
+
testDir,
|
|
685
|
+
);
|
|
686
|
+
expect(exitCode).toBe(0);
|
|
687
|
+
expect(stdout).toContain("Renamed 2 tasks");
|
|
688
|
+
expect(stdout).toContain("old-*");
|
|
689
|
+
expect(stdout).toContain("new-*");
|
|
690
|
+
|
|
691
|
+
// Verify new IDs work
|
|
692
|
+
const newId1 = id1.trim().replace("old-", "new-");
|
|
693
|
+
const { exitCode: showCode } = await run(["show", newId1], testDir);
|
|
694
|
+
expect(showCode).toBe(0);
|
|
695
|
+
|
|
696
|
+
// Verify references updated
|
|
697
|
+
const { stdout: showOut } = await run(
|
|
698
|
+
["show", id2.trim().replace("old-", "new-"), "--json"],
|
|
699
|
+
testDir,
|
|
700
|
+
);
|
|
701
|
+
const task = JSON.parse(showOut);
|
|
702
|
+
expect(task.blocked_by[0]).toBe(newId1);
|
|
703
|
+
});
|
|
550
704
|
});
|
|
551
705
|
|
|
552
706
|
describe("error handling", () => {
|
|
@@ -586,7 +740,7 @@ describe("tk CLI", () => {
|
|
|
586
740
|
test("rejects invalid ID format", async () => {
|
|
587
741
|
const { stderr, exitCode } = await run(["show", "invalid"], testDir);
|
|
588
742
|
expect(exitCode).toBe(1);
|
|
589
|
-
expect(stderr).toContain("
|
|
743
|
+
expect(stderr).toContain("not found");
|
|
590
744
|
});
|
|
591
745
|
});
|
|
592
746
|
|
|
@@ -615,6 +769,22 @@ describe("tk CLI", () => {
|
|
|
615
769
|
const tasks = JSON.parse(stdout);
|
|
616
770
|
expect(Array.isArray(tasks)).toBe(true);
|
|
617
771
|
});
|
|
772
|
+
|
|
773
|
+
test("log requires quoted message", async () => {
|
|
774
|
+
const { stdout: id } = await run(["add", "Task"], testDir);
|
|
775
|
+
const taskId = id.trim();
|
|
776
|
+
|
|
777
|
+
// Unquoted multiple words should error
|
|
778
|
+
const { stderr, exitCode } = await run(["log", taskId, "word1", "word2"], testDir);
|
|
779
|
+
expect(exitCode).toBe(1);
|
|
780
|
+
expect(stderr).toContain("must be quoted");
|
|
781
|
+
|
|
782
|
+
// Quoted message works (shell passes as single arg)
|
|
783
|
+
await run(["log", taskId, "Quoted message works"], testDir);
|
|
784
|
+
const { stdout } = await run(["show", "--json", taskId], testDir);
|
|
785
|
+
const task = JSON.parse(stdout);
|
|
786
|
+
expect(task.logs[0].msg).toBe("Quoted message works");
|
|
787
|
+
});
|
|
618
788
|
});
|
|
619
789
|
|
|
620
790
|
describe("ID resolution", () => {
|
|
@@ -632,5 +802,175 @@ describe("tk CLI", () => {
|
|
|
632
802
|
const { exitCode } = await run(["show", id.trim()], testDir);
|
|
633
803
|
expect(exitCode).toBe(0);
|
|
634
804
|
});
|
|
805
|
+
|
|
806
|
+
test("ambiguous ID shows matching tasks", async () => {
|
|
807
|
+
// Create tasks with same ref prefix in different projects
|
|
808
|
+
const { stdout: id1 } = await run(["add", "Task 1", "-P", "api"], testDir);
|
|
809
|
+
const { stdout: id2 } = await run(["add", "Task 2", "-P", "web"], testDir);
|
|
810
|
+
const ref1 = id1.trim().split("-")[1] ?? "";
|
|
811
|
+
const ref2 = id2.trim().split("-")[1] ?? "";
|
|
812
|
+
|
|
813
|
+
// Use first char which might match both
|
|
814
|
+
const prefix = ref1[0] ?? "";
|
|
815
|
+
|
|
816
|
+
// Only test if both refs start with same char (otherwise not ambiguous)
|
|
817
|
+
if (ref2.startsWith(prefix)) {
|
|
818
|
+
const { stderr, exitCode } = await run(["show", prefix], testDir);
|
|
819
|
+
expect(exitCode).toBe(1);
|
|
820
|
+
expect(stderr).toContain("Ambiguous");
|
|
821
|
+
expect(stderr).toContain("matches");
|
|
822
|
+
}
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
test("not found shows clear error", async () => {
|
|
826
|
+
const { stderr, exitCode } = await run(["show", "zzzz"], testDir);
|
|
827
|
+
expect(exitCode).toBe(1);
|
|
828
|
+
expect(stderr).toContain("not found");
|
|
829
|
+
});
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
describe("tk check", () => {
|
|
833
|
+
test("reports all OK when no issues", async () => {
|
|
834
|
+
await run(["add", "Task 1"], testDir);
|
|
835
|
+
await run(["add", "Task 2"], testDir);
|
|
836
|
+
|
|
837
|
+
const { stdout, exitCode } = await run(["check"], testDir);
|
|
838
|
+
expect(exitCode).toBe(0);
|
|
839
|
+
expect(stdout).toContain("2 tasks OK");
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
test("auto-fixes orphaned blocker reference", async () => {
|
|
843
|
+
// Create two tasks and block one by the other
|
|
844
|
+
const { stdout: id1 } = await run(["add", "Blocker"], testDir);
|
|
845
|
+
const { stdout: id2 } = await run(["add", "Blocked"], testDir);
|
|
846
|
+
const blockerId = id1.trim();
|
|
847
|
+
const blockedId = id2.trim();
|
|
848
|
+
|
|
849
|
+
await run(["block", blockedId, blockerId], testDir);
|
|
850
|
+
|
|
851
|
+
// Manually delete blocker file to simulate merge
|
|
852
|
+
const fs = await import("fs");
|
|
853
|
+
const path = await import("path");
|
|
854
|
+
const blockerFile = path.join(testDir, ".tasks", `${blockerId}.json`);
|
|
855
|
+
fs.unlinkSync(blockerFile);
|
|
856
|
+
|
|
857
|
+
// Check should fix it
|
|
858
|
+
const { stdout } = await run(["check"], testDir);
|
|
859
|
+
expect(stdout).toContain("cleaned");
|
|
860
|
+
expect(stdout).toContain("orphaned");
|
|
861
|
+
|
|
862
|
+
// Verify the blocked_by is now empty
|
|
863
|
+
const { stdout: showOut } = await run(["show", "--json", blockedId], testDir);
|
|
864
|
+
const task = JSON.parse(showOut);
|
|
865
|
+
expect(task.blocked_by).toEqual([]);
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
test("auto-fixes orphaned parent reference", async () => {
|
|
869
|
+
// Create parent and child
|
|
870
|
+
const { stdout: parentId } = await run(["add", "Parent"], testDir);
|
|
871
|
+
const parent = parentId.trim();
|
|
872
|
+
const { stdout: childId } = await run(["add", "Child", "--parent", parent], testDir);
|
|
873
|
+
const child = childId.trim();
|
|
874
|
+
|
|
875
|
+
// Manually delete parent file
|
|
876
|
+
const fs = await import("fs");
|
|
877
|
+
const path = await import("path");
|
|
878
|
+
const parentFile = path.join(testDir, ".tasks", `${parent}.json`);
|
|
879
|
+
fs.unlinkSync(parentFile);
|
|
880
|
+
|
|
881
|
+
// Check should fix it
|
|
882
|
+
const { stdout } = await run(["check"], testDir);
|
|
883
|
+
expect(stdout).toContain("cleaned");
|
|
884
|
+
expect(stdout).toContain("orphaned parent");
|
|
885
|
+
|
|
886
|
+
// Verify parent is now null
|
|
887
|
+
const { stdout: showOut } = await run(["show", "--json", child], testDir);
|
|
888
|
+
const task = JSON.parse(showOut);
|
|
889
|
+
expect(task.parent).toBeNull();
|
|
890
|
+
});
|
|
891
|
+
|
|
892
|
+
test("auto-fix happens on show command", async () => {
|
|
893
|
+
// Create two tasks and block one
|
|
894
|
+
const { stdout: id1 } = await run(["add", "Blocker"], testDir);
|
|
895
|
+
const { stdout: id2 } = await run(["add", "Blocked"], testDir);
|
|
896
|
+
const blockerId = id1.trim();
|
|
897
|
+
const blockedId = id2.trim();
|
|
898
|
+
|
|
899
|
+
await run(["block", blockedId, blockerId], testDir);
|
|
900
|
+
|
|
901
|
+
// Manually delete blocker file
|
|
902
|
+
const fs = await import("fs");
|
|
903
|
+
const path = await import("path");
|
|
904
|
+
const blockerFile = path.join(testDir, ".tasks", `${blockerId}.json`);
|
|
905
|
+
fs.unlinkSync(blockerFile);
|
|
906
|
+
|
|
907
|
+
// Show should auto-fix and output cleanup message
|
|
908
|
+
const { stderr } = await run(["show", blockedId], testDir);
|
|
909
|
+
expect(stderr).toContain("cleaned");
|
|
910
|
+
expect(stderr).toContain("orphaned");
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
test("reports unfixable corrupted JSON", async () => {
|
|
914
|
+
await run(["add", "Good task"], testDir);
|
|
915
|
+
|
|
916
|
+
// Create a corrupted JSON file
|
|
917
|
+
const fs = await import("fs");
|
|
918
|
+
const path = await import("path");
|
|
919
|
+
const corruptFile = path.join(testDir, ".tasks", "test-bad1.json");
|
|
920
|
+
fs.writeFileSync(corruptFile, "{ invalid json");
|
|
921
|
+
|
|
922
|
+
const { stdout } = await run(["check"], testDir);
|
|
923
|
+
expect(stdout).toContain("Unfixable");
|
|
924
|
+
expect(stdout).toContain("test-bad1.json");
|
|
925
|
+
});
|
|
926
|
+
|
|
927
|
+
test("reports unfixable invalid task structure", async () => {
|
|
928
|
+
await run(["add", "Good task"], testDir);
|
|
929
|
+
|
|
930
|
+
// Create valid JSON but invalid task structure
|
|
931
|
+
const fs = await import("fs");
|
|
932
|
+
const path = await import("path");
|
|
933
|
+
const badFile = path.join(testDir, ".tasks", "test-bad2.json");
|
|
934
|
+
fs.writeFileSync(badFile, '{"foo": "bar"}');
|
|
935
|
+
|
|
936
|
+
const { stdout } = await run(["check"], testDir);
|
|
937
|
+
expect(stdout).toContain("Unfixable");
|
|
938
|
+
expect(stdout).toContain("test-bad2.json");
|
|
939
|
+
expect(stdout).toContain("Invalid task structure");
|
|
940
|
+
});
|
|
941
|
+
|
|
942
|
+
test("auto-fixes ID mismatch (filename vs content)", async () => {
|
|
943
|
+
// Create a task
|
|
944
|
+
const { stdout: id } = await run(["add", "Test task", "-P", "api"], testDir);
|
|
945
|
+
const taskId = id.trim();
|
|
946
|
+
|
|
947
|
+
// Manually rename the file to create a mismatch
|
|
948
|
+
const fs = await import("fs");
|
|
949
|
+
const path = await import("path");
|
|
950
|
+
const oldPath = path.join(testDir, ".tasks", `${taskId}.json`);
|
|
951
|
+
const newPath = path.join(testDir, ".tasks", "web-x1y2.json");
|
|
952
|
+
fs.renameSync(oldPath, newPath);
|
|
953
|
+
|
|
954
|
+
// Check should fix it
|
|
955
|
+
const { stdout } = await run(["check"], testDir);
|
|
956
|
+
expect(stdout).toContain("cleaned");
|
|
957
|
+
expect(stdout).toContain("ID mismatch");
|
|
958
|
+
|
|
959
|
+
// Verify the content was updated to match filename
|
|
960
|
+
const { stdout: showOut } = await run(["show", "--json", "web-x1y2"], testDir);
|
|
961
|
+
const task = JSON.parse(showOut);
|
|
962
|
+
expect(task.project).toBe("web");
|
|
963
|
+
expect(task.ref).toBe("x1y2");
|
|
964
|
+
});
|
|
965
|
+
|
|
966
|
+
test("check --json returns structured output", async () => {
|
|
967
|
+
await run(["add", "Task"], testDir);
|
|
968
|
+
|
|
969
|
+
const { stdout } = await run(["check", "--json"], testDir);
|
|
970
|
+
const result = JSON.parse(stdout);
|
|
971
|
+
expect(result).toHaveProperty("totalTasks");
|
|
972
|
+
expect(result).toHaveProperty("cleaned");
|
|
973
|
+
expect(result).toHaveProperty("unfixable");
|
|
974
|
+
});
|
|
635
975
|
});
|
|
636
976
|
});
|