@ronkovic/aad 0.3.0

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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +312 -0
  3. package/bin/aad.js +2 -0
  4. package/package.json +78 -0
  5. package/src/__tests__/e2e/pipeline-e2e.test.ts +279 -0
  6. package/src/__tests__/e2e/resume-e2e.test.ts +200 -0
  7. package/src/__tests__/integration/cli-smoke.test.ts +175 -0
  8. package/src/__tests__/integration/pipeline.test.ts +346 -0
  9. package/src/bun-imports.d.ts +14 -0
  10. package/src/main.ts +52 -0
  11. package/src/modules/claude-provider/__tests__/claude-cli.adapter.test.ts +277 -0
  12. package/src/modules/claude-provider/__tests__/claude-sdk-real-env.test.ts +127 -0
  13. package/src/modules/claude-provider/__tests__/claude-sdk.adapter.test.ts +347 -0
  14. package/src/modules/claude-provider/__tests__/effort-strategy.test.ts +212 -0
  15. package/src/modules/claude-provider/__tests__/provider-registry.test.ts +251 -0
  16. package/src/modules/claude-provider/__tests__/retry.test.ts +201 -0
  17. package/src/modules/claude-provider/claude-cli.adapter.ts +156 -0
  18. package/src/modules/claude-provider/claude-provider.port.ts +35 -0
  19. package/src/modules/claude-provider/claude-sdk.adapter.ts +217 -0
  20. package/src/modules/claude-provider/effort-strategy.ts +94 -0
  21. package/src/modules/claude-provider/index.ts +32 -0
  22. package/src/modules/claude-provider/provider-registry.ts +92 -0
  23. package/src/modules/claude-provider/retry.ts +81 -0
  24. package/src/modules/cli/__tests__/app.test.ts +160 -0
  25. package/src/modules/cli/__tests__/cleanup.test.ts +111 -0
  26. package/src/modules/cli/__tests__/commands.test.ts +186 -0
  27. package/src/modules/cli/__tests__/output.test.ts +329 -0
  28. package/src/modules/cli/__tests__/resume.test.ts +324 -0
  29. package/src/modules/cli/__tests__/run.test.ts +168 -0
  30. package/src/modules/cli/__tests__/shutdown.test.ts +168 -0
  31. package/src/modules/cli/__tests__/status.test.ts +144 -0
  32. package/src/modules/cli/app.ts +241 -0
  33. package/src/modules/cli/commands/cleanup.ts +120 -0
  34. package/src/modules/cli/commands/resume.ts +156 -0
  35. package/src/modules/cli/commands/run.ts +322 -0
  36. package/src/modules/cli/commands/status.ts +101 -0
  37. package/src/modules/cli/index.ts +29 -0
  38. package/src/modules/cli/output.ts +256 -0
  39. package/src/modules/cli/shutdown.ts +122 -0
  40. package/src/modules/dashboard/__tests__/api-routes.test.ts +204 -0
  41. package/src/modules/dashboard/__tests__/file-watcher.test.ts +34 -0
  42. package/src/modules/dashboard/__tests__/server.test.ts +120 -0
  43. package/src/modules/dashboard/__tests__/sse-broadcaster.test.ts +163 -0
  44. package/src/modules/dashboard/__tests__/sse-routes.test.ts +58 -0
  45. package/src/modules/dashboard/__tests__/state-aggregator.test.ts +330 -0
  46. package/src/modules/dashboard/index.ts +8 -0
  47. package/src/modules/dashboard/routes/api.ts +84 -0
  48. package/src/modules/dashboard/routes/sse.ts +37 -0
  49. package/src/modules/dashboard/server.ts +111 -0
  50. package/src/modules/dashboard/services/file-watcher.ts +36 -0
  51. package/src/modules/dashboard/services/sse-broadcaster.ts +81 -0
  52. package/src/modules/dashboard/services/state-aggregator.ts +132 -0
  53. package/src/modules/dashboard/ui/dashboard.html +405 -0
  54. package/src/modules/git-workspace/__tests__/branch-manager.test.ts +335 -0
  55. package/src/modules/git-workspace/__tests__/git-exec.test.ts +91 -0
  56. package/src/modules/git-workspace/__tests__/memory-sync.test.ts +273 -0
  57. package/src/modules/git-workspace/__tests__/merge-service.test.ts +286 -0
  58. package/src/modules/git-workspace/__tests__/settings-merge.test.ts +163 -0
  59. package/src/modules/git-workspace/__tests__/worktree-manager.test.ts +247 -0
  60. package/src/modules/git-workspace/branch-manager.ts +191 -0
  61. package/src/modules/git-workspace/git-exec.ts +124 -0
  62. package/src/modules/git-workspace/index.ts +17 -0
  63. package/src/modules/git-workspace/memory-sync.ts +89 -0
  64. package/src/modules/git-workspace/merge-service.ts +156 -0
  65. package/src/modules/git-workspace/settings-merge.ts +95 -0
  66. package/src/modules/git-workspace/worktree-manager.ts +199 -0
  67. package/src/modules/logging/__tests__/log-store.test.ts +242 -0
  68. package/src/modules/logging/__tests__/logger.test.ts +81 -0
  69. package/src/modules/logging/__tests__/sse-transport.test.ts +93 -0
  70. package/src/modules/logging/index.ts +7 -0
  71. package/src/modules/logging/log-store.ts +80 -0
  72. package/src/modules/logging/logger.ts +55 -0
  73. package/src/modules/logging/transports/sse-transport.ts +28 -0
  74. package/src/modules/multi-repo/__tests__/multi-repo-planner.test.ts +93 -0
  75. package/src/modules/multi-repo/__tests__/repo-context.test.ts +79 -0
  76. package/src/modules/multi-repo/index.ts +12 -0
  77. package/src/modules/multi-repo/multi-repo-planner.ts +112 -0
  78. package/src/modules/multi-repo/repo-context.ts +71 -0
  79. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/progress.json +10 -0
  80. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/completed/task-getall-2.json +10 -0
  81. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-1.json +13 -0
  82. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-getall-1.json +10 -0
  83. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/queue/pending/task-status-change.json +10 -0
  84. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-1.json +5 -0
  85. package/src/modules/persistence/__tests__/.tmp-stores-test-81991/workers/worker-2.json +5 -0
  86. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/progress.json +10 -0
  87. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/completed/task-getall-2.json +10 -0
  88. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-1.json +13 -0
  89. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-getall-1.json +10 -0
  90. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/queue/pending/task-status-change.json +10 -0
  91. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-1.json +5 -0
  92. package/src/modules/persistence/__tests__/.tmp-stores-test-82469/workers/worker-2.json +5 -0
  93. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/progress.json +10 -0
  94. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/completed/task-getall-2.json +10 -0
  95. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-1.json +13 -0
  96. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-getall-1.json +10 -0
  97. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/queue/pending/task-status-change.json +10 -0
  98. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-1.json +5 -0
  99. package/src/modules/persistence/__tests__/.tmp-stores-test-85301/workers/worker-2.json +5 -0
  100. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/progress.json +10 -0
  101. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/completed/task-getall-2.json +10 -0
  102. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-1.json +13 -0
  103. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-getall-1.json +10 -0
  104. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/queue/pending/task-status-change.json +10 -0
  105. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-1.json +5 -0
  106. package/src/modules/persistence/__tests__/.tmp-stores-test-85759/workers/worker-2.json +5 -0
  107. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/progress.json +10 -0
  108. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/completed/task-getall-2.json +10 -0
  109. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-1.json +13 -0
  110. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-getall-1.json +10 -0
  111. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/queue/pending/task-status-change.json +10 -0
  112. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-1.json +5 -0
  113. package/src/modules/persistence/__tests__/.tmp-stores-test-86184/workers/worker-2.json +5 -0
  114. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/progress.json +10 -0
  115. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/completed/task-getall-2.json +10 -0
  116. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-1.json +13 -0
  117. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-getall-1.json +10 -0
  118. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/queue/pending/task-status-change.json +10 -0
  119. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-1.json +5 -0
  120. package/src/modules/persistence/__tests__/.tmp-stores-test-88026/workers/worker-2.json +5 -0
  121. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/progress.json +10 -0
  122. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/completed/task-getall-2.json +10 -0
  123. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-1.json +13 -0
  124. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-getall-1.json +10 -0
  125. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/queue/pending/task-status-change.json +10 -0
  126. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-1.json +5 -0
  127. package/src/modules/persistence/__tests__/.tmp-stores-test-89475/workers/worker-2.json +5 -0
  128. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/progress.json +10 -0
  129. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/completed/task-getall-2.json +10 -0
  130. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-1.json +13 -0
  131. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-getall-1.json +10 -0
  132. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/queue/pending/task-status-change.json +10 -0
  133. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-1.json +5 -0
  134. package/src/modules/persistence/__tests__/.tmp-stores-test-89924/workers/worker-2.json +5 -0
  135. package/src/modules/persistence/__tests__/file-lock.test.ts +141 -0
  136. package/src/modules/persistence/__tests__/index.test.ts +38 -0
  137. package/src/modules/persistence/__tests__/stores.test.ts +594 -0
  138. package/src/modules/persistence/file-lock.ts +158 -0
  139. package/src/modules/persistence/fs-run-store.ts +73 -0
  140. package/src/modules/persistence/fs-task-store.ts +152 -0
  141. package/src/modules/persistence/fs-worker-store.ts +116 -0
  142. package/src/modules/persistence/in-memory-stores.ts +98 -0
  143. package/src/modules/persistence/index.ts +60 -0
  144. package/src/modules/persistence/stores.port.ts +60 -0
  145. package/src/modules/planning/__tests__/file-conflict-validator.test.ts +256 -0
  146. package/src/modules/planning/__tests__/planning-service.test.ts +366 -0
  147. package/src/modules/planning/__tests__/project-detection.test.ts +707 -0
  148. package/src/modules/planning/file-conflict-validator.ts +135 -0
  149. package/src/modules/planning/index.ts +40 -0
  150. package/src/modules/planning/planning.service.ts +262 -0
  151. package/src/modules/planning/project-detection.ts +525 -0
  152. package/src/modules/plugin/__tests__/plugin-loader.test.ts +83 -0
  153. package/src/modules/plugin/__tests__/plugin-manager.test.ts +187 -0
  154. package/src/modules/plugin/index.ts +3 -0
  155. package/src/modules/plugin/plugin-loader.ts +46 -0
  156. package/src/modules/plugin/plugin-manager.ts +90 -0
  157. package/src/modules/plugin/plugin.types.ts +37 -0
  158. package/src/modules/process-manager/__tests__/process-manager.test.ts +210 -0
  159. package/src/modules/process-manager/__tests__/worker.test.ts +89 -0
  160. package/src/modules/process-manager/index.ts +5 -0
  161. package/src/modules/process-manager/process-manager.ts +193 -0
  162. package/src/modules/process-manager/worker.ts +106 -0
  163. package/src/modules/task-execution/__tests__/default-spawner.test.ts +154 -0
  164. package/src/modules/task-execution/__tests__/executor.test.ts +760 -0
  165. package/src/modules/task-execution/__tests__/implementer-green.test.ts +286 -0
  166. package/src/modules/task-execution/__tests__/merge-phase.test.ts +368 -0
  167. package/src/modules/task-execution/__tests__/reviewer.test.ts +302 -0
  168. package/src/modules/task-execution/__tests__/tester-red.test.ts +281 -0
  169. package/src/modules/task-execution/__tests__/tester-verify.test.ts +313 -0
  170. package/src/modules/task-execution/executor.ts +303 -0
  171. package/src/modules/task-execution/index.ts +45 -0
  172. package/src/modules/task-execution/phases/default-spawner.ts +49 -0
  173. package/src/modules/task-execution/phases/implementer-green.ts +100 -0
  174. package/src/modules/task-execution/phases/merge.ts +122 -0
  175. package/src/modules/task-execution/phases/reviewer.ts +160 -0
  176. package/src/modules/task-execution/phases/tester-red.ts +100 -0
  177. package/src/modules/task-execution/phases/tester-verify.ts +120 -0
  178. package/src/modules/task-queue/__tests__/dependency-resolver.test.ts +456 -0
  179. package/src/modules/task-queue/__tests__/dispatcher.test.ts +824 -0
  180. package/src/modules/task-queue/__tests__/task-plan.test.ts +122 -0
  181. package/src/modules/task-queue/__tests__/task.test.ts +130 -0
  182. package/src/modules/task-queue/dependency-resolver.ts +171 -0
  183. package/src/modules/task-queue/dispatcher.ts +372 -0
  184. package/src/modules/task-queue/index.ts +16 -0
  185. package/src/modules/task-queue/task-plan.ts +40 -0
  186. package/src/modules/task-queue/task.ts +67 -0
  187. package/src/shared/__tests__/config.test.ts +204 -0
  188. package/src/shared/__tests__/errors.test.ts +285 -0
  189. package/src/shared/__tests__/events.test.ts +496 -0
  190. package/src/shared/__tests__/types.test.ts +360 -0
  191. package/src/shared/config.ts +133 -0
  192. package/src/shared/errors.ts +128 -0
  193. package/src/shared/events.ts +171 -0
  194. package/src/shared/types.ts +143 -0
  195. package/tsconfig.json +30 -0
