@litmers/cursorflow-orchestrator 0.1.20 → 0.1.28

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 (224) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/commands/cursorflow-clean.md +19 -0
  3. package/commands/cursorflow-runs.md +59 -0
  4. package/commands/cursorflow-stop.md +55 -0
  5. package/dist/cli/clean.js +171 -0
  6. package/dist/cli/clean.js.map +1 -1
  7. package/dist/cli/index.js +7 -0
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/cli/init.js +1 -1
  10. package/dist/cli/init.js.map +1 -1
  11. package/dist/cli/logs.js +83 -42
  12. package/dist/cli/logs.js.map +1 -1
  13. package/dist/cli/monitor.d.ts +7 -0
  14. package/dist/cli/monitor.js +1007 -189
  15. package/dist/cli/monitor.js.map +1 -1
  16. package/dist/cli/prepare.js +87 -3
  17. package/dist/cli/prepare.js.map +1 -1
  18. package/dist/cli/resume.js +188 -236
  19. package/dist/cli/resume.js.map +1 -1
  20. package/dist/cli/run.js +125 -3
  21. package/dist/cli/run.js.map +1 -1
  22. package/dist/cli/runs.d.ts +5 -0
  23. package/dist/cli/runs.js +214 -0
  24. package/dist/cli/runs.js.map +1 -0
  25. package/dist/cli/setup-commands.js +0 -0
  26. package/dist/cli/signal.js +1 -1
  27. package/dist/cli/signal.js.map +1 -1
  28. package/dist/cli/stop.d.ts +5 -0
  29. package/dist/cli/stop.js +215 -0
  30. package/dist/cli/stop.js.map +1 -0
  31. package/dist/cli/tasks.d.ts +10 -0
  32. package/dist/cli/tasks.js +165 -0
  33. package/dist/cli/tasks.js.map +1 -0
  34. package/dist/core/auto-recovery.d.ts +212 -0
  35. package/dist/core/auto-recovery.js +737 -0
  36. package/dist/core/auto-recovery.js.map +1 -0
  37. package/dist/core/failure-policy.d.ts +156 -0
  38. package/dist/core/failure-policy.js +488 -0
  39. package/dist/core/failure-policy.js.map +1 -0
  40. package/dist/core/orchestrator.d.ts +15 -2
  41. package/dist/core/orchestrator.js +397 -15
  42. package/dist/core/orchestrator.js.map +1 -1
  43. package/dist/core/reviewer.d.ts +2 -0
  44. package/dist/core/reviewer.js +2 -0
  45. package/dist/core/reviewer.js.map +1 -1
  46. package/dist/core/runner.d.ts +33 -10
  47. package/dist/core/runner.js +321 -146
  48. package/dist/core/runner.js.map +1 -1
  49. package/dist/services/logging/buffer.d.ts +67 -0
  50. package/dist/services/logging/buffer.js +309 -0
  51. package/dist/services/logging/buffer.js.map +1 -0
  52. package/dist/services/logging/console.d.ts +89 -0
  53. package/dist/services/logging/console.js +169 -0
  54. package/dist/services/logging/console.js.map +1 -0
  55. package/dist/services/logging/file-writer.d.ts +71 -0
  56. package/dist/services/logging/file-writer.js +516 -0
  57. package/dist/services/logging/file-writer.js.map +1 -0
  58. package/dist/services/logging/formatter.d.ts +39 -0
  59. package/dist/services/logging/formatter.js +227 -0
  60. package/dist/services/logging/formatter.js.map +1 -0
  61. package/dist/services/logging/index.d.ts +11 -0
  62. package/dist/services/logging/index.js +30 -0
  63. package/dist/services/logging/index.js.map +1 -0
  64. package/dist/services/logging/parser.d.ts +31 -0
  65. package/dist/services/logging/parser.js +222 -0
  66. package/dist/services/logging/parser.js.map +1 -0
  67. package/dist/services/process/index.d.ts +59 -0
  68. package/dist/services/process/index.js +257 -0
  69. package/dist/services/process/index.js.map +1 -0
  70. package/dist/types/agent.d.ts +20 -0
  71. package/dist/types/agent.js +6 -0
  72. package/dist/types/agent.js.map +1 -0
  73. package/dist/types/config.d.ts +65 -0
  74. package/dist/types/config.js +6 -0
  75. package/dist/types/config.js.map +1 -0
  76. package/dist/types/events.d.ts +125 -0
  77. package/dist/types/events.js +6 -0
  78. package/dist/types/events.js.map +1 -0
  79. package/dist/types/index.d.ts +12 -0
  80. package/dist/types/index.js +37 -0
  81. package/dist/types/index.js.map +1 -0
  82. package/dist/types/lane.d.ts +43 -0
  83. package/dist/types/lane.js +6 -0
  84. package/dist/types/lane.js.map +1 -0
  85. package/dist/types/logging.d.ts +71 -0
  86. package/dist/types/logging.js +16 -0
  87. package/dist/types/logging.js.map +1 -0
  88. package/dist/types/review.d.ts +17 -0
  89. package/dist/types/review.js +6 -0
  90. package/dist/types/review.js.map +1 -0
  91. package/dist/types/run.d.ts +32 -0
  92. package/dist/types/run.js +6 -0
  93. package/dist/types/run.js.map +1 -0
  94. package/dist/types/task.d.ts +71 -0
  95. package/dist/types/task.js +6 -0
  96. package/dist/types/task.js.map +1 -0
  97. package/dist/ui/components.d.ts +134 -0
  98. package/dist/ui/components.js +389 -0
  99. package/dist/ui/components.js.map +1 -0
  100. package/dist/ui/log-viewer.d.ts +49 -0
  101. package/dist/ui/log-viewer.js +449 -0
  102. package/dist/ui/log-viewer.js.map +1 -0
  103. package/dist/utils/checkpoint.d.ts +87 -0
  104. package/dist/utils/checkpoint.js +317 -0
  105. package/dist/utils/checkpoint.js.map +1 -0
  106. package/dist/utils/config.d.ts +4 -0
  107. package/dist/utils/config.js +11 -2
  108. package/dist/utils/config.js.map +1 -1
  109. package/dist/utils/cursor-agent.js.map +1 -1
  110. package/dist/utils/dependency.d.ts +74 -0
  111. package/dist/utils/dependency.js +420 -0
  112. package/dist/utils/dependency.js.map +1 -0
  113. package/dist/utils/doctor.js +10 -5
  114. package/dist/utils/doctor.js.map +1 -1
  115. package/dist/utils/enhanced-logger.d.ts +10 -33
  116. package/dist/utils/enhanced-logger.js +94 -9
  117. package/dist/utils/enhanced-logger.js.map +1 -1
  118. package/dist/utils/git.d.ts +121 -0
  119. package/dist/utils/git.js +322 -2
  120. package/dist/utils/git.js.map +1 -1
  121. package/dist/utils/health.d.ts +91 -0
  122. package/dist/utils/health.js +556 -0
  123. package/dist/utils/health.js.map +1 -0
  124. package/dist/utils/lock.d.ts +95 -0
  125. package/dist/utils/lock.js +332 -0
  126. package/dist/utils/lock.js.map +1 -0
  127. package/dist/utils/log-buffer.d.ts +17 -0
  128. package/dist/utils/log-buffer.js +14 -0
  129. package/dist/utils/log-buffer.js.map +1 -0
  130. package/dist/utils/log-constants.d.ts +23 -0
  131. package/dist/utils/log-constants.js +28 -0
  132. package/dist/utils/log-constants.js.map +1 -0
  133. package/dist/utils/log-formatter.d.ts +9 -0
  134. package/dist/utils/log-formatter.js +113 -70
  135. package/dist/utils/log-formatter.js.map +1 -1
  136. package/dist/utils/log-service.d.ts +19 -0
  137. package/dist/utils/log-service.js +47 -0
  138. package/dist/utils/log-service.js.map +1 -0
  139. package/dist/utils/logger.d.ts +46 -27
  140. package/dist/utils/logger.js +82 -60
  141. package/dist/utils/logger.js.map +1 -1
  142. package/dist/utils/process-manager.d.ts +21 -0
  143. package/dist/utils/process-manager.js +138 -0
  144. package/dist/utils/process-manager.js.map +1 -0
  145. package/dist/utils/retry.d.ts +121 -0
  146. package/dist/utils/retry.js +374 -0
  147. package/dist/utils/retry.js.map +1 -0
  148. package/dist/utils/run-service.d.ts +88 -0
  149. package/dist/utils/run-service.js +412 -0
  150. package/dist/utils/run-service.js.map +1 -0
  151. package/dist/utils/state.d.ts +58 -2
  152. package/dist/utils/state.js +306 -3
  153. package/dist/utils/state.js.map +1 -1
  154. package/dist/utils/task-service.d.ts +82 -0
  155. package/dist/utils/task-service.js +348 -0
  156. package/dist/utils/task-service.js.map +1 -0
  157. package/dist/utils/types.d.ts +2 -272
  158. package/dist/utils/types.js +16 -0
  159. package/dist/utils/types.js.map +1 -1
  160. package/package.json +38 -23
  161. package/scripts/ai-security-check.js +0 -1
  162. package/scripts/local-security-gate.sh +0 -0
  163. package/scripts/monitor-lanes.sh +94 -0
  164. package/scripts/patches/test-cursor-agent.js +0 -1
  165. package/scripts/release.sh +0 -0
  166. package/scripts/setup-security.sh +0 -0
  167. package/scripts/stream-logs.sh +72 -0
  168. package/scripts/verify-and-fix.sh +0 -0
  169. package/src/cli/clean.ts +180 -0
  170. package/src/cli/index.ts +7 -0
  171. package/src/cli/init.ts +1 -1
  172. package/src/cli/logs.ts +79 -42
  173. package/src/cli/monitor.ts +1815 -899
  174. package/src/cli/prepare.ts +97 -3
  175. package/src/cli/resume.ts +220 -277
  176. package/src/cli/run.ts +154 -3
  177. package/src/cli/runs.ts +212 -0
  178. package/src/cli/setup-commands.ts +0 -0
  179. package/src/cli/signal.ts +1 -1
  180. package/src/cli/stop.ts +209 -0
  181. package/src/cli/tasks.ts +154 -0
  182. package/src/core/auto-recovery.ts +909 -0
  183. package/src/core/failure-policy.ts +592 -0
  184. package/src/core/orchestrator.ts +1136 -675
  185. package/src/core/reviewer.ts +4 -0
  186. package/src/core/runner.ts +1443 -1217
  187. package/src/services/logging/buffer.ts +326 -0
  188. package/src/services/logging/console.ts +193 -0
  189. package/src/services/logging/file-writer.ts +526 -0
  190. package/src/services/logging/formatter.ts +268 -0
  191. package/src/services/logging/index.ts +16 -0
  192. package/src/services/logging/parser.ts +232 -0
  193. package/src/services/process/index.ts +261 -0
  194. package/src/types/agent.ts +24 -0
  195. package/src/types/config.ts +79 -0
  196. package/src/types/events.ts +156 -0
  197. package/src/types/index.ts +29 -0
  198. package/src/types/lane.ts +56 -0
  199. package/src/types/logging.ts +96 -0
  200. package/src/types/review.ts +20 -0
  201. package/src/types/run.ts +37 -0
  202. package/src/types/task.ts +79 -0
  203. package/src/ui/components.ts +430 -0
  204. package/src/ui/log-viewer.ts +485 -0
  205. package/src/utils/checkpoint.ts +374 -0
  206. package/src/utils/config.ts +11 -2
  207. package/src/utils/cursor-agent.ts +1 -1
  208. package/src/utils/dependency.ts +482 -0
  209. package/src/utils/doctor.ts +11 -5
  210. package/src/utils/enhanced-logger.ts +108 -49
  211. package/src/utils/git.ts +871 -499
  212. package/src/utils/health.ts +596 -0
  213. package/src/utils/lock.ts +346 -0
  214. package/src/utils/log-buffer.ts +28 -0
  215. package/src/utils/log-constants.ts +26 -0
  216. package/src/utils/log-formatter.ts +120 -37
  217. package/src/utils/log-service.ts +49 -0
  218. package/src/utils/logger.ts +100 -51
  219. package/src/utils/process-manager.ts +100 -0
  220. package/src/utils/retry.ts +413 -0
  221. package/src/utils/run-service.ts +433 -0
  222. package/src/utils/state.ts +369 -3
  223. package/src/utils/task-service.ts +370 -0
  224. package/src/utils/types.ts +2 -315
