trekoon 0.2.6 → 0.2.8
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/README.md +102 -708
- package/docs/ai-agents.md +198 -0
- package/docs/commands.md +226 -0
- package/docs/machine-contracts.md +253 -0
- package/docs/plans/2026-03-15-trekoon-board-design.md +13 -0
- package/docs/quickstart.md +207 -0
- package/package.json +3 -1
- package/src/board/assets/app.js +1498 -0
- package/src/board/assets/components/AppShell.js +17 -0
- package/src/board/assets/components/BoardTopbar.js +78 -0
- package/src/board/assets/components/ClampedText.js +31 -0
- package/src/board/assets/components/EpicRow.js +62 -0
- package/src/board/assets/components/EpicsOverview.js +43 -0
- package/src/board/assets/components/WorkspaceHeader.js +70 -0
- package/src/board/assets/components/assetMap.js +65 -0
- package/src/board/assets/index.html +76 -0
- package/src/board/assets/main.js +27 -0
- package/src/board/assets/manifest.json +12 -0
- package/src/board/assets/state/actions.js +334 -0
- package/src/board/assets/state/api.js +126 -0
- package/src/board/assets/state/store.js +172 -0
- package/src/board/assets/styles/board.css +1127 -0
- package/src/board/assets/utils/dom.js +308 -0
- package/src/board/install.ts +196 -0
- package/src/board/open-browser.ts +131 -0
- package/src/board/routes.ts +299 -0
- package/src/board/server.ts +184 -0
- package/src/board/snapshot.ts +277 -0
- package/src/board/types.ts +43 -0
- package/src/commands/board.ts +158 -0
- package/src/commands/epic.ts +104 -3
- package/src/commands/help.ts +52 -13
- package/src/commands/init.ts +29 -0
- package/src/commands/subtask.ts +78 -1
- package/src/commands/task.ts +113 -7
- package/src/domain/mutation-service.ts +116 -0
- package/src/domain/tracker-domain.ts +261 -5
- package/src/domain/types.ts +51 -0
- package/src/runtime/cli-shell.ts +5 -0
- package/src/storage/path.ts +36 -0
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type CompactTaskBatchCreateResult,
|
|
13
13
|
type CompactTaskSpec,
|
|
14
14
|
type DependencyRecord,
|
|
15
|
+
type DependencyNodeKind,
|
|
15
16
|
DomainError,
|
|
16
17
|
type EpicTreeDetailed,
|
|
17
18
|
type EpicRecord,
|
|
@@ -23,6 +24,11 @@ import {
|
|
|
23
24
|
type SearchFieldMatch,
|
|
24
25
|
type SearchNode,
|
|
25
26
|
type SearchSummary,
|
|
27
|
+
type StatusCascadeBlocker,
|
|
28
|
+
type StatusCascadeChange,
|
|
29
|
+
type StatusCascadePlan,
|
|
30
|
+
type StatusCascadeRootKind,
|
|
31
|
+
type StatusCascadeScopeNode,
|
|
26
32
|
type SubtaskRecord,
|
|
27
33
|
type TaskTreeDetailed,
|
|
28
34
|
type TaskRecord,
|
|
@@ -66,7 +72,7 @@ interface ReverseDependencyRow {
|
|
|
66
72
|
|
|
67
73
|
interface UnresolvedDependencyBlocker {
|
|
68
74
|
readonly id: string;
|
|
69
|
-
readonly kind:
|
|
75
|
+
readonly kind: DependencyNodeKind;
|
|
70
76
|
readonly status: string;
|
|
71
77
|
}
|
|
72
78
|
|
|
@@ -132,6 +138,14 @@ function normalizeStatus(value: string | undefined): string {
|
|
|
132
138
|
return assertNonEmpty("status", value);
|
|
133
139
|
}
|
|
134
140
|
|
|
141
|
+
function normalizeSubtaskDescription(value: string | undefined): string {
|
|
142
|
+
if (value === undefined) {
|
|
143
|
+
return "";
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return value.trim();
|
|
147
|
+
}
|
|
148
|
+
|
|
135
149
|
function mapEpic(row: EpicRow): EpicRecord {
|
|
136
150
|
return {
|
|
137
151
|
id: row.id,
|
|
@@ -425,7 +439,7 @@ export class TrackerDomain {
|
|
|
425
439
|
const id: string = randomUUID();
|
|
426
440
|
const taskId: string = assertNonEmpty("taskId", input.taskId);
|
|
427
441
|
const title: string = assertNonEmpty("title", input.title);
|
|
428
|
-
const description: string =
|
|
442
|
+
const description: string = normalizeSubtaskDescription(input.description);
|
|
429
443
|
const status: string = normalizeStatus(input.status);
|
|
430
444
|
|
|
431
445
|
this.getTaskOrThrow(taskId);
|
|
@@ -451,7 +465,7 @@ export class TrackerDomain {
|
|
|
451
465
|
tempKey: assertNonEmpty("tempKey", spec.tempKey),
|
|
452
466
|
taskId,
|
|
453
467
|
title: assertNonEmpty("title", spec.title),
|
|
454
|
-
description:
|
|
468
|
+
description: normalizeSubtaskDescription(spec.description),
|
|
455
469
|
status: normalizeStatus(spec.status),
|
|
456
470
|
};
|
|
457
471
|
});
|
|
@@ -568,7 +582,7 @@ export class TrackerDomain {
|
|
|
568
582
|
const existing: SubtaskRecord = this.getSubtaskOrThrow(id);
|
|
569
583
|
const nextTitle: string = input.title !== undefined ? assertNonEmpty("title", input.title) : existing.title;
|
|
570
584
|
const nextDescription: string =
|
|
571
|
-
input.description !== undefined ?
|
|
585
|
+
input.description !== undefined ? normalizeSubtaskDescription(input.description) : existing.description;
|
|
572
586
|
const nextStatus: string = input.status !== undefined ? assertNonEmpty("status", input.status) : existing.status;
|
|
573
587
|
this.assertNoUnresolvedDependenciesForStatusTransition(id, "subtask", existing.status, nextStatus);
|
|
574
588
|
const now: number = Date.now();
|
|
@@ -656,6 +670,40 @@ export class TrackerDomain {
|
|
|
656
670
|
};
|
|
657
671
|
}
|
|
658
672
|
|
|
673
|
+
planStatusCascade(rootKind: StatusCascadeRootKind, rootId: string, targetStatus: string): StatusCascadePlan {
|
|
674
|
+
const normalizedTargetStatus = assertNonEmpty("status", targetStatus);
|
|
675
|
+
const scope = this.#collectStatusCascadeScope(rootKind, rootId);
|
|
676
|
+
const scopeIdSet = new Set(scope.map((node) => node.id));
|
|
677
|
+
const orderedChanges = this.#orderStatusCascadeChanges(scope, normalizedTargetStatus);
|
|
678
|
+
const changedIds = orderedChanges.map((change) => change.id);
|
|
679
|
+
const changedIdSet = new Set(changedIds);
|
|
680
|
+
const unchangedIds = scope
|
|
681
|
+
.filter((node) => !changedIdSet.has(node.id))
|
|
682
|
+
.map((node) => node.id);
|
|
683
|
+
const blockers = this.#collectStatusCascadeBlockers(orderedChanges, scopeIdSet, changedIdSet, normalizedTargetStatus);
|
|
684
|
+
|
|
685
|
+
return {
|
|
686
|
+
rootKind,
|
|
687
|
+
rootId,
|
|
688
|
+
targetStatus: normalizedTargetStatus,
|
|
689
|
+
atomic: true,
|
|
690
|
+
scope,
|
|
691
|
+
orderedChanges,
|
|
692
|
+
changedIds,
|
|
693
|
+
unchangedIds,
|
|
694
|
+
blockers,
|
|
695
|
+
counts: {
|
|
696
|
+
scope: scope.length,
|
|
697
|
+
changed: orderedChanges.length,
|
|
698
|
+
unchanged: unchangedIds.length,
|
|
699
|
+
blockers: blockers.length,
|
|
700
|
+
changedEpics: orderedChanges.filter((change) => change.kind === "epic").length,
|
|
701
|
+
changedTasks: orderedChanges.filter((change) => change.kind === "task").length,
|
|
702
|
+
changedSubtasks: orderedChanges.filter((change) => change.kind === "subtask").length,
|
|
703
|
+
},
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
|
|
659
707
|
collectEpicSearchScope(epicId: string): readonly SearchNode[] {
|
|
660
708
|
const tree = this.buildEpicTreeDetailed(epicId);
|
|
661
709
|
|
|
@@ -1266,9 +1314,217 @@ export class TrackerDomain {
|
|
|
1266
1314
|
return row !== null;
|
|
1267
1315
|
}
|
|
1268
1316
|
|
|
1317
|
+
#collectStatusCascadeScope(rootKind: StatusCascadeRootKind, rootId: string): StatusCascadeScopeNode[] {
|
|
1318
|
+
if (rootKind === "task") {
|
|
1319
|
+
const tree = this.buildTaskTreeDetailed(rootId);
|
|
1320
|
+
return [
|
|
1321
|
+
{
|
|
1322
|
+
kind: "task",
|
|
1323
|
+
id: tree.id,
|
|
1324
|
+
parentId: tree.epicId,
|
|
1325
|
+
status: tree.status,
|
|
1326
|
+
},
|
|
1327
|
+
...tree.subtasks.map((subtask) => ({
|
|
1328
|
+
kind: "subtask" as const,
|
|
1329
|
+
id: subtask.id,
|
|
1330
|
+
parentId: subtask.taskId,
|
|
1331
|
+
status: subtask.status,
|
|
1332
|
+
})),
|
|
1333
|
+
];
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const tree = this.buildEpicTreeDetailed(rootId);
|
|
1337
|
+
return [
|
|
1338
|
+
{
|
|
1339
|
+
kind: "epic",
|
|
1340
|
+
id: tree.id,
|
|
1341
|
+
status: tree.status,
|
|
1342
|
+
},
|
|
1343
|
+
...tree.tasks.flatMap((task) => [
|
|
1344
|
+
{
|
|
1345
|
+
kind: "task" as const,
|
|
1346
|
+
id: task.id,
|
|
1347
|
+
parentId: task.epicId,
|
|
1348
|
+
status: task.status,
|
|
1349
|
+
},
|
|
1350
|
+
...task.subtasks.map((subtask) => ({
|
|
1351
|
+
kind: "subtask" as const,
|
|
1352
|
+
id: subtask.id,
|
|
1353
|
+
parentId: subtask.taskId,
|
|
1354
|
+
status: subtask.status,
|
|
1355
|
+
})),
|
|
1356
|
+
]),
|
|
1357
|
+
];
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
#orderStatusCascadeChanges(scope: readonly StatusCascadeScopeNode[], targetStatus: string): StatusCascadeChange[] {
|
|
1361
|
+
const changes = scope
|
|
1362
|
+
.filter((node) => node.status !== targetStatus)
|
|
1363
|
+
.map((node) => {
|
|
1364
|
+
const change: StatusCascadeChange = {
|
|
1365
|
+
kind: node.kind,
|
|
1366
|
+
id: node.id,
|
|
1367
|
+
previousStatus: node.status,
|
|
1368
|
+
nextStatus: targetStatus,
|
|
1369
|
+
...(node.parentId === undefined ? {} : { parentId: node.parentId }),
|
|
1370
|
+
};
|
|
1371
|
+
return change;
|
|
1372
|
+
});
|
|
1373
|
+
|
|
1374
|
+
if (targetStatus !== "done") {
|
|
1375
|
+
return changes;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
return this.#topologicallyOrderDoneCascadeChanges(changes);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
#topologicallyOrderDoneCascadeChanges(changes: readonly StatusCascadeChange[]): StatusCascadeChange[] {
|
|
1382
|
+
const indexById = new Map<string, number>();
|
|
1383
|
+
const changeById = new Map<string, StatusCascadeChange>();
|
|
1384
|
+
const dependencyTargetsBySource = new Map<string, Set<string>>();
|
|
1385
|
+
const dependents = new Map<string, Set<string>>();
|
|
1386
|
+
const indegree = new Map<string, number>();
|
|
1387
|
+
|
|
1388
|
+
changes.forEach((change, index) => {
|
|
1389
|
+
indexById.set(change.id, index);
|
|
1390
|
+
changeById.set(change.id, change);
|
|
1391
|
+
indegree.set(change.id, 0);
|
|
1392
|
+
|
|
1393
|
+
if (change.kind !== "task" && change.kind !== "subtask") {
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
const dependencyTargets = new Set(this.listDependencies(change.id).map((dependency) => dependency.dependsOnId));
|
|
1398
|
+
dependencyTargetsBySource.set(change.id, dependencyTargets);
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
const addEdge = (fromId: string, toId: string): void => {
|
|
1402
|
+
if (fromId === toId || !changeById.has(fromId) || !changeById.has(toId)) {
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
const neighbors = dependents.get(fromId) ?? new Set<string>();
|
|
1407
|
+
if (neighbors.has(toId)) {
|
|
1408
|
+
return;
|
|
1409
|
+
}
|
|
1410
|
+
|
|
1411
|
+
neighbors.add(toId);
|
|
1412
|
+
dependents.set(fromId, neighbors);
|
|
1413
|
+
indegree.set(toId, (indegree.get(toId) ?? 0) + 1);
|
|
1414
|
+
};
|
|
1415
|
+
|
|
1416
|
+
for (const change of changes) {
|
|
1417
|
+
const dependencyTargets = dependencyTargetsBySource.get(change.id);
|
|
1418
|
+
|
|
1419
|
+
if (change.kind === "subtask" && change.parentId !== undefined && !dependencyTargets?.has(change.parentId)) {
|
|
1420
|
+
addEdge(change.id, change.parentId);
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
if (change.kind === "task" && change.parentId !== undefined && !dependencyTargets?.has(change.parentId)) {
|
|
1424
|
+
addEdge(change.id, change.parentId);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
if (change.kind !== "task" && change.kind !== "subtask") {
|
|
1428
|
+
continue;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
for (const dependencyTargetId of dependencyTargets ?? []) {
|
|
1432
|
+
addEdge(dependencyTargetId, change.id);
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
const ordered: StatusCascadeChange[] = [];
|
|
1437
|
+
const ready = changes
|
|
1438
|
+
.filter((change) => (indegree.get(change.id) ?? 0) === 0)
|
|
1439
|
+
.sort((left, right) => (indexById.get(left.id) ?? 0) - (indexById.get(right.id) ?? 0));
|
|
1440
|
+
|
|
1441
|
+
while (ready.length > 0) {
|
|
1442
|
+
const next = ready.shift();
|
|
1443
|
+
if (next === undefined) {
|
|
1444
|
+
continue;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
ordered.push(next);
|
|
1448
|
+
for (const dependentId of dependents.get(next.id) ?? []) {
|
|
1449
|
+
const remaining = (indegree.get(dependentId) ?? 0) - 1;
|
|
1450
|
+
indegree.set(dependentId, remaining);
|
|
1451
|
+
if (remaining !== 0) {
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const dependent = changeById.get(dependentId);
|
|
1456
|
+
if (dependent === undefined) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
ready.push(dependent);
|
|
1461
|
+
ready.sort((left, right) => (indexById.get(left.id) ?? 0) - (indexById.get(right.id) ?? 0));
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
if (ordered.length !== changes.length) {
|
|
1466
|
+
throw new DomainError({
|
|
1467
|
+
code: "invalid_dependency",
|
|
1468
|
+
message: "unable to determine dependency-safe cascade order",
|
|
1469
|
+
details: {
|
|
1470
|
+
changedIds: changes.map((change) => change.id),
|
|
1471
|
+
},
|
|
1472
|
+
});
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
return ordered;
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
#collectStatusCascadeBlockers(
|
|
1479
|
+
changes: readonly StatusCascadeChange[],
|
|
1480
|
+
scopeIdSet: ReadonlySet<string>,
|
|
1481
|
+
changedIdSet: ReadonlySet<string>,
|
|
1482
|
+
targetStatus: string,
|
|
1483
|
+
): StatusCascadeBlocker[] {
|
|
1484
|
+
if (!DEPENDENCY_GATED_STATUSES.has(targetStatus)) {
|
|
1485
|
+
return [];
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const blockers: StatusCascadeBlocker[] = [];
|
|
1489
|
+
for (const change of changes) {
|
|
1490
|
+
if (change.kind !== "task" && change.kind !== "subtask") {
|
|
1491
|
+
continue;
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
for (const dependency of this.listDependencies(change.id)) {
|
|
1495
|
+
const dependencyStatus =
|
|
1496
|
+
dependency.dependsOnKind === "task"
|
|
1497
|
+
? this.getTaskOrThrow(dependency.dependsOnId).status
|
|
1498
|
+
: this.getSubtaskOrThrow(dependency.dependsOnId).status;
|
|
1499
|
+
const inScope = scopeIdSet.has(dependency.dependsOnId);
|
|
1500
|
+
const willCascade = targetStatus === "done" && changedIdSet.has(dependency.dependsOnId);
|
|
1501
|
+
if (dependencyStatus === "done" || willCascade) {
|
|
1502
|
+
continue;
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
blockers.push({
|
|
1506
|
+
sourceId: dependency.sourceId,
|
|
1507
|
+
sourceKind: dependency.sourceKind,
|
|
1508
|
+
dependsOnId: dependency.dependsOnId,
|
|
1509
|
+
dependsOnKind: dependency.dependsOnKind,
|
|
1510
|
+
dependsOnStatus: dependencyStatus,
|
|
1511
|
+
inScope,
|
|
1512
|
+
willCascade,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
|
|
1517
|
+
return blockers.sort(
|
|
1518
|
+
(left, right) =>
|
|
1519
|
+
left.sourceId.localeCompare(right.sourceId) ||
|
|
1520
|
+
left.dependsOnId.localeCompare(right.dependsOnId) ||
|
|
1521
|
+
left.dependsOnKind.localeCompare(right.dependsOnKind),
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1269
1525
|
private assertNoUnresolvedDependenciesForStatusTransition(
|
|
1270
1526
|
id: string,
|
|
1271
|
-
kind:
|
|
1527
|
+
kind: DependencyNodeKind,
|
|
1272
1528
|
existingStatus: string,
|
|
1273
1529
|
nextStatus: string,
|
|
1274
1530
|
): void {
|
package/src/domain/types.ts
CHANGED
|
@@ -192,6 +192,57 @@ export interface SearchNode {
|
|
|
192
192
|
readonly description: string;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
export type StatusCascadeRootKind = Extract<NodeKind, "epic" | "task">;
|
|
196
|
+
export type DependencyNodeKind = Extract<NodeKind, "task" | "subtask">;
|
|
197
|
+
|
|
198
|
+
export interface StatusCascadeScopeNode {
|
|
199
|
+
readonly kind: NodeKind;
|
|
200
|
+
readonly id: string;
|
|
201
|
+
readonly parentId?: string;
|
|
202
|
+
readonly status: string;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface StatusCascadeChange {
|
|
206
|
+
readonly kind: NodeKind;
|
|
207
|
+
readonly id: string;
|
|
208
|
+
readonly parentId?: string;
|
|
209
|
+
readonly previousStatus: string;
|
|
210
|
+
readonly nextStatus: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export interface StatusCascadeBlocker {
|
|
214
|
+
readonly sourceId: string;
|
|
215
|
+
readonly sourceKind: DependencyNodeKind;
|
|
216
|
+
readonly dependsOnId: string;
|
|
217
|
+
readonly dependsOnKind: DependencyNodeKind;
|
|
218
|
+
readonly dependsOnStatus: string;
|
|
219
|
+
readonly inScope: boolean;
|
|
220
|
+
readonly willCascade: boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface StatusCascadeCounts {
|
|
224
|
+
readonly scope: number;
|
|
225
|
+
readonly changed: number;
|
|
226
|
+
readonly unchanged: number;
|
|
227
|
+
readonly blockers: number;
|
|
228
|
+
readonly changedEpics: number;
|
|
229
|
+
readonly changedTasks: number;
|
|
230
|
+
readonly changedSubtasks: number;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export interface StatusCascadePlan {
|
|
234
|
+
readonly rootKind: StatusCascadeRootKind;
|
|
235
|
+
readonly rootId: string;
|
|
236
|
+
readonly targetStatus: string;
|
|
237
|
+
readonly atomic: true;
|
|
238
|
+
readonly scope: ReadonlyArray<StatusCascadeScopeNode>;
|
|
239
|
+
readonly orderedChanges: ReadonlyArray<StatusCascadeChange>;
|
|
240
|
+
readonly changedIds: ReadonlyArray<string>;
|
|
241
|
+
readonly unchangedIds: ReadonlyArray<string>;
|
|
242
|
+
readonly blockers: ReadonlyArray<StatusCascadeBlocker>;
|
|
243
|
+
readonly counts: StatusCascadeCounts;
|
|
244
|
+
}
|
|
245
|
+
|
|
195
246
|
export interface DomainErrorShape {
|
|
196
247
|
readonly code: string;
|
|
197
248
|
readonly message: string;
|
package/src/runtime/cli-shell.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { runBoard } from "../commands/board";
|
|
1
2
|
import { runHelp } from "../commands/help";
|
|
2
3
|
import { runDep } from "../commands/dep";
|
|
3
4
|
import { runEpic } from "../commands/epic";
|
|
@@ -19,6 +20,7 @@ import { resolveStoragePaths } from "../storage/path";
|
|
|
19
20
|
|
|
20
21
|
const SUPPORTED_ROOT_COMMANDS: readonly string[] = [
|
|
21
22
|
"help",
|
|
23
|
+
"board",
|
|
22
24
|
"init",
|
|
23
25
|
"quickstart",
|
|
24
26
|
"session",
|
|
@@ -334,6 +336,9 @@ export async function executeShell(parsed: ParsedInvocation, cwd: string = proce
|
|
|
334
336
|
case "help":
|
|
335
337
|
result = await runHelp(context);
|
|
336
338
|
break;
|
|
339
|
+
case "board":
|
|
340
|
+
result = await runBoard(context);
|
|
341
|
+
break;
|
|
337
342
|
case "init":
|
|
338
343
|
result = await runInit(context);
|
|
339
344
|
break;
|
package/src/storage/path.ts
CHANGED
|
@@ -4,6 +4,9 @@ import { resolve } from "node:path";
|
|
|
4
4
|
|
|
5
5
|
export const TREKOON_STORAGE_DIRNAME = ".trekoon";
|
|
6
6
|
export const TREKOON_DATABASE_FILENAME = "trekoon.db";
|
|
7
|
+
export const TREKOON_BOARD_DIRNAME = "board";
|
|
8
|
+
export const TREKOON_BOARD_ENTRY_FILENAME = "index.html";
|
|
9
|
+
export const TREKOON_BOARD_MANIFEST_FILENAME = "manifest.json";
|
|
7
10
|
|
|
8
11
|
export function resolveLegacyWorktreeStorageDir(worktreeRoot: string): string {
|
|
9
12
|
return resolve(worktreeRoot, TREKOON_STORAGE_DIRNAME);
|
|
@@ -15,6 +18,18 @@ export function resolveLegacyWorktreeDatabaseFile(worktreeRoot: string): string
|
|
|
15
18
|
|
|
16
19
|
export type StorageMode = "cwd" | "git_common_dir";
|
|
17
20
|
|
|
21
|
+
export function resolveBoardStorageDir(storageDir: string): string {
|
|
22
|
+
return resolve(storageDir, TREKOON_BOARD_DIRNAME);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function resolveBoardEntryFile(storageDir: string): string {
|
|
26
|
+
return resolve(resolveBoardStorageDir(storageDir), TREKOON_BOARD_ENTRY_FILENAME);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveBoardManifestFile(storageDir: string): string {
|
|
30
|
+
return resolve(resolveBoardStorageDir(storageDir), TREKOON_BOARD_MANIFEST_FILENAME);
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
export interface StoragePaths {
|
|
19
34
|
readonly invocationCwd: string;
|
|
20
35
|
readonly storageMode: StorageMode;
|
|
@@ -23,6 +38,9 @@ export interface StoragePaths {
|
|
|
23
38
|
readonly sharedStorageRoot: string;
|
|
24
39
|
readonly storageDir: string;
|
|
25
40
|
readonly databaseFile: string;
|
|
41
|
+
readonly boardDir: string;
|
|
42
|
+
readonly boardEntryFile: string;
|
|
43
|
+
readonly boardManifestFile: string;
|
|
26
44
|
readonly diagnostics: StoragePathDiagnostics;
|
|
27
45
|
}
|
|
28
46
|
|
|
@@ -35,6 +53,9 @@ export interface StoragePathIssue {
|
|
|
35
53
|
readonly worktreeRoot: string;
|
|
36
54
|
readonly sharedStorageRoot: string;
|
|
37
55
|
readonly databaseFile: string;
|
|
56
|
+
readonly boardDir: string;
|
|
57
|
+
readonly boardEntryFile: string;
|
|
58
|
+
readonly boardManifestFile: string;
|
|
38
59
|
}
|
|
39
60
|
|
|
40
61
|
export interface StoragePathDiagnostics {
|
|
@@ -44,6 +65,9 @@ export interface StoragePathDiagnostics {
|
|
|
44
65
|
readonly worktreeRoot: string;
|
|
45
66
|
readonly sharedStorageRoot: string;
|
|
46
67
|
readonly databaseFile: string;
|
|
68
|
+
readonly boardDir: string;
|
|
69
|
+
readonly boardEntryFile: string;
|
|
70
|
+
readonly boardManifestFile: string;
|
|
47
71
|
readonly warnings: readonly StoragePathIssue[];
|
|
48
72
|
readonly errors: readonly StoragePathIssue[];
|
|
49
73
|
}
|
|
@@ -76,6 +100,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
|
|
|
76
100
|
const sharedStorageRoot: string = repoCommonDir ? realpathSync(resolve(repoCommonDir, "..")) : invocationCwd;
|
|
77
101
|
const storageDir: string = resolve(sharedStorageRoot, TREKOON_STORAGE_DIRNAME);
|
|
78
102
|
const databaseFile: string = resolve(storageDir, TREKOON_DATABASE_FILENAME);
|
|
103
|
+
const boardDir: string = resolveBoardStorageDir(storageDir);
|
|
104
|
+
const boardEntryFile: string = resolveBoardEntryFile(storageDir);
|
|
105
|
+
const boardManifestFile: string = resolveBoardManifestFile(storageDir);
|
|
79
106
|
const warnings: StoragePathIssue[] = [];
|
|
80
107
|
|
|
81
108
|
const createIssue = (code: string, message: string): StoragePathIssue => ({
|
|
@@ -87,6 +114,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
|
|
|
87
114
|
worktreeRoot,
|
|
88
115
|
sharedStorageRoot,
|
|
89
116
|
databaseFile,
|
|
117
|
+
boardDir,
|
|
118
|
+
boardEntryFile,
|
|
119
|
+
boardManifestFile,
|
|
90
120
|
});
|
|
91
121
|
|
|
92
122
|
if (invocationCwd !== worktreeRoot) {
|
|
@@ -111,6 +141,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
|
|
|
111
141
|
worktreeRoot,
|
|
112
142
|
sharedStorageRoot,
|
|
113
143
|
databaseFile,
|
|
144
|
+
boardDir,
|
|
145
|
+
boardEntryFile,
|
|
146
|
+
boardManifestFile,
|
|
114
147
|
warnings,
|
|
115
148
|
errors: [],
|
|
116
149
|
};
|
|
@@ -123,6 +156,9 @@ export function resolveStoragePaths(workingDirectory: string = process.cwd()): S
|
|
|
123
156
|
sharedStorageRoot,
|
|
124
157
|
storageDir,
|
|
125
158
|
databaseFile,
|
|
159
|
+
boardDir,
|
|
160
|
+
boardEntryFile,
|
|
161
|
+
boardManifestFile,
|
|
126
162
|
diagnostics,
|
|
127
163
|
};
|
|
128
164
|
}
|