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.
@@ -0,0 +1,649 @@
1
+ """
2
+ CursorFlow - Main API Class
3
+
4
+ Simple, fast data collection engine that enables Cursor to autonomously test UI
5
+ and iterate on designs with immediate visual feedback.
6
+
7
+ Design Philosophy: Declarative Actions | Batch Execution | Universal Correlation
8
+ """
9
+
10
+ import asyncio
11
+ import time
12
+ import logging
13
+ from typing import Dict, List, Optional, Any
14
+ from pathlib import Path
15
+
16
+ from .browser_controller import BrowserController
17
+ from .log_collector import LogCollector
18
+ from .event_correlator import EventCorrelator
19
+ from .auth_handler import AuthHandler
20
+ from .css_iterator import CSSIterator
21
+ from .cursor_integration import CursorIntegration
22
+ from .persistent_session import PersistentSession, get_session_manager
23
+
24
+
25
+ class CursorFlow:
26
+ """
27
+ Main CursorFlow interface - Simple data collection for Cursor analysis
28
+
29
+ Usage:
30
+ flow = CursorFlow("http://localhost:3000", {"source": "local", "paths": ["logs/app.log"]})
31
+
32
+ # Test UI flow
33
+ results = await flow.execute_and_collect([
34
+ {"navigate": "/dashboard"},
35
+ {"click": "#refresh"},
36
+ {"screenshot": "refreshed"}
37
+ ])
38
+
39
+ # CSS iteration
40
+ visual_results = await flow.css_iteration_session(
41
+ base_actions=[{"navigate": "/page"}],
42
+ css_changes=[{"name": "fix", "css": ".item { margin: 1rem; }"}]
43
+ )
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ base_url: str,
49
+ log_config: Dict,
50
+ auth_config: Optional[Dict] = None,
51
+ browser_config: Optional[Dict] = None
52
+ ):
53
+ """
54
+ Initialize CursorFlow with environment configuration
55
+
56
+ Args:
57
+ base_url: "http://localhost:3000" or "https://staging.example.com"
58
+ log_config: {"source": "ssh|local|docker", "host": "...", "paths": [...]}
59
+ auth_config: {"method": "form", "username_selector": "#user", ...}
60
+ browser_config: {"headless": True, "debug_mode": False}
61
+ """
62
+ self.base_url = base_url
63
+ self.log_config = log_config
64
+ self.auth_config = auth_config or {}
65
+ self.browser_config = browser_config or {"headless": True}
66
+
67
+ # Initialize core components
68
+ self.browser = BrowserController(base_url, self.browser_config)
69
+ self.log_collector = LogCollector(log_config)
70
+ self.correlator = EventCorrelator()
71
+ self.auth_handler = AuthHandler(auth_config) if auth_config else None
72
+ self.css_iterator = CSSIterator()
73
+ self.cursor_integration = CursorIntegration()
74
+
75
+ # Session tracking
76
+ self.session_id = None
77
+ self.timeline = []
78
+ self.artifacts = {"screenshots": [], "console_logs": [], "server_logs": []}
79
+
80
+ # Persistent session support for hot reload
81
+ self.persistent_session: Optional[PersistentSession] = None
82
+ self.session_manager = get_session_manager()
83
+
84
+ self.logger = logging.getLogger(__name__)
85
+
86
+ # Check for updates on initialization (background task)
87
+ self._check_for_updates_async()
88
+
89
+ async def execute_and_collect(
90
+ self,
91
+ actions: List[Dict],
92
+ session_options: Optional[Dict] = None
93
+ ) -> Dict[str, Any]:
94
+ """
95
+ Execute UI actions and collect all correlated data
96
+
97
+ Args:
98
+ actions: [
99
+ {"navigate": "/dashboard"},
100
+ {"click": "#refresh-button"},
101
+ {"screenshot": "after-click"}
102
+ ]
103
+ session_options: {
104
+ "reuse_session": True,
105
+ "save_session": True,
106
+ "fresh_session": False
107
+ }
108
+
109
+ Returns:
110
+ {
111
+ "success": bool,
112
+ "session_id": str,
113
+ "timeline": [{"time": timestamp, "type": "browser|server", "event": "...", ...}],
114
+ "correlations": [{"browser_event": "...", "server_event": "...", "confidence": 0.95}],
115
+ "artifacts": {
116
+ "screenshots": ["before.png", "after.png"],
117
+ "console_logs": [...],
118
+ "server_logs": [...]
119
+ }
120
+ }
121
+ """
122
+ session_options = session_options or {}
123
+ start_time = time.time()
124
+
125
+ try:
126
+ # Initialize session
127
+ await self._initialize_session(session_options)
128
+
129
+ # Start monitoring
130
+ await self.log_collector.start_monitoring()
131
+
132
+ # Execute actions
133
+ success = await self._execute_actions(actions)
134
+
135
+ # Stop monitoring and collect data
136
+ server_logs = await self.log_collector.stop_monitoring()
137
+
138
+ # Organize timeline (NO analysis - just data organization)
139
+ organized_timeline = self.correlator.organize_timeline(
140
+ self.timeline, server_logs
141
+ )
142
+ summary = self.correlator.get_summary(organized_timeline)
143
+
144
+ # Package results
145
+ results = {
146
+ "success": success,
147
+ "session_id": self.session_id,
148
+ "execution_time": time.time() - start_time,
149
+ "timeline": organized_timeline, # Organized chronological data
150
+ "browser_events": self.timeline, # Raw browser events
151
+ "server_logs": server_logs, # Raw server logs
152
+ "summary": summary, # Basic counts
153
+ "artifacts": self.artifacts
154
+ }
155
+
156
+ self.logger.info(f"Test execution completed: {success}, timeline events: {len(organized_timeline)}")
157
+ return results
158
+
159
+ except Exception as e:
160
+ self.logger.error(f"Test execution failed: {e}")
161
+ return {
162
+ "success": False,
163
+ "error": str(e),
164
+ "timeline": self.timeline,
165
+ "artifacts": self.artifacts
166
+ }
167
+ finally:
168
+ await self._cleanup_session(session_options)
169
+
170
+ async def css_iteration_session(
171
+ self,
172
+ base_actions: List[Dict],
173
+ css_changes: List[Dict],
174
+ viewport_configs: Optional[List[Dict]] = None
175
+ ) -> Dict[str, Any]:
176
+ """
177
+ Rapid CSS iteration with visual feedback
178
+
179
+ Args:
180
+ base_actions: [
181
+ {"navigate": "/page"},
182
+ {"wait_for": "#main-content"},
183
+ {"screenshot": "baseline"}
184
+ ]
185
+ css_changes: [
186
+ {
187
+ "name": "flex-spacing-fix",
188
+ "css": ".container { display: flex; gap: 1rem; }",
189
+ "rationale": "Fix spacing between items"
190
+ }
191
+ ]
192
+ viewport_configs: [
193
+ {"width": 1440, "height": 900, "name": "desktop"},
194
+ {"width": 768, "height": 1024, "name": "tablet"}
195
+ ]
196
+
197
+ Returns:
198
+ {
199
+ "baseline": {
200
+ "screenshot": "baseline.png",
201
+ "computed_styles": {...},
202
+ "layout_metrics": {...}
203
+ },
204
+ "iterations": [
205
+ {
206
+ "name": "flex-spacing-fix",
207
+ "screenshot": "iteration_1.png",
208
+ "diff_image": "diff_1.png",
209
+ "layout_changes": [...],
210
+ "console_errors": [...],
211
+ "performance_impact": {...}
212
+ }
213
+ ]
214
+ }
215
+ """
216
+ try:
217
+ # Initialize for CSS iteration
218
+ await self.browser.initialize()
219
+
220
+ # Execute base actions and capture baseline
221
+ await self._execute_actions(base_actions)
222
+ baseline = await self.css_iterator.capture_baseline(self.browser.page)
223
+
224
+ # Iterate through CSS changes
225
+ iterations = []
226
+ for i, css_change in enumerate(css_changes):
227
+ iteration_result = await self.css_iterator.apply_css_and_capture(
228
+ self.browser.page, css_change, baseline
229
+ )
230
+ iterations.append(iteration_result)
231
+
232
+ # Test across viewports if specified
233
+ if viewport_configs:
234
+ for viewport in viewport_configs:
235
+ await self.browser.set_viewport(viewport["width"], viewport["height"])
236
+ viewport_baseline = await self.css_iterator.capture_baseline(self.browser.page)
237
+
238
+ for css_change in css_changes:
239
+ viewport_iteration = await self.css_iterator.apply_css_and_capture(
240
+ self.browser.page, css_change, viewport_baseline,
241
+ suffix=f"_{viewport['name']}"
242
+ )
243
+ iterations.append(viewport_iteration)
244
+
245
+ # Create raw results
246
+ raw_results = {
247
+ "baseline": baseline,
248
+ "iterations": iterations,
249
+ "summary": {
250
+ "total_changes": len(css_changes),
251
+ "viewports_tested": len(viewport_configs) if viewport_configs else 1,
252
+ "recommended_iteration": self._recommend_best_iteration(iterations)
253
+ }
254
+ }
255
+
256
+ # Format for Cursor with session management and actionable insights
257
+ cursor_results = self.cursor_integration.format_css_iteration_results(
258
+ raw_results=raw_results,
259
+ session_id=self.session_id,
260
+ project_context={
261
+ "framework": "auto-detected", # Could be enhanced with real detection
262
+ "base_url": self.base_url,
263
+ "test_type": "css_iteration"
264
+ }
265
+ )
266
+
267
+ return cursor_results
268
+
269
+ except Exception as e:
270
+ self.logger.error(f"CSS iteration failed: {e}")
271
+ return {"success": False, "error": str(e)}
272
+ finally:
273
+ await self.browser.cleanup()
274
+
275
+ async def css_iteration_persistent(
276
+ self,
277
+ base_actions: List[Dict],
278
+ css_changes: List[Dict],
279
+ session_options: Optional[Dict] = None
280
+ ) -> Dict[str, Any]:
281
+ """
282
+ CSS iteration using persistent sessions for hot reload environments
283
+
284
+ Maintains browser state between iterations, taking advantage of hot reload
285
+ capabilities for faster CSS iteration cycles.
286
+
287
+ Args:
288
+ base_actions: Initial actions to set up the page
289
+ css_changes: List of CSS changes to test
290
+ session_options: {
291
+ "session_id": "optional-custom-id",
292
+ "reuse_session": True,
293
+ "hot_reload": True,
294
+ "keep_session_alive": True
295
+ }
296
+
297
+ Returns:
298
+ Enhanced results with session information and hot reload data
299
+ """
300
+ session_options = session_options or {}
301
+ start_time = time.time()
302
+
303
+ try:
304
+ # Get or create persistent session
305
+ session_id = session_options.get("session_id", f"css_session_{int(time.time())}")
306
+ self.persistent_session = await self.session_manager.get_or_create_session(
307
+ session_id=session_id,
308
+ base_url=self.base_url,
309
+ config={
310
+ **self.browser_config,
311
+ "hot_reload_enabled": session_options.get("hot_reload", True),
312
+ "keep_alive": session_options.get("keep_session_alive", True)
313
+ }
314
+ )
315
+
316
+ # Initialize persistent session
317
+ session_initialized = await self.persistent_session.initialize()
318
+ if not session_initialized:
319
+ return {"success": False, "error": "Failed to initialize persistent session"}
320
+
321
+ # Execute base actions if this is a new session or explicitly requested
322
+ if (not session_options.get("reuse_session", True) or
323
+ not self.persistent_session.baseline_captured):
324
+
325
+ self.logger.info("Executing base actions for CSS iteration setup")
326
+ await self._execute_persistent_actions(base_actions)
327
+ self.persistent_session.baseline_captured = True
328
+
329
+ # Capture baseline state
330
+ baseline = await self._capture_persistent_baseline()
331
+
332
+ # Perform CSS iterations with persistent session
333
+ iterations = []
334
+
335
+ for i, css_change in enumerate(css_changes):
336
+ self.logger.info(f"Applying CSS iteration {i+1}/{len(css_changes)}: {css_change.get('name', 'unnamed')}")
337
+
338
+ # Apply CSS using persistent session (with hot reload when available)
339
+ iteration_result = await self.persistent_session.apply_css_persistent(
340
+ css=css_change.get("css", ""),
341
+ name=css_change.get("name", f"iteration_{i+1}"),
342
+ replace_previous=css_change.get("replace_previous", False)
343
+ )
344
+
345
+ # Enhance with CursorFlow iteration data
346
+ if iteration_result.get("success"):
347
+ enhanced_result = await self._enhance_iteration_result(
348
+ iteration_result, css_change, baseline
349
+ )
350
+ iterations.append(enhanced_result)
351
+ else:
352
+ iterations.append(iteration_result)
353
+
354
+ # Small delay to let changes settle
355
+ await asyncio.sleep(0.1)
356
+
357
+ # Get session information
358
+ session_info = await self.persistent_session.get_session_info()
359
+
360
+ # Create results
361
+ results = {
362
+ "success": True,
363
+ "session_id": session_id,
364
+ "execution_time": time.time() - start_time,
365
+ "baseline": baseline,
366
+ "iterations": iterations,
367
+ "session_info": session_info,
368
+ "hot_reload_used": session_info.get("hot_reload_available", False),
369
+ "total_iterations": len(iterations),
370
+ "summary": {
371
+ "successful_iterations": len([i for i in iterations if i.get("success", False)]),
372
+ "failed_iterations": len([i for i in iterations if not i.get("success", True)]),
373
+ "hot_reload_available": session_info.get("hot_reload_available", False),
374
+ "session_reused": session_options.get("reuse_session", True),
375
+ "recommended_iteration": self._recommend_best_iteration(iterations)
376
+ }
377
+ }
378
+
379
+ # Format for Cursor integration
380
+ cursor_results = self.cursor_integration.format_persistent_css_results(
381
+ results,
382
+ {"framework": "auto-detected", "hot_reload": True}
383
+ )
384
+
385
+ # Keep session alive if requested
386
+ if not session_options.get("keep_session_alive", True):
387
+ await self.persistent_session.cleanup(save_state=True)
388
+ self.persistent_session = None
389
+
390
+ return cursor_results
391
+
392
+ except Exception as e:
393
+ self.logger.error(f"Persistent CSS iteration failed: {e}")
394
+ return {
395
+ "success": False,
396
+ "error": str(e),
397
+ "session_id": session_id if 'session_id' in locals() else None
398
+ }
399
+
400
+ async def _execute_persistent_actions(self, actions: List[Dict]) -> bool:
401
+ """Execute actions using persistent session"""
402
+ try:
403
+ for action in actions:
404
+ if "navigate" in action:
405
+ path = action["navigate"]
406
+ if isinstance(path, dict):
407
+ path = path["url"]
408
+ await self.persistent_session.navigate_persistent(path)
409
+
410
+ elif "wait_for" in action:
411
+ selector = action["wait_for"]
412
+ if isinstance(selector, dict):
413
+ selector = selector["selector"]
414
+ await self.persistent_session.browser.wait_for_element(selector)
415
+
416
+ elif "screenshot" in action:
417
+ name = action["screenshot"]
418
+ await self.persistent_session.browser.screenshot(name)
419
+
420
+ # Add other actions as needed
421
+ return True
422
+
423
+ except Exception as e:
424
+ self.logger.error(f"Persistent action execution failed: {e}")
425
+ return False
426
+
427
+ async def _capture_persistent_baseline(self) -> Dict[str, Any]:
428
+ """Capture baseline using persistent session"""
429
+ if not self.persistent_session or not self.persistent_session.browser:
430
+ return {}
431
+
432
+ try:
433
+ # Use CSS iterator for baseline capture
434
+ baseline = await self.css_iterator.capture_baseline(self.persistent_session.browser.page)
435
+
436
+ # Enhance with persistent session data
437
+ session_state = await self.persistent_session._capture_session_state("baseline")
438
+ baseline.update({
439
+ "session_state": session_state,
440
+ "hot_reload_detected": await self.persistent_session._check_hot_reload_capability(),
441
+ "iteration_context": {
442
+ "session_id": self.persistent_session.session_id,
443
+ "previous_iterations": self.persistent_session.iteration_count
444
+ }
445
+ })
446
+
447
+ return baseline
448
+
449
+ except Exception as e:
450
+ self.logger.error(f"Persistent baseline capture failed: {e}")
451
+ return {"error": str(e)}
452
+
453
+ async def _enhance_iteration_result(
454
+ self,
455
+ iteration_result: Dict,
456
+ css_change: Dict,
457
+ baseline: Dict
458
+ ) -> Dict[str, Any]:
459
+ """Enhance iteration result with CursorFlow analysis data"""
460
+ try:
461
+ enhanced = iteration_result.copy()
462
+
463
+ # Add CSS iterator analysis
464
+ if self.persistent_session and self.persistent_session.browser:
465
+ css_analysis = await self.css_iterator.apply_css_and_capture(
466
+ page=self.persistent_session.browser.page,
467
+ css_change=css_change,
468
+ baseline=baseline,
469
+ suffix="_persistent"
470
+ )
471
+
472
+ # Merge analysis data
473
+ enhanced.update({
474
+ "css_analysis": css_analysis,
475
+ "visual_comparison": css_analysis.get("changes", {}),
476
+ "performance_impact": css_analysis.get("performance_metrics", {}),
477
+ "console_errors": css_analysis.get("console_errors", [])
478
+ })
479
+
480
+ # Add persistent session context
481
+ enhanced.update({
482
+ "iteration_method": iteration_result.get("method", "standard"),
483
+ "hot_reload_used": iteration_result.get("method") == "hot_reload",
484
+ "session_persistent": True
485
+ })
486
+
487
+ return enhanced
488
+
489
+ except Exception as e:
490
+ self.logger.error(f"Failed to enhance iteration result: {e}")
491
+ return iteration_result
492
+
493
+ async def get_persistent_session_info(self) -> Optional[Dict[str, Any]]:
494
+ """Get information about current persistent session"""
495
+ if self.persistent_session:
496
+ return await self.persistent_session.get_session_info()
497
+ return None
498
+
499
+ async def cleanup_persistent_session(self, save_state: bool = True):
500
+ """Clean up current persistent session"""
501
+ if self.persistent_session:
502
+ await self.persistent_session.cleanup(save_state=save_state)
503
+ self.persistent_session = None
504
+
505
+ async def _initialize_session(self, session_options: Dict):
506
+ """Initialize browser and authentication session"""
507
+ self.session_id = f"session_{int(time.time())}"
508
+
509
+ # Initialize browser
510
+ await self.browser.initialize()
511
+
512
+ # Handle authentication
513
+ if self.auth_handler and not session_options.get("skip_auth", False):
514
+ await self.auth_handler.authenticate(
515
+ self.browser.page,
516
+ session_options
517
+ )
518
+
519
+ async def _execute_actions(self, actions: List[Dict]) -> bool:
520
+ """Execute list of declarative actions"""
521
+ try:
522
+ for action in actions:
523
+ await self._execute_single_action(action)
524
+ return True
525
+ except Exception as e:
526
+ self.logger.error(f"Action execution failed: {e}")
527
+ return False
528
+
529
+ async def _execute_single_action(self, action: Dict):
530
+ """Execute a single declarative action and track it"""
531
+ action_start = time.time()
532
+
533
+ try:
534
+ # Navigation actions
535
+ if "navigate" in action:
536
+ url = action["navigate"]
537
+ if isinstance(url, dict):
538
+ url = url["url"]
539
+ await self.browser.navigate(url)
540
+
541
+ # Interaction actions
542
+ elif "click" in action:
543
+ selector = action["click"]
544
+ if isinstance(selector, dict):
545
+ selector = selector["selector"]
546
+ await self.browser.click(selector)
547
+
548
+ elif "fill" in action:
549
+ config = action["fill"]
550
+ await self.browser.fill(config["selector"], config["value"])
551
+
552
+ elif "type" in action:
553
+ config = action["type"]
554
+ await self.browser.type(config["selector"], config["text"])
555
+
556
+ # Waiting actions
557
+ elif "wait" in action:
558
+ await asyncio.sleep(action["wait"])
559
+
560
+ elif "wait_for" in action:
561
+ selector = action["wait_for"]
562
+ if isinstance(selector, dict):
563
+ selector = selector["selector"]
564
+ await self.browser.wait_for_element(selector)
565
+
566
+ # Capture actions
567
+ elif "screenshot" in action:
568
+ name = action["screenshot"]
569
+ screenshot_path = await self.browser.screenshot(name)
570
+ self.artifacts["screenshots"].append(screenshot_path)
571
+
572
+ elif "authenticate" in action:
573
+ if self.auth_handler:
574
+ await self.auth_handler.authenticate(self.browser.page, action["authenticate"])
575
+
576
+ # Record action in timeline
577
+ self.timeline.append({
578
+ "timestamp": action_start,
579
+ "type": "browser",
580
+ "event": list(action.keys())[0],
581
+ "data": action,
582
+ "duration": time.time() - action_start
583
+ })
584
+
585
+ except Exception as e:
586
+ self.logger.error(f"Action failed: {action}, error: {e}")
587
+ raise
588
+
589
+ async def _cleanup_session(self, session_options: Dict):
590
+ """Clean up browser session"""
591
+ try:
592
+ # Save session if requested
593
+ if session_options.get("save_session", False) and self.auth_handler:
594
+ await self.auth_handler.save_session(self.browser.page, self.session_id)
595
+
596
+ # Cleanup browser
597
+ await self.browser.cleanup()
598
+
599
+ except Exception as e:
600
+ self.logger.error(f"Session cleanup failed: {e}")
601
+
602
+ def _recommend_best_iteration(self, iterations: List[Dict]) -> Optional[str]:
603
+ """Recommend best CSS iteration based on metrics"""
604
+ if not iterations:
605
+ return None
606
+
607
+ # Simple scoring based on lack of console errors and performance
608
+ best_iteration = None
609
+ best_score = -1
610
+
611
+ for iteration in iterations:
612
+ score = 0
613
+
614
+ # Penalty for console errors
615
+ if not iteration.get("console_errors", []):
616
+ score += 50
617
+
618
+ # Bonus for good performance
619
+ perf = iteration.get("performance_impact", {})
620
+ if perf.get("render_time", 1000) < 100:
621
+ score += 30
622
+
623
+ if score > best_score:
624
+ best_score = score
625
+ best_iteration = iteration.get("name")
626
+
627
+ return best_iteration
628
+
629
+ def _check_for_updates_async(self):
630
+ """Check for updates in background (non-blocking)"""
631
+ try:
632
+ import asyncio
633
+ from ..auto_updater import check_for_updates_on_startup
634
+
635
+ # Try to run update check in background
636
+ try:
637
+ loop = asyncio.get_event_loop()
638
+ if loop.is_running():
639
+ # Schedule as background task
640
+ loop.create_task(check_for_updates_on_startup(str(Path.cwd())))
641
+ else:
642
+ # Create new loop for quick check
643
+ asyncio.run(check_for_updates_on_startup(str(Path.cwd())))
644
+ except Exception:
645
+ # If async fails, skip silently - updates not critical for operation
646
+ pass
647
+ except ImportError:
648
+ # Auto-updater not available, skip silently
649
+ pass