@@ -0,0 +1,330 @@
1
+ import { describe, test, expect, beforeEach } from "bun:test";
2
+ import { StateAggregator } from "../services/state-aggregator";
3
+ import { LogStore } from "../../logging";
4
+ import type { LogEntry } from "../../logging";
5
+ import type { TaskId, WorkerId, TaskStatus } from "../../../shared/types";
6
+ import { createTaskId, createWorkerId } from "../../../shared/types";
7
+
8
+ // Mock stores for testing
9
+ interface Task {
10
+ taskId: TaskId;
11
+ status: TaskStatus;
12
+ dependsOn: TaskId[];
13
+ startTime?: string;
14
+ endTime?: string;
15
+ }
16
+
17
+ class MockTaskStore {
18
+ private tasks: Map<TaskId, Task> = new Map();
19
+
20
+ add(task: Task): void {
21
+ this.tasks.set(task.taskId, task);
22
+ }
23
+
24
+ async getAll(): Promise<Task[]> {
25
+ return Array.from(this.tasks.values());
26
+ }
27
+
28
+ async get(id: TaskId): Promise<Task | null> {
29
+ return this.tasks.get(id) ?? null;
30
+ }
31
+
32
+ async getByStatus(): Promise<Task[]> {
33
+ return [];
34
+ }
35
+
36
+ async save(): Promise<void> {}
37
+
38
+ async delete(): Promise<void> {}
39
+ }
40
+
41
+ class MockWorkerStateMachine {
42
+ constructor(private id: WorkerId, private _status: "idle" | "busy" | "stopped" = "idle") {}
43
+
44
+ isIdle(): boolean {
45
+ return this._status === "idle";
46
+ }
47
+
48
+ isBusy(): boolean {
49
+ return this._status === "busy";
50
+ }
51
+
52
+ isStopped(): boolean {
53
+ return this._status === "stopped";
54
+ }
55
+
56
+ getWorkerId(): WorkerId {
57
+ return this.id;
58
+ }
59
+ }
60
+
61
+ class MockProcessManager {
62
+ private workers: Map<WorkerId, MockWorkerStateMachine> = new Map();
63
+
64
+ addWorker(id: WorkerId, status: "idle" | "busy" | "stopped" = "idle"): void {
65
+ this.workers.set(id, new MockWorkerStateMachine(id, status));
66
+ }
67
+
68
+ getStats() {
69
+ let idle = 0;
70
+ let busy = 0;
71
+ let stopped = 0;
72
+ for (const worker of this.workers.values()) {
73
+ if (worker.isIdle()) idle++;
74
+ else if (worker.isBusy()) busy++;
75
+ else if (worker.isStopped()) stopped++;
76
+ }
77
+ return {
78
+ total: this.workers.size,
79
+ idle,
80
+ busy,
81
+ stopped,
82
+ };
83
+ }
84
+
85
+ getAllWorkers(): WorkerId[] {
86
+ return Array.from(this.workers.keys());
87
+ }
88
+
89
+ getWorker(id: WorkerId): MockWorkerStateMachine | null {
90
+ return this.workers.get(id) ?? null;
91
+ }
92
+ }
93
+
94
+ describe("StateAggregator", () => {
95
+ let logStore: LogStore;
96
+ let taskStore: MockTaskStore;
97
+ let processManager: MockProcessManager;
98
+ let aggregator: StateAggregator;
99
+
100
+ beforeEach(() => {
101
+ logStore = new LogStore();
102
+ taskStore = new MockTaskStore();
103
+ processManager = new MockProcessManager();
104
+ aggregator = new StateAggregator({
105
+ logStore,
106
+ taskStore: taskStore as any,
107
+ processManager: processManager as any,
108
+ });
109
+ });
110
+
111
+ test("getProgress calculates progress state correctly", async () => {
112
+ taskStore.add({
113
+ taskId: createTaskId("task-1"),
114
+ status: "pending",
115
+ dependsOn: [],
116
+ });
117
+ taskStore.add({
118
+ taskId: createTaskId("task-2"),
119
+ status: "running",
120
+ dependsOn: [],
121
+ });
122
+ taskStore.add({
123
+ taskId: createTaskId("task-3"),
124
+ status: "completed",
125
+ dependsOn: [],
126
+ });
127
+ taskStore.add({
128
+ taskId: createTaskId("task-4"),
129
+ status: "failed",
130
+ dependsOn: [],
131
+ });
132
+
133
+ const result = await aggregator.getProgress();
134
+
135
+ expect(result.progress).toEqual({
136
+ total: 4,
137
+ pending: 1,
138
+ running: 1,
139
+ completed: 1,
140
+ failed: 1,
141
+ });
142
+ expect(result.percentComplete).toBe(25); // 1 completed out of 4
143
+ });
144
+
145
+ test("getProgress handles zero tasks", async () => {
146
+ const result = await aggregator.getProgress();
147
+
148
+ expect(result.progress).toEqual({
149
+ total: 0,
150
+ pending: 0,
151
+ running: 0,
152
+ completed: 0,
153
+ failed: 0,
154
+ });
155
+ expect(result.percentComplete).toBe(0);
156
+ });
157
+
158
+ test("getTasks returns all tasks", async () => {
159
+ const task1 = {
160
+ taskId: createTaskId("task-1"),
161
+ status: "pending" as TaskStatus,
162
+ dependsOn: [],
163
+ };
164
+ const task2 = {
165
+ taskId: createTaskId("task-2"),
166
+ status: "running" as TaskStatus,
167
+ dependsOn: [createTaskId("task-1")],
168
+ };
169
+
170
+ taskStore.add(task1);
171
+ taskStore.add(task2);
172
+
173
+ const result = await aggregator.getTasks();
174
+
175
+ expect(result.tasks).toHaveLength(2);
176
+ expect(result.total).toBe(2);
177
+ });
178
+
179
+ test("getTaskLogs queries LogStore with taskId filter", async () => {
180
+ const entry1: LogEntry = {
181
+ level: "info",
182
+ service: "task-execution",
183
+ message: "Task started",
184
+ timestamp: Date.now(),
185
+ taskId: "task-1",
186
+ };
187
+ const entry2: LogEntry = {
188
+ level: "error",
189
+ service: "task-execution",
190
+ message: "Task failed",
191
+ timestamp: Date.now(),
192
+ taskId: "task-1",
193
+ };
194
+ const entry3: LogEntry = {
195
+ level: "info",
196
+ service: "task-execution",
197
+ message: "Other task",
198
+ timestamp: Date.now(),
199
+ taskId: "task-2",
200
+ };
201
+
202
+ logStore.add(entry1);
203
+ logStore.add(entry2);
204
+ logStore.add(entry3);
205
+
206
+ const result = aggregator.getTaskLogs(createTaskId("task-1"));
207
+
208
+ expect(result).toHaveLength(2);
209
+ const logEntry0 = result[0];
210
+ const logEntry1 = result[1];
211
+ if (logEntry0) expect(logEntry0.message).toBe("Task started");
212
+ if (logEntry1) expect(logEntry1.message).toBe("Task failed");
213
+ });
214
+
215
+ test("getWorkers returns worker stats and list", () => {
216
+ processManager.addWorker(createWorkerId("worker-1"), "busy");
217
+ processManager.addWorker(createWorkerId("worker-2"), "idle");
218
+
219
+ const result = aggregator.getWorkers();
220
+
221
+ expect(result.stats).toEqual({
222
+ total: 2,
223
+ idle: 1,
224
+ busy: 1,
225
+ stopped: 0,
226
+ });
227
+ expect(result.workers).toHaveLength(2);
228
+ });
229
+
230
+ test("getGraph builds dependency graph", async () => {
231
+ const task1 = {
232
+ taskId: createTaskId("task-1"),
233
+ status: "completed" as TaskStatus,
234
+ dependsOn: [],
235
+ };
236
+ const task2 = {
237
+ taskId: createTaskId("task-2"),
238
+ status: "running" as TaskStatus,
239
+ dependsOn: [createTaskId("task-1")],
240
+ };
241
+ const task3 = {
242
+ taskId: createTaskId("task-3"),
243
+ status: "pending" as TaskStatus,
244
+ dependsOn: [createTaskId("task-1"), createTaskId("task-2")],
245
+ };
246
+
247
+ taskStore.add(task1);
248
+ taskStore.add(task2);
249
+ taskStore.add(task3);
250
+
251
+ const result = await aggregator.getGraph();
252
+
253
+ expect(result.nodes).toHaveLength(3);
254
+ expect(result.edges).toHaveLength(3); // task1->task2, task1->task3, task2->task3
255
+
256
+ const edge0 = result.edges[0];
257
+ const edge1 = result.edges[1];
258
+ const edge2 = result.edges[2];
259
+
260
+ if (edge0) {
261
+ expect(edge0.from).toBe("task-1");
262
+ expect(edge0.to).toBe("task-2");
263
+ }
264
+ if (edge1) {
265
+ expect(edge1.from).toBe("task-1");
266
+ expect(edge1.to).toBe("task-3");
267
+ }
268
+ if (edge2) {
269
+ expect(edge2.from).toBe("task-2");
270
+ expect(edge2.to).toBe("task-3");
271
+ }
272
+ });
273
+
274
+ test("getTimeline extracts task time ranges", async () => {
275
+ const now = Date.now();
276
+ taskStore.add({
277
+ taskId: createTaskId("task-1"),
278
+ status: "completed",
279
+ dependsOn: [],
280
+ startTime: new Date(now - 10000).toISOString(),
281
+ endTime: new Date(now - 5000).toISOString(),
282
+ });
283
+ taskStore.add({
284
+ taskId: createTaskId("task-2"),
285
+ status: "running",
286
+ dependsOn: [],
287
+ startTime: new Date(now - 3000).toISOString(),
288
+ });
289
+
290
+ const result = await aggregator.getTimeline();
291
+
292
+ expect(result.tasks).toHaveLength(2);
293
+ const task0 = result.tasks[0];
294
+ const task1 = result.tasks[1];
295
+ if (task0) {
296
+ expect(task0.id).toBe("task-1");
297
+ expect(task0.startTime).toBe(new Date(now - 10000).toISOString());
298
+ expect(task0.endTime).toBe(new Date(now - 5000).toISOString());
299
+ }
300
+ if (task1) {
301
+ expect(task1.id).toBe("task-2");
302
+ expect(task1.startTime).toBe(new Date(now - 3000).toISOString());
303
+ expect(task1.endTime).toBeUndefined();
304
+ }
305
+ });
306
+
307
+ test("queryLogs forwards filter to LogStore", () => {
308
+ const entry1: LogEntry = {
309
+ level: "error",
310
+ service: "planning",
311
+ message: "Error 1",
312
+ timestamp: Date.now(),
313
+ };
314
+ const entry2: LogEntry = {
315
+ level: "info",
316
+ service: "planning",
317
+ message: "Info 1",
318
+ timestamp: Date.now(),
319
+ };
320
+
321
+ logStore.add(entry1);
322
+ logStore.add(entry2);
323
+
324
+ const result = aggregator.queryLogs({ level: "error", limit: 10 });
325
+
326
+ expect(result).toHaveLength(1);
327
+ const entry = result[0];
328
+ if (entry) expect(entry.level).toBe("error");
329
+ });
330
+ });
@@ -0,0 +1,8 @@
1
+ // Dashboard Module Public API
2
+
3
+ export { DashboardServer, type DashboardServerOptions } from "./server";
4
+ export { SSEBroadcaster, type SSEClient } from "./services/sse-broadcaster";
5
+ export { StateAggregator, type StateAggregatorOptions } from "./services/state-aggregator";
6
+ export { FileWatcher } from "./services/file-watcher";
7
+ export { createAPIRoutes } from "./routes/api";
8
+ export { createSSERoutes } from "./routes/sse";
@@ -0,0 +1,84 @@
1
+ import { Hono } from "hono";
2
+ import { z } from "zod";
3
+ import type { StateAggregator } from "../services/state-aggregator";
4
+ import { createTaskId } from "../../../shared/types";
5
+
6
+ const logQuerySchema = z.object({
7
+ level: z.string().optional(),
8
+ service: z.string().optional(),
9
+ taskId: z.string().optional(),
10
+ workerId: z.string().optional(),
11
+ limit: z
12
+ .string()
13
+ .optional()
14
+ .transform((val) => (val ? parseInt(val, 10) : undefined))
15
+ .refine((val) => val === undefined || (val > 0 && !isNaN(val)), {
16
+ message: "limit must be a positive number",
17
+ }),
18
+ });
19
+
20
+ export function createAPIRoutes(aggregator: StateAggregator): Hono {
21
+ const app = new Hono();
22
+
23
+ app.get("/api/progress", async (c) => {
24
+ const data = await aggregator.getProgress();
25
+ return c.json(data);
26
+ });
27
+
28
+ app.get("/api/tasks", async (c) => {
29
+ const data = await aggregator.getTasks();
30
+ return c.json(data);
31
+ });
32
+
33
+ app.get("/api/tasks/:id/logs", (c) => {
34
+ const taskId = createTaskId(c.req.param("id"));
35
+ const logs = aggregator.getTaskLogs(taskId);
36
+ return c.json(logs);
37
+ });
38
+
39
+ app.get("/api/workers", (c) => {
40
+ const data = aggregator.getWorkers();
41
+ return c.json(data);
42
+ });
43
+
44
+ app.get("/api/graph", async (c) => {
45
+ const data = await aggregator.getGraph();
46
+ return c.json(data);
47
+ });
48
+
49
+ app.get("/api/timeline", async (c) => {
50
+ const data = await aggregator.getTimeline();
51
+ return c.json(data);
52
+ });
53
+
54
+ app.get("/api/logs", (c) => {
55
+ const query = c.req.query();
56
+
57
+ // Validate query parameters with Zod
58
+ const parseResult = logQuerySchema.safeParse(query);
59
+
60
+ if (!parseResult.success) {
61
+ return c.json(
62
+ {
63
+ error: "Invalid query parameters",
64
+ details: parseResult.error.errors,
65
+ },
66
+ 400
67
+ );
68
+ }
69
+
70
+ const validatedQuery = parseResult.data;
71
+
72
+ const logs = aggregator.queryLogs({
73
+ level: validatedQuery.level,
74
+ service: validatedQuery.service,
75
+ taskId: validatedQuery.taskId,
76
+ workerId: validatedQuery.workerId,
77
+ limit: validatedQuery.limit,
78
+ });
79
+
80
+ return c.json(logs);
81
+ });
82
+
83
+ return app;
84
+ }
@@ -0,0 +1,37 @@
1
+ import { Hono } from "hono";
2
+ import { streamSSE } from "hono/streaming";
3
+ import type { SSEBroadcaster, SSEClient } from "../services/sse-broadcaster";
4
+
5
+ export function createSSERoutes(broadcaster: SSEBroadcaster): Hono {
6
+ const app = new Hono();
7
+
8
+ app.get("/events/all", (c) => {
9
+ return streamSSE(c, async (stream) => {
10
+ // Create SSE client wrapper
11
+ const client: SSEClient = {
12
+ send: (data: string) => {
13
+ void stream.write(data);
14
+ },
15
+ close: () => {
16
+ // Stream will be closed automatically when function returns
17
+ },
18
+ };
19
+
20
+ // Register client with broadcaster
21
+ broadcaster.addClient(client);
22
+
23
+ // Keep connection alive until client disconnects
24
+ // Use a reasonable timeout loop instead of MAX_SAFE_INTEGER
25
+ try {
26
+ while (true) {
27
+ await stream.sleep(1000000); // Sleep for ~16 minutes, then loop
28
+ }
29
+ } finally {
30
+ // Cleanup on disconnect (via error or abort)
31
+ broadcaster.removeClient(client);
32
+ }
33
+ });
34
+ });
35
+
36
+ return app;
37
+ }
@@ -0,0 +1,111 @@
1
+ import { Hono } from "hono";
2
+ import type { Logger } from "pino";
3
+ import { SSEBroadcaster } from "./services/sse-broadcaster";
4
+ import { StateAggregator } from "./services/state-aggregator";
5
+ import { createAPIRoutes } from "./routes/api";
6
+ import { createSSERoutes } from "./routes/sse";
7
+ import type { EventBus } from "../../shared/events";
8
+ import type { LogStore } from "../../modules/logging";
9
+ import type { TaskStore } from "../../modules/task-queue";
10
+ import type { ProcessManager } from "../../modules/process-manager";
11
+ import { DashboardError } from "../../shared/errors";
12
+ import dashboardHTML from "./ui/dashboard.html" with { type: "text" };
13
+
14
+ /**
15
+ * Bun.serve() return type
16
+ */
17
+ interface BunServer {
18
+ stop(): void;
19
+ port: number | undefined;
20
+ hostname: string;
21
+ fetch: (request: Request) => Response | Promise<Response>;
22
+ }
23
+
24
+ export interface DashboardServerOptions {
25
+ eventBus: EventBus;
26
+ logStore: LogStore;
27
+ taskStore: TaskStore;
28
+ processManager: ProcessManager;
29
+ port: number;
30
+ host: string;
31
+ logger?: Logger;
32
+ }
33
+
34
+ export class DashboardServer {
35
+ private broadcaster: SSEBroadcaster;
36
+ private aggregator: StateAggregator;
37
+ private app: Hono;
38
+ private server: BunServer | null = null;
39
+ private port: number;
40
+ private host: string;
41
+ private logger?: Logger;
42
+
43
+ constructor(options: DashboardServerOptions) {
44
+ this.port = options.port;
45
+ this.host = options.host;
46
+ this.logger = options.logger;
47
+
48
+ // Initialize SSE broadcaster
49
+ this.broadcaster = new SSEBroadcaster(options.eventBus, options.logger);
50
+
51
+ // Initialize state aggregator
52
+ this.aggregator = new StateAggregator({
53
+ logStore: options.logStore,
54
+ taskStore: options.taskStore,
55
+ processManager: options.processManager,
56
+ });
57
+
58
+ // Create Hono app
59
+ this.app = new Hono();
60
+
61
+ // Serve static HTML dashboard (FIX #6: use text import for binary build)
62
+ this.app.get("/", (c) => {
63
+ return c.html(String(dashboardHTML));
64
+ });
65
+
66
+ // Mount API routes
67
+ this.app.route("/", createAPIRoutes(this.aggregator));
68
+
69
+ // Mount SSE routes
70
+ this.app.route("/", createSSERoutes(this.broadcaster));
71
+ }
72
+
73
+ start(): void {
74
+ if (this.server) {
75
+ // Already started
76
+ return;
77
+ }
78
+
79
+ // Start SSE broadcaster
80
+ this.broadcaster.start();
81
+
82
+ // Start HTTP server with Bun.serve
83
+ try {
84
+ this.server = Bun.serve({
85
+ port: this.port,
86
+ hostname: this.host,
87
+ fetch: this.app.fetch,
88
+ }) as BunServer;
89
+ this.logger?.info({ port: this.port, host: this.host }, "Dashboard server started");
90
+ } catch (error) {
91
+ throw new DashboardError("Failed to start dashboard server", {
92
+ port: this.port,
93
+ host: this.host,
94
+ error: error instanceof Error ? error.message : String(error),
95
+ });
96
+ }
97
+ }
98
+
99
+ stop(): void {
100
+ if (!this.server) {
101
+ return;
102
+ }
103
+
104
+ // Stop SSE broadcaster
105
+ this.broadcaster.stop();
106
+
107
+ // Stop HTTP server
108
+ this.server.stop();
109
+ this.server = null;
110
+ }
111
+ }
@@ -0,0 +1,36 @@
1
+ import type { EventBus } from "../../../shared/events";
2
+ import type { FSWatcher } from "chokidar";
3
+
4
+ /**
5
+ * FileWatcher - Bash script ブリッジ (オプション機能)
6
+ *
7
+ * Bashスクリプトがファイルシステムに書き込むイベントを監視し、
8
+ * EventBusに転送する。Phase 3では最小限のスタブ実装。
9
+ */
10
+ export class FileWatcher {
11
+ private _eventBus: EventBus;
12
+ private _watchPath: string;
13
+ private watcher: FSWatcher | null = null;
14
+
15
+ constructor(eventBus: EventBus, watchPath: string) {
16
+ this._eventBus = eventBus;
17
+ this._watchPath = watchPath;
18
+ }
19
+
20
+ start(): void {
21
+ // Phase 3: 最小限のスタブ実装
22
+ // 実際のchokidarインスタンス化とファイル監視は将来の拡張として残す
23
+ // 現時点ではstart/stopのライフサイクルのみ提供
24
+
25
+ // Suppress unused variable warnings
26
+ void this._eventBus;
27
+ void this._watchPath;
28
+ }
29
+
30
+ stop(): void {
31
+ if (this.watcher) {
32
+ void this.watcher.close();
33
+ this.watcher = null;
34
+ }
35
+ }
36
+ }
@@ -0,0 +1,81 @@
1
+ import type { Logger } from "pino";
2
+ import type { EventBus, AADEvent, EventListener } from "../../../shared/events";
3
+
4
+ export interface SSEClient {
5
+ send(data: string): void;
6
+ close(): void;
7
+ }
8
+
9
+ export class SSEBroadcaster {
10
+ private eventBus: EventBus;
11
+ private clients: Set<SSEClient> = new Set();
12
+ private listener: EventListener<AADEvent> | null = null;
13
+ private logger?: Logger;
14
+
15
+ constructor(eventBus: EventBus, logger?: Logger) {
16
+ this.eventBus = eventBus;
17
+ this.logger = logger;
18
+ }
19
+
20
+ start(): void {
21
+ this.listener = (event: AADEvent) => {
22
+ this.broadcast(event);
23
+ };
24
+
25
+ this.eventBus.on("*", this.listener);
26
+ }
27
+
28
+ stop(): void {
29
+ if (this.listener) {
30
+ this.eventBus.off("*", this.listener);
31
+ this.listener = null;
32
+ }
33
+
34
+ // Close all clients
35
+ for (const client of this.clients) {
36
+ try {
37
+ client.close();
38
+ } catch (error) {
39
+ this.logger?.debug({ error }, "Failed to close SSE client during cleanup");
40
+ }
41
+ }
42
+
43
+ this.clients.clear();
44
+ }
45
+
46
+ addClient(client: SSEClient): void {
47
+ this.clients.add(client);
48
+ }
49
+
50
+ removeClient(client: SSEClient): void {
51
+ this.clients.delete(client);
52
+ }
53
+
54
+ getClientCount(): number {
55
+ return this.clients.size;
56
+ }
57
+
58
+ private broadcast(event: AADEvent): void {
59
+ const data = `data: ${JSON.stringify(event)}\n\n`;
60
+ const failedClients: SSEClient[] = [];
61
+
62
+ for (const client of this.clients) {
63
+ try {
64
+ client.send(data);
65
+ } catch (error) {
66
+ this.logger?.debug({ error, eventType: event.type }, "Failed to send SSE event to client");
67
+ failedClients.push(client);
68
+ }
69
+ }
70
+
71
+ // Remove failed clients
72
+ for (const client of failedClients) {
73
+ this.clients.delete(client);
74
+ try {
75
+ client.close();
76
+ } catch (error) {
77
+ this.logger?.debug({ error }, "Failed to close disconnected SSE client");
78
+ }
79
+ }
80
+ }
81
+ }