trekoon 0.1.0

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.
Files changed (45) hide show
  1. package/.agents/skills/trekoon/SKILL.md +91 -0
  2. package/AGENTS.md +54 -0
  3. package/CONTRIBUTING.md +18 -0
  4. package/README.md +151 -0
  5. package/bin/trekoon +5 -0
  6. package/bun.lock +28 -0
  7. package/package.json +24 -0
  8. package/src/commands/arg-parser.ts +93 -0
  9. package/src/commands/dep.ts +105 -0
  10. package/src/commands/epic.ts +539 -0
  11. package/src/commands/help.ts +61 -0
  12. package/src/commands/init.ts +24 -0
  13. package/src/commands/quickstart.ts +61 -0
  14. package/src/commands/subtask.ts +187 -0
  15. package/src/commands/sync.ts +128 -0
  16. package/src/commands/task.ts +554 -0
  17. package/src/commands/wipe.ts +39 -0
  18. package/src/domain/tracker-domain.ts +576 -0
  19. package/src/domain/types.ts +99 -0
  20. package/src/index.ts +21 -0
  21. package/src/io/human-table.ts +191 -0
  22. package/src/io/output.ts +70 -0
  23. package/src/runtime/cli-shell.ts +158 -0
  24. package/src/runtime/command-types.ts +33 -0
  25. package/src/storage/database.ts +35 -0
  26. package/src/storage/migrations.ts +46 -0
  27. package/src/storage/path.ts +22 -0
  28. package/src/storage/schema.ts +116 -0
  29. package/src/storage/types.ts +15 -0
  30. package/src/sync/branch-db.ts +49 -0
  31. package/src/sync/event-writes.ts +49 -0
  32. package/src/sync/git-context.ts +67 -0
  33. package/src/sync/service.ts +654 -0
  34. package/src/sync/types.ts +31 -0
  35. package/tests/commands/dep.test.ts +101 -0
  36. package/tests/commands/epic.test.ts +383 -0
  37. package/tests/commands/subtask.test.ts +132 -0
  38. package/tests/commands/sync/sync-command.test.ts +1 -0
  39. package/tests/commands/sync.test.ts +199 -0
  40. package/tests/commands/task.test.ts +474 -0
  41. package/tests/integration/sync-workflow.test.ts +279 -0
  42. package/tests/io/human-table.test.ts +81 -0
  43. package/tests/runtime/output-mode.test.ts +54 -0
  44. package/tests/storage/database.test.ts +91 -0
  45. package/tsconfig.json +19 -0
