trekoon 0.1.4 → 0.1.6
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/.agents/skills/trekoon/SKILL.md +7 -3
- package/README.md +13 -4
- package/package.json +1 -1
- package/src/commands/dep.ts +5 -3
- package/src/commands/epic.ts +7 -5
- package/src/commands/help.ts +2 -2
- package/src/commands/quickstart.ts +1 -1
- package/src/commands/skills.ts +210 -29
- package/src/commands/subtask.ts +112 -7
- package/src/commands/sync.ts +98 -3
- package/src/commands/task.ts +7 -5
- package/src/domain/mutation-operations.ts +27 -0
- package/src/domain/mutation-service.ts +169 -0
- package/src/sync/service.ts +350 -64
- package/src/sync/types.ts +35 -0
package/src/commands/subtask.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { hasFlag, parseArgs, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
|
|
1
|
+
import { hasFlag, parseArgs, parseStrictPositiveInt, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { MutationService } from "../domain/mutation-service";
|
|
4
4
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
5
|
+
import { DomainError, type SubtaskRecord } from "../domain/types";
|
|
5
6
|
import { formatHumanTable } from "../io/human-table";
|
|
6
7
|
import { failResult, okResult } from "../io/output";
|
|
7
8
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
@@ -12,6 +13,8 @@ function formatSubtask(subtask: SubtaskRecord): string {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
const VIEW_MODES = ["table", "compact"] as const;
|
|
16
|
+
const DEFAULT_SUBTASK_LIST_LIMIT = 10;
|
|
17
|
+
const DEFAULT_OPEN_SUBTASK_STATUSES = ["in_progress", "in-progress", "todo"] as const;
|
|
15
18
|
|
|
16
19
|
function parseIdsOption(rawIds: string | undefined): string[] {
|
|
17
20
|
if (rawIds === undefined) {
|
|
@@ -24,6 +27,45 @@ function parseIdsOption(rawIds: string | undefined): string[] {
|
|
|
24
27
|
.filter((value) => value.length > 0);
|
|
25
28
|
}
|
|
26
29
|
|
|
30
|
+
function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
|
|
31
|
+
if (rawStatuses === undefined) {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return rawStatuses
|
|
36
|
+
.split(",")
|
|
37
|
+
.map((value) => value.trim())
|
|
38
|
+
.filter((value) => value.length > 0);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function subtaskStatusPriority(status: string): number {
|
|
42
|
+
if (status === "in_progress" || status === "in-progress") {
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (status === "todo") {
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return 2;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function filterSortAndLimitSubtasks(
|
|
54
|
+
subtasks: readonly SubtaskRecord[],
|
|
55
|
+
statuses: readonly string[] | undefined,
|
|
56
|
+
limit: number | undefined,
|
|
57
|
+
): SubtaskRecord[] {
|
|
58
|
+
const allowedStatuses = statuses === undefined ? undefined : new Set(statuses);
|
|
59
|
+
const filtered = allowedStatuses === undefined ? [...subtasks] : subtasks.filter((subtask) => allowedStatuses.has(subtask.status));
|
|
60
|
+
const sorted = [...filtered].sort((left, right) => subtaskStatusPriority(left.status) - subtaskStatusPriority(right.status));
|
|
61
|
+
|
|
62
|
+
if (limit === undefined) {
|
|
63
|
+
return sorted;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return sorted.slice(0, limit);
|
|
67
|
+
}
|
|
68
|
+
|
|
27
69
|
function appendLine(existing: string, line: string): string {
|
|
28
70
|
return existing.length > 0 ? `${existing}\n${line}` : line;
|
|
29
71
|
}
|
|
@@ -85,6 +127,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
85
127
|
const parsed = parseArgs(context.args);
|
|
86
128
|
const subcommand: string | undefined = parsed.positional[0];
|
|
87
129
|
const domain = new TrackerDomain(database.db);
|
|
130
|
+
const mutations = new MutationService(database.db, context.cwd);
|
|
88
131
|
|
|
89
132
|
switch (subcommand) {
|
|
90
133
|
case "create": {
|
|
@@ -100,7 +143,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
100
143
|
const title: string | undefined = readOption(parsed.options, "title") ?? parsed.positional[2];
|
|
101
144
|
const description: string | undefined = readOption(parsed.options, "description", "d");
|
|
102
145
|
const status: string | undefined = readOption(parsed.options, "status", "s");
|
|
103
|
-
const subtask =
|
|
146
|
+
const subtask = mutations.createSubtask({
|
|
104
147
|
taskId: taskId ?? "",
|
|
105
148
|
title: title ?? "",
|
|
106
149
|
description,
|
|
@@ -116,6 +159,8 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
116
159
|
case "list": {
|
|
117
160
|
const missingListOption =
|
|
118
161
|
readMissingOptionValue(parsed.missingOptionValues, "view") ??
|
|
162
|
+
readMissingOptionValue(parsed.missingOptionValues, "status", "s") ??
|
|
163
|
+
readMissingOptionValue(parsed.missingOptionValues, "limit", "l") ??
|
|
119
164
|
readMissingOptionValue(parsed.missingOptionValues, "task", "t");
|
|
120
165
|
if (missingListOption !== undefined) {
|
|
121
166
|
return failMissingOptionValue("subtask.list", missingListOption);
|
|
@@ -123,6 +168,10 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
123
168
|
|
|
124
169
|
const rawView: string | undefined = readOption(parsed.options, "view");
|
|
125
170
|
const view = readEnumOption(parsed.options, VIEW_MODES, "view");
|
|
171
|
+
const includeAll = hasFlag(parsed.flags, "all");
|
|
172
|
+
const rawStatuses = readOption(parsed.options, "status", "s");
|
|
173
|
+
const rawLimit = readOption(parsed.options, "limit", "l");
|
|
174
|
+
|
|
126
175
|
if (rawView !== undefined && view === undefined) {
|
|
127
176
|
return failResult({
|
|
128
177
|
command: "subtask.list",
|
|
@@ -135,8 +184,64 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
135
184
|
});
|
|
136
185
|
}
|
|
137
186
|
|
|
187
|
+
if (includeAll && rawStatuses !== undefined) {
|
|
188
|
+
return failResult({
|
|
189
|
+
command: "subtask.list",
|
|
190
|
+
human: "Use either --all or --status, not both.",
|
|
191
|
+
data: { code: "invalid_input", flags: ["all", "status"] },
|
|
192
|
+
error: {
|
|
193
|
+
code: "invalid_input",
|
|
194
|
+
message: "--all and --status are mutually exclusive",
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (includeAll && rawLimit !== undefined) {
|
|
200
|
+
return failResult({
|
|
201
|
+
command: "subtask.list",
|
|
202
|
+
human: "Use either --all or --limit, not both.",
|
|
203
|
+
data: { code: "invalid_input", flags: ["all", "limit"] },
|
|
204
|
+
error: {
|
|
205
|
+
code: "invalid_input",
|
|
206
|
+
message: "--all and --limit are mutually exclusive",
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const statuses = parseStatusCsv(rawStatuses);
|
|
212
|
+
if (rawStatuses !== undefined && statuses !== undefined && statuses.length === 0) {
|
|
213
|
+
return failResult({
|
|
214
|
+
command: "subtask.list",
|
|
215
|
+
human: "Provide at least one status with --status.",
|
|
216
|
+
data: { code: "invalid_input", status: rawStatuses },
|
|
217
|
+
error: {
|
|
218
|
+
code: "invalid_input",
|
|
219
|
+
message: "Invalid --status value",
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const parsedLimit = parseStrictPositiveInt(rawLimit);
|
|
225
|
+
if (Number.isNaN(parsedLimit)) {
|
|
226
|
+
return failResult({
|
|
227
|
+
command: "subtask.list",
|
|
228
|
+
human: "Invalid --limit value. Use an integer >= 1.",
|
|
229
|
+
data: { code: "invalid_input", limit: rawLimit },
|
|
230
|
+
error: {
|
|
231
|
+
code: "invalid_input",
|
|
232
|
+
message: "Invalid --limit value",
|
|
233
|
+
},
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
138
237
|
const taskId: string | undefined = readOption(parsed.options, "task", "t") ?? parsed.positional[1];
|
|
139
|
-
const
|
|
238
|
+
const selectedStatuses = includeAll
|
|
239
|
+
? undefined
|
|
240
|
+
: statuses ?? [...DEFAULT_OPEN_SUBTASK_STATUSES];
|
|
241
|
+
const selectedLimit = includeAll
|
|
242
|
+
? undefined
|
|
243
|
+
: parsedLimit ?? DEFAULT_SUBTASK_LIST_LIMIT;
|
|
244
|
+
const subtasks = filterSortAndLimitSubtasks(domain.listSubtasks(taskId), selectedStatuses, selectedLimit);
|
|
140
245
|
const listView = view ?? "table";
|
|
141
246
|
const human =
|
|
142
247
|
subtasks.length === 0
|
|
@@ -234,7 +339,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
234
339
|
|
|
235
340
|
const targets = updateAll ? [...domain.listSubtasks()] : ids.map((id) => domain.getSubtaskOrThrow(id));
|
|
236
341
|
const subtasks = targets.map((target) =>
|
|
237
|
-
|
|
342
|
+
mutations.updateSubtask(target.id, {
|
|
238
343
|
status,
|
|
239
344
|
description: append === undefined ? undefined : appendLine(target.description, append),
|
|
240
345
|
}),
|
|
@@ -267,7 +372,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
267
372
|
append === undefined
|
|
268
373
|
? description
|
|
269
374
|
: appendLine(domain.getSubtaskOrThrow(subtaskId).description, append);
|
|
270
|
-
const subtask =
|
|
375
|
+
const subtask = mutations.updateSubtask(subtaskId, { title, description: nextDescription, status });
|
|
271
376
|
|
|
272
377
|
return okResult({
|
|
273
378
|
command: "subtask.update",
|
|
@@ -277,7 +382,7 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
277
382
|
}
|
|
278
383
|
case "delete": {
|
|
279
384
|
const subtaskId: string = parsed.positional[1] ?? "";
|
|
280
|
-
|
|
385
|
+
mutations.deleteSubtask(subtaskId);
|
|
281
386
|
|
|
282
387
|
return okResult({
|
|
283
388
|
command: "subtask.delete",
|
package/src/commands/sync.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { failResult, okResult } from "../io/output";
|
|
2
2
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
3
3
|
import { MissingBranchDatabaseError } from "../sync/branch-db";
|
|
4
|
-
import { syncPull, syncResolve, syncStatus } from "../sync/service";
|
|
5
|
-
import { type SyncResolution } from "../sync/types";
|
|
4
|
+
import { getSyncConflict, listSyncConflicts, syncPull, syncResolve, syncStatus } from "../sync/service";
|
|
5
|
+
import { type SyncConflictMode, type SyncResolution } from "../sync/types";
|
|
6
6
|
|
|
7
7
|
function parseOption(args: readonly string[], option: string): string | null {
|
|
8
8
|
const index: number = args.indexOf(option);
|
|
@@ -17,7 +17,7 @@ function parseOption(args: readonly string[], option: string): string | null {
|
|
|
17
17
|
function usage(message: string): CliResult {
|
|
18
18
|
return failResult({
|
|
19
19
|
command: "sync",
|
|
20
|
-
human: `${message}\nUsage: trekoon sync <status|pull|resolve> [options]`,
|
|
20
|
+
human: `${message}\nUsage: trekoon sync <status|pull|resolve|conflicts> [options]`,
|
|
21
21
|
data: { message },
|
|
22
22
|
error: {
|
|
23
23
|
code: "invalid_args",
|
|
@@ -35,6 +35,50 @@ function statusMessage(sourceBranch: string, ahead: number, behind: number, conf
|
|
|
35
35
|
].join("\n");
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
function formatConflictList(
|
|
39
|
+
conflicts: ReadonlyArray<{
|
|
40
|
+
id: string;
|
|
41
|
+
entity_kind: string;
|
|
42
|
+
entity_id: string;
|
|
43
|
+
field_name: string;
|
|
44
|
+
resolution: string;
|
|
45
|
+
}>,
|
|
46
|
+
): string {
|
|
47
|
+
if (conflicts.length === 0) {
|
|
48
|
+
return "No conflicts found.";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return conflicts
|
|
52
|
+
.map((conflict) =>
|
|
53
|
+
[
|
|
54
|
+
conflict.id,
|
|
55
|
+
conflict.entity_kind,
|
|
56
|
+
conflict.entity_id,
|
|
57
|
+
conflict.field_name,
|
|
58
|
+
conflict.resolution,
|
|
59
|
+
].join(" | "),
|
|
60
|
+
)
|
|
61
|
+
.join("\n");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function parseConflictMode(args: readonly string[]): SyncConflictMode | null {
|
|
65
|
+
const modeIndex = args.indexOf("--mode");
|
|
66
|
+
if (modeIndex < 0) {
|
|
67
|
+
return "pending";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const explicitMode = args[modeIndex + 1];
|
|
71
|
+
if (!explicitMode || explicitMode.startsWith("--")) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (explicitMode === "pending" || explicitMode === "all") {
|
|
76
|
+
return explicitMode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
|
|
38
82
|
export async function runSync(context: CliContext): Promise<CliResult> {
|
|
39
83
|
const subcommand: string | undefined = context.args[0];
|
|
40
84
|
|
|
@@ -95,6 +139,57 @@ export async function runSync(context: CliContext): Promise<CliResult> {
|
|
|
95
139
|
});
|
|
96
140
|
}
|
|
97
141
|
|
|
142
|
+
if (subcommand === "conflicts") {
|
|
143
|
+
const conflictsCommand: string | undefined = context.args[1];
|
|
144
|
+
if (!conflictsCommand) {
|
|
145
|
+
return usage("sync conflicts requires list|show.");
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (conflictsCommand === "list") {
|
|
149
|
+
const mode = parseConflictMode(context.args);
|
|
150
|
+
if (!mode) {
|
|
151
|
+
return usage("sync conflicts list --mode only accepts pending|all.");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const conflicts = listSyncConflicts(context.cwd, mode);
|
|
155
|
+
|
|
156
|
+
return okResult({
|
|
157
|
+
command: "sync conflicts list",
|
|
158
|
+
human: formatConflictList(conflicts),
|
|
159
|
+
data: {
|
|
160
|
+
mode,
|
|
161
|
+
conflicts,
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (conflictsCommand === "show") {
|
|
167
|
+
const conflictId: string | undefined = context.args[2];
|
|
168
|
+
if (!conflictId) {
|
|
169
|
+
return usage("sync conflicts show requires <conflict-id>.");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const conflict = getSyncConflict(context.cwd, conflictId);
|
|
173
|
+
|
|
174
|
+
return okResult({
|
|
175
|
+
command: "sync conflicts show",
|
|
176
|
+
human: [
|
|
177
|
+
`Conflict: ${conflict.id}`,
|
|
178
|
+
`Entity: ${conflict.entityKind} ${conflict.entityId}`,
|
|
179
|
+
`Field: ${conflict.fieldName}`,
|
|
180
|
+
`Resolution: ${conflict.resolution}`,
|
|
181
|
+
`Ours: ${JSON.stringify(conflict.oursValue)}`,
|
|
182
|
+
`Theirs: ${JSON.stringify(conflict.theirsValue)}`,
|
|
183
|
+
].join("\n"),
|
|
184
|
+
data: {
|
|
185
|
+
conflict,
|
|
186
|
+
},
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return usage(`Unknown sync conflicts subcommand '${conflictsCommand}'.`);
|
|
191
|
+
}
|
|
192
|
+
|
|
98
193
|
return usage(`Unknown sync subcommand '${subcommand}'.`);
|
|
99
194
|
} catch (error) {
|
|
100
195
|
if (error instanceof MissingBranchDatabaseError) {
|
package/src/commands/task.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { hasFlag, parseArgs, parseStrictPositiveInt, readEnumOption, readMissingOptionValue, readOption } from "./arg-parser";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { MutationService } from "../domain/mutation-service";
|
|
4
4
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
5
|
+
import { DomainError, type TaskRecord } from "../domain/types";
|
|
5
6
|
import { formatHumanTable } from "../io/human-table";
|
|
6
7
|
import { failResult, okResult } from "../io/output";
|
|
7
8
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
@@ -192,6 +193,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
192
193
|
const parsed = parseArgs(context.args);
|
|
193
194
|
const subcommand: string | undefined = parsed.positional[0];
|
|
194
195
|
const domain = new TrackerDomain(database.db);
|
|
196
|
+
const mutations = new MutationService(database.db, context.cwd);
|
|
195
197
|
|
|
196
198
|
switch (subcommand) {
|
|
197
199
|
case "create": {
|
|
@@ -207,7 +209,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
207
209
|
const title: string | undefined = readOption(parsed.options, "title", "t");
|
|
208
210
|
const description: string | undefined = readOption(parsed.options, "description", "d");
|
|
209
211
|
const status: string | undefined = readOption(parsed.options, "status", "s");
|
|
210
|
-
const task =
|
|
212
|
+
const task = mutations.createTask({
|
|
211
213
|
epicId: epicId ?? "",
|
|
212
214
|
title: title ?? "",
|
|
213
215
|
description: description ?? "",
|
|
@@ -482,7 +484,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
482
484
|
|
|
483
485
|
const targets = updateAll ? [...domain.listTasks()] : ids.map((id) => domain.getTaskOrThrow(id));
|
|
484
486
|
const tasks = targets.map((target) =>
|
|
485
|
-
|
|
487
|
+
mutations.updateTask(target.id, {
|
|
486
488
|
status,
|
|
487
489
|
description: append === undefined ? undefined : appendLine(target.description, append),
|
|
488
490
|
}),
|
|
@@ -515,7 +517,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
515
517
|
append === undefined
|
|
516
518
|
? description
|
|
517
519
|
: appendLine(domain.getTaskOrThrow(taskId).description, append);
|
|
518
|
-
const task =
|
|
520
|
+
const task = mutations.updateTask(taskId, { title, description: nextDescription, status });
|
|
519
521
|
|
|
520
522
|
return okResult({
|
|
521
523
|
command: "task.update",
|
|
@@ -525,7 +527,7 @@ export async function runTask(context: CliContext): Promise<CliResult> {
|
|
|
525
527
|
}
|
|
526
528
|
case "delete": {
|
|
527
529
|
const taskId: string = parsed.positional[1] ?? "";
|
|
528
|
-
|
|
530
|
+
mutations.deleteTask(taskId);
|
|
529
531
|
|
|
530
532
|
return okResult({
|
|
531
533
|
command: "task.delete",
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const ENTITY_OPERATIONS = {
|
|
2
|
+
epic: {
|
|
3
|
+
created: "epic.created",
|
|
4
|
+
updated: "epic.updated",
|
|
5
|
+
deleted: "epic.deleted",
|
|
6
|
+
},
|
|
7
|
+
task: {
|
|
8
|
+
created: "task.created",
|
|
9
|
+
updated: "task.updated",
|
|
10
|
+
deleted: "task.deleted",
|
|
11
|
+
},
|
|
12
|
+
subtask: {
|
|
13
|
+
created: "subtask.created",
|
|
14
|
+
updated: "subtask.updated",
|
|
15
|
+
deleted: "subtask.deleted",
|
|
16
|
+
},
|
|
17
|
+
dependency: {
|
|
18
|
+
added: "dependency.added",
|
|
19
|
+
removed: "dependency.removed",
|
|
20
|
+
},
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
export type MutationOperation =
|
|
24
|
+
| (typeof ENTITY_OPERATIONS)["epic"][keyof (typeof ENTITY_OPERATIONS)["epic"]]
|
|
25
|
+
| (typeof ENTITY_OPERATIONS)["task"][keyof (typeof ENTITY_OPERATIONS)["task"]]
|
|
26
|
+
| (typeof ENTITY_OPERATIONS)["subtask"][keyof (typeof ENTITY_OPERATIONS)["subtask"]]
|
|
27
|
+
| (typeof ENTITY_OPERATIONS)["dependency"][keyof (typeof ENTITY_OPERATIONS)["dependency"]];
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { type Database } from "bun:sqlite";
|
|
2
|
+
|
|
3
|
+
import { appendEventWithGitContext } from "../sync/event-writes";
|
|
4
|
+
import { ENTITY_OPERATIONS } from "./mutation-operations";
|
|
5
|
+
import { TrackerDomain } from "./tracker-domain";
|
|
6
|
+
import { type DependencyRecord, type EpicRecord, type SubtaskRecord, type TaskRecord } from "./types";
|
|
7
|
+
|
|
8
|
+
export class MutationService {
|
|
9
|
+
readonly #db: Database;
|
|
10
|
+
readonly #cwd: string;
|
|
11
|
+
readonly #domain: TrackerDomain;
|
|
12
|
+
|
|
13
|
+
constructor(db: Database, cwd: string) {
|
|
14
|
+
this.#db = db;
|
|
15
|
+
this.#cwd = cwd;
|
|
16
|
+
this.#domain = new TrackerDomain(db);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
createEpic(input: { title: string; description: string; status?: string | undefined }): EpicRecord {
|
|
20
|
+
return this.#db.transaction((): EpicRecord => {
|
|
21
|
+
const epic = this.#domain.createEpic(input);
|
|
22
|
+
this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.created, {
|
|
23
|
+
title: epic.title,
|
|
24
|
+
description: epic.description,
|
|
25
|
+
status: epic.status,
|
|
26
|
+
});
|
|
27
|
+
return epic;
|
|
28
|
+
})();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
updateEpic(
|
|
32
|
+
id: string,
|
|
33
|
+
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
34
|
+
): EpicRecord {
|
|
35
|
+
return this.#db.transaction((): EpicRecord => {
|
|
36
|
+
const epic = this.#domain.updateEpic(id, input);
|
|
37
|
+
this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.updated, {
|
|
38
|
+
title: epic.title,
|
|
39
|
+
description: epic.description,
|
|
40
|
+
status: epic.status,
|
|
41
|
+
});
|
|
42
|
+
return epic;
|
|
43
|
+
})();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
deleteEpic(id: string): void {
|
|
47
|
+
this.#db.transaction((): void => {
|
|
48
|
+
this.#domain.deleteEpic(id);
|
|
49
|
+
this.#appendEntityEvent("epic", id, ENTITY_OPERATIONS.epic.deleted, {});
|
|
50
|
+
})();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
createTask(input: { epicId: string; title: string; description: string; status?: string | undefined }): TaskRecord {
|
|
54
|
+
return this.#db.transaction((): TaskRecord => {
|
|
55
|
+
const task = this.#domain.createTask(input);
|
|
56
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
|
|
57
|
+
epic_id: task.epicId,
|
|
58
|
+
title: task.title,
|
|
59
|
+
description: task.description,
|
|
60
|
+
status: task.status,
|
|
61
|
+
});
|
|
62
|
+
return task;
|
|
63
|
+
})();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
updateTask(
|
|
67
|
+
id: string,
|
|
68
|
+
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
69
|
+
): TaskRecord {
|
|
70
|
+
return this.#db.transaction((): TaskRecord => {
|
|
71
|
+
const task = this.#domain.updateTask(id, input);
|
|
72
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.updated, {
|
|
73
|
+
epic_id: task.epicId,
|
|
74
|
+
title: task.title,
|
|
75
|
+
description: task.description,
|
|
76
|
+
status: task.status,
|
|
77
|
+
});
|
|
78
|
+
return task;
|
|
79
|
+
})();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
deleteTask(id: string): void {
|
|
83
|
+
this.#db.transaction((): void => {
|
|
84
|
+
this.#domain.deleteTask(id);
|
|
85
|
+
this.#appendEntityEvent("task", id, ENTITY_OPERATIONS.task.deleted, {});
|
|
86
|
+
})();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
createSubtask(input: {
|
|
90
|
+
taskId: string;
|
|
91
|
+
title: string;
|
|
92
|
+
description?: string | undefined;
|
|
93
|
+
status?: string | undefined;
|
|
94
|
+
}): SubtaskRecord {
|
|
95
|
+
return this.#db.transaction((): SubtaskRecord => {
|
|
96
|
+
const subtask = this.#domain.createSubtask(input);
|
|
97
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
|
|
98
|
+
task_id: subtask.taskId,
|
|
99
|
+
title: subtask.title,
|
|
100
|
+
description: subtask.description,
|
|
101
|
+
status: subtask.status,
|
|
102
|
+
});
|
|
103
|
+
return subtask;
|
|
104
|
+
})();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
updateSubtask(
|
|
108
|
+
id: string,
|
|
109
|
+
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
110
|
+
): SubtaskRecord {
|
|
111
|
+
return this.#db.transaction((): SubtaskRecord => {
|
|
112
|
+
const subtask = this.#domain.updateSubtask(id, input);
|
|
113
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.updated, {
|
|
114
|
+
task_id: subtask.taskId,
|
|
115
|
+
title: subtask.title,
|
|
116
|
+
description: subtask.description,
|
|
117
|
+
status: subtask.status,
|
|
118
|
+
});
|
|
119
|
+
return subtask;
|
|
120
|
+
})();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
deleteSubtask(id: string): void {
|
|
124
|
+
this.#db.transaction((): void => {
|
|
125
|
+
this.#domain.deleteSubtask(id);
|
|
126
|
+
this.#appendEntityEvent("subtask", id, ENTITY_OPERATIONS.subtask.deleted, {});
|
|
127
|
+
})();
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
addDependency(sourceId: string, dependsOnId: string): DependencyRecord {
|
|
131
|
+
return this.#db.transaction((): DependencyRecord => {
|
|
132
|
+
const dependency = this.#domain.addDependency(sourceId, dependsOnId);
|
|
133
|
+
this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
|
|
134
|
+
source_id: dependency.sourceId,
|
|
135
|
+
source_kind: dependency.sourceKind,
|
|
136
|
+
depends_on_id: dependency.dependsOnId,
|
|
137
|
+
depends_on_kind: dependency.dependsOnKind,
|
|
138
|
+
});
|
|
139
|
+
return dependency;
|
|
140
|
+
})();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
removeDependency(sourceId: string, dependsOnId: string): number {
|
|
144
|
+
return this.#db.transaction((): number => {
|
|
145
|
+
const removed = this.#domain.removeDependency(sourceId, dependsOnId);
|
|
146
|
+
if (removed > 0) {
|
|
147
|
+
this.#appendEntityEvent("dependency", `${sourceId}->${dependsOnId}`, ENTITY_OPERATIONS.dependency.removed, {
|
|
148
|
+
source_id: sourceId,
|
|
149
|
+
depends_on_id: dependsOnId,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return removed;
|
|
153
|
+
})();
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
#appendEntityEvent(
|
|
157
|
+
entityKind: "epic" | "task" | "subtask" | "dependency",
|
|
158
|
+
entityId: string,
|
|
159
|
+
operation: string,
|
|
160
|
+
fields: Record<string, unknown>,
|
|
161
|
+
): void {
|
|
162
|
+
appendEventWithGitContext(this.#db, this.#cwd, {
|
|
163
|
+
entityKind,
|
|
164
|
+
entityId,
|
|
165
|
+
operation,
|
|
166
|
+
fields,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|