cursorflow 1.2.0__py3-none-any.whl
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.
- cursorflow/__init__.py +78 -0
- cursorflow/auto_updater.py +244 -0
- cursorflow/cli.py +408 -0
- cursorflow/core/agent.py +272 -0
- cursorflow/core/auth_handler.py +433 -0
- cursorflow/core/browser_controller.py +534 -0
- cursorflow/core/browser_engine.py +386 -0
- cursorflow/core/css_iterator.py +397 -0
- cursorflow/core/cursor_integration.py +744 -0
- cursorflow/core/cursorflow.py +649 -0
- cursorflow/core/error_correlator.py +322 -0
- cursorflow/core/event_correlator.py +182 -0
- cursorflow/core/file_change_monitor.py +548 -0
- cursorflow/core/log_collector.py +410 -0
- cursorflow/core/log_monitor.py +179 -0
- cursorflow/core/persistent_session.py +910 -0
- cursorflow/core/report_generator.py +282 -0
- cursorflow/log_sources/local_file.py +198 -0
- cursorflow/log_sources/ssh_remote.py +210 -0
- cursorflow/updater.py +512 -0
- cursorflow-1.2.0.dist-info/METADATA +444 -0
- cursorflow-1.2.0.dist-info/RECORD +25 -0
- cursorflow-1.2.0.dist-info/WHEEL +5 -0
- cursorflow-1.2.0.dist-info/entry_points.txt +2 -0
- cursorflow-1.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,397 @@
|
|
1
|
+
"""
|
2
|
+
CSS Iterator
|
3
|
+
|
4
|
+
Visual development support for rapid CSS iteration with instant feedback.
|
5
|
+
Captures visual states, applies CSS changes, and provides comparison data
|
6
|
+
for Cursor to analyze layout improvements.
|
7
|
+
"""
|
8
|
+
|
9
|
+
import asyncio
|
10
|
+
import time
|
11
|
+
import json
|
12
|
+
from typing import Dict, List, Optional, Any
|
13
|
+
from pathlib import Path
|
14
|
+
import logging
|
15
|
+
|
16
|
+
|
17
|
+
class CSSIterator:
|
18
|
+
"""
|
19
|
+
CSS iteration support - captures visual data for Cursor analysis
|
20
|
+
|
21
|
+
Provides visual comparison data without interpretation - Cursor decides
|
22
|
+
which CSS changes are improvements.
|
23
|
+
"""
|
24
|
+
|
25
|
+
def __init__(self):
|
26
|
+
"""Initialize CSS iterator"""
|
27
|
+
self.logger = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
# Create artifacts in current working directory (user's project)
|
30
|
+
self.artifacts_base = Path.cwd() / ".cursorflow" / "artifacts"
|
31
|
+
self.artifacts_base.mkdir(parents=True, exist_ok=True)
|
32
|
+
|
33
|
+
# Ensure subdirectories exist
|
34
|
+
(self.artifacts_base / "css_iterations").mkdir(exist_ok=True)
|
35
|
+
(self.artifacts_base / "screenshots").mkdir(exist_ok=True)
|
36
|
+
(self.artifacts_base / "sessions").mkdir(exist_ok=True)
|
37
|
+
|
38
|
+
async def capture_baseline(self, page) -> Dict[str, Any]:
|
39
|
+
"""
|
40
|
+
Capture baseline visual state before CSS changes
|
41
|
+
|
42
|
+
Args:
|
43
|
+
page: Playwright page object
|
44
|
+
|
45
|
+
Returns:
|
46
|
+
Baseline data for Cursor to compare against
|
47
|
+
"""
|
48
|
+
try:
|
49
|
+
timestamp = int(time.time())
|
50
|
+
baseline_name = f"baseline_{timestamp}"
|
51
|
+
|
52
|
+
# Capture screenshot
|
53
|
+
screenshot_path = str(self.artifacts_base / "css_iterations" / f"{baseline_name}.png")
|
54
|
+
await page.screenshot(path=screenshot_path, full_page=True)
|
55
|
+
|
56
|
+
# Capture layout metrics
|
57
|
+
layout_metrics = await self._capture_layout_metrics(page)
|
58
|
+
|
59
|
+
# Capture computed styles for key elements
|
60
|
+
computed_styles = await self._capture_computed_styles(page)
|
61
|
+
|
62
|
+
# Capture performance baseline
|
63
|
+
performance_metrics = await self._capture_performance_metrics(page)
|
64
|
+
|
65
|
+
baseline_data = {
|
66
|
+
"timestamp": time.time(),
|
67
|
+
"screenshot": screenshot_path,
|
68
|
+
"layout_metrics": layout_metrics,
|
69
|
+
"computed_styles": computed_styles,
|
70
|
+
"performance_metrics": performance_metrics,
|
71
|
+
"name": baseline_name
|
72
|
+
}
|
73
|
+
|
74
|
+
self.logger.info(f"Captured baseline: {baseline_name}")
|
75
|
+
return baseline_data
|
76
|
+
|
77
|
+
except Exception as e:
|
78
|
+
self.logger.error(f"Baseline capture failed: {e}")
|
79
|
+
return {"error": str(e)}
|
80
|
+
|
81
|
+
async def apply_css_and_capture(
|
82
|
+
self,
|
83
|
+
page,
|
84
|
+
css_change: Dict,
|
85
|
+
baseline: Dict,
|
86
|
+
suffix: str = ""
|
87
|
+
) -> Dict[str, Any]:
|
88
|
+
"""
|
89
|
+
Apply CSS change and capture resulting visual state
|
90
|
+
|
91
|
+
Args:
|
92
|
+
page: Playwright page object
|
93
|
+
css_change: {"name": "fix-name", "css": ".class { prop: value; }", "rationale": "why"}
|
94
|
+
baseline: Baseline data for comparison
|
95
|
+
suffix: Optional suffix for file naming (e.g., "_mobile")
|
96
|
+
|
97
|
+
Returns:
|
98
|
+
Iteration data for Cursor to analyze
|
99
|
+
"""
|
100
|
+
try:
|
101
|
+
iteration_name = css_change.get("name", "unnamed")
|
102
|
+
css_code = css_change.get("css", "")
|
103
|
+
|
104
|
+
if not css_code:
|
105
|
+
return {"error": "No CSS code provided"}
|
106
|
+
|
107
|
+
timestamp = int(time.time())
|
108
|
+
iteration_file_name = f"{iteration_name}_{timestamp}{suffix}"
|
109
|
+
|
110
|
+
# Apply CSS to page
|
111
|
+
await page.add_style_tag(content=css_code)
|
112
|
+
|
113
|
+
# Wait for layout to stabilize
|
114
|
+
await page.wait_for_timeout(200)
|
115
|
+
|
116
|
+
# Capture new visual state
|
117
|
+
screenshot_path = str(self.artifacts_base / "css_iterations" / f"{iteration_file_name}.png")
|
118
|
+
await page.screenshot(path=screenshot_path, full_page=True)
|
119
|
+
|
120
|
+
# Capture new layout metrics
|
121
|
+
new_layout_metrics = await self._capture_layout_metrics(page)
|
122
|
+
|
123
|
+
# Capture new computed styles
|
124
|
+
new_computed_styles = await self._capture_computed_styles(page)
|
125
|
+
|
126
|
+
# Capture performance impact
|
127
|
+
new_performance_metrics = await self._capture_performance_metrics(page)
|
128
|
+
|
129
|
+
# Capture any console errors introduced by CSS
|
130
|
+
console_errors = await self._capture_console_errors(page)
|
131
|
+
|
132
|
+
# Create comparison data (simple differences, no analysis)
|
133
|
+
layout_changes = self._calculate_layout_differences(
|
134
|
+
baseline.get("layout_metrics", {}),
|
135
|
+
new_layout_metrics
|
136
|
+
)
|
137
|
+
|
138
|
+
style_changes = self._calculate_style_differences(
|
139
|
+
baseline.get("computed_styles", {}),
|
140
|
+
new_computed_styles
|
141
|
+
)
|
142
|
+
|
143
|
+
iteration_data = {
|
144
|
+
"timestamp": time.time(),
|
145
|
+
"name": iteration_name,
|
146
|
+
"css_applied": css_code,
|
147
|
+
"rationale": css_change.get("rationale", ""),
|
148
|
+
"screenshot": screenshot_path,
|
149
|
+
"layout_metrics": new_layout_metrics,
|
150
|
+
"computed_styles": new_computed_styles,
|
151
|
+
"performance_metrics": new_performance_metrics,
|
152
|
+
"console_errors": console_errors,
|
153
|
+
"changes": {
|
154
|
+
"layout_differences": layout_changes,
|
155
|
+
"style_differences": style_changes
|
156
|
+
}
|
157
|
+
}
|
158
|
+
|
159
|
+
self.logger.info(f"Applied CSS iteration: {iteration_name}")
|
160
|
+
return iteration_data
|
161
|
+
|
162
|
+
except Exception as e:
|
163
|
+
self.logger.error(f"CSS iteration failed: {e}")
|
164
|
+
return {"error": str(e), "name": css_change.get("name", "unknown")}
|
165
|
+
|
166
|
+
async def _capture_layout_metrics(self, page) -> Dict[str, Any]:
|
167
|
+
"""Capture layout metrics for comparison"""
|
168
|
+
try:
|
169
|
+
metrics = await page.evaluate("""
|
170
|
+
() => {
|
171
|
+
const body = document.body;
|
172
|
+
const html = document.documentElement;
|
173
|
+
|
174
|
+
// Document dimensions
|
175
|
+
const documentHeight = Math.max(
|
176
|
+
body.scrollHeight, body.offsetHeight,
|
177
|
+
html.clientHeight, html.scrollHeight, html.offsetHeight
|
178
|
+
);
|
179
|
+
|
180
|
+
const documentWidth = Math.max(
|
181
|
+
body.scrollWidth, body.offsetWidth,
|
182
|
+
html.clientWidth, html.scrollWidth, html.offsetWidth
|
183
|
+
);
|
184
|
+
|
185
|
+
// Viewport info
|
186
|
+
const viewport = {
|
187
|
+
width: window.innerWidth,
|
188
|
+
height: window.innerHeight
|
189
|
+
};
|
190
|
+
|
191
|
+
// Scroll info
|
192
|
+
const scroll = {
|
193
|
+
x: window.pageXOffset,
|
194
|
+
y: window.pageYOffset,
|
195
|
+
maxX: documentWidth - viewport.width,
|
196
|
+
maxY: documentHeight - viewport.height
|
197
|
+
};
|
198
|
+
|
199
|
+
// Element positions for key elements
|
200
|
+
const elements = [];
|
201
|
+
const selectors = ['main', 'header', 'nav', 'aside', 'footer', '.container', '#content'];
|
202
|
+
|
203
|
+
selectors.forEach(selector => {
|
204
|
+
const el = document.querySelector(selector);
|
205
|
+
if (el) {
|
206
|
+
const rect = el.getBoundingClientRect();
|
207
|
+
elements.push({
|
208
|
+
selector: selector,
|
209
|
+
x: rect.x,
|
210
|
+
y: rect.y,
|
211
|
+
width: rect.width,
|
212
|
+
height: rect.height,
|
213
|
+
top: rect.top,
|
214
|
+
left: rect.left
|
215
|
+
});
|
216
|
+
}
|
217
|
+
});
|
218
|
+
|
219
|
+
return {
|
220
|
+
document: {
|
221
|
+
width: documentWidth,
|
222
|
+
height: documentHeight
|
223
|
+
},
|
224
|
+
viewport: viewport,
|
225
|
+
scroll: scroll,
|
226
|
+
elements: elements
|
227
|
+
};
|
228
|
+
}
|
229
|
+
""")
|
230
|
+
|
231
|
+
return metrics
|
232
|
+
|
233
|
+
except Exception as e:
|
234
|
+
self.logger.error(f"Layout metrics capture failed: {e}")
|
235
|
+
return {}
|
236
|
+
|
237
|
+
async def _capture_computed_styles(self, page) -> Dict[str, Any]:
|
238
|
+
"""Capture computed styles for key elements"""
|
239
|
+
try:
|
240
|
+
styles = await page.evaluate("""
|
241
|
+
() => {
|
242
|
+
const elements = {};
|
243
|
+
|
244
|
+
// Key selectors to monitor
|
245
|
+
const selectors = [
|
246
|
+
'body', 'main', 'header', 'nav', 'aside', 'footer',
|
247
|
+
'.container', '.content', '.sidebar', '.wrapper',
|
248
|
+
'#main', '#content', '#sidebar'
|
249
|
+
];
|
250
|
+
|
251
|
+
selectors.forEach(selector => {
|
252
|
+
const el = document.querySelector(selector);
|
253
|
+
if (el) {
|
254
|
+
const computed = window.getComputedStyle(el);
|
255
|
+
elements[selector] = {
|
256
|
+
display: computed.display,
|
257
|
+
position: computed.position,
|
258
|
+
flexDirection: computed.flexDirection,
|
259
|
+
justifyContent: computed.justifyContent,
|
260
|
+
alignItems: computed.alignItems,
|
261
|
+
gridTemplateColumns: computed.gridTemplateColumns,
|
262
|
+
gridTemplateRows: computed.gridTemplateRows,
|
263
|
+
width: computed.width,
|
264
|
+
height: computed.height,
|
265
|
+
margin: computed.margin,
|
266
|
+
padding: computed.padding,
|
267
|
+
backgroundColor: computed.backgroundColor,
|
268
|
+
color: computed.color,
|
269
|
+
fontSize: computed.fontSize,
|
270
|
+
lineHeight: computed.lineHeight
|
271
|
+
};
|
272
|
+
}
|
273
|
+
});
|
274
|
+
|
275
|
+
return elements;
|
276
|
+
}
|
277
|
+
""")
|
278
|
+
|
279
|
+
return styles
|
280
|
+
|
281
|
+
except Exception as e:
|
282
|
+
self.logger.error(f"Computed styles capture failed: {e}")
|
283
|
+
return {}
|
284
|
+
|
285
|
+
async def _capture_performance_metrics(self, page) -> Dict[str, Any]:
|
286
|
+
"""Capture performance metrics"""
|
287
|
+
try:
|
288
|
+
metrics = await page.evaluate("""
|
289
|
+
() => {
|
290
|
+
const perf = performance;
|
291
|
+
const navigation = perf.getEntriesByType('navigation')[0];
|
292
|
+
const paint = perf.getEntriesByType('paint');
|
293
|
+
|
294
|
+
return {
|
295
|
+
renderTime: navigation ? navigation.loadEventEnd - navigation.loadEventStart : 0,
|
296
|
+
domContentLoaded: navigation ? navigation.domContentLoadedEventEnd - navigation.navigationStart : 0,
|
297
|
+
firstPaint: paint.find(p => p.name === 'first-paint')?.startTime || 0,
|
298
|
+
firstContentfulPaint: paint.find(p => p.name === 'first-contentful-paint')?.startTime || 0,
|
299
|
+
layoutShifts: perf.getEntriesByType('layout-shift').length
|
300
|
+
};
|
301
|
+
}
|
302
|
+
""")
|
303
|
+
|
304
|
+
return metrics
|
305
|
+
|
306
|
+
except Exception as e:
|
307
|
+
self.logger.error(f"Performance metrics capture failed: {e}")
|
308
|
+
return {}
|
309
|
+
|
310
|
+
async def _capture_console_errors(self, page) -> List[Dict]:
|
311
|
+
"""Capture any console errors that occurred"""
|
312
|
+
# This would be captured by the browser controller's console monitoring
|
313
|
+
# For now, return empty - browser controller handles console errors
|
314
|
+
return []
|
315
|
+
|
316
|
+
def _calculate_layout_differences(self, baseline: Dict, current: Dict) -> Dict[str, Any]:
|
317
|
+
"""Calculate simple layout differences - NO ANALYSIS"""
|
318
|
+
differences = {}
|
319
|
+
|
320
|
+
# Document size changes
|
321
|
+
baseline_doc = baseline.get("document", {})
|
322
|
+
current_doc = current.get("document", {})
|
323
|
+
|
324
|
+
if baseline_doc and current_doc:
|
325
|
+
differences["document_size"] = {
|
326
|
+
"width_change": current_doc.get("width", 0) - baseline_doc.get("width", 0),
|
327
|
+
"height_change": current_doc.get("height", 0) - baseline_doc.get("height", 0)
|
328
|
+
}
|
329
|
+
|
330
|
+
# Element position changes
|
331
|
+
baseline_elements = {el["selector"]: el for el in baseline.get("elements", [])}
|
332
|
+
current_elements = {el["selector"]: el for el in current.get("elements", [])}
|
333
|
+
|
334
|
+
element_changes = {}
|
335
|
+
for selector in set(baseline_elements.keys()) | set(current_elements.keys()):
|
336
|
+
baseline_el = baseline_elements.get(selector, {})
|
337
|
+
current_el = current_elements.get(selector, {})
|
338
|
+
|
339
|
+
if baseline_el and current_el:
|
340
|
+
element_changes[selector] = {
|
341
|
+
"x_change": current_el.get("x", 0) - baseline_el.get("x", 0),
|
342
|
+
"y_change": current_el.get("y", 0) - baseline_el.get("y", 0),
|
343
|
+
"width_change": current_el.get("width", 0) - baseline_el.get("width", 0),
|
344
|
+
"height_change": current_el.get("height", 0) - baseline_el.get("height", 0)
|
345
|
+
}
|
346
|
+
|
347
|
+
differences["elements"] = element_changes
|
348
|
+
return differences
|
349
|
+
|
350
|
+
def _calculate_style_differences(self, baseline: Dict, current: Dict) -> Dict[str, Any]:
|
351
|
+
"""Calculate simple style differences - NO ANALYSIS"""
|
352
|
+
differences = {}
|
353
|
+
|
354
|
+
all_selectors = set(baseline.keys()) | set(current.keys())
|
355
|
+
|
356
|
+
for selector in all_selectors:
|
357
|
+
baseline_styles = baseline.get(selector, {})
|
358
|
+
current_styles = current.get(selector, {})
|
359
|
+
|
360
|
+
selector_changes = {}
|
361
|
+
all_properties = set(baseline_styles.keys()) | set(current_styles.keys())
|
362
|
+
|
363
|
+
for prop in all_properties:
|
364
|
+
baseline_value = baseline_styles.get(prop, "")
|
365
|
+
current_value = current_styles.get(prop, "")
|
366
|
+
|
367
|
+
if baseline_value != current_value:
|
368
|
+
selector_changes[prop] = {
|
369
|
+
"from": baseline_value,
|
370
|
+
"to": current_value
|
371
|
+
}
|
372
|
+
|
373
|
+
if selector_changes:
|
374
|
+
differences[selector] = selector_changes
|
375
|
+
|
376
|
+
return differences
|
377
|
+
|
378
|
+
def create_comparison_summary(self, baseline: Dict, iterations: List[Dict]) -> Dict[str, Any]:
|
379
|
+
"""Create simple comparison data for Cursor analysis"""
|
380
|
+
return {
|
381
|
+
"baseline": {
|
382
|
+
"screenshot": baseline.get("screenshot"),
|
383
|
+
"timestamp": baseline.get("timestamp")
|
384
|
+
},
|
385
|
+
"iterations": [
|
386
|
+
{
|
387
|
+
"name": iteration.get("name"),
|
388
|
+
"screenshot": iteration.get("screenshot"),
|
389
|
+
"css_applied": iteration.get("css_applied"),
|
390
|
+
"has_console_errors": len(iteration.get("console_errors", [])) > 0,
|
391
|
+
"layout_changed": bool(iteration.get("changes", {}).get("layout_differences", {})),
|
392
|
+
"styles_changed": bool(iteration.get("changes", {}).get("style_differences", {}))
|
393
|
+
}
|
394
|
+
for iteration in iterations
|
395
|
+
],
|
396
|
+
"total_iterations": len(iterations)
|
397
|
+
}
|