@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,433 @@
1
+ /**
2
+ * RunService - Manages CursorFlow run history and active processes
3
+ *
4
+ * Provides:
5
+ * - List all runs (with filtering by status)
6
+ * - Get detailed run information
7
+ * - Stop running processes
8
+ * - Delete run resources
9
+ */
10
+
11
+ import * as fs from 'fs';
12
+ import * as path from 'path';
13
+ import { RunStatus, RunInfo, LaneInfo, LaneState } from './types';
14
+ import * as logger from './logger';
15
+
16
+ export interface RunFilter {
17
+ status?: RunStatus;
18
+ limit?: number;
19
+ taskName?: string;
20
+ }
21
+
22
+ export interface RunResources {
23
+ branches: string[];
24
+ worktrees: string[];
25
+ logSize: number;
26
+ }
27
+
28
+ export class RunService {
29
+ private logsDir: string;
30
+ private runsDir: string;
31
+
32
+ constructor(logsDir: string) {
33
+ this.logsDir = logsDir;
34
+ this.runsDir = path.join(logsDir, 'runs');
35
+ }
36
+
37
+ /**
38
+ * List all runs with optional filtering
39
+ */
40
+ listRuns(filter: RunFilter = {}): RunInfo[] {
41
+ const { status, limit, taskName } = filter;
42
+
43
+ if (!fs.existsSync(this.runsDir)) {
44
+ return [];
45
+ }
46
+
47
+ const runDirs = fs.readdirSync(this.runsDir)
48
+ .filter(name => name.startsWith('run-'))
49
+ .sort((a, b) => b.localeCompare(a)); // Most recent first
50
+
51
+ let runs: RunInfo[] = [];
52
+
53
+ for (const runId of runDirs) {
54
+ const runInfo = this.getRunInfo(runId);
55
+
56
+ if (!runInfo) continue;
57
+
58
+ // Apply filters
59
+ if (status && runInfo.status !== status) continue;
60
+ if (taskName && !runInfo.taskName.toLowerCase().includes(taskName.toLowerCase())) continue;
61
+
62
+ runs.push(runInfo);
63
+
64
+ if (limit && runs.length >= limit) break;
65
+ }
66
+
67
+ return runs;
68
+ }
69
+
70
+ /**
71
+ * Get detailed information about a specific run
72
+ */
73
+ getRunInfo(runId: string): RunInfo | null {
74
+ const runPath = path.join(this.runsDir, runId);
75
+
76
+ if (!fs.existsSync(runPath)) {
77
+ return null;
78
+ }
79
+
80
+ try {
81
+ // Read orchestrator state
82
+ const statePath = path.join(runPath, 'state.json');
83
+ let taskName = 'Unknown';
84
+ let lanes: LaneInfo[] = [];
85
+ let branches: string[] = [];
86
+ let worktrees: string[] = [];
87
+
88
+ if (fs.existsSync(statePath)) {
89
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
90
+ taskName = state.taskName || this.extractTaskNameFromRunId(runId);
91
+
92
+ // Extract lane info from state
93
+ if (state.lanes) {
94
+ for (const [laneName, laneState] of Object.entries(state.lanes)) {
95
+ const ls = laneState as LaneState;
96
+ lanes.push({
97
+ name: laneName,
98
+ status: ls.status,
99
+ currentTask: ls.currentTaskIndex,
100
+ totalTasks: ls.totalTasks,
101
+ pid: ls.pid,
102
+ pipelineBranch: ls.pipelineBranch || undefined,
103
+ });
104
+
105
+ if (ls.pipelineBranch) {
106
+ branches.push(ls.pipelineBranch);
107
+ }
108
+ if (ls.worktreeDir) {
109
+ worktrees.push(ls.worktreeDir);
110
+ }
111
+ }
112
+ }
113
+ }
114
+
115
+ // Alternatively, scan lanes directory
116
+ const lanesDir = path.join(runPath, 'lanes');
117
+ if (fs.existsSync(lanesDir) && lanes.length === 0) {
118
+ const laneDirs = fs.readdirSync(lanesDir);
119
+ for (const laneName of laneDirs) {
120
+ const laneStatePath = path.join(lanesDir, laneName, 'state.json');
121
+ if (fs.existsSync(laneStatePath)) {
122
+ try {
123
+ const laneState = JSON.parse(fs.readFileSync(laneStatePath, 'utf-8')) as LaneState;
124
+ lanes.push({
125
+ name: laneName,
126
+ status: laneState.status,
127
+ currentTask: laneState.currentTaskIndex,
128
+ totalTasks: laneState.totalTasks,
129
+ pid: laneState.pid,
130
+ pipelineBranch: laneState.pipelineBranch || undefined,
131
+ });
132
+
133
+ if (laneState.pipelineBranch) {
134
+ branches.push(laneState.pipelineBranch);
135
+ }
136
+ if (laneState.worktreeDir) {
137
+ worktrees.push(laneState.worktreeDir);
138
+ }
139
+ } catch {
140
+ // Skip invalid state files
141
+ }
142
+ }
143
+ }
144
+ }
145
+
146
+ // Calculate run status and timing
147
+ const status = this.calculateRunStatus(lanes);
148
+ const startTime = this.extractTimestampFromRunId(runId);
149
+ const endTime = this.getRunEndTime(runPath, lanes);
150
+ const duration = endTime ? endTime - startTime : Date.now() - startTime;
151
+
152
+ return {
153
+ id: runId,
154
+ path: runPath,
155
+ taskName,
156
+ status,
157
+ startTime,
158
+ endTime,
159
+ duration,
160
+ lanes,
161
+ branches: [...new Set(branches)],
162
+ worktrees: [...new Set(worktrees)],
163
+ };
164
+ } catch (error) {
165
+ logger.debug(`Failed to read run info for ${runId}: ${error}`);
166
+ return null;
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Get currently active (running) runs
172
+ */
173
+ getActiveRuns(): RunInfo[] {
174
+ return this.listRuns({ status: 'running' });
175
+ }
176
+
177
+ /**
178
+ * Calculate overall run status based on lane statuses
179
+ */
180
+ private calculateRunStatus(lanes: LaneInfo[]): RunStatus {
181
+ if (lanes.length === 0) return 'pending';
182
+
183
+ const statuses = lanes.map(l => l.status);
184
+
185
+ // If any lane is running, the run is running
186
+ if (statuses.some(s => s === 'running' || s === 'reviewing')) {
187
+ return 'running';
188
+ }
189
+
190
+ // If all lanes are completed, the run is completed
191
+ if (statuses.every(s => s === 'completed')) {
192
+ return 'completed';
193
+ }
194
+
195
+ // If any lane failed and none are running, check for partial completion
196
+ if (statuses.some(s => s === 'failed')) {
197
+ const hasCompleted = statuses.some(s => s === 'completed');
198
+ return hasCompleted ? 'partial' : 'failed';
199
+ }
200
+
201
+ // If some are pending and some completed
202
+ if (statuses.some(s => s === 'pending' || s === 'waiting')) {
203
+ return statuses.some(s => s === 'completed') ? 'partial' : 'pending';
204
+ }
205
+
206
+ return 'pending';
207
+ }
208
+
209
+ /**
210
+ * Extract timestamp from run ID (format: run-TIMESTAMP)
211
+ */
212
+ private extractTimestampFromRunId(runId: string): number {
213
+ const match = runId.match(/run-(\d+)/);
214
+ return match ? parseInt(match[1], 10) : Date.now();
215
+ }
216
+
217
+ /**
218
+ * Extract task name from run ID or directory structure
219
+ */
220
+ private extractTaskNameFromRunId(runId: string): string {
221
+ // Try to read from any lane's tasks file reference
222
+ const runPath = path.join(this.runsDir, runId);
223
+ const lanesDir = path.join(runPath, 'lanes');
224
+
225
+ if (fs.existsSync(lanesDir)) {
226
+ const laneDirs = fs.readdirSync(lanesDir);
227
+ for (const laneName of laneDirs) {
228
+ const statePath = path.join(lanesDir, laneName, 'state.json');
229
+ if (fs.existsSync(statePath)) {
230
+ try {
231
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
232
+ if (state.tasksFile) {
233
+ const taskDir = path.basename(path.dirname(state.tasksFile));
234
+ // Extract feature name from timestamp_FeatureName format
235
+ const parts = taskDir.split('_');
236
+ if (parts.length >= 2) {
237
+ return parts.slice(1).join('_');
238
+ }
239
+ return taskDir;
240
+ }
241
+ } catch {
242
+ // Continue to next lane
243
+ }
244
+ }
245
+ }
246
+ }
247
+
248
+ return 'Unknown';
249
+ }
250
+
251
+ /**
252
+ * Get run end time from lane states
253
+ */
254
+ private getRunEndTime(runPath: string, lanes: LaneInfo[]): number | undefined {
255
+ const terminalStatuses = ['completed', 'failed'];
256
+
257
+ if (!lanes.every(l => terminalStatuses.includes(l.status))) {
258
+ return undefined; // Still running
259
+ }
260
+
261
+ // Find the latest end time from lane states
262
+ const lanesDir = path.join(runPath, 'lanes');
263
+ let latestEndTime = 0;
264
+
265
+ if (fs.existsSync(lanesDir)) {
266
+ const laneDirs = fs.readdirSync(lanesDir);
267
+ for (const laneName of laneDirs) {
268
+ const statePath = path.join(lanesDir, laneName, 'state.json');
269
+ if (fs.existsSync(statePath)) {
270
+ try {
271
+ const state = JSON.parse(fs.readFileSync(statePath, 'utf-8')) as LaneState;
272
+ if (state.endTime && state.endTime > latestEndTime) {
273
+ latestEndTime = state.endTime;
274
+ }
275
+ } catch {
276
+ // Skip
277
+ }
278
+ }
279
+ }
280
+ }
281
+
282
+ return latestEndTime > 0 ? latestEndTime : undefined;
283
+ }
284
+
285
+ /**
286
+ * Get resources linked to a run (branches, worktrees)
287
+ */
288
+ getLinkedResources(runId: string): RunResources {
289
+ const runInfo = this.getRunInfo(runId);
290
+
291
+ if (!runInfo) {
292
+ return { branches: [], worktrees: [], logSize: 0 };
293
+ }
294
+
295
+ // Calculate log size
296
+ let logSize = 0;
297
+ try {
298
+ logSize = this.getDirectorySize(runInfo.path);
299
+ } catch {
300
+ // Ignore size calculation errors
301
+ }
302
+
303
+ return {
304
+ branches: runInfo.branches,
305
+ worktrees: runInfo.worktrees,
306
+ logSize,
307
+ };
308
+ }
309
+
310
+ /**
311
+ * Stop a running workflow
312
+ */
313
+ stopRun(runId: string): boolean {
314
+ const runInfo = this.getRunInfo(runId);
315
+
316
+ if (!runInfo || runInfo.status !== 'running') {
317
+ return false;
318
+ }
319
+
320
+ let stopped = false;
321
+ for (const lane of runInfo.lanes) {
322
+ if (lane.pid && lane.status === 'running') {
323
+ try {
324
+ process.kill(lane.pid, 'SIGTERM');
325
+ stopped = true;
326
+ logger.info(`Stopped lane ${lane.name} (PID: ${lane.pid})`, { context: runId });
327
+ } catch (error) {
328
+ logger.debug(`Failed to stop lane ${lane.name}: ${error}`);
329
+ }
330
+ }
331
+ }
332
+
333
+ return stopped;
334
+ }
335
+
336
+ /**
337
+ * Stop all running workflows
338
+ */
339
+ stopAllRuns(): number {
340
+ const activeRuns = this.getActiveRuns();
341
+ let stoppedCount = 0;
342
+
343
+ for (const run of activeRuns) {
344
+ if (this.stopRun(run.id)) {
345
+ stoppedCount++;
346
+ }
347
+ }
348
+
349
+ return stoppedCount;
350
+ }
351
+
352
+ /**
353
+ * Delete a run and optionally its associated resources
354
+ */
355
+ deleteRun(runId: string, options: { includeBranches?: boolean; force?: boolean } = {}): void {
356
+ const runInfo = this.getRunInfo(runId);
357
+
358
+ if (!runInfo) {
359
+ throw new Error(`Run not found: ${runId}`);
360
+ }
361
+
362
+ // Don't delete running runs
363
+ if (runInfo.status === 'running') {
364
+ throw new Error(`Cannot delete running run: ${runId}. Stop it first.`);
365
+ }
366
+
367
+ // Delete log directory
368
+ if (fs.existsSync(runInfo.path)) {
369
+ fs.rmSync(runInfo.path, { recursive: true, force: true });
370
+ logger.success(`Deleted run logs: ${runId}`);
371
+ }
372
+
373
+ // Note: Branch and worktree deletion should be handled by clean command
374
+ // as it requires git operations
375
+ }
376
+
377
+ /**
378
+ * Calculate directory size in bytes
379
+ */
380
+ private getDirectorySize(dirPath: string): number {
381
+ let size = 0;
382
+
383
+ const files = fs.readdirSync(dirPath);
384
+ for (const file of files) {
385
+ const filePath = path.join(dirPath, file);
386
+ const stat = fs.statSync(filePath);
387
+
388
+ if (stat.isDirectory()) {
389
+ size += this.getDirectorySize(filePath);
390
+ } else {
391
+ size += stat.size;
392
+ }
393
+ }
394
+
395
+ return size;
396
+ }
397
+
398
+ /**
399
+ * Format duration for display
400
+ */
401
+ static formatDuration(ms: number): string {
402
+ const seconds = Math.floor(ms / 1000);
403
+ const minutes = Math.floor(seconds / 60);
404
+ const hours = Math.floor(minutes / 60);
405
+
406
+ if (hours > 0) {
407
+ return `${hours}h ${minutes % 60}m`;
408
+ } else if (minutes > 0) {
409
+ return `${minutes}m ${seconds % 60}s`;
410
+ } else {
411
+ return `${seconds}s`;
412
+ }
413
+ }
414
+
415
+ /**
416
+ * Format bytes for display
417
+ */
418
+ static formatBytes(bytes: number): string {
419
+ if (bytes < 1024) return `${bytes} B`;
420
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
421
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
422
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Create a RunService instance with default paths
428
+ */
429
+ export function createRunService(projectRoot?: string): RunService {
430
+ const root = projectRoot || process.cwd();
431
+ const logsDir = path.join(root, '_cursorflow', 'logs');
432
+ return new RunService(logsDir);
433
+ }