trekoon 0.3.7 → 0.3.9
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 +198 -73
- package/.agents/skills/trekoon/reference/execution-with-team.md +9 -11
- package/.agents/skills/trekoon/reference/execution.md +26 -9
- package/.agents/skills/trekoon/reference/planning.md +48 -0
- package/README.md +40 -14
- package/docs/ai-agents.md +1 -0
- package/docs/commands.md +18 -0
- package/docs/quickstart.md +35 -0
- package/package.json +1 -1
- package/src/board/assets/app.js +8 -25
- package/src/board/assets/state/api.js +5 -6
- package/src/board/assets/state/utils.js +50 -17
- package/src/board/routes.ts +22 -19
- package/src/board/server.ts +57 -4
- package/src/board/snapshot.ts +133 -84
- package/src/commands/board.ts +1 -1
- package/src/commands/epic.ts +84 -1
- package/src/commands/help.ts +19 -1
- package/src/commands/quickstart.ts +13 -0
- package/src/domain/mutation-service.ts +179 -65
- package/src/domain/tracker-domain.ts +16 -2
- package/src/export/build-epic-export-bundle.ts +178 -0
- package/src/export/path.ts +48 -0
- package/src/export/render-markdown.ts +256 -0
- package/src/export/types.ts +61 -0
- package/src/export/write.ts +97 -0
- package/src/storage/migrations.ts +27 -2
- package/src/storage/schema.ts +2 -1
- package/src/sync/event-writes.ts +11 -7
- package/src/sync/service.ts +183 -4
package/src/board/snapshot.ts
CHANGED
|
@@ -85,6 +85,15 @@ export interface BoardSnapshot {
|
|
|
85
85
|
};
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
interface SnapshotDeltaSelection {
|
|
89
|
+
readonly epicIds?: readonly string[];
|
|
90
|
+
readonly taskIds?: readonly string[];
|
|
91
|
+
readonly subtaskIds?: readonly string[];
|
|
92
|
+
readonly dependencyIds?: readonly string[];
|
|
93
|
+
readonly deletedSubtaskIds?: readonly string[];
|
|
94
|
+
readonly deletedDependencyIds?: readonly string[];
|
|
95
|
+
}
|
|
96
|
+
|
|
88
97
|
function normalizeStatusBucket(status: string): keyof Omit<StatusCounts, "total"> {
|
|
89
98
|
if (status === "todo") return "todo";
|
|
90
99
|
if (status === "blocked") return "blocked";
|
|
@@ -125,56 +134,32 @@ function mapDependency(record: DependencyRecord): BoardSnapshotDependency {
|
|
|
125
134
|
};
|
|
126
135
|
}
|
|
127
136
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
function uniqueIds(ids: readonly string[]): string[] {
|
|
138
|
+
return [...new Set(ids.filter((id) => id.length > 0))];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function buildDependencyIndexes(dependenciesBySourceId: Map<string, readonly DependencyRecord[]>, sourceIds: readonly string[]) {
|
|
137
142
|
const dependencyIdsBySource = new Map<string, string[]>();
|
|
138
143
|
const blockedByIdsBySource = new Map<string, string[]>();
|
|
139
144
|
const dependentIdsByTarget = new Map<string, string[]>();
|
|
140
145
|
const blocksByTarget = new Map<string, string[]>();
|
|
141
146
|
const dependencies: BoardSnapshotDependency[] = [];
|
|
142
147
|
|
|
143
|
-
for (const task of tasks) {
|
|
144
|
-
const existing = tasksByEpicId.get(task.epicId) ?? [];
|
|
145
|
-
existing.push(task);
|
|
146
|
-
tasksByEpicId.set(task.epicId, existing);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
for (const subtask of subtasks) {
|
|
150
|
-
const existing = subtasksByTaskId.get(subtask.taskId) ?? [];
|
|
151
|
-
existing.push(subtask);
|
|
152
|
-
subtasksByTaskId.set(subtask.taskId, existing);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
148
|
for (const sourceId of sourceIds) {
|
|
156
149
|
for (const dependency of dependenciesBySourceId.get(sourceId) ?? []) {
|
|
157
150
|
dependencies.push(mapDependency(dependency));
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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);
|
|
151
|
+
(dependencyIdsBySource.get(dependency.sourceId) ?? dependencyIdsBySource.set(dependency.sourceId, []).get(dependency.sourceId) ?? []).push(dependency.id);
|
|
152
|
+
(blockedByIdsBySource.get(dependency.sourceId) ?? blockedByIdsBySource.set(dependency.sourceId, []).get(dependency.sourceId) ?? []).push(dependency.dependsOnId);
|
|
153
|
+
(dependentIdsByTarget.get(dependency.dependsOnId) ?? dependentIdsByTarget.set(dependency.dependsOnId, []).get(dependency.dependsOnId) ?? []).push(dependency.id);
|
|
154
|
+
(blocksByTarget.get(dependency.dependsOnId) ?? blocksByTarget.set(dependency.dependsOnId, []).get(dependency.dependsOnId) ?? []).push(dependency.sourceId);
|
|
174
155
|
}
|
|
175
156
|
}
|
|
176
157
|
|
|
177
|
-
|
|
158
|
+
return { dependencies, dependencyIdsBySource, blockedByIdsBySource, dependentIdsByTarget, blocksByTarget };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function mapSnapshotSubtask(subtask: SubtaskRecord, indexes: ReturnType<typeof buildDependencyIndexes>): BoardSnapshotSubtask {
|
|
162
|
+
return {
|
|
178
163
|
id: subtask.id,
|
|
179
164
|
kind: "subtask",
|
|
180
165
|
taskId: subtask.taskId,
|
|
@@ -184,12 +169,113 @@ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
|
|
|
184
169
|
owner: subtask.owner ?? null,
|
|
185
170
|
createdAt: subtask.createdAt,
|
|
186
171
|
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) ?? [],
|
|
172
|
+
blockedBy: indexes.blockedByIdsBySource.get(subtask.id) ?? [],
|
|
173
|
+
blocks: indexes.blocksByTarget.get(subtask.id) ?? [],
|
|
174
|
+
dependencyIds: indexes.dependencyIdsBySource.get(subtask.id) ?? [],
|
|
175
|
+
dependentIds: indexes.dependentIdsByTarget.get(subtask.id) ?? [],
|
|
191
176
|
searchText: [subtask.title, subtask.description, subtask.status].join(" ").toLowerCase(),
|
|
192
|
-
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function mapSnapshotTask(task: TaskRecord, taskSubtasks: readonly BoardSnapshotSubtask[], indexes: ReturnType<typeof buildDependencyIndexes>): BoardSnapshotTask {
|
|
181
|
+
return {
|
|
182
|
+
id: task.id,
|
|
183
|
+
kind: "task",
|
|
184
|
+
epicId: task.epicId,
|
|
185
|
+
title: task.title,
|
|
186
|
+
description: task.description,
|
|
187
|
+
status: task.status,
|
|
188
|
+
owner: task.owner ?? null,
|
|
189
|
+
createdAt: task.createdAt,
|
|
190
|
+
updatedAt: task.updatedAt,
|
|
191
|
+
blockedBy: indexes.blockedByIdsBySource.get(task.id) ?? [],
|
|
192
|
+
blocks: indexes.blocksByTarget.get(task.id) ?? [],
|
|
193
|
+
dependencyIds: indexes.dependencyIdsBySource.get(task.id) ?? [],
|
|
194
|
+
dependentIds: indexes.dependentIdsByTarget.get(task.id) ?? [],
|
|
195
|
+
subtasks: taskSubtasks,
|
|
196
|
+
searchText: [task.title, task.description, task.status, ...taskSubtasks.map((subtask) => `${subtask.title} ${subtask.description} ${subtask.status}`)].join(" ").toLowerCase(),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function mapSnapshotEpic(epic: EpicRecord, epicTasks: readonly BoardSnapshotTask[]): BoardSnapshotEpic {
|
|
201
|
+
return {
|
|
202
|
+
id: epic.id,
|
|
203
|
+
title: epic.title,
|
|
204
|
+
description: epic.description,
|
|
205
|
+
status: epic.status,
|
|
206
|
+
createdAt: epic.createdAt,
|
|
207
|
+
updatedAt: epic.updatedAt,
|
|
208
|
+
taskIds: epicTasks.map((task) => task.id),
|
|
209
|
+
counts: deriveFlatCounts(epicTasks),
|
|
210
|
+
searchText: [epic.title, epic.description, ...epicTasks.map((task) => task.searchText)].join(" ").toLowerCase(),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function buildBoardSnapshotDelta(domain: TrackerDomain, selection: SnapshotDeltaSelection): Record<string, unknown> {
|
|
215
|
+
const epicIds = uniqueIds(selection.epicIds ?? []);
|
|
216
|
+
const requestedTaskIds = uniqueIds(selection.taskIds ?? []);
|
|
217
|
+
const requestedSubtaskIds = uniqueIds(selection.subtaskIds ?? []);
|
|
218
|
+
const requestedDependencyIds = new Set(selection.dependencyIds ?? []);
|
|
219
|
+
const relatedTaskIds = uniqueIds([
|
|
220
|
+
...requestedTaskIds,
|
|
221
|
+
...requestedSubtaskIds.map((subtaskId) => domain.getSubtask(subtaskId)?.taskId ?? ""),
|
|
222
|
+
]);
|
|
223
|
+
const tasks = relatedTaskIds.map((taskId) => domain.getTask(taskId)).filter((task): task is TaskRecord => task !== null);
|
|
224
|
+
const subtasksByTaskId = new Map<string, readonly SubtaskRecord[]>();
|
|
225
|
+
for (const task of tasks) {
|
|
226
|
+
subtasksByTaskId.set(task.id, domain.listSubtasks(task.id));
|
|
227
|
+
}
|
|
228
|
+
const allSubtasks = uniqueIds([
|
|
229
|
+
...requestedSubtaskIds,
|
|
230
|
+
...[...subtasksByTaskId.values()].flatMap((taskSubtasks) => taskSubtasks.map((subtask) => subtask.id)),
|
|
231
|
+
]).map((subtaskId) => domain.getSubtask(subtaskId)).filter((subtask): subtask is SubtaskRecord => subtask !== null);
|
|
232
|
+
const sourceIds = uniqueIds([...tasks.map((task) => task.id), ...allSubtasks.map((subtask) => subtask.id)]);
|
|
233
|
+
const indexes = buildDependencyIndexes(domain.listDependenciesBySourceIds(sourceIds), sourceIds);
|
|
234
|
+
const snapshotSubtasksByTaskId = new Map<string, BoardSnapshotSubtask[]>();
|
|
235
|
+
for (const subtask of allSubtasks) {
|
|
236
|
+
const mappedSubtask = mapSnapshotSubtask(subtask, indexes);
|
|
237
|
+
const taskSubtasks = snapshotSubtasksByTaskId.get(subtask.taskId) ?? [];
|
|
238
|
+
taskSubtasks.push(mappedSubtask);
|
|
239
|
+
snapshotSubtasksByTaskId.set(subtask.taskId, taskSubtasks);
|
|
240
|
+
}
|
|
241
|
+
const snapshotTasks = tasks.map((task) => mapSnapshotTask(task, snapshotSubtasksByTaskId.get(task.id) ?? [], indexes));
|
|
242
|
+
const snapshotEpics = epicIds.map((epicId) => domain.getEpic(epicId)).filter((epic): epic is EpicRecord => epic !== null).map((epic) => mapSnapshotEpic(epic, snapshotTasks.filter((task) => task.epicId === epic.id)));
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
generatedAt: Date.now(),
|
|
246
|
+
epics: snapshotEpics,
|
|
247
|
+
tasks: snapshotTasks.filter((task) => requestedTaskIds.includes(task.id)),
|
|
248
|
+
subtasks: allSubtasks.map((subtask) => mapSnapshotSubtask(subtask, indexes)).filter((subtask) => requestedSubtaskIds.includes(subtask.id)),
|
|
249
|
+
dependencies: indexes.dependencies.filter((dependency) => requestedDependencyIds.has(dependency.id)),
|
|
250
|
+
deletedSubtaskIds: [...(selection.deletedSubtaskIds ?? [])],
|
|
251
|
+
deletedDependencyIds: [...(selection.deletedDependencyIds ?? [])],
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
|
|
256
|
+
const generatedAt = Date.now();
|
|
257
|
+
const epics = domain.listEpics();
|
|
258
|
+
const tasks = domain.listTasks();
|
|
259
|
+
const subtasks = domain.listSubtasks();
|
|
260
|
+
const sourceIds = [...tasks.map((task) => task.id), ...subtasks.map((subtask) => subtask.id)];
|
|
261
|
+
const dependenciesBySourceId = domain.listDependenciesBySourceIds(sourceIds);
|
|
262
|
+
const subtasksByTaskId = new Map<string, SubtaskRecord[]>();
|
|
263
|
+
const tasksByEpicId = new Map<string, TaskRecord[]>();
|
|
264
|
+
const indexes = buildDependencyIndexes(dependenciesBySourceId, sourceIds);
|
|
265
|
+
|
|
266
|
+
for (const task of tasks) {
|
|
267
|
+
const existing = tasksByEpicId.get(task.epicId) ?? [];
|
|
268
|
+
existing.push(task);
|
|
269
|
+
tasksByEpicId.set(task.epicId, existing);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const subtask of subtasks) {
|
|
273
|
+
const existing = subtasksByTaskId.get(subtask.taskId) ?? [];
|
|
274
|
+
existing.push(subtask);
|
|
275
|
+
subtasksByTaskId.set(subtask.taskId, existing);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const snapshotSubtasks: BoardSnapshotSubtask[] = subtasks.map((subtask) => mapSnapshotSubtask(subtask, indexes));
|
|
193
279
|
const snapshotSubtasksByTaskId = new Map<string, BoardSnapshotSubtask[]>();
|
|
194
280
|
for (const subtask of snapshotSubtasks) {
|
|
195
281
|
const existing = snapshotSubtasksByTaskId.get(subtask.taskId) ?? [];
|
|
@@ -197,31 +283,7 @@ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
|
|
|
197
283
|
snapshotSubtasksByTaskId.set(subtask.taskId, existing);
|
|
198
284
|
}
|
|
199
285
|
|
|
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
|
-
});
|
|
286
|
+
const snapshotTasks: BoardSnapshotTask[] = tasks.map((task) => mapSnapshotTask(task, snapshotSubtasksByTaskId.get(task.id) ?? [], indexes));
|
|
225
287
|
const taskSearchTextByEpicId = new Map<string, string[]>();
|
|
226
288
|
for (const task of snapshotTasks) {
|
|
227
289
|
const existing = taskSearchTextByEpicId.get(task.epicId) ?? [];
|
|
@@ -229,32 +291,19 @@ export function buildBoardSnapshot(domain: TrackerDomain): BoardSnapshot {
|
|
|
229
291
|
taskSearchTextByEpicId.set(task.epicId, existing);
|
|
230
292
|
}
|
|
231
293
|
|
|
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
|
-
});
|
|
294
|
+
const snapshotEpics: BoardSnapshotEpic[] = epics.map((epic) => mapSnapshotEpic(epic, snapshotTasks.filter((task) => task.epicId === epic.id)));
|
|
246
295
|
|
|
247
296
|
return {
|
|
248
297
|
generatedAt,
|
|
249
298
|
epics: snapshotEpics,
|
|
250
299
|
tasks: snapshotTasks,
|
|
251
300
|
subtasks: snapshotSubtasks,
|
|
252
|
-
dependencies,
|
|
301
|
+
dependencies: indexes.dependencies,
|
|
253
302
|
counts: {
|
|
254
303
|
epics: countStatuses(epics),
|
|
255
304
|
tasks: countStatuses(tasks),
|
|
256
305
|
subtasks: countStatuses(subtasks),
|
|
257
|
-
dependencies: dependencies.length,
|
|
306
|
+
dependencies: indexes.dependencies.length,
|
|
258
307
|
},
|
|
259
308
|
};
|
|
260
309
|
}
|
package/src/commands/board.ts
CHANGED
|
@@ -121,7 +121,7 @@ export async function runBoard(context: CliContext): Promise<CliResult> {
|
|
|
121
121
|
return okResult({
|
|
122
122
|
command: "board.open",
|
|
123
123
|
human: [
|
|
124
|
-
`Board ready at ${server.
|
|
124
|
+
`Board ready at ${server.fallbackUrl}`,
|
|
125
125
|
launch.launched
|
|
126
126
|
? `Browser launched with ${launch.command}`
|
|
127
127
|
: `Browser launch failed: ${launch.errorMessage ?? "unknown failure"}`,
|
package/src/commands/epic.ts
CHANGED
|
@@ -36,7 +36,12 @@ import {
|
|
|
36
36
|
import { formatHumanTable } from "../io/human-table";
|
|
37
37
|
import { failResult, okResult } from "../io/output";
|
|
38
38
|
import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
39
|
+
import { buildEpicExportBundle } from "../export/build-epic-export-bundle";
|
|
40
|
+
import { resolveExportPath } from "../export/path";
|
|
41
|
+
import { renderMarkdown } from "../export/render-markdown";
|
|
42
|
+
import { atomicWrite, ExportWriteError } from "../export/write";
|
|
39
43
|
import { openTrekoonDatabase, type TrekoonDatabase } from "../storage/database";
|
|
44
|
+
import { resolveStoragePaths } from "../storage/path";
|
|
40
45
|
|
|
41
46
|
function formatEpic(epic: EpicRecord): string {
|
|
42
47
|
return `${epic.id} | ${epic.title} | ${epic.status}`;
|
|
@@ -53,6 +58,7 @@ const SEARCH_OPTIONS = ["fields", "preview"] as const;
|
|
|
53
58
|
const REPLACE_OPTIONS = ["search", "replace", "fields", "preview", "apply"] as const;
|
|
54
59
|
const EXPAND_OPTIONS = ["task", "subtask", "dep"] as const;
|
|
55
60
|
const UPDATE_OPTIONS = ["all", "ids", "append", "description", "d", "status", "s", "title", "t"] as const;
|
|
61
|
+
const EXPORT_OPTIONS = ["path", "overwrite"] as const;
|
|
56
62
|
const STATUS_CASCADE_UPDATE_STATUSES = ["done", "todo"] as const;
|
|
57
63
|
|
|
58
64
|
function parseStatusCsv(rawStatuses: string | undefined): string[] | undefined {
|
|
@@ -1472,10 +1478,87 @@ export async function runEpic(context: CliContext): Promise<CliResult> {
|
|
|
1472
1478
|
data: { id: epicId },
|
|
1473
1479
|
});
|
|
1474
1480
|
}
|
|
1481
|
+
case "export": {
|
|
1482
|
+
const epicId: string = parsed.positional[1] ?? "";
|
|
1483
|
+
const exportUnknown = findUnknownOption(parsed, EXPORT_OPTIONS);
|
|
1484
|
+
if (exportUnknown !== undefined) {
|
|
1485
|
+
return unknownOption("epic.export", exportUnknown, EXPORT_OPTIONS);
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const missingExportOption = readMissingOptionValue(parsed.missingOptionValues, "path");
|
|
1489
|
+
if (missingExportOption !== undefined) {
|
|
1490
|
+
return failMissingOptionValue("epic.export", missingExportOption);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
if (!epicId) {
|
|
1494
|
+
return failResult({
|
|
1495
|
+
command: "epic.export",
|
|
1496
|
+
human: "Usage: trekoon epic export <epic-id> [--path <path>] [--overwrite]",
|
|
1497
|
+
data: {},
|
|
1498
|
+
error: { code: "invalid_input", message: "Missing epic ID" },
|
|
1499
|
+
});
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
const bundle = buildEpicExportBundle(domain, epicId);
|
|
1503
|
+
const markdown = renderMarkdown(bundle);
|
|
1504
|
+
const storagePaths = resolveStoragePaths(context.cwd);
|
|
1505
|
+
const customPath = readOption(parsed.options, "path");
|
|
1506
|
+
const overwrite = hasFlag(parsed.flags, "overwrite");
|
|
1507
|
+
|
|
1508
|
+
const exportPath = resolveExportPath({
|
|
1509
|
+
customPath,
|
|
1510
|
+
epicId: bundle.epic.id,
|
|
1511
|
+
epicTitle: bundle.epic.title,
|
|
1512
|
+
worktreeRoot: storagePaths.worktreeRoot,
|
|
1513
|
+
cwd: context.cwd,
|
|
1514
|
+
});
|
|
1515
|
+
|
|
1516
|
+
try {
|
|
1517
|
+
const result = atomicWrite({ path: exportPath, content: markdown, overwrite });
|
|
1518
|
+
const action = result.overwritten ? "Updated" : "Exported";
|
|
1519
|
+
return okResult({
|
|
1520
|
+
command: "epic.export",
|
|
1521
|
+
human: `${action} epic to ${result.path}`,
|
|
1522
|
+
data: {
|
|
1523
|
+
epicId: bundle.epic.id,
|
|
1524
|
+
path: result.path,
|
|
1525
|
+
overwritten: result.overwritten,
|
|
1526
|
+
summary: bundle.summary,
|
|
1527
|
+
},
|
|
1528
|
+
});
|
|
1529
|
+
} catch (error: unknown) {
|
|
1530
|
+
if (error instanceof ExportWriteError) {
|
|
1531
|
+
return failResult({
|
|
1532
|
+
command: "epic.export",
|
|
1533
|
+
human: error.message,
|
|
1534
|
+
data: { path: exportPath, epicId: bundle.epic.id },
|
|
1535
|
+
error: { code: error.code, message: error.message },
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
const fsCode = typeof error === "object" && error !== null && "code" in error ? (error as { code: string }).code : null;
|
|
1539
|
+
if (fsCode === "EACCES" || fsCode === "EPERM" || fsCode === "EROFS") {
|
|
1540
|
+
return failResult({
|
|
1541
|
+
command: "epic.export",
|
|
1542
|
+
human: `Permission denied: cannot write to ${exportPath}`,
|
|
1543
|
+
data: { path: exportPath, epicId: bundle.epic.id, fsError: fsCode },
|
|
1544
|
+
error: { code: "permission_denied", message: `Permission denied: ${exportPath}` },
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
if (fsCode === "EISDIR") {
|
|
1548
|
+
return failResult({
|
|
1549
|
+
command: "epic.export",
|
|
1550
|
+
human: `Path is a directory, not a file: ${exportPath}`,
|
|
1551
|
+
data: { path: exportPath, epicId: bundle.epic.id, fsError: fsCode },
|
|
1552
|
+
error: { code: "invalid_path", message: `Path is a directory: ${exportPath}` },
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
throw error;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1475
1558
|
default:
|
|
1476
1559
|
return failResult({
|
|
1477
1560
|
command: "epic",
|
|
1478
|
-
human: "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress>",
|
|
1561
|
+
human: "Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress|export>",
|
|
1479
1562
|
data: {
|
|
1480
1563
|
args: context.args,
|
|
1481
1564
|
},
|
package/src/commands/help.ts
CHANGED
|
@@ -103,7 +103,7 @@ const BOARD_HELP = [
|
|
|
103
103
|
].join("\n");
|
|
104
104
|
|
|
105
105
|
const EPIC_HELP = [
|
|
106
|
-
"Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress> [options]",
|
|
106
|
+
"Usage: trekoon epic <create|expand|list|show|search|replace|update|delete|progress|export> [options]",
|
|
107
107
|
"",
|
|
108
108
|
"Create:",
|
|
109
109
|
" trekoon epic create --title \"...\" --description \"...\" [--status <status>]",
|
|
@@ -149,6 +149,24 @@ const EPIC_HELP = [
|
|
|
149
149
|
"Progress:",
|
|
150
150
|
" trekoon epic progress <epic-id>",
|
|
151
151
|
" Returns done/in_progress/blocked/todo counts, readyCount, and nextCandidate.",
|
|
152
|
+
"",
|
|
153
|
+
"Export:",
|
|
154
|
+
" trekoon epic export <epic-id> [--path <path>] [--overwrite]",
|
|
155
|
+
" Writes a Markdown snapshot of the epic including tasks, subtasks, dependencies,",
|
|
156
|
+
" external node stubs, and warnings. The Markdown is a point-in-time artifact;",
|
|
157
|
+
" the database remains the source of truth.",
|
|
158
|
+
"",
|
|
159
|
+
" Default path: <worktree-root>/plans/<slugified-title>.md",
|
|
160
|
+
" --path <path> Custom output path (relative or absolute).",
|
|
161
|
+
" With extension (e.g. docs/plan.md): creates that file.",
|
|
162
|
+
" Without extension (e.g. docs/plans): creates the default-named file inside.",
|
|
163
|
+
" --overwrite Resave if the file already exists.",
|
|
164
|
+
"",
|
|
165
|
+
" Examples:",
|
|
166
|
+
" trekoon epic export abc-123",
|
|
167
|
+
" trekoon epic export abc-123 --path docs/plan.md # writes docs/plan.md",
|
|
168
|
+
" trekoon epic export abc-123 --path docs/plans # writes docs/plans/<title>.md",
|
|
169
|
+
" trekoon epic export abc-123 --overwrite",
|
|
152
170
|
].join("\n");
|
|
153
171
|
|
|
154
172
|
const TASK_HELP = [
|
|
@@ -4,6 +4,16 @@ import { type CliContext, type CliResult } from "../runtime/command-types";
|
|
|
4
4
|
const QUICKSTART_TEXT = [
|
|
5
5
|
"Trekoon quickstart",
|
|
6
6
|
"",
|
|
7
|
+
"Human workflow:",
|
|
8
|
+
" 1. Gather context through discussion, brainstorming, or research.",
|
|
9
|
+
" 2. Run: trekoon plan <goal>",
|
|
10
|
+
" Use this when you want Trekoon to turn the goal into an execution-ready epic.",
|
|
11
|
+
" 3. Run: trekoon <epic-id>",
|
|
12
|
+
" Use this to inspect the epic, next ready work, and blockers before execution.",
|
|
13
|
+
" 4. Run: trekoon <epic-id> execute",
|
|
14
|
+
" Use this when you want the agent to keep working until the epic is done,",
|
|
15
|
+
" all remaining work is blocked, or it needs your input.",
|
|
16
|
+
"",
|
|
7
17
|
"Agents: always use --toon on every command.",
|
|
8
18
|
"Aligned with: .agents/skills/trekoon/SKILL.md",
|
|
9
19
|
"",
|
|
@@ -49,6 +59,7 @@ const QUICKSTART_TEXT = [
|
|
|
49
59
|
" Bulk update: trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
50
60
|
" Ready queue: trekoon --toon task ready [--limit <n>] [--epic <id>]",
|
|
51
61
|
" Next candidate: trekoon --toon task next [--epic <id>]",
|
|
62
|
+
" Export epic to MD: trekoon --toon epic export <epic-id> [--path <path>] [--overwrite]",
|
|
52
63
|
"",
|
|
53
64
|
"6) List and view defaults",
|
|
54
65
|
" Default scope: open work (in_progress, todo), limit 10.",
|
|
@@ -142,6 +153,7 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
142
153
|
"trekoon --toon task list --status in_progress,todo --limit 20",
|
|
143
154
|
"trekoon --toon task list --cursor <n>",
|
|
144
155
|
"trekoon --toon task update --ids id1,id2 --append \"...\" --status in_progress",
|
|
156
|
+
"trekoon --toon epic export <epic-id>",
|
|
145
157
|
],
|
|
146
158
|
machineExamples: [
|
|
147
159
|
"trekoon --toon quickstart",
|
|
@@ -156,6 +168,7 @@ export async function runQuickstart(_: CliContext): Promise<CliResult> {
|
|
|
156
168
|
"trekoon --toon task ready --limit 5",
|
|
157
169
|
"trekoon --toon task next",
|
|
158
170
|
"trekoon --toon dep reverse <task-or-subtask-id>",
|
|
171
|
+
"trekoon --toon epic export <epic-id>",
|
|
159
172
|
],
|
|
160
173
|
wipeWarning: {
|
|
161
174
|
command: "trekoon wipe --yes",
|