trekoon 0.1.9 → 0.2.1
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 +176 -230
- package/README.md +299 -7
- package/package.json +1 -1
- package/src/commands/arg-parser.ts +198 -0
- package/src/commands/dep.ts +197 -25
- package/src/commands/epic.ts +674 -28
- package/src/commands/error-utils.ts +111 -0
- package/src/commands/events.ts +23 -3
- package/src/commands/help.ts +66 -4
- package/src/commands/init.ts +11 -3
- package/src/commands/migrate.ts +11 -4
- package/src/commands/subtask.ts +408 -26
- package/src/commands/sync.ts +7 -1
- package/src/commands/task.ts +381 -26
- package/src/domain/mutation-service.ts +394 -1
- package/src/domain/tracker-domain.ts +674 -0
- package/src/domain/types.ts +107 -0
- package/src/sync/event-writes.ts +21 -1
- package/src/sync/service.ts +42 -0
|
@@ -3,7 +3,91 @@ import { type Database } from "bun:sqlite";
|
|
|
3
3
|
import { appendEventWithGitContext } from "../sync/event-writes";
|
|
4
4
|
import { ENTITY_OPERATIONS } from "./mutation-operations";
|
|
5
5
|
import { TrackerDomain } from "./tracker-domain";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
type CompactEpicCreateResult,
|
|
8
|
+
type CompactEpicExpandResult,
|
|
9
|
+
type CompactDependencyBatchAddResult,
|
|
10
|
+
type CompactDependencySpec,
|
|
11
|
+
type CompactSubtaskBatchCreateResult,
|
|
12
|
+
type CompactSubtaskSpec,
|
|
13
|
+
type CompactTaskBatchCreateResult,
|
|
14
|
+
type CompactTaskSpec,
|
|
15
|
+
type DependencyRecord,
|
|
16
|
+
type EpicRecord,
|
|
17
|
+
type SearchEntityMatch,
|
|
18
|
+
type SearchField,
|
|
19
|
+
type SearchNode,
|
|
20
|
+
type SearchSummary,
|
|
21
|
+
type SubtaskRecord,
|
|
22
|
+
type TaskRecord,
|
|
23
|
+
} from "./types";
|
|
24
|
+
|
|
25
|
+
function countMatches(value: string, searchText: string): number {
|
|
26
|
+
if (searchText.length === 0) {
|
|
27
|
+
return 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let count = 0;
|
|
31
|
+
let offset = 0;
|
|
32
|
+
while (offset <= value.length - searchText.length) {
|
|
33
|
+
const nextIndex = value.indexOf(searchText, offset);
|
|
34
|
+
if (nextIndex === -1) {
|
|
35
|
+
return count;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
count += 1;
|
|
39
|
+
offset = nextIndex + searchText.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return count;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function replaceMatches(value: string, searchText: string, replacement: string): string {
|
|
46
|
+
return searchText.length === 0 ? value : value.split(searchText).join(replacement);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildMatchSnippet(value: string, searchText: string, contextSize = 24): string {
|
|
50
|
+
if (searchText.length === 0) {
|
|
51
|
+
return "";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const matchIndex = value.indexOf(searchText);
|
|
55
|
+
if (matchIndex === -1) {
|
|
56
|
+
return "";
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const start = Math.max(0, matchIndex - contextSize);
|
|
60
|
+
const end = Math.min(value.length, matchIndex + searchText.length + contextSize);
|
|
61
|
+
const rawSnippet = value.slice(start, end).replace(/\s+/g, " ").trim();
|
|
62
|
+
const prefix = start > 0 ? "…" : "";
|
|
63
|
+
const suffix = end < value.length ? "…" : "";
|
|
64
|
+
return `${prefix}${rawSnippet}${suffix}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildReplacementSnippet(value: string, replacementIndex: number, replacementLength: number, contextSize = 24): string {
|
|
68
|
+
const start = Math.max(0, replacementIndex - contextSize);
|
|
69
|
+
const end = Math.min(value.length, replacementIndex + replacementLength + contextSize);
|
|
70
|
+
const rawSnippet = value.slice(start, end).replace(/\s+/g, " ").trim();
|
|
71
|
+
const prefix = start > 0 ? "…" : "";
|
|
72
|
+
const suffix = end < value.length ? "…" : "";
|
|
73
|
+
return `${prefix}${rawSnippet}${suffix}`;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function summarizeMatches(matches: readonly SearchEntityMatch[]): SearchSummary {
|
|
77
|
+
return {
|
|
78
|
+
matchedEntities: matches.length,
|
|
79
|
+
matchedFields: matches.reduce((total, match) => total + match.fields.length, 0),
|
|
80
|
+
totalMatches: matches.reduce(
|
|
81
|
+
(total, match) => total + match.fields.reduce((fieldTotal, field) => fieldTotal + field.count, 0),
|
|
82
|
+
0,
|
|
83
|
+
),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface ScopeReplacementResult {
|
|
88
|
+
readonly matches: readonly SearchEntityMatch[];
|
|
89
|
+
readonly summary: SearchSummary;
|
|
90
|
+
}
|
|
7
91
|
|
|
8
92
|
export class MutationService {
|
|
9
93
|
readonly #db: Database;
|
|
@@ -28,6 +112,66 @@ export class MutationService {
|
|
|
28
112
|
})();
|
|
29
113
|
}
|
|
30
114
|
|
|
115
|
+
createEpicGraph(input: {
|
|
116
|
+
title: string;
|
|
117
|
+
description: string;
|
|
118
|
+
status?: string | undefined;
|
|
119
|
+
taskSpecs: readonly CompactTaskSpec[];
|
|
120
|
+
subtaskSpecs: readonly CompactSubtaskSpec[];
|
|
121
|
+
dependencySpecs: readonly CompactDependencySpec[];
|
|
122
|
+
}): CompactEpicCreateResult {
|
|
123
|
+
return this.#db.transaction((): CompactEpicCreateResult => {
|
|
124
|
+
const epic = this.#domain.createEpic(input);
|
|
125
|
+
const created = this.#domain.expandEpic({
|
|
126
|
+
epicId: epic.id,
|
|
127
|
+
taskSpecs: input.taskSpecs,
|
|
128
|
+
subtaskSpecs: input.subtaskSpecs,
|
|
129
|
+
dependencySpecs: input.dependencySpecs,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.created, {
|
|
133
|
+
title: epic.title,
|
|
134
|
+
description: epic.description,
|
|
135
|
+
status: epic.status,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
for (const task of created.tasks) {
|
|
139
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
|
|
140
|
+
epic_id: task.epicId,
|
|
141
|
+
title: task.title,
|
|
142
|
+
description: task.description,
|
|
143
|
+
status: task.status,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const subtask of created.subtasks) {
|
|
148
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
|
|
149
|
+
task_id: subtask.taskId,
|
|
150
|
+
title: subtask.title,
|
|
151
|
+
description: subtask.description,
|
|
152
|
+
status: subtask.status,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
for (const dependency of created.dependencies) {
|
|
157
|
+
this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
|
|
158
|
+
source_id: dependency.sourceId,
|
|
159
|
+
source_kind: dependency.sourceKind,
|
|
160
|
+
depends_on_id: dependency.dependsOnId,
|
|
161
|
+
depends_on_kind: dependency.dependsOnKind,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
epic,
|
|
167
|
+
tasks: created.tasks,
|
|
168
|
+
subtasks: created.subtasks,
|
|
169
|
+
dependencies: created.dependencies,
|
|
170
|
+
result: created.result,
|
|
171
|
+
};
|
|
172
|
+
})();
|
|
173
|
+
}
|
|
174
|
+
|
|
31
175
|
updateEpic(
|
|
32
176
|
id: string,
|
|
33
177
|
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
@@ -63,6 +207,60 @@ export class MutationService {
|
|
|
63
207
|
})();
|
|
64
208
|
}
|
|
65
209
|
|
|
210
|
+
createTaskBatch(input: { epicId: string; specs: readonly CompactTaskSpec[] }): CompactTaskBatchCreateResult {
|
|
211
|
+
return this.#db.transaction((): CompactTaskBatchCreateResult => {
|
|
212
|
+
const created = this.#domain.createTaskBatch(input);
|
|
213
|
+
for (const task of created.tasks) {
|
|
214
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
|
|
215
|
+
epic_id: task.epicId,
|
|
216
|
+
title: task.title,
|
|
217
|
+
description: task.description,
|
|
218
|
+
status: task.status,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
return created;
|
|
222
|
+
})();
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
expandEpic(input: {
|
|
226
|
+
epicId: string;
|
|
227
|
+
taskSpecs: readonly CompactTaskSpec[];
|
|
228
|
+
subtaskSpecs: readonly CompactSubtaskSpec[];
|
|
229
|
+
dependencySpecs: readonly CompactDependencySpec[];
|
|
230
|
+
}): CompactEpicExpandResult {
|
|
231
|
+
return this.#db.transaction((): CompactEpicExpandResult => {
|
|
232
|
+
const created = this.#domain.expandEpic(input);
|
|
233
|
+
for (const task of created.tasks) {
|
|
234
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.created, {
|
|
235
|
+
epic_id: task.epicId,
|
|
236
|
+
title: task.title,
|
|
237
|
+
description: task.description,
|
|
238
|
+
status: task.status,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
for (const subtask of created.subtasks) {
|
|
243
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
|
|
244
|
+
task_id: subtask.taskId,
|
|
245
|
+
title: subtask.title,
|
|
246
|
+
description: subtask.description,
|
|
247
|
+
status: subtask.status,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const dependency of created.dependencies) {
|
|
252
|
+
this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
|
|
253
|
+
source_id: dependency.sourceId,
|
|
254
|
+
source_kind: dependency.sourceKind,
|
|
255
|
+
depends_on_id: dependency.dependsOnId,
|
|
256
|
+
depends_on_kind: dependency.dependsOnKind,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return created;
|
|
261
|
+
})();
|
|
262
|
+
}
|
|
263
|
+
|
|
66
264
|
updateTask(
|
|
67
265
|
id: string,
|
|
68
266
|
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
@@ -104,6 +302,21 @@ export class MutationService {
|
|
|
104
302
|
})();
|
|
105
303
|
}
|
|
106
304
|
|
|
305
|
+
createSubtaskBatch(input: { taskId: string; specs: readonly CompactSubtaskSpec[] }): CompactSubtaskBatchCreateResult {
|
|
306
|
+
return this.#db.transaction((): CompactSubtaskBatchCreateResult => {
|
|
307
|
+
const created = this.#domain.createSubtaskBatch(input);
|
|
308
|
+
for (const subtask of created.subtasks) {
|
|
309
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.created, {
|
|
310
|
+
task_id: subtask.taskId,
|
|
311
|
+
title: subtask.title,
|
|
312
|
+
description: subtask.description,
|
|
313
|
+
status: subtask.status,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return created;
|
|
317
|
+
})();
|
|
318
|
+
}
|
|
319
|
+
|
|
107
320
|
updateSubtask(
|
|
108
321
|
id: string,
|
|
109
322
|
input: { title?: string | undefined; description?: string | undefined; status?: string | undefined },
|
|
@@ -140,6 +353,21 @@ export class MutationService {
|
|
|
140
353
|
})();
|
|
141
354
|
}
|
|
142
355
|
|
|
356
|
+
addDependencyBatch(input: { specs: readonly CompactDependencySpec[] }): CompactDependencyBatchAddResult {
|
|
357
|
+
return this.#db.transaction((): CompactDependencyBatchAddResult => {
|
|
358
|
+
const created = this.#domain.addDependencyBatch(input);
|
|
359
|
+
for (const dependency of created.dependencies) {
|
|
360
|
+
this.#appendEntityEvent("dependency", dependency.id, ENTITY_OPERATIONS.dependency.added, {
|
|
361
|
+
source_id: dependency.sourceId,
|
|
362
|
+
source_kind: dependency.sourceKind,
|
|
363
|
+
depends_on_id: dependency.dependsOnId,
|
|
364
|
+
depends_on_kind: dependency.dependsOnKind,
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
return created;
|
|
368
|
+
})();
|
|
369
|
+
}
|
|
370
|
+
|
|
143
371
|
removeDependency(sourceId: string, dependsOnId: string): number {
|
|
144
372
|
return this.#db.transaction((): number => {
|
|
145
373
|
const removed = this.#domain.removeDependency(sourceId, dependsOnId);
|
|
@@ -153,6 +381,60 @@ export class MutationService {
|
|
|
153
381
|
})();
|
|
154
382
|
}
|
|
155
383
|
|
|
384
|
+
previewEpicReplacement(
|
|
385
|
+
epicId: string,
|
|
386
|
+
searchText: string,
|
|
387
|
+
replacementText: string,
|
|
388
|
+
fields: readonly SearchField[],
|
|
389
|
+
): ScopeReplacementResult {
|
|
390
|
+
return this.#previewScopeReplacement(this.#domain.collectEpicSearchScope(epicId), searchText, replacementText, fields);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
applyEpicReplacement(
|
|
394
|
+
epicId: string,
|
|
395
|
+
searchText: string,
|
|
396
|
+
replacementText: string,
|
|
397
|
+
fields: readonly SearchField[],
|
|
398
|
+
): ScopeReplacementResult {
|
|
399
|
+
return this.#applyScopeReplacement(this.#domain.collectEpicSearchScope(epicId), searchText, replacementText, fields);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
previewTaskReplacement(
|
|
403
|
+
taskId: string,
|
|
404
|
+
searchText: string,
|
|
405
|
+
replacementText: string,
|
|
406
|
+
fields: readonly SearchField[],
|
|
407
|
+
): ScopeReplacementResult {
|
|
408
|
+
return this.#previewScopeReplacement(this.#domain.collectTaskSearchScope(taskId), searchText, replacementText, fields);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
applyTaskReplacement(
|
|
412
|
+
taskId: string,
|
|
413
|
+
searchText: string,
|
|
414
|
+
replacementText: string,
|
|
415
|
+
fields: readonly SearchField[],
|
|
416
|
+
): ScopeReplacementResult {
|
|
417
|
+
return this.#applyScopeReplacement(this.#domain.collectTaskSearchScope(taskId), searchText, replacementText, fields);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
previewSubtaskReplacement(
|
|
421
|
+
subtaskId: string,
|
|
422
|
+
searchText: string,
|
|
423
|
+
replacementText: string,
|
|
424
|
+
fields: readonly SearchField[],
|
|
425
|
+
): ScopeReplacementResult {
|
|
426
|
+
return this.#previewScopeReplacement(this.#domain.collectSubtaskSearchScope(subtaskId), searchText, replacementText, fields);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
applySubtaskReplacement(
|
|
430
|
+
subtaskId: string,
|
|
431
|
+
searchText: string,
|
|
432
|
+
replacementText: string,
|
|
433
|
+
fields: readonly SearchField[],
|
|
434
|
+
): ScopeReplacementResult {
|
|
435
|
+
return this.#applyScopeReplacement(this.#domain.collectSubtaskSearchScope(subtaskId), searchText, replacementText, fields);
|
|
436
|
+
}
|
|
437
|
+
|
|
156
438
|
#appendEntityEvent(
|
|
157
439
|
entityKind: "epic" | "task" | "subtask" | "dependency",
|
|
158
440
|
entityId: string,
|
|
@@ -166,4 +448,115 @@ export class MutationService {
|
|
|
166
448
|
fields,
|
|
167
449
|
});
|
|
168
450
|
}
|
|
451
|
+
|
|
452
|
+
#previewScopeReplacement(
|
|
453
|
+
nodes: readonly SearchNode[],
|
|
454
|
+
searchText: string,
|
|
455
|
+
replacementText: string,
|
|
456
|
+
fields: readonly SearchField[],
|
|
457
|
+
): ScopeReplacementResult {
|
|
458
|
+
return this.#buildScopeReplacementResult(nodes, searchText, replacementText, fields);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
#applyScopeReplacement(
|
|
462
|
+
nodes: readonly SearchNode[],
|
|
463
|
+
searchText: string,
|
|
464
|
+
replacementText: string,
|
|
465
|
+
fields: readonly SearchField[],
|
|
466
|
+
): ScopeReplacementResult {
|
|
467
|
+
const result = this.#buildScopeReplacementResult(nodes, searchText, replacementText, fields, "apply");
|
|
468
|
+
|
|
469
|
+
this.#db.transaction((): void => {
|
|
470
|
+
for (const node of nodes) {
|
|
471
|
+
const nextTitle = fields.includes("title") ? replaceMatches(node.title, searchText, replacementText) : node.title;
|
|
472
|
+
const nextDescription = fields.includes("description")
|
|
473
|
+
? replaceMatches(node.description, searchText, replacementText)
|
|
474
|
+
: node.description;
|
|
475
|
+
|
|
476
|
+
if (nextTitle === node.title && nextDescription === node.description) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if (node.kind === "epic") {
|
|
481
|
+
const epic = this.#domain.updateEpic(node.id, { title: nextTitle, description: nextDescription });
|
|
482
|
+
this.#appendEntityEvent("epic", epic.id, ENTITY_OPERATIONS.epic.updated, {
|
|
483
|
+
title: epic.title,
|
|
484
|
+
description: epic.description,
|
|
485
|
+
status: epic.status,
|
|
486
|
+
});
|
|
487
|
+
continue;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (node.kind === "task") {
|
|
491
|
+
const task = this.#domain.updateTask(node.id, { title: nextTitle, description: nextDescription });
|
|
492
|
+
this.#appendEntityEvent("task", task.id, ENTITY_OPERATIONS.task.updated, {
|
|
493
|
+
epic_id: task.epicId,
|
|
494
|
+
title: task.title,
|
|
495
|
+
description: task.description,
|
|
496
|
+
status: task.status,
|
|
497
|
+
});
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const subtask = this.#domain.updateSubtask(node.id, { title: nextTitle, description: nextDescription });
|
|
502
|
+
this.#appendEntityEvent("subtask", subtask.id, ENTITY_OPERATIONS.subtask.updated, {
|
|
503
|
+
task_id: subtask.taskId,
|
|
504
|
+
title: subtask.title,
|
|
505
|
+
description: subtask.description,
|
|
506
|
+
status: subtask.status,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
})();
|
|
510
|
+
|
|
511
|
+
return result;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#buildScopeReplacementResult(
|
|
515
|
+
nodes: readonly SearchNode[],
|
|
516
|
+
searchText: string,
|
|
517
|
+
replacementText: string,
|
|
518
|
+
fields: readonly SearchField[],
|
|
519
|
+
mode: "preview" | "apply" = "preview",
|
|
520
|
+
): ScopeReplacementResult {
|
|
521
|
+
const matches: SearchEntityMatch[] = [];
|
|
522
|
+
|
|
523
|
+
for (const node of nodes) {
|
|
524
|
+
const fieldMatches = fields
|
|
525
|
+
.map((field) => {
|
|
526
|
+
const value = field === "title" ? node.title : node.description;
|
|
527
|
+
const matchIndex = value.indexOf(searchText);
|
|
528
|
+
const nextValue = replaceMatches(value, searchText, replacementText);
|
|
529
|
+
const count = nextValue === value ? 0 : countMatches(value, searchText);
|
|
530
|
+
|
|
531
|
+
if (count === 0) {
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
return {
|
|
536
|
+
field,
|
|
537
|
+
count,
|
|
538
|
+
snippet:
|
|
539
|
+
mode === "apply"
|
|
540
|
+
? buildReplacementSnippet(nextValue, matchIndex, replacementText.length)
|
|
541
|
+
: buildMatchSnippet(value, searchText),
|
|
542
|
+
};
|
|
543
|
+
})
|
|
544
|
+
.filter((fieldMatch) => fieldMatch !== null);
|
|
545
|
+
|
|
546
|
+
if (fieldMatches.length === 0) {
|
|
547
|
+
continue;
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
matches.push({
|
|
551
|
+
kind: node.kind,
|
|
552
|
+
id: node.id,
|
|
553
|
+
fields: fieldMatches,
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return {
|
|
558
|
+
matches,
|
|
559
|
+
summary: summarizeMatches(matches),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
169
562
|
}
|