trekoon 0.2.7 → 0.2.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.
Files changed (45) hide show
  1. package/README.md +60 -0
  2. package/docs/commands.md +100 -0
  3. package/docs/quickstart.md +74 -1
  4. package/package.json +2 -1
  5. package/src/board/assets/app.js +589 -0
  6. package/src/board/assets/components/ClampedText.js +31 -0
  7. package/src/board/assets/components/Component.js +271 -0
  8. package/src/board/assets/components/ConfirmDialog.js +81 -0
  9. package/src/board/assets/components/EpicRow.js +64 -0
  10. package/src/board/assets/components/EpicsOverview.js +80 -0
  11. package/src/board/assets/components/Inspector.js +335 -0
  12. package/src/board/assets/components/Notice.js +80 -0
  13. package/src/board/assets/components/SubtaskModal.js +100 -0
  14. package/src/board/assets/components/TaskCard.js +82 -0
  15. package/src/board/assets/components/TaskModal.js +99 -0
  16. package/src/board/assets/components/TopBar.js +167 -0
  17. package/src/board/assets/components/Workspace.js +308 -0
  18. package/src/board/assets/components/assetMap.js +80 -0
  19. package/src/board/assets/components/helpers.js +244 -0
  20. package/src/board/assets/fonts/inter-latin.woff2 +0 -0
  21. package/src/board/assets/fonts/material-symbols-rounded.woff2 +0 -0
  22. package/src/board/assets/index.html +39 -0
  23. package/src/board/assets/main.js +11 -0
  24. package/src/board/assets/manifest.json +12 -0
  25. package/src/board/assets/runtime/delegation.js +309 -0
  26. package/src/board/assets/state/actions.js +454 -0
  27. package/src/board/assets/state/api.js +281 -0
  28. package/src/board/assets/state/store.js +472 -0
  29. package/src/board/assets/state/url.js +184 -0
  30. package/src/board/assets/state/utils.js +222 -0
  31. package/src/board/assets/styles/board.css +1811 -0
  32. package/src/board/assets/styles/fonts.css +22 -0
  33. package/src/board/install.ts +196 -0
  34. package/src/board/open-browser.ts +131 -0
  35. package/src/board/routes.ts +308 -0
  36. package/src/board/server.ts +185 -0
  37. package/src/board/snapshot.ts +277 -0
  38. package/src/board/types.ts +43 -0
  39. package/src/commands/board.ts +158 -0
  40. package/src/commands/help.ts +21 -0
  41. package/src/commands/init.ts +29 -0
  42. package/src/domain/mutation-service.ts +40 -0
  43. package/src/domain/tracker-domain.ts +11 -3
  44. package/src/runtime/cli-shell.ts +5 -0
  45. package/src/storage/path.ts +36 -0
