trekoon 0.3.5 → 0.3.7
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/board/assets/app.js +11 -0
- package/src/board/assets/components/Notice.js +18 -4
- package/src/board/assets/state/actions.js +6 -6
- package/src/board/assets/state/api.js +151 -26
- package/src/board/assets/state/store.js +38 -6
- package/src/board/assets/state/utils.js +73 -13
- package/src/board/routes.ts +392 -52
- package/src/board/snapshot.ts +151 -168
- package/src/commands/events.ts +17 -11
- package/src/commands/init.ts +36 -0
- package/src/commands/subtask.ts +2 -2
- package/src/domain/mutation-service.ts +310 -26
- package/src/domain/tracker-domain.ts +169 -5
- package/src/storage/migrations.ts +98 -0
- package/src/storage/path.ts +12 -1
- package/src/storage/schema.ts +17 -1
- package/src/storage/worktree-recovery.ts +12 -6
- package/src/sync/branch-db.ts +12 -1
- package/src/sync/event-writes.ts +43 -7
- package/src/sync/git-context.ts +10 -6
- package/src/sync/service.ts +578 -149
package/src/board/snapshot.ts
CHANGED
|
@@ -1,11 +1,7 @@
|
|
|
1
1
|
import { TrackerDomain } from "../domain/tracker-domain";
|
|
2
2
|
import { type DependencyRecord, type EpicRecord, type SubtaskRecord, type TaskRecord } from "../domain/types";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
readonly title: string;
|
|
6
|
-
readonly description: string;
|
|
7
|
-
readonly text: string;
|
|
8
|
-
}
|
|
4
|
+
type BoardStatus = "todo" | "blocked" | "in_progress" | "done";
|
|
9
5
|
|
|
10
6
|
interface StatusCounts {
|
|
11
7
|
readonly total: number;
|
|
@@ -16,65 +12,63 @@ interface StatusCounts {
|
|
|
16
12
|
readonly other: number;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
interface FlatCounts extends Record<BoardStatus, number> {}
|
|
16
|
+
|
|
17
|
+
export interface BoardSnapshotDependency {
|
|
20
18
|
readonly id: string;
|
|
21
|
-
readonly
|
|
22
|
-
readonly
|
|
23
|
-
readonly
|
|
19
|
+
readonly sourceId: string;
|
|
20
|
+
readonly sourceKind: "task" | "subtask";
|
|
21
|
+
readonly dependsOnId: string;
|
|
22
|
+
readonly dependsOnKind: "task" | "subtask";
|
|
24
23
|
readonly createdAt: number;
|
|
25
24
|
readonly updatedAt: number;
|
|
26
|
-
readonly taskIds: readonly string[];
|
|
27
|
-
readonly counts: {
|
|
28
|
-
readonly tasks: StatusCounts;
|
|
29
|
-
readonly subtasks: StatusCounts;
|
|
30
|
-
};
|
|
31
|
-
readonly search: SearchFields;
|
|
32
25
|
}
|
|
33
26
|
|
|
34
|
-
|
|
27
|
+
interface BoardSnapshotSubtask {
|
|
35
28
|
readonly id: string;
|
|
36
|
-
readonly
|
|
29
|
+
readonly kind: "subtask";
|
|
30
|
+
readonly taskId: string;
|
|
37
31
|
readonly title: string;
|
|
38
32
|
readonly description: string;
|
|
39
33
|
readonly status: string;
|
|
34
|
+
readonly owner: string | null;
|
|
40
35
|
readonly createdAt: number;
|
|
41
36
|
readonly updatedAt: number;
|
|
42
|
-
readonly
|
|
37
|
+
readonly blockedBy: readonly string[];
|
|
38
|
+
readonly blocks: readonly string[];
|
|
43
39
|
readonly dependencyIds: readonly string[];
|
|
44
40
|
readonly dependentIds: readonly string[];
|
|
45
|
-
readonly
|
|
46
|
-
readonly subtasks: StatusCounts;
|
|
47
|
-
readonly dependencies: number;
|
|
48
|
-
readonly dependents: number;
|
|
49
|
-
};
|
|
50
|
-
readonly search: SearchFields;
|
|
41
|
+
readonly searchText: string;
|
|
51
42
|
}
|
|
52
43
|
|
|
53
|
-
|
|
44
|
+
interface BoardSnapshotTask {
|
|
54
45
|
readonly id: string;
|
|
55
|
-
readonly
|
|
46
|
+
readonly kind: "task";
|
|
47
|
+
readonly epicId: string;
|
|
56
48
|
readonly title: string;
|
|
57
49
|
readonly description: string;
|
|
58
50
|
readonly status: string;
|
|
51
|
+
readonly owner: string | null;
|
|
59
52
|
readonly createdAt: number;
|
|
60
53
|
readonly updatedAt: number;
|
|
54
|
+
readonly blockedBy: readonly string[];
|
|
55
|
+
readonly blocks: readonly string[];
|
|
61
56
|
readonly dependencyIds: readonly string[];
|
|
62
57
|
readonly dependentIds: readonly string[];
|
|
63
|
-
readonly
|
|
64
|
-
|
|
65
|
-
readonly dependents: number;
|
|
66
|
-
};
|
|
67
|
-
readonly search: SearchFields;
|
|
58
|
+
readonly subtasks: readonly BoardSnapshotSubtask[];
|
|
59
|
+
readonly searchText: string;
|
|
68
60
|
}
|
|
69
61
|
|
|
70
|
-
|
|
62
|
+
interface BoardSnapshotEpic {
|
|
71
63
|
readonly id: string;
|
|
72
|
-
readonly
|
|
73
|
-
readonly
|
|
74
|
-
readonly
|
|
75
|
-
readonly dependsOnKind: "task" | "subtask";
|
|
64
|
+
readonly title: string;
|
|
65
|
+
readonly description: string;
|
|
66
|
+
readonly status: string;
|
|
76
67
|
readonly createdAt: number;
|
|
77
68
|
readonly updatedAt: number;
|
|
69
|
+
readonly taskIds: readonly string[];
|
|
70
|
+
readonly counts: FlatCounts;
|
|
71
|
+
readonly searchText: string;
|
|
78
72
|
}
|
|
79
73
|
|
|
80
74
|
export interface BoardSnapshot {
|
|
@@ -92,56 +86,31 @@ export interface BoardSnapshot {
|
|
|
92
86
|
}
|
|
93
87
|
|
|
94
88
|
function normalizeStatusBucket(status: string): keyof Omit<StatusCounts, "total"> {
|
|
95
|
-
if (status === "todo")
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if (status === "blocked") {
|
|
100
|
-
return "blocked";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (status === "in_progress" || status === "in-progress") {
|
|
104
|
-
return "inProgress";
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (status === "done") {
|
|
108
|
-
return "done";
|
|
109
|
-
}
|
|
110
|
-
|
|
89
|
+
if (status === "todo") return "todo";
|
|
90
|
+
if (status === "blocked") return "blocked";
|
|
91
|
+
if (status === "in_progress" || status === "in-progress") return "inProgress";
|
|
92
|
+
if (status === "done") return "done";
|
|
111
93
|
return "other";
|
|
112
94
|
}
|
|
113
95
|
|
|
114
96
|
function countStatuses(records: readonly { readonly status: string }[]): StatusCounts {
|
|
115
|
-
const counts:
|
|
116
|
-
total: number;
|
|
117
|
-
todo: number;
|
|
118
|
-
blocked: number;
|
|
119
|
-
inProgress: number;
|
|
120
|
-
done: number;
|
|
121
|
-
other: number;
|
|
122
|
-
} = {
|
|
123
|
-
total: records.length,
|
|
124
|
-
todo: 0,
|
|
125
|
-
blocked: 0,
|
|
126
|
-
inProgress: 0,
|
|
127
|
-
done: 0,
|
|
128
|
-
other: 0,
|
|
129
|
-
};
|
|
130
|
-
|
|
97
|
+
const counts = { total: records.length, todo: 0, blocked: 0, inProgress: 0, done: 0, other: 0 };
|
|
131
98
|
for (const record of records) {
|
|
132
|
-
|
|
133
|
-
counts[bucket] += 1;
|
|
99
|
+
counts[normalizeStatusBucket(record.status)] += 1;
|
|
134
100
|
}
|
|
135
|
-
|
|
136
101
|
return counts;
|
|
137
102
|
}
|
|
138
103
|
|
|
139
|
-
function
|
|
140
|
-
return
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
104
|
+
function deriveFlatCounts(records: readonly { readonly status: string }[]): FlatCounts {
|
|
105
|
+
return records.reduce<FlatCounts>(
|
|
106
|
+
(counts, record) => {
|
|
107
|
+
if (record.status === "todo" || record.status === "blocked" || record.status === "in_progress" || record.status === "done") {
|
|
108
|
+
counts[record.status] += 1;
|
|
109
|
+
}
|
|
110
|
+
return counts;
|
|
111
|
+
},
|
|
112
|
+
{ todo: 0, blocked: 0, in_progress: 0, done: 0 },
|
|
113
|
+
);
|
|
145
114
|
}
|
|
146
115
|
|
|
147
116
|
function mapDependency(record: DependencyRecord): BoardSnapshotDependency {
|
|
@@ -157,115 +126,129 @@ function mapDependency(record: DependencyRecord): BoardSnapshotDependency {
|
|
|
157
126
|
}
|
|
158
127
|
|
|
159
128
|
export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
|
|
160
|
-
const generatedAt
|
|
161
|
-
const epics
|
|
162
|
-
const tasks
|
|
163
|
-
const subtasks
|
|
129
|
+
const generatedAt = Date.now();
|
|
130
|
+
const epics = domain.listEpics();
|
|
131
|
+
const tasks = domain.listTasks();
|
|
132
|
+
const subtasks = domain.listSubtasks();
|
|
133
|
+
const sourceIds = [...tasks.map((task) => task.id), ...subtasks.map((subtask) => subtask.id)];
|
|
134
|
+
const dependenciesBySourceId = domain.listDependenciesBySourceIds(sourceIds);
|
|
135
|
+
const subtasksByTaskId = new Map<string, SubtaskRecord[]>();
|
|
136
|
+
const tasksByEpicId = new Map<string, TaskRecord[]>();
|
|
137
|
+
const dependencyIdsBySource = new Map<string, string[]>();
|
|
138
|
+
const blockedByIdsBySource = new Map<string, string[]>();
|
|
139
|
+
const dependentIdsByTarget = new Map<string, string[]>();
|
|
140
|
+
const blocksByTarget = new Map<string, string[]>();
|
|
164
141
|
const dependencies: BoardSnapshotDependency[] = [];
|
|
165
142
|
|
|
166
|
-
const tasksByEpic = new Map<string, TaskRecord[]>();
|
|
167
143
|
for (const task of tasks) {
|
|
168
|
-
const existing =
|
|
144
|
+
const existing = tasksByEpicId.get(task.epicId) ?? [];
|
|
169
145
|
existing.push(task);
|
|
170
|
-
|
|
146
|
+
tasksByEpicId.set(task.epicId, existing);
|
|
171
147
|
}
|
|
172
148
|
|
|
173
|
-
const subtasksByTask = new Map<string, SubtaskRecord[]>();
|
|
174
149
|
for (const subtask of subtasks) {
|
|
175
|
-
const existing =
|
|
150
|
+
const existing = subtasksByTaskId.get(subtask.taskId) ?? [];
|
|
176
151
|
existing.push(subtask);
|
|
177
|
-
|
|
152
|
+
subtasksByTaskId.set(subtask.taskId, existing);
|
|
178
153
|
}
|
|
179
154
|
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
for (const task of tasks) {
|
|
183
|
-
for (const dependency of domain.listDependencies(task.id)) {
|
|
155
|
+
for (const sourceId of sourceIds) {
|
|
156
|
+
for (const dependency of dependenciesBySourceId.get(sourceId) ?? []) {
|
|
184
157
|
dependencies.push(mapDependency(dependency));
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
158
|
+
|
|
159
|
+
const sourceDependencyIds = dependencyIdsBySource.get(dependency.sourceId) ?? [];
|
|
160
|
+
sourceDependencyIds.push(dependency.id);
|
|
161
|
+
dependencyIdsBySource.set(dependency.sourceId, sourceDependencyIds);
|
|
162
|
+
|
|
163
|
+
const sourceBlockedBy = blockedByIdsBySource.get(dependency.sourceId) ?? [];
|
|
164
|
+
sourceBlockedBy.push(dependency.dependsOnId);
|
|
165
|
+
blockedByIdsBySource.set(dependency.sourceId, sourceBlockedBy);
|
|
166
|
+
|
|
167
|
+
const targetDependentIds = dependentIdsByTarget.get(dependency.dependsOnId) ?? [];
|
|
168
|
+
targetDependentIds.push(dependency.id);
|
|
169
|
+
dependentIdsByTarget.set(dependency.dependsOnId, targetDependentIds);
|
|
170
|
+
|
|
171
|
+
const targetBlocks = blocksByTarget.get(dependency.dependsOnId) ?? [];
|
|
172
|
+
targetBlocks.push(dependency.sourceId);
|
|
173
|
+
blocksByTarget.set(dependency.dependsOnId, targetBlocks);
|
|
191
174
|
}
|
|
192
175
|
}
|
|
193
176
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
177
|
+
const snapshotSubtasks: BoardSnapshotSubtask[] = subtasks.map((subtask) => ({
|
|
178
|
+
id: subtask.id,
|
|
179
|
+
kind: "subtask",
|
|
180
|
+
taskId: subtask.taskId,
|
|
181
|
+
title: subtask.title,
|
|
182
|
+
description: subtask.description,
|
|
183
|
+
status: subtask.status,
|
|
184
|
+
owner: subtask.owner ?? null,
|
|
185
|
+
createdAt: subtask.createdAt,
|
|
186
|
+
updatedAt: subtask.updatedAt,
|
|
187
|
+
blockedBy: blockedByIdsBySource.get(subtask.id) ?? [],
|
|
188
|
+
blocks: blocksByTarget.get(subtask.id) ?? [],
|
|
189
|
+
dependencyIds: dependencyIdsBySource.get(subtask.id) ?? [],
|
|
190
|
+
dependentIds: dependentIdsByTarget.get(subtask.id) ?? [],
|
|
191
|
+
searchText: [subtask.title, subtask.description, subtask.status].join(" ").toLowerCase(),
|
|
192
|
+
}));
|
|
193
|
+
const snapshotSubtasksByTaskId = new Map<string, BoardSnapshotSubtask[]>();
|
|
194
|
+
for (const subtask of snapshotSubtasks) {
|
|
195
|
+
const existing = snapshotSubtasksByTaskId.get(subtask.taskId) ?? [];
|
|
196
|
+
existing.push(subtask);
|
|
197
|
+
snapshotSubtasksByTaskId.set(subtask.taskId, existing);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const snapshotTasks: BoardSnapshotTask[] = tasks.map((task) => {
|
|
201
|
+
const taskSubtasks = snapshotSubtasksByTaskId.get(task.id) ?? [];
|
|
202
|
+
return {
|
|
203
|
+
id: task.id,
|
|
204
|
+
kind: "task",
|
|
205
|
+
epicId: task.epicId,
|
|
206
|
+
title: task.title,
|
|
207
|
+
description: task.description,
|
|
208
|
+
status: task.status,
|
|
209
|
+
owner: task.owner ?? null,
|
|
210
|
+
createdAt: task.createdAt,
|
|
211
|
+
updatedAt: task.updatedAt,
|
|
212
|
+
blockedBy: blockedByIdsBySource.get(task.id) ?? [],
|
|
213
|
+
blocks: blocksByTarget.get(task.id) ?? [],
|
|
214
|
+
dependencyIds: dependencyIdsBySource.get(task.id) ?? [],
|
|
215
|
+
dependentIds: dependentIdsByTarget.get(task.id) ?? [],
|
|
216
|
+
subtasks: taskSubtasks,
|
|
217
|
+
searchText: [
|
|
218
|
+
task.title,
|
|
219
|
+
task.description,
|
|
220
|
+
task.status,
|
|
221
|
+
...taskSubtasks.map((subtask) => `${subtask.title} ${subtask.description} ${subtask.status}`),
|
|
222
|
+
].join(" ").toLowerCase(),
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
const taskSearchTextByEpicId = new Map<string, string[]>();
|
|
226
|
+
for (const task of snapshotTasks) {
|
|
227
|
+
const existing = taskSearchTextByEpicId.get(task.epicId) ?? [];
|
|
228
|
+
existing.push(task.searchText);
|
|
229
|
+
taskSearchTextByEpicId.set(task.epicId, existing);
|
|
204
230
|
}
|
|
205
231
|
|
|
232
|
+
const snapshotEpics: BoardSnapshotEpic[] = epics.map((epic) => {
|
|
233
|
+
const epicTasks = tasksByEpicId.get(epic.id) ?? [];
|
|
234
|
+
return {
|
|
235
|
+
id: epic.id,
|
|
236
|
+
title: epic.title,
|
|
237
|
+
description: epic.description,
|
|
238
|
+
status: epic.status,
|
|
239
|
+
createdAt: epic.createdAt,
|
|
240
|
+
updatedAt: epic.updatedAt,
|
|
241
|
+
taskIds: epicTasks.map((task) => task.id),
|
|
242
|
+
counts: deriveFlatCounts(epicTasks),
|
|
243
|
+
searchText: [epic.title, epic.description, ...(taskSearchTextByEpicId.get(epic.id) ?? [])].join(" ").toLowerCase(),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
|
|
206
247
|
return {
|
|
207
248
|
generatedAt,
|
|
208
|
-
epics:
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
return {
|
|
212
|
-
id: epic.id,
|
|
213
|
-
title: epic.title,
|
|
214
|
-
description: epic.description,
|
|
215
|
-
status: epic.status,
|
|
216
|
-
createdAt: epic.createdAt,
|
|
217
|
-
updatedAt: epic.updatedAt,
|
|
218
|
-
taskIds: epicTasks.map((task) => task.id),
|
|
219
|
-
counts: {
|
|
220
|
-
tasks: countStatuses(epicTasks),
|
|
221
|
-
subtasks: countStatuses(epicSubtasks),
|
|
222
|
-
},
|
|
223
|
-
search: buildSearchFields(epic.title, epic.description),
|
|
224
|
-
};
|
|
225
|
-
}),
|
|
226
|
-
tasks: tasks.map((task) => {
|
|
227
|
-
const taskSubtasks = subtasksByTask.get(task.id) ?? [];
|
|
228
|
-
const dependencyIds = dependencyIdsBySource.get(task.id) ?? [];
|
|
229
|
-
const dependentIds = dependentIdsByTarget.get(task.id) ?? [];
|
|
230
|
-
return {
|
|
231
|
-
id: task.id,
|
|
232
|
-
epicId: task.epicId,
|
|
233
|
-
title: task.title,
|
|
234
|
-
description: task.description,
|
|
235
|
-
status: task.status,
|
|
236
|
-
createdAt: task.createdAt,
|
|
237
|
-
updatedAt: task.updatedAt,
|
|
238
|
-
subtaskIds: taskSubtasks.map((subtask) => subtask.id),
|
|
239
|
-
dependencyIds,
|
|
240
|
-
dependentIds,
|
|
241
|
-
counts: {
|
|
242
|
-
subtasks: countStatuses(taskSubtasks),
|
|
243
|
-
dependencies: dependencyIds.length,
|
|
244
|
-
dependents: dependentIds.length,
|
|
245
|
-
},
|
|
246
|
-
search: buildSearchFields(task.title, task.description),
|
|
247
|
-
};
|
|
248
|
-
}),
|
|
249
|
-
subtasks: subtasks.map((subtask) => {
|
|
250
|
-
const dependencyIds = dependencyIdsBySource.get(subtask.id) ?? [];
|
|
251
|
-
const dependentIds = dependentIdsByTarget.get(subtask.id) ?? [];
|
|
252
|
-
return {
|
|
253
|
-
id: subtask.id,
|
|
254
|
-
taskId: subtask.taskId,
|
|
255
|
-
title: subtask.title,
|
|
256
|
-
description: subtask.description,
|
|
257
|
-
status: subtask.status,
|
|
258
|
-
createdAt: subtask.createdAt,
|
|
259
|
-
updatedAt: subtask.updatedAt,
|
|
260
|
-
dependencyIds,
|
|
261
|
-
dependentIds,
|
|
262
|
-
counts: {
|
|
263
|
-
dependencies: dependencyIds.length,
|
|
264
|
-
dependents: dependentIds.length,
|
|
265
|
-
},
|
|
266
|
-
search: buildSearchFields(subtask.title, subtask.description),
|
|
267
|
-
};
|
|
268
|
-
}),
|
|
249
|
+
epics: snapshotEpics,
|
|
250
|
+
tasks: snapshotTasks,
|
|
251
|
+
subtasks: snapshotSubtasks,
|
|
269
252
|
dependencies,
|
|
270
253
|
counts: {
|
|
271
254
|
epics: countStatuses(epics),
|
package/src/commands/events.ts
CHANGED
|
@@ -73,17 +73,23 @@ export async function runEvents(context: CliContext): Promise<CliResult> {
|
|
|
73
73
|
archive,
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
76
|
+
return okResult({
|
|
77
|
+
command: "events.prune",
|
|
78
|
+
human: [
|
|
79
|
+
dryRun ? "Dry run complete." : "Prune complete.",
|
|
80
|
+
`Retention days: ${summary.retentionDays}`,
|
|
81
|
+
`Candidates: ${summary.candidateCount}`,
|
|
82
|
+
`Archived: ${summary.archivedCount}`,
|
|
83
|
+
`Deleted: ${summary.deletedCount}`,
|
|
84
|
+
summary.staleCursorCount > 0
|
|
85
|
+
? `Sync guidance: ${summary.staleCursorCount} cursor(s) reference pruned history. Run 'trekoon sync pull --from <branch>' and rebuild if stale cursor hints persist.`
|
|
86
|
+
: "Sync guidance: pruning stayed within retained cursor history.",
|
|
87
|
+
archive
|
|
88
|
+
? "Retention automation: archived copies were kept before deletion."
|
|
89
|
+
: "Retention automation: rerun with --archive to keep retained copies before deletion.",
|
|
90
|
+
].join("\n"),
|
|
91
|
+
data: summary,
|
|
92
|
+
});
|
|
87
93
|
} catch (error: unknown) {
|
|
88
94
|
const busyFailure = sqliteBusyFailure("events.prune", error);
|
|
89
95
|
if (busyFailure !== null) {
|
package/src/commands/init.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
1
4
|
import { unexpectedFailureResult } from "./error-utils";
|
|
2
5
|
|
|
3
6
|
import { ensureBoardInstalled } from "../board/install";
|
|
@@ -6,6 +9,11 @@ import { DomainError } from "../domain/types";
|
|
|
6
9
|
import { failResult, okResult } from "../io/output";
|
|
7
10
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
8
11
|
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
12
|
+
import { type StorageMode } from "../storage/path";
|
|
13
|
+
|
|
14
|
+
type GitignoreAction = "created" | "already_exists" | "skipped";
|
|
15
|
+
|
|
16
|
+
const GITIGNORE_CONTENT = "*\n";
|
|
9
17
|
|
|
10
18
|
function buildRecoverySummary(database: TrekoonDatabase): string[] {
|
|
11
19
|
const diagnostics = database.diagnostics;
|
|
@@ -76,6 +84,24 @@ function recoveryFailureResult(error: DomainError): CliResult | null {
|
|
|
76
84
|
});
|
|
77
85
|
}
|
|
78
86
|
|
|
87
|
+
function ensureGitignore(storageDir: string, storageMode: StorageMode): GitignoreAction {
|
|
88
|
+
if (storageMode === "cwd") {
|
|
89
|
+
return "skipped";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const gitignorePath: string = resolve(storageDir, ".gitignore");
|
|
93
|
+
|
|
94
|
+
if (existsSync(gitignorePath)) {
|
|
95
|
+
const existing: string = readFileSync(gitignorePath, "utf8");
|
|
96
|
+
if (existing === GITIGNORE_CONTENT) {
|
|
97
|
+
return "already_exists";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
writeFileSync(gitignorePath, GITIGNORE_CONTENT, "utf8");
|
|
102
|
+
return "created";
|
|
103
|
+
}
|
|
104
|
+
|
|
79
105
|
export async function runInit(context: CliContext): Promise<CliResult> {
|
|
80
106
|
let database: TrekoonDatabase | undefined;
|
|
81
107
|
|
|
@@ -87,6 +113,11 @@ export async function runInit(context: CliContext): Promise<CliResult> {
|
|
|
87
113
|
workingDirectory: context.cwd,
|
|
88
114
|
...(bundledAssetRoot === undefined ? {} : { bundledAssetRoot }),
|
|
89
115
|
});
|
|
116
|
+
const gitignoreAction: GitignoreAction = ensureGitignore(
|
|
117
|
+
database.paths.storageDir,
|
|
118
|
+
diagnostics.storageMode,
|
|
119
|
+
);
|
|
120
|
+
|
|
90
121
|
const humanLines: string[] = [
|
|
91
122
|
"Trekoon initialized.",
|
|
92
123
|
`Storage mode: ${diagnostics.storageMode}`,
|
|
@@ -96,6 +127,7 @@ export async function runInit(context: CliContext): Promise<CliResult> {
|
|
|
96
127
|
`Database file: ${database.paths.databaseFile}`,
|
|
97
128
|
`Board assets: ${board.action}`,
|
|
98
129
|
`Board runtime root: ${board.paths.runtimeRoot}`,
|
|
130
|
+
`Gitignore: ${gitignoreAction}`,
|
|
99
131
|
...buildRecoverySummary(database),
|
|
100
132
|
];
|
|
101
133
|
|
|
@@ -115,6 +147,10 @@ export async function runInit(context: CliContext): Promise<CliResult> {
|
|
|
115
147
|
paths: board.paths,
|
|
116
148
|
manifest: board.manifest,
|
|
117
149
|
},
|
|
150
|
+
gitignore: {
|
|
151
|
+
action: gitignoreAction,
|
|
152
|
+
path: resolve(database.paths.storageDir, ".gitignore"),
|
|
153
|
+
},
|
|
118
154
|
legacyStateDetected: diagnostics.legacyStateDetected,
|
|
119
155
|
recoveryRequired: diagnostics.recoveryRequired,
|
|
120
156
|
recoveryStatus: diagnostics.recoveryStatus,
|
package/src/commands/subtask.ts
CHANGED
|
@@ -935,12 +935,12 @@ export async function runSubtask(context: CliContext): Promise<CliResult> {
|
|
|
935
935
|
}
|
|
936
936
|
case "delete": {
|
|
937
937
|
const subtaskId: string = parsed.positional[1] ?? "";
|
|
938
|
-
mutations.deleteSubtask(subtaskId);
|
|
938
|
+
const result = mutations.deleteSubtask(subtaskId);
|
|
939
939
|
|
|
940
940
|
return okResult({
|
|
941
941
|
command: "subtask.delete",
|
|
942
942
|
human: `Deleted subtask ${subtaskId}`,
|
|
943
|
-
data: { id: subtaskId },
|
|
943
|
+
data: { id: subtaskId, deletedDependencyIds: result.deletedDependencyIds },
|
|
944
944
|
});
|
|
945
945
|
}
|
|
946
946
|
default:
|