@@ -0,0 +1,101 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+
7
+ import { runDep } from "../../src/commands/dep";
8
+ import { runEpic } from "../../src/commands/epic";
9
+ import { runSubtask } from "../../src/commands/subtask";
10
+ import { runTask } from "../../src/commands/task";
11
+
12
+ const tempDirs: string[] = [];
13
+
14
+ function createWorkspace(): string {
15
+ const workspace = mkdtempSync(join(tmpdir(), "trekoon-dep-"));
16
+ tempDirs.push(workspace);
17
+ return workspace;
18
+ }
19
+
20
+ afterEach((): void => {
21
+ while (tempDirs.length > 0) {
22
+ const next = tempDirs.pop();
23
+ if (next) {
24
+ rmSync(next, { recursive: true, force: true });
25
+ }
26
+ }
27
+ });
28
+
29
+ async function createTaskGraph(cwd: string): Promise<{ taskA: string; taskB: string; subtask: string }> {
30
+ const epic = await runEpic({
31
+ cwd,
32
+ mode: "human",
33
+ args: ["create", "--title", "Roadmap", "--description", "desc"],
34
+ });
35
+ const epicId = (epic.data as { epic: { id: string } }).epic.id;
36
+
37
+ const taskA = await runTask({
38
+ cwd,
39
+ mode: "human",
40
+ args: ["create", "--epic", epicId, "--title", "Task A", "--description", "desc a"],
41
+ });
42
+ const taskAId = (taskA.data as { task: { id: string } }).task.id;
43
+
44
+ const taskB = await runTask({
45
+ cwd,
46
+ mode: "human",
47
+ args: ["create", "--epic", epicId, "--title", "Task B", "--description", "desc b"],
48
+ });
49
+ const taskBId = (taskB.data as { task: { id: string } }).task.id;
50
+
51
+ const subtask = await runSubtask({
52
+ cwd,
53
+ mode: "human",
54
+ args: ["create", "--task", taskBId, "--title", "Subtask"],
55
+ });
56
+
57
+ return {
58
+ taskA: taskAId,
59
+ taskB: taskBId,
60
+ subtask: (subtask.data as { subtask: { id: string } }).subtask.id,
61
+ };
62
+ }
63
+
64
+ describe("dep command", (): void => {
65
+ test("supports add/list/remove", async (): Promise<void> => {
66
+ const cwd = createWorkspace();
67
+ const nodes = await createTaskGraph(cwd);
68
+
69
+ const added = await runDep({ cwd, mode: "human", args: ["add", nodes.taskA, nodes.subtask] });
70
+ expect(added.ok).toBeTrue();
71
+
72
+ const listed = await runDep({ cwd, mode: "human", args: ["list", nodes.taskA] });
73
+ expect(listed.ok).toBeTrue();
74
+ expect((listed.data as { dependencies: unknown[] }).dependencies.length).toBe(1);
75
+
76
+ const removed = await runDep({ cwd, mode: "human", args: ["remove", nodes.taskA, nodes.subtask] });
77
+ expect(removed.ok).toBeTrue();
78
+ expect((removed.data as { removed: number }).removed).toBe(1);
79
+ });
80
+
81
+ test("enforces referential checks for task/subtask nodes", async (): Promise<void> => {
82
+ const cwd = createWorkspace();
83
+ const nodes = await createTaskGraph(cwd);
84
+
85
+ const bad = await runDep({ cwd, mode: "human", args: ["add", nodes.taskA, "missing-node-id"] });
86
+ expect(bad.ok).toBeFalse();
87
+ expect(bad.error?.code).toBe("not_found");
88
+ });
89
+
90
+ test("detects dependency cycles", async (): Promise<void> => {
91
+ const cwd = createWorkspace();
92
+ const nodes = await createTaskGraph(cwd);
93
+
94
+ const first = await runDep({ cwd, mode: "human", args: ["add", nodes.taskA, nodes.taskB] });
95
+ expect(first.ok).toBeTrue();
96
+
97
+ const cycle = await runDep({ cwd, mode: "human", args: ["add", nodes.taskB, nodes.taskA] });
98
+ expect(cycle.ok).toBeFalse();
99
+ expect(cycle.error?.code).toBe("invalid_dependency");
100
+ });
101
+ });
@@ -0,0 +1,383 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+
7
+ import { runEpic } from "../../src/commands/epic";
8
+ import { runSubtask } from "../../src/commands/subtask";
9
+ import { runTask } from "../../src/commands/task";
10
+
11
+ const tempDirs: string[] = [];
12
+
13
+ function createWorkspace(): string {
14
+ const workspace = mkdtempSync(join(tmpdir(), "trekoon-epic-"));
15
+ tempDirs.push(workspace);
16
+ return workspace;
17
+ }
18
+
19
+ async function createEpic(
20
+ cwd: string,
21
+ input: { title: string; description: string; status?: string },
22
+ ): Promise<{ id: string; status: string; title: string }> {
23
+ const args = ["create", "--title", input.title, "--description", input.description];
24
+ if (input.status !== undefined) {
25
+ args.push("--status", input.status);
26
+ }
27
+
28
+ const result = await runEpic({
29
+ cwd,
30
+ mode: "human",
31
+ args,
32
+ });
33
+
34
+ expect(result.ok).toBeTrue();
35
+ return (result.data as { epic: { id: string; status: string; title: string } }).epic;
36
+ }
37
+
38
+ afterEach((): void => {
39
+ while (tempDirs.length > 0) {
40
+ const next = tempDirs.pop();
41
+ if (next) {
42
+ rmSync(next, { recursive: true, force: true });
43
+ }
44
+ }
45
+ });
46
+
47
+ describe("epic command", (): void => {
48
+ test("requires description on create", async (): Promise<void> => {
49
+ const cwd = createWorkspace();
50
+ const result = await runEpic({
51
+ cwd,
52
+ mode: "human",
53
+ args: ["create", "--title", "Roadmap"],
54
+ });
55
+
56
+ expect(result.ok).toBeFalse();
57
+ expect(result.error?.code).toBe("invalid_input");
58
+ });
59
+
60
+ test("creates uuid epic with default status", async (): Promise<void> => {
61
+ const cwd = createWorkspace();
62
+ const result = await runEpic({
63
+ cwd,
64
+ mode: "human",
65
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work"],
66
+ });
67
+
68
+ expect(result.ok).toBeTrue();
69
+ const epic = (result.data as { epic: { id: string; status: string } }).epic;
70
+ expect(epic.id).toMatch(
71
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
72
+ );
73
+ expect(epic.status).toBe("todo");
74
+ });
75
+
76
+ test("epic show returns aggregate tree", async (): Promise<void> => {
77
+ const cwd = createWorkspace();
78
+ const createdEpic = await runEpic({
79
+ cwd,
80
+ mode: "human",
81
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work", "--status", "backlog"],
82
+ });
83
+ const epicId = (createdEpic.data as { epic: { id: string } }).epic.id;
84
+
85
+ const createdTask = await runTask({
86
+ cwd,
87
+ mode: "human",
88
+ args: ["create", "--epic", epicId, "--title", "Implement", "--description", "Task desc"],
89
+ });
90
+ const taskId = (createdTask.data as { task: { id: string } }).task.id;
91
+
92
+ await runSubtask({
93
+ cwd,
94
+ mode: "human",
95
+ args: ["create", "--task", taskId, "--title", "Write tests"],
96
+ });
97
+
98
+ const show = await runEpic({ cwd, mode: "toon", args: ["show", epicId, "--all"] });
99
+
100
+ expect(show.ok).toBeTrue();
101
+ const tree = (
102
+ show.data as {
103
+ tree: {
104
+ id: string;
105
+ status: string;
106
+ description: string;
107
+ tasks: Array<{ description: string; subtasks: Array<{ description: string }> }>;
108
+ };
109
+ }
110
+ ).tree;
111
+ expect(tree.id).toBe(epicId);
112
+ expect(tree.status).toBe("backlog");
113
+ expect(tree.description).toBe("Top-level work");
114
+ expect(tree.tasks.length).toBe(1);
115
+ expect(tree.tasks[0]?.subtasks.length).toBe(1);
116
+ expect(tree.tasks[0]?.description).toBe("Task desc");
117
+ expect(tree.tasks[0]?.subtasks[0]?.description).toBe("");
118
+ });
119
+
120
+ test("list defaults to table and supports compact view", async (): Promise<void> => {
121
+ const cwd = createWorkspace();
122
+ await runEpic({
123
+ cwd,
124
+ mode: "human",
125
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work"],
126
+ });
127
+
128
+ const listedDefault = await runEpic({ cwd, mode: "human", args: ["list"] });
129
+ expect(listedDefault.ok).toBeTrue();
130
+ expect(listedDefault.human).toContain("ID");
131
+ expect(listedDefault.human).toContain("TITLE");
132
+ expect(listedDefault.human).toContain("STATUS");
133
+
134
+ const listedCompact = await runEpic({ cwd, mode: "human", args: ["list", "--view", "compact"] });
135
+ expect(listedCompact.ok).toBeTrue();
136
+ expect(listedCompact.human).not.toContain("ID | TITLE | STATUS");
137
+ expect(listedCompact.human).toContain("Roadmap");
138
+ });
139
+
140
+ test("show defaults to table and handles empty task tree", async (): Promise<void> => {
141
+ const cwd = createWorkspace();
142
+ const createdEpic = await runEpic({
143
+ cwd,
144
+ mode: "human",
145
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work"],
146
+ });
147
+ const epicId = (createdEpic.data as { epic: { id: string } }).epic.id;
148
+
149
+ const shown = await runEpic({ cwd, mode: "human", args: ["show", epicId] });
150
+ expect(shown.ok).toBeTrue();
151
+ expect(shown.human).toContain("EPIC");
152
+ expect(shown.human).toContain("TASKS");
153
+ expect(shown.human).toContain("No tasks found.");
154
+ });
155
+
156
+ test("show compact and tree views are distinct", async (): Promise<void> => {
157
+ const cwd = createWorkspace();
158
+ const createdEpic = await runEpic({
159
+ cwd,
160
+ mode: "human",
161
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work"],
162
+ });
163
+ const epicId = (createdEpic.data as { epic: { id: string } }).epic.id;
164
+
165
+ const createdTask = await runTask({
166
+ cwd,
167
+ mode: "human",
168
+ args: ["create", "--epic", epicId, "--title", "Implement", "--description", "Task desc"],
169
+ });
170
+ const taskId = (createdTask.data as { task: { id: string } }).task.id;
171
+
172
+ await runSubtask({
173
+ cwd,
174
+ mode: "human",
175
+ args: ["create", "--task", taskId, "--title", "Write tests"],
176
+ });
177
+
178
+ const compact = await runEpic({ cwd, mode: "human", args: ["show", epicId, "--view", "compact"] });
179
+ const tree = await runEpic({ cwd, mode: "human", args: ["show", epicId, "--view", "tree"] });
180
+
181
+ expect(compact.ok).toBeTrue();
182
+ expect(tree.ok).toBeTrue();
183
+ expect(compact.human).toContain("Roadmap");
184
+ expect(compact.human).not.toContain("task ");
185
+ expect(tree.human).toContain("Roadmap");
186
+ expect(tree.human).toContain("task ");
187
+ expect(tree.human).not.toBe(compact.human);
188
+ });
189
+
190
+ test("bulk update supports --all with --append and --status", async (): Promise<void> => {
191
+ const cwd = createWorkspace();
192
+ await runEpic({
193
+ cwd,
194
+ mode: "human",
195
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work"],
196
+ });
197
+ await runEpic({
198
+ cwd,
199
+ mode: "human",
200
+ args: ["create", "--title", "Release", "--description", "Ship candidate"],
201
+ });
202
+
203
+ const updated = await runEpic({
204
+ cwd,
205
+ mode: "human",
206
+ args: ["update", "--all", "--append", "follow policy", "--status", "in-progress"],
207
+ });
208
+
209
+ expect(updated.ok).toBeTrue();
210
+ const epics = (updated.data as { epics: Array<{ description: string; status: string }> }).epics;
211
+ expect(epics.length).toBe(2);
212
+ expect(epics[0]?.description).toContain("follow policy");
213
+ expect(epics[1]?.description).toContain("follow policy");
214
+ expect(epics[0]?.status).toBe("in-progress");
215
+ expect(epics[1]?.status).toBe("in-progress");
216
+ });
217
+
218
+ test("bulk update rejects title field", async (): Promise<void> => {
219
+ const cwd = createWorkspace();
220
+ await runEpic({
221
+ cwd,
222
+ mode: "human",
223
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work"],
224
+ });
225
+
226
+ const result = await runEpic({
227
+ cwd,
228
+ mode: "human",
229
+ args: ["update", "--all", "--title", "Renamed", "--append", "follow policy"],
230
+ });
231
+
232
+ expect(result.ok).toBeFalse();
233
+ expect(result.error?.code).toBe("invalid_input");
234
+ });
235
+
236
+ test("default list includes only open statuses and max 10", async (): Promise<void> => {
237
+ const cwd = createWorkspace();
238
+
239
+ for (let index = 0; index < 7; index += 1) {
240
+ await createEpic(cwd, {
241
+ title: `In progress ${index}`,
242
+ description: "Top-level work",
243
+ status: "in_progress",
244
+ });
245
+ }
246
+
247
+ for (let index = 0; index < 7; index += 1) {
248
+ await createEpic(cwd, {
249
+ title: `Done ${index}`,
250
+ description: "Top-level work",
251
+ status: "done",
252
+ });
253
+ }
254
+
255
+ const listed = await runEpic({ cwd, mode: "human", args: ["list"] });
256
+ expect(listed.ok).toBeTrue();
257
+
258
+ const epics = (listed.data as { epics: Array<{ status: string }> }).epics;
259
+ expect(epics.length).toBe(7);
260
+ expect(epics.every((epic) => ["in_progress", "in-progress", "todo"].includes(epic.status))).toBeTrue();
261
+
262
+ for (let index = 0; index < 5; index += 1) {
263
+ await createEpic(cwd, {
264
+ title: `Todo ${index}`,
265
+ description: "Top-level work",
266
+ status: "todo",
267
+ });
268
+ }
269
+
270
+ const listedLimited = await runEpic({ cwd, mode: "human", args: ["list"] });
271
+ expect(listedLimited.ok).toBeTrue();
272
+ const limitedEpics = (listedLimited.data as { epics: Array<{ status: string }> }).epics;
273
+ expect(limitedEpics.length).toBe(10);
274
+ expect(limitedEpics.every((epic) => ["in_progress", "in-progress", "todo"].includes(epic.status))).toBeTrue();
275
+ });
276
+
277
+ test("default ordering puts in-progress before todo", async (): Promise<void> => {
278
+ const cwd = createWorkspace();
279
+ await createEpic(cwd, { title: "Todo", description: "Top-level work", status: "todo" });
280
+ await createEpic(cwd, { title: "In progress hyphen", description: "Top-level work", status: "in-progress" });
281
+ await createEpic(cwd, { title: "In progress underscore", description: "Top-level work", status: "in_progress" });
282
+
283
+ const listed = await runEpic({ cwd, mode: "human", args: ["list"] });
284
+ expect(listed.ok).toBeTrue();
285
+
286
+ const statuses = (listed.data as { epics: Array<{ status: string }> }).epics.map((epic) => epic.status);
287
+ const todoIndex = statuses.indexOf("todo");
288
+ const inProgressIndex = statuses.findIndex((status) => status === "in_progress" || status === "in-progress");
289
+
290
+ expect(inProgressIndex).toBeGreaterThanOrEqual(0);
291
+ expect(todoIndex).toBeGreaterThanOrEqual(0);
292
+ expect(inProgressIndex).toBeLessThan(todoIndex);
293
+ });
294
+
295
+ test("--status done returns only done", async (): Promise<void> => {
296
+ const cwd = createWorkspace();
297
+ await createEpic(cwd, { title: "Done", description: "Top-level work", status: "done" });
298
+ await createEpic(cwd, { title: "Todo", description: "Top-level work", status: "todo" });
299
+
300
+ const listed = await runEpic({ cwd, mode: "human", args: ["list", "--status", "done"] });
301
+ expect(listed.ok).toBeTrue();
302
+
303
+ const epics = (listed.data as { epics: Array<{ status: string }> }).epics;
304
+ expect(epics.length).toBe(1);
305
+ expect(epics[0]?.status).toBe("done");
306
+ });
307
+
308
+ test("--all includes done and bypasses limit", async (): Promise<void> => {
309
+ const cwd = createWorkspace();
310
+
311
+ for (let index = 0; index < 12; index += 1) {
312
+ await createEpic(cwd, {
313
+ title: `Done ${index}`,
314
+ description: "Top-level work",
315
+ status: "done",
316
+ });
317
+ }
318
+
319
+ const listed = await runEpic({ cwd, mode: "human", args: ["list", "--all"] });
320
+ expect(listed.ok).toBeTrue();
321
+
322
+ const epics = (listed.data as { epics: Array<{ status: string }> }).epics;
323
+ expect(epics.length).toBe(12);
324
+ expect(epics.some((epic) => epic.status === "done")).toBeTrue();
325
+ });
326
+
327
+ test("rejects --all with --status", async (): Promise<void> => {
328
+ const cwd = createWorkspace();
329
+ const result = await runEpic({ cwd, mode: "human", args: ["list", "--all", "--status", "done"] });
330
+
331
+ expect(result.ok).toBeFalse();
332
+ expect(result.error?.code).toBe("invalid_input");
333
+ });
334
+
335
+ test("rejects --all with --limit", async (): Promise<void> => {
336
+ const cwd = createWorkspace();
337
+ const result = await runEpic({ cwd, mode: "human", args: ["list", "--all", "--limit", "5"] });
338
+
339
+ expect(result.ok).toBeFalse();
340
+ expect(result.error?.code).toBe("invalid_input");
341
+ });
342
+
343
+ test("rejects invalid --limit", async (): Promise<void> => {
344
+ const cwd = createWorkspace();
345
+ const result = await runEpic({ cwd, mode: "human", args: ["list", "--limit", "0"] });
346
+
347
+ expect(result.ok).toBeFalse();
348
+ expect(result.error?.code).toBe("invalid_input");
349
+ });
350
+
351
+ test("rejects strict edge-case --limit values", async (): Promise<void> => {
352
+ const cwd = createWorkspace();
353
+ const leadingZero = await runEpic({ cwd, mode: "human", args: ["list", "--limit", "01"] });
354
+
355
+ expect(leadingZero.ok).toBeFalse();
356
+ expect(leadingZero.error?.code).toBe("invalid_input");
357
+ });
358
+
359
+ test("errors when value-required epic options are missing values", async (): Promise<void> => {
360
+ const cwd = createWorkspace();
361
+ const createdEpic = await runEpic({
362
+ cwd,
363
+ mode: "human",
364
+ args: ["create", "--title", "Roadmap", "--description", "Top-level work"],
365
+ });
366
+ const epicId = (createdEpic.data as { epic: { id: string } }).epic.id;
367
+
368
+ const missingStatus = await runEpic({ cwd, mode: "human", args: ["list", "--status"] });
369
+ expect(missingStatus.ok).toBeFalse();
370
+ expect(missingStatus.error?.code).toBe("invalid_input");
371
+ expect((missingStatus.data as { option: string }).option).toBe("status");
372
+
373
+ const missingView = await runEpic({ cwd, mode: "human", args: ["show", epicId, "--view"] });
374
+ expect(missingView.ok).toBeFalse();
375
+ expect(missingView.error?.code).toBe("invalid_input");
376
+ expect((missingView.data as { option: string }).option).toBe("view");
377
+
378
+ const missingAppend = await runEpic({ cwd, mode: "human", args: ["update", epicId, "--append"] });
379
+ expect(missingAppend.ok).toBeFalse();
380
+ expect(missingAppend.error?.code).toBe("invalid_input");
381
+ expect((missingAppend.data as { option: string }).option).toBe("append");
382
+ });
383
+ });
@@ -0,0 +1,132 @@
1
+ import { mkdtempSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+
7
+ import { runEpic } from "../../src/commands/epic";
8
+ import { runSubtask } from "../../src/commands/subtask";
9
+ import { runTask } from "../../src/commands/task";
10
+
11
+ const tempDirs: string[] = [];
12
+
13
+ function createWorkspace(): string {
14
+ const workspace = mkdtempSync(join(tmpdir(), "trekoon-subtask-"));
15
+ tempDirs.push(workspace);
16
+ return workspace;
17
+ }
18
+
19
+ afterEach((): void => {
20
+ while (tempDirs.length > 0) {
21
+ const next = tempDirs.pop();
22
+ if (next) {
23
+ rmSync(next, { recursive: true, force: true });
24
+ }
25
+ }
26
+ });
27
+
28
+ describe("subtask command", (): void => {
29
+ test("description is optional on create", async (): Promise<void> => {
30
+ const cwd = createWorkspace();
31
+ const epicCreated = await runEpic({
32
+ cwd,
33
+ mode: "human",
34
+ args: ["create", "--title", "Roadmap", "--description", "desc"],
35
+ });
36
+ const epicId = (epicCreated.data as { epic: { id: string } }).epic.id;
37
+ const taskCreated = await runTask({
38
+ cwd,
39
+ mode: "human",
40
+ args: ["create", "--epic", epicId, "--title", "Implement", "--description", "task desc"],
41
+ });
42
+ const taskId = (taskCreated.data as { task: { id: string } }).task.id;
43
+
44
+ const created = await runSubtask({ cwd, mode: "human", args: ["create", "--task", taskId, "--title", "A subtask"] });
45
+ expect(created.ok).toBeTrue();
46
+ expect((created.data as { subtask: { description: string; status: string } }).subtask.description).toBe("");
47
+ expect((created.data as { subtask: { status: string } }).subtask.status).toBe("todo");
48
+ });
49
+
50
+ test("supports list/update/delete", async (): Promise<void> => {
51
+ const cwd = createWorkspace();
52
+ const epicCreated = await runEpic({
53
+ cwd,
54
+ mode: "human",
55
+ args: ["create", "--title", "Roadmap", "--description", "desc"],
56
+ });
57
+ const epicId = (epicCreated.data as { epic: { id: string } }).epic.id;
58
+ const taskCreated = await runTask({
59
+ cwd,
60
+ mode: "human",
61
+ args: ["create", "--epic", epicId, "--title", "Implement", "--description", "task desc"],
62
+ });
63
+ const taskId = (taskCreated.data as { task: { id: string } }).task.id;
64
+
65
+ const created = await runSubtask({ cwd, mode: "human", args: ["create", "--task", taskId, "--title", "A subtask"] });
66
+ const subtaskId = (created.data as { subtask: { id: string } }).subtask.id;
67
+
68
+ const listed = await runSubtask({ cwd, mode: "human", args: ["list", "--task", taskId] });
69
+ expect(listed.ok).toBeTrue();
70
+ expect((listed.data as { subtasks: unknown[] }).subtasks.length).toBe(1);
71
+
72
+ const updated = await runSubtask({ cwd, mode: "human", args: ["update", subtaskId, "--status", "doing"] });
73
+ expect(updated.ok).toBeTrue();
74
+ expect((updated.data as { subtask: { status: string } }).subtask.status).toBe("doing");
75
+
76
+ const removed = await runSubtask({ cwd, mode: "human", args: ["delete", subtaskId] });
77
+ expect(removed.ok).toBeTrue();
78
+ });
79
+
80
+ test("list defaults to table and supports compact view", async (): Promise<void> => {
81
+ const cwd = createWorkspace();
82
+ const epicCreated = await runEpic({
83
+ cwd,
84
+ mode: "human",
85
+ args: ["create", "--title", "Roadmap", "--description", "desc"],
86
+ });
87
+ const epicId = (epicCreated.data as { epic: { id: string } }).epic.id;
88
+ const taskCreated = await runTask({
89
+ cwd,
90
+ mode: "human",
91
+ args: ["create", "--epic", epicId, "--title", "Implement", "--description", "task desc"],
92
+ });
93
+ const taskId = (taskCreated.data as { task: { id: string } }).task.id;
94
+
95
+ await runSubtask({ cwd, mode: "human", args: ["create", "--task", taskId, "--title", "A subtask"] });
96
+
97
+ const listedDefault = await runSubtask({ cwd, mode: "human", args: ["list", "--task", taskId] });
98
+ expect(listedDefault.ok).toBeTrue();
99
+ expect(listedDefault.human).toContain("ID");
100
+ expect(listedDefault.human).toContain("TASK");
101
+
102
+ const listedCompact = await runSubtask({ cwd, mode: "human", args: ["list", "--task", taskId, "--view", "compact"] });
103
+ expect(listedCompact.ok).toBeTrue();
104
+ expect(listedCompact.human).toContain("task=");
105
+ });
106
+
107
+ test("errors when value-required subtask options are missing values", async (): Promise<void> => {
108
+ const cwd = createWorkspace();
109
+ const epicCreated = await runEpic({
110
+ cwd,
111
+ mode: "human",
112
+ args: ["create", "--title", "Roadmap", "--description", "desc"],
113
+ });
114
+ const epicId = (epicCreated.data as { epic: { id: string } }).epic.id;
115
+ const taskCreated = await runTask({
116
+ cwd,
117
+ mode: "human",
118
+ args: ["create", "--epic", epicId, "--title", "Implement", "--description", "task desc"],
119
+ });
120
+ const taskId = (taskCreated.data as { task: { id: string } }).task.id;
121
+
122
+ const missingTask = await runSubtask({ cwd, mode: "human", args: ["create", "--task", "--title", "A subtask"] });
123
+ expect(missingTask.ok).toBeFalse();
124
+ expect(missingTask.error?.code).toBe("invalid_input");
125
+ expect((missingTask.data as { option: string }).option).toBe("task");
126
+
127
+ const missingView = await runSubtask({ cwd, mode: "human", args: ["list", "--task", taskId, "--view"] });
128
+ expect(missingView.ok).toBeFalse();
129
+ expect(missingView.error?.code).toBe("invalid_input");
130
+ expect((missingView.data as { option: string }).option).toBe("view");
131
+ });
132
+ });
@@ -0,0 +1 @@
1
+ import "../sync.test";