@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +25 -7
- package/commands/cursorflow-clean.md +19 -0
- package/commands/cursorflow-runs.md +59 -0
- package/commands/cursorflow-stop.md +55 -0
- package/dist/cli/clean.js +178 -6
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.js +8 -7
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +126 -77
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -0
- package/dist/cli/monitor.js +1021 -202
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +39 -21
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +268 -163
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +11 -5
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/runs.d.ts +5 -0
- package/dist/cli/runs.js +214 -0
- package/dist/cli/runs.js.map +1 -0
- package/dist/cli/setup-commands.js +0 -0
- package/dist/cli/signal.js +8 -8
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/stop.d.ts +5 -0
- package/dist/cli/stop.js +215 -0
- package/dist/cli/stop.js.map +1 -0
- package/dist/cli/tasks.d.ts +10 -0
- package/dist/cli/tasks.js +165 -0
- package/dist/cli/tasks.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +212 -0
- package/dist/core/auto-recovery.js +737 -0
- package/dist/core/auto-recovery.js.map +1 -0
- package/dist/core/failure-policy.d.ts +156 -0
- package/dist/core/failure-policy.js +488 -0
- package/dist/core/failure-policy.js.map +1 -0
- package/dist/core/orchestrator.d.ts +16 -2
- package/dist/core/orchestrator.js +439 -105
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +2 -0
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +33 -10
- package/dist/core/runner.js +374 -164
- package/dist/core/runner.js.map +1 -1
- package/dist/services/logging/buffer.d.ts +67 -0
- package/dist/services/logging/buffer.js +309 -0
- package/dist/services/logging/buffer.js.map +1 -0
- package/dist/services/logging/console.d.ts +89 -0
- package/dist/services/logging/console.js +169 -0
- package/dist/services/logging/console.js.map +1 -0
- package/dist/services/logging/file-writer.d.ts +71 -0
- package/dist/services/logging/file-writer.js +516 -0
- package/dist/services/logging/file-writer.js.map +1 -0
- package/dist/services/logging/formatter.d.ts +39 -0
- package/dist/services/logging/formatter.js +227 -0
- package/dist/services/logging/formatter.js.map +1 -0
- package/dist/services/logging/index.d.ts +11 -0
- package/dist/services/logging/index.js +30 -0
- package/dist/services/logging/index.js.map +1 -0
- package/dist/services/logging/parser.d.ts +31 -0
- package/dist/services/logging/parser.js +222 -0
- package/dist/services/logging/parser.js.map +1 -0
- package/dist/services/process/index.d.ts +59 -0
- package/dist/services/process/index.js +257 -0
- package/dist/services/process/index.js.map +1 -0
- package/dist/types/agent.d.ts +20 -0
- package/dist/types/agent.js +6 -0
- package/dist/types/agent.js.map +1 -0
- package/dist/types/config.d.ts +65 -0
- package/dist/types/config.js +6 -0
- package/dist/types/config.js.map +1 -0
- package/dist/types/events.d.ts +125 -0
- package/dist/types/events.js +6 -0
- package/dist/types/events.js.map +1 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/index.js +37 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/lane.d.ts +43 -0
- package/dist/types/lane.js +6 -0
- package/dist/types/lane.js.map +1 -0
- package/dist/types/logging.d.ts +71 -0
- package/dist/types/logging.js +16 -0
- package/dist/types/logging.js.map +1 -0
- package/dist/types/review.d.ts +17 -0
- package/dist/types/review.js +6 -0
- package/dist/types/review.js.map +1 -0
- package/dist/types/run.d.ts +32 -0
- package/dist/types/run.js +6 -0
- package/dist/types/run.js.map +1 -0
- package/dist/types/task.d.ts +71 -0
- package/dist/types/task.js +6 -0
- package/dist/types/task.js.map +1 -0
- package/dist/ui/components.d.ts +134 -0
- package/dist/ui/components.js +389 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/log-viewer.d.ts +49 -0
- package/dist/ui/log-viewer.js +449 -0
- package/dist/ui/log-viewer.js.map +1 -0
- package/dist/utils/checkpoint.d.ts +87 -0
- package/dist/utils/checkpoint.js +317 -0
- package/dist/utils/checkpoint.js.map +1 -0
- package/dist/utils/config.d.ts +4 -0
- package/dist/utils/config.js +18 -8
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/dependency.d.ts +74 -0
- package/dist/utils/dependency.js +420 -0
- package/dist/utils/dependency.js.map +1 -0
- package/dist/utils/doctor.js +17 -11
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +10 -33
- package/dist/utils/enhanced-logger.js +108 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +121 -0
- package/dist/utils/git.js +484 -11
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/health.d.ts +91 -0
- package/dist/utils/health.js +556 -0
- package/dist/utils/health.js.map +1 -0
- package/dist/utils/lock.d.ts +95 -0
- package/dist/utils/lock.js +332 -0
- package/dist/utils/lock.js.map +1 -0
- package/dist/utils/log-buffer.d.ts +17 -0
- package/dist/utils/log-buffer.js +14 -0
- package/dist/utils/log-buffer.js.map +1 -0
- package/dist/utils/log-constants.d.ts +23 -0
- package/dist/utils/log-constants.js +28 -0
- package/dist/utils/log-constants.js.map +1 -0
- package/dist/utils/log-formatter.d.ts +25 -0
- package/dist/utils/log-formatter.js +237 -0
- package/dist/utils/log-formatter.js.map +1 -0
- package/dist/utils/log-service.d.ts +19 -0
- package/dist/utils/log-service.js +47 -0
- package/dist/utils/log-service.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -27
- package/dist/utils/logger.js +82 -60
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/path.d.ts +19 -0
- package/dist/utils/path.js +77 -0
- package/dist/utils/path.js.map +1 -0
- package/dist/utils/process-manager.d.ts +21 -0
- package/dist/utils/process-manager.js +138 -0
- package/dist/utils/process-manager.js.map +1 -0
- package/dist/utils/retry.d.ts +121 -0
- package/dist/utils/retry.js +374 -0
- package/dist/utils/retry.js.map +1 -0
- package/dist/utils/run-service.d.ts +88 -0
- package/dist/utils/run-service.js +412 -0
- package/dist/utils/run-service.js.map +1 -0
- package/dist/utils/state.d.ts +62 -3
- package/dist/utils/state.js +317 -11
- package/dist/utils/state.js.map +1 -1
- package/dist/utils/task-service.d.ts +82 -0
- package/dist/utils/task-service.js +348 -0
- package/dist/utils/task-service.js.map +1 -0
- package/dist/utils/template.d.ts +14 -0
- package/dist/utils/template.js +122 -0
- package/dist/utils/template.js.map +1 -0
- package/dist/utils/types.d.ts +2 -271
- package/dist/utils/types.js +16 -0
- package/dist/utils/types.js.map +1 -1
- package/package.json +38 -23
- package/scripts/ai-security-check.js +0 -1
- package/scripts/local-security-gate.sh +0 -0
- package/scripts/monitor-lanes.sh +94 -0
- package/scripts/patches/test-cursor-agent.js +0 -1
- package/scripts/release.sh +0 -0
- package/scripts/setup-security.sh +0 -0
- package/scripts/stream-logs.sh +72 -0
- package/scripts/verify-and-fix.sh +0 -0
- package/src/cli/clean.ts +187 -6
- package/src/cli/index.ts +12 -1
- package/src/cli/init.ts +8 -7
- package/src/cli/logs.ts +124 -77
- package/src/cli/monitor.ts +1815 -898
- package/src/cli/prepare.ts +41 -21
- package/src/cli/resume.ts +753 -626
- package/src/cli/run.ts +12 -5
- package/src/cli/runs.ts +212 -0
- package/src/cli/setup-commands.ts +0 -0
- package/src/cli/signal.ts +8 -7
- package/src/cli/stop.ts +209 -0
- package/src/cli/tasks.ts +154 -0
- package/src/core/auto-recovery.ts +909 -0
- package/src/core/failure-policy.ts +592 -0
- package/src/core/orchestrator.ts +1131 -704
- package/src/core/reviewer.ts +4 -0
- package/src/core/runner.ts +444 -180
- package/src/services/logging/buffer.ts +326 -0
- package/src/services/logging/console.ts +193 -0
- package/src/services/logging/file-writer.ts +526 -0
- package/src/services/logging/formatter.ts +268 -0
- package/src/services/logging/index.ts +16 -0
- package/src/services/logging/parser.ts +232 -0
- package/src/services/process/index.ts +261 -0
- package/src/types/agent.ts +24 -0
- package/src/types/config.ts +79 -0
- package/src/types/events.ts +156 -0
- package/src/types/index.ts +29 -0
- package/src/types/lane.ts +56 -0
- package/src/types/logging.ts +96 -0
- package/src/types/review.ts +20 -0
- package/src/types/run.ts +37 -0
- package/src/types/task.ts +79 -0
- package/src/ui/components.ts +430 -0
- package/src/ui/log-viewer.ts +485 -0
- package/src/utils/checkpoint.ts +374 -0
- package/src/utils/config.ts +18 -8
- package/src/utils/cursor-agent.ts +1 -1
- package/src/utils/dependency.ts +482 -0
- package/src/utils/doctor.ts +18 -11
- package/src/utils/enhanced-logger.ts +122 -60
- package/src/utils/git.ts +517 -11
- package/src/utils/health.ts +596 -0
- package/src/utils/lock.ts +346 -0
- package/src/utils/log-buffer.ts +28 -0
- package/src/utils/log-constants.ts +26 -0
- package/src/utils/log-formatter.ts +245 -0
- package/src/utils/log-service.ts +49 -0
- package/src/utils/logger.ts +100 -51
- package/src/utils/path.ts +45 -0
- package/src/utils/process-manager.ts +100 -0
- package/src/utils/retry.ts +413 -0
- package/src/utils/run-service.ts +433 -0
- package/src/utils/state.ts +385 -11
- package/src/utils/task-service.ts +370 -0
- package/src/utils/template.ts +92 -0
- package/src/utils/types.ts +2 -314
- package/templates/basic.json +21 -0
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency management utilities for CursorFlow
|
|
3
|
+
*
|
|
4
|
+
* Features:
|
|
5
|
+
* - Cyclic dependency detection
|
|
6
|
+
* - Dependency wait with timeout
|
|
7
|
+
* - Topological sorting
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
import { safeJoin } from './path';
|
|
13
|
+
import { loadState } from './state';
|
|
14
|
+
import { LaneState } from './types';
|
|
15
|
+
import * as logger from './logger';
|
|
16
|
+
|
|
17
|
+
export interface DependencyInfo {
|
|
18
|
+
name: string;
|
|
19
|
+
dependsOn: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CycleDetectionResult {
|
|
23
|
+
hasCycle: boolean;
|
|
24
|
+
cycle: string[] | null;
|
|
25
|
+
sortedOrder: string[] | null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface DependencyWaitOptions {
|
|
29
|
+
/** Maximum time to wait in milliseconds */
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Polling interval in milliseconds */
|
|
32
|
+
pollIntervalMs?: number;
|
|
33
|
+
/** Callback when timeout is reached */
|
|
34
|
+
onTimeout?: 'fail' | 'warn' | 'continue';
|
|
35
|
+
/** Callback for progress updates */
|
|
36
|
+
onProgress?: (pending: string[], completed: string[]) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DEFAULT_DEPENDENCY_WAIT_OPTIONS: Required<Omit<DependencyWaitOptions, 'onProgress'>> = {
|
|
40
|
+
timeoutMs: 30 * 60 * 1000, // 30 minutes
|
|
41
|
+
pollIntervalMs: 5000, // 5 seconds
|
|
42
|
+
onTimeout: 'fail',
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface DependencyWaitResult {
|
|
46
|
+
success: boolean;
|
|
47
|
+
timedOut: boolean;
|
|
48
|
+
failedDependencies: string[];
|
|
49
|
+
completedDependencies: string[];
|
|
50
|
+
elapsedMs: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Detect cyclic dependencies in a list of lanes
|
|
55
|
+
*/
|
|
56
|
+
export function detectCyclicDependencies(lanes: DependencyInfo[]): CycleDetectionResult {
|
|
57
|
+
// Build adjacency graph
|
|
58
|
+
const graph = new Map<string, Set<string>>();
|
|
59
|
+
const allNodes = new Set<string>();
|
|
60
|
+
|
|
61
|
+
for (const lane of lanes) {
|
|
62
|
+
allNodes.add(lane.name);
|
|
63
|
+
graph.set(lane.name, new Set(lane.dependsOn));
|
|
64
|
+
|
|
65
|
+
// Add dependency nodes even if they're not in the list
|
|
66
|
+
for (const dep of lane.dependsOn) {
|
|
67
|
+
allNodes.add(dep);
|
|
68
|
+
if (!graph.has(dep)) {
|
|
69
|
+
graph.set(dep, new Set());
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Kahn's algorithm for topological sort with cycle detection
|
|
75
|
+
const inDegree = new Map<string, number>();
|
|
76
|
+
|
|
77
|
+
// Initialize in-degrees
|
|
78
|
+
for (const node of allNodes) {
|
|
79
|
+
inDegree.set(node, 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const [, deps] of graph) {
|
|
83
|
+
for (const dep of deps) {
|
|
84
|
+
inDegree.set(dep, (inDegree.get(dep) || 0) + 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Queue of nodes with no incoming edges
|
|
89
|
+
const queue: string[] = [];
|
|
90
|
+
for (const [node, degree] of inDegree) {
|
|
91
|
+
if (degree === 0) {
|
|
92
|
+
queue.push(node);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const sorted: string[] = [];
|
|
97
|
+
|
|
98
|
+
while (queue.length > 0) {
|
|
99
|
+
const node = queue.shift()!;
|
|
100
|
+
sorted.push(node);
|
|
101
|
+
|
|
102
|
+
const deps = graph.get(node) || new Set();
|
|
103
|
+
for (const dep of deps) {
|
|
104
|
+
const newDegree = (inDegree.get(dep) || 0) - 1;
|
|
105
|
+
inDegree.set(dep, newDegree);
|
|
106
|
+
|
|
107
|
+
if (newDegree === 0) {
|
|
108
|
+
queue.push(dep);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If not all nodes are in sorted order, there's a cycle
|
|
114
|
+
if (sorted.length !== allNodes.size) {
|
|
115
|
+
// Find the cycle using DFS
|
|
116
|
+
const cycle = findCycle(graph, allNodes);
|
|
117
|
+
return {
|
|
118
|
+
hasCycle: true,
|
|
119
|
+
cycle,
|
|
120
|
+
sortedOrder: null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
hasCycle: false,
|
|
126
|
+
cycle: null,
|
|
127
|
+
sortedOrder: sorted,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Find a cycle in the graph using DFS
|
|
133
|
+
*/
|
|
134
|
+
function findCycle(graph: Map<string, Set<string>>, allNodes: Set<string>): string[] | null {
|
|
135
|
+
const visited = new Set<string>();
|
|
136
|
+
const recursionStack = new Set<string>();
|
|
137
|
+
const parent = new Map<string, string>();
|
|
138
|
+
|
|
139
|
+
function dfs(node: string): string | null {
|
|
140
|
+
visited.add(node);
|
|
141
|
+
recursionStack.add(node);
|
|
142
|
+
|
|
143
|
+
const deps = graph.get(node) || new Set();
|
|
144
|
+
for (const dep of deps) {
|
|
145
|
+
if (!visited.has(dep)) {
|
|
146
|
+
parent.set(dep, node);
|
|
147
|
+
const cycleNode = dfs(dep);
|
|
148
|
+
if (cycleNode) return cycleNode;
|
|
149
|
+
} else if (recursionStack.has(dep)) {
|
|
150
|
+
// Found a cycle
|
|
151
|
+
parent.set(dep, node);
|
|
152
|
+
return dep;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
recursionStack.delete(node);
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
for (const node of allNodes) {
|
|
161
|
+
if (!visited.has(node)) {
|
|
162
|
+
const cycleNode = dfs(node);
|
|
163
|
+
if (cycleNode) {
|
|
164
|
+
// Reconstruct the cycle
|
|
165
|
+
const cycle: string[] = [cycleNode];
|
|
166
|
+
let current = parent.get(cycleNode);
|
|
167
|
+
while (current && current !== cycleNode) {
|
|
168
|
+
cycle.push(current);
|
|
169
|
+
current = parent.get(current);
|
|
170
|
+
}
|
|
171
|
+
cycle.push(cycleNode);
|
|
172
|
+
return cycle.reverse();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get topologically sorted order for lanes
|
|
182
|
+
*/
|
|
183
|
+
export function getExecutionOrder(lanes: DependencyInfo[]): string[] | null {
|
|
184
|
+
const result = detectCyclicDependencies(lanes);
|
|
185
|
+
|
|
186
|
+
if (result.hasCycle) {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Reverse the sorted order (we want dependencies first)
|
|
191
|
+
return result.sortedOrder!.reverse();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Wait for task-level dependencies with timeout and progress tracking
|
|
196
|
+
*/
|
|
197
|
+
export async function waitForTaskDependencies(
|
|
198
|
+
deps: string[],
|
|
199
|
+
lanesRoot: string,
|
|
200
|
+
options: DependencyWaitOptions = {}
|
|
201
|
+
): Promise<DependencyWaitResult> {
|
|
202
|
+
const opts = { ...DEFAULT_DEPENDENCY_WAIT_OPTIONS, ...options };
|
|
203
|
+
const startTime = Date.now();
|
|
204
|
+
const pendingDeps = new Set(deps);
|
|
205
|
+
const completedDeps: string[] = [];
|
|
206
|
+
const failedDeps: string[] = [];
|
|
207
|
+
|
|
208
|
+
if (deps.length === 0) {
|
|
209
|
+
return {
|
|
210
|
+
success: true,
|
|
211
|
+
timedOut: false,
|
|
212
|
+
failedDependencies: [],
|
|
213
|
+
completedDependencies: [],
|
|
214
|
+
elapsedMs: 0,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
logger.info(`Waiting for task dependencies: ${deps.join(', ')}`);
|
|
219
|
+
|
|
220
|
+
while (pendingDeps.size > 0) {
|
|
221
|
+
// Check timeout
|
|
222
|
+
const elapsed = Date.now() - startTime;
|
|
223
|
+
if (elapsed > opts.timeoutMs) {
|
|
224
|
+
logger.warn(`Dependency wait timeout after ${Math.round(elapsed / 1000)}s`);
|
|
225
|
+
|
|
226
|
+
if (opts.onTimeout === 'fail') {
|
|
227
|
+
return {
|
|
228
|
+
success: false,
|
|
229
|
+
timedOut: true,
|
|
230
|
+
failedDependencies: Array.from(pendingDeps),
|
|
231
|
+
completedDependencies: completedDeps,
|
|
232
|
+
elapsedMs: elapsed,
|
|
233
|
+
};
|
|
234
|
+
} else if (opts.onTimeout === 'warn') {
|
|
235
|
+
logger.warn('Continuing despite timeout');
|
|
236
|
+
return {
|
|
237
|
+
success: true,
|
|
238
|
+
timedOut: true,
|
|
239
|
+
failedDependencies: [],
|
|
240
|
+
completedDependencies: completedDeps,
|
|
241
|
+
elapsedMs: elapsed,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
// 'continue' - just return success
|
|
245
|
+
return {
|
|
246
|
+
success: true,
|
|
247
|
+
timedOut: true,
|
|
248
|
+
failedDependencies: [],
|
|
249
|
+
completedDependencies: completedDeps,
|
|
250
|
+
elapsedMs: elapsed,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
for (const dep of pendingDeps) {
|
|
255
|
+
const [laneName, taskName] = dep.split(':');
|
|
256
|
+
|
|
257
|
+
if (!laneName || !taskName) {
|
|
258
|
+
logger.warn(`Invalid dependency format: ${dep}. Expected "lane:task"`);
|
|
259
|
+
pendingDeps.delete(dep);
|
|
260
|
+
failedDeps.push(dep);
|
|
261
|
+
continue;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const depStatePath = safeJoin(lanesRoot, laneName, 'state.json');
|
|
265
|
+
|
|
266
|
+
if (!fs.existsSync(depStatePath)) {
|
|
267
|
+
// Lane hasn't started yet - continue waiting
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const state = loadState<LaneState>(depStatePath);
|
|
273
|
+
|
|
274
|
+
if (!state) {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Check if task is completed
|
|
279
|
+
if (state.completedTasks && state.completedTasks.includes(taskName)) {
|
|
280
|
+
logger.info(`✓ Dependency met: ${dep}`);
|
|
281
|
+
pendingDeps.delete(dep);
|
|
282
|
+
completedDeps.push(dep);
|
|
283
|
+
} else if (state.status === 'failed') {
|
|
284
|
+
// Dependency lane failed
|
|
285
|
+
logger.error(`✗ Dependency failed: ${dep} (Lane ${laneName} failed)`);
|
|
286
|
+
pendingDeps.delete(dep);
|
|
287
|
+
failedDeps.push(dep);
|
|
288
|
+
}
|
|
289
|
+
} catch (e: any) {
|
|
290
|
+
// Ignore parse errors, file might be being written
|
|
291
|
+
logger.warn(`Error reading dependency state: ${e.message}`);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Report progress
|
|
296
|
+
if (options.onProgress) {
|
|
297
|
+
options.onProgress(Array.from(pendingDeps), completedDeps);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Check for failed dependencies
|
|
301
|
+
if (failedDeps.length > 0) {
|
|
302
|
+
return {
|
|
303
|
+
success: false,
|
|
304
|
+
timedOut: false,
|
|
305
|
+
failedDependencies: failedDeps,
|
|
306
|
+
completedDependencies: completedDeps,
|
|
307
|
+
elapsedMs: Date.now() - startTime,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (pendingDeps.size > 0) {
|
|
312
|
+
await new Promise(resolve => setTimeout(resolve, opts.pollIntervalMs));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
success: true,
|
|
318
|
+
timedOut: false,
|
|
319
|
+
failedDependencies: [],
|
|
320
|
+
completedDependencies: completedDeps,
|
|
321
|
+
elapsedMs: Date.now() - startTime,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Check if a lane can start based on its dependencies
|
|
327
|
+
*/
|
|
328
|
+
export function canLaneStart(
|
|
329
|
+
laneName: string,
|
|
330
|
+
lanes: DependencyInfo[],
|
|
331
|
+
completedLanes: Set<string>,
|
|
332
|
+
failedLanes: Set<string>
|
|
333
|
+
): { canStart: boolean; reason?: string } {
|
|
334
|
+
const lane = lanes.find(l => l.name === laneName);
|
|
335
|
+
|
|
336
|
+
if (!lane) {
|
|
337
|
+
return { canStart: false, reason: `Lane ${laneName} not found` };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
for (const dep of lane.dependsOn) {
|
|
341
|
+
if (failedLanes.has(dep)) {
|
|
342
|
+
return {
|
|
343
|
+
canStart: false,
|
|
344
|
+
reason: `Dependency ${dep} has failed`
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!completedLanes.has(dep)) {
|
|
349
|
+
return {
|
|
350
|
+
canStart: false,
|
|
351
|
+
reason: `Waiting for dependency ${dep}`
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { canStart: true };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get all transitive dependencies for a lane
|
|
361
|
+
*/
|
|
362
|
+
export function getTransitiveDependencies(
|
|
363
|
+
laneName: string,
|
|
364
|
+
lanes: DependencyInfo[]
|
|
365
|
+
): string[] {
|
|
366
|
+
const laneMap = new Map(lanes.map(l => [l.name, l]));
|
|
367
|
+
const visited = new Set<string>();
|
|
368
|
+
const result: string[] = [];
|
|
369
|
+
|
|
370
|
+
function visit(name: string): void {
|
|
371
|
+
if (visited.has(name)) return;
|
|
372
|
+
visited.add(name);
|
|
373
|
+
|
|
374
|
+
const lane = laneMap.get(name);
|
|
375
|
+
if (!lane) return;
|
|
376
|
+
|
|
377
|
+
for (const dep of lane.dependsOn) {
|
|
378
|
+
visit(dep);
|
|
379
|
+
if (!result.includes(dep)) {
|
|
380
|
+
result.push(dep);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
visit(laneName);
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Get lanes that depend on a given lane
|
|
391
|
+
*/
|
|
392
|
+
export function getDependentLanes(
|
|
393
|
+
laneName: string,
|
|
394
|
+
lanes: DependencyInfo[]
|
|
395
|
+
): string[] {
|
|
396
|
+
return lanes
|
|
397
|
+
.filter(l => l.dependsOn.includes(laneName))
|
|
398
|
+
.map(l => l.name);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Validate dependency configuration
|
|
403
|
+
*/
|
|
404
|
+
export function validateDependencies(lanes: DependencyInfo[]): {
|
|
405
|
+
valid: boolean;
|
|
406
|
+
errors: string[];
|
|
407
|
+
warnings: string[];
|
|
408
|
+
} {
|
|
409
|
+
const errors: string[] = [];
|
|
410
|
+
const warnings: string[] = [];
|
|
411
|
+
const laneNames = new Set(lanes.map(l => l.name));
|
|
412
|
+
|
|
413
|
+
// Check for missing dependencies
|
|
414
|
+
for (const lane of lanes) {
|
|
415
|
+
for (const dep of lane.dependsOn) {
|
|
416
|
+
// Check if it's a task-level dependency
|
|
417
|
+
if (dep.includes(':')) {
|
|
418
|
+
const [depLane] = dep.split(':');
|
|
419
|
+
if (!laneNames.has(depLane!)) {
|
|
420
|
+
errors.push(`Lane "${lane.name}" depends on unknown lane "${depLane}"`);
|
|
421
|
+
}
|
|
422
|
+
} else if (!laneNames.has(dep)) {
|
|
423
|
+
errors.push(`Lane "${lane.name}" depends on unknown lane "${dep}"`);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Check for cycles
|
|
429
|
+
const cycleResult = detectCyclicDependencies(lanes);
|
|
430
|
+
if (cycleResult.hasCycle && cycleResult.cycle) {
|
|
431
|
+
errors.push(`Cyclic dependency detected: ${cycleResult.cycle.join(' -> ')}`);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Warning for deeply nested dependencies
|
|
435
|
+
for (const lane of lanes) {
|
|
436
|
+
const transitive = getTransitiveDependencies(lane.name, lanes);
|
|
437
|
+
if (transitive.length > 5) {
|
|
438
|
+
warnings.push(`Lane "${lane.name}" has ${transitive.length} transitive dependencies`);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
return {
|
|
443
|
+
valid: errors.length === 0,
|
|
444
|
+
errors,
|
|
445
|
+
warnings,
|
|
446
|
+
};
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Print dependency graph to console
|
|
451
|
+
*/
|
|
452
|
+
export function printDependencyGraph(lanes: DependencyInfo[]): void {
|
|
453
|
+
const cycleResult = detectCyclicDependencies(lanes);
|
|
454
|
+
|
|
455
|
+
logger.section('📊 Dependency Graph');
|
|
456
|
+
|
|
457
|
+
if (cycleResult.hasCycle) {
|
|
458
|
+
logger.error(`⚠️ Cyclic dependency detected: ${cycleResult.cycle?.join(' -> ')}`);
|
|
459
|
+
console.log('');
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
for (const lane of lanes) {
|
|
463
|
+
const deps = lane.dependsOn.length > 0
|
|
464
|
+
? ` [depends on: ${lane.dependsOn.join(', ')}]`
|
|
465
|
+
: '';
|
|
466
|
+
console.log(` ${logger.COLORS.cyan}${lane.name}${logger.COLORS.reset}${deps}`);
|
|
467
|
+
|
|
468
|
+
for (let i = 0; i < lane.dependsOn.length; i++) {
|
|
469
|
+
const isLast = i === lane.dependsOn.length - 1;
|
|
470
|
+
const prefix = isLast ? '└─' : '├─';
|
|
471
|
+
console.log(` ${prefix} ${lane.dependsOn[i]}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (cycleResult.sortedOrder) {
|
|
476
|
+
console.log('');
|
|
477
|
+
console.log(` Execution order: ${cycleResult.sortedOrder.reverse().join(' → ')}`);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
console.log('');
|
|
481
|
+
}
|
|
482
|
+
|
package/src/utils/doctor.ts
CHANGED
|
@@ -18,6 +18,7 @@ import * as path from 'path';
|
|
|
18
18
|
import * as git from './git';
|
|
19
19
|
import { checkCursorAgentInstalled, checkCursorAuth } from './cursor-agent';
|
|
20
20
|
import { areCommandsInstalled } from '../cli/setup-commands';
|
|
21
|
+
import { safeJoin } from './path';
|
|
21
22
|
|
|
22
23
|
export type DoctorSeverity = 'error' | 'warn';
|
|
23
24
|
|
|
@@ -73,9 +74,11 @@ function addIssue(issues: DoctorIssue[], issue: DoctorIssue): void {
|
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
function resolveRepoRoot(cwd: string): string | null {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
try {
|
|
78
|
+
return git.getMainRepoRoot(cwd);
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
function isInsideGitWorktree(cwd: string): boolean {
|
|
@@ -149,7 +152,7 @@ function readLaneJsonFiles(tasksDir: string): { path: string; json: any; fileNam
|
|
|
149
152
|
.readdirSync(tasksDir)
|
|
150
153
|
.filter(f => f.endsWith('.json'))
|
|
151
154
|
.sort()
|
|
152
|
-
.map(f =>
|
|
155
|
+
.map(f => safeJoin(tasksDir, f));
|
|
153
156
|
|
|
154
157
|
return files.map(p => {
|
|
155
158
|
const raw = fs.readFileSync(p, 'utf8');
|
|
@@ -160,8 +163,12 @@ function readLaneJsonFiles(tasksDir: string): { path: string; json: any; fileNam
|
|
|
160
163
|
|
|
161
164
|
function collectBaseBranchesFromLanes(lanes: { path: string; json: any }[], defaultBaseBranch: string): string[] {
|
|
162
165
|
const set = new Set<string>();
|
|
166
|
+
// CursorFlow now defaults to the current branch to maintain dependency structure
|
|
167
|
+
const currentBranch = git.getCurrentBranch();
|
|
168
|
+
set.add(currentBranch);
|
|
169
|
+
|
|
163
170
|
for (const lane of lanes) {
|
|
164
|
-
const baseBranch = String(lane.json?.baseBranch || defaultBaseBranch ||
|
|
171
|
+
const baseBranch = String(lane.json?.baseBranch || defaultBaseBranch || currentBranch).trim();
|
|
165
172
|
if (baseBranch) set.add(baseBranch);
|
|
166
173
|
}
|
|
167
174
|
return Array.from(set);
|
|
@@ -428,7 +435,7 @@ function checkDiskSpace(dir: string): { ok: boolean; freeBytes?: number; error?:
|
|
|
428
435
|
const { spawnSync } = require('child_process');
|
|
429
436
|
try {
|
|
430
437
|
// Validate and normalize the directory path to prevent command injection
|
|
431
|
-
const safePath = path.resolve(dir);
|
|
438
|
+
const safePath = path.resolve(dir); // nosemgrep
|
|
432
439
|
|
|
433
440
|
// Use spawnSync instead of execSync to avoid shell interpolation vulnerabilities
|
|
434
441
|
// df -B1 returns bytes. We look for the line corresponding to our directory.
|
|
@@ -612,7 +619,7 @@ function validateBranchNames(
|
|
|
612
619
|
const DOCTOR_STATUS_FILE = '.cursorflow/doctor-status.json';
|
|
613
620
|
|
|
614
621
|
export function saveDoctorStatus(repoRoot: string, report: DoctorReport): void {
|
|
615
|
-
const statusPath =
|
|
622
|
+
const statusPath = safeJoin(repoRoot, DOCTOR_STATUS_FILE);
|
|
616
623
|
const statusDir = path.dirname(statusPath);
|
|
617
624
|
|
|
618
625
|
if (!fs.existsSync(statusDir)) {
|
|
@@ -630,7 +637,7 @@ export function saveDoctorStatus(repoRoot: string, report: DoctorReport): void {
|
|
|
630
637
|
}
|
|
631
638
|
|
|
632
639
|
export function getDoctorStatus(repoRoot: string): { lastRun: number; ok: boolean; issueCount: number } | null {
|
|
633
|
-
const statusPath =
|
|
640
|
+
const statusPath = safeJoin(repoRoot, DOCTOR_STATUS_FILE);
|
|
634
641
|
if (!fs.existsSync(statusPath)) return null;
|
|
635
642
|
|
|
636
643
|
try {
|
|
@@ -830,7 +837,7 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
|
|
|
830
837
|
});
|
|
831
838
|
} else {
|
|
832
839
|
// Advanced check: .gitignore check for worktrees
|
|
833
|
-
const gitignorePath =
|
|
840
|
+
const gitignorePath = safeJoin(gitCwd, '.gitignore');
|
|
834
841
|
const worktreeDirName = '_cursorflow'; // Default directory name
|
|
835
842
|
if (fs.existsSync(gitignorePath)) {
|
|
836
843
|
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
@@ -853,7 +860,7 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
|
|
|
853
860
|
if (options.tasksDir) {
|
|
854
861
|
const tasksDirAbs = path.isAbsolute(options.tasksDir)
|
|
855
862
|
? options.tasksDir
|
|
856
|
-
:
|
|
863
|
+
: safeJoin(cwd, options.tasksDir);
|
|
857
864
|
context.tasksDir = tasksDirAbs;
|
|
858
865
|
|
|
859
866
|
if (!fs.existsSync(tasksDirAbs)) {
|
|
@@ -893,7 +900,7 @@ export function runDoctor(options: DoctorOptions = {}): DoctorReport {
|
|
|
893
900
|
});
|
|
894
901
|
} else {
|
|
895
902
|
// Validate base branches
|
|
896
|
-
const baseBranches = collectBaseBranchesFromLanes(lanes,
|
|
903
|
+
const baseBranches = collectBaseBranchesFromLanes(lanes, git.getCurrentBranch());
|
|
897
904
|
for (const baseBranch of baseBranches) {
|
|
898
905
|
if (!branchExists(gitCwd, baseBranch)) {
|
|
899
906
|
addIssue(issues, {
|