@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nijaru/tk",
3
- "version": "0.0.3",
3
+ "version": "0.0.5",
4
4
  "description": "Minimal task tracker",
5
5
  "type": "module",
6
6
  "bin": {
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
- Bun.spawnSync(["git", "init"], { cwd: newDir });
608
-
609
- const { stdout, exitCode } = await run(["init"], newDir);
610
- expect(exitCode).toBe(0);
611
- expect(stdout).toContain("Initialized");
612
- expect(existsSync(join(newDir, ".tasks"))).toBe(true);
613
-
614
- rmSync(newDir, { recursive: true, force: true });
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
- Bun.spawnSync(["git", "init"], { cwd: newDir });
620
-
621
- await run(["init", "-P", "api"], newDir);
622
- const { stdout } = await run(["add", "Task"], newDir);
623
- expect(stdout).toMatch(/^api-[a-z0-9]{4}$/);
624
-
625
- rmSync(newDir, { recursive: true, force: true });
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
- // 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
- }
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
  });