@@ -0,0 +1,222 @@
1
+ /** @type {readonly string[]} */
2
+ export const STATUS_ORDER = ["todo", "blocked", "in_progress", "done"];
3
+
4
+ /** @type {readonly string[]} */
5
+ export const VIEW_MODES = ["kanban", "list"];
6
+
7
+ const VALID_STATUSES = new Set(STATUS_ORDER);
8
+
9
+ /**
10
+ * Normalize a raw status string into one of the canonical status values.
11
+ * @param {string} rawStatus
12
+ * @returns {"todo"|"blocked"|"in_progress"|"done"}
13
+ */
14
+ export function normalizeStatus(rawStatus) {
15
+ if (rawStatus === "in-progress") return "in_progress";
16
+ if (VALID_STATUSES.has(rawStatus)) return rawStatus;
17
+ return "todo";
18
+ }
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {any[]}
23
+ */
24
+ function normalizeArray(value) {
25
+ return Array.isArray(value) ? value : [];
26
+ }
27
+
28
+ /**
29
+ * @param {{ id?: string }} record
30
+ * @returns {string}
31
+ */
32
+ function getId(record) {
33
+ return typeof record?.id === "string" && record.id.length > 0 ? record.id : crypto.randomUUID();
34
+ }
35
+
36
+ /**
37
+ * @param {any[]} tasks
38
+ * @returns {Record<string, number>}
39
+ */
40
+ function deriveCounts(tasks) {
41
+ return STATUS_ORDER.reduce((counts, status) => {
42
+ counts[status] = tasks.filter((task) => task.status === status).length;
43
+ return counts;
44
+ }, {});
45
+ }
46
+
47
+ /**
48
+ * Normalize a raw board snapshot into the canonical shape with search text,
49
+ * dependency cross-references, and epic counts.
50
+ * @param {object} rawSnapshot
51
+ * @returns {object}
52
+ */
53
+ export function normalizeSnapshot(rawSnapshot) {
54
+ const rawEpics = normalizeArray(rawSnapshot?.epics);
55
+ const rawTasks = normalizeArray(rawSnapshot?.tasks);
56
+ const rawSubtasks = normalizeArray(rawSnapshot?.subtasks);
57
+ const rawDependencies = normalizeArray(rawSnapshot?.dependencies);
58
+ const taskIndex = new Map();
59
+ const subtaskIndex = new Map();
60
+
61
+ const tasks = rawTasks.map((task) => {
62
+ const normalizedTask = {
63
+ id: getId(task),
64
+ kind: "task",
65
+ epicId: task.epicId ?? task.epic?.id ?? null,
66
+ title: String(task.title ?? "Untitled task"),
67
+ description: String(task.description ?? "").replace(/\\n/g, "\n"),
68
+ status: normalizeStatus(task.status),
69
+ createdAt: Number(task.createdAt ?? Date.now()),
70
+ updatedAt: Number(task.updatedAt ?? task.createdAt ?? Date.now()),
71
+ blockedBy: [],
72
+ blocks: [],
73
+ dependencyIds: [],
74
+ dependentIds: [],
75
+ subtasks: [],
76
+ searchText: "",
77
+ };
78
+
79
+ taskIndex.set(normalizedTask.id, normalizedTask);
80
+ return normalizedTask;
81
+ });
82
+
83
+ const subtasks = rawSubtasks.map((subtask) => {
84
+ const normalizedSubtask = {
85
+ id: getId(subtask),
86
+ kind: "subtask",
87
+ taskId: subtask.taskId ?? subtask.task?.id ?? null,
88
+ title: String(subtask.title ?? "Untitled subtask"),
89
+ description: String(subtask.description ?? "").replace(/\\n/g, "\n"),
90
+ status: normalizeStatus(subtask.status),
91
+ createdAt: Number(subtask.createdAt ?? Date.now()),
92
+ updatedAt: Number(subtask.updatedAt ?? subtask.createdAt ?? Date.now()),
93
+ blockedBy: [],
94
+ blocks: [],
95
+ dependencyIds: [],
96
+ dependentIds: [],
97
+ searchText: "",
98
+ };
99
+
100
+ subtaskIndex.set(normalizedSubtask.id, normalizedSubtask);
101
+ return normalizedSubtask;
102
+ });
103
+
104
+ for (const subtask of subtasks) {
105
+ const parentTask = taskIndex.get(subtask.taskId);
106
+ if (parentTask) {
107
+ parentTask.subtasks.push(subtask);
108
+ }
109
+ }
110
+
111
+ const dependencies = rawDependencies.map((dependency) => ({
112
+ id: getId(dependency),
113
+ sourceId: String(dependency.sourceId ?? ""),
114
+ sourceKind: dependency.sourceKind === "subtask" ? "subtask" : "task",
115
+ dependsOnId: String(dependency.dependsOnId ?? ""),
116
+ dependsOnKind: dependency.dependsOnKind === "subtask" ? "subtask" : "task",
117
+ }));
118
+
119
+ const lookupNode = (kind, id) => {
120
+ if (kind === "subtask") {
121
+ return subtaskIndex.get(id) ?? null;
122
+ }
123
+ return taskIndex.get(id) ?? null;
124
+ };
125
+
126
+ for (const dependency of dependencies) {
127
+ const source = lookupNode(dependency.sourceKind, dependency.sourceId);
128
+ const target = lookupNode(dependency.dependsOnKind, dependency.dependsOnId);
129
+ if (source) {
130
+ source.blockedBy.push(dependency.dependsOnId);
131
+ source.dependencyIds.push(dependency.id);
132
+ }
133
+ if (target) {
134
+ target.blocks.push(dependency.sourceId);
135
+ target.dependentIds.push(dependency.id);
136
+ }
137
+ }
138
+
139
+ const epics = rawEpics.map((epic) => {
140
+ const epicId = getId(epic);
141
+ const epicTasks = tasks.filter((task) => task.epicId === epicId);
142
+ const normalizedEpic = {
143
+ id: epicId,
144
+ title: String(epic.title ?? "Untitled epic"),
145
+ description: String(epic.description ?? "").replace(/\\n/g, "\n"),
146
+ status: String(epic.status ?? "todo"),
147
+ createdAt: Number(epic.createdAt ?? Date.now()),
148
+ updatedAt: Number(epic.updatedAt ?? epic.createdAt ?? Date.now()),
149
+ taskIds: epicTasks.map((task) => task.id),
150
+ counts: deriveCounts(epicTasks),
151
+ searchText: "",
152
+ };
153
+
154
+ normalizedEpic.searchText = [normalizedEpic.title, normalizedEpic.description, ...epicTasks.map((task) => task.title)].join(" ").toLowerCase();
155
+ return normalizedEpic;
156
+ });
157
+
158
+ for (const subtask of subtasks) {
159
+ subtask.searchText = [subtask.title, subtask.description, subtask.status].join(" ").toLowerCase();
160
+ }
161
+
162
+ for (const task of tasks) {
163
+ task.searchText = [
164
+ task.title,
165
+ task.description,
166
+ task.status,
167
+ ...task.subtasks.map((subtask) => `${subtask.title} ${subtask.description} ${subtask.status}`),
168
+ ].join(" ").toLowerCase();
169
+ }
170
+
171
+ const taskSearchTextByEpicId = new Map();
172
+ for (const task of tasks) {
173
+ if (!task.epicId) {
174
+ continue;
175
+ }
176
+
177
+ const entries = taskSearchTextByEpicId.get(task.epicId) ?? [];
178
+ entries.push(task.searchText);
179
+ taskSearchTextByEpicId.set(task.epicId, entries);
180
+ }
181
+
182
+ return {
183
+ generatedAt: rawSnapshot?.generatedAt ?? null,
184
+ epics: epics.map((epic) => ({
185
+ ...epic,
186
+ searchText: [
187
+ epic.title,
188
+ epic.description,
189
+ ...(taskSearchTextByEpicId.get(epic.id) ?? []),
190
+ ].join(" ").toLowerCase(),
191
+ })),
192
+ tasks,
193
+ subtasks,
194
+ dependencies,
195
+ };
196
+ }
197
+
198
+ const dateFormatter = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short" });
199
+
200
+ /**
201
+ * Format a timestamp into a human-readable date string.
202
+ * Uses a cached Intl.DateTimeFormat instance for performance.
203
+ * @param {number|null|undefined} timestamp
204
+ * @returns {string}
205
+ */
206
+ export function formatDate(timestamp) {
207
+ if (!timestamp) return "Unknown";
208
+ return dateFormatter.format(timestamp);
209
+ }
210
+
211
+ /**
212
+ * Escape HTML special characters to prevent XSS.
213
+ * @param {string} value
214
+ * @returns {string}
215
+ */
216
+ export function escapeHtml(value) {
217
+ return String(value)
218
+ .replaceAll("&", "&amp;")
219
+ .replaceAll("<", "&lt;")
220
+ .replaceAll(">", "&gt;")
221
+ .replaceAll('"', "&quot;");
222
+ }