@nijaru/tk 0.0.3 → 0.0.5
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/package.json +1 -1
- package/src/cli.test.ts +236 -40
- package/src/cli.ts +52 -308
- package/src/db/storage.ts +87 -51
- package/src/lib/completions.ts +23 -8
- package/src/lib/format.test.ts +81 -5
- package/src/lib/format.ts +48 -35
- package/src/lib/help.ts +266 -0
- package/src/lib/time.test.ts +222 -0
- package/src/lib/time.ts +24 -1
- package/src/types.ts +4 -4
package/package.json
CHANGED
package/src/cli.test.ts
CHANGED
|
@@ -57,6 +57,19 @@ describe("tk CLI", () => {
|
|
|
57
57
|
expect(lsHelp).toContain("--status");
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
test("tk mv --help shows mv help", async () => {
|
|
61
|
+
const { stdout, exitCode } = await run(["mv", "--help"], testDir);
|
|
62
|
+
expect(exitCode).toBe(0);
|
|
63
|
+
expect(stdout).toContain("tk mv");
|
|
64
|
+
expect(stdout).toContain("<project>");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("tk move --help shows move help", async () => {
|
|
68
|
+
const { stdout, exitCode } = await run(["move", "--help"], testDir);
|
|
69
|
+
expect(exitCode).toBe(0);
|
|
70
|
+
expect(stdout).toContain("tk move");
|
|
71
|
+
});
|
|
72
|
+
|
|
60
73
|
test("help <command> shows command-specific help", async () => {
|
|
61
74
|
const { stdout, exitCode } = await run(["help", "add"], testDir);
|
|
62
75
|
expect(exitCode).toBe(0);
|
|
@@ -320,6 +333,62 @@ describe("tk CLI", () => {
|
|
|
320
333
|
expect(lines[0]).toContain("Done Second");
|
|
321
334
|
expect(lines[1]).toContain("Done First");
|
|
322
335
|
});
|
|
336
|
+
|
|
337
|
+
test("shows [due today] annotation for task due today", async () => {
|
|
338
|
+
await run(["add", "Due Today Task", "--due", "+0d"], testDir);
|
|
339
|
+
|
|
340
|
+
const { stdout } = await run(["ls"], testDir);
|
|
341
|
+
expect(stdout).toContain("[due today]");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
test("shows [due Nd] annotation for task due within threshold", async () => {
|
|
345
|
+
await run(["add", "Due Soon Task", "--due", "+3d"], testDir);
|
|
346
|
+
|
|
347
|
+
const { stdout } = await run(["ls"], testDir);
|
|
348
|
+
expect(stdout).toContain("[due 3d]");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("no due-soon annotation for task due beyond threshold", async () => {
|
|
352
|
+
await run(["add", "Far Future Task", "--due", "2099-01-01"], testDir);
|
|
353
|
+
|
|
354
|
+
const { stdout } = await run(["ls"], testDir);
|
|
355
|
+
expect(stdout).not.toContain("[due ");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("no due-soon annotation for overdue task (shows [OVERDUE] instead)", async () => {
|
|
359
|
+
await run(["add", "Overdue Task", "--due", "2020-01-01"], testDir);
|
|
360
|
+
|
|
361
|
+
const { stdout } = await run(["ls"], testDir);
|
|
362
|
+
expect(stdout).toContain("[OVERDUE]");
|
|
363
|
+
expect(stdout).not.toContain("[due today]");
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("--json includes days_until_due field", async () => {
|
|
367
|
+
await run(["add", "Due Today", "--due", "+0d"], testDir);
|
|
368
|
+
|
|
369
|
+
const { stdout } = await run(["ls", "--json"], testDir);
|
|
370
|
+
const tasks = JSON.parse(stdout);
|
|
371
|
+
expect(tasks[0]).toHaveProperty("days_until_due");
|
|
372
|
+
expect(tasks[0].days_until_due).toBe(0);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe("show (due-soon)", () => {
|
|
377
|
+
test("shows [due today] in detail view", async () => {
|
|
378
|
+
const { stdout: idOut } = await run(["add", "Due Today", "--due", "+0d"], testDir);
|
|
379
|
+
const id = idOut.trim();
|
|
380
|
+
|
|
381
|
+
const { stdout } = await run(["show", id], testDir);
|
|
382
|
+
expect(stdout).toContain("[due today]");
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
test("shows [due Nd] in detail view", async () => {
|
|
386
|
+
const { stdout: idOut } = await run(["add", "Due Soon", "--due", "+4d"], testDir);
|
|
387
|
+
const id = idOut.trim();
|
|
388
|
+
|
|
389
|
+
const { stdout } = await run(["show", id], testDir);
|
|
390
|
+
expect(stdout).toContain("[due 4d]");
|
|
391
|
+
});
|
|
323
392
|
});
|
|
324
393
|
|
|
325
394
|
describe("ready", () => {
|
|
@@ -604,25 +673,29 @@ describe("tk CLI", () => {
|
|
|
604
673
|
describe("init", () => {
|
|
605
674
|
test("creates .tasks directory", async () => {
|
|
606
675
|
const newDir = mkdtempSync(join(tmpdir(), "tk-init-"));
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
676
|
+
try {
|
|
677
|
+
Bun.spawnSync(["git", "init"], { cwd: newDir });
|
|
678
|
+
|
|
679
|
+
const { stdout, exitCode } = await run(["init"], newDir);
|
|
680
|
+
expect(exitCode).toBe(0);
|
|
681
|
+
expect(stdout).toContain("Initialized");
|
|
682
|
+
expect(existsSync(join(newDir, ".tasks"))).toBe(true);
|
|
683
|
+
} finally {
|
|
684
|
+
rmSync(newDir, { recursive: true, force: true });
|
|
685
|
+
}
|
|
615
686
|
});
|
|
616
687
|
|
|
617
688
|
test("creates .tasks with custom project", async () => {
|
|
618
689
|
const newDir = mkdtempSync(join(tmpdir(), "tk-init-"));
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
690
|
+
try {
|
|
691
|
+
Bun.spawnSync(["git", "init"], { cwd: newDir });
|
|
692
|
+
|
|
693
|
+
await run(["init", "-P", "api"], newDir);
|
|
694
|
+
const { stdout } = await run(["add", "Task"], newDir);
|
|
695
|
+
expect(stdout).toMatch(/^api-[a-z0-9]{4}$/);
|
|
696
|
+
} finally {
|
|
697
|
+
rmSync(newDir, { recursive: true, force: true });
|
|
698
|
+
}
|
|
626
699
|
});
|
|
627
700
|
|
|
628
701
|
test("reports already initialized", async () => {
|
|
@@ -661,15 +734,6 @@ describe("tk CLI", () => {
|
|
|
661
734
|
expect(stdout).toMatch(/^api-[a-z0-9]{4}$/);
|
|
662
735
|
});
|
|
663
736
|
|
|
664
|
-
test("manages aliases", async () => {
|
|
665
|
-
await run(["init"], testDir);
|
|
666
|
-
await run(["config", "alias", "api", "packages/api"], testDir);
|
|
667
|
-
|
|
668
|
-
const { stdout } = await run(["config", "alias"], testDir);
|
|
669
|
-
expect(stdout).toContain("api");
|
|
670
|
-
expect(stdout).toContain("packages/api");
|
|
671
|
-
});
|
|
672
|
-
|
|
673
737
|
test("renames project and updates references", async () => {
|
|
674
738
|
await run(["init", "-P", "old"], testDir);
|
|
675
739
|
|
|
@@ -804,22 +868,14 @@ describe("tk CLI", () => {
|
|
|
804
868
|
});
|
|
805
869
|
|
|
806
870
|
test("ambiguous ID shows matching tasks", async () => {
|
|
807
|
-
//
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
const
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
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
|
-
}
|
|
871
|
+
// Two tasks in the same project share the "api-" prefix — guaranteed ambiguous
|
|
872
|
+
await run(["add", "Task 1", "-P", "api"], testDir);
|
|
873
|
+
await run(["add", "Task 2", "-P", "api"], testDir);
|
|
874
|
+
|
|
875
|
+
const { stderr, exitCode } = await run(["show", "api"], testDir);
|
|
876
|
+
expect(exitCode).toBe(1);
|
|
877
|
+
expect(stderr).toContain("Ambiguous");
|
|
878
|
+
expect(stderr).toContain("matches");
|
|
823
879
|
});
|
|
824
880
|
|
|
825
881
|
test("not found shows clear error", async () => {
|
|
@@ -973,4 +1029,144 @@ describe("tk CLI", () => {
|
|
|
973
1029
|
expect(result).toHaveProperty("unfixable");
|
|
974
1030
|
});
|
|
975
1031
|
});
|
|
1032
|
+
|
|
1033
|
+
describe("mv", () => {
|
|
1034
|
+
test("moves task to a different project", async () => {
|
|
1035
|
+
await run(["init"], testDir);
|
|
1036
|
+
const { stdout: idOut } = await run(["add", "Move me", "-P", "src"], testDir);
|
|
1037
|
+
const oldId = idOut.trim();
|
|
1038
|
+
const ref = oldId.split("-")[1];
|
|
1039
|
+
|
|
1040
|
+
const { stdout, exitCode } = await run(["mv", oldId, "dst"], testDir);
|
|
1041
|
+
expect(exitCode).toBe(0);
|
|
1042
|
+
const newId = `dst-${ref}`;
|
|
1043
|
+
expect(stdout).toContain(oldId);
|
|
1044
|
+
expect(stdout).toContain(newId);
|
|
1045
|
+
|
|
1046
|
+
// Old ID gone, new ID exists with same content
|
|
1047
|
+
const { exitCode: notFound } = await run(["show", oldId], testDir);
|
|
1048
|
+
expect(notFound).toBe(1);
|
|
1049
|
+
|
|
1050
|
+
const { stdout: showOut } = await run(["show", newId, "--json"], testDir);
|
|
1051
|
+
const task = JSON.parse(showOut);
|
|
1052
|
+
expect(task.project).toBe("dst");
|
|
1053
|
+
expect(task.title).toBe("Move me");
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
test("updates blocked_by and parent references", async () => {
|
|
1057
|
+
await run(["init"], testDir);
|
|
1058
|
+
const { stdout: idA } = await run(["add", "A", "-P", "src"], testDir);
|
|
1059
|
+
const { stdout: idB } = await run(["add", "B", "-P", "src"], testDir);
|
|
1060
|
+
const aId = idA.trim();
|
|
1061
|
+
const bId = idB.trim();
|
|
1062
|
+
|
|
1063
|
+
// B is blocked by A, and B has A as parent
|
|
1064
|
+
await run(["block", bId, aId], testDir);
|
|
1065
|
+
await run(["edit", bId, "--parent", aId], testDir);
|
|
1066
|
+
|
|
1067
|
+
const ref = aId.split("-")[1];
|
|
1068
|
+
await run(["mv", aId, "dst"], testDir);
|
|
1069
|
+
const newAId = `dst-${ref}`;
|
|
1070
|
+
|
|
1071
|
+
const { stdout: showB } = await run(["show", bId, "--json"], testDir);
|
|
1072
|
+
const bTask = JSON.parse(showB);
|
|
1073
|
+
expect(bTask.blocked_by).toContain(newAId);
|
|
1074
|
+
expect(bTask.parent).toBe(newAId);
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
test("errors when moving to same project", async () => {
|
|
1078
|
+
await run(["init"], testDir);
|
|
1079
|
+
const { stdout: idOut } = await run(["add", "Task", "-P", "api"], testDir);
|
|
1080
|
+
const id = idOut.trim();
|
|
1081
|
+
|
|
1082
|
+
const { stderr, exitCode } = await run(["mv", id, "api"], testDir);
|
|
1083
|
+
expect(exitCode).toBe(1);
|
|
1084
|
+
expect(stderr).toContain("already in project");
|
|
1085
|
+
});
|
|
1086
|
+
|
|
1087
|
+
test("errors on ref collision", async () => {
|
|
1088
|
+
await run(["init"], testDir);
|
|
1089
|
+
const { stdout: idOut } = await run(["add", "Task A", "-P", "src"], testDir);
|
|
1090
|
+
const id = idOut.trim();
|
|
1091
|
+
const ref = id.split("-")[1]!;
|
|
1092
|
+
|
|
1093
|
+
// Manually create a collision in dst project
|
|
1094
|
+
const fs = await import("fs");
|
|
1095
|
+
const path = await import("path");
|
|
1096
|
+
const collisionPath = path.join(testDir, ".tasks", `dst-${ref}.json`);
|
|
1097
|
+
fs.writeFileSync(
|
|
1098
|
+
collisionPath,
|
|
1099
|
+
JSON.stringify({
|
|
1100
|
+
project: "dst",
|
|
1101
|
+
ref,
|
|
1102
|
+
title: "Collision",
|
|
1103
|
+
status: "open",
|
|
1104
|
+
priority: 3,
|
|
1105
|
+
labels: [],
|
|
1106
|
+
assignees: [],
|
|
1107
|
+
parent: null,
|
|
1108
|
+
blocked_by: [],
|
|
1109
|
+
estimate: null,
|
|
1110
|
+
due_date: null,
|
|
1111
|
+
logs: [],
|
|
1112
|
+
description: null,
|
|
1113
|
+
created_at: new Date().toISOString(),
|
|
1114
|
+
updated_at: new Date().toISOString(),
|
|
1115
|
+
completed_at: null,
|
|
1116
|
+
external: {},
|
|
1117
|
+
}),
|
|
1118
|
+
);
|
|
1119
|
+
|
|
1120
|
+
const { stderr, exitCode } = await run(["mv", id, "dst"], testDir);
|
|
1121
|
+
expect(exitCode).toBe(1);
|
|
1122
|
+
expect(stderr).toContain("already exists");
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
test("errors on missing arguments", async () => {
|
|
1126
|
+
await run(["init"], testDir);
|
|
1127
|
+
const { stdout: idOut } = await run(["add", "Task"], testDir);
|
|
1128
|
+
const id = idOut.trim();
|
|
1129
|
+
|
|
1130
|
+
const { stderr: noProject } = await run(["mv", id], testDir);
|
|
1131
|
+
expect(noProject).toContain("Project required");
|
|
1132
|
+
});
|
|
1133
|
+
|
|
1134
|
+
test("errors on invalid project name", async () => {
|
|
1135
|
+
await run(["init"], testDir);
|
|
1136
|
+
const { stdout: idOut } = await run(["add", "Task", "-P", "src"], testDir);
|
|
1137
|
+
const id = idOut.trim();
|
|
1138
|
+
|
|
1139
|
+
const { stderr, exitCode } = await run(["mv", id, "Bad-Project"], testDir);
|
|
1140
|
+
expect(exitCode).toBe(1);
|
|
1141
|
+
expect(stderr).toContain("Invalid project name");
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
test("--json output structure", async () => {
|
|
1145
|
+
await run(["init"], testDir);
|
|
1146
|
+
const { stdout: idOut } = await run(["add", "Task", "-P", "src"], testDir);
|
|
1147
|
+
const id = idOut.trim();
|
|
1148
|
+
|
|
1149
|
+
const { stdout, exitCode } = await run(["mv", id, "dst", "--json"], testDir);
|
|
1150
|
+
expect(exitCode).toBe(0);
|
|
1151
|
+
const result = JSON.parse(stdout);
|
|
1152
|
+
expect(result).toHaveProperty("old_id");
|
|
1153
|
+
expect(result).toHaveProperty("new_id");
|
|
1154
|
+
expect(result).toHaveProperty("referencesUpdated");
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
test("partial ID resolution", async () => {
|
|
1158
|
+
await run(["init"], testDir);
|
|
1159
|
+
const { stdout: idOut } = await run(["add", "Task", "-P", "src"], testDir);
|
|
1160
|
+
const id = idOut.trim();
|
|
1161
|
+
const ref = id.split("-")[1]!;
|
|
1162
|
+
|
|
1163
|
+
// Use just the ref as partial ID
|
|
1164
|
+
const { exitCode } = await run(["mv", ref, "dst"], testDir);
|
|
1165
|
+
expect(exitCode).toBe(0);
|
|
1166
|
+
|
|
1167
|
+
// Verify moved
|
|
1168
|
+
const { exitCode: found } = await run(["show", `dst-${ref}`], testDir);
|
|
1169
|
+
expect(found).toBe(0);
|
|
1170
|
+
});
|
|
1171
|
+
});
|
|
976
1172
|
});
|