@@ -0,0 +1,485 @@
1
+ /**
2
+ * Interactive Log Viewer
3
+ *
4
+ * Provides scrollable, filterable, real-time log viewing with:
5
+ * - Free scrolling (up/down, page up/down, home/end)
6
+ * - Auto-scroll toggle (new logs vs. current position)
7
+ * - Lane filtering (tab to cycle, numbers for direct select)
8
+ * - Importance filtering
9
+ * - Text search
10
+ * - Readable format toggle
11
+ */
12
+
13
+ import * as readline from 'readline';
14
+ import * as path from 'path';
15
+ import * as logger from '../utils/logger';
16
+ import { LogBufferService, BufferedLogEntry } from '../services/logging/buffer';
17
+ import { LogImportance } from '../types/logging';
18
+ import { formatReadableEntry } from '../services/logging/formatter';
19
+
20
+ interface LogViewerState {
21
+ scrollOffset: number; // 현재 스크롤 위치
22
+ autoScroll: boolean; // 자동 스크롤 ON/OFF
23
+ laneFilter: string | null; // 레인 필터 (null = 전체)
24
+ importanceFilter: LogImportance | null; // 중요도 필터
25
+ searchQuery: string | null; // 검색어
26
+ searchMode: boolean; // 검색 입력 모드
27
+ searchInput: string; // 검색 입력 버퍼
28
+ readableFormat: boolean; // 리더블 포맷 ON/OFF
29
+ }
30
+
31
+ export class LogViewer {
32
+ private runDir: string;
33
+ private runId: string;
34
+ private logBuffer: LogBufferService;
35
+ private state: LogViewerState;
36
+ private pageSize: number;
37
+ private renderInterval: NodeJS.Timeout | null = null;
38
+
39
+ constructor(runDir: string) {
40
+ this.runDir = runDir;
41
+ this.runId = path.basename(runDir);
42
+ this.logBuffer = new LogBufferService(runDir);
43
+
44
+ this.state = {
45
+ scrollOffset: 0,
46
+ autoScroll: true,
47
+ laneFilter: null,
48
+ importanceFilter: null,
49
+ searchQuery: null,
50
+ searchMode: false,
51
+ searchInput: '',
52
+ readableFormat: false,
53
+ };
54
+
55
+ // 화면 높이 - 헤더/푸터 (6줄)
56
+ this.pageSize = (process.stdout.rows || 24) - 6;
57
+ }
58
+
59
+ /**
60
+ * 뷰어 시작
61
+ */
62
+ start(): void {
63
+ // 화면 초기화
64
+ this.setupTerminal();
65
+
66
+ // 로그 스트리밍 시작
67
+ this.logBuffer.startStreaming();
68
+
69
+ // 새 로그 이벤트
70
+ this.logBuffer.on('update', () => {
71
+ if (this.state.autoScroll) {
72
+ this.scrollToBottom();
73
+ this.logBuffer.acknowledgeNewEntries();
74
+ }
75
+ this.render();
76
+ });
77
+
78
+ // 키 입력 처리
79
+ this.setupKeyHandlers();
80
+
81
+ // 정기적 렌더링 (100ms) - 화면 크기 변화 대응 및 실시간성 유지
82
+ this.renderInterval = setInterval(() => {
83
+ this.render();
84
+ }, 100);
85
+
86
+ // 초기 렌더링
87
+ this.render();
88
+ }
89
+
90
+ /**
91
+ * 뷰어 종료
92
+ */
93
+ stop(): void {
94
+ if (this.renderInterval) {
95
+ clearInterval(this.renderInterval);
96
+ }
97
+ this.logBuffer.stopStreaming();
98
+ this.cleanupTerminal();
99
+ }
100
+
101
+ // ─────────────────────────────────────────────────────────────
102
+ // Key Handlers
103
+ // ─────────────────────────────────────────────────────────────
104
+
105
+ private setupKeyHandlers(): void {
106
+ readline.emitKeypressEvents(process.stdin);
107
+ if (process.stdin.isTTY) {
108
+ process.stdin.setRawMode(true);
109
+ }
110
+
111
+ process.stdin.on('keypress', (str, key) => {
112
+ if (this.state.searchMode) {
113
+ this.handleSearchKey(str, key);
114
+ } else {
115
+ this.handleNormalKey(str, key);
116
+ }
117
+ });
118
+ }
119
+
120
+ private handleNormalKey(str: string | undefined, key: any): void {
121
+ if (!key && !str) return;
122
+
123
+ const keyName = key?.name;
124
+
125
+ // Quit
126
+ if (keyName === 'q' || (key?.ctrl && keyName === 'c')) {
127
+ this.stop();
128
+ process.exit(0);
129
+ }
130
+
131
+ // Scroll
132
+ if (keyName === 'up' || str === 'k') {
133
+ this.scrollUp(1);
134
+ } else if (keyName === 'down' || str === 'j') {
135
+ this.scrollDown(1);
136
+ } else if (keyName === 'pageup' || (key?.ctrl && keyName === 'u')) {
137
+ this.scrollUp(this.pageSize);
138
+ } else if (keyName === 'pagedown' || (key?.ctrl && keyName === 'd')) {
139
+ this.scrollDown(this.pageSize);
140
+ } else if (keyName === 'home' || str === 'g') {
141
+ this.scrollToTop();
142
+ } else if (keyName === 'end' || str === 'G') {
143
+ this.scrollToBottom();
144
+ this.state.autoScroll = true;
145
+ }
146
+
147
+ // Auto-scroll toggle
148
+ else if (str === 'a' || str === 'A') {
149
+ this.state.autoScroll = !this.state.autoScroll;
150
+ if (this.state.autoScroll) {
151
+ this.scrollToBottom();
152
+ this.logBuffer.acknowledgeNewEntries();
153
+ }
154
+ }
155
+
156
+ // Lane filter
157
+ else if (keyName === 'tab') {
158
+ this.cycleLaneFilter();
159
+ } else if (str && str >= '0' && str <= '9') {
160
+ this.selectLaneByNumber(parseInt(str, 10));
161
+ }
162
+
163
+ // Importance filter
164
+ else if (str === 'f' || str === 'F') {
165
+ this.cycleImportanceFilter();
166
+ }
167
+
168
+ // Readable format toggle
169
+ else if (str === 'r' || str === 'R') {
170
+ this.state.readableFormat = !this.state.readableFormat;
171
+ }
172
+
173
+ // Search
174
+ else if (str === '/') {
175
+ this.state.searchMode = true;
176
+ this.state.searchInput = '';
177
+ }
178
+
179
+ // Clear filters
180
+ else if (keyName === 'escape') {
181
+ this.clearFilters();
182
+ }
183
+
184
+ this.render();
185
+ }
186
+
187
+ private handleSearchKey(str: string | undefined, key: any): void {
188
+ if (!key) return;
189
+
190
+ if (key.name === 'return') {
191
+ // 검색 실행
192
+ this.state.searchQuery = this.state.searchInput || null;
193
+ this.state.searchMode = false;
194
+ this.state.scrollOffset = 0;
195
+ } else if (key.name === 'escape') {
196
+ // 검색 취소
197
+ this.state.searchMode = false;
198
+ this.state.searchInput = '';
199
+ } else if (key.name === 'backspace') {
200
+ this.state.searchInput = this.state.searchInput.slice(0, -1);
201
+ } else if (str && str.length === 1 && !key.ctrl && !key.meta) {
202
+ this.state.searchInput += str;
203
+ }
204
+
205
+ this.render();
206
+ }
207
+
208
+ // ─────────────────────────────────────────────────────────────
209
+ // Scroll Methods
210
+ // ─────────────────────────────────────────────────────────────
211
+
212
+ private scrollUp(lines: number): void {
213
+ this.state.autoScroll = false;
214
+ this.state.scrollOffset = Math.max(0, this.state.scrollOffset - lines);
215
+ }
216
+
217
+ private scrollDown(lines: number): void {
218
+ const totalCount = this.getFilteredCount();
219
+ const maxOffset = Math.max(0, totalCount - this.pageSize);
220
+ this.state.scrollOffset = Math.min(maxOffset, this.state.scrollOffset + lines);
221
+
222
+ // 맨 아래에 도달하면 자동 스크롤 ON
223
+ if (this.state.scrollOffset >= maxOffset) {
224
+ this.state.autoScroll = true;
225
+ this.logBuffer.acknowledgeNewEntries();
226
+ }
227
+ }
228
+
229
+ private scrollToTop(): void {
230
+ this.state.autoScroll = false;
231
+ this.state.scrollOffset = 0;
232
+ }
233
+
234
+ private scrollToBottom(): void {
235
+ const totalCount = this.getFilteredCount();
236
+ this.state.scrollOffset = Math.max(0, totalCount - this.pageSize);
237
+ this.logBuffer.acknowledgeNewEntries();
238
+ }
239
+
240
+ // ─────────────────────────────────────────────────────────────
241
+ // Filter Methods
242
+ // ─────────────────────────────────────────────────────────────
243
+
244
+ private cycleLaneFilter(): void {
245
+ const lanes = this.logBuffer.getLanes();
246
+
247
+ if (!this.state.laneFilter) {
248
+ // null -> first lane
249
+ this.state.laneFilter = lanes[0] || null;
250
+ } else {
251
+ const currentIndex = lanes.indexOf(this.state.laneFilter);
252
+ if (currentIndex === -1 || currentIndex === lanes.length - 1) {
253
+ // last lane or not found -> null (all)
254
+ this.state.laneFilter = null;
255
+ } else {
256
+ this.state.laneFilter = lanes[currentIndex + 1];
257
+ }
258
+ }
259
+
260
+ this.state.scrollOffset = 0;
261
+ if (this.state.autoScroll) {
262
+ this.scrollToBottom();
263
+ }
264
+ }
265
+
266
+ private selectLaneByNumber(num: number): void {
267
+ const lanes = this.logBuffer.getLanes();
268
+
269
+ if (num === 0) {
270
+ this.state.laneFilter = null; // All
271
+ } else if (num <= lanes.length) {
272
+ this.state.laneFilter = lanes[num - 1];
273
+ }
274
+
275
+ this.state.scrollOffset = 0;
276
+ if (this.state.autoScroll) {
277
+ this.scrollToBottom();
278
+ }
279
+ }
280
+
281
+ private cycleImportanceFilter(): void {
282
+ const levels: (LogImportance | null)[] = [
283
+ null,
284
+ LogImportance.CRITICAL,
285
+ LogImportance.HIGH,
286
+ LogImportance.MEDIUM,
287
+ LogImportance.LOW,
288
+ LogImportance.INFO,
289
+ LogImportance.DEBUG
290
+ ];
291
+ const currentIndex = levels.indexOf(this.state.importanceFilter);
292
+ this.state.importanceFilter = levels[(currentIndex + 1) % levels.length];
293
+ this.state.scrollOffset = 0;
294
+ if (this.state.autoScroll) {
295
+ this.scrollToBottom();
296
+ }
297
+ }
298
+
299
+ private clearFilters(): void {
300
+ this.state.laneFilter = null;
301
+ this.state.importanceFilter = null;
302
+ this.state.searchQuery = null;
303
+ this.state.scrollOffset = 0;
304
+ if (this.state.autoScroll) {
305
+ this.scrollToBottom();
306
+ }
307
+ }
308
+
309
+ private getFilteredCount(): number {
310
+ return this.logBuffer.getTotalCount({
311
+ lane: this.state.laneFilter || undefined,
312
+ importance: this.state.importanceFilter || undefined,
313
+ search: this.state.searchQuery || undefined,
314
+ });
315
+ }
316
+
317
+ // ─────────────────────────────────────────────────────────────
318
+ // Rendering
319
+ // ─────────────────────────────────────────────────────────────
320
+
321
+ private setupTerminal(): void {
322
+ process.stdout.write('\x1b[?1049h'); // Alternative screen
323
+ process.stdout.write('\x1b[?25l'); // Hide cursor
324
+ console.clear();
325
+ }
326
+
327
+ private cleanupTerminal(): void {
328
+ process.stdout.write('\x1b[?25h'); // Show cursor
329
+ process.stdout.write('\x1b[?1049l'); // Restore screen
330
+ }
331
+
332
+ private render(): void {
333
+ const { gray, cyan, yellow, reset } = logger.COLORS;
334
+ const width = process.stdout.columns || 80;
335
+ const height = process.stdout.rows || 24;
336
+ this.pageSize = height - 6;
337
+
338
+ // 데이터 조회
339
+ const entries = this.logBuffer.getEntries({
340
+ offset: this.state.scrollOffset,
341
+ limit: this.pageSize,
342
+ filter: { lane: this.state.laneFilter || undefined, importance: this.state.importanceFilter || undefined, search: this.state.searchQuery || undefined },
343
+ });
344
+
345
+ const totalCount = this.getFilteredCount();
346
+ const newCount = this.logBuffer.getNewEntriesCount();
347
+ const bufferState = this.logBuffer.getState();
348
+
349
+ // 출력 버퍼
350
+ let output = '';
351
+
352
+ // 커서 위치 초기화
353
+ output += '\x1b[H';
354
+
355
+ // 헤더
356
+ const line = '━'.repeat(width);
357
+ output += `${cyan}${line}${reset}\n`;
358
+ output += `${cyan}📜 Log Viewer - ${this.runId}${reset}`;
359
+ output += ' '.repeat(Math.max(0, width - 30 - this.runId.length));
360
+ output += `${gray}[F] Filter [/] Search${reset}\n`;
361
+ output += `${cyan}${line}${reset}\n`;
362
+
363
+ // 상태 바
364
+ const laneLabel = this.state.laneFilter || 'All Lanes';
365
+ const filterLabel = this.state.importanceFilter || 'none';
366
+ const autoLabel = this.state.autoScroll ? 'ON' : 'OFF';
367
+ const liveIndicator = bufferState.isStreaming ? '🔴 LIVE' : '⚫ STOPPED';
368
+
369
+ const readableLabel = this.state.readableFormat ? 'ON' : 'OFF';
370
+
371
+ output += `View: [${yellow}${laneLabel}${reset}] `;
372
+ output += `Entries: ${totalCount} `;
373
+ output += `Filter: ${filterLabel} `;
374
+ output += `Readable: ${readableLabel} `;
375
+ output += `${liveIndicator} (Auto-scroll: ${autoLabel})`;
376
+
377
+ // 새 로그 카운터 (자동 스크롤 OFF 시)
378
+ if (!this.state.autoScroll && newCount > 0) {
379
+ output += ` ${yellow}▼ +${newCount} new${reset}`;
380
+ }
381
+
382
+ // 우측 정렬을 위한 공백 채우기
383
+ const statusLineLen = stripAnsi(output.split('\n').pop() || '').length;
384
+ output += ' '.repeat(Math.max(0, width - statusLineLen));
385
+ output += '\n';
386
+
387
+ output += `${cyan}${line}${reset}\n`;
388
+
389
+ // 로그 라인
390
+ for (let i = 0; i < this.pageSize; i++) {
391
+ const entry = entries[i];
392
+ if (entry) {
393
+ output += this.formatLogEntry(entry, width) + '\x1b[K\n';
394
+ } else {
395
+ output += '\x1b[K\n';
396
+ }
397
+ }
398
+
399
+ // 검색 모드
400
+ if (this.state.searchMode) {
401
+ output += `${cyan}Search: ${reset}${this.state.searchInput}█\x1b[K\n`;
402
+ } else {
403
+ output += `${cyan}${line}${reset}\x1b[K\n`;
404
+ }
405
+
406
+ // 푸터
407
+ const footer = `${gray}[↑/↓/PgUp/PgDn] Scroll [Tab] Lane [A] Auto-scroll [F] Filter [R] Readable [/] Search [Q] Quit${reset}`;
408
+ output += footer;
409
+ output += '\x1b[K'; // 현재 라인 끝까지 지우기
410
+
411
+ // 화면 출력
412
+ process.stdout.write(output);
413
+ }
414
+
415
+ private formatLogEntry(entry: BufferedLogEntry, width: number): string {
416
+ const { gray, reset } = logger.COLORS;
417
+
418
+ // Use readable format if enabled
419
+ if (this.state.readableFormat) {
420
+ const msgType = (entry.type || entry.level) as any;
421
+ return formatReadableEntry(
422
+ entry.timestamp,
423
+ entry.laneName,
424
+ msgType,
425
+ entry.message,
426
+ { showLane: true, maxWidth: width - 30 }
427
+ );
428
+ }
429
+
430
+ const ts = entry.timestamp.toLocaleTimeString('en-US', { hour12: false });
431
+ const lanePad = entry.laneName.substring(0, 12).padEnd(12);
432
+ const levelPad = entry.level.toUpperCase().padEnd(6);
433
+ const levelColor = this.getLevelColor(entry.level);
434
+
435
+ // 메시지 길이 제한 (ANSI 코드 제외한 실제 너비 고려)
436
+ const prefix = `[${ts}] [${lanePad}] [${levelPad}] `;
437
+ const prefixLen = prefix.length;
438
+ const maxMsgLen = Math.max(20, width - prefixLen);
439
+
440
+ // ANSI 코드 제거 후 길이 계산
441
+ const cleanMsg = stripAnsi(entry.message);
442
+ const msg = cleanMsg.length > maxMsgLen
443
+ ? cleanMsg.substring(0, maxMsgLen - 3) + '...'
444
+ : entry.message; // 원본 메시지 사용 (색상 유지 위해)
445
+
446
+ return `${gray}[${ts}]${reset} ${entry.laneColor}[${lanePad}]${reset} ${levelColor}[${levelPad}]${reset} ${msg}`;
447
+ }
448
+
449
+ private getLevelColor(level: string): string {
450
+ const colors: Record<string, string> = {
451
+ error: '\x1b[31m', // red
452
+ stderr: '\x1b[31m', // red
453
+ warn: '\x1b[33m', // yellow
454
+ warning: '\x1b[33m', // yellow
455
+ info: '\x1b[36m', // cyan
456
+ stdout: '\x1b[37m', // white
457
+ tool: '\x1b[35m', // magenta
458
+ result: '\x1b[32m', // green
459
+ debug: '\x1b[90m', // gray
460
+ };
461
+ return colors[level.toLowerCase()] || '\x1b[37m';
462
+ }
463
+ }
464
+
465
+ /**
466
+ * ANSI 코드 제거 유틸리티
467
+ */
468
+ function stripAnsi(str: string): string {
469
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
470
+ }
471
+
472
+ /**
473
+ * LogViewer 실행 함수 (CLI 진입점)
474
+ */
475
+ export async function startLogViewer(runDir: string): Promise<void> {
476
+ const viewer = new LogViewer(runDir);
477
+
478
+ // 종료 핸들러
479
+ process.on('SIGINT', () => {
480
+ viewer.stop();
481
+ process.exit(0);
482
+ });
483
+
484
+ viewer.start();
485
+ }